1
0

🚨 Fix most of the linting error and warnings

This commit is contained in:
2022-01-06 18:15:56 +01:00
parent 0a18826978
commit 931cc57b1b
18 changed files with 829 additions and 695 deletions

View File

@@ -1,22 +1,28 @@
import { hasVip, say } from "..";
import { chatClient } from "../..";
import { say } from "..";
async function addVip(
channel: string,
username: string,
message?: string
channel: string,
username: string,
message?: string
): Promise<boolean> {
try {
await chatClient.addVip(channel, username);
} catch (e) {
return false;
}
username = username.toLowerCase();
if (!(await hasVip(channel, username))) {
return false;
}
if (message) {
await say(channel, message);
}
try {
await chatClient.addVip(channel, username);
} catch (e) {
return false;
}
return true;
if (message) {
await say(channel, message);
}
return true;
}
export { addVip };

View File

@@ -2,23 +2,54 @@ import { chatClient } from "../..";
type CacheType = Record<string, Array<string>>;
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> {
if (!username) {
return false;
}
username = username.toLowerCase();
if (!cache[channel]) {
cache[channel] = await chatClient.getVips(channel);
if (!cache[channel]) {
await fetchVips(channel);
}
setTimeout(() => {
delete cache[channel];
}, 2500);
}
const vips = cache[channel];
const vips = cache[channel];
return vips.includes(username);
return vips.includes(username);
}
export { hasVip };

View File

@@ -1,7 +1,18 @@
import { say, sayError, sayInfo, saySuccess, sayWarn } from "./say";
import { addVip } from "./addVip";
import { hasVip } from "./hasVip";
import { removeVip } from "./removeVip";
import { say } from "./say";
import { timeout } from "./timeout";
export { say, timeout, addVip, removeVip, hasVip };
export {
addVip,
hasVip,
removeVip,
say,
sayError,
sayInfo,
saySuccess,
sayWarn,
timeout,
};

View File

@@ -1,22 +1,29 @@
import { hasVip, say } from "..";
import { chatClient } from "../..";
import { say } from "..";
async function removeVip(
channel: string,
username: string,
message?: string
channel: string,
username: string,
message?: string
): Promise<boolean> {
try {
await chatClient.removeVip(channel, username);
} catch (e) {
return false;
}
username = username.toLowerCase();
if (message) {
await say(channel, message);
}
if (await hasVip(channel, username)) {
return false;
}
return true;
try {
await chatClient.removeVip(channel, username);
} catch (e) {
return false;
}
if (message) {
await say(channel, message);
}
return true;
}
export { removeVip };

View File

