diff --git a/ts/src/activities/index.ts b/ts/src/activities/index.ts index dc76e0c..9159151 100644 --- a/ts/src/activities/index.ts +++ b/ts/src/activities/index.ts @@ -4,4 +4,5 @@ export * from "./database"; export * from "./guild"; +export * from "./leaderboards"; export * from "./players"; diff --git a/ts/src/activities/leaderboards.ts b/ts/src/activities/leaderboards.ts new file mode 100644 index 0000000..1410104 --- /dev/null +++ b/ts/src/activities/leaderboards.ts @@ -0,0 +1,34 @@ +import { c } from "#/di"; +import { WApi } from "#/lib/wynn/wapi"; +import { PG } from "#/services/pg"; +import { type } from "arktype"; + +export async function update_guild_levels() { + const api = await c.getAsync(WApi) + const ans = await api.get('/v3/leaderboards/guildLevel', {resultLimit: 1000}) + if(ans.status !== 200){ + throw new Error('Failed to get guild list from wapi') + } + const parsed = type({ + "[string]": { + uuid: "string", + name: "string", + prefix: "string", + xp: "number", + level: "number", + } + }).assert(ans.data) + const { db } = await c.getAsync(PG) + await db.begin(async (sql) => { + for(const [_, guild] of Object.entries(parsed)){ + await sql`insert into wynn_guild_info + (uid, name, prefix, xp, level) + values + (${guild.uuid}, ${guild.name}, ${guild.prefix}, ${guild.xp}, ${guild.level}) + on conflict (uid) do update set + xp = EXCLUDED.xp, + level = EXCLUDED.level + ` + } + }) +} diff --git a/ts/src/activities/players.ts b/ts/src/activities/players.ts index a6687ea..039f229 100644 --- a/ts/src/activities/players.ts +++ b/ts/src/activities/players.ts @@ -95,7 +95,6 @@ export const scrape_online_players = async()=>{ name = EXCLUDED.name, server = EXCLUDED.server ` - log.info(`inserted ${playerName} with uuid ${uuid} on ${server}`) }catch(e) { log.warn(`failed to get uuid for ${playerName}`, { "err": e, diff --git a/ts/src/bot/botevent/handler.ts b/ts/src/bot/botevent/handler.ts index 1a92898..05317bd 100644 --- a/ts/src/bot/botevent/handler.ts +++ b/ts/src/bot/botevent/handler.ts @@ -1,12 +1,10 @@ import {bot} from "#/bot" import { ActivityTypes, ApplicationCommandOptionTypes, InteractionTypes } from "discordeno" -import { InteractionHandler, MuxHandler, SlashHandler } from "./types" -import { SlashCommandHandler } from "./slash_commands" +import { InteractionType, MuxHandler, SlashHandler } from "./types" import { uuid4 } from "@temporalio/workflow" -import { c } from "#/di" -export const slashHandler: InteractionHandler = async (interaction) => { +export const slashHandler = async (interaction: InteractionType, rootHandler: SlashHandler) => { if(!interaction.data) { return } @@ -22,7 +20,6 @@ export const slashHandler: InteractionHandler = async (interaction) => { } } - const rootHandler = (await c.getAsync(SlashCommandHandler)).root() let cur: SlashHandler | MuxHandler = rootHandler for(let i = 0; i < commandPath.length; i++) { @@ -47,10 +44,9 @@ export const slashHandler: InteractionHandler = async (interaction) => { return interaction.respond(`invokation exception: ${errId}`, {isPrivate: true}) } } - // ok now we need to go down the handler tree. } -export const events = { +export const events = (rootHandler: SlashHandler) => {return { interactionCreate: async (interaction) => { if(interaction.acknowledged) { return @@ -58,7 +54,7 @@ export const events = { if(interaction.type !== InteractionTypes.ApplicationCommand) { return } - await slashHandler(interaction) + await slashHandler(interaction, rootHandler) return }, ready: async ({shardId}) => { @@ -75,4 +71,4 @@ export const events = { ], }) } -} as typeof bot.events +} as typeof bot.events} diff --git a/ts/src/bot/botevent/slash_commands.ts b/ts/src/bot/botevent/slash_commands.ts index 5188507..9135e69 100644 --- a/ts/src/bot/botevent/slash_commands.ts +++ b/ts/src/bot/botevent/slash_commands.ts @@ -1,8 +1,9 @@ -import { formGuildLeaderboardMessage, formGuildOnlineMessage } from "#/bot/common/guild" +import { formGuildInfoMessage, formGuildLeaderboardMessage, formGuildOnlineMessage } from "#/bot/common/guild" import { WYNN_GUILD_ID } from "#/constants" import { inject, injectable } from "@needle-di/core" import { SlashHandler } from "./types" import { PG } from "#/services/pg" +import { ApplicationCommandOptionTypes, ApplicationCommandTypes, CreateApplicationCommand } from "discordeno" @injectable() export class SlashCommandHandler { @@ -10,12 +11,58 @@ export class SlashCommandHandler { public readonly db = inject(PG) ) { } - + commands(): CreateApplicationCommand[] { + return [ + { + name: `guild`, + description: "guild commands", + type: ApplicationCommandTypes.ChatInput, + options: [ + { + name: "leaderboard", + description: "view the current leaderboard", + type: ApplicationCommandOptionTypes.SubCommand, + }, + { + name: "info", + description: "view guild information", + type: ApplicationCommandOptionTypes.SubCommand, + }, + { + name: "online", + description: "show online players", + type: ApplicationCommandOptionTypes.SubCommand, + }, + ], + }, + { + name: "admin", + description: "admin commands", + type: ApplicationCommandTypes.ChatInput, + defaultMemberPermissions: [ + "ADMINISTRATOR", + ], + options: [ + { + name: "set_wynn_guild", + description: "set the default wynncraft guild for the server", + type: ApplicationCommandOptionTypes.SubCommand, + }, + ], + } + ] + } root(): SlashHandler { return { guild: { info: async (interaction) => { - interaction.respond("TODO: guild info") + const msg = await formGuildInfoMessage( + WYNN_GUILD_ID, + this.db.sql, + ) + await interaction.respond(msg, { + withResponse: true, + }) }, online: async (interaction) => { const msg = await formGuildOnlineMessage( diff --git a/ts/src/bot/common/guild.ts b/ts/src/bot/common/guild.ts index 9b2bcce..54dac8f 100644 --- a/ts/src/bot/common/guild.ts +++ b/ts/src/bot/common/guild.ts @@ -5,16 +5,58 @@ import { TabWriter } from "#/lib/util/tabwriter" import * as md from 'ts-markdown-builder'; +export const formGuildInfoMessage = async (guild_id: string, sql:Sql): Promise => { + const result = await sql` + +with ranked as (select + uid, + name, + prefix, + level, + xp, + territories, + wars, + rank() over (order by xp desc) as xp_rank + from wynn_guild_info +) +select * from ranked +where ranked.uid = ${guild_id} +` + + if(result.length == 0) { + return { + content: "no guild found", + } + } + + const guild = result[0] + + const output = [ + md.heading("[wip] guild info"), + md.heading("overview"), + `[${guild.prefix}] ${guild.name} +level: ${guild.level} +guild xp rank: ${guild.xp_rank === 1000 ? "1000+" : guild.xp_rank} +xp: ${guild.xp} +territories: ${guild.territories} +wars: ${guild.wars}`, + ].join("\n\n") + + + return { + content: output, + } +} export const formGuildOnlineMessage = async (guild_id: string, sql:Sql): Promise => { const result = await sql`select name, rank, contributed, minecraft_user.server as server - from wynn_guild_members inner join minecraft_user - on minecraft_user.uid = wynn_guild_members.member_id - where minecraft_user.server is not null - and wynn_guild_members.guild_id = ${guild_id} + from wynn_guild_members inner join minecraft_user + on minecraft_user.uid = wynn_guild_members.member_id + where minecraft_user.server is not null + and wynn_guild_members.guild_id = ${guild_id} ` const members = type({ name: "string", @@ -56,9 +98,9 @@ export const formGuildLeaderboardMessage = async (guild_id: string, sql:Sql): Pr name, rank, contributed - from wynn_guild_members inner join minecraft_user - on minecraft_user.uid = wynn_guild_members.member_id - where wynn_guild_members.guild_id = ${guild_id} + from wynn_guild_members inner join minecraft_user + on minecraft_user.uid = wynn_guild_members.member_id + where wynn_guild_members.guild_id = ${guild_id} ` const members = type({ name: "string", diff --git a/ts/src/cmd/bot.js b/ts/src/cmd/bot.js deleted file mode 100644 index e2a242a..0000000 --- a/ts/src/cmd/bot.js +++ /dev/null @@ -1,90 +0,0 @@ -"use strict"; -var __extends = (this && this.__extends) || (function () { - var extendStatics = function (d, b) { - extendStatics = Object.setPrototypeOf || - ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || - function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; - return extendStatics(d, b); - }; - return function (d, b) { - if (typeof b !== "function" && b !== null) - throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); - extendStatics(d, b); - function __() { this.constructor = d; } - d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); - }; -})(); -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -var __generator = (this && this.__generator) || function (thisArg, body) { - var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); - return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; - function verb(n) { return function (v) { return step([n, v]); }; } - function step(op) { - if (f) throw new TypeError("Generator is already executing."); - while (g && (g = 0, op[0] && (_ = 0)), _) try { - if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; - if (y = 0, t) op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: case 1: t = op; break; - case 4: _.label++; return { value: op[1], done: false }; - case 5: _.label++; y = op[1]; op = [0]; continue; - case 7: op = _.ops.pop(); _.trys.pop(); continue; - default: - if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } - if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } - if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } - if (t[2]) _.ops.pop(); - _.trys.pop(); continue; - } - op = body.call(thisArg, _); - } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } - if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; - } -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.BotCommand = void 0; -var clipanion_1 = require("clipanion"); -var BotCommands = require("#/slashcommands"); -// di -require("#/services/pg"); -var constants_1 = require("#/constants"); -var bot_1 = require("#/bot"); -var handler_1 = require("#/bot/botevent/handler"); -var BotCommand = /** @class */ (function (_super) { - __extends(BotCommand, _super); - function BotCommand() { - return _super !== null && _super.apply(this, arguments) || this; - } - BotCommand.prototype.execute = function () { - return __awaiter(this, void 0, void 0, function () { - return __generator(this, function (_a) { - switch (_a.label) { - case 0: - bot_1.bot.events = handler_1.events; - console.log('registring slash commands'); - return [4 /*yield*/, bot_1.bot.rest.upsertGuildApplicationCommands(constants_1.DISCORD_GUILD_ID, Object.values(BotCommands))]; - case 1: - _a.sent(); - console.log('connecting bot to gateway'); - return [4 /*yield*/, bot_1.bot.start()]; - case 2: - _a.sent(); - console.log('bot connected'); - return [2 /*return*/]; - } - }); - }); - }; - BotCommand.paths = [['bot']]; - return BotCommand; -}(clipanion_1.Command)); -exports.BotCommand = BotCommand; diff --git a/ts/src/cmd/bot.ts b/ts/src/cmd/bot.ts index bcc2a67..5128875 100644 --- a/ts/src/cmd/bot.ts +++ b/ts/src/cmd/bot.ts @@ -1,20 +1,29 @@ import { Command } from 'clipanion'; -import * as BotCommands from "#/slashcommands"; // di import "#/services/pg" import { DISCORD_GUILD_ID } from '#/constants'; import { bot } from '#/bot'; import { events } from '#/bot/botevent/handler'; +import { SlashCommandHandler } from '#/bot/botevent/slash_commands'; +import { c } from '#/di'; +import { config } from '#/config'; export class BotCommand extends Command { static paths = [['bot']]; async execute() { - bot.events = events + if(!config.DISCORD_TOKEN) { + throw new Error('no discord token found. bot cant start'); + } + const rootHandler = await c.getAsync(SlashCommandHandler) + bot.events = events(rootHandler.root()) console.log('registring slash commands'); - await bot.rest.upsertGuildApplicationCommands(DISCORD_GUILD_ID, Object.values(BotCommands)) + await bot.rest.upsertGuildApplicationCommands(DISCORD_GUILD_ID, rootHandler.commands()).catch(console.error) + await bot.rest.upsertGuildApplicationCommands("547828454972850196", rootHandler.commands()).catch(console.error) + + console.log('connecting bot to gateway'); await bot.start(); console.log('bot connected'); diff --git a/ts/src/cmd/worker.ts b/ts/src/cmd/worker.ts index 1f793fa..322574b 100644 --- a/ts/src/cmd/worker.ts +++ b/ts/src/cmd/worker.ts @@ -12,7 +12,7 @@ import { config } from '#/config'; import * as activities from '../activities'; import path from 'path'; import { Client, ScheduleNotFoundError, ScheduleOptions, ScheduleOverlapPolicy } from '@temporalio/client'; -import { workflowSyncAllGuilds, workflowSyncGuilds, workflowSyncOnline } from '#/workflows'; +import { workflowSyncAllGuilds, workflowSyncGuilds, workflowSyncOnline, workflowSyncGuildLeaderboardInfo } from '#/workflows'; import { PG } from '#/services/pg'; @@ -35,7 +35,23 @@ const schedules: ScheduleOptions[] = [ }, }, { - scheduleId: "update-all-guilds", + scheduleId: "update_guild_leaderboards", + action: { + type: 'startWorkflow', + workflowType: workflowSyncGuildLeaderboardInfo, + taskQueue: 'wynn-worker', + }, + policies: { + overlap: ScheduleOverlapPolicy.SKIP, + }, + spec: { + intervals: [{ + every: '5 minutes', + }] + }, + }, + { + scheduleId: "update-all-guilds", action: { type: 'startWorkflow', workflowType: workflowSyncAllGuilds, diff --git a/ts/src/services/pg/migrations.ts b/ts/src/services/pg/migrations.ts index c4fbef0..b8d9e86 100644 --- a/ts/src/services/pg/migrations.ts +++ b/ts/src/services/pg/migrations.ts @@ -43,7 +43,7 @@ create table if not exists wynn_guild_info ( uid UUID not null, name text not null, prefix text not null, - level bigint , + level bigint, xp_percent bigint, territories bigint, wars bigint, @@ -79,8 +79,11 @@ create table if not exists wynn_guild_season_results ( value jsonb not null, primary key (discord_guild, name) )` - }) + }), + migration("add-guild-xp", async (sql)=>{ + await sql`alter table wynn_guild_info add xp bigint not null default 0;` + }), ] diff --git a/ts/src/slashcommands/admin.ts b/ts/src/slashcommands/admin.ts deleted file mode 100644 index 847597e..0000000 --- a/ts/src/slashcommands/admin.ts +++ /dev/null @@ -1,20 +0,0 @@ - -import {ApplicationCommandOptionTypes, ApplicationCommandTypes, CreateApplicationCommand } from "discordeno"; - - - -export const AdminCommands: CreateApplicationCommand = { - name: "admin", - description: "admin commands", - type: ApplicationCommandTypes.ChatInput, - defaultMemberPermissions: [ - "ADMINISTRATOR", - ], - options: [ - { - name: "set_wynn_guild", - description: "set the default wynncraft guild for the server", - type: ApplicationCommandOptionTypes.SubCommand, - }, - ], -} diff --git a/ts/src/slashcommands/guild_overview.ts b/ts/src/slashcommands/guild_overview.ts deleted file mode 100644 index 378acc0..0000000 --- a/ts/src/slashcommands/guild_overview.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {ApplicationCommandOptionTypes, ApplicationCommandTypes, CreateApplicationCommand } from "discordeno"; - - - -export const GuildCommands: CreateApplicationCommand = { - name: "guild", - description: "guild commands", - type: ApplicationCommandTypes.ChatInput, - options: [ - { - name: "leaderboard", - description: "view the current leaderboard", - type: ApplicationCommandOptionTypes.SubCommand, - }, - { - name: "info", - description: "view guild information", - type: ApplicationCommandOptionTypes.SubCommand, - }, - { - name: "online", - description: "show online players", - type: ApplicationCommandOptionTypes.SubCommand, - }, - ], -} diff --git a/ts/src/slashcommands/index.ts b/ts/src/slashcommands/index.ts deleted file mode 100644 index fe109f6..0000000 --- a/ts/src/slashcommands/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * @file Automatically generated by barrelsby. - */ - -export * from "./admin"; -export * from "./guild_overview"; diff --git a/ts/src/workflows/guilds.ts b/ts/src/workflows/guilds.ts index f54b6ce..3c7d528 100644 --- a/ts/src/workflows/guilds.ts +++ b/ts/src/workflows/guilds.ts @@ -2,7 +2,7 @@ import { proxyActivities } from '@temporalio/workflow'; import type * as activities from '#/activities'; -const { update_guild, update_all_guilds } = proxyActivities({ +const { update_guild, update_all_guilds, update_guild_levels } = proxyActivities({ startToCloseTimeout: '1 minute', }); @@ -10,6 +10,10 @@ export const workflowSyncAllGuilds = async() => { await update_all_guilds() } +export const workflowSyncGuildLeaderboardInfo = async() => { + await update_guild_levels() +} + export const workflowSyncGuilds = async() => { // TODO side effect const guildNames = [ @@ -22,3 +26,4 @@ export const workflowSyncGuilds = async() => { }) } } +