🚨 Fix most of the linting error and warnings
This commit is contained in:
@@ -1,22 +1,28 @@
|
|||||||
|
import { hasVip, say } from "..";
|
||||||
|
|
||||||
import { chatClient } from "../..";
|
import { chatClient } from "../..";
|
||||||
import { say } from "..";
|
|
||||||
|
|
||||||
async function addVip(
|
async function addVip(
|
||||||
channel: string,
|
channel: string,
|
||||||
username: string,
|
username: string,
|
||||||
message?: string
|
message?: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
username = username.toLowerCase();
|
||||||
await chatClient.addVip(channel, username);
|
if (!(await hasVip(channel, username))) {
|
||||||
} catch (e) {
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (message) {
|
try {
|
||||||
await say(channel, message);
|
await chatClient.addVip(channel, username);
|
||||||
}
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
if (message) {
|
||||||
|
await say(channel, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { addVip };
|
export { addVip };
|
||||||
|
@@ -2,23 +2,54 @@ import { chatClient } from "../..";
|
|||||||
|
|
||||||
type CacheType = Record<string, Array<string>>;
|
type CacheType = Record<string, Array<string>>;
|
||||||
const cache: CacheType = {};
|
const cache: CacheType = {};
|
||||||
|
const cacheKeepTime = 2.5e3;
|
||||||
|
|
||||||
|
interface ChannelFetching {
|
||||||
|
channel: string;
|
||||||
|
promise: Promise<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const channelsFetching: Array<ChannelFetching> = [];
|
||||||
|
|
||||||
|
async function fetchVips(channel: string): Promise<void> {
|
||||||
|
const alreadyChecking = channelsFetching.find((c) => c.channel === channel);
|
||||||
|
|
||||||
|
if (alreadyChecking) {
|
||||||
|
await alreadyChecking.promise;
|
||||||
|
} else {
|
||||||
|
const promise = new Promise<void>((res) => {
|
||||||
|
chatClient.getVips(channel).then((vips) => {
|
||||||
|
cache[channel] = vips.map((u) => u.toLowerCase());
|
||||||
|
|
||||||
|
res();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
channelsFetching.push({ channel, promise });
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-magic-numbers
|
||||||
|
const addedIdx = channelsFetching.length - 1;
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
channelsFetching.splice(addedIdx);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
delete cache[channel];
|
||||||
|
}, cacheKeepTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function hasVip(channel: string, username: string): Promise<boolean> {
|
async function hasVip(channel: string, username: string): Promise<boolean> {
|
||||||
if (!username) {
|
username = username.toLowerCase();
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cache[channel]) {
|
if (!cache[channel]) {
|
||||||
cache[channel] = await chatClient.getVips(channel);
|
await fetchVips(channel);
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
const vips = cache[channel];
|
||||||
delete cache[channel];
|
|
||||||
}, 2500);
|
|
||||||
}
|
|
||||||
|
|
||||||
const vips = cache[channel];
|
return vips.includes(username);
|
||||||
|
|
||||||
return vips.includes(username);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { hasVip };
|
export { hasVip };
|
||||||
|
@@ -1,7 +1,18 @@
|
|||||||
|
import { say, sayError, sayInfo, saySuccess, sayWarn } from "./say";
|
||||||
|
|
||||||
import { addVip } from "./addVip";
|
import { addVip } from "./addVip";
|
||||||
import { hasVip } from "./hasVip";
|
import { hasVip } from "./hasVip";
|
||||||
import { removeVip } from "./removeVip";
|
import { removeVip } from "./removeVip";
|
||||||
import { say } from "./say";
|
|
||||||
import { timeout } from "./timeout";
|
import { timeout } from "./timeout";
|
||||||
|
|
||||||
export { say, timeout, addVip, removeVip, hasVip };
|
export {
|
||||||
|
addVip,
|
||||||
|
hasVip,
|
||||||
|
removeVip,
|
||||||
|
say,
|
||||||
|
sayError,
|
||||||
|
sayInfo,
|
||||||
|
saySuccess,
|
||||||
|
sayWarn,
|
||||||
|
timeout,
|
||||||
|
};
|
||||||
|
@@ -1,22 +1,29 @@
|
|||||||
|
import { hasVip, say } from "..";
|
||||||
|
|
||||||
import { chatClient } from "../..";
|
import { chatClient } from "../..";
|
||||||
import { say } from "..";
|
|
||||||
|
|
||||||
async function removeVip(
|
async function removeVip(
|
||||||
channel: string,
|
channel: string,
|
||||||
username: string,
|
username: string,
|
||||||
message?: string
|
message?: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
username = username.toLowerCase();
|
||||||
await chatClient.removeVip(channel, username);
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message) {
|
if (await hasVip(channel, username)) {
|
||||||
await say(channel, message);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
try {
|
||||||
|
await chatClient.removeVip(channel, username);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
await say(channel, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { removeVip };
|
export { removeVip };
|
||||||
|
@@ -1,19 +1,36 @@
|
|||||||
import { chatClient } from "../..";
|
import { chatClient } from "../..";
|
||||||
|
|
||||||
const maxMessageLength = 500;
|
|
||||||
|
|
||||||
async function say(channel: string, message: string): Promise<void> {
|
async function say(channel: string, message: string): Promise<void> {
|
||||||
// message = `MrDestructoid ${message}`;
|
const maxMessageLength = 500;
|
||||||
|
// message = `MrDestructoid ${message}`;
|
||||||
|
|
||||||
if (message.length > 500) {
|
if (message.length > maxMessageLength) {
|
||||||
const suffix = "...";
|
const startIndex = 0;
|
||||||
message = `${message.substring(
|
const suffix = "...";
|
||||||
0,
|
|
||||||
maxMessageLength - suffix.length
|
|
||||||
)}${suffix}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
await chatClient.say(channel, message);
|
message = `${message.substring(
|
||||||
|
startIndex,
|
||||||
|
maxMessageLength - suffix.length
|
||||||
|
)}${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await chatClient.say(channel, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { say };
|
async function sayError(channel: string, message: string): Promise<void> {
|
||||||
|
await say(channel, `[ERROR] ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sayWarn(channel: string, message: string): Promise<void> {
|
||||||
|
await say(channel, `[WARN] ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sayInfo(channel: string, message: string) {
|
||||||
|
await say(channel, `[INFO] ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saySuccess(channel: string, message: string): Promise<void> {
|
||||||
|
await say(channel, `[SUCCESS] ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { say, sayError, sayWarn, sayInfo, saySuccess };
|
||||||
|
@@ -1,21 +1,22 @@
|
|||||||
import { chatClient } from "../..";
|
import { chatClient } from "../..";
|
||||||
|
|
||||||
// timeouts a user in a channel
|
const defaultTime = 60;
|
||||||
|
|
||||||
async function timeout(
|
async function timeout(
|
||||||
channel: string,
|
channel: string,
|
||||||
username: string,
|
username: string,
|
||||||
time?: number,
|
time?: number,
|
||||||
reason?: string
|
reason?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!time) {
|
if (!time) {
|
||||||
time = 60;
|
time = defaultTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!reason) {
|
if (!reason) {
|
||||||
reason = "";
|
reason = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
await chatClient.timeout(channel, username, time, reason);
|
await chatClient.timeout(channel, username, time, reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { timeout };
|
export { timeout };
|
||||||
|
@@ -1,42 +1,42 @@
|
|||||||
import { LOG_PREFIX, say } from "..";
|
import { sayError, saySuccess } from "../clientActions";
|
||||||
|
|
||||||
|
import { LOG_PREFIX } from "..";
|
||||||
import { TwitchPrivateMessage } from "@twurple/chat/lib/commands/TwitchPrivateMessage";
|
import { TwitchPrivateMessage } from "@twurple/chat/lib/commands/TwitchPrivateMessage";
|
||||||
import { createReward as createChannelPointsReward } from "../../helpers/twitch";
|
import { createReward as createChannelPointsReward } from "../../helpers/twitch";
|
||||||
|
|
||||||
async function createReward(
|
async function createReward(
|
||||||
channel: string,
|
channel: string,
|
||||||
user: string,
|
_user: string,
|
||||||
message: string,
|
message: string,
|
||||||
msg: TwitchPrivateMessage
|
msg: TwitchPrivateMessage
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const args = message.split(" ");
|
const args = message.split(" ");
|
||||||
|
|
||||||
const title = args.shift();
|
const title = args.shift();
|
||||||
const cost = Math.max(1, parseInt(args.shift() ?? "0"));
|
|
||||||
|
|
||||||
if (!title || !cost) {
|
if (!title) {
|
||||||
await say(
|
await sayError(channel, "Debes indicar el título de la recompensa");
|
||||||
channel,
|
return;
|
||||||
"No se ha especificado el nombre de la recompensa o costo"
|
}
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
const minRewardPrice = 1;
|
||||||
await createChannelPointsReward(msg.channelId as string, {
|
const cost = Math.max(minRewardPrice, parseInt(args.shift() ?? "0"));
|
||||||
title,
|
|
||||||
cost
|
|
||||||
});
|
|
||||||
|
|
||||||
say(
|
try {
|
||||||
channel,
|
await createChannelPointsReward(msg.channelId as string, {
|
||||||
`✅ Creada recompensa de canal "${title}" con un costo de ${cost}`
|
title,
|
||||||
);
|
cost,
|
||||||
} catch (e) {
|
});
|
||||||
if (e instanceof Error) {
|
|
||||||
console.log(`${LOG_PREFIX}${e.message}`);
|
saySuccess(
|
||||||
}
|
channel,
|
||||||
}
|
`Creada recompensa de canal "${title}" con un costo de ${cost}`
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
console.log(`${LOG_PREFIX}${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { createReward };
|
export { createReward };
|
||||||
|
@@ -12,107 +12,115 @@ import { start } from "../helpers/miniDb";
|
|||||||
|
|
||||||
let chatClient: ChatClient;
|
let chatClient: ChatClient;
|
||||||
|
|
||||||
export { chatClient, connect, handleClientAction, say, LOG_PREFIX };
|
|
||||||
|
|
||||||
const LOG_PREFIX = "[ChatClient] ";
|
const LOG_PREFIX = "[ChatClient] ";
|
||||||
|
|
||||||
async function connect(channels: Array<any>): Promise<void> {
|
function onConnect(): Promise<void> {
|
||||||
const authProvider = await getAuthProvider();
|
console.log(`${LOG_PREFIX}Connected`);
|
||||||
|
|
||||||
if (chatClient && (chatClient.isConnecting || chatClient.isConnected)) {
|
start();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
chatClient = new ChatClient({
|
return Promise.resolve();
|
||||||
authProvider,
|
|
||||||
channels
|
|
||||||
});
|
|
||||||
|
|
||||||
chatClient.onConnect(onConnect);
|
|
||||||
|
|
||||||
chatClient.onDisconnect((e: any) => {
|
|
||||||
console.log(`${LOG_PREFIX}Disconnected ${e.message}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
chatClient.onNoPermission((channel, message) => {
|
|
||||||
console.log(`${LOG_PREFIX}No permission on ${channel}: ${message}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
chatClient.onMessage(onMessage);
|
|
||||||
|
|
||||||
await chatClient.connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onConnect(): Promise<void> {
|
|
||||||
console.log(`${LOG_PREFIX}Connected`);
|
|
||||||
|
|
||||||
start();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleClientAction(action: Action): Promise<void> {
|
|
||||||
const [channel, username] = await Promise.all([
|
|
||||||
getUsernameFromId(parseInt(action.channelId)),
|
|
||||||
getUsernameFromId(parseInt(action.userId))
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!channel || !username) {
|
|
||||||
console.log(`${[LOG_PREFIX]}ChannelId or userId could not be solved`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (action.type) {
|
|
||||||
case ActionType.Say:
|
|
||||||
say(channel, action.data.message);
|
|
||||||
break;
|
|
||||||
case ActionType.Timeout:
|
|
||||||
try {
|
|
||||||
await timeout(channel, username, action.data.time, action.data.reason);
|
|
||||||
} catch (e) {
|
|
||||||
// user cannot be timed out
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case ActionType.Broadcast:
|
|
||||||
broadcast(action.data.message);
|
|
||||||
break;
|
|
||||||
case ActionType.AddVip:
|
|
||||||
await addVip(channel, username, `Otorgado VIP a @${username}`);
|
|
||||||
break;
|
|
||||||
case ActionType.RemoveVip:
|
|
||||||
await removeVip(channel, username, `Eliminado VIP de @${username}`);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.log(`${[LOG_PREFIX]}Couldn't handle action:`, action);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const commandPrefix = "!";
|
const commandPrefix = "!";
|
||||||
|
|
||||||
async function onMessage(
|
async function onMessage(
|
||||||
channel: string,
|
channel: string,
|
||||||
user: string,
|
user: string,
|
||||||
message: string,
|
message: string,
|
||||||
msg: TwitchPrivateMessage
|
msg: TwitchPrivateMessage
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (msg.userInfo.isBroadcaster && message.startsWith(commandPrefix)) {
|
if (msg.userInfo.isBroadcaster && message.startsWith(commandPrefix)) {
|
||||||
message = message.substring(commandPrefix.length);
|
message = message.substring(commandPrefix.length);
|
||||||
|
|
||||||
const args = message.split(" ");
|
const args = message.split(" ");
|
||||||
const commandName = args.shift();
|
const commandName = args.shift();
|
||||||
|
|
||||||
switch (commandName) {
|
switch (commandName) {
|
||||||
case ChatCommands.Commands:
|
case ChatCommands.Commands:
|
||||||
await say(
|
await say(
|
||||||
channel,
|
channel,
|
||||||
`Comandos disponibles: "${Object.values(ChatCommands).join('", "')}"`
|
`Comandos disponibles: "${Object.values(ChatCommands).join('", "')}"`
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case ChatCommands.CreateReward:
|
case ChatCommands.CreateReward:
|
||||||
await createReward(channel, user, args.join(" "), msg);
|
await createReward(channel, user, args.join(" "), msg);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log(
|
console.log(
|
||||||
`${LOG_PREFIX}Command ${commandPrefix}${commandName} not handled`
|
`${LOG_PREFIX}Command ${commandPrefix}${commandName} not handled`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function connect(channels: Array<string>): Promise<void> {
|
||||||
|
const authProvider = await getAuthProvider();
|
||||||
|
|
||||||
|
if (chatClient && (chatClient.isConnecting || chatClient.isConnected)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
chatClient = new ChatClient({
|
||||||
|
authProvider,
|
||||||
|
channels,
|
||||||
|
webSocket: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
chatClient.onConnect(onConnect);
|
||||||
|
|
||||||
|
chatClient.onDisconnect((manually, reason) => {
|
||||||
|
if (manually) {
|
||||||
|
console.log(`${LOG_PREFIX}Disconnected manually`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${LOG_PREFIX}Disconnected ${reason}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
chatClient.onNoPermission((channel, message) => {
|
||||||
|
console.log(`${LOG_PREFIX}No permission on ${channel}: ${message}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
chatClient.onMessage(onMessage);
|
||||||
|
|
||||||
|
await chatClient.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleClientAction(action: Action): Promise<void> {
|
||||||
|
const [channel, username] = await Promise.all([
|
||||||
|
getUsernameFromId(parseInt(action.channelId)),
|
||||||
|
getUsernameFromId(parseInt(action.userId)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!channel || !username) {
|
||||||
|
console.log(`${[LOG_PREFIX]}ChannelId or userId could not be solved`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionType.Say:
|
||||||
|
say(channel, action.data.message);
|
||||||
|
break;
|
||||||
|
case ActionType.Timeout:
|
||||||
|
try {
|
||||||
|
await timeout(channel, username, action.data.time, action.data.reason);
|
||||||
|
} catch (e) {
|
||||||
|
// user cannot be timed out
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ActionType.Broadcast:
|
||||||
|
broadcast(action.data.message);
|
||||||
|
break;
|
||||||
|
case ActionType.AddVip:
|
||||||
|
await addVip(channel, username, `Otorgado VIP a @${username}`);
|
||||||
|
break;
|
||||||
|
case ActionType.RemoveVip:
|
||||||
|
await removeVip(channel, username, `Eliminado VIP de @${username}`);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log(`${[LOG_PREFIX]}Couldn't handle action:`, action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { chatClient, connect, handleClientAction, say, LOG_PREFIX };
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { Action } from "../../interfaces/actions/Action";
|
||||||
import { promises as fs } from "fs";
|
import { promises as fs } from "fs";
|
||||||
import { handleClientAction } from "../chatClient";
|
import { handleClientAction } from "../chatClient";
|
||||||
import { resolve } from "path";
|
import { resolve } from "path";
|
||||||
@@ -12,104 +13,124 @@ const FILES_BASE = resolve(process.cwd(), "./storage");
|
|||||||
const SCHEDULED_FILE = resolve(FILES_BASE, "./scheduled.json");
|
const SCHEDULED_FILE = resolve(FILES_BASE, "./scheduled.json");
|
||||||
const VIP_USERS_FILE = resolve(FILES_BASE, "./vips.json");
|
const VIP_USERS_FILE = resolve(FILES_BASE, "./vips.json");
|
||||||
|
|
||||||
const scheduledActions: Array<any> = [];
|
const defaultScheduledTimestamp = 0;
|
||||||
|
|
||||||
|
let scheduledActions: Array<Action> = [];
|
||||||
|
|
||||||
const vipUsers: Record<string, Array<string>> = {};
|
const vipUsers: Record<string, Array<string>> = {};
|
||||||
|
|
||||||
let checkingScheduled = false;
|
let checkingScheduled = false;
|
||||||
let scheduledActionsInterval: NodeJS.Timeout;
|
let scheduledActionsInterval: NodeJS.Timeout;
|
||||||
let saveScheduledActionsTimeout: NodeJS.Timeout | null;
|
let saveScheduledActionsTimeout: NodeJS.Timeout | null;
|
||||||
|
|
||||||
// *Check this, not working
|
function save(): void {
|
||||||
async function start(): Promise<void> {
|
if (saveScheduledActionsTimeout) {
|
||||||
if (scheduledActionsInterval) {
|
clearTimeout(saveScheduledActionsTimeout);
|
||||||
return;
|
saveScheduledActionsTimeout = null;
|
||||||
}
|
console.log(`${LOG_PREFIX}Removed save timeout.`);
|
||||||
|
}
|
||||||
|
|
||||||
let savedActions = [];
|
saveScheduledActionsTimeout = setTimeout(async () => {
|
||||||
try {
|
await Promise.all([
|
||||||
savedActions = JSON.parse((await fs.readFile(SCHEDULED_FILE)).toString());
|
fs.writeFile(SCHEDULED_FILE, JSON.stringify(scheduledActions)),
|
||||||
} catch (e) {
|
fs.writeFile(VIP_USERS_FILE, JSON.stringify(vipUsers)),
|
||||||
// probably file does not exist
|
]);
|
||||||
if (e instanceof Error) {
|
|
||||||
console.log(`${LOG_PREFIX}${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scheduledActions.push.apply(scheduledActions, savedActions);
|
console.log(`${LOG_PREFIX}Saved actions.`);
|
||||||
scheduledActions.sort((a, b) => a.scheduledAt - b.scheduledAt);
|
saveScheduledActionsTimeout = null;
|
||||||
|
}, SAVE_TIMEOUT);
|
||||||
setTimeout(checkScheduledActions, FIRST_CHECK_TIMEOUT);
|
|
||||||
scheduledActionsInterval = setInterval(checkScheduledActions, CHECK_INTERVAL);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const savedVipUsers = JSON.parse(
|
|
||||||
await (await fs.readFile(VIP_USERS_FILE)).toString()
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const key of Object.keys(savedVipUsers)) {
|
|
||||||
vipUsers[key] = savedVipUsers[key];
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// probably file does not exist
|
|
||||||
if (e instanceof Error) {
|
|
||||||
console.log(`${LOG_PREFIX}${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkScheduledActions(): Promise<void> {
|
async function checkScheduledActions(): Promise<void> {
|
||||||
if (checkingScheduled) {
|
if (checkingScheduled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkingScheduled = true;
|
checkingScheduled = true;
|
||||||
|
|
||||||
let hasToSave = false;
|
let hasToSave = false;
|
||||||
|
|
||||||
for (
|
for (
|
||||||
let i = 0;
|
let i = 0;
|
||||||
i < scheduledActions.length &&
|
i < scheduledActions.length &&
|
||||||
scheduledActions[i].scheduledAt <= Date.now();
|
(scheduledActions[i].scheduledAt ?? defaultScheduledTimestamp) <=
|
||||||
i++
|
Date.now();
|
||||||
) {
|
i++
|
||||||
hasToSave = true;
|
) {
|
||||||
|
hasToSave = true;
|
||||||
|
|
||||||
const action = scheduledActions.splice(i, 1)[0];
|
const deleteCount = 1;
|
||||||
await handleClientAction(action);
|
const deletedActions = scheduledActions.splice(i, deleteCount);
|
||||||
console.log(`${LOG_PREFIX}Executed: ${JSON.stringify(action)}`);
|
const action = deletedActions.shift();
|
||||||
}
|
|
||||||
|
|
||||||
if (hasToSave) {
|
if (action) {
|
||||||
save();
|
await handleClientAction(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkingScheduled = false;
|
console.log(`${LOG_PREFIX}Executed: ${JSON.stringify(action)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasToSave) {
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
checkingScheduled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function start(): Promise<void> {
|
||||||
|
if (scheduledActionsInterval) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let savedActions = [];
|
||||||
|
try {
|
||||||
|
savedActions = JSON.parse((await fs.readFile(SCHEDULED_FILE)).toString());
|
||||||
|
} catch (e) {
|
||||||
|
// probably file does not exist
|
||||||
|
if (e instanceof Error) {
|
||||||
|
console.log(`${LOG_PREFIX}${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduledActions = [...scheduledActions, ...savedActions];
|
||||||
|
scheduledActions.sort((a, b) => {
|
||||||
|
if (typeof a.scheduledAt === "undefined") {
|
||||||
|
a.scheduledAt = defaultScheduledTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof b.scheduledAt === "undefined") {
|
||||||
|
b.scheduledAt = defaultScheduledTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.scheduledAt - b.scheduledAt;
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(checkScheduledActions, FIRST_CHECK_TIMEOUT);
|
||||||
|
// eslint-disable-next-line require-atomic-updates
|
||||||
|
scheduledActionsInterval = setInterval(checkScheduledActions, CHECK_INTERVAL);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const savedVipUsers = JSON.parse(
|
||||||
|
await (await fs.readFile(VIP_USERS_FILE)).toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const key of Object.keys(savedVipUsers)) {
|
||||||
|
vipUsers[key] = savedVipUsers[key];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// probably file does not exist
|
||||||
|
if (e instanceof Error) {
|
||||||
|
console.log(`${LOG_PREFIX}${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createSaveDirectory() {
|
async function createSaveDirectory() {
|
||||||
try {
|
try {
|
||||||
await fs.stat(FILES_BASE);
|
await fs.stat(FILES_BASE);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await fs.mkdir(FILES_BASE);
|
await fs.mkdir(FILES_BASE);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function save(): void {
|
|
||||||
if (saveScheduledActionsTimeout) {
|
|
||||||
clearTimeout(saveScheduledActionsTimeout);
|
|
||||||
saveScheduledActionsTimeout = null;
|
|
||||||
console.log(`${LOG_PREFIX}Removed save timeout.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
saveScheduledActionsTimeout = setTimeout(async () => {
|
|
||||||
await Promise.all([
|
|
||||||
fs.writeFile(SCHEDULED_FILE, JSON.stringify(scheduledActions)),
|
|
||||||
fs.writeFile(VIP_USERS_FILE, JSON.stringify(vipUsers))
|
|
||||||
]);
|
|
||||||
|
|
||||||
console.log(`${LOG_PREFIX}Saved actions.`);
|
|
||||||
saveScheduledActionsTimeout = null;
|
|
||||||
}, SAVE_TIMEOUT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createSaveDirectory();
|
createSaveDirectory();
|
||||||
|
@@ -5,45 +5,51 @@ import { resolve } from "path";
|
|||||||
const TOKENS_FILE = "tokens.json";
|
const TOKENS_FILE = "tokens.json";
|
||||||
const LOG_PREFIX = "[TokenData] ";
|
const LOG_PREFIX = "[TokenData] ";
|
||||||
|
|
||||||
export { getTokenData, saveTokenData };
|
|
||||||
|
|
||||||
function getTokenDataFilePath(): string {
|
function getTokenDataFilePath(): string {
|
||||||
return resolve(process.cwd(), TOKENS_FILE);
|
return resolve(process.cwd(), TOKENS_FILE);
|
||||||
}
|
|
||||||
|
|
||||||
async function getTokenData(): Promise<AccessToken> {
|
|
||||||
const tokenDataFilePath = getTokenDataFilePath();
|
|
||||||
let buffer: Buffer;
|
|
||||||
|
|
||||||
try {
|
|
||||||
buffer = await fs.readFile(tokenDataFilePath);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(
|
|
||||||
`${LOG_PREFIX}${TOKENS_FILE} not found on ${tokenDataFilePath}.`
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenData = await JSON.parse(buffer.toString());
|
|
||||||
|
|
||||||
checkTokenData(tokenData);
|
|
||||||
|
|
||||||
return tokenData;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveTokenData(tokenData: AccessToken): Promise<void> {
|
|
||||||
const tokenDataFilePath = getTokenDataFilePath();
|
|
||||||
const jsonTokenData = JSON.stringify(tokenData);
|
|
||||||
|
|
||||||
await fs.writeFile(tokenDataFilePath, jsonTokenData);
|
|
||||||
console.log(`${LOG_PREFIX}Token data saved`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkTokenData(tokenData: AccessToken): void {
|
function checkTokenData(tokenData: AccessToken): void {
|
||||||
if (!tokenData.accessToken || !tokenData.refreshToken) {
|
if (!tokenData.accessToken || !tokenData.refreshToken) {
|
||||||
console.error(
|
console.error(
|
||||||
`${LOG_PREFIX}Missing refresh_token or access_token in ${TOKENS_FILE}.`
|
`${LOG_PREFIX}Missing refreshToken or accessToken in ${TOKENS_FILE}.`
|
||||||
);
|
);
|
||||||
process.exit(1);
|
|
||||||
}
|
const exitCode = 1;
|
||||||
|
|
||||||
|
process.exit(exitCode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getTokenData(): Promise<AccessToken> {
|
||||||
|
const tokenDataFilePath = getTokenDataFilePath();
|
||||||
|
let buffer: Buffer;
|
||||||
|
|
||||||
|
try {
|
||||||
|
buffer = await fs.readFile(tokenDataFilePath);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
`${LOG_PREFIX}${TOKENS_FILE} not found on ${tokenDataFilePath}.`
|
||||||
|
);
|
||||||
|
|
||||||
|
const exitCode = 1;
|
||||||
|
|
||||||
|
process.exit(exitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenData = await JSON.parse(buffer.toString());
|
||||||
|
|
||||||
|
checkTokenData(tokenData);
|
||||||
|
|
||||||
|
return tokenData;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveTokenData(tokenData: AccessToken): Promise<void> {
|
||||||
|
const tokenDataFilePath = getTokenDataFilePath();
|
||||||
|
const jsonTokenData = JSON.stringify(tokenData);
|
||||||
|
|
||||||
|
await fs.writeFile(tokenDataFilePath, jsonTokenData);
|
||||||
|
console.log(`${LOG_PREFIX}Token data saved`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getTokenData, saveTokenData };
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import { AccessToken, RefreshingAuthProvider } from "@twurple/auth";
|
import { AccessToken, RefreshingAuthProvider } from "@twurple/auth";
|
||||||
import {
|
import {
|
||||||
ApiClient,
|
ApiClient,
|
||||||
HelixCreateCustomRewardData,
|
HelixCreateCustomRewardData,
|
||||||
UserIdResolvable
|
UserIdResolvable,
|
||||||
} from "@twurple/api";
|
} from "@twurple/api";
|
||||||
import { getTokenData, saveTokenData } from "./tokenData";
|
import { getTokenData, saveTokenData } from "./tokenData";
|
||||||
|
|
||||||
@@ -13,115 +13,118 @@ const LOG_PREFIX = "[Twitch] ";
|
|||||||
let refreshAuthProvider: RefreshingAuthProvider;
|
let refreshAuthProvider: RefreshingAuthProvider;
|
||||||
|
|
||||||
function getClientCredentials(): ClientCredentials {
|
function getClientCredentials(): ClientCredentials {
|
||||||
if (!process.env.TWITCH_CLIENT_ID || !process.env.TWITCH_CLIENT_SECRET) {
|
if (!process.env.TWITCH_CLIENT_ID || !process.env.TWITCH_CLIENT_SECRET) {
|
||||||
console.error(
|
console.error(
|
||||||
`${LOG_PREFIX}Missing environment parameters TWITCH_CLIENT_ID or TWITCH_CLIENT_SECRET`
|
`${LOG_PREFIX}Missing environment parameters TWITCH_CLIENT_ID or TWITCH_CLIENT_SECRET`
|
||||||
);
|
);
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
const exitCode = 1;
|
||||||
clientId: process.env.TWITCH_CLIENT_ID,
|
|
||||||
clientSecret: process.env.TWITCH_CLIENT_SECRET
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAuthProvider(): Promise<RefreshingAuthProvider> {
|
process.exit(exitCode);
|
||||||
if (refreshAuthProvider) {
|
}
|
||||||
return refreshAuthProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tokenData = await getTokenData();
|
return {
|
||||||
|
clientId: process.env.TWITCH_CLIENT_ID,
|
||||||
const credentials = getClientCredentials();
|
clientSecret: process.env.TWITCH_CLIENT_SECRET,
|
||||||
|
};
|
||||||
refreshAuthProvider = new RefreshingAuthProvider(
|
|
||||||
{
|
|
||||||
clientId: credentials.clientId,
|
|
||||||
clientSecret: credentials.clientSecret,
|
|
||||||
onRefresh
|
|
||||||
},
|
|
||||||
tokenData
|
|
||||||
);
|
|
||||||
|
|
||||||
return refreshAuthProvider;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onRefresh(refreshData: AccessToken): Promise<void> {
|
async function onRefresh(refreshData: AccessToken): Promise<void> {
|
||||||
console.log(`${LOG_PREFIX}Tokens refreshed`);
|
console.log(`${LOG_PREFIX}Tokens refreshed`);
|
||||||
|
|
||||||
await saveTokenData(refreshData);
|
await saveTokenData(refreshData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAuthProvider(): Promise<RefreshingAuthProvider> {
|
||||||
|
if (refreshAuthProvider) {
|
||||||
|
return refreshAuthProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenData = await getTokenData();
|
||||||
|
const credentials = getClientCredentials();
|
||||||
|
|
||||||
|
// eslint-disable-next-line require-atomic-updates
|
||||||
|
refreshAuthProvider = new RefreshingAuthProvider(
|
||||||
|
{
|
||||||
|
clientId: credentials.clientId,
|
||||||
|
clientSecret: credentials.clientSecret,
|
||||||
|
onRefresh,
|
||||||
|
},
|
||||||
|
tokenData
|
||||||
|
);
|
||||||
|
|
||||||
|
return refreshAuthProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getApiClient(): Promise<ApiClient> {
|
async function getApiClient(): Promise<ApiClient> {
|
||||||
const authProvider = await getAuthProvider();
|
const authProvider = await getAuthProvider();
|
||||||
|
|
||||||
return await new ApiClient({ authProvider });
|
return new ApiClient({ authProvider });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUsernameFromId(userId: number): Promise<string | null> {
|
async function getUsernameFromId(userId: number): Promise<string | null> {
|
||||||
const apiClient = await getApiClient();
|
const apiClient = await getApiClient();
|
||||||
const user = await apiClient.users.getUserById(userId);
|
const user = await apiClient.users.getUserById(userId);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return user.displayName;
|
return user.displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createReward(
|
async function createReward(
|
||||||
userId: UserIdResolvable,
|
userId: UserIdResolvable,
|
||||||
data: HelixCreateCustomRewardData
|
data: HelixCreateCustomRewardData
|
||||||
) {
|
) {
|
||||||
const apiClient = await getApiClient();
|
const apiClient = await getApiClient();
|
||||||
|
|
||||||
await apiClient.channelPoints.createCustomReward(userId, data);
|
await apiClient.channelPoints.createCustomReward(userId, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function completeRewards(
|
async function completeRewards(
|
||||||
channel: UserIdResolvable,
|
channel: UserIdResolvable,
|
||||||
rewardId: string,
|
rewardId: string,
|
||||||
redemptionIds: Array<string> | string
|
redemptionIds: Array<string> | string
|
||||||
) {
|
) {
|
||||||
if (!Array.isArray(redemptionIds)) {
|
if (!Array.isArray(redemptionIds)) {
|
||||||
redemptionIds = [redemptionIds];
|
redemptionIds = [redemptionIds];
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiClient = await getApiClient();
|
const apiClient = await getApiClient();
|
||||||
|
|
||||||
await apiClient.channelPoints.updateRedemptionStatusByIds(
|
await apiClient.channelPoints.updateRedemptionStatusByIds(
|
||||||
channel,
|
channel,
|
||||||
rewardId,
|
rewardId,
|
||||||
redemptionIds,
|
redemptionIds,
|
||||||
"FULFILLED"
|
"FULFILLED"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cancelRewards(
|
async function cancelRewards(
|
||||||
channel: UserIdResolvable,
|
channel: UserIdResolvable,
|
||||||
rewardId: string,
|
rewardId: string,
|
||||||
redemptionIds: Array<string> | string
|
redemptionIds: Array<string> | string
|
||||||
) {
|
) {
|
||||||
if (!Array.isArray(redemptionIds)) {
|
if (!Array.isArray(redemptionIds)) {
|
||||||
redemptionIds = [redemptionIds];
|
redemptionIds = [redemptionIds];
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiClient = await getApiClient();
|
const apiClient = await getApiClient();
|
||||||
|
|
||||||
await apiClient.channelPoints.updateRedemptionStatusByIds(
|
await apiClient.channelPoints.updateRedemptionStatusByIds(
|
||||||
channel,
|
channel,
|
||||||
rewardId,
|
rewardId,
|
||||||
redemptionIds,
|
redemptionIds,
|
||||||
"CANCELED"
|
"CANCELED"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getAuthProvider,
|
getAuthProvider,
|
||||||
getApiClient,
|
getApiClient,
|
||||||
getUsernameFromId,
|
getUsernameFromId,
|
||||||
completeRewards,
|
completeRewards,
|
||||||
cancelRewards,
|
cancelRewards,
|
||||||
createReward
|
createReward,
|
||||||
};
|
};
|
||||||
|
@@ -1,9 +1,8 @@
|
|||||||
|
import { AddressInfo, Socket } from "net";
|
||||||
import { IncomingMessage, Server } from "http";
|
import { IncomingMessage, Server } from "http";
|
||||||
import { save, scheduledActions } from "./miniDb";
|
import { save, scheduledActions } from "./miniDb";
|
||||||
|
|
||||||
import { Action } from "../../interfaces/actions/Action";
|
import { Action } from "../../interfaces/actions/Action";
|
||||||
import { AddressInfo } from "net";
|
|
||||||
import { Socket } from "net";
|
|
||||||
import WebSocket from "ws";
|
import WebSocket from "ws";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import { handleClientAction } from "../chatClient";
|
import { handleClientAction } from "../chatClient";
|
||||||
@@ -17,98 +16,103 @@ const app = express();
|
|||||||
const sockets: Array<WebSocket> = [];
|
const sockets: Array<WebSocket> = [];
|
||||||
|
|
||||||
const wsServer = new WebSocket.Server({
|
const wsServer = new WebSocket.Server({
|
||||||
noServer: true
|
noServer: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
let server: Server;
|
let server: Server;
|
||||||
|
|
||||||
export { listen, broadcast };
|
function broadcast(msg: string, socket?: WebSocket) {
|
||||||
|
const filteredSockets = socket
|
||||||
|
? sockets.filter((s) => s !== socket)
|
||||||
|
: sockets;
|
||||||
|
|
||||||
|
filteredSockets.forEach((s) => s.send(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onMessage(this: WebSocket, msg: string) {
|
||||||
|
const data = JSON.parse(msg);
|
||||||
|
|
||||||
|
if (!data.actions) {
|
||||||
|
broadcast(msg, this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions: Array<Action> = data.actions;
|
||||||
|
|
||||||
|
for (const action of actions) {
|
||||||
|
if (!action.scheduledAt) {
|
||||||
|
await handleClientAction(action);
|
||||||
|
} else {
|
||||||
|
scheduledActions.push(action);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
scheduledActions.sort((a: any, b: any) => a.scheduledAt - b.scheduledAt);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`${LOG_PREFIX_WS}Received message with ${data.actions.length} actions:`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose(this: WebSocket) {
|
||||||
|
const socketIdx = sockets.indexOf(this);
|
||||||
|
const deleteCount = 1;
|
||||||
|
|
||||||
|
sockets.splice(socketIdx, deleteCount);
|
||||||
|
console.log(`${LOG_PREFIX_WS}Connection closed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onListening() {
|
||||||
|
console.log(
|
||||||
|
`${LOG_PREFIX_HTTP}Listening on port ${
|
||||||
|
(server.address() as AddressInfo).port
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUpgrade(req: IncomingMessage, socket: Socket, head: Buffer) {
|
||||||
|
wsServer.handleUpgrade(req, socket, head, (socket) => {
|
||||||
|
wsServer.emit("connection", socket, req);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onConnection(socket: WebSocket, req: IncomingMessage) {
|
||||||
|
console.log(
|
||||||
|
`${LOG_PREFIX_WS}${req.socket.remoteAddress} New connection established`
|
||||||
|
);
|
||||||
|
sockets.push(socket);
|
||||||
|
socket.send(
|
||||||
|
JSON.stringify({
|
||||||
|
env: isDevelopment ? "dev" : "prod",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
socket.on("message", onMessage);
|
||||||
|
socket.on("close", onClose);
|
||||||
|
}
|
||||||
|
|
||||||
wsServer.on("connection", onConnection);
|
wsServer.on("connection", onConnection);
|
||||||
|
|
||||||
app.use(express.static(join(process.cwd(), "client")));
|
app.use(express.static(join(process.cwd(), "client")));
|
||||||
|
|
||||||
function listen() {
|
function listen() {
|
||||||
if (server) {
|
if (server) {
|
||||||
console.log(`${LOG_PREFIX_HTTP}Server is already running`);
|
console.log(`${LOG_PREFIX_HTTP}Server is already running`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
server = app.listen(!isDevelopment ? 8080 : 8081, "0.0.0.0");
|
let port = 8080;
|
||||||
|
|
||||||
server.on("listening", onListening);
|
if (isDevelopment) {
|
||||||
server.on("upgrade", onUpgrade);
|
port++;
|
||||||
|
}
|
||||||
|
|
||||||
|
server = app.listen(port, "0.0.0.0");
|
||||||
|
|
||||||
|
server.on("listening", onListening);
|
||||||
|
server.on("upgrade", onUpgrade);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onListening() {
|
export { listen, broadcast };
|
||||||
console.log(
|
|
||||||
`${LOG_PREFIX_HTTP}Listening on port ${
|
|
||||||
(server.address() as AddressInfo).port
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onUpgrade(req: IncomingMessage, socket: Socket, head: Buffer) {
|
|
||||||
wsServer.handleUpgrade(req, socket, head, socket => {
|
|
||||||
wsServer.emit("connection", socket, req);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onConnection(socket: WebSocket, req: IncomingMessage) {
|
|
||||||
console.log(
|
|
||||||
`${LOG_PREFIX_WS}${req.socket.remoteAddress} New connection established`
|
|
||||||
);
|
|
||||||
sockets.push(socket);
|
|
||||||
socket.send(
|
|
||||||
JSON.stringify({
|
|
||||||
env: isDevelopment ? "dev" : "prod"
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
socket.on("message", onMessage);
|
|
||||||
socket.on("close", onClose);
|
|
||||||
}
|
|
||||||
|
|
||||||
// broadcast a message to all clients
|
|
||||||
function broadcast(msg: string, socket?: any) {
|
|
||||||
const filteredSockets = socket ? sockets.filter(s => s !== socket) : sockets;
|
|
||||||
|
|
||||||
filteredSockets.forEach(s => s.send(msg));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onMessage(msg: string) {
|
|
||||||
// @ts-ignore
|
|
||||||
const socket = this as WebSocket;
|
|
||||||
const data = JSON.parse(msg);
|
|
||||||
|
|
||||||
if (!data.actions || data.actions.length === 0) {
|
|
||||||
broadcast(msg, socket);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const actions: Array<Action> = data.actions;
|
|
||||||
|
|
||||||
for (const action of actions) {
|
|
||||||
if (!action.scheduledAt) {
|
|
||||||
await handleClientAction(action);
|
|
||||||
} else {
|
|
||||||
scheduledActions.push(action);
|
|
||||||
scheduledActions.sort((a: any, b: any) => a.scheduledAt - b.scheduledAt);
|
|
||||||
save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`${LOG_PREFIX_WS}Received message with ${data.actions.length} actions:`,
|
|
||||||
data
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onClose() {
|
|
||||||
// @ts-ignore
|
|
||||||
const socket: WebSocket = this as WebSocket;
|
|
||||||
|
|
||||||
const socketIdx = sockets.indexOf(socket);
|
|
||||||
sockets.splice(socketIdx, 1);
|
|
||||||
console.log(`${LOG_PREFIX_WS}Connection closed`);
|
|
||||||
}
|
|
||||||
|
@@ -4,39 +4,40 @@ import { getUsernameFromId } from "../../helpers/twitch";
|
|||||||
import { timeout } from "../../chatClient/clientActions";
|
import { timeout } from "../../chatClient/clientActions";
|
||||||
|
|
||||||
async function highlightMessage(
|
async function highlightMessage(
|
||||||
msg: RedemptionMessage
|
msg: RedemptionMessage
|
||||||
): Promise<RedemptionMessage | undefined> {
|
): Promise<RedemptionMessage | undefined> {
|
||||||
if (!msg.message) {
|
if (!msg.message) {
|
||||||
console.log(`${LOG_PREFIX}Redemption has no message`);
|
console.log(`${LOG_PREFIX}Redemption has no message`);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlRegex =
|
const urlRegex =
|
||||||
/(https?:\/\/)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&/=]*)/;
|
/(https?:\/\/)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)/;
|
||||||
|
|
||||||
if (urlRegex.test(msg.message)) {
|
if (urlRegex.test(msg.message)) {
|
||||||
console.log(`${LOG_PREFIX}Message contains a url`);
|
console.log(`${LOG_PREFIX}Message contains a url`);
|
||||||
const channel = await getUsernameFromId(parseInt(msg.channelId));
|
const channel = await getUsernameFromId(parseInt(msg.channelId));
|
||||||
|
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
console.log(`${LOG_PREFIX}No channel found`);
|
console.log(`${LOG_PREFIX}No channel found`);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const reason = "No se permite enviar enlaces en mensajes destacados";
|
const reason = "No se permite enviar enlaces en mensajes destacados";
|
||||||
|
const timeoutSeconds = 10;
|
||||||
|
|
||||||
await timeout(channel, msg.userDisplayName, 10, reason);
|
await timeout(channel, msg.userDisplayName, timeoutSeconds, reason);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// user probably cannot be timed out
|
// user probably cannot be timed out
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { highlightMessage };
|
export { highlightMessage };
|
||||||
|
@@ -12,56 +12,60 @@ const timeoutSeconds = 60;
|
|||||||
const maxSafeShots = 5;
|
const maxSafeShots = 5;
|
||||||
|
|
||||||
async function russianRoulette(
|
async function russianRoulette(
|
||||||
msg: RedemptionMessage
|
msg: RedemptionMessage
|
||||||
): Promise<RedemptionMessage | undefined> {
|
): Promise<RedemptionMessage | undefined> {
|
||||||
const { channelId, userDisplayName } = msg;
|
const { channelId, userDisplayName } = msg;
|
||||||
const channel = await getUsernameFromId(parseInt(channelId));
|
const channel = await getUsernameFromId(parseInt(channelId));
|
||||||
|
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
console.log(`${LOG_PREFIX}No channel found`);
|
console.log(`${LOG_PREFIX}No channel found`);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!gunsSafeShots[channelId]) {
|
if (!gunsSafeShots[channelId]) {
|
||||||
gunsSafeShots[channelId] = maxSafeShots;
|
gunsSafeShots[channelId] = maxSafeShots;
|
||||||
}
|
}
|
||||||
|
|
||||||
const win =
|
const noShots = 0;
|
||||||
gunsSafeShots[channelId] > 0 &&
|
|
||||||
randomInt(gunsSafeShots[channelId]-- + 1) !== 0;
|
|
||||||
|
|
||||||
if (gunsSafeShots[channelId] < 0 || !win) {
|
const win =
|
||||||
gunsSafeShots[channelId] = maxSafeShots;
|
gunsSafeShots[channelId] > noShots &&
|
||||||
}
|
// eslint-disable-next-line no-magic-numbers
|
||||||
|
randomInt(gunsSafeShots[channelId]-- + 1) !== 0;
|
||||||
|
|
||||||
msg.message = win ? "" : "got shot";
|
if (gunsSafeShots[channelId] < noShots || !win) {
|
||||||
|
gunsSafeShots[channelId] = maxSafeShots;
|
||||||
|
}
|
||||||
|
|
||||||
const promises: Array<Promise<unknown>> = [];
|
// eslint-disable-next-line require-atomic-updates
|
||||||
|
msg.message = win ? "" : "got shot";
|
||||||
|
|
||||||
if (!win) {
|
const promises: Array<Promise<unknown>> = [];
|
||||||
promises.push(
|
|
||||||
timeout(channel, userDisplayName, timeoutSeconds, "F en la ruleta")
|
|
||||||
);
|
|
||||||
promises.push(
|
|
||||||
say(
|
|
||||||
channel,
|
|
||||||
`PepeHands ${userDisplayName} no ha sobrevivido para contarlo`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
promises.push(say(channel, `rdCool Clap ${userDisplayName}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
if (!win) {
|
||||||
await Promise.allSettled(promises);
|
promises.push(
|
||||||
} catch (e) {
|
timeout(channel, userDisplayName, timeoutSeconds, "F en la ruleta")
|
||||||
if (e instanceof Error) {
|
);
|
||||||
console.log(`${LOG_PREFIX}${e.message}`);
|
promises.push(
|
||||||
}
|
say(
|
||||||
}
|
channel,
|
||||||
|
`PepeHands ${userDisplayName} no ha sobrevivido para contarlo`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
promises.push(say(channel, `rdCool Clap ${userDisplayName}`));
|
||||||
|
}
|
||||||
|
|
||||||
return msg;
|
try {
|
||||||
|
await Promise.allSettled(promises);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
console.log(`${LOG_PREFIX}${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { russianRoulette };
|
export { russianRoulette };
|
||||||
|
@@ -7,70 +7,73 @@ import { getUsernameFromId } from "../../helpers/twitch";
|
|||||||
|
|
||||||
// remove vip from a user to grant it to yourself
|
// remove vip from a user to grant it to yourself
|
||||||
async function stealVip(
|
async function stealVip(
|
||||||
msg: RedemptionMessage
|
msg: RedemptionMessage
|
||||||
): Promise<RedemptionMessage | undefined> {
|
): Promise<RedemptionMessage | undefined> {
|
||||||
if (!msg.message) {
|
if (!msg.message) {
|
||||||
console.log(`${LOG_PREFIX}Redemption has no message`);
|
console.log(`${LOG_PREFIX}Redemption has no message`);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const channelId = parseInt(msg.channelId);
|
const channelId = parseInt(msg.channelId);
|
||||||
const channel = await getUsernameFromId(channelId);
|
const channel = await getUsernameFromId(channelId);
|
||||||
|
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
console.log(`${LOG_PREFIX}No channel found`);
|
console.log(`${LOG_PREFIX}No channel found`);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const addVipUser = msg.userDisplayName;
|
const addVipUser = msg.userDisplayName;
|
||||||
const removeVipUser = msg.message.toLowerCase();
|
const removeVipUser = msg.message.toLowerCase();
|
||||||
const channelVips = vipUsers[channelId];
|
const channelVips = vipUsers[channelId];
|
||||||
|
|
||||||
if (!channelVips.find(u => u.toLowerCase() === removeVipUser)) {
|
if (!channelVips.find((u) => u.toLowerCase() === removeVipUser)) {
|
||||||
const message =
|
const message =
|
||||||
channelVips.length === 0
|
// eslint-disable-next-line no-magic-numbers
|
||||||
? "No hay nadie a quien puedas robar el VIP"
|
channelVips.length === 0
|
||||||
: `Solo puedes robar el VIP de: "${channelVips.sort().join('", "')}"`;
|
? "No hay nadie a quien puedas robar el VIP"
|
||||||
await say(channel, message);
|
: `Solo puedes robar el VIP de: "${channelVips.sort().join('", "')}"`;
|
||||||
|
await say(channel, message);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (channelVips.includes(addVipUser) || (await hasVip(channel, addVipUser))) {
|
if (channelVips.includes(addVipUser) || (await hasVip(channel, addVipUser))) {
|
||||||
console.log(`${LOG_PREFIX}@${addVipUser} is already VIP`);
|
console.log(`${LOG_PREFIX}@${addVipUser} is already VIP`);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const removed = await removeVip(channel, removeVipUser);
|
const removed = await removeVip(channel, removeVipUser);
|
||||||
|
|
||||||
if (!removed && (await hasVip(channel, removeVipUser))) {
|
if (!removed && (await hasVip(channel, removeVipUser))) {
|
||||||
console.log(`${LOG_PREFIX}Could not remove VIP of @${removeVipUser}`);
|
console.log(`${LOG_PREFIX}Could not remove VIP of @${removeVipUser}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const added = await addVip(channel, addVipUser);
|
const added = await addVip(channel, addVipUser);
|
||||||
|
|
||||||
if (!added) {
|
if (!added) {
|
||||||
await addVip(channel, removeVipUser);
|
await addVip(channel, removeVipUser);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeIdx = channelVips.findIndex(
|
const removeIdx = channelVips.findIndex(
|
||||||
u => u.toLowerCase() === removeVipUser
|
(u) => u.toLowerCase() === removeVipUser
|
||||||
);
|
);
|
||||||
|
|
||||||
channelVips.splice(removeIdx);
|
channelVips.splice(removeIdx);
|
||||||
channelVips.push(addVipUser);
|
channelVips.push(addVipUser);
|
||||||
save();
|
save();
|
||||||
|
|
||||||
msg.message = `@${addVipUser} ha "tomado prestado" el VIP de @${removeVipUser}`;
|
// eslint-disable-next-line require-atomic-updates
|
||||||
await say(channel, msg.message);
|
msg.message = `@${addVipUser} ha "tomado prestado" el VIP de @${removeVipUser}`;
|
||||||
|
|
||||||
return msg;
|
await say(channel, msg.message);
|
||||||
|
|
||||||
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { stealVip };
|
export { stealVip };
|
||||||
|
@@ -4,40 +4,41 @@ import { getUsernameFromId } from "../../helpers/twitch";
|
|||||||
import { timeout } from "../../chatClient/clientActions";
|
import { timeout } from "../../chatClient/clientActions";
|
||||||
|
|
||||||
async function timeoutFriend(
|
async function timeoutFriend(
|
||||||
msg: RedemptionMessage
|
msg: RedemptionMessage
|
||||||
): Promise<RedemptionMessage | undefined> {
|
): Promise<RedemptionMessage | undefined> {
|
||||||
const { message, channelId, userDisplayName } = msg;
|
const { message, channelId, userDisplayName } = msg;
|
||||||
if (!msg.message) {
|
if (!msg.message) {
|
||||||
console.log(`${LOG_PREFIX}Redemption has no message`);
|
console.log(`${LOG_PREFIX}Redemption has no message`);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const channel = await getUsernameFromId(parseInt(channelId));
|
const channel = await getUsernameFromId(parseInt(channelId));
|
||||||
|
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
console.log(`${LOG_PREFIX}No channel found`);
|
console.log(`${LOG_PREFIX}No channel found`);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const time = 60;
|
const time = 60;
|
||||||
const reason = `Timeout dado por @${userDisplayName} con puntos del canal`;
|
const reason = `Timeout dado por @${userDisplayName} con puntos del canal`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await timeout(channel, msg.message, time, reason);
|
await timeout(channel, msg.message, time, reason);
|
||||||
|
|
||||||
msg.message = `@${userDisplayName} ha expulsado a @${message} por ${time} segundos`;
|
// eslint-disable-next-line require-atomic-updates
|
||||||
} catch (e) {
|
msg.message = `@${userDisplayName} ha expulsado a @${message} por ${time} segundos`;
|
||||||
// user can not be timed out
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
// user can not be timed out
|
||||||
console.error(`${LOG_PREFIX} ${e.message}`);
|
if (e instanceof Error) {
|
||||||
}
|
console.error(`${LOG_PREFIX} ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { timeoutFriend };
|
export { timeoutFriend };
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import { PubSubClient, PubSubRedemptionMessage } from "@twurple/pubsub";
|
import { PubSubClient, PubSubRedemptionMessage } from "@twurple/pubsub";
|
||||||
import {
|
import {
|
||||||
cancelRewards,
|
cancelRewards,
|
||||||
completeRewards,
|
completeRewards,
|
||||||
getAuthProvider
|
getAuthProvider,
|
||||||
} from "../helpers/twitch";
|
} from "../helpers/twitch";
|
||||||
|
|
||||||
import { RedemptionIds } from "../../enums/Redemptions";
|
import { RedemptionIds } from "../../enums/Redemptions";
|
||||||
@@ -20,107 +20,116 @@ import { timeoutFriend } from "./actions/timeoutFriend";
|
|||||||
|
|
||||||
const LOG_PREFIX = "[PubSub] ";
|
const LOG_PREFIX = "[PubSub] ";
|
||||||
|
|
||||||
async function registerUserListener(user: UserIdResolvable) {
|
async function onRedemption(message: PubSubRedemptionMessage) {
|
||||||
const pubSubClient = new PubSubClient();
|
console.log(
|
||||||
const userId = await pubSubClient.registerUserListener(
|
`${LOG_PREFIX}Reward: "${message.rewardTitle}" (${message.rewardId}) redeemed by ${message.userDisplayName}`
|
||||||
await getAuthProvider(),
|
);
|
||||||
user
|
|
||||||
);
|
|
||||||
/*const listener = */ await pubSubClient.onRedemption(userId, onRedemption);
|
|
||||||
|
|
||||||
console.log(`${LOG_PREFIX}Connected & registered`);
|
const raw = message[rawDataSymbol];
|
||||||
|
|
||||||
|
const msg: RedemptionMessage = {
|
||||||
|
id: message.id,
|
||||||
|
channelId: message.channelId,
|
||||||
|
rewardId: message.rewardId,
|
||||||
|
rewardName: message.rewardTitle,
|
||||||
|
rewardImage: message.rewardImage
|
||||||
|
? message.rewardImage.url_4x
|
||||||
|
: "https://static-cdn.jtvnw.net/custom-reward-images/default-4.png",
|
||||||
|
message: message.message,
|
||||||
|
userId: message.userId,
|
||||||
|
userDisplayName: message.userDisplayName,
|
||||||
|
backgroundColor: raw.data.redemption.reward.background_color,
|
||||||
|
};
|
||||||
|
|
||||||
|
let handledMessage: RedemptionMessage | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// TODO: extract to function
|
||||||
|
// Returns the executor function, then call it inside a try/catch
|
||||||
|
switch (msg.rewardId) {
|
||||||
|
case RedemptionIds.GetVip:
|
||||||
|
handledMessage = await getVip(msg);
|
||||||
|
break;
|
||||||
|
case RedemptionIds.Hidrate:
|
||||||
|
handledMessage = await hidrate(msg);
|
||||||
|
break;
|
||||||
|
case RedemptionIds.HighlightMessage:
|
||||||
|
handledMessage = await highlightMessage(msg);
|
||||||
|
break;
|
||||||
|
case RedemptionIds.RussianRoulette:
|
||||||
|
handledMessage = await russianRoulette(msg);
|
||||||
|
break;
|
||||||
|
case RedemptionIds.StealVip:
|
||||||
|
handledMessage = await stealVip(msg);
|
||||||
|
break;
|
||||||
|
case RedemptionIds.TimeoutFriend:
|
||||||
|
handledMessage = await timeoutFriend(msg);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log(`${LOG_PREFIX}Unhandled redemption ${msg.rewardId}`);
|
||||||
|
|
||||||
|
handledMessage = msg;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
console.error(`${LOG_PREFIX}${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handledMessage) {
|
||||||
|
const rewardEnumValues = Object.values(RedemptionIds);
|
||||||
|
const rewardIdValueIndex = rewardEnumValues.indexOf(
|
||||||
|
// @ts-expect-error String is not assignable to... but all keys are strings
|
||||||
|
handledMessage.rewardId
|
||||||
|
);
|
||||||
|
const rewardName = Object.keys(RedemptionIds)[rewardIdValueIndex];
|
||||||
|
|
||||||
|
handledMessage.rewardId = rewardName;
|
||||||
|
|
||||||
|
broadcast(JSON.stringify(handledMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: improve this check
|
||||||
|
const keepInQueueRewards = [RedemptionIds.KaraokeTime];
|
||||||
|
|
||||||
|
// @ts-expect-error String is not assignable to... but all keys are strings
|
||||||
|
if (keepInQueueRewards.includes(message.rewardId)) {
|
||||||
|
console.log(`${LOG_PREFIX}Reward kept in queue due to config`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const completeOrCancelReward =
|
||||||
|
handledMessage && isProduction ? completeRewards : cancelRewards;
|
||||||
|
|
||||||
|
if (message.rewardIsQueued) {
|
||||||
|
try {
|
||||||
|
await completeOrCancelReward(
|
||||||
|
message.channelId,
|
||||||
|
message.rewardId,
|
||||||
|
message.id
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`${LOG_PREFIX}Reward removed from queue (completed or canceled)`
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
console.log(`${LOG_PREFIX}${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onRedemption(message: PubSubRedemptionMessage) {
|
async function registerUserListener(user: UserIdResolvable) {
|
||||||
console.log(
|
const pubSubClient = new PubSubClient();
|
||||||
`${LOG_PREFIX}Reward: "${message.rewardTitle}" (${message.rewardId}) redeemed by ${message.userDisplayName}`
|
const userId = await pubSubClient.registerUserListener(
|
||||||
);
|
await getAuthProvider(),
|
||||||
|
user
|
||||||
|
);
|
||||||
|
|
||||||
const raw = message[rawDataSymbol];
|
await pubSubClient.onRedemption(userId, onRedemption);
|
||||||
|
|
||||||
const msg: RedemptionMessage = {
|
console.log(`${LOG_PREFIX}Connected & registered`);
|
||||||
id: message.id,
|
|
||||||
channelId: message.channelId,
|
|
||||||
rewardId: message.rewardId,
|
|
||||||
rewardName: message.rewardTitle,
|
|
||||||
rewardImage: message.rewardImage
|
|
||||||
? message.rewardImage.url_4x
|
|
||||||
: "https://static-cdn.jtvnw.net/custom-reward-images/default-4.png",
|
|
||||||
message: message.message,
|
|
||||||
userId: message.userId,
|
|
||||||
userDisplayName: message.userDisplayName,
|
|
||||||
backgroundColor: raw.data.redemption.reward.background_color
|
|
||||||
};
|
|
||||||
|
|
||||||
let handledMessage: RedemptionMessage | undefined;
|
|
||||||
|
|
||||||
switch (msg.rewardId) {
|
|
||||||
case RedemptionIds.RussianRoulette:
|
|
||||||
handledMessage = await russianRoulette(msg);
|
|
||||||
break;
|
|
||||||
case RedemptionIds.TimeoutFriend:
|
|
||||||
handledMessage = await timeoutFriend(msg);
|
|
||||||
break;
|
|
||||||
case RedemptionIds.HighlightMessage:
|
|
||||||
handledMessage = await highlightMessage(msg);
|
|
||||||
break;
|
|
||||||
case RedemptionIds.GetVip:
|
|
||||||
handledMessage = await getVip(msg);
|
|
||||||
break;
|
|
||||||
case RedemptionIds.StealVip:
|
|
||||||
handledMessage = await stealVip(msg);
|
|
||||||
break;
|
|
||||||
case RedemptionIds.Hidrate:
|
|
||||||
handledMessage = await hidrate(msg);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.log(`${LOG_PREFIX}Unhandled redemption ${msg.rewardId}`);
|
|
||||||
|
|
||||||
handledMessage = msg;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (handledMessage) {
|
|
||||||
const rewardEnumValues = Object.values(RedemptionIds);
|
|
||||||
const rewardIdValueIndex = rewardEnumValues.indexOf(
|
|
||||||
// @ts-expect-error String is not assignable to... but all keys are strings
|
|
||||||
handledMessage.rewardId
|
|
||||||
);
|
|
||||||
const rewardName = Object.keys(RedemptionIds)[rewardIdValueIndex];
|
|
||||||
|
|
||||||
handledMessage.rewardId = rewardName;
|
|
||||||
|
|
||||||
broadcast(JSON.stringify(handledMessage));
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: improve this check
|
|
||||||
const keepInQueueRewards = [RedemptionIds.KaraokeTime];
|
|
||||||
|
|
||||||
// @ts-expect-error String is not assignable to... but all keys are strings
|
|
||||||
if (keepInQueueRewards.includes(message.rewardId)) {
|
|
||||||
console.log(`${LOG_PREFIX}Reward kept in queue due to config`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const completeOrCancelReward =
|
|
||||||
handledMessage && isProduction ? completeRewards : cancelRewards;
|
|
||||||
|
|
||||||
if (message.rewardIsQueued) {
|
|
||||||
try {
|
|
||||||
await completeOrCancelReward(
|
|
||||||
message.channelId,
|
|
||||||
message.rewardId,
|
|
||||||
message.id
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`${LOG_PREFIX}Reward removed from queue (completed or canceled)`
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Error) {
|
|
||||||
console.log(`${LOG_PREFIX}${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { registerUserListener, LOG_PREFIX };
|
export { registerUserListener, LOG_PREFIX };
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
import { ActionType } from "../../enums/ActionType";
|
import { ActionType } from "../../enums/ActionType";
|
||||||
|
|
||||||
export interface Action {
|
export interface Action {
|
||||||
type: ActionType;
|
type: ActionType;
|
||||||
channelId: string;
|
channelId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
scheduledAt?: number;
|
scheduledAt?: number;
|
||||||
data: any;
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
data: any;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user