Compare commits

...

17 Commits

46 changed files with 1489 additions and 290 deletions

View File

@@ -47,7 +47,7 @@ jobs:
mkdir -p apps/web/src/lib/instrumentation/
export SLACK_TOKEN=${{ secrets.SLACK_TOKEN }}
./slack-import-emojis-bin
./slack-import-emojis-bin default
cp emojis.json apps/web/

4
.gitignore vendored
View File

@@ -45,4 +45,6 @@ packages/db/generated/client
*dist
slack-import-emojis/target
**/*/emojis.json
**/*/emojis.json
.idea

View File

@@ -1,8 +0,0 @@
```
npm install
npm run dev
```
```
open http://localhost:3000
```

View File

@@ -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, type User } from '@hctv/db';
import { getRedisConnection, prisma, type BotAccount, type BotApiKey, type User } from '@hctv/db';
import uFuzzy from '@leeoniya/ufuzzy';
import { randomString } from './utils/randomString.js';
@@ -29,71 +29,92 @@ app.get('/up', async (c) => {
app.get(
'/ws/:username',
upgradeWebSocket((c) => ({
// https://hono.dev/helpers/websocket
async onOpen(evt, ws) {
const token = getCookie(c, 'auth_session');
const grant = c.req.query('grant');
console.log({
token,
grant,
})
const authHeader = c.req.header('Authorization');
// random checks that actually make sense if you read trust me bro
if (!token && !grant) {
ws.close();
return;
}
if (!token && grant === 'null') {
if (!token && (!grant || grant === 'null') && !authHeader) {
ws.close();
return;
}
let user: User | null = null
let chatUser: ChatUser | null = null;
let personalChannel: any = null;
if (authHeader && authHeader.startsWith('Bearer ')) {
const apiKey = authHeader.substring(7);
const botAccount = await prisma.botApiKey.findUnique({
where: { key: apiKey },
include: { botAccount: true }
});
if (botAccount) {
chatUser = {
id: botAccount.botAccount.id,
username: botAccount.botAccount.slug,
pfpUrl: botAccount.botAccount.pfpUrl,
displayName: botAccount.botAccount.displayName,
isBot: true
};
personalChannel = {
id: botAccount.botAccount.id,
name: botAccount.botAccount.slug
};
}
}
if (!chatUser && token) {
const session = await lucia.validateSession(token);
if (session.user) {
const userChannel = await getPersonalChannel(session.user.id);
if (userChannel) {
chatUser = {
id: session.user.id,
username: userChannel.name,
pfpUrl: session.user.pfpUrl,
isBot: false
};
personalChannel = userChannel;
}
}
}
const dbGrant = await prisma.channel.findFirst({
where: {
obsChatGrantToken: grant,
}
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) {
if (!chatUser && !dbGrant) {
ws.close();
return;
}
const { username } = c.req.param();
if (dbGrant && dbGrant?.name !== username) {
if (dbGrant && dbGrant.name !== username) {
ws.close();
return;
}
ws.targetUsername = username;
ws.user = user;
ws.chatUser = chatUser;
ws.personalChannel = personalChannel;
ws.viewerId = randomString(10);
if (ws.raw) {
ws.raw.targetUsername = username;
// @ts-ignore
ws.raw.user = user;
ws.raw.personalChannel = ws.personalChannel;
ws.raw.chatUser = chatUser;
ws.raw.personalChannel = personalChannel;
}
const redis = getRedisConnection();
const channelKey = `chat:history:${username}`;
const messages = await redis.zrange(channelKey, 0, MESSAGE_HISTORY_SIZE - 1);
if (messages.length > 0) {
ws.send(
JSON.stringify({
type: 'history',
messages: messages.map((msg) => JSON.parse(msg)),
})
);
ws.send(JSON.stringify({
type: 'history',
messages: messages.map((msg) => JSON.parse(msg)),
}));
}
},
async onClose(evt, ws) {
@@ -116,33 +137,34 @@ app.get(
},
async onMessage(evt, ws) {
const msg = JSON.parse(evt.data.toString());
if (msg.type === 'ping') {
await redis.setex(`viewer:${ws.targetUsername}:${ws.viewerId}`, 30, '1');
ws.send(
JSON.stringify({
type: 'pong',
})
);
ws.send(JSON.stringify({ type: 'pong' }));
return;
}
if (msg.type === 'message') {
if (!ws.personalChannel) return;
if (!ws.chatUser || !ws.personalChannel) return;
const message = (msg.message as string).trim();
const msgObj = {
user: {
id: ws.user.id,
username: ws.personalChannel.name,
pfpUrl: ws.user.pfpUrl,
id: ws.chatUser.id,
username: ws.chatUser.username,
pfpUrl: ws.chatUser.pfpUrl,
displayName: ws.chatUser.displayName,
isBot: ws.chatUser.isBot || false
},
message,
};
// Save to Redis without the type field to maintain compatibility
const redisObj = {
user: msgObj.user,
message: msgObj.message,
type: 'message',
};
const redisStr = JSON.stringify(redisObj);
const msgStr = JSON.stringify(msgObj);
@@ -238,3 +260,11 @@ const server = serve(
}
);
injectWebSocket(server);
interface ChatUser {
id: string;
username: string;
pfpUrl: string;
displayName?: string;
isBot: boolean;
}

View File

@@ -1,49 +0,0 @@
# Starlight Starter Kit: Basics
[![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build)
```
yarn create astro@latest -- --template starlight
```
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
## 🚀 Project Structure
Inside of your Astro + Starlight project, you'll see the following folders and files:
```
.
├── public/
├── src/
│ ├── assets/
│ ├── content/
│ │ └── docs/
│ └── content.config.ts
├── astro.config.mjs
├── package.json
└── tsconfig.json
```
Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.
Images can be added to `src/assets/` and embedded in Markdown with a relative link.
Static assets, like favicons, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `yarn install` | Installs dependencies |
| `yarn dev` | Starts local dev server at `localhost:4321` |
| `yarn build` | Build your production site to `./dist/` |
| `yarn preview` | Preview your build locally, before deploying |
| `yarn astro ...` | Run CLI commands like `astro add`, `astro check` |
| `yarn astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Check out [Starlights docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).

View File

@@ -3,12 +3,19 @@ title: Chat
description: Chat websocket
---
import { Aside } from '@astrojs/starlight/components';
The chat system is powered by a websocket server. Please read the entire page before implementing anything, as there are some important notes.
## Connection and messages
The websocket server is located at `wss://hctv.srizan.dev/api/chat/ws/:username`, where `:username` is the channel you want to connect to.
You'll need to provide authentication, which can be done by providing an `auth_session` cookie, just like the REST API.
The websocket server is located at `wss://hctv.srizan.dev/api/chat/ws/:username`, where `:username` is the channel you want to connect to.
You'll need to provide authentication, which can be done by providing an `auth_session` cookie, just like the REST API.
<Aside type="tip">
Bot accounts are now supported. You can choose to connect as a bot by providing a bot account's API key on the Authentication header: `Bearer hctvb_xxxxxxx`
It is highly advised to use a bot account for any automated task, and to implement anything pointed out in this page.
</Aside>
Once connected, you must implement a subroutine in your code to send ping messages every 5 seconds. This is because of Cloudflare limitations.

View File

@@ -20,7 +20,7 @@ RUN turbo prune @hctv/web --docker
# Add lockfile and package.json's of isolated subworkspace
FROM base AS installer
RUN apk update
RUN apk add --no-cache libc6-compat git
RUN apk add --no-cache libc6-compat git vips vips-dev python3 make g++
# Get the commit hash from the builder stage
COPY --from=builder /tmp/commit_hash /tmp/commit_hash
# Read commit hash and set as build arg
@@ -33,6 +33,8 @@ WORKDIR /app
COPY --from=builder /app/out/json/ .
RUN yarn install --frozen-lockfile
RUN cd apps/web && yarn add sharp --platform=linuxmusl --arch=x64
COPY --from=builder /app/out/full/ .
RUN --mount=type=secret,id=TURBO_TOKEN --mount=type=secret,id=TURBO_TEAM \
. /tmp/build_env && \
@@ -42,7 +44,7 @@ RUN --mount=type=secret,id=TURBO_TOKEN --mount=type=secret,id=TURBO_TEAM \
FROM base AS runner
WORKDIR /app
RUN apk add --no-cache ffmpeg
RUN apk add --no-cache ffmpeg vips vips-dev
# Don't run production as root
RUN addgroup --system --gid 1001 nodejs

View File

@@ -33,6 +33,9 @@ const nextConfig = {
hostname: 'cdn.jsdelivr.net',
pathname: '/npm/emoji-datasource-twitter@15.1.2/img/twitter/64/*',
},
{
hostname: 'eoceqrx2r7.ufs.sh'
},
],
minimumCacheTTL: 120,
},
@@ -44,6 +47,7 @@ const nextConfig = {
reactStrictMode: false,
output: 'standalone',
outputFileTracingRoot: path.join(__dirname, '../../'),
serverExternalPackages: ['bullmq'],
async rewrites() {
return [
{

View File

@@ -20,6 +20,7 @@
"@hookform/resolvers": "^3.9.1",
"@lucia-auth/adapter-prisma": "^4.0.1",
"@node-rs/argon2": "^2.0.2",
"@omit/react-confirm-dialog": "^1.2.0",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-dialog": "^1.1.5",
@@ -46,11 +47,10 @@
"clsx": "^2.1.0",
"cmdk": "1.0.0",
"hls-video-element": "^1.5.0",
"ioredis": "5.7.0",
"lucia": "^3.2.2",
"lucide-react": "^0.473.0",
"media-chrome": "^4.8.0",
"next": "^15.3.4",
"next": "^15.6.0-canary.34",
"next-themes": "^0.4.4",
"node-cron": "^3.0.3",
"nuqs": "^2.4.3",

View File

@@ -16,4 +16,5 @@ Sentry.init({
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
enabled: process.env.NODE_ENV === 'production',
});

View File

@@ -1,18 +0,0 @@
// This file configures the initialization of Sentry on the server.
// The config you add here will be used whenever the server handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: "https://f3c26671c39af48406c6e23702a4f3dd@o4506961023860736.ingest.us.sentry.io/4509895816773632",
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: 1,
// Enable logs to be sent to Sentry
enableLogs: true,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});

View File

@@ -0,0 +1,124 @@
import { validateRequest } from "@/lib/auth/validate";
import { prisma } from "@hctv/db";
import { NextRequest } from "next/server";
import { z } from "zod";
type Params = Promise<{ slug: string }>;
export async function POST(request: NextRequest, segmentData: { params: Params }) {
const { slug } = await segmentData.params;
const { user } = await validateRequest();
if (!user) {
return new Response(JSON.stringify({ success: false, error: 'Unauthorized' }), { status: 401 });
}
const bodySchema = z.object({
action: z.enum(['revoke', 'regenerate', 'create']),
name: z.string().min(3, 'Name must be at least 3 characters long').max(50, 'Name must be at most 50 characters long'),
});
const body = await request.json();
const parsedBody = bodySchema.safeParse(body);
if (!parsedBody.success) {
return new Response(JSON.stringify({ success: false, error: parsedBody.error.errors.map(e => e.message).join(', ') }), { status: 400 });
}
const { action, name } = parsedBody.data;
if (action === 'create') {
const exists = await prisma.botApiKey.findFirst({
where: {
name,
botAccount: {
ownerId: user.id,
slug,
}
}
});
if (exists) {
return new Response(JSON.stringify({ success: false, error: 'API Key with this name already exists' }), { status: 400 });
}
const newKey = await prisma.botApiKey.create({
data: {
name,
botAccount: {
connect: {
ownerId: user.id,
slug,
}
},
key: generateApiKey(),
}
});
return new Response(JSON.stringify({ success: true, apiKey: newKey.key, id: newKey.id }));
}
if (action === 'regenerate') {
const existingKey = await prisma.botApiKey.findFirst({
where: {
name,
botAccount: {
ownerId: user.id,
slug,
}
}
});
if (!existingKey) {
return new Response(JSON.stringify({ success: false, error: 'API Key not found' }), { status: 404 });
}
const newKey = generateApiKey();
await prisma.botApiKey.update({
where: { id: existingKey.id },
data: { key: newKey },
});
return new Response(JSON.stringify({ success: true, apiKey: newKey, id: existingKey.id }));
}
if (action === 'revoke') {
const existingKey = await prisma.botApiKey.findFirst({
where: {
name,
botAccount: {
ownerId: user.id,
slug,
}
}
});
if (!existingKey) {
return new Response(JSON.stringify({ success: false, error: 'API Key not found' }), { status: 404 });
}
await prisma.botApiKey.delete({
where: { id: existingKey.id },
});
return new Response(JSON.stringify({ success: true }));
}
return new Response(JSON.stringify({ success: false, error: 'Invalid action' }), { status: 400 });
}
export async function GET(request: NextRequest, segmentData: { params: Params }) {
const { slug } = await segmentData.params;
const { user } = await validateRequest();
if (!user) {
return new Response(JSON.stringify({ success: false, error: 'Unauthorized' }), { status: 401 });
}
const apiKeys = await prisma.botApiKey.findMany({
where: {
botAccount: {
ownerId: user.id,
slug,
}
},
select: {
id: true,
name: true,
createdAt: true,
},
orderBy: {
createdAt: 'desc',
}
});
return new Response(JSON.stringify({ success: true, apiKeys }));
}
function generateApiKey() {
const uuid = crypto.randomUUID().replace(/-/g, '');
return `hctvb_${uuid}`;
}

View File

@@ -0,0 +1,221 @@
'use client';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Skeleton } from '@/components/ui/skeleton';
import { fetcher } from '@/lib/services/swr';
import { useConfirm } from '@omit/react-confirm-dialog';
import { Plus, RefreshCcw, Trash } from 'lucide-react';
import { useState } from 'react';
import { toast } from 'sonner';
import useSWR from 'swr';
import useSWRMutation from 'swr/mutation';
export function ApiKeys({ slug }: { slug: string }) {
const confirm = useConfirm();
const [newApiKeyName, setNewApiKeyName] = useState('');
const { data, error, isLoading, mutate } = useSWR<GetResponse>(
`/api/settings/bot/${slug}/apiKey`,
fetcher
);
const { trigger } = useSWRMutation(`/api/settings/bot/${slug}/apiKey`, createApiKey);
const apiKeyCreate = () => {
if (newApiKeyName.trim().length < 3) {
toast.error('API Key name must be at least 3 characters long');
return;
}
if (newApiKeyName.trim().length > 50) {
toast.error('API Key name must be at most 50 characters long');
return;
}
trigger({ action: 'create', name: newApiKeyName }).then(
async (res: PostResponse) => {
if (res.success) {
setNewApiKeyName('');
await navigator.clipboard
.writeText(res.apiKey || '')
.then(() => toast.success('API key copied to clipboard'))
.catch(() => {
alert('Failed to copy API key to clipboard, here it is: ' + res.apiKey);
});
await mutate();
} else {
toast.error(res.error || 'Error creating API key');
}
}
);
};
return (
<Card>
<CardHeader>
<CardTitle>API Keys</CardTitle>
<CardDescription>Manage your API keys</CardDescription>
</CardHeader>
<CardContent>
{isLoading && <ApiKeysSkeleton />}
{error && <p>Error loading API keys</p>}
{data && !data.success && <p>Error: Could not fetch API keys</p>}
{data && (
<div className="flex">
<Input
placeholder="New API Key Name"
className="flex-1 mr-2"
value={newApiKeyName}
onChange={(e) => setNewApiKeyName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
apiKeyCreate();
}
}}
/>
<Button
size="icon"
onClick={apiKeyCreate}
>
<Plus />
</Button>
</div>
)}
{data && data.success && data.apiKeys.length === 0 && <p>No API keys found</p>}
{data && data.success && data.apiKeys.length > 0 && (
<ul className="space-y-2 pt-4">
{data.apiKeys.map((key) => (
<li
key={key.id}
className="flex items-center justify-between p-3 bg-mantle/50 rounded-md"
>
<div className="flex-1">
<strong className="text-sm font-medium">{key.name}</strong>
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={async () => {
const confirmation = await confirm({
title: 'Regenerate API Key',
description:
'Are you sure you want to regenerate this API key? The old key will stop working.',
confirmText: 'Regenerate',
cancelText: 'Cancel',
});
if (!confirmation) return;
trigger({ action: 'regenerate', name: key.name }).then(
async (res: PostResponse) => {
if (res.success) {
await navigator.clipboard
.writeText(res.apiKey || '')
.then(() => {
toast.success('API key copied to clipboard');
})
.catch(() => {
alert(
'Failed to copy API key to clipboard, here it is: ' + res.apiKey
);
});
mutate();
} else {
toast.error(res.error || 'Error regenerating API key');
}
}
);
}}
>
<RefreshCcw className="h-4 w-4" />
</Button>
<Button
variant="destructive"
size="sm"
onClick={async () => {
const confirmation = await confirm({
title: 'Revoke API Key',
description:
'Are you sure you want to revoke this API key? This action cannot be undone.',
confirmText: 'Revoke',
cancelText: 'Cancel',
});
if (!confirmation) return;
trigger({ action: 'revoke', name: key.name }).then(
async (res: PostResponse) => {
if (res.success) {
await mutate();
} else {
toast.error(res.error || 'Error revoking API key');
}
}
);
}}
>
<Trash className="h-4 w-4" />
</Button>
</div>
</li>
))}
</ul>
)}
</CardContent>
</Card>
);
}
function ApiKeysSkeleton() {
return (
<>
<div className='flex'>
<Skeleton className='h-10 flex-1 mr-2' />
<Skeleton className='h-10 w-10' />
</div>
<div className='space-y-2 pt-4'>
{[1, 2, 3].map((i) => (
<div key={i} className='flex items-center justify-between p-3 bg-mantle/50 rounded-md'>
<div className='flex-1'>
<Skeleton className='h-4 w-1/2' />
</div>
<div className='flex items-center space-x-2'>
<Skeleton className='h-8 w-8' />
<Skeleton className='h-8 w-8' />
</div>
</div>
))}
</div>
</>
)
}
async function createApiKey(url: string, { arg }: { arg: PostRequest }) {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(arg),
});
return res.json();
}
interface GetResponse {
success: boolean;
apiKeys: Array<{ id: string; name: string; createdAt: string }>;
}
interface PostRequest {
action: 'revoke' | 'regenerate' | 'create';
name: string;
}
interface PostResponse {
success: boolean;
apiKey?: string;
id?: string;
error?: string;
}

View File

@@ -0,0 +1,62 @@
'use client';
import { UniversalForm } from '@/components/app/UniversalForm/UniversalForm';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { editBot } from '@/lib/form/actions';
import { BotAccount } from '@hctv/db';
import { useRouter } from 'next/navigation';
export function GeneralSettings(props: BotAccount) {
const router = useRouter();
return (
<Card>
<CardHeader>
<CardTitle>General Settings</CardTitle>
<CardDescription>Edit your bot settings!</CardDescription>
</CardHeader>
<CardContent>
<UniversalForm
fields={[
{
name: 'from',
type: 'hidden',
value: props.id,
required: true,
},
{
name: 'name',
type: 'text',
label: 'Bot Name',
placeholder: 'Enter bot name',
required: true,
value: props.displayName,
},
{
name: 'slug',
type: 'text',
label: 'Bot Slug',
placeholder: 'Enter bot slug',
required: true,
value: props.slug
},
{
name: 'description',
type: 'textarea',
label: 'Description',
placeholder: 'Enter bot description',
value: props.description,
textArea: true,
},
]}
schemaName={'editBot'}
action={editBot}
/>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,45 @@
import { getBotBySlug } from '@/lib/db/resolve';
import { validateRequest } from '@/lib/auth/validate';
import { redirect } from 'next/navigation';
import Image from 'next/image';
import { GeneralSettings } from '@/app/(ui)/(protected)/settings/bot/[slug]/gensettings';
import { ApiKeys } from '@/app/(ui)/(protected)/settings/bot/[slug]/apikeys';
import Link from 'next/link';
import { ArrowLeft } from 'lucide-react';
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { user } = await validateRequest();
const { slug } = await params;
const bot = await getBotBySlug(slug);
if (!bot || bot.ownerId !== user?.id) {
redirect('/settings/bot');
}
return (
<div className={'container mx-auto py-6 space-y-6'}>
<Link href={'/settings/bot'} className="text-sm text-muted-foreground hover:underline flex items-center gap-2">
<ArrowLeft className='size-4' /> Back to Bot Accounts
</Link>
<div className="flex items-center justify-between">
<div className={'flex items-center space-x-4'}>
<Image
src={bot.pfpUrl}
alt={'Bot Avatar'}
width={48}
height={48}
className="rounded-full"
/>
<div className={'flex flex-col'}>
<h1 className="text-3xl font-bold tracking-tight">{bot.displayName}</h1>
<p className="text-muted-foreground">Manage your bot account settings</p>
</div>
</div>
</div>
<div className="flex w-full gap-4 flex-col md:flex-row *:w-1/2">
<GeneralSettings {...bot} />
<ApiKeys slug={slug} />
</div>
</div>
);
}

View File

@@ -0,0 +1,65 @@
'use client';
import { UniversalForm } from '@/components/app/UniversalForm/UniversalForm';
import { createBot } from '@/lib/form/actions';
import { Bot } from 'lucide-react';
import { useRouter } from 'next/navigation';
export default function Page() {
const router = useRouter();
return (
<div className="min-h-screen bg-background">
<div className="flex h-full w-full flex-col items-center justify-center px-4 py-12">
<div className="mb-8 text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-primary shadow-lg">
<Bot className="h-8 w-8 text-primary-foreground" />
</div>
<h1 className="mb-2 text-3xl font-bold text-foreground">Create Bot Account</h1>
<p className="text-muted-foreground max-w-xl">
Create an automated bot account to provide custom functionality for your community.
</p>
</div>
<div className="w-full max-w-md bg-card rounded-xl p-8 border border-border">
<UniversalForm
fields={[
{
name: 'name',
type: 'text',
label: 'Bot Name',
placeholder: 'Enter bot name',
required: true,
},
{
name: 'slug',
type: 'text',
label: 'Bot Slug',
placeholder: 'Enter bot slug',
required: true,
},
{
name: 'description',
type: 'textarea',
label: 'Description',
placeholder: 'Enter bot description',
},
]}
schemaName={'createBot'}
action={createBot}
onActionComplete={(res) => {
router.push(`/settings/bot/${res.slug}`);
}}
/>
</div>
{/*
<p className="mt-6 text-sm text-muted-foreground text-center max-w-md">
Your bot will be created with chat permissions. You can configure advanced settings and
permissions after creation.
</p>
*/}
</div>
</div>
);
}

View File

@@ -0,0 +1,88 @@
import { validateRequest } from '@/lib/auth/validate';
import { prisma } from '@hctv/db';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import Link from 'next/link';
import { Plus, Bot, Calendar, Hash } from 'lucide-react';
import { redirect } from 'next/navigation';
import Image from 'next/image';
export default async function Page() {
const { user } = await validateRequest();
if (!user) {
redirect('/');
}
const bots = await prisma.user.findFirst({
where: { id: user.id },
select: {
botAccounts: true,
},
});
return (
<div className="container mx-auto py-6 space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Bot Accounts</h1>
<p className="text-muted-foreground">Manage your automated bot accounts</p>
</div>
<Link href="/settings/bot/create">
<Button>
<Plus className="mr-2 h-4 w-4" />
Create Bot
</Button>
</Link>
</div>
<Separator />
{bots?.botAccounts.length ? (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{bots.botAccounts.map((bot) => (
<Link href={`/settings/bot/${bot.slug}`} key={bot.id}>
<Card key={bot.id} className="hover:shadow-md transition-shadow">
<CardHeader className="pb-3">
<div className="flex items-center space-x-2">
<Image src={bot.pfpUrl} alt={'Bot Avatar'} width={32} height={32} className="rounded-full" />
<CardTitle className="text-lg">{bot.displayName}</CardTitle>
</div>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
<Hash className="h-4 w-4" />
<span>{bot.slug}</span>
</div>
<div className="flex items-center space-x-2 text-sm text-muted-foreground">
<Calendar className="h-4 w-4" />
<span>{new Date(bot.createdAt).toLocaleDateString()}</span>
</div>
</CardContent>
</Card>
</Link>
))}
</div>
) : (
<Card className="text-center py-8">
<CardContent className="space-y-4">
<Bot className="mx-auto h-12 w-12 text-muted-foreground" />
<div>
<CardTitle>No bot accounts yet</CardTitle>
<CardDescription className="mt-2">
Get started by creating your first bot account
</CardDescription>
</div>
<Link href="/settings/bot/create">
<Button>
<Plus className="mr-2 h-4 w-4" />
Create Your First Bot
</Button>
</Link>
</CardContent>
</Card>
)}
</div>
);
}

View File

@@ -150,7 +150,7 @@ export default function ChannelSettingsClient({
value={channel.name}
onSelect={(value) => {
if (value === 'create') {
router.push(`/create`);
router.push(`/settings/channel/create`);
} else {
router.push(`/settings/channel/${value}?tab=${selTab}`);
}

View File

@@ -32,8 +32,7 @@ function CreateChannelPage() {
schemaName="createChannel"
action={createChannel}
onActionComplete={(r) => {
// @ts-expect-error
const channelName = r?.channel;
const channelName = r.channel;
if (channelName) {
router.push(`/${channelName}`);
}

View File

@@ -16,6 +16,7 @@ import { extractRouterConfig } from 'uploadthing/server';
import { ourFileRouter } from '@/lib/services/uploadthing/fileRouter';
import { NuqsAdapter } from 'nuqs/adapters/next/app'
import SonnerNewVersion from '@/components/app/SonnerNewVersion/SonnerNewVersion';
import ConfirmDialogProvider from '@/lib/providers/ConfirmProvider';
const inter = Inter({ subsets: ['latin'] });
@@ -44,20 +45,22 @@ export default async function RootLayout({
<NextSSRPlugin
routerConfig={extractRouterConfig(ourFileRouter)}
/>
<NuqsAdapter>
<SidebarProvider>
<StreamInfoProvider>
{/* this promise is ugly but i'm lazy to fix the type errors */}
<Navbar editLivestream={Promise.resolve(<EditLivestream />)} />
<div className="flex flex-1 pt-16">
{/* pt-16 for navbar height */}
<Sidebar className="pt-16" />
<main className="flex-1 overflow-auto">{children}</main>
</div>
<Toaster />
</StreamInfoProvider>
</SidebarProvider>
</NuqsAdapter>
<ConfirmDialogProvider>
<NuqsAdapter>
<SidebarProvider>
<StreamInfoProvider>
{/* this promise is ugly but i'm lazy to fix the type errors */}
<Navbar editLivestream={Promise.resolve(<EditLivestream />)} />
<div className="flex flex-1 pt-16">
{/* pt-16 for navbar height */}
<Sidebar className="pt-16" />
<main className="flex-1 overflow-auto">{children}</main>
</div>
<Toaster />
</StreamInfoProvider>
</SidebarProvider>
</NuqsAdapter>
</ConfirmDialogProvider>
</ThemeProvider>
</SessionProvider>
</body>

View File

@@ -119,6 +119,7 @@
body {
@apply bg-background text-foreground;
}
.scrollbar-hide::-webkit-scrollbar { display: none; }
.scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }
}

View File

@@ -313,6 +313,8 @@ export interface User {
id: string;
username: string;
pfpUrl: string;
isBot: boolean;
displayName?: string;
}
interface Props {

View File

@@ -1,11 +1,8 @@
import { User } from './ChatPanel';
import React from 'react';
import Image from 'next/image';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Bot } from 'lucide-react';
export function Message({ user, message, type, emojiMap }: MessageProps) {
if (type === 'systemMsg') {
@@ -18,12 +15,18 @@ export function Message({ user, message, type, emojiMap }: MessageProps) {
return (
<div className="flex">
<div
lang="en"
className="max-w-full break-all whitespace-pre-wrap hyphens-auto"
>
<p>
<span className="font-bold mr-2">{user?.username}</span>
<div lang="en" className="max-w-full break-all whitespace-pre-wrap hyphens-auto">
<p className="flex flex-wrap items-center">
<span className="font-bold mr-2 flex items-center">
{user?.isBot && (
<span className="text-xs text-muted-foreground flex mr-1">
{' '}
<Bot className="size-5" />
</span>
)}
{user?.displayName || user?.username}
</span>
<EmojiRenderer text={message} emojiMap={emojiMap} />
</p>
</div>
@@ -47,13 +50,21 @@ export function EmojiRenderer({ text, emojiMap }: EmojiRendererProps) {
return (
<Tooltip key={index} delayDuration={250}>
<TooltipTrigger>
<span key={index} className="inline-block align-middle" style={{ height: '1.2em' }}>
<Image src={emojiUrl} alt={part} width={20} height={20} className="inline-block" />
<span
key={index}
className="inline-block align-middle"
style={{ height: '1.2em' }}
>
<Image
src={emojiUrl}
alt={part}
width={20}
height={20}
className="inline-block"
/>
</span>
</TooltipTrigger>
<TooltipContent>
{part}
</TooltipContent>
<TooltipContent>{part}</TooltipContent>
</Tooltip>
);
}

View File

@@ -51,9 +51,22 @@ export default function Navbar(props: Props) {
<Link href={`/settings/follows`}>
<DropdownMenuItem className="cursor-pointer">Follows</DropdownMenuItem>
</Link>
<Link href={`/create`}>
<Link href={`/settings/channel/create`}>
<DropdownMenuItem className="cursor-pointer">Create channel</DropdownMenuItem>
</Link>
<Link href={`/settings/bot`}>
<DropdownMenuItem className="cursor-pointer">Bot accounts</DropdownMenuItem>
</Link>
<DropdownMenuSeparator />
<Link href={'https://docs.hctv.srizan.dev'} target="_blank" rel="noreferrer">
<DropdownMenuItem className="cursor-pointer">API Docs</DropdownMenuItem>
</Link>
<Link href={'https://github.com/SrIzan10/hctv'} target="_blank" rel="noreferrer">
<DropdownMenuItem className="cursor-pointer">Github</DropdownMenuItem>
</Link>
<Link href={'https://github.com/sponsors/SrIzan10'} target="_blank" rel="noreferrer">
<DropdownMenuItem className="cursor-pointer">Sponsor</DropdownMenuItem>
</Link>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>

View File

@@ -19,13 +19,18 @@ import React from 'react';
import { toast } from 'sonner';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import { createChannelSchema, onboardSchema, streamInfoEditSchema, updateChannelSettingsSchema } from '@/lib/form/zod';
import {
createBotSchema,
createChannelSchema, editBotSchema, onboardSchema, streamInfoEditSchema, updateChannelSettingsSchema
} from '@/lib/form/zod';
export const schemaDb = [
{ name: 'streamInfoEdit', zod: streamInfoEditSchema },
{ name: 'onboard', zod: onboardSchema },
{ name: 'createChannel', zod: createChannelSchema },
{ name: 'updateChannelSettings', zod: updateChannelSettingsSchema },
{ name: 'createBot', zod: createBotSchema },
{ name: 'editBot', zod: editBotSchema }
] as const;
export function UniversalForm<T extends z.ZodType>({

View File

@@ -14,13 +14,14 @@ export type FormFieldConfig = {
textAreaRows?: number;
component?: (props: { field: ControllerRenderProps<any, any> } & any) => React.ReactNode;
componentProps?: Record<string, any>;
required?: boolean;
};
export type UniversalFormProps<T extends z.ZodType> = {
fields: FormFieldConfig[];
schemaName: (typeof schemaDb)[number]['name'];
action: (prev: any, formData: FormData) => void;
onActionComplete?: (result: unknown) => void;
onActionComplete?: (result: any) => void;
defaultValues?: Partial<z.infer<T>>;
submitText?: string;
submitClassname?: string;

View File

@@ -14,6 +14,7 @@ Sentry.init({
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
enabled: process.env.NODE_ENV === 'production'
});
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;

View File

@@ -1,3 +1,7 @@
import * as Sentry from "@sentry/nextjs";
export const onRequestError = Sentry.captureRequestError;
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await (await import('@/lib/instrumentation/streamInfo')).default();
@@ -40,4 +44,21 @@ export async function register() {
await viewerCountSync();
}, 2000);
}
process.env.NODE_ENV === 'production' && Sentry.init({
dsn: "https://f3c26671c39af48406c6e23702a4f3dd@o4506961023860736.ingest.us.sentry.io/4509895816773632",
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: 1,
// Enable logs to be sent to Sentry
enableLogs: true,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
integrations: [
Sentry.extraErrorDataIntegration(),
],
});
}

View File

@@ -1,4 +1,5 @@
import { prisma } from '@hctv/db';
import {Prisma, prisma} from '@hctv/db';
import {validateRequest} from "@/lib/auth/validate";
export async function resolveChannelNameId(channelName: string) {
const channel = await prisma.channel.findUnique({
@@ -28,4 +29,14 @@ export async function resolveUserPersonalChannel(userId: string) {
}
return channel;
}
export async function getBotBySlug(slug: string) {
const bot = await prisma.botAccount.findFirst({
where: {
slug,
},
});
return bot;
}

View File

@@ -4,7 +4,10 @@ import { revalidatePath } from 'next/cache';
import { validateRequest } from '@/lib/auth/validate';
import { prisma } from '@hctv/db';
import zodVerify from '../zodVerify';
import { createChannelSchema, onboardSchema, streamInfoEditSchema, updateChannelSettingsSchema } from './zod';
import {
createBotSchema,
createChannelSchema, editBotSchema, onboardSchema, streamInfoEditSchema, updateChannelSettingsSchema
} from './zod';
import { initializeStreamInfo } from '../instrumentation/streamInfo';
import { resolveFollowedChannels, resolveStreamInfo, resolveUserFromPersonalChannelName } from '../auth/resolve';
import { genIdenticonUpload } from '../utils/genIdenticonUpload';
@@ -353,4 +356,76 @@ export async function deleteChannel(channelId: string) {
});
return { success: true }; */
}
export async function createBot(prev: any, formData: FormData) {
const { user } = await validateRequest();
if (!user) {
return { success: false, error: 'Unauthorized' };
}
const zod = await zodVerify(createBotSchema, formData);
if (!zod.success) {
return zod;
}
const botExists = await prisma.botAccount.findFirst({
where: { slug: zod.data.slug },
});
if (botExists) {
return { success: false, error: 'Bot slug already exists' };
}
const createdBot = await prisma.botAccount.create({
data: {
displayName: zod.data.name,
slug: zod.data.slug,
ownerId: user.id,
description: zod.data.description,
pfpUrl: await genIdenticonUpload(zod.data.slug, 'botpfp'),
}
});
return { success: true, slug: createdBot.slug }
}
export async function editBot(prev: any, formData: FormData) {
const { user } = await validateRequest();
if (!user) {
return { success: false, error: 'Unauthorized' };
}
const zod = await zodVerify(editBotSchema, formData);
if (!zod.success) {
return zod;
}
const bot = await prisma.botAccount.findUnique({
where: { id: zod.data.from },
});
if (!bot) {
return { success: false, error: 'Bot not found' };
}
if (bot.ownerId !== user.id) {
return { success: false, error: 'Unauthorized' };
}
if (bot.slug !== zod.data.slug) {
const botExists = await prisma.botAccount.findFirst({
where: { slug: zod.data.slug },
});
if (botExists) {
return { success: false, error: 'Bot slug already exists' };
}
}
const updatedBot = await prisma.botAccount.update({
where: { id: zod.data.from },
data: {
displayName: zod.data.name,
slug: zod.data.slug,
description: zod.data.description,
}
});
revalidatePath(`/settings/bot/${updatedBot.slug}`);
return { success: true, slug: updatedBot.slug }
}

View File

@@ -1,9 +1,20 @@
import { z } from 'zod';
const disallowedUsernames = [
'admin',
'administrator',
'settings',
'create',
// i hope this doesn't age well tbh
'zrl',
];
const username = z
.string()
.min(1)
.regex(/^[a-z0-9_-]+$/, { message: 'Only characters from a-z, 0-9, underscores and dashes' });
.regex(/^[a-z0-9_-]+$/, { message: 'Only characters from a-z, 0-9, underscores and dashes' })
.refine((val) => !disallowedUsernames.includes(val.toLowerCase()), {
message: 'This username is reserved',
});
export const streamInfoEditSchema = z.object({
username: z.string().min(1),
@@ -25,4 +36,16 @@ export const updateChannelSettingsSchema = z.object({
pfpUrl: z.string(),
description: z.string().min(1).max(500),
is247: z.boolean(),
});
});
export const createBotSchema = z.object({
name: z.string().min(1, { message: 'Name is required' }),
slug: username.refine((val) => val !== 'settings', { message: 'This slug is reserved' }),
description: z.string().max(300).optional(),
});
export const editBotSchema = createBotSchema.and(
z.object({
from: z.string().min(1),
})
);

View File

@@ -0,0 +1,21 @@
'use client';
import {
ConfirmDialogProvider as BaseConfirmDialogProvider,
ConfirmOptions,
} from '@omit/react-confirm-dialog';
interface Props {
children: React.ReactNode;
defaultOptions?: ConfirmOptions;
}
export const ConfirmDialogProvider = ({ children, defaultOptions }: Props) => {
return (
<BaseConfirmDialogProvider defaultOptions={defaultOptions}>
{children}
</BaseConfirmDialogProvider>
);
};
export default ConfirmDialogProvider;

View File

@@ -14,7 +14,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{

View File

@@ -28,8 +28,8 @@ rtmp {
hls on;
hls_type live;
hls_path /dev/shm/hls;
hls_fragment 1s;
hls_playlist_length 3s;
hls_fragment 2s;
hls_playlist_length 10s;
hls_cleanup on;
hls_fragment_naming timestamp;

View File

@@ -20,6 +20,7 @@
"r:rtmp": "docker compose -f dev/docker-compose.yml restart nginx-rtmp -t 0"
},
"devDependencies": {
"prettier": "^3.6.2",
"turbo": "^2.4.4"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"

View File

@@ -0,0 +1,42 @@
-- CreateTable
CREATE TABLE "BotAccounts" (
"id" TEXT NOT NULL,
"displayName" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"description" TEXT NOT NULL,
"pfpUrl" TEXT NOT NULL,
"channelId" TEXT NOT NULL,
CONSTRAINT "BotAccounts_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ApiKey" (
"id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"botAccountId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ApiKey_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "BotAccounts_slug_key" ON "BotAccounts"("slug");
-- CreateIndex
CREATE INDEX "BotAccounts_channelId_idx" ON "BotAccounts"("channelId");
-- CreateIndex
CREATE INDEX "BotAccounts_slug_idx" ON "BotAccounts"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "ApiKey_key_key" ON "ApiKey"("key");
-- CreateIndex
CREATE INDEX "ApiKey_botAccountId_idx" ON "ApiKey"("botAccountId");
-- AddForeignKey
ALTER TABLE "BotAccounts" ADD CONSTRAINT "BotAccounts_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "Channel"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ApiKey" ADD CONSTRAINT "ApiKey_botAccountId_fkey" FOREIGN KEY ("botAccountId") REFERENCES "BotAccounts"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,22 @@
/*
Warnings:
- You are about to drop the column `channelId` on the `BotAccounts` table. All the data in the column will be lost.
- Added the required column `ownerId` to the `BotAccounts` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "BotAccounts" DROP CONSTRAINT "BotAccounts_channelId_fkey";
-- DropIndex
DROP INDEX "BotAccounts_channelId_idx";
-- AlterTable
ALTER TABLE "BotAccounts" DROP COLUMN "channelId",
ADD COLUMN "ownerId" TEXT NOT NULL;
-- CreateIndex
CREATE INDEX "BotAccounts_ownerId_idx" ON "BotAccounts"("ownerId");
-- AddForeignKey
ALTER TABLE "BotAccounts" ADD CONSTRAINT "BotAccounts_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,9 @@
/*
Warnings:
- Added the required column `updatedAt` to the `BotAccounts` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "BotAccounts" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL;

View File

@@ -0,0 +1,43 @@
/*
Warnings:
- You are about to drop the `BotAccounts` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "ApiKey" DROP CONSTRAINT "ApiKey_botAccountId_fkey";
-- DropForeignKey
ALTER TABLE "BotAccounts" DROP CONSTRAINT "BotAccounts_ownerId_fkey";
-- DropTable
DROP TABLE "BotAccounts";
-- CreateTable
CREATE TABLE "BotAccount" (
"id" TEXT NOT NULL,
"displayName" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"description" TEXT NOT NULL,
"pfpUrl" TEXT NOT NULL,
"ownerId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BotAccount_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "BotAccount_slug_key" ON "BotAccount"("slug");
-- CreateIndex
CREATE INDEX "BotAccount_ownerId_idx" ON "BotAccount"("ownerId");
-- CreateIndex
CREATE INDEX "BotAccount_slug_idx" ON "BotAccount"("slug");
-- AddForeignKey
ALTER TABLE "BotAccount" ADD CONSTRAINT "BotAccount_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ApiKey" ADD CONSTRAINT "ApiKey_botAccountId_fkey" FOREIGN KEY ("botAccountId") REFERENCES "BotAccount"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "BotAccount" ALTER COLUMN "description" SET DEFAULT 'A hctv bot account';

View File

@@ -0,0 +1,30 @@
/*
Warnings:
- You are about to drop the `ApiKey` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "ApiKey" DROP CONSTRAINT "ApiKey_botAccountId_fkey";
-- DropTable
DROP TABLE "ApiKey";
-- CreateTable
CREATE TABLE "BotApiKey" (
"id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"botAccountId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "BotApiKey_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "BotApiKey_key_key" ON "BotApiKey"("key");
-- CreateIndex
CREATE INDEX "BotApiKey_botAccountId_idx" ON "BotApiKey"("botAccountId");
-- AddForeignKey
ALTER TABLE "BotApiKey" ADD CONSTRAINT "BotApiKey_botAccountId_fkey" FOREIGN KEY ("botAccountId") REFERENCES "BotAccount"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,8 @@
/*
Warnings:
- Added the required column `name` to the `BotApiKey` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "BotApiKey" ADD COLUMN "name" TEXT NOT NULL;

View File

@@ -5,55 +5,56 @@
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
output = "../generated/client"
provider = "prisma-client-js"
output = "../generated/client"
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DATABASE_DIRECT_URL")
}
model User {
id String @id @default(cuid())
slack_id String
pfpUrl String
hasOnboarded Boolean @default(false)
id String @id @default(cuid())
slack_id String
pfpUrl String
hasOnboarded Boolean @default(false)
personalChannel Channel? @relation("PersonalChannel", fields: [personalChannelId], references: [id])
personalChannelId String? @unique
personalChannel Channel? @relation("PersonalChannel", fields: [personalChannelId], references: [id])
personalChannelId String? @unique
ownedChannels Channel[] @relation("ChannelOwner")
managedChannels Channel[] @relation("ChannelManagers")
sessions Session[]
streams StreamInfo[]
followers Follow[] @relation("UserFollows")
ownedChannels Channel[] @relation("ChannelOwner")
managedChannels Channel[] @relation("ChannelManagers")
sessions Session[]
streams StreamInfo[]
followers Follow[] @relation("UserFollows")
botAccounts BotAccount[]
@@index([personalChannelId])
}
model Channel {
id String @id @default(cuid())
name String @unique
id String @id @default(cuid())
name String @unique
description String @default("A hctv channel")
pfpUrl String
pfpUrl String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
personalFor User? @relation("PersonalChannel")
owner User @relation("ChannelOwner", fields: [ownerId], references: [id])
ownerId String
managers User[] @relation("ChannelManagers")
streamInfo StreamInfo[]
followers Follow[] @relation("ChannelFollowers")
streamKey StreamKey?
obsChatGrantToken String @unique @default(cuid())
is247 Boolean @default(false)
owner User @relation("ChannelOwner", fields: [ownerId], references: [id])
ownerId String
managers User[] @relation("ChannelManagers")
streamInfo StreamInfo[]
followers Follow[] @relation("ChannelFollowers")
streamKey StreamKey?
obsChatGrantToken String @unique @default(cuid())
is247 Boolean @default(false)
@@index([ownerId])
}
@@ -65,20 +66,20 @@ model Session {
}
model StreamInfo {
id String @id @default(cuid())
username String @unique
id String @id @default(cuid())
username String @unique
title String
thumbnail String
viewers Int
category String
startedAt DateTime
isLive Boolean
channelId String
channel Channel @relation(fields: [channelId], references: [id])
ownedBy User @relation(fields: [userId], references: [id])
userId String
ownedBy User @relation(fields: [userId], references: [id])
userId String
enableNotifications Boolean @default(true)
@@ -88,13 +89,13 @@ model StreamInfo {
model Follow {
id String @id @default(cuid())
createdAt DateTime @default(now())
user User @relation("UserFollows", fields: [userId], references: [id], onDelete: Cascade)
userId String
channel Channel @relation("ChannelFollowers", fields: [channelId], references: [id], onDelete: Cascade)
user User @relation("UserFollows", fields: [userId], references: [id], onDelete: Cascade)
userId String
channel Channel @relation("ChannelFollowers", fields: [channelId], references: [id], onDelete: Cascade)
channelId String
notifyStream Boolean @default(false)
@@unique([userId, channelId])
@@ -103,9 +104,37 @@ model Follow {
}
model StreamKey {
id String @id @default(cuid())
key String @unique
id String @id @default(cuid())
key String @unique
channelId String @unique
channel Channel @relation(fields: [channelId], references: [id])
}
channelId String @unique
channel Channel @relation(fields: [channelId], references: [id])
}
model BotAccount {
id String @id @default(cuid())
displayName String
slug String @unique
description String @default("A hctv bot account")
pfpUrl String
owner User @relation(fields: [ownerId], references: [id])
ownerId String
apiKeys BotApiKey[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([ownerId])
@@index([slug])
}
model BotApiKey {
id String @id @default(cuid())
name String
key String @unique
botAccount BotAccount @relation(fields: [botAccountId], references: [id], onDelete: Cascade)
botAccountId String
createdAt DateTime @default(now())
@@index([botAccountId])
}

View File

@@ -249,12 +249,15 @@ interface ModifiedWSContext extends WSContext<ModifiedWebSocket> {
user?: any;
personalChannel?: any;
viewerId?: string;
botUsername?: string;
chatUser?: ChatUser | null;
}
export interface ModifiedWebSocket extends WebSocket {
targetUsername?: string;
user?: User;
personalChannel?: Channel;
chatUser?: ChatUser | null;
}
interface CloseEventInit extends EventInit {
@@ -262,3 +265,11 @@ interface CloseEventInit extends EventInit {
reason?: string;
wasClean?: boolean;
}
interface ChatUser {
id: string;
username: string;
pfpUrl: string;
displayName?: string;
isBot: boolean;
}

385
yarn.lock
View File

@@ -747,7 +747,7 @@
"@emnapi/wasi-threads" "1.0.1"
tslib "^2.4.0"
"@emnapi/runtime@^1.2.0", "@emnapi/runtime@^1.4.4":
"@emnapi/runtime@^1.2.0", "@emnapi/runtime@^1.4.4", "@emnapi/runtime@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.5.0.tgz#9aebfcb9b17195dce3ab53c86787a6b7d058db73"
integrity sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==
@@ -1130,6 +1130,11 @@
local-pkg "^1.0.0"
mlly "^1.7.4"
"@img/colour@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@img/colour/-/colour-1.0.0.tgz#d2fabb223455a793bf3bf9c70de3d28526aa8311"
integrity sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==
"@img/sharp-darwin-arm64@0.33.5":
version "0.33.5"
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz#ef5b5a07862805f1e8145a377c8ba6e98813ca08"
@@ -1151,6 +1156,13 @@
optionalDependencies:
"@img/sharp-libvips-darwin-arm64" "1.2.0"
"@img/sharp-darwin-arm64@0.34.4":
version "0.34.4"
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz#8a0dcac9e621ff533fbf2e830f6a977b38d67a0c"
integrity sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==
optionalDependencies:
"@img/sharp-libvips-darwin-arm64" "1.2.3"
"@img/sharp-darwin-x64@0.33.5":
version "0.33.5"
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz#e03d3451cd9e664faa72948cc70a403ea4063d61"
@@ -1172,6 +1184,13 @@
optionalDependencies:
"@img/sharp-libvips-darwin-x64" "1.2.0"
"@img/sharp-darwin-x64@0.34.4":
version "0.34.4"
resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz#0ba2bd9dbf07f7300fab73305b787e66156f7752"
integrity sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==
optionalDependencies:
"@img/sharp-libvips-darwin-x64" "1.2.3"
"@img/sharp-libvips-darwin-arm64@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz#447c5026700c01a993c7804eb8af5f6e9868c07f"
@@ -1187,6 +1206,11 @@
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz#e20e9041031acde1de19da121dc5162c7d2cf251"
integrity sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==
"@img/sharp-libvips-darwin-arm64@1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz#f43c9aa3b74fd307e4318da63ebbe0ed4c34e744"
integrity sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==
"@img/sharp-libvips-darwin-x64@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz#e0456f8f7c623f9dbfbdc77383caa72281d86062"
@@ -1202,6 +1226,11 @@
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz#918ca81c5446f31114834cb908425a7532393185"
integrity sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==
"@img/sharp-libvips-darwin-x64@1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz#c42ff786d4a1f42ef8929dba4a989dd5df6417f0"
integrity sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==
"@img/sharp-libvips-linux-arm64@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz#979b1c66c9a91f7ff2893556ef267f90ebe51704"
@@ -1217,6 +1246,11 @@
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz#1a5beafc857b43f378c3030427aa981ee3edbc54"
integrity sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==
"@img/sharp-libvips-linux-arm64@1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz#c9073e5c4b629ee417f777db21c552910d84ed77"
integrity sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==
"@img/sharp-libvips-linux-arm@1.0.5":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz#99f922d4e15216ec205dcb6891b721bfd2884197"
@@ -1232,6 +1266,11 @@
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz#bff51182d5238ca35c5fe9e9f594a18ad6a5480d"
integrity sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==
"@img/sharp-libvips-linux-arm@1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz#3cbc333fd6b8f224a14d69b03a1dd11df897c799"
integrity sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==
"@img/sharp-libvips-linux-ppc64@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz#682334595f2ca00e0a07a675ba170af165162802"
@@ -1242,6 +1281,11 @@
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz#10c53ccf6f2d47d71fb3fa282697072c8fe9e40e"
integrity sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==
"@img/sharp-libvips-linux-ppc64@1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz#68e0e0076299f43d838468675674fabcc7161d16"
integrity sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==
"@img/sharp-libvips-linux-s390x@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz#f8a5eb1f374a082f72b3f45e2fb25b8118a8a5ce"
@@ -1257,6 +1301,11 @@
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz#392fd7557ddc5c901f1bed7ab3c567c08833ef3b"
integrity sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==
"@img/sharp-libvips-linux-s390x@1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz#7da9ab11a50c0ca905979f0aae14a4ccffab27b2"
integrity sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==
"@img/sharp-libvips-linux-x64@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz#d4c4619cdd157774906e15770ee119931c7ef5e0"
@@ -1272,6 +1321,11 @@
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz#9315cf90a2fdcdc0e29ea7663cbd8b0f15254400"
integrity sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==
"@img/sharp-libvips-linux-x64@1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz#3b162d6b190cf77926819040e09fb15eec42135e"
integrity sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==
"@img/sharp-libvips-linuxmusl-arm64@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz#166778da0f48dd2bded1fa3033cee6b588f0d5d5"
@@ -1287,6 +1341,11 @@
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz#705e03e67d477f6f842f37eb7f66285b1150dc06"
integrity sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==
"@img/sharp-libvips-linuxmusl-arm64@1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz#ac99576630dd8e33cb598d7c4586f6e0655912ea"
integrity sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==
"@img/sharp-libvips-linuxmusl-x64@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz#93794e4d7720b077fcad3e02982f2f1c246751ff"
@@ -1302,6 +1361,11 @@
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz#ec905071cc538df64848d5900e0d386d77c55f13"
integrity sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==
"@img/sharp-libvips-linuxmusl-x64@1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz#93e9495af7bf6c4e0d41dd71d0196c35c3753a1c"
integrity sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==
"@img/sharp-linux-arm64@0.33.5":
version "0.33.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz#edb0697e7a8279c9fc829a60fc35644c4839bb22"
@@ -1323,6 +1387,13 @@
optionalDependencies:
"@img/sharp-libvips-linux-arm64" "1.2.0"
"@img/sharp-linux-arm64@0.34.4":
version "0.34.4"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz#0570ff1a4fa6e1d6779456fca8b5e8c18a6a9cf2"
integrity sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==
optionalDependencies:
"@img/sharp-libvips-linux-arm64" "1.2.3"
"@img/sharp-linux-arm@0.33.5":
version "0.33.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz#422c1a352e7b5832842577dc51602bcd5b6f5eff"
@@ -1344,6 +1415,13 @@
optionalDependencies:
"@img/sharp-libvips-linux-arm" "1.2.0"
"@img/sharp-linux-arm@0.34.4":
version "0.34.4"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz#5f020d933f54f3fc49203d32c3b7dd0ec11ffcdb"
integrity sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==
optionalDependencies:
"@img/sharp-libvips-linux-arm" "1.2.3"
"@img/sharp-linux-ppc64@0.34.3":
version "0.34.3"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz#6a7cd4c608011333a0ddde6d96e03ac042dd9079"
@@ -1351,6 +1429,13 @@
optionalDependencies:
"@img/sharp-libvips-linux-ppc64" "1.2.0"
"@img/sharp-linux-ppc64@0.34.4":
version "0.34.4"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz#8d5775f6dc7e30ea3a1efa43798b7690bb5cb344"
integrity sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==
optionalDependencies:
"@img/sharp-libvips-linux-ppc64" "1.2.3"
"@img/sharp-linux-s390x@0.33.5":
version "0.33.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz#f5c077926b48e97e4a04d004dfaf175972059667"
@@ -1372,6 +1457,13 @@
optionalDependencies:
"@img/sharp-libvips-linux-s390x" "1.2.0"
"@img/sharp-linux-s390x@0.34.4":
version "0.34.4"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz#740aa5b369188ee2c1913b1015e7f830f4dfdb50"
integrity sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==
optionalDependencies:
"@img/sharp-libvips-linux-s390x" "1.2.3"
"@img/sharp-linux-x64@0.33.5":
version "0.33.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz#d806e0afd71ae6775cc87f0da8f2d03a7c2209cb"
@@ -1393,6 +1485,13 @@
optionalDependencies:
"@img/sharp-libvips-linux-x64" "1.2.0"
"@img/sharp-linux-x64@0.34.4":
version "0.34.4"
resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz#573ce4196b2d0771bba32acc13a37b7adc9b6212"
integrity sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==
optionalDependencies:
"@img/sharp-libvips-linux-x64" "1.2.3"
"@img/sharp-linuxmusl-arm64@0.33.5":
version "0.33.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz#252975b915894fb315af5deea174651e208d3d6b"
@@ -1414,6 +1513,13 @@
optionalDependencies:
"@img/sharp-libvips-linuxmusl-arm64" "1.2.0"
"@img/sharp-linuxmusl-arm64@0.34.4":
version "0.34.4"
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz#3c91bc8348cc3b42b43c6fca14f9dbb5cb47bd0d"
integrity sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==
optionalDependencies:
"@img/sharp-libvips-linuxmusl-arm64" "1.2.3"
"@img/sharp-linuxmusl-x64@0.33.5":
version "0.33.5"
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz#3f4609ac5d8ef8ec7dadee80b560961a60fd4f48"
@@ -1435,6 +1541,13 @@
optionalDependencies:
"@img/sharp-libvips-linuxmusl-x64" "1.2.0"
"@img/sharp-linuxmusl-x64@0.34.4":
version "0.34.4"
resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz#33de7d476ac9e2db7ef654331b54cc679b806bda"
integrity sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==
optionalDependencies:
"@img/sharp-libvips-linuxmusl-x64" "1.2.3"
"@img/sharp-wasm32@0.33.5":
version "0.33.5"
resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz#6f44f3283069d935bb5ca5813153572f3e6f61a1"
@@ -1456,6 +1569,13 @@
dependencies:
"@emnapi/runtime" "^1.4.4"
"@img/sharp-wasm32@0.34.4":
version "0.34.4"
resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz#d617f7b3f851f899802298f360667c20605c0198"
integrity sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==
dependencies:
"@emnapi/runtime" "^1.5.0"
"@img/sharp-win32-arm64@0.34.2":
version "0.34.2"
resolved "https://registry.yarnpkg.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.2.tgz#f37bee0f60c167f825a09d2b8de6849b823e8b30"
@@ -1466,6 +1586,11 @@
resolved "https://registry.yarnpkg.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz#3e8654e368bb349d45799a0d7aeb29db2298628e"
integrity sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==
"@img/sharp-win32-arm64@0.34.4":
version "0.34.4"
resolved "https://registry.yarnpkg.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz#38e2c8a88826eac647f7c3f99efefb39897a8f5c"
integrity sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==
"@img/sharp-win32-ia32@0.33.5":
version "0.33.5"
resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz#1a0c839a40c5351e9885628c85f2e5dfd02b52a9"
@@ -1481,6 +1606,11 @@
resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz#9d4c105e8d5074a351a81a0b6d056e0af913bf76"
integrity sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==
"@img/sharp-win32-ia32@0.34.4":
version "0.34.4"
resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz#003a7eb0fdaba600790c3007cfd756e41a9cf749"
integrity sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==
"@img/sharp-win32-x64@0.33.5":
version "0.33.5"
resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz#56f00962ff0c4e0eb93d34a047d29fa995e3e342"
@@ -1496,6 +1626,11 @@
resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz#d20c89bd41b1dd3d76d8575714aaaa3c43204b6a"
integrity sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==
"@img/sharp-win32-x64@0.34.4":
version "0.34.4"
resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz#b19f1f88ace8bfc20784a0ad31767f3438e025d1"
integrity sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==
"@inquirer/confirm@^5.0.0":
version "5.1.12"
resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-5.1.12.tgz#387037889a5a558ceefe52e978228630aa6e7d0e"
@@ -1811,10 +1946,10 @@
"@emnapi/runtime" "^1.3.1"
"@tybys/wasm-util" "^0.9.0"
"@next/env@15.3.4":
version "15.3.4"
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.3.4.tgz#5b41485596d5bfea0918db73f90b7a6db734da3f"
integrity sha512-ZkdYzBseS6UjYzz6ylVKPOK+//zLWvD6Ta+vpoye8cW11AjiQjGYVibF0xuvT4L0iJfAPfZLFidaEzAOywyOAQ==
"@next/env@15.6.0-canary.34":
version "15.6.0-canary.34"
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.6.0-canary.34.tgz#e97bce92f8918239fce88a688b260c81cc043720"
integrity sha512-OpjhOCarzTE7c5GvNGYrpVlPh60seQDwa1Vau76ZDgFx8BLwODL7wpOkgKaWqHepu68zV9EuLQy1IkrU34Vjcw==
"@next/eslint-plugin-next@15.1.3":
version "15.1.3"
@@ -1823,45 +1958,45 @@
dependencies:
fast-glob "3.3.1"
"@next/swc-darwin-arm64@15.3.4":
version "15.3.4"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.4.tgz#cb2849b8374eb6b52376d4e7abed2a21a2ff24d6"
integrity sha512-z0qIYTONmPRbwHWvpyrFXJd5F9YWLCsw3Sjrzj2ZvMYy9NPQMPZ1NjOJh4ojr4oQzcGYwgJKfidzehaNa1BpEg==
"@next/swc-darwin-arm64@15.6.0-canary.34":
version "15.6.0-canary.34"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.6.0-canary.34.tgz#553d843b8da5ece93cbd6f5d2f5eb327e9601544"
integrity sha512-YAGGiAJyfqISxu+0vTvZO6wNnjHpPDqQ/Km9HuoUgdFa9Gl02qgYenkl0wtLv4ULizpvDP4An2ik1S63JzFasw==
"@next/swc-darwin-x64@15.3.4":
version "15.3.4"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.4.tgz#aa7fd968af7e53aa17d4f234cf7722b3899712cf"
integrity sha512-Z0FYJM8lritw5Wq+vpHYuCIzIlEMjewG2aRkc3Hi2rcbULknYL/xqfpBL23jQnCSrDUGAo/AEv0Z+s2bff9Zkw==
"@next/swc-darwin-x64@15.6.0-canary.34":
version "15.6.0-canary.34"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.6.0-canary.34.tgz#995ebc272fb8c6b79e565ade0b738b361f173f58"
integrity sha512-7Ils05FOT0jLL/zpvobtqlodswfpTe28z4a701ZIUxUcRMb0p3F3aySjd3oc/1ZElQf2buGabxbJZIsoPKNXrw==
"@next/swc-linux-arm64-gnu@15.3.4":
version "15.3.4"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.4.tgz#5da3d6d6055665d0c3a2dab0bc0471064bc9eece"
integrity sha512-l8ZQOCCg7adwmsnFm8m5q9eIPAHdaB2F3cxhufYtVo84pymwKuWfpYTKcUiFcutJdp9xGHC+F1Uq3xnFU1B/7g==
"@next/swc-linux-arm64-gnu@15.6.0-canary.34":
version "15.6.0-canary.34"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.6.0-canary.34.tgz#dca81392730b3fbab59df6ee7a1a39929c992a05"
integrity sha512-MWqankIvInIQlWclHOZwC5FHJ7dx6OFrtpqJCtThabncSy7imkKBTzfL/Bytb8WQw6e85YzNwUzH1PGA8voHrw==
"@next/swc-linux-arm64-musl@15.3.4":
version "15.3.4"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.4.tgz#9043ccc397746c94c2452d301e8f95a33aec22e8"
integrity sha512-wFyZ7X470YJQtpKot4xCY3gpdn8lE9nTlldG07/kJYexCUpX1piX+MBfZdvulo+t1yADFVEuzFfVHfklfEx8kw==
"@next/swc-linux-arm64-musl@15.6.0-canary.34":
version "15.6.0-canary.34"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.6.0-canary.34.tgz#2c7bbb7ed2028321772e220e213ce8f399d8d78f"
integrity sha512-8Cv+0TS8m3S7n4LBDe+GLn/6GZyutPxuHFaeZouFOgU7uHwXo/CRzRwPh10RoxyobooRoDIovVeA9moKy/JqiQ==
"@next/swc-linux-x64-gnu@15.3.4":
version "15.3.4"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.4.tgz#49a33f904a51a8c665406ca7e5a748f480bf195d"
integrity sha512-gEbH9rv9o7I12qPyvZNVTyP/PWKqOp8clvnoYZQiX800KkqsaJZuOXkWgMa7ANCCh/oEN2ZQheh3yH8/kWPSEg==
"@next/swc-linux-x64-gnu@15.6.0-canary.34":
version "15.6.0-canary.34"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.6.0-canary.34.tgz#2e1825a9c13d339561262103b6f6109bde605766"
integrity sha512-TJ/cCYk/aXp7Gxz6WtmuiklTpojfhikqEFrZRzNt4vLB+s1/6BdOx7FgIp2Y6m0Xjxqqq+i+3DI094L392ssFw==
"@next/swc-linux-x64-musl@15.3.4":
version "15.3.4"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.4.tgz#8beaff35d8f11961ea80d12a10226581df4c5a74"
integrity sha512-Cf8sr0ufuC/nu/yQ76AnarbSAXcwG/wj+1xFPNbyNo8ltA6kw5d5YqO8kQuwVIxk13SBdtgXrNyom3ZosHAy4A==
"@next/swc-linux-x64-musl@15.6.0-canary.34":
version "15.6.0-canary.34"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.6.0-canary.34.tgz#a2a2a04eb3dd43b270e2c712e519d96cb550d39b"
integrity sha512-+zpMXFVGspAYfTR30v6EM0pOPaTjBSwwDbcRg2hOsI1PdFGVLwvljdDw2jZrS9JwyaIF7btCfTUi4w636G66TQ==
"@next/swc-win32-arm64-msvc@15.3.4":
version "15.3.4"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.4.tgz#149d9a35068ecda317af138814539929c9c269af"
integrity sha512-ay5+qADDN3rwRbRpEhTOreOn1OyJIXS60tg9WMYTWCy3fB6rGoyjLVxc4dR9PYjEdR2iDYsaF5h03NA+XuYPQQ==
"@next/swc-win32-arm64-msvc@15.6.0-canary.34":
version "15.6.0-canary.34"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.6.0-canary.34.tgz#80f9cf75616f7d3dae54839c0795a03a0cd84100"
integrity sha512-f40lriU/Zqy5v5QqmdTaCZywCeH/sg5Q5gE1rTFpJXfSf3O7uDAfh960A//Yatl/ADeNGNvY1BdA/NXdKntEsQ==
"@next/swc-win32-x64-msvc@15.3.4":
version "15.3.4"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.4.tgz#a652327782d838c2b875eaf216187c51b8409775"
integrity sha512-4kDt31Bc9DGyYs41FTL1/kNpDeHyha2TC0j5sRRoKCyrhNcfZ/nRQkAUlF27mETwm8QyHqIjHJitfcza2Iykfg==
"@next/swc-win32-x64-msvc@15.6.0-canary.34":
version "15.6.0-canary.34"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.6.0-canary.34.tgz#bc1eb485053f3fe869dcc7c2bacf01c8348a9a75"
integrity sha512-2V5q/qv9s70IQjhzaRZG6X22qdI67gBknBeNAD6/TR7JB2dCpPjsUXt8JpR+LiUueth26GR8Hbn+w68S3hNHug==
"@node-rs/argon2-android-arm-eabi@2.0.2":
version "2.0.2"
@@ -1981,6 +2116,17 @@
resolved "https://registry.yarnpkg.com/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz#3dc35ba0f1e66b403c00b39344f870298ebb1c8e"
integrity sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==
"@omit/react-confirm-dialog@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@omit/react-confirm-dialog/-/react-confirm-dialog-1.2.0.tgz#fff9c47570a91411d8cd8a1dc64385b09fbd12b7"
integrity sha512-FXfEnOLPQdGwPXe0zTjgVVbS6OZdAZ+Qs+tSBU819CwtXLNjvx+mimvYvnGnm6dIHK4PGV17D7ViSn+KI8HPRg==
dependencies:
"@radix-ui/react-alert-dialog" "^1.1.13"
"@radix-ui/react-slot" "^1.2.2"
class-variance-authority "^0.7.1"
clsx "^2.1.1"
tailwind-merge "^2.6.0"
"@open-draft/deferred-promise@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz#4a822d10f6f0e316be4d67b4d4f8c9a124b073bd"
@@ -2435,6 +2581,23 @@
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.2.tgz#83f415c4425f21e3d27914c12b3272a32e3dae65"
integrity sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==
"@radix-ui/primitive@1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.3.tgz#e2dbc13bdc5e4168f4334f75832d7bdd3e2de5ba"
integrity sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==
"@radix-ui/react-alert-dialog@^1.1.13":
version "1.1.15"
resolved "https://registry.yarnpkg.com/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz#fa751d0fdd9aa2a90961c9901dba18e638dd4b41"
integrity sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==
dependencies:
"@radix-ui/primitive" "1.1.3"
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-context" "1.1.2"
"@radix-ui/react-dialog" "1.1.15"
"@radix-ui/react-primitive" "2.1.3"
"@radix-ui/react-slot" "1.2.3"
"@radix-ui/react-arrow@1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz#30c0d574d7bb10eed55cd7007b92d38b03c6b2ab"
@@ -2548,6 +2711,26 @@
aria-hidden "^1.1.1"
react-remove-scroll "2.5.5"
"@radix-ui/react-dialog@1.1.15":
version "1.1.15"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz#1de3d7a7e9a17a9874d29c07f5940a18a119b632"
integrity sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==
dependencies:
"@radix-ui/primitive" "1.1.3"
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-context" "1.1.2"
"@radix-ui/react-dismissable-layer" "1.1.11"
"@radix-ui/react-focus-guards" "1.1.3"
"@radix-ui/react-focus-scope" "1.1.7"
"@radix-ui/react-id" "1.1.1"
"@radix-ui/react-portal" "1.1.9"
"@radix-ui/react-presence" "1.1.5"
"@radix-ui/react-primitive" "2.1.3"
"@radix-ui/react-slot" "1.2.3"
"@radix-ui/react-use-controllable-state" "1.2.2"
aria-hidden "^1.2.4"
react-remove-scroll "^2.6.3"
"@radix-ui/react-dialog@^1.1.5":
version "1.1.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz#65b4465e99ad900f28a98eed9a94bb21ec644bf7"
@@ -2601,6 +2784,17 @@
"@radix-ui/react-use-callback-ref" "1.1.1"
"@radix-ui/react-use-escape-keydown" "1.1.1"
"@radix-ui/react-dismissable-layer@1.1.11":
version "1.1.11"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz#e33ab6f6bdaa00f8f7327c408d9f631376b88b37"
integrity sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==
dependencies:
"@radix-ui/primitive" "1.1.3"
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-primitive" "2.1.3"
"@radix-ui/react-use-callback-ref" "1.1.1"
"@radix-ui/react-use-escape-keydown" "1.1.1"
"@radix-ui/react-dismissable-layer@1.1.5":
version "1.1.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz#96dde2be078c694a621e55e047406c58cd5fe774"
@@ -2637,6 +2831,11 @@
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz#8635edd346304f8b42cae86b05912b61aef27afe"
integrity sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==
"@radix-ui/react-focus-guards@1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz#2a5669e464ad5fde9f86d22f7fdc17781a4dfa7f"
integrity sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==
"@radix-ui/react-focus-scope@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz#2ac45fce8c5bb33eb18419cdc1905ef4f1906525"
@@ -2656,6 +2855,15 @@
"@radix-ui/react-primitive" "2.0.2"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-focus-scope@1.1.7":
version "1.1.7"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz#dfe76fc103537d80bf42723a183773fd07bfb58d"
integrity sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==
dependencies:
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-primitive" "2.1.3"
"@radix-ui/react-use-callback-ref" "1.1.1"
"@radix-ui/react-hover-card@^1.1.14":
version "1.1.14"
resolved "https://registry.yarnpkg.com/@radix-ui/react-hover-card/-/react-hover-card-1.1.14.tgz#a557cda6470e214e744e46ede839496e8b291843"
@@ -2826,6 +3034,14 @@
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-use-layout-effect" "1.1.1"
"@radix-ui/react-presence@1.1.5":
version "1.1.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.5.tgz#5d8f28ac316c32f078afce2996839250c10693db"
integrity sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==
dependencies:
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-use-layout-effect" "1.1.1"
"@radix-ui/react-primitive@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz#d49ea0f3f0b2fe3ab1cb5667eb03e8b843b914d0"
@@ -2927,7 +3143,7 @@
dependencies:
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-slot@1.2.3":
"@radix-ui/react-slot@1.2.3", "@radix-ui/react-slot@^1.2.2":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz#502d6e354fc847d4169c3bc5f189de777f68cfe1"
integrity sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==
@@ -4101,11 +4317,6 @@
resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.0.0.tgz#f193b73dc316c4170f2e82a881da0f550d551b9c"
integrity sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==
"@swc/counter@0.1.3":
version "0.1.3"
resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9"
integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==
"@swc/helpers@0.5.15":
version "0.5.15"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.15.tgz#79efab344c5819ecf83a43f3f9f811fc84b516d7"
@@ -5498,13 +5709,6 @@ bundle-require@^5.1.0:
dependencies:
load-tsconfig "^0.2.3"
busboy@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
dependencies:
streamsearch "^1.1.0"
bytes@3.1.2, bytes@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
@@ -6486,6 +6690,11 @@ detect-libc@^2.0.3, detect-libc@^2.0.4:
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.4.tgz#f04715b8ba815e53b4d8109655b6508a6865a7e8"
integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==
detect-libc@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.1.tgz#9f1e511ace6bb525efea4651345beac424dac7b9"
integrity sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==
detect-node-es@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493"
@@ -9815,28 +10024,26 @@ next-themes@^0.4.4:
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.4.6.tgz#8d7e92d03b8fea6582892a50a928c9b23502e8b6"
integrity sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==
next@^15.3.4:
version "15.3.4"
resolved "https://registry.yarnpkg.com/next/-/next-15.3.4.tgz#7a4863be14c998f1ec1e6d8d4e9e1a1291c8cbe3"
integrity sha512-mHKd50C+mCjam/gcnwqL1T1vPx/XQNFlXqFIVdgQdVAFY9iIQtY0IfaVflEYzKiqjeA7B0cYYMaCrmAYFjs4rA==
next@^15.6.0-canary.34:
version "15.6.0-canary.34"
resolved "https://registry.yarnpkg.com/next/-/next-15.6.0-canary.34.tgz#37dfec95a7cdb6edd87d3559667ec28674d55576"
integrity sha512-H2lVHtMc8TSMvQe3zu66AlLGYM3Jn22KW+TpIFIIQaplZZQyyPN8hcq76fO/iIIb0KI5902rHesmoPxxlFAWYA==
dependencies:
"@next/env" "15.3.4"
"@swc/counter" "0.1.3"
"@next/env" "15.6.0-canary.34"
"@swc/helpers" "0.5.15"
busboy "1.6.0"
caniuse-lite "^1.0.30001579"
postcss "8.4.31"
styled-jsx "5.1.6"
optionalDependencies:
"@next/swc-darwin-arm64" "15.3.4"
"@next/swc-darwin-x64" "15.3.4"
"@next/swc-linux-arm64-gnu" "15.3.4"
"@next/swc-linux-arm64-musl" "15.3.4"
"@next/swc-linux-x64-gnu" "15.3.4"
"@next/swc-linux-x64-musl" "15.3.4"
"@next/swc-win32-arm64-msvc" "15.3.4"
"@next/swc-win32-x64-msvc" "15.3.4"
sharp "^0.34.1"
"@next/swc-darwin-arm64" "15.6.0-canary.34"
"@next/swc-darwin-x64" "15.6.0-canary.34"
"@next/swc-linux-arm64-gnu" "15.6.0-canary.34"
"@next/swc-linux-arm64-musl" "15.6.0-canary.34"
"@next/swc-linux-x64-gnu" "15.6.0-canary.34"
"@next/swc-linux-x64-musl" "15.6.0-canary.34"
"@next/swc-win32-arm64-msvc" "15.6.0-canary.34"
"@next/swc-win32-x64-msvc" "15.6.0-canary.34"
sharp "^0.34.4"
nlcst-to-string@^4.0.0:
version "4.0.0"
@@ -10566,6 +10773,11 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prettier@^3.6.2:
version "3.6.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.2.tgz#ccda02a1003ebbb2bfda6f83a074978f608b9393"
integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==
pretty-bytes@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-6.1.1.tgz#38cd6bb46f47afbf667c202cfc754bffd2016a3b"
@@ -11548,7 +11760,7 @@ sharp@^0.33.3:
"@img/sharp-win32-ia32" "0.33.5"
"@img/sharp-win32-x64" "0.33.5"
sharp@^0.34.1, sharp@^0.34.2:
sharp@^0.34.2:
version "0.34.2"
resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.34.2.tgz#648bd639854dbe48047b0b420213c186d036b32d"
integrity sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==
@@ -11611,6 +11823,38 @@ sharp@^0.34.3:
"@img/sharp-win32-ia32" "0.34.3"
"@img/sharp-win32-x64" "0.34.3"
sharp@^0.34.4:
version "0.34.4"
resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.34.4.tgz#73c2c5a425e98250b8b927e5537f711da8966e38"
integrity sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==
dependencies:
"@img/colour" "^1.0.0"
detect-libc "^2.1.0"
semver "^7.7.2"
optionalDependencies:
"@img/sharp-darwin-arm64" "0.34.4"
"@img/sharp-darwin-x64" "0.34.4"
"@img/sharp-libvips-darwin-arm64" "1.2.3"
"@img/sharp-libvips-darwin-x64" "1.2.3"
"@img/sharp-libvips-linux-arm" "1.2.3"
"@img/sharp-libvips-linux-arm64" "1.2.3"
"@img/sharp-libvips-linux-ppc64" "1.2.3"
"@img/sharp-libvips-linux-s390x" "1.2.3"
"@img/sharp-libvips-linux-x64" "1.2.3"
"@img/sharp-libvips-linuxmusl-arm64" "1.2.3"
"@img/sharp-libvips-linuxmusl-x64" "1.2.3"
"@img/sharp-linux-arm" "0.34.4"
"@img/sharp-linux-arm64" "0.34.4"
"@img/sharp-linux-ppc64" "0.34.4"
"@img/sharp-linux-s390x" "0.34.4"
"@img/sharp-linux-x64" "0.34.4"
"@img/sharp-linuxmusl-arm64" "0.34.4"
"@img/sharp-linuxmusl-x64" "0.34.4"
"@img/sharp-wasm32" "0.34.4"
"@img/sharp-win32-arm64" "0.34.4"
"@img/sharp-win32-ia32" "0.34.4"
"@img/sharp-win32-x64" "0.34.4"
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@@ -11805,11 +12049,6 @@ stream-replace-string@^2.0.0:
resolved "https://registry.yarnpkg.com/stream-replace-string/-/stream-replace-string-2.0.0.tgz#e49fd584bd1c633613e010bc73b9db49cb5024ad"
integrity sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==
streamsearch@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
strict-event-emitter@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz#1602ece81c51574ca39c6815e09f1a3e8550bd93"
@@ -12048,7 +12287,7 @@ tabbable@^6.2.0:
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97"
integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==
tailwind-merge@^2.2.2, tailwind-merge@^2.5.5:
tailwind-merge@^2.2.2, tailwind-merge@^2.5.5, tailwind-merge@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz#ac5fb7e227910c038d458f396b7400d93a3142d5"
integrity sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==