@@ -1,19 +1,36 @@
import { chatClient } from "../..";
const maxMessageLength = 500;
async function say(channel: string, message: string): Promise<void> {
// message = `MrDestructoid ${message}`;
const maxMessageLength = 500;
// message = `MrDestructoid ${message}`;
if (message.length > 500) {
const suffix = "...";
message = `${message.substring(
0,
maxMessageLength - suffix.length
)}${suffix}`;
}
if (message.length > maxMessageLength) {
const startIndex = 0;
const 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 };

View File

@@ -1,21 +1,22 @@
import { chatClient } from "../..";
// timeouts a user in a channel
const defaultTime = 60;
async function timeout(
channel: string,
username: string,
time?: number,
reason?: string
channel: string,
username: string,
time?: number,
reason?: string
): Promise<void> {
if (!time) {
time = 60;
}
if (!time) {
time = defaultTime;
}
if (!reason) {
reason = "";
}
if (!reason) {
reason = "";
}
await chatClient.timeout(channel, username, time, reason);
await chatClient.timeout(channel, username, time, reason);
}
export { timeout };

View File

@@ -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 { createReward as createChannelPointsReward } from "../../helpers/twitch";
async function createReward(
channel: string,
user: string,
message: string,
msg: TwitchPrivateMessage
channel: string,
_user: string,
message: string,
msg: TwitchPrivateMessage
): Promise<void> {
const args = message.split(" ");
const args = message.split(" ");
const title = args.shift();
const cost = Math.max(1, parseInt(args.shift() ?? "0"));
const title = args.shift();
if (!title || !cost) {
await say(
channel,
"No se ha especificado el nombre de la recompensa o costo"
);
return;
}
if (!title) {
await sayError(channel, "Debes indicar el título de la recompensa");
return;
}
try {
await createChannelPointsReward(msg.channelId as string, {
title,
cost
});
const minRewardPrice = 1;
const cost = Math.max(minRewardPrice, parseInt(args.shift() ?? "0"));
say(
channel,
`✅ Creada recompensa de canal "${title}" con un costo de ${cost}`
);
} catch (e) {
if (e instanceof Error) {
console.log(`${LOG_PREFIX}${e.message}`);
}
}
try {
await createChannelPointsReward(msg.channelId as string, {
title,
cost,
});
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 };

View File

@@ -12,107 +12,115 @@ import { start } from "../helpers/miniDb";
let chatClient: ChatClient;
export { chatClient, connect, handleClientAction, say, LOG_PREFIX };
const LOG_PREFIX = "[ChatClient] ";
async function connect(channels: Array<any>): Promise<void> {
const authProvider = await getAuthProvider();
function onConnect(): Promise<void> {
console.log(`${LOG_PREFIX}Connected`);
if (chatClient && (chatClient.isConnecting || chatClient.isConnected)) {
return;
}
start();
chatClient = new ChatClient({
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);
}
return Promise.resolve();
}
const commandPrefix = "!";
async function onMessage(
channel: string,
user: string,
message: string,
msg: TwitchPrivateMessage
channel: string,
user: string,
message: string,
msg: TwitchPrivateMessage
): Promise<void> {
if (msg.userInfo.isBroadcaster && message.startsWith(commandPrefix)) {
message = message.substring(commandPrefix.length);
if (msg.userInfo.isBroadcaster && message.startsWith(commandPrefix)) {
message = message.substring(commandPrefix.length);
const args = message.split(" ");
const commandName = args.shift();
const args = message.split(" ");
const commandName = args.shift();
switch (commandName) {
case ChatCommands.Commands:
await say(
channel,
`Comandos disponibles: "${Object.values(ChatCommands).join('", "')}"`
);
break;
case ChatCommands.CreateReward:
await createReward(channel, user, args.join(" "), msg);
break;
default:
console.log(
`${LOG_PREFIX}Command ${commandPrefix}${commandName} not handled`
);
}
}
switch (commandName) {
case ChatCommands.Commands:
await say(
channel,
`Comandos disponibles: "${Object.values(ChatCommands).join('", "')}"`
);
break;
case ChatCommands.CreateReward:
await createReward(channel, user, args.join(" "), msg);
break;
default:
console.log(
`${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 };

View File

@@ -1,3 +1,4 @@
import { Action } from "../../interfaces/actions/Action";
import { promises as fs } from "fs";
import { handleClientAction } from "../chatClient";
import { resolve } from "path";
@@ -12,104 +13,124 @@ const FILES_BASE = resolve(process.cwd(), "./storage");
const SCHEDULED_FILE = resolve(FILES_BASE, "./scheduled.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>> = {};
let checkingScheduled = false;
let scheduledActionsInterval: NodeJS.Timeout;
let saveScheduledActionsTimeout: NodeJS.Timeout | null;
// *Check this, not working
async function start(): Promise<void> {
if (scheduledActionsInterval) {
return;
}
function save(): void {
if (saveScheduledActionsTimeout) {
clearTimeout(saveScheduledActionsTimeout);
saveScheduledActionsTimeout = null;
console.log(`${LOG_PREFIX}Removed save timeout.`);
}
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}`);
}
}
saveScheduledActionsTimeout = setTimeout(async () => {
await Promise.all([
fs.writeFile(SCHEDULED_FILE, JSON.stringify(scheduledActions)),
fs.writeFile(VIP_USERS_FILE, JSON.stringify(vipUsers)),
]);
scheduledActions.push.apply(scheduledActions, savedActions);
scheduledActions.sort((a, b) => a.scheduledAt - b.scheduledAt);
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}`);
}
}
console.log(`${LOG_PREFIX}Saved actions.`);
saveScheduledActionsTimeout = null;
}, SAVE_TIMEOUT);
}
async function checkScheduledActions(): Promise<void> {
if (checkingScheduled) {
return;
}
if (checkingScheduled) {
return;
}
checkingScheduled = true;
checkingScheduled = true;
let hasToSave = false;
let hasToSave = false;
for (
let i = 0;
i < scheduledActions.length &&
scheduledActions[i].scheduledAt <= Date.now();
i++
) {
hasToSave = true;
for (
let i = 0;
i < scheduledActions.length &&
(scheduledActions[i].scheduledAt ?? defaultScheduledTimestamp) <=
Date.now();
i++
) {
hasToSave = true;
const action = scheduledActions.splice(i, 1)[0];
await handleClientAction(action);
console.log(`${LOG_PREFIX}Executed: ${JSON.stringify(action)}`);
}
const deleteCount = 1;
const deletedActions = scheduledActions.splice(i, deleteCount);
const action = deletedActions.shift();
if (hasToSave) {
save();
}
if (action) {
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() {
try {
await fs.stat(FILES_BASE);
} catch (e) {
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);
try {
await fs.stat(FILES_BASE);
} catch (e) {
await fs.mkdir(FILES_BASE);
}
}
createSaveDirectory();

View File

@@ -5,45 +5,51 @@ import { resolve } from "path";
const TOKENS_FILE = "tokens.json";
const LOG_PREFIX = "[TokenData] ";
export { getTokenData, saveTokenData };
function getTokenDataFilePath(): string {
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`);
return resolve(process.cwd(), TOKENS_FILE);
}
function checkTokenData(tokenData: AccessToken): void {
if (!tokenData.accessToken || !tokenData.refreshToken) {
console.error(
`${LOG_PREFIX}Missing refresh_token or access_token in ${TOKENS_FILE}.`
);
process.exit(1);
}
if (!tokenData.accessToken || !tokenData.refreshToken) {
console.error(
`${LOG_PREFIX}Missing refreshToken or accessToken in ${TOKENS_FILE}.`
);
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 };

View File

@@ -1,8 +1,8 @@
import { AccessToken, RefreshingAuthProvider } from "@twurple/auth";
import {
ApiClient,
HelixCreateCustomRewardData,
UserIdResolvable
ApiClient,
HelixCreateCustomRewardData,
UserIdResolvable,
} from "@twurple/api";
import { getTokenData, saveTokenData } from "./tokenData";
@@ -13,115 +13,118 @@ const LOG_PREFIX = "[Twitch] ";
let refreshAuthProvider: RefreshingAuthProvider;
function getClientCredentials(): ClientCredentials {
if (!process.env.TWITCH_CLIENT_ID || !process.env.TWITCH_CLIENT_SECRET) {
console.error(
`${LOG_PREFIX}Missing environment parameters TWITCH_CLIENT_ID or TWITCH_CLIENT_SECRET`
);
process.exit(1);
}
if (!process.env.TWITCH_CLIENT_ID || !process.env.TWITCH_CLIENT_SECRET) {
console.error(
`${LOG_PREFIX}Missing environment parameters TWITCH_CLIENT_ID or TWITCH_CLIENT_SECRET`
);
return {
clientId: process.env.TWITCH_CLIENT_ID,
clientSecret: process.env.TWITCH_CLIENT_SECRET
};
}
const exitCode = 1;
async function getAuthProvider(): Promise<RefreshingAuthProvider> {
if (refreshAuthProvider) {
return refreshAuthProvider;
}
process.exit(exitCode);
}
let tokenData = await getTokenData();
const credentials = getClientCredentials();
refreshAuthProvider = new RefreshingAuthProvider(
{
clientId: credentials.clientId,
clientSecret: credentials.clientSecret,
onRefresh
},
tokenData
);
return refreshAuthProvider;
return {
clientId: process.env.TWITCH_CLIENT_ID,
clientSecret: process.env.TWITCH_CLIENT_SECRET,
};
}
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> {
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> {
const apiClient = await getApiClient();
const user = await apiClient.users.getUserById(userId);
const apiClient = await getApiClient();
const user = await apiClient.users.getUserById(userId);
if (!user) {
return null;
}
if (!user) {
return null;
}
return user.displayName;
return user.displayName;
}
async function createReward(
userId: UserIdResolvable,
data: HelixCreateCustomRewardData
userId: UserIdResolvable,
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(
channel: UserIdResolvable,
rewardId: string,
redemptionIds: Array<string> | string
channel: UserIdResolvable,
rewardId: string,
redemptionIds: Array<string> | string
) {
if (!Array.isArray(redemptionIds)) {
redemptionIds = [redemptionIds];
}
if (!Array.isArray(redemptionIds)) {
redemptionIds = [redemptionIds];
}
const apiClient = await getApiClient();
const apiClient = await getApiClient();
await apiClient.channelPoints.updateRedemptionStatusByIds(
channel,
rewardId,
redemptionIds,
"FULFILLED"
);
await apiClient.channelPoints.updateRedemptionStatusByIds(
channel,
rewardId,
redemptionIds,
"FULFILLED"
);
}
async function cancelRewards(
channel: UserIdResolvable,
rewardId: string,
redemptionIds: Array<string> | string
channel: UserIdResolvable,
rewardId: string,
redemptionIds: Array<string> | string
) {
if (!Array.isArray(redemptionIds)) {
redemptionIds = [redemptionIds];
}
if (!Array.isArray(redemptionIds)) {
redemptionIds = [redemptionIds];
}
const apiClient = await getApiClient();
const apiClient = await getApiClient();
await apiClient.channelPoints.updateRedemptionStatusByIds(
channel,
rewardId,
redemptionIds,
"CANCELED"
);
await apiClient.channelPoints.updateRedemptionStatusByIds(
channel,
rewardId,
redemptionIds,
"CANCELED"
);
}
export {
getAuthProvider,
getApiClient,
getUsernameFromId,
completeRewards,
cancelRewards,
createReward
getAuthProvider,
getApiClient,
getUsernameFromId,
completeRewards,
cancelRewards,
createReward,
};

View File

@@ -1,9 +1,8 @@
import { AddressInfo, Socket } from "net";
import { IncomingMessage, Server } from "http";
import { save, scheduledActions } from "./miniDb";
import { Action } from "../../interfaces/actions/Action";
import { AddressInfo } from "net";
import { Socket } from "net";
import WebSocket from "ws";
import express from "express";
import { handleClientAction } from "../chatClient";
@@ -17,98 +16,103 @@ const app = express();
const sockets: Array<WebSocket> = [];
const wsServer = new WebSocket.Server({
noServer: true
noServer: true,
});
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);
app.use(express.static(join(process.cwd(), "client")));
function listen() {
if (server) {
console.log(`${LOG_PREFIX_HTTP}Server is already running`);
return;
}
if (server) {
console.log(`${LOG_PREFIX_HTTP}Server is already running`);
return;
}
server = app.listen(!isDevelopment ? 8080 : 8081, "0.0.0.0");
let port = 8080;
server.on("listening", onListening);
server.on("upgrade", onUpgrade);
if (isDevelopment) {
port++;
}
server = app.listen(port, "0.0.0.0");
server.on("listening", onListening);
server.on("upgrade", onUpgrade);
}
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);
}
// 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`);
}
export { listen, broadcast };

View File

@@ -4,39 +4,40 @@ import { getUsernameFromId } from "../../helpers/twitch";
import { timeout } from "../../chatClient/clientActions";
async function highlightMessage(
msg: RedemptionMessage
msg: RedemptionMessage
): Promise<RedemptionMessage | undefined> {
if (!msg.message) {
console.log(`${LOG_PREFIX}Redemption has no message`);
if (!msg.message) {
console.log(`${LOG_PREFIX}Redemption has no message`);
return;
}
return;
}
const urlRegex =
/(https?:\/\/)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&/=]*)/;
const urlRegex =
/(https?:\/\/)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)/;
if (urlRegex.test(msg.message)) {
console.log(`${LOG_PREFIX}Message contains a url`);
const channel = await getUsernameFromId(parseInt(msg.channelId));
if (urlRegex.test(msg.message)) {
console.log(`${LOG_PREFIX}Message contains a url`);
const channel = await getUsernameFromId(parseInt(msg.channelId));
if (!channel) {
console.log(`${LOG_PREFIX}No channel found`);
if (!channel) {
console.log(`${LOG_PREFIX}No channel found`);
return;
}
return;
}
try {
const reason = "No se permite enviar enlaces en mensajes destacados";
try {
const reason = "No se permite enviar enlaces en mensajes destacados";
const timeoutSeconds = 10;
await timeout(channel, msg.userDisplayName, 10, reason);
} catch (e) {
// user probably cannot be timed out
}
await timeout(channel, msg.userDisplayName, timeoutSeconds, reason);
} catch (e) {
// user probably cannot be timed out
}
return;
}
return;
}
return msg;
return msg;
}
export { highlightMessage };

