rework auth (i'm an idiot), add whisper commands, change whispercmds, back to webhook

This commit is contained in:
2025-11-20 20:23:24 +01:00
parent 977082f38e
commit 34fa80e292
20 changed files with 263 additions and 200 deletions

View File

@@ -50,28 +50,23 @@ async function initAuth(userId: string, clientId: string, clientSecret: string,
return tokenData;
};
export async function createAuthProvider(user: string, intents: string[], streamer = false): Promise<RefreshingAuthProvider> {
export type authProviderInstructions = {
userId: string;
intents: string[];
streamer: boolean;
};
export async function createAuthProvider(data: authProviderInstructions[]): Promise<RefreshingAuthProvider> {
const clientId = process.env.CLIENT_ID;
if (!clientId) { logger.enverr("CLIENT_ID"); process.exit(1); };
const clientSecret = process.env.CLIENT_SECRET;
if (!clientSecret) { logger.enverr("CLIENT_SECRET"); process.exit(1); };
const authRecord = await getAuthRecord(user, intents);
const token = authRecord ? authRecord.accesstoken : await initAuth(user, clientId, clientSecret, intents, streamer);
const authData = new RefreshingAuthProvider({
clientId,
clientSecret
});
try {
await authData.addUserForToken(token, intents);
} catch (err) {
logger.err(`Failed to setup user auth. Please restart the bot and re-authenticate.`);
await deleteAuthRecord(user);
process.exit(1);
};
authData.onRefresh(async (user, token) => {
logger.ok(`Successfully refreshed auth for user ${user}`);
@@ -82,12 +77,26 @@ export async function createAuthProvider(user: string, intents: string[], stream
logger.err(`Failed to refresh auth for user ${user}: ${err.name} ${err.message}`);
});
try {
await authData.refreshAccessTokenForUser(user);
} catch (err) {
logger.err(`Failed to refresh user ${user}. Please restart the bot and re-authenticate it. Make sure the user that auths the bot and the user that's defined in .env are the same.`);
await deleteAuthRecord(user);
process.exit(1);
for (const user of data) {
const authRecord = await getAuthRecord(user.userId, user.intents);
const token = authRecord ? authRecord.accesstoken : await initAuth(user.userId, clientId, clientSecret, user.intents, user.streamer);
try {
await authData.addUserForToken(token, user.intents);
} catch (err) {
logger.err(`Failed to setup user auth. Please restart the bot and re-authenticate.`);
await deleteAuthRecord(user.userId);
process.exit(1);
};
try {
await authData.refreshAccessTokenForUser(user.userId);
} catch (err) {
logger.err(`Failed to refresh user ${user.userId}. Please restart the bot and re-authenticate it. Make sure the user that auths the bot and the user that's defined in .env are the same.`);
await deleteAuthRecord(user.userId);
process.exit(1);
};
};
return authData;

View File

@@ -3,7 +3,7 @@ import { timeout } from "lib/timeout";
export default new Command({
name: 'fakemodme',
aliases: ['modme'],
aliases: ['modme', 'mod'],
usertype: 'chatter',
execution: async (_msg, user) => {
await Promise.all([

View File

@@ -1,12 +1,12 @@
import { streamerId } from "main";
import { deleteBannedUserMessagesFromChatWidget } from "web/chatWidget/message";
import { eventSub, streamerApi } from "index";
import { eventSub, api } from "index";
import { redis } from "lib/redis";
eventSub.onChannelBan(streamerId, async msg => {
deleteBannedUserMessagesFromChatWidget(msg);
const welcomemessageid = await redis.get(`user:${msg.userId}:welcomemessageid`);
if (welcomemessageid) { await streamerApi.moderation.deleteChatMessages(streamerId, welcomemessageid); await redis.del(`user:${msg.userId}:welcomemessageid`); };
if (welcomemessageid) { await api.moderation.deleteChatMessages(streamerId, welcomemessageid); await redis.del(`user:${msg.userId}:welcomemessageid`); };
await redis.set(`user:${msg.userId}:timeout`, '1');
if (msg.endDate) await redis.expire(`user:${msg.userId}:timeout`, Math.floor((msg.endDate.getTime() - Date.now()) / 1000));
});

View File

@@ -1,7 +1,7 @@
import { eventSub } from "index";
import { streamerId } from "main";
import { chatterId, streamerId } from "main";
import { deleteMessageFromChatWidget } from "web/chatWidget/message";
eventSub.onChannelChatMessageDelete(streamerId, streamerId, async msg => {
eventSub.onChannelChatMessageDelete(streamerId, chatterId, async msg => {
deleteMessageFromChatWidget(msg);
});

View File

@@ -1,99 +0,0 @@
import { streamerId, chatterId } from "main";
import { HelixEventSubSubscription } from "@twurple/api";
import kleur from "kleur";
import logger from "lib/logger";
import { chatterApi, chatterEventSub, eventSub, streamerApi } from "index";
// This file is such a fucking disaster lmaooooo
eventSub.onRevoke(event => {
logger.ok(`Successfully revoked streamer EventSub subscription: ${kleur.underline(event.id)}`);
});
eventSub.onSubscriptionCreateSuccess(event => {
logger.ok(`Successfully created streamer EventSub subscription: ${kleur.underline(event.id)}`);
deleteDuplicateStreamerSubscriptions.refresh();
});
eventSub.onSubscriptionCreateFailure(event => {
logger.err(`Failed to create streamer EventSub subscription: ${kleur.underline(event.id)}`);
});
eventSub.onSubscriptionDeleteSuccess(event => {
logger.ok(`Successfully deleted streamer EventSub subscription: ${kleur.underline(event.id)}`);
});
eventSub.onSubscriptionDeleteFailure(event => {
logger.err(`Failed to delete streamer EventSub subscription: ${kleur.underline(event.id)}`);
});
chatterEventSub.onRevoke(event => {
logger.ok(`Successfully revoked chatter EventSub subscription: ${kleur.underline(event.id)}`);
});
chatterEventSub.onSubscriptionCreateSuccess(event => {
logger.ok(`Successfully created chatter EventSub subscription: ${kleur.underline(event.id)}`);
deleteDuplicateChatterSubscriptions.refresh();
});
chatterEventSub.onSubscriptionCreateFailure(event => {
logger.err(`Failed to create chatter EventSub subscription: ${kleur.underline(event.id)}`);
});
chatterEventSub.onSubscriptionDeleteSuccess(event => {
logger.ok(`Successfully deleted chatter EventSub subscription: ${kleur.underline(event.id)}`);
});
chatterEventSub.onSubscriptionDeleteFailure(event => {
logger.err(`Failed to delete chatter EventSub subscription: ${kleur.underline(event.id)}`);
});
const deleteDuplicateStreamerSubscriptions = setTimeout(async () => {
logger.info('Deleting all double streamer subscriptions');
await streamerApi.asUser(streamerId, async tempapi => {
const subs = await tempapi.eventSub.getSubscriptionsForStatus("enabled");
const seen = new Map();
const duplicates: HelixEventSubSubscription[] = [];
for (const sub of subs.data) {
if (seen.has(sub.type)) {
duplicates.push(sub);
} else {
seen.set(sub.type, sub);
};
};
for (const sub of duplicates) {
await tempapi.eventSub.deleteSubscription(sub.id);
logger.ok(`Deleted streamer sub: id: ${sub.id}, type: ${sub.type}`);
};
if (duplicates.length === 0) logger.ok('No duplicate streamer subscriptions found');
else logger.ok('Deleted all duplicate streamer EventSub subscriptions');
});
}, 5000);
const deleteDuplicateChatterSubscriptions = setTimeout(async () => {
logger.info('Deleting all double chatter subscriptions');
await chatterApi.asUser(chatterId, async tempapi => {
const subs = await tempapi.eventSub.getSubscriptionsForStatus("enabled");
const seen = new Map();
const duplicates: HelixEventSubSubscription[] = [];
for (const sub of subs.data) {
if (seen.has(sub.type)) {
duplicates.push(sub);
} else {
seen.set(sub.type, sub);
};
};
for (const sub of duplicates) {
await tempapi.eventSub.deleteSubscription(sub.id);
logger.ok(`Deleted chatter sub: id: ${sub.id}, type: ${sub.type}`);
};
if (duplicates.length === 0) logger.ok('No duplicate chatter subscriptions found');
else logger.ok('Deleted all duplicate chatter EventSub subscriptions');
});
}, 10000);

View File

@@ -1,4 +1,28 @@
import { eventSub, chatterEventSub } from "index";
import { api, eventSub } from "index";
import kleur from "kleur";
import logger from "lib/logger";
eventSub.onRevoke(event => {
logger.ok(`Successfully revoked EventSub subscription: ${kleur.underline(event.id)}`);
});
eventSub.onSubscriptionCreateSuccess(event => {
logger.ok(`Successfully created EventSub subscription: ${kleur.underline(event.id)}`);
});
eventSub.onSubscriptionCreateFailure(event => {
logger.err(`Failed to create EventSub subscription: ${kleur.underline(event.id)}`);
});
eventSub.onSubscriptionDeleteSuccess(event => {
logger.ok(`Successfully deleted EventSub subscription: ${kleur.underline(event.id)}`);
});
eventSub.onSubscriptionDeleteFailure(event => {
logger.err(`Failed to delete EventSub subscription: ${kleur.underline(event.id)}`);
});
await api.eventSub.deleteAllSubscriptions();
import { readdir } from 'node:fs/promises';
const files = await readdir(import.meta.dir);
@@ -9,4 +33,3 @@ for (const file of files) {
};
eventSub.start();
chatterEventSub.start();

View File

@@ -1,5 +1,5 @@
import { EventSubChannelChatMessageEvent } from "@twurple/eventsub-base"
import { streamerId, commandPrefix, streamerUsers } from "main";
import { streamerId, commandPrefix, streamerUsers, chatterId } from "main";
import User from "user";
import commands, { specialAliasCommands } from "commands";
import { Command, sendMessage } from "lib/commandUtils";
@@ -15,7 +15,7 @@ import { Item } from "items";
import { eventSub } from "index";
import { redis } from "lib/redis";
eventSub.onChannelChatMessage(streamerId, streamerId, parseChatMessage);
eventSub.onChannelChatMessage(streamerId, chatterId, parseChatMessage);
async function parseChatMessage(msg: EventSubChannelChatMessageEvent) {
addMessageToChatWidget(msg);

View File

@@ -1,6 +1,6 @@
import { sendMessage } from "lib/commandUtils";
import { getUserRecord } from "db/dbUser";
import { eventSub, streamerApi } from "index";
import { eventSub, api } from "index";
import { changeItemCount } from "items";
import logger from "lib/logger";
import { streamerId } from "main";
@@ -13,10 +13,9 @@ eventSub.onChannelRaidTo(streamerId, async msg => {
await redis.expire(`user:${msg.raidingBroadcasterId}:recentraid`, 60 * 30); // raid cooldown is 30 minutes
await sendMessage(`Ty for raiding ${msg.raidingBroadcasterDisplayName}. You get 3 pieces of TNT. Enjoy!`);
try {
await streamerApi.chat.shoutoutUser(streamerId, msg.raidingBroadcasterId);
await api.chat.shoutoutUser(streamerId, msg.raidingBroadcasterId);
} catch (e) {
logger.warn(`Failed to give automatic shoutout to ${msg.raidingBroadcasterDisplayName}`);
logger.warn(e as string);
};
const raider = await User.initUsername(msg.raidingBroadcasterName);
const result = await changeItemCount(raider!, await getUserRecord(raider!), 'tnt', 3);

View File

@@ -1,21 +1,36 @@
import { sendMessage } from "lib/commandUtils";
import { chatterApi, chatterEventSub } from "index";
import { api, eventSub } from "index";
import { buildTimeString } from "lib/dateManager";
import { chatterId } from "main";
import { chatterId, commandPrefix } from "main";
import { redis } from "lib/redis";
const WHISPERCOOLDOWN = 60 * 5; // 5 minutes
chatterEventSub.onUserWhisperMessage(chatterId, async msg => {
if (await redis.ttl(`user:${msg.senderUserId}:timeout`) < 0) return;
const cooldown = await redis.expiretime(`user:${msg.senderUserId}:whispercooldown`);
if (cooldown < 0) {
if (msg.messageText.length > 200) { await chatterApi.whispers.sendWhisper(chatterId, msg.senderUserId, `Message too long. Please send a shorter one.`); return; };
await redis.set(`user:${msg.senderUserId}:whispercooldown`, '1');
await redis.expire(`user:${msg.senderUserId}:whispercooldown`, WHISPERCOOLDOWN);
await sendMessage(`The ghost of ${msg.senderUserDisplayName} whispered: ${msg.messageText.replaceAll(/cheer[0-9]+/gi, '')}`);
await chatterApi.whispers.sendWhisper(chatterId, msg.senderUserId, `Message sent. You can send another ghost whisper in ${Math.floor(WHISPERCOOLDOWN / 60)} minutes.`);
} else {
await chatterApi.whispers.sendWhisper(chatterId, msg.senderUserId, `Wait another ${buildTimeString(cooldown * 1000, Date.now())} before sending another ghost whisper.`);
eventSub.onUserWhisperMessage(chatterId, async msg => {
if (!msg.messageText.startsWith(commandPrefix)) { await whisper(msg.senderUserId, `Whisper commands start with '${commandPrefix}'. All whisper commands can be found here: https://github.com/qwerinope/qweribot#whisper-commands-1`); return; };
const cmd = msg.messageText.slice(commandPrefix.length).trim().toLowerCase().split(' ')[0]!;
switch (cmd) {
case 'help':
case 'h':
await whisper(msg.senderUserId, `All whisper commands can be found here: https://github.com/qwerinope/qweribot#whisper-commands-1`);
break;
case 'ghostwhisper':
case 'ghost':
case 'g':
if (await redis.ttl(`user:${msg.senderUserId}:timeout`) < 0) { await whisper(msg.senderUserId, 'Cannot send ghost whisper while not timed out'); return; };
const cooldown = await redis.expiretime(`user:${msg.senderUserId}:whispercooldown`);
if (cooldown < 0) {
if (msg.messageText.length > 200) { await whisper(msg.senderUserId, `Message too long. Please send a shorter one.`); return; };
await redis.set(`user:${msg.senderUserId}:whispercooldown`, '1');
await redis.expire(`user:${msg.senderUserId}:whispercooldown`, WHISPERCOOLDOWN);
await sendMessage(`The ghost of ${msg.senderUserDisplayName} whispered: ${msg.messageText.split(' ').slice(1).join(' ').replaceAll(/cheer[0-9]+/gi, '')}`);
await whisper(msg.senderUserId, `Message sent. You can send another ghost whisper in ${Math.floor(WHISPERCOOLDOWN / 60)} minutes.`);
} else {
await whisper(msg.senderUserId, `Wait another ${buildTimeString(cooldown * 1000, Date.now())} before sending another ghost whisper.`);
};
break;
};
});
const whisper = async (target: string, message: string) => await api.whispers.sendWhisper(chatterId, target, message);

View File

@@ -3,28 +3,10 @@ import { ApiClient } from "@twurple/api";
import { connectionCheck } from "connectionCheck";
import logger from "lib/logger";
import { redis } from "lib/redis";
import { createAuthProvider } from "auth";
import { EventSubWsListener } from "@twurple/eventsub-ws";
import { createAuthProvider, type authProviderInstructions } from "auth";
import { user, password, database, host } from "db/connection";
await connectionCheck();
const CHATTERINTENTS = ["user:read:chat", "user:write:chat", "user:bot", "user:manage:whispers"];
const STREAMERINTENTS = ["channel:bot", "user:read:chat", "moderation:read", "channel:manage:moderators", "moderator:manage:chat_messages", "moderator:manage:banned_users", "bits:read", "channel:moderate", "moderator:manage:shoutouts", "channel:read:subscriptions", "channel:manage:redemptions"];
export const chatterAuthProvider = await createAuthProvider(chatterId, singleUserMode ? CHATTERINTENTS.concat(STREAMERINTENTS) : CHATTERINTENTS);
export const streamerAuthProvider = singleUserMode ? undefined : await createAuthProvider(streamerId, STREAMERINTENTS, true);
/** chatterApi should be used for sending messages, retrieving user data, etc */
export const chatterApi = new ApiClient({ authProvider: chatterAuthProvider });
/** streamerApi should be used for: adding/removing mods, managing timeouts, etc. */
export const streamerApi = streamerAuthProvider ? new ApiClient({ authProvider: streamerAuthProvider }) : chatterApi; // if there is no streamer user, use the chatter user
/** As the streamerApi has either the streamer or the chatter if the chatter IS the streamer this has streamer permissions */
export const eventSub = new EventSubWsListener({ apiClient: streamerApi });
export const chatterEventSub = singleUserMode ? eventSub : new EventSubWsListener({ apiClient: chatterApi });
import { ConnectionAdapter, EventSubHttpListener, ReverseProxyAdapter } from "@twurple/eventsub-http";
import { NgrokAdapter } from "@twurple/eventsub-ngrok";
if (chatterId === "") { logger.enverr('CHATTER_ID'); process.exit(1); };
if (streamerId === "") { logger.enverr('STREAMER_ID'); process.exit(1); };
@@ -33,6 +15,59 @@ if (!password) { logger.enverr("POSTGRES_USER"); process.exit(1); };
if (!database) { logger.enverr("POSTGRES_DB"); process.exit(1); };
if (!host) { logger.enverr("POSTGRES_HOST"); process.exit(1); };
const eventSubHostName = process.env.EVENTSUB_HOSTNAME ?? (() => {
logger.enverr('EVENTSUB_HOSTNAME');
process.exit(1);
})();
const eventSubPort = process.env.EVENTSUB_PORT ?? (() => {
logger.enverr('EVENTSUB_PORT');
process.exit(1);
})();
const eventSubSecret = process.env.EVENTSUB_SECRET ?? (() => {
logger.enverr('EVENTSUB_SECRET');
process.exit(1);
})();
const eventSubPath = process.env.EVENTSUB_PATH;
await connectionCheck();
const CHATTERINTENTS = ["user:read:chat", "user:write:chat", "user:bot", "user:manage:whispers"];
const STREAMERINTENTS = ["channel:bot", "user:read:chat", "moderation:read", "channel:manage:moderators", "moderator:manage:chat_messages", "moderator:manage:banned_users", "bits:read", "channel:moderate", "moderator:manage:shoutouts", "channel:read:subscriptions", "channel:manage:redemptions"];
const users: authProviderInstructions[] = [
{
userId: streamerId,
intents: singleUserMode ? CHATTERINTENTS.concat(STREAMERINTENTS) : STREAMERINTENTS,
streamer: true
}
];
if (!singleUserMode) users.push({
userId: chatterId,
intents: CHATTERINTENTS,
streamer: false
});
const adapter: ConnectionAdapter = process.env.NODE_ENV === 'development' ? new NgrokAdapter({
ngrokConfig: {
authtoken: process.env.EVENTSUB_NGROK_TOKEN ?? (() => {
logger.enverr('EVENTSUB_NGROK_TOKEN');
process.exit(1);
})()
}
}) : new ReverseProxyAdapter({
pathPrefix: eventSubPath, hostName: eventSubHostName, port: parseInt(eventSubPort)
});
const authProvider = await createAuthProvider(users);
export const api = new ApiClient({ authProvider });
export const eventSub = new EventSubHttpListener({ apiClient: api, secret: eventSubSecret, adapter });
if (!singleUserMode) await redis.set(`user:${chatterId}:bot`, '1');
import { addAdmin } from "lib/admins";
@@ -42,7 +77,7 @@ import { remodMod, timeoutDuration } from "lib/timeout";
streamerUsers.forEach(async id => await Promise.all([addAdmin(id), addInvuln(id), redis.set(`user:${id}:mod`, '1')]));
const banned = await streamerApi.moderation.getBannedUsers(streamerId).then(a => a.data);
const banned = await api.moderation.getBannedUsers(streamerId).then(a => a.data);
for (const ban of banned) {
await redis.set(`user:${ban.userId}:timeout`, '1');
const banlength = ban.expiryDate;
@@ -52,7 +87,7 @@ for (const ban of banned) {
};
};
const mods = await streamerApi.moderation.getModerators(streamerId).then(a => a.data);
const mods = await api.moderation.getModerators(streamerId).then(a => a.data);
for (const mod of mods) {
await redis.set(`user:${mod.userId}:mod`, '1');
logger.info(`Set the mod status of ${mod.userDisplayName} in the Redis/Valkey database.`);
@@ -68,7 +103,7 @@ for (const remod of bannedmods) {
logger.info(`Set the remod timer for ${target?.displayName} to ${duration} seconds.`);
};
const subs = await streamerApi.subscriptions.getSubscriptions(streamerId).then(a => a.data);
const subs = await api.subscriptions.getSubscriptions(streamerId).then(a => a.data);
const redisSubs = await redis.keys('user:*:subbed').then(a => a.map(b => b.slice(5, -7)));
for (const sub of subs) {
if (redisSubs.includes(sub.userId)) {
@@ -81,7 +116,7 @@ for (const sub of subs) {
redisSubs.map(async a => await redis.del(`user:${a}:subbed`));
const streamdata = await streamerApi.streams.getStreamByUserId(streamerId);
const streamdata = await api.streams.getStreamByUserId(streamerId);
if (streamdata) await redis.set('streamIsLive', '1');
await import("./events");

View File

@@ -1,6 +1,7 @@
import { EventSubChannelChatMessageEvent } from "@twurple/eventsub-base";
import User from "user";
import { chatterId, streamerId } from "main";
import { api } from "index";
export type userType = 'chatter' | 'admin' | 'streamer' | 'moderator';
@@ -37,10 +38,9 @@ export class Command {
/** Helper function to send a message to the stream */
export const sendMessage = async (message: string, replyParentMessageId?: string) => {
const chatterApi = await import("index").then(m => m.chatterApi);
try {
return await chatterApi.chat.sendChatMessageAsApp(chatterId, streamerId, message, { replyParentMessageId });
return await api.chat.sendChatMessageAsApp(chatterId, streamerId, message, { replyParentMessageId });
} catch (e) {
return await chatterApi.chat.sendChatMessageAsApp(chatterId, streamerId, message);
return await api.chat.sendChatMessageAsApp(chatterId, streamerId, message);
};
};
};

View File

@@ -2,7 +2,7 @@ import { streamerId } from "main";
import logger from "lib/logger";
import User from "user";
import { isInvuln } from "lib/invuln";
import { streamerApi } from "index";
import { api } from "index";
import { redis } from "lib/redis";
type SuccessfulTimeout = { status: true; };
@@ -28,11 +28,11 @@ export const timeout = async (user: User, reason: string, duration?: number): Pr
if (!duration) duration = 60; // make sure that mods don't get perma-banned
await redis.set(`user:${user.id}:remod`, '1');
remodMod(user, duration);
await streamerApi.moderation.removeModerator(streamerId, user.id!);
await api.moderation.removeModerator(streamerId, user.id!);
};
try {
await streamerApi.moderation.banUser(streamerId, { user: user.id, reason, duration });
await api.moderation.banUser(streamerId, { user: user.id, reason, duration });
} catch (err) {
logger.err(err as string);
return { status: false, reason: 'unknown' };
@@ -54,7 +54,7 @@ export function remodMod(target: User, duration: number) {
remodMod(target, timeoutleft); // Call the current function with new time (recursion)
} else {
try {
await streamerApi.moderation.addModerator(streamerId, target.id);
await api.moderation.addModerator(streamerId, target.id);
await redis.del(`user:${target.id}:remod`);
} catch (err) { }; // This triggers when the timeout got shortened. try/catch so no runtime error
};

View File

@@ -56,10 +56,10 @@ const idMap = new Map<string, string>;
import { streamerId } from "main";
import logger from "lib/logger";
import { streamerApi } from "index";
import { api } from "index";
const currentRedeems = new Map<string, string>;
await streamerApi.channelPoints.getCustomRewards(streamerId).then(a => a.map(b => currentRedeems.set(b.title, b.id)));
await api.channelPoints.getCustomRewards(streamerId).then(a => a.map(b => currentRedeems.set(b.title, b.id)));
for (const [_, redeem] of Array.from(namedRedeems)) {
const selection = currentRedeems.get(redeem.title);
if (selection) {
@@ -68,7 +68,7 @@ for (const [_, redeem] of Array.from(namedRedeems)) {
activeRedeems.set(selection, redeem);
} else {
if (process.env.NODE_ENV !== 'production') continue; // If created with dev-app we won't be able to change it with prod app
const creation = await streamerApi.channelPoints.createCustomReward(streamerId, {
const creation = await api.channelPoints.createCustomReward(streamerId, {
title: redeem.title,
prompt: redeem.prompt,
cost: redeem.cost,
@@ -82,14 +82,14 @@ for (const [_, redeem] of Array.from(namedRedeems)) {
Array.from(currentRedeems).map(async ([title, redeem]) => {
if (process.env.NODE_ENV !== 'production') return;
await streamerApi.channelPoints.deleteCustomReward(streamerId, redeem); logger.ok(`Deleted custom point redeem ${title}`);
await api.channelPoints.deleteCustomReward(streamerId, redeem); logger.ok(`Deleted custom point redeem ${title}`);
});
logger.ok("Successfully synced all custom point redeems");
export async function enableRedeem(redeem: PointRedeem, id: string) {
if (process.env.NODE_ENV !== 'production') return;
await streamerApi.channelPoints.updateCustomReward(streamerId, id, {
await api.channelPoints.updateCustomReward(streamerId, id, {
isEnabled: true
});
activeRedeems.set(id, redeem);
@@ -98,7 +98,7 @@ export async function enableRedeem(redeem: PointRedeem, id: string) {
export async function disableRedeem(redeem: PointRedeem, id: string) {
if (process.env.NODE_ENV !== 'production') return;
await streamerApi.channelPoints.updateCustomReward(streamerId, id, {
await api.channelPoints.updateCustomReward(streamerId, id, {
isEnabled: false
});
activeRedeems.delete(id);

View File

@@ -1,5 +1,5 @@
import { redis } from "lib/redis";
import { chatterApi } from "index";
import { api } from "index";
import { HelixUser } from "@twurple/api"
import logger from "lib/logger";
@@ -27,7 +27,7 @@ export default class User {
userObj.username = username;
const userid = await redis.get(`userlookup:${username}`);
if (!userid) {
const userdata = await chatterApi.users.getUserByName(username);
const userdata = await api.users.getUserByName(username);
if (!userdata) return null;
userObj._setCache(userdata);
userObj.id = userdata.id;
@@ -50,7 +50,7 @@ export default class User {
const userObj = new User();
userObj.id = userId;
if (!await redis.exists(`user:${userId}:displayName`)) {
const userdata = await chatterApi.users.getUserById(userId);
const userdata = await api.users.getUserById(userId);
if (!userdata) return null;
userObj._setCache(userdata);
userObj.username = userdata.name;

View File

@@ -1,5 +1,5 @@
import { streamerId } from "main";
import { chatterApi } from "index";
import { api } from "index";
import { redis } from "lib/redis";
type badgeObject = {
@@ -16,8 +16,8 @@ export async function getBadges() {
const redisdata = await redis.get('chatwidget:badges');
if (redisdata) return new Response(redisdata);
const globalBadges = chatterApi.chat.getGlobalBadges();
const channelBadges = chatterApi.chat.getChannelBadges(streamerId);
const globalBadges = api.chat.getGlobalBadges();
const channelBadges = api.chat.getChannelBadges(streamerId);
const rawBadges = await Promise.all([globalBadges, channelBadges]);
const newObj: badgeObject = {};

View File

@@ -27,10 +27,10 @@ export default Bun.serve({
if (req.headers.get('Upgrade') === "websocket") { srv.upgrade(req); return; };
const streamer = await User.initUserId(streamerId);
return Response.redirect(`https://twitch.tv/${streamer?.username}`);
}
},
},
websocket: {
message(ws, omessage) {
async message(ws, omessage) {
try {
const message = JSON.parse(omessage.toString()) as serverInstruction;
if (!message.type) return;
@@ -42,7 +42,7 @@ export default Bun.serve({
ws.send(JSON.stringify({
function: 'serverNotification',
message: `Successfully subscribed to ${target} events`
} as serverNotificationEvent)); // Both alerts and chatwidget eventsub subscriptions have the notification field
} as serverNotificationEvent)); // Both alerts and chatwidget subscriptions have the notification field
break;
};
} catch (e) {
@@ -56,7 +56,7 @@ export default Bun.serve({
},
development: process.env.NODE_ENV === "development",
error(error) {
logger.err(`Error at chatwidget server: ${error}`);
logger.err(`Error at fullstack web server: ${error}`);
return new Response("Internal Server Error", { status: 500 })
},
});