noot
Some checks failed
commit-tag / commit-tag-image (push) Failing after 9s
commit-tag / commit-tag-image (./cmd/caddy) (push) Failing after 4m50s

This commit is contained in:
a 2025-03-02 05:49:36 -06:00
parent f6cade5065
commit 3f353f1630
No known key found for this signature in database
GPG Key ID: 2F22877AA4DFDADB
14 changed files with 180 additions and 170 deletions

View File

@ -4,4 +4,5 @@
export * from "./database"; export * from "./database";
export * from "./guild"; export * from "./guild";
export * from "./leaderboards";
export * from "./players"; export * from "./players";

View File

@ -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
`
}
})
}

View File

@ -95,7 +95,6 @@ export const scrape_online_players = async()=>{
name = EXCLUDED.name, name = EXCLUDED.name,
server = EXCLUDED.server server = EXCLUDED.server
` `
log.info(`inserted ${playerName} with uuid ${uuid} on ${server}`)
}catch(e) { }catch(e) {
log.warn(`failed to get uuid for ${playerName}`, { log.warn(`failed to get uuid for ${playerName}`, {
"err": e, "err": e,

View File

@ -1,12 +1,10 @@
import {bot} from "#/bot" import {bot} from "#/bot"
import { ActivityTypes, ApplicationCommandOptionTypes, InteractionTypes } from "discordeno" import { ActivityTypes, ApplicationCommandOptionTypes, InteractionTypes } from "discordeno"
import { InteractionHandler, MuxHandler, SlashHandler } from "./types" import { InteractionType, MuxHandler, SlashHandler } from "./types"
import { SlashCommandHandler } from "./slash_commands"
import { uuid4 } from "@temporalio/workflow" 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) { if(!interaction.data) {
return return
} }
@ -22,7 +20,6 @@ export const slashHandler: InteractionHandler = async (interaction) => {
} }
} }
const rootHandler = (await c.getAsync(SlashCommandHandler)).root()
let cur: SlashHandler | MuxHandler<any> = rootHandler let cur: SlashHandler | MuxHandler<any> = rootHandler
for(let i = 0; i < commandPath.length; i++) { 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}) 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) => { interactionCreate: async (interaction) => {
if(interaction.acknowledged) { if(interaction.acknowledged) {
return return
@ -58,7 +54,7 @@ export const events = {
if(interaction.type !== InteractionTypes.ApplicationCommand) { if(interaction.type !== InteractionTypes.ApplicationCommand) {
return return
} }
await slashHandler(interaction) await slashHandler(interaction, rootHandler)
return return
}, },
ready: async ({shardId}) => { ready: async ({shardId}) => {
@ -75,4 +71,4 @@ export const events = {
], ],
}) })
} }
} as typeof bot.events } as typeof bot.events}

View File

@ -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 { WYNN_GUILD_ID } from "#/constants"
import { inject, injectable } from "@needle-di/core" import { inject, injectable } from "@needle-di/core"
import { SlashHandler } from "./types" import { SlashHandler } from "./types"
import { PG } from "#/services/pg" import { PG } from "#/services/pg"
import { ApplicationCommandOptionTypes, ApplicationCommandTypes, CreateApplicationCommand } from "discordeno"
@injectable() @injectable()
export class SlashCommandHandler { export class SlashCommandHandler {
@ -10,12 +11,58 @@ export class SlashCommandHandler {
public readonly db = inject(PG) 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 { root(): SlashHandler {
return { return {
guild: { guild: {
info: async (interaction) => { 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) => { online: async (interaction) => {
const msg = await formGuildOnlineMessage( const msg = await formGuildOnlineMessage(

View File

@ -5,6 +5,48 @@ import { TabWriter } from "#/lib/util/tabwriter"
import * as md from 'ts-markdown-builder'; import * as md from 'ts-markdown-builder';
export const formGuildInfoMessage = async (guild_id: string, sql:Sql): Promise<CreateMessageOptions & InteractionCallbackOptions> => {
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<CreateMessageOptions & InteractionCallbackOptions> => { export const formGuildOnlineMessage = async (guild_id: string, sql:Sql): Promise<CreateMessageOptions & InteractionCallbackOptions> => {
const result = await sql`select const result = await sql`select
name, name,

View File

@ -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;

View File

@ -1,20 +1,29 @@
import { Command } from 'clipanion'; import { Command } from 'clipanion';
import * as BotCommands from "#/slashcommands";
// di // di
import "#/services/pg" import "#/services/pg"
import { DISCORD_GUILD_ID } from '#/constants'; import { DISCORD_GUILD_ID } from '#/constants';
import { bot } from '#/bot'; import { bot } from '#/bot';
import { events } from '#/bot/botevent/handler'; 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 { export class BotCommand extends Command {
static paths = [['bot']]; static paths = [['bot']];
async execute() { 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'); 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'); console.log('connecting bot to gateway');
await bot.start(); await bot.start();
console.log('bot connected'); console.log('bot connected');

View File

@ -12,7 +12,7 @@ import { config } from '#/config';
import * as activities from '../activities'; import * as activities from '../activities';
import path from 'path'; import path from 'path';
import { Client, ScheduleNotFoundError, ScheduleOptions, ScheduleOverlapPolicy } from '@temporalio/client'; 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'; import { PG } from '#/services/pg';
@ -34,6 +34,22 @@ const schedules: ScheduleOptions[] = [
}] }]
}, },
}, },
{
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", scheduleId: "update-all-guilds",
action: { action: {

View File

@ -43,7 +43,7 @@ create table if not exists wynn_guild_info (
uid UUID not null, uid UUID not null,
name text not null, name text not null,
prefix text not null, prefix text not null,
level bigint , level bigint,
xp_percent bigint, xp_percent bigint,
territories bigint, territories bigint,
wars bigint, wars bigint,
@ -79,8 +79,11 @@ create table if not exists wynn_guild_season_results (
value jsonb not null, value jsonb not null,
primary key (discord_guild, name) 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;`
}),
] ]

View File

@ -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,
},
],
}

View File

@ -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,
},
],
}

View File

@ -1,6 +0,0 @@
/**
* @file Automatically generated by barrelsby.
*/
export * from "./admin";
export * from "./guild_overview";

View File

@ -2,7 +2,7 @@
import { proxyActivities } from '@temporalio/workflow'; import { proxyActivities } from '@temporalio/workflow';
import type * as activities from '#/activities'; import type * as activities from '#/activities';
const { update_guild, update_all_guilds } = proxyActivities<typeof activities>({ const { update_guild, update_all_guilds, update_guild_levels } = proxyActivities<typeof activities>({
startToCloseTimeout: '1 minute', startToCloseTimeout: '1 minute',
}); });
@ -10,6 +10,10 @@ export const workflowSyncAllGuilds = async() => {
await update_all_guilds() await update_all_guilds()
} }
export const workflowSyncGuildLeaderboardInfo = async() => {
await update_guild_levels()
}
export const workflowSyncGuilds = async() => { export const workflowSyncGuilds = async() => {
// TODO side effect // TODO side effect
const guildNames = [ const guildNames = [
@ -22,3 +26,4 @@ export const workflowSyncGuilds = async() => {
}) })
} }
} }