View File

@@ -12,56 +12,60 @@ const timeoutSeconds = 60;
const maxSafeShots = 5;
async function russianRoulette(
msg: RedemptionMessage
msg: RedemptionMessage
): Promise<RedemptionMessage | undefined> {
const { channelId, userDisplayName } = msg;
const channel = await getUsernameFromId(parseInt(channelId));
const { channelId, userDisplayName } = msg;
const channel = await getUsernameFromId(parseInt(channelId));
if (!channel) {
console.log(`${LOG_PREFIX}No channel found`);
if (!channel) {
console.log(`${LOG_PREFIX}No channel found`);
return;
}
return;
}
if (!gunsSafeShots[channelId]) {
gunsSafeShots[channelId] = maxSafeShots;
}
if (!gunsSafeShots[channelId]) {
gunsSafeShots[channelId] = maxSafeShots;
}
const win =
gunsSafeShots[channelId] > 0 &&
randomInt(gunsSafeShots[channelId]-- + 1) !== 0;
const noShots = 0;
if (gunsSafeShots[channelId] < 0 || !win) {
gunsSafeShots[channelId] = maxSafeShots;
}
const win =
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) {
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}`));
}
const promises: Array<Promise<unknown>> = [];
try {
await Promise.allSettled(promises);
} catch (e) {
if (e instanceof Error) {
console.log(`${LOG_PREFIX}${e.message}`);
}
}
if (!win) {
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}`));
}
return msg;
try {
await Promise.allSettled(promises);
} catch (e) {
if (e instanceof Error) {
console.log(`${LOG_PREFIX}${e.message}`);
}
}
return msg;
}
export { russianRoulette };

