noot
Some checks failed
commit-tag / commit-tag-image (push) Failing after 57s

This commit is contained in:
a 2025-03-01 15:10:31 -06:00
parent 7affca2bbc
commit 6e1595a7c5
No known key found for this signature in database
GPG Key ID: 2F22877AA4DFDADB
35 changed files with 792 additions and 286 deletions

8
ts/.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

6
ts/.idea/jsLibraryMappings.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<file url="file://$PROJECT_DIR$" libraries="{Node.js Core}" />
</component>
</project>

6
ts/.idea/misc.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
ts/.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/ts.iml" filepath="$PROJECT_DIR$/.idea/ts.iml" />
</modules>
</component>
</project>

9
ts/.idea/ts.iml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
ts/.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

Binary file not shown.

View File

@ -14,4 +14,7 @@ COPY --from=builder /build/node_modules node_modules
COPY tsconfig.json package.json cli .
COPY src src
# make sure it passes compilation before compiling
RUN yarn tsc
ENTRYPOINT ["/app/cli"]

View File

@ -1,12 +1,13 @@
{
"name": "backend",
"packageManager": "yarn@4.6.0",
"packageManager": "yarn@4.6.0+sha512.5383cc12567a95f1d668fbe762dfe0075c595b4bfff433be478dbbe24e05251a8e8c3eb992a986667c1d53b6c3a9c85b8398c35a960587fbd9fa3a0915406728",
"scripts": {
"barrels": "barrelsby -c barrelsby.json --delete"
},
"devDependencies": {
"@types/object-hash": "^3",
"barrelsby": "^2.8.1",
"knip": "^5.45.0",
"rollup": "^4.34.8",
"typescript": "5.7.3"
},

View File

@ -1,6 +1,6 @@
import { WApiV3ItemDatabase } from "#/lib/types";
import { WApi } from "#/lib/wapi";
import { WApiV3ItemDatabase } from "#/lib/wynn/types";
import { WApi } from "#/lib/wynn/wapi";
import { ArkError, ArkErrors } from "arktype";
export async function update_wynn_items() {

View File

@ -1,6 +1,7 @@
import { container, T_PG } from "#/di";
import { WapiV3GuildOverview } from "#/lib/types";
import { WApi } from "#/lib/wapi";
import { c } from "#/di";
import { WapiV3GuildOverview } from "#/lib/wynn/types";
import { WApi } from "#/lib/wynn/wapi";
import { PG } from "#/services/pg";
import { type } from "arktype";
import {parseDate} from "chrono-node";
@ -17,7 +18,7 @@ export async function update_all_guilds() {
}
}).assert(ans.data)
const db = container.get(T_PG)
const { db } = await c.getAsync(PG)
await db.begin(async (sql) => {
for(const [guild_name, guild] of Object.entries(parsed)){
await sql`insert into wynn_guild_info
@ -45,7 +46,7 @@ export async function update_guild({
}
const parsed = WapiV3GuildOverview.assert(ans.data)
const db = container.get(T_PG)
const { db } = await c.getAsync(PG)
await db.begin(async (sql) => {
await sql`insert into wynn_guild_info

View File

@ -1,5 +1,6 @@
import { container, T_PG } from "#/di"
import { WApi } from "#/lib/wapi"
import { c } from "#/di"
import { WApi } from "#/lib/wynn/wapi"
import { PG } from "#/services/pg"
import { log } from "@temporalio/activity"
import { type } from "arktype"
import axios from "axios"
@ -66,7 +67,7 @@ export const scrape_online_players = async()=>{
}).assert(raw.data)
const sql = container.get(T_PG)
const { sql } = await c.getAsync(PG)
for(const [playerName, server] of Object.entries(onlineList.players)){
// we do this optimistically without a tx, because temporal will probably handle

View File

@ -1,8 +1,9 @@
import {bot} from "#/bot"
import { ActivityTypes, ApplicationCommandOptionTypes, InteractionTypes } from "discordeno"
import { InteractionHandler, MuxHandler, SlashHandler } from "./types"
import { root } from "./slash_commands"
import { SlashCommandHandler } from "./slash_commands"
import { uuid4 } from "@temporalio/workflow"
import { c } from "#/di"
export const slashHandler: InteractionHandler = async (interaction) => {
@ -21,7 +22,7 @@ export const slashHandler: InteractionHandler = async (interaction) => {
}
}
let rootHandler: SlashHandler = root
const rootHandler = (await c.getAsync(SlashCommandHandler)).root()
let cur: SlashHandler | MuxHandler<any> = rootHandler
for(let i = 0; i < commandPath.length; i++) {

View File

@ -0,0 +1,45 @@
import { 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"
@injectable()
export class SlashCommandHandler {
constructor(
public readonly db = inject(PG)
) {
}
root(): SlashHandler {
return {
guild: {
info: async (interaction) => {
interaction.respond("TODO: guild info")
},
online: async (interaction) => {
const msg = await formGuildOnlineMessage(
WYNN_GUILD_ID,
this.db.sql,
)
await interaction.respond(msg, {
withResponse: true,
})
},
leaderboard: async (interaction) => {
const leaderboard = await formGuildLeaderboardMessage(
WYNN_GUILD_ID,
this.db.sql,
)
await interaction.respond(leaderboard, {
withResponse: true,
})
},
},
admin: {
set_wynn_guild: async (interaction) => {
},
}
}
}
}

View File

@ -1,7 +1,7 @@
import { Sql } from "postgres";
import { CreateMessageOptions, Embed, InteractionCallbackOptions } from "discordeno"
import {type} from "arktype"
import { TabWriter } from "#/lib/tabwriter"
import { TabWriter } from "#/lib/util/tabwriter"
import * as md from 'ts-markdown-builder';

View File

@ -18,11 +18,36 @@ const intents = [
Intents.Guilds ,
Intents.GuildInvites ,
Intents.GuildMessages,
]
] as const
export const bot = createBot({
export const createBotWithToken = (token: string) => createBot({
intents: intents.reduce((acc, curr) => acc | curr, Intents.Guilds),
token: config.DISCORD_TOKEN,
token: token,
desiredProperties: {
interaction: {
id: true,
data: true,
type: true,
token: true,
message: true,
channelId: true,
channel: true,
guildId: true,
guild: true,
user: true,
member: true,
},
message: {
id: true,
member: true,
guildId: true,
},
}
})
export const bot = createBot({
intents: intents.reduce((acc, curr) => acc | curr, Intents.Guilds),
token: config.DISCORD_TOKEN || "",
desiredProperties: {
interaction: {
id: true,

View File

@ -1,4 +1,4 @@
import { container } from "#/di";
import { c } from "#/di";

View File

@ -1,39 +0,0 @@
import { formGuildLeaderboardMessage, formGuildOnlineMessage } from "#/bot/common/guild"
import { WYNN_GUILD_ID } from "#/constants"
import { container, T_PG } from "#/di"
import { SlashHandler } from "./types"
export const root: SlashHandler = {
guild: {
info: async (interaction) => {
interaction.respond("TODO: guild info")
},
online: async (interaction) => {
const db = container.get(T_PG)
const msg = await formGuildOnlineMessage(
WYNN_GUILD_ID,
db,
)
await interaction.respond(msg, {
withResponse: true,
})
},
leaderboard: async (interaction) => {
const db = container.get(T_PG)
const leaderboard = await formGuildLeaderboardMessage(
WYNN_GUILD_ID,
db,
)
await interaction.respond(leaderboard, {
withResponse: true,
})
},
},
admin: {
set_wynn_guild: async (interaction) => {
const db = container.get(T_PG)
},
}
}

90
ts/src/cmd/bot.js Normal file
View File

@ -0,0 +1,90 @@
"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

@ -6,8 +6,7 @@ import * as BotCommands from "#/slashcommands";
import "#/services/pg"
import { DISCORD_GUILD_ID } from '#/constants';
import { bot } from '#/bot';
import { events } from '#/botevent';
import { events } from '#/bot/botevent/handler';
export class BotCommand extends Command {

View File

@ -1,6 +1,6 @@
import { Command } from 'clipanion';
import { container, T_PG } from '#/di';
import { c } from '#/di';
import { runMigrations } from '#/services/pg/migrations';
// di
@ -13,6 +13,7 @@ import * as activities from '../activities';
import path from 'path';
import { Client, ScheduleNotFoundError, ScheduleOptions, ScheduleOverlapPolicy } from '@temporalio/client';
import { workflowSyncAllGuilds, workflowSyncGuilds, workflowSyncOnline } from '#/workflows';
import { PG } from '#/services/pg';
@ -88,10 +89,11 @@ const addSchedules = async (c: Client) => {
export class WorkerCommand extends Command {
static paths = [['worker']];
async execute() {
const pg = container.get(T_PG);
await runMigrations(pg);
const { db } = await c.getAsync(PG);
const client = await container.getAsync(Client);
await runMigrations(db);
const client = await c.getAsync(Client);
// schedules
await addSchedules(client);
@ -119,10 +121,8 @@ export class WorkerCommand extends Command {
await worker.run();
console.log("worked.run exited");
await pg.end();
await db.end();
await connection.close();
}
}

View File

@ -1,20 +0,0 @@
import { Command } from 'clipanion';
import { container, T_PG } from '#/di';
import { runMigrations } from '#/services/pg/migrations';
// di
import "#/services/pg"
export const WynnCommands = [
class extends Command {
static paths = [['wynn', 'refetch']];
async execute() {
const pg = container.get(T_PG);
await runMigrations(pg);
await pg.end()
}
}
]

View File

@ -4,7 +4,7 @@ import {config as dotenvConfig} from 'dotenv';
dotenvConfig();
const schemaConfig = {
DISCORD_TOKEN: z.string(),
DISCORD_TOKEN: z.string().optional(),
TEMPORAL_HOSTPORT: z.string().default('localhost:7233'),
TEMPORAL_NAMESPACE: z.string().default('default'),

View File

@ -1,5 +1,5 @@
import { Container, InjectionToken } from "@needle-di/core";
import { Sql } from "postgres";
export const container = new Container();
export const c = new Container();
export const T_PG = new InjectionToken<Sql>("T_PG")

9
ts/src/main.js Normal file
View File

@ -0,0 +1,9 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var clipanion_1 = require("clipanion");
var worker_1 = require("#/cmd/worker");
var bot_1 = require("#/cmd/bot");
(0, clipanion_1.runExit)([
worker_1.WorkerCommand,
bot_1.BotCommand,
]);

View File

@ -1,12 +1,10 @@
import { runExit } from "clipanion";
import { WorkerCommand } from "#/cmd/worker";
import { WynnCommands } from "#/cmd/wynn";
import { BotCommand } from "./cmd/bot";
import { BotCommand } from "#/cmd/bot";
runExit([
WorkerCommand,
BotCommand,
...WynnCommands,
])

View File

@ -1,17 +1,23 @@
import { config } from "#/config";
import { container, T_PG } from "#/di";
import postgres from "postgres";
import { injectable } from "@needle-di/core";
import postgres, { Sql } from "postgres";
container.bind({
provide: T_PG,
useFactory: () => {
@injectable()
export class PG {
readonly db: Sql;
get sql() {
return this.db
}
constructor() {
const opts = {
onnotice: () => {},
}
let db: Sql;
if(config.PG_URL) {
return postgres(config.PG_URL, opts);
}
return postgres({
db = postgres(config.PG_URL, opts);
}else {
db = postgres({
host: config.PG_HOST,
port: config.PG_PORT,
user: config.PG_USER,
@ -20,5 +26,8 @@ container.bind({
ssl: (config.PG_SSLMODE as any) || 'prefer',
...opts,
})
},
});
}
this.db = db
}
}

View File

@ -1,8 +1,8 @@
import { config } from "#/config";
import { container } from "#/di";
import { c } from "#/di";
import { Client, Connection} from '@temporalio/client';
container.bind({
c.bind({
provide: Client,
async: true,
useFactory: async () => {

File diff suppressed because it is too large Load Diff