all repos — stealth-developers @ 72242da6ffc76515ee4703abdff2555b095861de

api: api for widgets
vi did:web:vt3e.cat
Wed, 08 Jul 2026 00:16:19 +0100
commit

72242da6ffc76515ee4703abdff2555b095861de

parent

124009fe8d16aefd3401cb00740df5e6d26d6a44

A apps/api/scripts/bootstrap.ts

@@ -0,0 +1,50 @@

+import config from "@stealth-developers/config"; + +import { GroundWarWidget } from "../src/lib/widgets/meow"; + +const APP_ID = config.discord.app_id; +const BOT_TOKEN = config.discord.token; + +async function bootstrapWidget() { + const widgetConfigPayload = GroundWarWidget; + + const createRes = await fetch( + `https://discord.com/api/v10/applications/${APP_ID}/widget-configs`, + { + method: "POST", + headers: { + Authorization: `Bot ${BOT_TOKEN}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(widgetConfigPayload), + }, + ); + + if (!createRes.ok) { + const errorText = await createRes.text(); + throw new Error(`failedd to push config: ${createRes.status} - ${errorText}`); + } + + const createdConfig = (await createRes.json()) as { config_id: string; [key: string]: any }; + const configId = createdConfig.config_id; + console.log(`created config (id: ${configId}).`); + + console.log("publishing widget config..."); + const publishRes = await fetch( + `https://discord.com/api/v10/applications/${APP_ID}/widget-configs/${configId}/publish`, + { + method: "POST", + headers: { Authorization: `Bot ${BOT_TOKEN}` }, + }, + ); + + if (!publishRes.ok) { + const errorText = await publishRes.text(); + throw new Error(`failed to publish: ${publishRes.status} - ${errorText}`); + } + + const publishedConfig = await publishRes.json(); + console.log(publishedConfig); +} + +bootstrapWidget().catch(console.error);
A apps/api/scripts/upload-asset.ts

@@ -0,0 +1,105 @@

+import config from "@stealth-developers/config"; +import fs from "fs"; +import path from "path"; + +const args = process.argv.slice(2); +const filepath = args[0]; +const assetKey = args[1]; + +const APP_ID = config.discord.app_id; +const BOT_TOKEN = config.discord.token; +const FILE_PATH = filepath; +const ASSET_KEY = assetKey; + +const MIME_TYPES = { + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", +} as const; + +async function uploadApplicationAsset() { + if (!FILE_PATH || !ASSET_KEY) { + console.error("usage: command <path> <assetKey>"); + process.exit(1); + } + + const stats = fs.statSync(FILE_PATH); + const fileSize = stats.size; + const fileName = path.basename(FILE_PATH); + const fileExt = path.extname(FILE_PATH); + const fileMimeType = MIME_TYPES[fileExt as keyof typeof MIME_TYPES]; + + if (!fileMimeType) throw new Error("Unsupported file type"); + + const initResponse = await fetch( + `https://discord.com/api/v10/applications/${APP_ID}/assets/upload`, + { + method: "POST", + headers: { + Authorization: `Bot ${BOT_TOKEN}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + filename: fileName, + file_size: fileSize, + }), + }, + ); + + if (!initResponse.ok) { + const errorText = await initResponse.text(); + throw new Error(`Step 1 Failed: ${initResponse.status} - ${errorText}`); + } + + const uploadMetadata = (await initResponse.json()) as { + upload_url: string; + upload_filename: string; + }; + const { upload_url, upload_filename } = uploadMetadata; + + const fileStream = fs.createReadStream(FILE_PATH); + + const uploadResponse = await fetch(upload_url, { + method: "PUT", + headers: { + "Content-Length": fileSize.toString(), + "Content-Type": fileMimeType, + }, + body: fileStream, + }); + + if (!uploadResponse.ok) { + const errorText = await uploadResponse.text(); + throw new Error(`Step 2 Failed: ${uploadResponse.status} - ${errorText}`); + } + + console.log("uploaded file to GCS."); + + const registerResponse = await fetch( + `https://discord.com/api/v10/applications/${APP_ID}/assets`, + { + method: "POST", + headers: { + Authorization: `Bot ${BOT_TOKEN}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + key: ASSET_KEY, + upload_filename: upload_filename, + visibility: "public", + }), + }, + ); + + if (!registerResponse.ok) { + const errorText = await registerResponse.text(); + throw new Error( + `failed to register application asset: ${registerResponse.status} - ${errorText}`, + ); + } + + const finalisedAsset = await registerResponse.json(); + console.log("registered application asset:", finalisedAsset); +} + +uploadApplicationAsset().catch(console.error);
A apps/api/src/lib/widgets/builders.ts

@@ -0,0 +1,576 @@

+import type { + ActivityAccessoryStatLayout, + ActivityAccessorySurface, + AddWidgetPreviewContainedLayout, + AddWidgetPreviewHeroLayout, + AddWidgetPreviewSurface, + BottomStatFields, + ItemFields, + MiniProfileContainedStatLayout, + MiniProfileHeroStatLayout, + MiniProfileSurface, + ObjectiveFields, + ProgressFields, + StatFields, + SubtitleFields, + WidgetBottomCollectionLayout, + WidgetBottomProgressLayout, + WidgetBottomStatsLayout, + WidgetBottomSurface, + WidgetField, + WidgetPayload, + WidgetPresentationType, + WidgetTopContainedLayout, + WidgetTopHeroLayout, + WidgetTopSurface, + WidgetValueType, +} from "./types"; + +export const createField = <P extends WidgetPresentationType>( + valueType: WidgetValueType, + presentationType: P, + value: string, + fallback?: WidgetField<P>, +): WidgetField<P> => ({ + value_type: valueType, + presentation_type: presentationType, + value, + fallback, +}); + +export const customString = <P extends WidgetPresentationType>( + presentation: P, + value: string, + fallback?: WidgetField<P>, +): WidgetField<P> => createField("custom_string", presentation, value, fallback); + +export const appAsset = (key: string, fallback?: WidgetField<"image">): WidgetField<"image"> => + createField("application_asset", "image", key, fallback); + +export const appLocalizedString = <P extends WidgetPresentationType>( + presentation: P, + value: string, + fallback?: WidgetField<P>, +): WidgetField<P> => createField("application_localized_string", presentation, value, fallback); + +export const dataField = <P extends WidgetPresentationType>( + presentation: P, + value: string, + fallback?: WidgetField<P>, +): WidgetField<P> => createField("data", presentation, value, fallback); + +// + +export const topHero = ( + heroImage: WidgetField<"image">, + titleText: WidgetField<"number" | "text">, + subtitles?: { + subtitle_1?: SubtitleFields; + subtitle_2?: SubtitleFields; + subtitle_3?: SubtitleFields; + }, +): WidgetTopHeroLayout => ({ + layout: "widget_top_hero", + components: { + hero_image: { fields: { image: heroImage } }, + title: { fields: { text: titleText } }, + ...(subtitles?.subtitle_1 ? { subtitle_1: { fields: subtitles.subtitle_1 } } : {}), + ...(subtitles?.subtitle_2 ? { subtitle_2: { fields: subtitles.subtitle_2 } } : {}), + ...(subtitles?.subtitle_3 ? { subtitle_3: { fields: subtitles.subtitle_3 } } : {}), + }, +}); + +export const topContained = ( + containedImage: WidgetField<"image">, + titleText: WidgetField<"number" | "text">, + subtitles?: { + subtitle_1?: SubtitleFields; + subtitle_2?: SubtitleFields; + subtitle_3?: SubtitleFields; + }, +): WidgetTopContainedLayout => ({ + layout: "widget_top_contained", + components: { + contained_image: { fields: { image: containedImage } }, + title: { fields: { text: titleText } }, + ...(subtitles?.subtitle_1 ? { subtitle_1: { fields: subtitles.subtitle_1 } } : {}), + ...(subtitles?.subtitle_2 ? { subtitle_2: { fields: subtitles.subtitle_2 } } : {}), + ...(subtitles?.subtitle_3 ? { subtitle_3: { fields: subtitles.subtitle_3 } } : {}), + }, +}); + +export const bottomStats = ( + stats: [ + BottomStatFields, + BottomStatFields, + BottomStatFields, + BottomStatFields, + BottomStatFields, + BottomStatFields, + ], +): WidgetBottomStatsLayout => ({ + layout: "widget_bottom_stats", + components: { + stat_1: { fields: stats[0] }, + stat_2: { fields: stats[1] }, + stat_3: { fields: stats[2] }, + stat_4: { fields: stats[3] }, + stat_5: { fields: stats[4] }, + stat_6: { fields: stats[5] }, + }, +}); + +export const bottomProgress = ( + objective: ObjectiveFields, + progress: ProgressFields, +): WidgetBottomProgressLayout => ({ + layout: "widget_bottom_progress", + components: { + objective: { fields: objective }, + progress: { fields: progress }, + }, +}); + +export const bottomCollection = ( + items: [ItemFields, ItemFields, ItemFields, ItemFields], +): WidgetBottomCollectionLayout => ({ + layout: "widget_bottom_collection", + components: { + item_1: { fields: items[0] }, + item_2: { fields: items[1] }, + item_3: { fields: items[2] }, + item_4: { fields: items[3] }, + }, +}); + +export const miniProfileHeroStat = ( + stat: StatFields, + heroImage: WidgetField<"image">, +): MiniProfileHeroStatLayout => ({ + layout: "mini_profile_hero_stat", + components: { + stat: { fields: stat }, + hero_image: { fields: { image: heroImage } }, + }, +}); + +export const miniProfileContainedStat = ( + stat: StatFields, + containedImage: WidgetField<"image">, +): MiniProfileContainedStatLayout => ({ + layout: "mini_profile_contained_stat", + components: { + stat: { fields: stat }, + contained_image: { fields: { image: containedImage } }, + }, +}); + +export const activityAccessoryStat = (stat: StatFields): ActivityAccessoryStatLayout => ({ + layout: "activity_accessory_stat", + components: { + stat: { fields: stat }, + }, +}); + +export const addWidgetPreviewHero = ( + heroImage: WidgetField<"image">, +): AddWidgetPreviewHeroLayout => ({ + layout: "add_widget_preview_hero", + components: { + hero_image: { fields: { image: heroImage } }, + }, +}); + +export const addWidgetPreviewContained = ( + containedImage: WidgetField<"image">, +): AddWidgetPreviewContainedLayout => ({ + layout: "add_widget_preview_contained", + components: { + contained_image: { fields: { image: containedImage } }, + }, +}); + +export class WidgetTopHeroBuilder { + private heroImage?: WidgetField<"image">; + private title?: WidgetField<"number" | "text">; + private subtitles: SubtitleFields[] = []; + + setHeroImage(image: WidgetField<"image">): this { + this.heroImage = image; + return this; + } + + setTitle(text: WidgetField<"number" | "text">): this { + this.title = text; + return this; + } + + addSubtitle( + text: WidgetField<"number" | "text">, + label?: WidgetField<"number" | "text">, + icon?: WidgetField<"image">, + ): this { + if (this.subtitles.length >= 3) { + throw new Error("widget_top_hero allows a maximum of 3 subtitles."); + } + this.subtitles.push({ text, label, icon }); + return this; + } + + build(): WidgetTopHeroLayout { + if (!this.heroImage) { + throw new Error("Hero Image is required for widget_top_hero."); + } + if (!this.title) { + throw new Error("Title is required for widget_top_hero."); + } + + return { + layout: "widget_top_hero", + components: { + hero_image: { fields: { image: this.heroImage } }, + title: { fields: { text: this.title } }, + ...(this.subtitles[0] ? { subtitle_1: { fields: this.subtitles[0] } } : {}), + ...(this.subtitles[1] ? { subtitle_2: { fields: this.subtitles[1] } } : {}), + ...(this.subtitles[2] ? { subtitle_3: { fields: this.subtitles[2] } } : {}), + }, + }; + } +} + +export class WidgetTopContainedBuilder { + private containedImage?: WidgetField<"image">; + private title?: WidgetField<"number" | "text">; + private subtitles: SubtitleFields[] = []; + + setContainedImage(image: WidgetField<"image">): this { + this.containedImage = image; + return this; + } + + setTitle(text: WidgetField<"number" | "text">): this { + this.title = text; + return this; + } + + addSubtitle( + text: WidgetField<"number" | "text">, + label?: WidgetField<"number" | "text">, + icon?: WidgetField<"image">, + ): this { + if (this.subtitles.length >= 3) { + throw new Error("widget_top_contained allows a maximum of 3 subtitles."); + } + this.subtitles.push({ text, label, icon }); + return this; + } + + build(): WidgetTopContainedLayout { + if (!this.containedImage) { + throw new Error("Contained Image is required for widget_top_contained."); + } + if (!this.title) { + throw new Error("Title is required for widget_top_contained."); + } + + return { + layout: "widget_top_contained", + components: { + contained_image: { fields: { image: this.containedImage } }, + title: { fields: { text: this.title } }, + ...(this.subtitles[0] ? { subtitle_1: { fields: this.subtitles[0] } } : {}), + ...(this.subtitles[1] ? { subtitle_2: { fields: this.subtitles[1] } } : {}), + ...(this.subtitles[2] ? { subtitle_3: { fields: this.subtitles[2] } } : {}), + }, + }; + } +} + +export class WidgetBottomStatsBuilder { + private stats: BottomStatFields[] = []; + + addStat( + value: WidgetField<"duration" | "number" | "text">, + label: WidgetField<"text">, + icon?: WidgetField<"image">, + ): this { + if (this.stats.length >= 6) { + throw new Error("widget_bottom_stats allows a maximum of 6 stats."); + } + this.stats.push({ value, label, icon }); + return this; + } + + build(): WidgetBottomStatsLayout { + if (this.stats.length !== 6) { + throw new Error( + `widget_bottom_stats requires exactly 6 stats. Currently has ${this.stats.length}.`, + ); + } + + return { + layout: "widget_bottom_stats", + components: { + stat_1: { fields: this.stats[0]! }, + stat_2: { fields: this.stats[1]! }, + stat_3: { fields: this.stats[2]! }, + stat_4: { fields: this.stats[3]! }, + stat_5: { fields: this.stats[4]! }, + stat_6: { fields: this.stats[5]! }, + }, + }; + } +} + +export class WidgetBottomProgressBuilder { + private objective?: ObjectiveFields; + private progress?: ProgressFields; + + setObjective( + image: WidgetField<"image">, + name: WidgetField<"text">, + description?: WidgetField<"text">, + ): this { + this.objective = { image, name, description }; + return this; + } + + setProgress(current: WidgetField<"number">, max?: WidgetField<"number">): this { + this.progress = { current, max }; + return this; + } + + build(): WidgetBottomProgressLayout { + if (!this.objective) { + throw new Error("Objective component is required for widget_bottom_progress."); + } + if (!this.progress) { + throw new Error("Progress component is required for widget_bottom_progress."); + } + + return { + layout: "widget_bottom_progress", + components: { + objective: { fields: this.objective }, + progress: { fields: this.progress }, + }, + }; + } +} + +export class WidgetBottomCollectionBuilder { + private items: ItemFields[] = []; + + addItem( + image: WidgetField<"image">, + name: WidgetField<"text">, + description?: WidgetField<"text">, + ): this { + if (this.items.length >= 4) { + throw new Error("widget_bottom_collection allows a maximum of 4 items."); + } + this.items.push({ image, name, description }); + return this; + } + + build(): WidgetBottomCollectionLayout { + if (this.items.length !== 4) { + throw new Error( + `widget_bottom_collection requires exactly 4 items. Currently has ${this.items.length}.`, + ); + } + + return { + layout: "widget_bottom_collection", + components: { + item_1: { fields: this.items[0]! }, + item_2: { fields: this.items[1]! }, + item_3: { fields: this.items[2]! }, + item_4: { fields: this.items[3]! }, + }, + }; + } +} + +export class MiniProfileHeroStatBuilder { + private stat?: StatFields; + private heroImage?: WidgetField<"image">; + + setStat( + text: WidgetField<"number" | "text">, + label?: WidgetField<"number" | "text">, + icon?: WidgetField<"image">, + ): this { + this.stat = { text, label, icon }; + return this; + } + + setHeroImage(image: WidgetField<"image">): this { + this.heroImage = image; + return this; + } + + build(): MiniProfileHeroStatLayout { + if (!this.stat) { + throw new Error("Stat is required for mini_profile_hero_stat."); + } + if (!this.heroImage) { + throw new Error("Hero Image is required for mini_profile_hero_stat."); + } + + return { + layout: "mini_profile_hero_stat", + components: { + stat: { fields: this.stat }, + hero_image: { fields: { image: this.heroImage } }, + }, + }; + } +} + +export class AddWidgetPreviewHeroBuilder { + private heroImage?: WidgetField<"image">; + + setHeroImage(image: WidgetField<"image">): this { + this.heroImage = image; + return this; + } + + build(): AddWidgetPreviewHeroLayout { + if (!this.heroImage) { + throw new Error("Hero Image is required for add_widget_preview_hero."); + } + + return { + layout: "add_widget_preview_hero", + components: { + hero_image: { fields: { image: this.heroImage } }, + }, + }; + } +} + +export class AddWidgetPreviewContainedBuilder { + private containedImage?: WidgetField<"image">; + + setContainedImage(image: WidgetField<"image">): this { + this.containedImage = image; + return this; + } + + build(): AddWidgetPreviewContainedLayout { + if (!this.containedImage) { + throw new Error("Contained Image is required for add_widget_preview_contained."); + } + + return { + layout: "add_widget_preview_contained", + components: { + contained_image: { fields: { image: this.containedImage } }, + }, + }; + } +} + +export class MiniProfileContainedStatBuilder { + private stat?: StatFields; + private containedImage?: WidgetField<"image">; + + setStat( + text: WidgetField<"number" | "text">, + label?: WidgetField<"number" | "text">, + icon?: WidgetField<"image">, + ): this { + this.stat = { text, label, icon }; + return this; + } + + setContainedImage(image: WidgetField<"image">): this { + this.containedImage = image; + return this; + } + + build(): MiniProfileContainedStatLayout { + if (!this.stat) { + throw new Error("Stat is required for mini_profile_contained_stat."); + } + if (!this.containedImage) { + throw new Error("Contained Image is required for mini_profile_contained_stat."); + } + + return { + layout: "mini_profile_contained_stat", + components: { + stat: { fields: this.stat }, + contained_image: { fields: { image: this.containedImage } }, + }, + }; + } +} + +export class ActivityAccessoryStatBuilder { + private stat?: StatFields; + + setStat( + text: WidgetField<"number" | "text">, + label?: WidgetField<"number" | "text">, + icon?: WidgetField<"image">, + ): this { + this.stat = { text, label, icon }; + return this; + } + + build(): ActivityAccessoryStatLayout { + if (!this.stat) { + throw new Error("Stat is required for activity_accessory_stat."); + } + + return { + layout: "activity_accessory_stat", + components: { + stat: { fields: this.stat }, + }, + }; + } +} + +export class WidgetPayloadBuilder { + private payload: WidgetPayload; + + constructor(displayName: string) { + this.payload = { + display_name: displayName, + // @ts-expect-error: defined later + surfaces: {}, + }; + } + + widgetTop(layout: WidgetTopSurface): this { + this.payload.surfaces.widget_top = layout; + return this; + } + + widgetBottom(layout: WidgetBottomSurface): this { + this.payload.surfaces.widget_bottom = layout; + return this; + } + + miniProfile(layout: MiniProfileSurface): this { + this.payload.surfaces.mini_profile = layout; + return this; + } + + activityAccessory(layout: ActivityAccessorySurface): this { + this.payload.surfaces.activity_accessory = layout; + return this; + } + + addWidgetPreview(layout: AddWidgetPreviewSurface): this { + this.payload.surfaces.add_widget_preview = layout; + return this; + } + + build(): WidgetPayload { + return this.payload; + } +}
A apps/api/src/lib/widgets/constants.ts

@@ -0,0 +1,9 @@

+import { appAsset } from "./builders"; + +export const ASSETS = { + hero: { + key: "khls", + asset_id: "1523451878141202442", + image: appAsset("khls"), + }, +};
A apps/api/src/lib/widgets/meow.ts

@@ -0,0 +1,49 @@

+import { + AddWidgetPreviewHeroBuilder, + MiniProfileContainedStatBuilder, + WidgetBottomCollectionBuilder, + WidgetPayloadBuilder, + WidgetTopHeroBuilder, + customString, +} from "./builders"; +import { ASSETS } from "./constants"; + +export function generateWidget(values: { + kills: string; + deaths: string; + level: string; + matchesWon: string; +}) { + const { kills, deaths, level, matchesWon } = values; + if (!kills || !deaths || !level || !matchesWon) throw new Error("Missing values"); + + return new WidgetPayloadBuilder("Ground War") + .widgetTop( + new WidgetTopHeroBuilder() + .setHeroImage(ASSETS.hero.image) + .setTitle(customString("text", "Ground War")) + .addSubtitle(customString("text", "sub 1")) + .addSubtitle(customString("text", "sub 2")) + .build(), + ) + .widgetBottom( + new WidgetBottomCollectionBuilder() + .addItem(ASSETS.hero.image, customString("text", "Kills"), customString("text", kills)) + .addItem(ASSETS.hero.image, customString("text", "Deaths"), customString("text", deaths)) + .addItem(ASSETS.hero.image, customString("text", "Level"), customString("text", level)) + .addItem( + ASSETS.hero.image, + customString("text", "Matches Won"), + customString("text", matchesWon), + ) + .build(), + ) + .addWidgetPreview(new AddWidgetPreviewHeroBuilder().setHeroImage(ASSETS.hero.image).build()) + .miniProfile( + new MiniProfileContainedStatBuilder() + .setStat(customString("text", "Ground War")) + .setContainedImage(ASSETS.hero.image) + .build(), + ) + .build(); +}
A apps/api/src/lib/widgets/types.ts

@@ -0,0 +1,170 @@

+export type WidgetValueType = + | "data" + | "custom_string" + | "application_asset" + | "application_localized_string"; + +export type WidgetPresentationType = "image" | "number" | "text" | "duration"; + +export interface WidgetField<P extends WidgetPresentationType = WidgetPresentationType> { + value_type: WidgetValueType; + presentation_type: P; + value: string; + fallback?: WidgetField<P>; +} + +export interface WidgetComponent<Fields> { + fields: Fields; +} + +export interface SubtitleFields { + label?: WidgetField<"number" | "text">; + text: WidgetField<"number" | "text">; + icon?: WidgetField<"image">; +} + +export interface BottomStatFields { + value: WidgetField<"duration" | "number" | "text">; + label: WidgetField<"text">; + icon?: WidgetField<"image">; +} + +export interface ObjectiveFields { + image: WidgetField<"image">; + name: WidgetField<"text">; + description?: WidgetField<"text">; +} + +export interface ProgressFields { + current: WidgetField<"number">; + max?: WidgetField<"number">; +} + +export interface ItemFields { + image: WidgetField<"image">; + name: WidgetField<"text">; + description?: WidgetField<"text">; +} + +export interface StatFields { + label?: WidgetField<"number" | "text">; + text: WidgetField<"number" | "text">; + icon?: WidgetField<"image">; +} + +export interface WidgetTopHeroLayout { + layout: "widget_top_hero"; + components: { + hero_image: WidgetComponent<{ image: WidgetField<"image"> }>; + title: WidgetComponent<{ text: WidgetField<"number" | "text"> }>; + subtitle_1?: WidgetComponent<SubtitleFields>; + subtitle_2?: WidgetComponent<SubtitleFields>; + subtitle_3?: WidgetComponent<SubtitleFields>; + }; +} + +export interface WidgetTopContainedLayout { + layout: "widget_top_contained"; + components: { + contained_image: WidgetComponent<{ image: WidgetField<"image"> }>; + title: WidgetComponent<{ text: WidgetField<"number" | "text"> }>; + subtitle_1?: WidgetComponent<SubtitleFields>; + subtitle_2?: WidgetComponent<SubtitleFields>; + subtitle_3?: WidgetComponent<SubtitleFields>; + }; +} + +export type WidgetTopSurface = WidgetTopHeroLayout | WidgetTopContainedLayout; + +export interface WidgetBottomStatsLayout { + layout: "widget_bottom_stats"; + components: { + stat_1: WidgetComponent<BottomStatFields>; + stat_2: WidgetComponent<BottomStatFields>; + stat_3: WidgetComponent<BottomStatFields>; + stat_4: WidgetComponent<BottomStatFields>; + stat_5: WidgetComponent<BottomStatFields>; + stat_6: WidgetComponent<BottomStatFields>; + }; +} + +export interface WidgetBottomProgressLayout { + layout: "widget_bottom_progress"; + components: { + objective: WidgetComponent<ObjectiveFields>; + progress: WidgetComponent<ProgressFields>; + }; +} + +export interface WidgetBottomCollectionLayout { + layout: "widget_bottom_collection"; + components: { + item_1: WidgetComponent<ItemFields>; + item_2: WidgetComponent<ItemFields>; + item_3: WidgetComponent<ItemFields>; + item_4: WidgetComponent<ItemFields>; + }; +} + +export type WidgetBottomSurface = + | WidgetBottomStatsLayout + | WidgetBottomProgressLayout + | WidgetBottomCollectionLayout; + +export interface MiniProfileHeroStatLayout { + layout: "mini_profile_hero_stat"; + components: { + stat: WidgetComponent<StatFields>; + hero_image: WidgetComponent<{ image: WidgetField<"image"> }>; + }; +} + +export interface MiniProfileContainedStatLayout { + layout: "mini_profile_contained_stat"; + components: { + stat: WidgetComponent<StatFields>; + contained_image: WidgetComponent<{ image: WidgetField<"image"> }>; + }; +} + +export type MiniProfileSurface = MiniProfileHeroStatLayout | MiniProfileContainedStatLayout; + +export interface ActivityAccessoryStatLayout { + layout: "activity_accessory_stat"; + components: { + stat: WidgetComponent<StatFields>; + }; +} + +export type ActivityAccessorySurface = ActivityAccessoryStatLayout; + +export interface AddWidgetPreviewHeroLayout { + layout: "add_widget_preview_hero"; + components: { + hero_image: WidgetComponent<{ image: WidgetField<"image"> }>; + }; +} + +export interface AddWidgetPreviewContainedLayout { + layout: "add_widget_preview_contained"; + components: { + contained_image: WidgetComponent<{ image: WidgetField<"image"> }>; + }; +} + +export type AddWidgetPreviewSurface = AddWidgetPreviewHeroLayout | AddWidgetPreviewContainedLayout; + +export type EmptySurface = Record<string, never>; + +export interface WidgetSurfaces { + widget_top: WidgetTopSurface; + widget_bottom: WidgetBottomSurface; + mini_profile: MiniProfileSurface; + activity_accessory?: ActivityAccessorySurface; + add_widget_preview?: AddWidgetPreviewSurface; +} + +export interface WidgetPayload { + display_name: string; + surfaces: WidgetSurfaces; +}
M apps/api/src/routes/sync.tsapps/api/src/routes/sync.ts

@@ -1,33 +1,181 @@

-import { editActor, getActorByPin } from "@stealth-developers/db"; -import Elysia, { t } from "elysia"; +import config from "@stealth-developers/config"; +import { editActor, getActorByPin, getActorByRobloxId, getToken } from "@stealth-developers/db"; +import Elysia, { type Static, t } from "elysia"; + +import { generateWidget } from "@/lib/widgets/meow"; -export const syncRoutes = new Elysia({ prefix: "/sync" }).post( - "/verify", - async ({ body, set }) => { - const user = await getActorByPin(body.pin); - if (!user) { - set.status = 404; - return { error: "Pin not found" }; - } +const syncPushResponseItem = t.Union([ + t.Object( + { + user_id: t.String(), + success: t.Literal(true), + }, + { title: "SyncPushResponseItemSuccess" }, + ), + t.Object( + { + user_id: t.String(), + success: t.Literal(false), + error: t.String(), + }, + { title: "SyncPushResponseItemError" }, + ), +]); +const syncPushResponse = t.Array(syncPushResponseItem); + +export type SyncPushResponseItem = Static<typeof syncPushResponseItem>; +export type SyncPushResponse = Static<typeof syncPushResponse>; + +export const syncRoutes = new Elysia({ prefix: "/sync" }) + .resolve(async ({ request }) => { + const auth = request.headers.get("Authorization"); + const tokenString = auth?.split(" ")[1]; + if (!tokenString) + throw new Error("Found no token, note that it must be prefixed with `Bearer`"); + + const token = await getToken(tokenString); + if (!token) throw new Error("Invalid token"); + + return { token }; + }) + .post( + "/verify", + async ({ body, set }) => { + const user = await getActorByPin(body.pin); + if (!user) { + set.status = 404; + return { error: "Pin not found" }; + } - const expiryDate = new Date(user.pinExpiresIn!); - if (expiryDate < new Date()) { - set.status = 401; - return { error: "Pin expired" }; - } + const expiryDate = new Date(user.pinExpiresIn!); + if (expiryDate < new Date()) { + set.status = 401; + return { error: "Pin expired" }; + } - await editActor(user.id, { - robloxId: body.roblox_user_id, - pin: null, - pinExpiresIn: null, - }); + await editActor(user.id, { + robloxId: body.roblox_user_id, + pin: null, + pinExpiresIn: null, + }); - return { success: true }; - }, - { - body: t.Object({ - pin: t.String(), - roblox_user_id: t.String(), - }), - }, -); + return { success: true }; + }, + { + tags: ["widgets"], + body: t.Object({ + pin: t.String(), + roblox_user_id: t.String(), + }), + response: { + 200: t.Object({ + success: t.Boolean(), + }), + 401: t.Object({ + error: t.String(), + }), + 404: t.Object({ + error: t.String(), + }), + }, + }, + ) + .post( + "/push", + async ({ body, set }) => { + const results: SyncPushResponse = []; + for (const user of body) { + try { + const actor = await getActorByRobloxId(user.user_id); + if (!actor) { + results.push({ + user_id: user.user_id, + success: false, + error: "User not found", + }); + continue; + } + + const discordId = actor.discordId; + if (!discordId) { + results.push({ + user_id: user.user_id, + success: false, + error: "Discord ID not found", + }); + continue; + } + + const widget = generateWidget({ + kills: String(user.kills), + deaths: String(user.deaths), + matchesWon: String(user.wins), + level: String(user.level), + }); + + const response = await fetch( + `https://discord.com/api/v10/applications/${config.discord.app_id}/users/${discordId}/identities/${user.user_id}/profile`, + { + method: "PATCH", + headers: { + Authorization: `Bot ${config.discord.token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(widget), + }, + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error( + `Discord API Error for ${user.user_id}: ${response.status} - ${errorText}`, + ); + results.push({ + user_id: user.user_id, + success: false, + error: `Discord API Error: ${response.status}`, + }); + continue; + } + + results.push({ + user_id: user.user_id, + success: true, + }); + } catch (error) { + console.error(`Unexpected error processing user ${user.user_id}:`, error); + results.push({ + user_id: user.user_id, + success: false, + error: error instanceof Error ? error.message : "Internal Server Error", + }); + } + } + + const hasFailures = results.some((r) => !r.success); + const hasSuccesses = results.some((r) => r.success); + + if (!hasFailures) set.status = 200; + else if (hasSuccesses) set.status = 207; + else set.status = 400; + + return results; + }, + { + tags: ["widgets"], + body: t.Array( + t.Object({ + user_id: t.String(), + kills: t.Number(), + deaths: t.Number(), + wins: t.Number(), + level: t.Number(), + }), + ), + response: { + 200: syncPushResponse, + 207: syncPushResponse, + 400: syncPushResponse, + }, + }, + );
M bun.lockbun.lock

@@ -57,6 +57,7 @@ "pino": "^10.3.1",

"pino-pretty": "^13.1.3", "sharp-phash": "^2.2.0", "tesseract.js": "^7.0.0", + "uwuifier": "^4.2.2", }, "devDependencies": { "@sapphire/cli": "^1.9.3",

@@ -1198,6 +1199,8 @@

"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "uwuifier": ["uwuifier@4.2.2", "", {}, "sha512-Fhtj3Yg3rrDTEs+L1fqkgM3VYGJrP4U9GY+4fPdw08T5kzOB8NSosZnZfa5Zhv1Hh7NCpH5G6t1DIZyM1wPK1w=="], "vite": ["vite@8.1.0", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "~1.1.2", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q=="],
A pkgs/db/drizzle/20260707231539_normal_power_pack/migration.sql

@@ -0,0 +1,6 @@

+CREATE TABLE `api_tokens` ( + `id` text PRIMARY KEY, + `token` text NOT NULL, + `name` text NOT NULL, + `created_at` integer +);
A pkgs/db/drizzle/20260707231539_normal_power_pack/snapshot.json

@@ -0,0 +1,1893 @@

+{ + "version": "7", + "dialect": "sqlite", + "id": "a0e224cf-20bb-46bc-b839-2442ac13a5dd", + "prevIds": [ + "36bbda0e-f332-4677-8166-05a4b56c3a06" + ], + "ddl": [ + { + "name": "api_tokens", + "entityType": "tables" + }, + { + "name": "sessions", + "entityType": "tables" + }, + { + "name": "users", + "entityType": "tables" + }, + { + "name": "actors", + "entityType": "tables" + }, + { + "name": "guilds", + "entityType": "tables" + }, + { + "name": "ticket_attachments", + "entityType": "tables" + }, + { + "name": "ticket_comments", + "entityType": "tables" + }, + { + "name": "ticket_messages", + "entityType": "tables" + }, + { + "name": "ticket_participants", + "entityType": "tables" + }, + { + "name": "tickets", + "entityType": "tables" + }, + { + "name": "bugs", + "entityType": "tables" + }, + { + "name": "image_hashes", + "entityType": "tables" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "api_tokens" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token", + "entityType": "columns", + "table": "api_tokens" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "api_tokens" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "api_tokens" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "sessions" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "sessions" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "sessions" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "sessions" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "scope", + "entityType": "columns", + "table": "sessions" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_type", + "entityType": "columns", + "table": "sessions" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "expires_at", + "entityType": "columns", + "table": "sessions" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "sessions" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "users" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "discord_id", + "entityType": "columns", + "table": "users" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "display_name", + "entityType": "columns", + "table": "users" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "username", + "entityType": "columns", + "table": "users" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "avatar", + "entityType": "columns", + "table": "users" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "accepted_privacy_policy", + "entityType": "columns", + "table": "users" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "accepted_terms_of_service", + "entityType": "columns", + "table": "users" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "users" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "users" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "actors" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "guild_id", + "entityType": "columns", + "table": "actors" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "user_id", + "entityType": "columns", + "table": "actors" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "discord_id", + "entityType": "columns", + "table": "actors" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "display_name", + "entityType": "columns", + "table": "actors" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "username", + "entityType": "columns", + "table": "actors" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "avatar", + "entityType": "columns", + "table": "actors" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "pin", + "entityType": "columns", + "table": "actors" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "pin_expires_in", + "entityType": "columns", + "table": "actors" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "roblox_id", + "entityType": "columns", + "table": "actors" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "moderator", + "entityType": "columns", + "table": "actors" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "actors" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "actors" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "deleted_at", + "entityType": "columns", + "table": "actors" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "guilds" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "discord_id", + "entityType": "columns", + "table": "guilds" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "guilds" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon", + "entityType": "columns", + "table": "guilds" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ticket_category", + "entityType": "columns", + "table": "guilds" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ticket_log_channel", + "entityType": "columns", + "table": "guilds" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ticket_greeting", + "entityType": "columns", + "table": "guilds" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ticket_prompt_channel", + "entityType": "columns", + "table": "guilds" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ticket_prompt", + "entityType": "columns", + "table": "guilds" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "moderator_role", + "entityType": "columns", + "table": "guilds" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "guilds" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "guilds" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "ticket_attachments" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "ticket_attachments" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ticket_id", + "entityType": "columns", + "table": "ticket_attachments" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "actor_id", + "entityType": "columns", + "table": "ticket_attachments" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'watever'", + "generated": null, + "name": "key", + "entityType": "columns", + "table": "ticket_attachments" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "file_name", + "entityType": "columns", + "table": "ticket_attachments" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "file_type", + "entityType": "columns", + "table": "ticket_attachments" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "file_size", + "entityType": "columns", + "table": "ticket_attachments" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "ticket_comments" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ticket_id", + "entityType": "columns", + "table": "ticket_comments" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "actor_id", + "entityType": "columns", + "table": "ticket_comments" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "ticket_comments" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "ticket_comments" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "ticket_comments" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "ticket_messages" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "ticket_messages" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ticket_id", + "entityType": "columns", + "table": "ticket_messages" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "actor_id", + "entityType": "columns", + "table": "ticket_messages" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "actor_role", + "entityType": "columns", + "table": "ticket_messages" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "is_private", + "entityType": "columns", + "table": "ticket_messages" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "ticket_messages" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "embeds", + "entityType": "columns", + "table": "ticket_messages" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "components", + "entityType": "columns", + "table": "ticket_messages" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "ticket_messages" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "edited_at", + "entityType": "columns", + "table": "ticket_messages" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "deleted_at", + "entityType": "columns", + "table": "ticket_messages" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "ticket_participants" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ticket_id", + "entityType": "columns", + "table": "ticket_participants" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "actor_id", + "entityType": "columns", + "table": "ticket_participants" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "role", + "entityType": "columns", + "table": "ticket_participants" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "reason", + "entityType": "columns", + "table": "ticket_participants" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "ticket_participants" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "guild_id", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "local_id", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "channel_id", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "thread_id", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "close_reason", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "private_reason", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "opened_at", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "closed_at", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "closed_by", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_subject_activity_at", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_moderator_activity_at", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "opened_by", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "open_reason", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "subject", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "claimed_by", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "claimed_at", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "thanked_at", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "warned_at", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_message_at", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "has_moderator_message", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "has_author_message", + "entityType": "columns", + "table": "tickets" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "guild_id", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "actor_id", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "bug_number", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "title", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "description", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'[]'", + "generated": null, + "name": "affected_projects", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "false", + "generated": null, + "name": "sent", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "thread_id", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())", + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())", + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "closed_at", + "entityType": "columns", + "table": "bugs" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "image_hashes" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "hash", + "entityType": "columns", + "table": "image_hashes" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "first_seen", + "entityType": "columns", + "table": "image_hashes" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_seen", + "entityType": "columns", + "table": "image_hashes" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "1", + "generated": null, + "name": "seen_times", + "entityType": "columns", + "table": "image_hashes" + }, + { + "columns": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_sessions_user_id_users_id_fk", + "entityType": "fks", + "table": "sessions" + }, + { + "columns": [ + "guild_id" + ], + "tableTo": "guilds", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_actors_guild_id_guilds_id_fk", + "entityType": "fks", + "table": "actors" + }, + { + "columns": [ + "user_id" + ], + "tableTo": "users", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_actors_user_id_users_id_fk", + "entityType": "fks", + "table": "actors" + }, + { + "columns": [ + "message_id" + ], + "tableTo": "ticket_messages", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_ticket_attachments_message_id_ticket_messages_id_fk", + "entityType": "fks", + "table": "ticket_attachments" + }, + { + "columns": [ + "ticket_id" + ], + "tableTo": "tickets", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_ticket_attachments_ticket_id_tickets_id_fk", + "entityType": "fks", + "table": "ticket_attachments" + }, + { + "columns": [ + "actor_id" + ], + "tableTo": "actors", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_ticket_attachments_actor_id_actors_id_fk", + "entityType": "fks", + "table": "ticket_attachments" + }, + { + "columns": [ + "ticket_id" + ], + "tableTo": "tickets", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_ticket_comments_ticket_id_tickets_id_fk", + "entityType": "fks", + "table": "ticket_comments" + }, + { + "columns": [ + "actor_id" + ], + "tableTo": "actors", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_ticket_comments_actor_id_actors_id_fk", + "entityType": "fks", + "table": "ticket_comments" + }, + { + "columns": [ + "ticket_id" + ], + "tableTo": "tickets", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_ticket_messages_ticket_id_tickets_id_fk", + "entityType": "fks", + "table": "ticket_messages" + }, + { + "columns": [ + "actor_id" + ], + "tableTo": "actors", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_ticket_messages_actor_id_actors_id_fk", + "entityType": "fks", + "table": "ticket_messages" + }, + { + "columns": [ + "ticket_id" + ], + "tableTo": "tickets", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_ticket_participants_ticket_id_tickets_id_fk", + "entityType": "fks", + "table": "ticket_participants" + }, + { + "columns": [ + "actor_id" + ], + "tableTo": "actors", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_ticket_participants_actor_id_actors_id_fk", + "entityType": "fks", + "table": "ticket_participants" + }, + { + "columns": [ + "guild_id" + ], + "tableTo": "guilds", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_tickets_guild_id_guilds_id_fk", + "entityType": "fks", + "table": "tickets" + }, + { + "columns": [ + "closed_by" + ], + "tableTo": "actors", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_tickets_closed_by_actors_id_fk", + "entityType": "fks", + "table": "tickets" + }, + { + "columns": [ + "opened_by" + ], + "tableTo": "actors", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_tickets_opened_by_actors_id_fk", + "entityType": "fks", + "table": "tickets" + }, + { + "columns": [ + "subject" + ], + "tableTo": "actors", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_tickets_subject_actors_id_fk", + "entityType": "fks", + "table": "tickets" + }, + { + "columns": [ + "claimed_by" + ], + "tableTo": "actors", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_tickets_claimed_by_actors_id_fk", + "entityType": "fks", + "table": "tickets" + }, + { + "columns": [ + "guild_id" + ], + "tableTo": "guilds", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_bugs_guild_id_guilds_id_fk", + "entityType": "fks", + "table": "bugs" + }, + { + "columns": [ + "actor_id" + ], + "tableTo": "actors", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_bugs_actor_id_actors_id_fk", + "entityType": "fks", + "table": "bugs" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "api_tokens_pk", + "table": "api_tokens", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "sessions_pk", + "table": "sessions", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "users_pk", + "table": "users", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "actors_pk", + "table": "actors", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "guilds_pk", + "table": "guilds", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ticket_attachments_pk", + "table": "ticket_attachments", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ticket_comments_pk", + "table": "ticket_comments", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ticket_messages_pk", + "table": "ticket_messages", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "ticket_participants_pk", + "table": "ticket_participants", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "tickets_pk", + "table": "tickets", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "bugs_pk", + "table": "bugs", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "image_hashes_pk", + "table": "image_hashes", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "guild_id", + "isExpression": false + }, + { + "value": "user_id", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "actors_guild_user_idx", + "entityType": "indexes", + "table": "actors" + }, + { + "columns": [ + { + "value": "ticket_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "ticket_attachments_ticket_idx", + "entityType": "indexes", + "table": "ticket_attachments" + }, + { + "columns": [ + { + "value": "actor_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "ticket_attachments_actor_idx", + "entityType": "indexes", + "table": "ticket_attachments" + }, + { + "columns": [ + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "ticket_attachments_public_id_idx", + "entityType": "indexes", + "table": "ticket_attachments" + }, + { + "columns": [ + { + "value": "key", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "ticket_attachments_key_idx", + "entityType": "indexes", + "table": "ticket_attachments" + }, + { + "columns": [ + { + "value": "ticket_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "ticket_comments_ticket_idx", + "entityType": "indexes", + "table": "ticket_comments" + }, + { + "columns": [ + { + "value": "actor_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "ticket_comments_actor_idx", + "entityType": "indexes", + "table": "ticket_comments" + }, + { + "columns": [ + { + "value": "ticket_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "ticket_messages_ticket_idx", + "entityType": "indexes", + "table": "ticket_messages" + }, + { + "columns": [ + { + "value": "actor_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "ticket_messages_actor_idx", + "entityType": "indexes", + "table": "ticket_messages" + }, + { + "columns": [ + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "ticket_messages_public_id_idx", + "entityType": "indexes", + "table": "ticket_messages" + }, + { + "columns": [ + { + "value": "ticket_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "ticket_participants_ticket_idx", + "entityType": "indexes", + "table": "ticket_participants" + }, + { + "columns": [ + { + "value": "actor_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "ticket_participants_actor_idx", + "entityType": "indexes", + "table": "ticket_participants" + }, + { + "columns": [ + { + "value": "role", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "ticket_participants_role_idx", + "entityType": "indexes", + "table": "ticket_participants" + }, + { + "columns": [ + { + "value": "guild_id", + "isExpression": false + }, + { + "value": "local_id", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "tickets_guild_local_idx", + "entityType": "indexes", + "table": "tickets" + }, + { + "columns": [ + "discord_id" + ], + "nameExplicit": false, + "name": "users_discord_id_unique", + "entityType": "uniques", + "table": "users" + }, + { + "columns": [ + "discord_id" + ], + "nameExplicit": false, + "name": "guilds_discord_id_unique", + "entityType": "uniques", + "table": "guilds" + }, + { + "columns": [ + "hash" + ], + "nameExplicit": false, + "name": "image_hashes_hash_unique", + "entityType": "uniques", + "table": "image_hashes" + } + ], + "renames": [] +}
M pkgs/db/src/schemas/index.tspkgs/db/src/schemas/index.ts

@@ -1,3 +1,5 @@

+import * as s from "drizzle-orm/sqlite-core"; + export * from "./users"; export * from "./actors";

@@ -6,3 +8,13 @@ export * from "./tickets";

export * from "./bugs"; export * from "./automod"; + +export const apiKeys = s.sqliteTable("api_tokens", { + id: s + .text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + token: s.text("token").notNull(), + name: s.text("name").notNull(), + createdAt: s.integer("created_at", { mode: "timestamp" }).$defaultFn(() => new Date()), +});
M pkgs/db/src/utils/index.tspkgs/db/src/utils/index.ts

@@ -1,4 +1,23 @@

+import { eq } from "drizzle-orm"; + +import db, { apiKeys } from ".."; + export * from "./guild"; export * from "./session"; export * from "./ticket"; export * from "./user"; + +export async function createToken(name: string) { + const token = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(name)); + const tokenString = Array.from(new Uint8Array(token)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); + const [insertedToken] = await db.insert(apiKeys).values({ name, token: tokenString }).returning(); + + return insertedToken; +} + +export async function getToken(token: string) { + const [apiKey] = await db.select().from(apiKeys).where(eq(apiKeys.token, token)); + return apiKey; +}
M pkgs/db/src/utils/user.tspkgs/db/src/utils/user.ts

@@ -113,7 +113,11 @@ }

export async function getActorByPin(pin: string) { const [result] = await db.select().from(actors).where(eq(actors.pin, pin)); + return result; +} +export async function getActorByRobloxId(robloxId: string) { + const [result] = await db.select().from(actors).where(eq(actors.robloxId, robloxId)); return result; }