feat(notif): actually send messages

This commit is contained in:
2026-04-04 22:49:16 +02:00
parent f1f6d20d53
commit a14762d3a1
5 changed files with 100 additions and 28 deletions

View File

@@ -1109,7 +1109,7 @@ export default function ChannelSettingsClient({
<label className="block text-sm font-medium mb-1">
Notification channels
</label>
<Textarea
<Textarea
name={field.name}
ref={field.ref}
value={
@@ -1119,13 +1119,20 @@ export default function ChannelSettingsClient({
? field.value.join('\n')
: ''
}
disabled={channel.is247}
rows={4}
placeholder="Enter channel IDs, one per line"
placeholder={
channel.is247
? 'Notifications are disabled for 24/7 channels'
: 'Enter channel IDs, one per line'
}
onBlur={field.onBlur}
onChange={field.onChange}
/>
<p className="text-xs text-muted-foreground mt-1">
Enter one channel ID per line. You can find channel IDs in their URLs.
{channel.is247
? '24/7 channels do not send go-live notifications, so notification channels cannot be edited.'
: 'Enter one channel ID per line. You can find channel IDs in their URLs.'}
</p>
</div>
),

View File

@@ -59,21 +59,20 @@ export const changeUsernameSchema = z.object({
newUsername: username,
});
const notificationChannelsSchema: z.ZodType<string[], z.ZodTypeDef, string | string[]> =
z.preprocess(
(value) => {
if (typeof value === 'string') {
return value
.replace(/\r\n/g, '\n')
.split('\n')
.map((channel) => channel.trim())
.filter(Boolean);
}
const notificationChannelsSchema = z
.union([z.string(), z.array(z.string())])
.transform((value) => {
if (typeof value === 'string') {
return value
.replace(/\r\n/g, '\n')
.split('\n')
.map((channel) => channel.trim())
.filter(Boolean);
}
return value;
},
z.array(z.string()).max(10)
);
return value.map((channel) => channel.trim()).filter(Boolean);
})
.pipe(z.array(z.string()).max(10));
export const updateNotificationChannelsSchema = z.object({
channelId: z.string().min(1),

View File

@@ -141,7 +141,13 @@ export async function syncStream() {
for (const [username, regionKey] of allActiveStreams) {
const existingStream = await prisma.streamInfo.findUnique({
where: { username },
include: { channel: true },
include: {
channel: {
include: {
owner: true,
},
},
},
});
if (existingStream && !existingStream.isLive) {
@@ -173,6 +179,20 @@ export async function syncStream() {
channel: process.env.NOTIFICATION_CHANNEL_ID!,
unfurl_links: true,
});
for (const channelId of existingStream.channel.notifChannels) {
queue.add(`streamStartChannel:${existingStream.username}`, {
text: `${existingStream.username} is now *live*, streaming *${existingStream.title}* (${existingStream.category})!\n<https://hackclub.tv/${existingStream.username}|Go check them out>`,
channel: channelId,
unfurl_links: true,
metadata: {
type: 'custom_stream_announcement',
managedChannelId: existingStream.channel.id,
ownerSlackId: existingStream.channel.owner.slack_id,
ownerChannelName: existingStream.channel.name,
},
});
}
}
if (existingStream.enableNotifications && !existingStream.channel.is247) {

View File

@@ -1,8 +1,20 @@
import { Queue, Worker } from 'bullmq';
import { getRedisConnection } from '@hctv/db';
export type SlackNotificationJobData = {
channel: string;
text: string;
unfurl_links?: boolean;
metadata?: {
type: 'custom_stream_announcement';
managedChannelId: string;
ownerSlackId: string;
ownerChannelName: string;
};
};
const globalForNotifier = global as unknown as {
notificationQueue: Queue | null;
notificationQueue: Queue<SlackNotificationJobData> | null;
notificationWorker: Worker | null;
thumbnailQueue: Queue | null;
@@ -14,9 +26,9 @@ if (!globalForNotifier.notificationQueue) {
globalForNotifier.notificationWorker = null;
}
export function getNotificationQueue(): Queue {
export function getNotificationQueue(): Queue<SlackNotificationJobData> {
if (!globalForNotifier.notificationQueue) {
globalForNotifier.notificationQueue = new Queue('notifications', {
globalForNotifier.notificationQueue = new Queue<SlackNotificationJobData>('notifications', {
connection: getRedisConnection().options,
defaultJobOptions: {
attempts: 3,
@@ -44,4 +56,4 @@ export function getThumbnailQueue(): Queue {
});
}
return globalForNotifier.thumbnailQueue;
}
}

View File

@@ -1,6 +1,7 @@
import { Worker } from 'bullmq';
import { getRedisConnection } from '@hctv/db';
import { getRedisConnection, prisma } from '@hctv/db';
import snClient from '@/lib/services/slackNotifier';
import type { SlackNotificationJobData } from '@/lib/workers';
const globalForWorker = global as unknown as {
notificationWorker: Worker | null;
@@ -18,14 +19,47 @@ export async function registerNotificationWorker(): Promise<void> {
console.log('Registering notification worker...');
const worker = new Worker('notifications', async (job) => {
const worker = new Worker<SlackNotificationJobData>('notifications', async (job) => {
try {
await snClient.chat.postMessage(job.data);
const { metadata: _metadata, ...slackMessage } = job.data;
await snClient.chat.postMessage(slackMessage);
return { success: true };
} catch (e) {
console.error('Slack notification failed:', e);
// @ts-ignore e is unknown
return { success: false, error: e.message };
if (job.data.metadata?.type === 'custom_stream_announcement') {
const channel = await prisma.channel.findUnique({
where: { id: job.data.metadata.managedChannelId },
select: {
notifChannels: true,
},
});
if (channel?.notifChannels.includes(job.data.channel)) {
await prisma.channel.update({
where: { id: job.data.metadata.managedChannelId },
data: {
notifChannels: channel.notifChannels.filter(
(channelId) => channelId !== job.data.channel
),
},
});
}
try {
await snClient.chat.postMessage({
channel: job.data.metadata.ownerSlackId,
text: `I couldn't send a go-live notification for *${job.data.metadata.ownerChannelName}* to Slack channel \`${job.data.channel}\`, so I removed it from that channel's notification list.\nIf you still want notifications there, please make sure the bot can post in that channel and add it again in settings.`,
});
} catch (ownerNotificationError) {
console.error('Failed to notify channel owner about Slack notification removal:', ownerNotificationError);
}
}
return {
success: false,
error: e instanceof Error ? e.message : 'Unknown Slack notification error',
};
}
}, {
connection: getRedisConnection().options,
@@ -45,4 +79,4 @@ export async function closeNotificationWorker(): Promise<void> {
await globalForWorker.notificationWorker.close();
globalForWorker.notificationWorker = null;
}
}
}