mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-12 12:20:23 +01:00
Update commit checkpoints, add API route for stream viewers, and synchronize viewer count to database
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"last_commit": {
|
"last_commit": {
|
||||||
"timestamp": "2025-12-21T23:12:34.993938+00:00",
|
"timestamp": "2025-12-21T23:15:16.584895+00:00",
|
||||||
"session_id": "60ef8f57-a5e4-4edb-a61d-0a4778a6de32",
|
"session_id": "60ef8f57-a5e4-4edb-a61d-0a4778a6de32",
|
||||||
"last_entry_timestamp": "2025-12-21T23:12:32.638Z"
|
"last_entry_timestamp": "2025-12-21T23:15:15.232Z"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,7 +117,7 @@ export function useStreamViewers(username: string): UseStreamViewersResult {
|
|||||||
const perSession = presenceFeed.perSession
|
const perSession = presenceFeed.perSession
|
||||||
if (perSession) {
|
if (perSession) {
|
||||||
for (const sessionId of Object.keys(perSession)) {
|
for (const sessionId of Object.keys(perSession)) {
|
||||||
const entry = (perSession as Record<string, { value?: { lastActive: number } }>)[sessionId]
|
const entry = (presenceFeed.perSession as Record<string, { value?: { lastActive: number } }>)[sessionId]
|
||||||
if (entry?.value) {
|
if (entry?.value) {
|
||||||
const age = now - entry.value.lastActive
|
const age = now - entry.value.lastActive
|
||||||
if (age < PRESENCE_STALE_MS) {
|
if (age < PRESENCE_STALE_MS) {
|
||||||
@@ -139,6 +139,24 @@ export function useStreamViewers(username: string): UseStreamViewersResult {
|
|||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [presenceFeed?.$isLoaded, presenceFeed])
|
}, [presenceFeed?.$isLoaded, presenceFeed])
|
||||||
|
|
||||||
|
// Sync viewer count to database for external access
|
||||||
|
useEffect(() => {
|
||||||
|
if (viewerCount === 0) return
|
||||||
|
|
||||||
|
// Debounce the API call
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
fetch(`/api/streams/${username}/viewers`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ viewerCount }),
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error("Failed to sync viewer count:", err)
|
||||||
|
})
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
|
}, [viewerCount, username])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
viewerCount,
|
viewerCount,
|
||||||
isConnected: me.$isLoaded,
|
isConnected: me.$isLoaded,
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import { Route as DemoStartSsrIndexRouteImport } from './routes/demo/start.ssr.i
|
|||||||
import { Route as DemoStartSsrSpaModeRouteImport } from './routes/demo/start.ssr.spa-mode'
|
import { Route as DemoStartSsrSpaModeRouteImport } from './routes/demo/start.ssr.spa-mode'
|
||||||
import { Route as DemoStartSsrFullSsrRouteImport } from './routes/demo/start.ssr.full-ssr'
|
import { Route as DemoStartSsrFullSsrRouteImport } from './routes/demo/start.ssr.full-ssr'
|
||||||
import { Route as DemoStartSsrDataOnlyRouteImport } from './routes/demo/start.ssr.data-only'
|
import { Route as DemoStartSsrDataOnlyRouteImport } from './routes/demo/start.ssr.data-only'
|
||||||
|
import { Route as ApiStreamsUsernameViewersRouteImport } from './routes/api/streams.$username.viewers'
|
||||||
import { Route as ApiCanvasImagesImageIdRouteImport } from './routes/api/canvas.images.$imageId'
|
import { Route as ApiCanvasImagesImageIdRouteImport } from './routes/api/canvas.images.$imageId'
|
||||||
import { Route as ApiCanvasImagesImageIdGenerateRouteImport } from './routes/api/canvas.images.$imageId.generate'
|
import { Route as ApiCanvasImagesImageIdGenerateRouteImport } from './routes/api/canvas.images.$imageId.generate'
|
||||||
|
|
||||||
@@ -295,6 +296,12 @@ const DemoStartSsrDataOnlyRoute = DemoStartSsrDataOnlyRouteImport.update({
|
|||||||
path: '/demo/start/ssr/data-only',
|
path: '/demo/start/ssr/data-only',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const ApiStreamsUsernameViewersRoute =
|
||||||
|
ApiStreamsUsernameViewersRouteImport.update({
|
||||||
|
id: '/viewers',
|
||||||
|
path: '/viewers',
|
||||||
|
getParentRoute: () => ApiStreamsUsernameRoute,
|
||||||
|
} as any)
|
||||||
const ApiCanvasImagesImageIdRoute = ApiCanvasImagesImageIdRouteImport.update({
|
const ApiCanvasImagesImageIdRoute = ApiCanvasImagesImageIdRouteImport.update({
|
||||||
id: '/$imageId',
|
id: '/$imageId',
|
||||||
path: '/$imageId',
|
path: '/$imageId',
|
||||||
@@ -343,7 +350,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/api/chat/guest': typeof ApiChatGuestRoute
|
'/api/chat/guest': typeof ApiChatGuestRoute
|
||||||
'/api/chat/mutations': typeof ApiChatMutationsRoute
|
'/api/chat/mutations': typeof ApiChatMutationsRoute
|
||||||
'/api/flowglad/$': typeof ApiFlowgladSplatRoute
|
'/api/flowglad/$': typeof ApiFlowgladSplatRoute
|
||||||
'/api/streams/$username': typeof ApiStreamsUsernameRoute
|
'/api/streams/$username': typeof ApiStreamsUsernameRouteWithChildren
|
||||||
'/api/stripe/checkout': typeof ApiStripeCheckoutRoute
|
'/api/stripe/checkout': typeof ApiStripeCheckoutRoute
|
||||||
'/api/stripe/webhooks': typeof ApiStripeWebhooksRoute
|
'/api/stripe/webhooks': typeof ApiStripeWebhooksRoute
|
||||||
'/api/usage-events/create': typeof ApiUsageEventsCreateRoute
|
'/api/usage-events/create': typeof ApiUsageEventsCreateRoute
|
||||||
@@ -352,6 +359,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/demo/start/api-request': typeof DemoStartApiRequestRoute
|
'/demo/start/api-request': typeof DemoStartApiRequestRoute
|
||||||
'/demo/start/server-funcs': typeof DemoStartServerFuncsRoute
|
'/demo/start/server-funcs': typeof DemoStartServerFuncsRoute
|
||||||
'/api/canvas/images/$imageId': typeof ApiCanvasImagesImageIdRouteWithChildren
|
'/api/canvas/images/$imageId': typeof ApiCanvasImagesImageIdRouteWithChildren
|
||||||
|
'/api/streams/$username/viewers': typeof ApiStreamsUsernameViewersRoute
|
||||||
'/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute
|
'/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute
|
||||||
'/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute
|
'/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute
|
||||||
'/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute
|
'/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute
|
||||||
@@ -393,7 +401,7 @@ export interface FileRoutesByTo {
|
|||||||
'/api/chat/guest': typeof ApiChatGuestRoute
|
'/api/chat/guest': typeof ApiChatGuestRoute
|
||||||
'/api/chat/mutations': typeof ApiChatMutationsRoute
|
'/api/chat/mutations': typeof ApiChatMutationsRoute
|
||||||
'/api/flowglad/$': typeof ApiFlowgladSplatRoute
|
'/api/flowglad/$': typeof ApiFlowgladSplatRoute
|
||||||
'/api/streams/$username': typeof ApiStreamsUsernameRoute
|
'/api/streams/$username': typeof ApiStreamsUsernameRouteWithChildren
|
||||||
'/api/stripe/checkout': typeof ApiStripeCheckoutRoute
|
'/api/stripe/checkout': typeof ApiStripeCheckoutRoute
|
||||||
'/api/stripe/webhooks': typeof ApiStripeWebhooksRoute
|
'/api/stripe/webhooks': typeof ApiStripeWebhooksRoute
|
||||||
'/api/usage-events/create': typeof ApiUsageEventsCreateRoute
|
'/api/usage-events/create': typeof ApiUsageEventsCreateRoute
|
||||||
@@ -402,6 +410,7 @@ export interface FileRoutesByTo {
|
|||||||
'/demo/start/api-request': typeof DemoStartApiRequestRoute
|
'/demo/start/api-request': typeof DemoStartApiRequestRoute
|
||||||
'/demo/start/server-funcs': typeof DemoStartServerFuncsRoute
|
'/demo/start/server-funcs': typeof DemoStartServerFuncsRoute
|
||||||
'/api/canvas/images/$imageId': typeof ApiCanvasImagesImageIdRouteWithChildren
|
'/api/canvas/images/$imageId': typeof ApiCanvasImagesImageIdRouteWithChildren
|
||||||
|
'/api/streams/$username/viewers': typeof ApiStreamsUsernameViewersRoute
|
||||||
'/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute
|
'/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute
|
||||||
'/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute
|
'/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute
|
||||||
'/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute
|
'/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute
|
||||||
@@ -445,7 +454,7 @@ export interface FileRoutesById {
|
|||||||
'/api/chat/guest': typeof ApiChatGuestRoute
|
'/api/chat/guest': typeof ApiChatGuestRoute
|
||||||
'/api/chat/mutations': typeof ApiChatMutationsRoute
|
'/api/chat/mutations': typeof ApiChatMutationsRoute
|
||||||
'/api/flowglad/$': typeof ApiFlowgladSplatRoute
|
'/api/flowglad/$': typeof ApiFlowgladSplatRoute
|
||||||
'/api/streams/$username': typeof ApiStreamsUsernameRoute
|
'/api/streams/$username': typeof ApiStreamsUsernameRouteWithChildren
|
||||||
'/api/stripe/checkout': typeof ApiStripeCheckoutRoute
|
'/api/stripe/checkout': typeof ApiStripeCheckoutRoute
|
||||||
'/api/stripe/webhooks': typeof ApiStripeWebhooksRoute
|
'/api/stripe/webhooks': typeof ApiStripeWebhooksRoute
|
||||||
'/api/usage-events/create': typeof ApiUsageEventsCreateRoute
|
'/api/usage-events/create': typeof ApiUsageEventsCreateRoute
|
||||||
@@ -454,6 +463,7 @@ export interface FileRoutesById {
|
|||||||
'/demo/start/api-request': typeof DemoStartApiRequestRoute
|
'/demo/start/api-request': typeof DemoStartApiRequestRoute
|
||||||
'/demo/start/server-funcs': typeof DemoStartServerFuncsRoute
|
'/demo/start/server-funcs': typeof DemoStartServerFuncsRoute
|
||||||
'/api/canvas/images/$imageId': typeof ApiCanvasImagesImageIdRouteWithChildren
|
'/api/canvas/images/$imageId': typeof ApiCanvasImagesImageIdRouteWithChildren
|
||||||
|
'/api/streams/$username/viewers': typeof ApiStreamsUsernameViewersRoute
|
||||||
'/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute
|
'/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute
|
||||||
'/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute
|
'/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute
|
||||||
'/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute
|
'/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute
|
||||||
@@ -507,6 +517,7 @@ export interface FileRouteTypes {
|
|||||||
| '/demo/start/api-request'
|
| '/demo/start/api-request'
|
||||||
| '/demo/start/server-funcs'
|
| '/demo/start/server-funcs'
|
||||||
| '/api/canvas/images/$imageId'
|
| '/api/canvas/images/$imageId'
|
||||||
|
| '/api/streams/$username/viewers'
|
||||||
| '/demo/start/ssr/data-only'
|
| '/demo/start/ssr/data-only'
|
||||||
| '/demo/start/ssr/full-ssr'
|
| '/demo/start/ssr/full-ssr'
|
||||||
| '/demo/start/ssr/spa-mode'
|
| '/demo/start/ssr/spa-mode'
|
||||||
@@ -557,6 +568,7 @@ export interface FileRouteTypes {
|
|||||||
| '/demo/start/api-request'
|
| '/demo/start/api-request'
|
||||||
| '/demo/start/server-funcs'
|
| '/demo/start/server-funcs'
|
||||||
| '/api/canvas/images/$imageId'
|
| '/api/canvas/images/$imageId'
|
||||||
|
| '/api/streams/$username/viewers'
|
||||||
| '/demo/start/ssr/data-only'
|
| '/demo/start/ssr/data-only'
|
||||||
| '/demo/start/ssr/full-ssr'
|
| '/demo/start/ssr/full-ssr'
|
||||||
| '/demo/start/ssr/spa-mode'
|
| '/demo/start/ssr/spa-mode'
|
||||||
@@ -608,6 +620,7 @@ export interface FileRouteTypes {
|
|||||||
| '/demo/start/api-request'
|
| '/demo/start/api-request'
|
||||||
| '/demo/start/server-funcs'
|
| '/demo/start/server-funcs'
|
||||||
| '/api/canvas/images/$imageId'
|
| '/api/canvas/images/$imageId'
|
||||||
|
| '/api/streams/$username/viewers'
|
||||||
| '/demo/start/ssr/data-only'
|
| '/demo/start/ssr/data-only'
|
||||||
| '/demo/start/ssr/full-ssr'
|
| '/demo/start/ssr/full-ssr'
|
||||||
| '/demo/start/ssr/spa-mode'
|
| '/demo/start/ssr/spa-mode'
|
||||||
@@ -644,7 +657,7 @@ export interface RootRouteChildren {
|
|||||||
ApiChatGuestRoute: typeof ApiChatGuestRoute
|
ApiChatGuestRoute: typeof ApiChatGuestRoute
|
||||||
ApiChatMutationsRoute: typeof ApiChatMutationsRoute
|
ApiChatMutationsRoute: typeof ApiChatMutationsRoute
|
||||||
ApiFlowgladSplatRoute: typeof ApiFlowgladSplatRoute
|
ApiFlowgladSplatRoute: typeof ApiFlowgladSplatRoute
|
||||||
ApiStreamsUsernameRoute: typeof ApiStreamsUsernameRoute
|
ApiStreamsUsernameRoute: typeof ApiStreamsUsernameRouteWithChildren
|
||||||
ApiStripeCheckoutRoute: typeof ApiStripeCheckoutRoute
|
ApiStripeCheckoutRoute: typeof ApiStripeCheckoutRoute
|
||||||
ApiStripeWebhooksRoute: typeof ApiStripeWebhooksRoute
|
ApiStripeWebhooksRoute: typeof ApiStripeWebhooksRoute
|
||||||
DemoApiNamesRoute: typeof DemoApiNamesRoute
|
DemoApiNamesRoute: typeof DemoApiNamesRoute
|
||||||
@@ -987,6 +1000,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof DemoStartSsrDataOnlyRouteImport
|
preLoaderRoute: typeof DemoStartSsrDataOnlyRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/api/streams/$username/viewers': {
|
||||||
|
id: '/api/streams/$username/viewers'
|
||||||
|
path: '/viewers'
|
||||||
|
fullPath: '/api/streams/$username/viewers'
|
||||||
|
preLoaderRoute: typeof ApiStreamsUsernameViewersRouteImport
|
||||||
|
parentRoute: typeof ApiStreamsUsernameRoute
|
||||||
|
}
|
||||||
'/api/canvas/images/$imageId': {
|
'/api/canvas/images/$imageId': {
|
||||||
id: '/api/canvas/images/$imageId'
|
id: '/api/canvas/images/$imageId'
|
||||||
path: '/$imageId'
|
path: '/$imageId'
|
||||||
@@ -1115,6 +1135,17 @@ const ApiUsersRouteWithChildren = ApiUsersRoute._addFileChildren(
|
|||||||
ApiUsersRouteChildren,
|
ApiUsersRouteChildren,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
interface ApiStreamsUsernameRouteChildren {
|
||||||
|
ApiStreamsUsernameViewersRoute: typeof ApiStreamsUsernameViewersRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const ApiStreamsUsernameRouteChildren: ApiStreamsUsernameRouteChildren = {
|
||||||
|
ApiStreamsUsernameViewersRoute: ApiStreamsUsernameViewersRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ApiStreamsUsernameRouteWithChildren =
|
||||||
|
ApiStreamsUsernameRoute._addFileChildren(ApiStreamsUsernameRouteChildren)
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
UsernameRoute: UsernameRoute,
|
UsernameRoute: UsernameRoute,
|
||||||
@@ -1144,7 +1175,7 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
ApiChatGuestRoute: ApiChatGuestRoute,
|
ApiChatGuestRoute: ApiChatGuestRoute,
|
||||||
ApiChatMutationsRoute: ApiChatMutationsRoute,
|
ApiChatMutationsRoute: ApiChatMutationsRoute,
|
||||||
ApiFlowgladSplatRoute: ApiFlowgladSplatRoute,
|
ApiFlowgladSplatRoute: ApiFlowgladSplatRoute,
|
||||||
ApiStreamsUsernameRoute: ApiStreamsUsernameRoute,
|
ApiStreamsUsernameRoute: ApiStreamsUsernameRouteWithChildren,
|
||||||
ApiStripeCheckoutRoute: ApiStripeCheckoutRoute,
|
ApiStripeCheckoutRoute: ApiStripeCheckoutRoute,
|
||||||
ApiStripeWebhooksRoute: ApiStripeWebhooksRoute,
|
ApiStripeWebhooksRoute: ApiStripeWebhooksRoute,
|
||||||
DemoApiNamesRoute: DemoApiNamesRoute,
|
DemoApiNamesRoute: DemoApiNamesRoute,
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { eq } from "drizzle-orm"
|
||||||
|
import { getDb } from "@/db/connection"
|
||||||
|
import { streams, users } from "@/db/schema"
|
||||||
|
|
||||||
|
const resolveDatabaseUrl = () => {
|
||||||
|
try {
|
||||||
|
const { getServerContext } = require("@tanstack/react-start/server") as {
|
||||||
|
getServerContext: () => {
|
||||||
|
cloudflare?: { env?: Record<string, string> }
|
||||||
|
} | null
|
||||||
|
}
|
||||||
|
const ctx = getServerContext()
|
||||||
|
const url = ctx?.cloudflare?.env?.DATABASE_URL
|
||||||
|
if (url) return url
|
||||||
|
} catch {
|
||||||
|
// Not in server context
|
||||||
|
}
|
||||||
|
if (process.env.DATABASE_URL) return process.env.DATABASE_URL
|
||||||
|
throw new Error("DATABASE_URL not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET - fetch current viewer count from database
|
||||||
|
const getViewerCount = async ({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
request: Request
|
||||||
|
params: { username: string }
|
||||||
|
}) => {
|
||||||
|
const { username } = params
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = getDb(resolveDatabaseUrl())
|
||||||
|
|
||||||
|
const user = await db.query.users.findFirst({
|
||||||
|
where: eq(users.username, username),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return Response.json({ viewerCount: 0, username })
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = await db.query.streams.findFirst({
|
||||||
|
where: eq(streams.user_id, user.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
viewerCount: stream?.viewer_count ?? 0,
|
||||||
|
username,
|
||||||
|
isLive: stream?.is_live ?? false,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to get viewer count:", error)
|
||||||
|
return Response.json({ viewerCount: 0, username })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST - update viewer count (called by Jazz client)
|
||||||
|
const updateViewerCount = async ({
|
||||||
|
request,
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
request: Request
|
||||||
|
params: { username: string }
|
||||||
|
}) => {
|
||||||
|
const { username } = params
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const { viewerCount } = body as { viewerCount: number }
|
||||||
|
|
||||||
|
if (typeof viewerCount !== "number" || viewerCount < 0) {
|
||||||
|
return Response.json({ error: "Invalid viewer count" }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = getDb(resolveDatabaseUrl())
|
||||||
|
|
||||||
|
const user = await db.query.users.findFirst({
|
||||||
|
where: eq(users.username, username),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return Response.json({ error: "User not found" }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(streams)
|
||||||
|
.set({ viewer_count: viewerCount })
|
||||||
|
.where(eq(streams.user_id, user.id))
|
||||||
|
|
||||||
|
return Response.json({ success: true, viewerCount, username })
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update viewer count:", error)
|
||||||
|
return Response.json({ error: "Failed to update" }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/api/streams/$username/viewers")({
|
||||||
|
server: {
|
||||||
|
handlers: {
|
||||||
|
GET: getViewerCount,
|
||||||
|
POST: updateViewerCount,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user