View File

@@ -7,70 +7,73 @@ import { getUsernameFromId } from "../../helpers/twitch";
// remove vip from a user to grant it to yourself
async function stealVip(
msg: RedemptionMessage
msg: RedemptionMessage
): Promise<RedemptionMessage | undefined> {
if (!msg.message) {
console.log(`${LOG_PREFIX}Redemption has no message`);
if (!msg.message) {
console.log(`${LOG_PREFIX}Redemption has no message`);
return;
}
return;
}
const channelId = parseInt(msg.channelId);
const channel = await getUsernameFromId(channelId);
const channelId = parseInt(msg.channelId);
const channel = await getUsernameFromId(channelId);
if (!channel) {
console.log(`${LOG_PREFIX}No channel found`);
if (!channel) {
console.log(`${LOG_PREFIX}No channel found`);
return;
}
return;
}
const addVipUser = msg.userDisplayName;
const removeVipUser = msg.message.toLowerCase();
const channelVips = vipUsers[channelId];
const addVipUser = msg.userDisplayName;
const removeVipUser = msg.message.toLowerCase();
const channelVips = vipUsers[channelId];
if (!channelVips.find(u => u.toLowerCase() === removeVipUser)) {
const message =
channelVips.length === 0
? "No hay nadie a quien puedas robar el VIP"
: `Solo puedes robar el VIP de: "${channelVips.sort().join('", "')}"`;
await say(channel, message);
if (!channelVips.find((u) => u.toLowerCase() === removeVipUser)) {
const message =
// eslint-disable-next-line no-magic-numbers
channelVips.length === 0
? "No hay nadie a quien puedas robar el VIP"
: `Solo puedes robar el VIP de: "${channelVips.sort().join('", "')}"`;
await say(channel, message);
return;
}
return;
}
if (channelVips.includes(addVipUser) || (await hasVip(channel, addVipUser))) {
console.log(`${LOG_PREFIX}@${addVipUser} is already VIP`);
if (channelVips.includes(addVipUser) || (await hasVip(channel, addVipUser))) {
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))) {
console.log(`${LOG_PREFIX}Could not remove VIP of @${removeVipUser}`);
return;
}
if (!removed && (await hasVip(channel, removeVipUser))) {
console.log(`${LOG_PREFIX}Could not remove VIP of @${removeVipUser}`);
return;
}
const added = await addVip(channel, addVipUser);
const added = await addVip(channel, addVipUser);
if (!added) {
await addVip(channel, removeVipUser);
if (!added) {
await addVip(channel, removeVipUser);
return;
}
return;
}
const removeIdx = channelVips.findIndex(
u => u.toLowerCase() === removeVipUser
);
const removeIdx = channelVips.findIndex(
(u) => u.toLowerCase() === removeVipUser
);
channelVips.splice(removeIdx);
channelVips.push(addVipUser);
save();
channelVips.splice(removeIdx);
channelVips.push(addVipUser);
save();
msg.message = `@${addVipUser} ha "tomado prestado" el VIP de @${removeVipUser}`;
await say(channel, msg.message);
// eslint-disable-next-line require-atomic-updates
msg.message = `@${addVipUser} ha "tomado prestado" el VIP de @${removeVipUser}`;
return msg;
await say(channel, msg.message);
return msg;
}
export { stealVip };

