Skip to content

Commit

Permalink
Merge pull request #285 from slmnio/rescheduling
Browse files Browse the repository at this point in the history
Rescheduling system
  • Loading branch information
slmnio authored Jan 1, 2025
2 parents 6c76015 + 5d6c289 commit b81084f
Show file tree
Hide file tree
Showing 77 changed files with 4,377 additions and 769 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"globals": "^15.1.0",
"postcss-html": "^1.6.0",
"typescript": "^5.4.5",
"typescript-eslint": "^7.8.0"
"typescript-eslint": "^7.8.0",
"@vue/language-server": "^2.1.10"
},
"packageManager": "[email protected]"
}
797 changes: 727 additions & 70 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"chalk": "^5.3.0",
"cors": "^2.8.5",
"discord-api-types": "^0.37.79",
"discord.js": "^14.15.2",
"discord.js": "14.17.0",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"libsodium-wrappers": "^0.7.10",
Expand Down
3 changes: 2 additions & 1 deletion server/src/action-utils/action-manager-models.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class Action {
* @param {string[]} auth
* @param {string[]} requiredParams
* @param {string[]} optionalParams
* @param {function} registerFunction
*/
constructor({
key,
Expand Down Expand Up @@ -75,7 +76,7 @@ export class Action {
get: (...args) => Cache.get(...args),
createRecord: (tableName, data) => createRecord(Cache, tableName, [data]),
createRecords: (tableName, items) => createRecord(Cache, tableName, items),
updateRecord: (tableName, item, data, source) => updateRecord(Cache, tableName, item, data, source),
updateRecord: (tableName, item, data, source) => updateRecord(Cache, tableName, item, data, source || `actions/${this.key}`),
auth: Cache.auth,
permissions
};
Expand Down
114 changes: 107 additions & 7 deletions server/src/action-utils/action-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { StaticAuthProvider } from "@twurple/auth";
import { ApiClient } from "@twurple/api";
import { verboseLog } from "../discord/slmngg-log.js";
import { get } from "./action-cache.js";
import client from "../discord/client.js";

const airtable = new Airtable({ apiKey: process.env.AIRTABLE_KEY });
const slmngg = airtable.base(process.env.AIRTABLE_APP);
Expand Down Expand Up @@ -51,14 +52,14 @@ const TimeOffset = 3 * 1000;
*/
export async function updateRecord(Cache, tableName, item, data, source = undefined) {
// see: airtable-interface.js customUpdater
console.log(`[update record] updating table=${tableName} id=${item.id}`, data);
console.log(`[update record] ${source ? `{${source}} ` : ""}updating table=${tableName} id=${item.id}`, data);

let slmnggData = {
__tableName: tableName,
...deAirtable({ ...item, ...data }),
modified: (new Date((new Date()).getTime() + TimeOffset)).toString()
};
verboseLog(`Editing record on **${tableName}** \`${item.id}\``, data);
verboseLog(`Editing record on **${tableName}** \`${item.id}\`${source ? ` {${source}}` : ""}`, data);
// Eager update
Cache.set(cleanID(item.id), slmnggData, { eager: true, source });

Expand All @@ -81,12 +82,13 @@ export async function updateRecord(Cache, tableName, item, data, source = undefi
* @param {Cache} Cache
* @param {string} tableName
* @param {object[]} records
* @param {string | null} source
*/
export async function createRecord(Cache, tableName, records) {
console.log(`[create record] creating table=${tableName} records=${records.length}`);
export async function createRecord(Cache, tableName, records, source = null) {
console.log(`[create record] ${source ? `{${source}} ` : ""}creating table=${tableName} records=${records.length}`);
try {
let newRecords = await slmngg(tableName).create(records.map(recordData => {
verboseLog(`Creating record on **${tableName}** `, recordData);
verboseLog(`Creating record on **${tableName}**${source ? ` {${source}}` : ""}`, recordData);
return {
fields: recordData
};
Expand Down Expand Up @@ -193,6 +195,7 @@ export async function getMatchData(broadcast, requireAll) {
* @returns {Promise<({report: Report | undefined, match: Match})>}
*/
export async function getMatchScoreReporting(matchID) {
/** @type {Match} */
const match = await get(matchID);
let report;

Expand All @@ -211,8 +214,45 @@ export async function getMatchScoreReporting(matchID) {
if (!eventSettings?.reporting?.score?.use) throw "Score reporting is not enabled on this match";

// check existing report
if (match?.reports?.[0]) {
const firstReport = await get(match?.reports?.[0]);
if ((match?.reports || []).length) {
const reports = await Promise.all((match?.reports || []).map(rID => get(rID)));
const firstReport = reports.find(r => r.type === "Scores");
if (firstReport?.id) {
report = firstReport;
}
}

return { match, report };
}
/**
*
* @param matchID
* @param { {excludeCompleted: Boolean } } settings
* @returns {Promise<({report: Report | undefined, match: Match})>}
*/
export async function getMatchRescheduling(matchID, { excludeCompleted } = {}) {
const match = await get(matchID);
let report;

if (!match?.id) throw "Couldn't load match data";

if (!match?.event?.[0]) throw "Couldn't load event data for this match";
const event = await get(match?.event?.[0]);
if (!event?.id) throw "Couldn't load event data for this match";

// event score reporting must be active

if (!event?.blocks) throw "Event doesn't have rescheduling set up";

/** @type {EventSettings} */
const eventSettings = JSON.parse(event.blocks);
if (!eventSettings?.reporting?.rescheduling?.use) throw "Rescheduling is not enabled on this event";
if (!(match?.earliest_start || match?.latest_start)) throw "Rescheduling is not set up on this match";

// check existing report
if (match?.reports?.length) {
const reports = await Promise.all((match?.reports || []).map(rID => get(rID)));
const firstReport = reports.find(r => r.type === "Rescheduling" && (excludeCompleted ? !r.approved : true));
if (firstReport?.id) {
report = firstReport;
}
Expand All @@ -225,6 +265,7 @@ export async function getTwitchAPIClient(channel) {
if (!channel) throw("Internal error connecting to Twitch");
try {
const accessToken = await Cache.auth.getTwitchAccessToken(channel);
// this warning is because the twitch auth data is thrown into the general auth map but it shouldn't actually cause an error here
const authProvider = new StaticAuthProvider(process.env.TWITCH_CLIENT_ID, accessToken);
return new ApiClient({authProvider});
} catch (e) {
Expand Down Expand Up @@ -319,3 +360,62 @@ export async function findMember(player, team, guild) {
}
return { member, fixes };
}

export async function checkDeleteMessage(mapObject, keyPrefix) {
if (mapObject.get(`${keyPrefix}_message_id`) && mapObject.get(`${keyPrefix}_channel_id`)) {
try {
const channel = await client.channels.fetch(mapObject.get(`${keyPrefix}_channel_id`));
if (channel?.isSendable()) await channel.messages.delete(mapObject.get(`${keyPrefix}_message_id`));
} catch (e) {
console.error(`Error trying to delete ${keyPrefix} message`, e);
} finally {
mapObject.push(`${keyPrefix}_message_id`, null);
mapObject.push(`${keyPrefix}_channel_id`, null);
}
}
}

/**
* @param {object} options
* @param {string} options.key
* @param {MapObject} options.mapObject
* @param {(Snowflake | null)=} options.channelID
* @param {(string | null | object)=} options.content
* @param {Function=} options.success
* @returns {Promise<MapObject>}
* @deprecated Use keyed sendRecordedMessage instead
*/
export async function sendMessage({
key,
mapObject,
channelID,
content,
success,
}) {
if (!channelID) {
console.warn(`Can't send ${key} message without a channel`);
return mapObject;
}
if (!content) {
console.warn(`Can't send ${key} message without content`);
return mapObject;
}
const channel = await client.channels.fetch(channelID);
if (channel?.isSendable()) {
try {
const message = await channel.send(content);
mapObject.push(`${key}_channel_id`, channel.id);
mapObject.push(`${key}_message_id`, message.id);
if (success) await success(mapObject);
} catch (e) {
console.error(`Sending error ${key}`, e);
console.dir(e?.rawError?.errors, { depth: null, colors: true });
}
}
return mapObject;
}

export function hammerTime(timeString) {
let start = new Date(timeString).getTime();
return `<t:${Math.floor(start / 1000)}:F>`;
}
72 changes: 68 additions & 4 deletions server/src/action-utils/ts-action-utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { Snowflake } from "discord-api-types/globals";
import { Match } from "../types.js";
import { get } from "./action-cache.js";
import { MapObject } from "../discord/managers.js";
import client from "../discord/client.js";
import { Guild } from "discord.js";
import { cleanID } from "./action-utils.js";
import { ChannelType, Guild, MessageCreateOptions, MessagePayload } from "discord.js";
import { cleanID, sendMessage } from "./action-utils.js";


export async function generateMatchReportText(match: Match) {
Expand Down Expand Up @@ -126,9 +127,19 @@ export async function generateMatchReportText(match: Match) {
mapLine.push(map.replay_code);
}


lines.push(mapLine.join(" - "));

if (map.team_1_picks?.length || map.team_2_picks?.length) {
const teamPicks = [];

for (const teamI of [0, 1]) {
const team = teams[teamI];
const bannedHeroes = await Promise.all(([map.team_1_picks, map.team_2_picks][teamI] || []).map(id => get(id)));
teamPicks.push(`${team.code || team.name} ban: ${bannedHeroes.map(hero => hero.icon_emoji_text || hero.name).join(" ")}`);
}

lines.push(`Picks: ${teamPicks.join(" / ")}`);
}
if (map.team_1_bans?.length || map.team_2_bans?.length) {
const teamBans = [];

Expand All @@ -138,8 +149,16 @@ export async function generateMatchReportText(match: Match) {
teamBans.push(`${team.code || team.name} ban: ${bannedHeroes.map(hero => hero.icon_emoji_text || hero.name).join(" ")}`);
}

lines.push(`> ${teamBans.join(" | ")}`);
const banCount = (map.team_1_bans?.length || 0) + (map.team_2_bans?.length || 0);
// if there are only 2 bans and no picks, add to the current line
if (banCount <= 2 && !map.team_1_picks?.length && !map.team_2_picks?.length) {
lines[lines.length - 1] += ` - Bans: ${teamBans.join(" / ")}`;
} else {
lines.push(`Bans: ${teamBans.join(" / ")}`);
}

}

}


Expand All @@ -150,3 +169,48 @@ export async function generateMatchReportText(match: Match) {
return null;
}
}

export async function checkDeleteMessage<KeyType extends string>(mapObject: MapObject, keyPrefix: KeyType) {
if (mapObject.get(`${keyPrefix}_message_id`) && mapObject.get(`${keyPrefix}_channel_id`)) {
try {
const channel = await client.channels.fetch(mapObject.get(`${keyPrefix}_channel_id`));
if (channel) {
console.log(`${keyPrefix} - ${channel.id} ${channel.type !== ChannelType.DM ? channel.name : ""}`);
} else {
console.warn(`${keyPrefix} - No channel`);
}
if (channel?.isSendable()) await channel.messages.delete(mapObject.get(`${keyPrefix}_message_id`));
} catch (e) {
console.error(`Error trying to delete ${keyPrefix} message`, e);
} finally {
mapObject.push(`${keyPrefix}_message_id`, null);
mapObject.push(`${keyPrefix}_channel_id`, null);
}
}
}

export async function looseDeleteRecordedMessage<KeyType extends string>(mapObject: MapObject, keyPrefix: KeyType) {
console.log("Loose delete", mapObject.data, keyPrefix);
await checkDeleteMessage(mapObject, keyPrefix);
return mapObject;
}
export async function looseDeleteRecordedMessages<KeyType extends string>(mapObject: MapObject, keyPrefixes: KeyType[]) {
console.log("Loose delete multiple", mapObject.data, keyPrefixes);
if (!mapObject?.data) return mapObject;
console.log(mapObject.data);
await Promise.all(keyPrefixes.map(async keyPrefix => checkDeleteMessage(mapObject, keyPrefix)));
return mapObject;
}

export async function sendRecordedMessage<KeyType extends string>({ key, mapObject, channelID, content, success } :
{
key: KeyType;
mapObject: MapObject;
channelID?: Snowflake;
content?: null | string | MessagePayload | MessageCreateOptions;
success?: (updatedMapObject: MapObject) => void;

Check warning on line 211 in server/src/action-utils/ts-action-utils.ts

View workflow job for this annotation

GitHub Actions / build (server)

'updatedMapObject' is defined but never used
}
): Promise<MapObject> {
console.log("Recorded message", key, mapObject.data);
return sendMessage({ key, mapObject, channelID, content, success });
}
Loading

0 comments on commit b81084f

Please sign in to comment.