mirror of
https://github.com/SrIzan10/hctv.git
synced 2026-06-06 00:56:56 +00:00
feat: grant system for obs chat overlay
This commit is contained in:
@@ -5,7 +5,7 @@ import { readFile } from 'node:fs/promises';
|
||||
import { lucia } from '@hctv/auth';
|
||||
import { getCookie } from 'hono/cookie';
|
||||
import { getPersonalChannel } from './utils/personalChannel.js';
|
||||
import { getRedisConnection, prisma } from '@hctv/db';
|
||||
import { getRedisConnection, prisma, type User } from '@hctv/db';
|
||||
import uFuzzy from '@leeoniya/ufuzzy';
|
||||
|
||||
const redis = getRedisConnection();
|
||||
@@ -31,32 +31,53 @@ app.get(
|
||||
// https://hono.dev/helpers/websocket
|
||||
async onOpen(evt, ws) {
|
||||
const token = getCookie(c, 'auth_session');
|
||||
if (!token) {
|
||||
const grant = c.req.query('grant');
|
||||
console.log({
|
||||
token,
|
||||
grant,
|
||||
})
|
||||
if (!token && !grant) {
|
||||
console.log('closing a')
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
// but if there is actually a token with no grant, we let it pass through
|
||||
if (!token && grant === 'null') {
|
||||
console.log('closing b')
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const { user } = await lucia.validateSession(token);
|
||||
if (!user) {
|
||||
let user: User | null = null
|
||||
const dbGrant = await prisma.channel.findFirst({
|
||||
where: {
|
||||
obsChatGrantToken: grant,
|
||||
}
|
||||
});
|
||||
if (token) {
|
||||
user = (await lucia.validateSession(token)).user;
|
||||
const personalChannel = await getPersonalChannel(user!.id);
|
||||
if (!personalChannel) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
ws.personalChannel = personalChannel;
|
||||
}
|
||||
if (!user && !dbGrant) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const personalChannel = await getPersonalChannel(user.id);
|
||||
if (!personalChannel) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
// ignoring user here which might be undefined so
|
||||
|
||||
const { username } = c.req.param();
|
||||
ws.targetUsername = username;
|
||||
ws.user = user;
|
||||
ws.personalChannel = personalChannel;
|
||||
if (ws.raw) {
|
||||
ws.raw.targetUsername = username;
|
||||
// @ts-ignore
|
||||
ws.raw.user = user;
|
||||
ws.raw.personalChannel = personalChannel;
|
||||
ws.raw.personalChannel = ws.personalChannel;
|
||||
}
|
||||
|
||||
const redis = getRedisConnection();
|
||||
@@ -71,20 +92,24 @@ app.get(
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await prisma.streamInfo.update({
|
||||
where: {
|
||||
username,
|
||||
},
|
||||
data: {
|
||||
viewers: {
|
||||
increment: 1,
|
||||
if (token && grant === 'null') {
|
||||
await prisma.streamInfo.update({
|
||||
where: {
|
||||
username,
|
||||
},
|
||||
},
|
||||
});
|
||||
data: {
|
||||
viewers: {
|
||||
increment: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
async onClose(evt, ws) {
|
||||
// if prematurely exiting due to authentication issues
|
||||
console.log('client disconnected');
|
||||
if (!ws.targetUsername) return;
|
||||
|
||||
const streamInfo = await prisma.streamInfo.findUnique({
|
||||
where: {
|
||||
username: ws.targetUsername,
|
||||
@@ -116,6 +141,7 @@ app.get(
|
||||
return;
|
||||
}
|
||||
if (msg.type === 'message') {
|
||||
if (!ws.personalChannel) return;
|
||||
const message = (msg.message as string).trim();
|
||||
const msgObj = {
|
||||
user: {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import ChatPanel from "@/components/app/ChatPanel/ChatPanel";
|
||||
import LiveStream from "@/components/app/Livestream/Livestream";
|
||||
import { prisma } from '@hctv/db';
|
||||
|
||||
export default async function Page({ params }: { params: Promise<{ username: string }> }) {
|
||||
@@ -12,6 +11,8 @@ export default async function Page({ params }: { params: Promise<{ username: str
|
||||
return <div>Stream not found</div>;
|
||||
}
|
||||
return (
|
||||
<ChatPanel isObsPanel />
|
||||
<div className="bg-green-500 h-screen">
|
||||
<ChatPanel isObsPanel />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import './globals.css'
|
||||
import { NuqsAdapter } from 'nuqs/adapters/next/app';
|
||||
import './globals.css';
|
||||
|
||||
export default function Layout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<main>{children}</main>
|
||||
</body>
|
||||
<NuqsAdapter>
|
||||
<body>
|
||||
<main>{children}</main>
|
||||
</body>
|
||||
</NuqsAdapter>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,9 +8,11 @@ import { useParams } from 'next/navigation';
|
||||
import { Message } from './message';
|
||||
import { useMap } from '@uidotdev/usehooks';
|
||||
import { EmojiSearch } from './EmojiSearch';
|
||||
import { useQueryState } from 'nuqs';
|
||||
|
||||
export default function ChatPanel(props: Props) {
|
||||
const { username } = useParams();
|
||||
const [grant, setGrant] = useQueryState('grant');
|
||||
const [message, setMessage] = useState('');
|
||||
const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
@@ -20,12 +22,15 @@ export default function ChatPanel(props: Props) {
|
||||
const [cursorPosition, setCursorPosition] = useState(0);
|
||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(grant)
|
||||
}, [grant]);
|
||||
useEffect(() => {
|
||||
console.log('Initializing WebSocket connection for user:', username);
|
||||
const socket = new WebSocket(
|
||||
`ws${window.location.protocol === 'https:' ? 's' : ''}://${
|
||||
window.location.host
|
||||
}/api/stream/chat/ws/${username}`
|
||||
}/api/stream/chat/ws/${username}?grant=${grant}`
|
||||
);
|
||||
socketRef.current = socket;
|
||||
|
||||
@@ -91,7 +96,7 @@ export default function ChatPanel(props: Props) {
|
||||
const socket = new WebSocket(
|
||||
`ws${window.location.protocol === 'https:' ? 's' : ''}://${
|
||||
window.location.host
|
||||
}/api/stream/chat/ws/${username}`
|
||||
}/api/stream/chat/ws/${username}?grant=${grant}`
|
||||
);
|
||||
socket.onopen = () => {
|
||||
socket.send(JSON.stringify({ type: 'message', message }));
|
||||
@@ -145,7 +150,7 @@ export default function ChatPanel(props: Props) {
|
||||
const socket = new WebSocket(
|
||||
`ws${window.location.protocol === 'https:' ? 's' : ''}://${
|
||||
window.location.host
|
||||
}/api/stream/chat/ws/${username}`
|
||||
}/api/stream/chat/ws/${username}?grant=${grant}`
|
||||
);
|
||||
|
||||
socket.onopen = () => {
|
||||
@@ -244,7 +249,7 @@ export default function ChatPanel(props: Props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${props.isObsPanel ? '' : 'md:border bg-mantle'} flex flex-col w-[350px] max-w-[350px] h-full`}>
|
||||
<div className={`${props.isObsPanel ? 'w-full text-white' : 'md:border bg-mantle w-[350px] max-w-[350px]'} flex flex-col h-full`}>
|
||||
<div ref={scrollRef} className="flex-1 p-4 overflow-y-auto flex flex-col">
|
||||
<div className="space-y-4 flex-1">
|
||||
{chatMessages.map((msg, i) => (
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[obsChatGrantToken]` on the table `Channel` will be added. If there are existing duplicate values, this will fail.
|
||||
- The required column `obsChatGrantToken` was added to the `Channel` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required.
|
||||
|
||||
*/
|
||||
-- AlterTable: add column as nullable first
|
||||
ALTER TABLE "Channel" ADD COLUMN "obsChatGrantToken" TEXT;
|
||||
|
||||
-- Update: set random string for existing rows
|
||||
UPDATE "Channel"
|
||||
SET "obsChatGrantToken" = substr(md5(random()::text || clock_timestamp()::text), 1, 32)
|
||||
WHERE "obsChatGrantToken" IS NULL;
|
||||
|
||||
-- AlterTable: make column NOT NULL
|
||||
ALTER TABLE "Channel" ALTER COLUMN "obsChatGrantToken" SET NOT NULL;
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Channel_obsChatGrantToken_key" ON "Channel"("obsChatGrantToken");
|
||||
@@ -50,6 +50,7 @@ model Channel {
|
||||
streamInfo StreamInfo[]
|
||||
followers Follow[] @relation("ChannelFollowers")
|
||||
streamKey StreamKey?
|
||||
obsChatGrantToken String @unique @default(cuid())
|
||||
|
||||
@@index([ownerId])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user