View File

@@ -4,40 +4,41 @@ import { getUsernameFromId } from "../../helpers/twitch";
import { timeout } from "../../chatClient/clientActions";
async function timeoutFriend(
msg: RedemptionMessage
msg: RedemptionMessage
): Promise<RedemptionMessage | undefined> {
const { message, channelId, userDisplayName } = msg;
if (!msg.message) {
console.log(`${LOG_PREFIX}Redemption has no message`);
const { message, channelId, userDisplayName } = msg;
if (!msg.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) {
console.log(`${LOG_PREFIX}No channel found`);
if (!channel) {
console.log(`${LOG_PREFIX}No channel found`);
return;
}
return;
}
const time = 60;
const reason = `Timeout dado por @${userDisplayName} con puntos del canal`;
const time = 60;
const reason = `Timeout dado por @${userDisplayName} con puntos del canal`;
try {
await timeout(channel, msg.message, time, reason);
try {
await timeout(channel, msg.message, time, reason);
msg.message = `@${userDisplayName} ha expulsado a @${message} por ${time} segundos`;
} catch (e) {
// user can not be timed out
if (e instanceof Error) {
console.error(`${LOG_PREFIX} ${e.message}`);
}
// eslint-disable-next-line require-atomic-updates
msg.message = `@${userDisplayName} ha expulsado a @${message} por ${time} segundos`;
} catch (e) {
// user can not be timed out
if (e instanceof Error) {
console.error(`${LOG_PREFIX} ${e.message}`);
}
return;
}
return;
}
return msg;
return msg;
}
export { timeoutFriend };

View File

@@ -1,8 +1,8 @@
import { PubSubClient, PubSubRedemptionMessage } from "@twurple/pubsub";
import {
cancelRewards,
completeRewards,
getAuthProvider
cancelRewards,
completeRewards,
getAuthProvider,
} from "../helpers/twitch";
import { RedemptionIds } from "../../enums/Redemptions";
@@ -20,107 +20,116 @@ import { timeoutFriend } from "./actions/timeoutFriend";
const LOG_PREFIX = "[PubSub] ";
async function registerUserListener(user: UserIdResolvable) {
const pubSubClient = new PubSubClient();
const userId = await pubSubClient.registerUserListener(
await getAuthProvider(),
user
);
/*const listener = */ await pubSubClient.onRedemption(userId, onRedemption);
async function onRedemption(message: PubSubRedemptionMessage) {
console.log(
`${LOG_PREFIX}Reward: "${message.rewardTitle}" (${message.rewardId}) redeemed by ${message.userDisplayName}`
);
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) {
console.log(
`${LOG_PREFIX}Reward: "${message.rewardTitle}" (${message.rewardId}) redeemed by ${message.userDisplayName}`
);
async function registerUserListener(user: UserIdResolvable) {
const pubSubClient = new PubSubClient();
const userId = await pubSubClient.registerUserListener(
await getAuthProvider(),
user
);
const raw = message[rawDataSymbol];
await pubSubClient.onRedemption(userId, onRedemption);
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;
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}`);
}
}
}
console.log(`${LOG_PREFIX}Connected & registered`);
}
export { registerUserListener, LOG_PREFIX };