all repos — stealth-developers @ 11ae44fd13ccdbcc13f9599ffeb859e5016664ee

migration: blah blbha whatever
vi did:web:vt3e.cat
Mon, 29 Jun 2026 17:42:00 +0100
commit

11ae44fd13ccdbcc13f9599ffeb859e5016664ee

parent

c17ac1a822fd9ea6899e13ebefde048635ef2a33

M apps/bot/package.jsonapps/bot/package.json

@@ -13,6 +13,7 @@ "@sapphire/discord.js-utilities": "7.3.2",

"@sapphire/framework": "^5.5.0", "@sapphire/plugin-logger": "^4.1.0", "@sapphire/plugin-subcommands": "^7.0.1", + "@sapphire/result": "^2.8.0", "@sapphire/utilities": "^3.18.2", "@stealth-developers/config": "workspace:*", "@stealth-developers/db": "workspace:*",
M apps/bot/src/feats/tickets/listeners.tsapps/bot/src/feats/tickets/listeners.ts

@@ -22,6 +22,7 @@ type UploadStatus = "pending" | "uploading" | "success" | "failed";

type AttachmentStatus = { attachment: Attachment; fileName: string; + key: string; status: UploadStatus; uploadedUrl?: string; };

@@ -95,6 +96,7 @@

const attachmentStatuses: AttachmentStatus[] = attachments.map((attachment) => ({ attachment, status: "pending", + key: "temp", fileName: `${randomUUID()}.${attachment.name.split(".").pop()}`, }));

@@ -141,6 +143,7 @@ continue;

} attachmentStatuses[i]!.status = "success"; + attachmentStatuses[i]!.key = key; attachmentStatuses[i]!.uploadedUrl = publicUrl; statusMessage = await statusMessage.edit({

@@ -172,7 +175,7 @@ components: components,

attachments: attachmentStatuses.map((status) => ({ id: status.uploadedUrl!, fileName: status.fileName, - key: "", + key: status.key, fileType: status.attachment.contentType || status.fileName.split(".").pop()!, fileSize: status.attachment.size, })),
A apps/migration/src/1.guild-actor.ts

@@ -0,0 +1,54 @@

+import db, { actors, guilds, users } from "@stealth-developers/db"; + +const [guild] = await db + .insert(guilds) + .values({ + createdAt: new Date(), + updatedAt: new Date(), + name: "Stealth Developers", + discordId: "895998762630127616", + icon: "36c6e14d0933eb338826599b632df818", + }) + .returning(); + +if (!guild) throw new Error("Failed to create guild"); + +const [user] = await db + .insert(users) + .values({ + createdAt: new Date(), + updatedAt: new Date(), + + avatar: + "https://cdn.discordapp.com/icons/895998762630127616/36c6e14d0933eb338826599b632df818.png?size=4096", + discordId: "895998762630127616", + displayName: "Stealth Developers", + username: "Stealth Developers", + acceptedPrivacyPolicy: true, + acceptedTermsOfService: true, + }) + .returning(); + +if (!user) throw new Error("Failed to create user"); + +const [actor] = await db + .insert(actors) + .values({ + createdAt: new Date(), + updatedAt: new Date(), + + userId: user.id, + guildId: guild.id, + + avatar: user.avatar, + username: user.username, + displayName: user.displayName, + discordId: user.discordId, + }) + .returning(); + +if (!actor) throw new Error("Failed to create actor"); + +console.log("Guild:", guild); +console.log("User:", user); +console.log("Actor:", actor);
A apps/migration/src/2.get-members.ts

@@ -0,0 +1,63 @@

+import type { GuildMemberResponse } from "@purrkit/types"; + +import fs from "node:fs"; + +import { DISCORD_GUILD_ID } from "./constants"; +import { rest } from "./lib/rest"; +import { db, tickets } from "./old-db"; + +const neededUsers = await db + .select({ + subjectId: tickets.authorId, + closerId: tickets.closedBy, + }) + .from(tickets) + .execute(); + +const activeUserIds = new Set<string>(); +for (const row of neededUsers) { + if (row.subjectId) activeUserIds.add(row.subjectId); + if (row.closerId) activeUserIds.add(row.closerId); +} +console.log(`found ${activeUserIds.size} needed user ids`); + +let lastUserId: string | undefined; +const fetchedMembers: GuildMemberResponse[] = []; + +while (true) { + const response = await rest.guilds(DISCORD_GUILD_ID).members.get({ + query: { + limit: 1000, + after: lastUserId ? Number(lastUserId) : undefined, + }, + }); + + if (!response.ok || response.data.length === 0) { + console.log(JSON.stringify(response.data)); + break; + } + + lastUserId = response.data[response.data.length - 1]!.user.id; + + fs.writeFileSync(`members/members.${lastUserId}.json`, JSON.stringify(response.data)); + fetchedMembers.push(...response.data); + + console.log( + `fetched ${response.data.length} members page (total ${fetchedMembers.length + response.data.length})`, + ); + + if (response.data.length < 1000) break; +} + +fs.writeFileSync(`members/all-members.json`, JSON.stringify(fetchedMembers)); + +const missingUsers = new Set<string>(activeUserIds); +for (const member of fetchedMembers) missingUsers.delete(member.user.id); + +missingUsers.delete("system"); +missingUsers.delete("reporter"); +missingUsers.delete("0"); +missingUsers.delete("1782593884"); + +console.log(`missing ${missingUsers.size} users`); +fs.writeFileSync(`members/missing-users.json`, JSON.stringify(Array.from(missingUsers)));
A apps/migration/src/3.get-users.ts

@@ -0,0 +1,28 @@

+import type { UserResponse } from "@purrkit/types"; + +import fs from "node:fs"; + +import { rest } from "./lib/rest"; + +const missingUserIds = JSON.parse( + fs.readFileSync("./members/missing-users.json", "utf8"), +) as string[]; + +const out: { users: UserResponse[]; errors: any[] } = { + users: [], + errors: [], +}; + +for (const userId of missingUserIds) { + const user = await rest.users(userId).get(); + if (user.ok) { + out.users.push(user.data); + process.stdout.cursorTo(0); + process.stdout.write(`Fetched ${out.users.length}/${missingUserIds.length} users`); + } else { + console.error(`Failed to fetch user ${userId}:`, user.status, user.data); + out.errors.push({ userId, status: user.status, data: user.data }); + } +} + +fs.writeFileSync("./members/fetched-users.json", JSON.stringify(out, null, 2));
A apps/migration/src/4.save-users.ts

@@ -0,0 +1,92 @@

+import type { GuildMemberResponse, UserResponse } from "@purrkit/types"; + +import db, { actors, users } from "@stealth-developers/db"; +import fs from "node:fs"; + +import { GUILD_ID } from "./constants"; + +const allUsers = JSON.parse( + fs.readFileSync("members/fetched-users.json", "utf8"), +) as UserResponse[]; +const allMembers = JSON.parse( + fs.readFileSync("members/all-members.json", "utf8"), +) as GuildMemberResponse[]; + +for (const member of allMembers) { + const [user] = await db + .insert(users) + .values({ + discordId: member.user.id, + displayName: member.nick ?? member.user.global_name, + username: member.user.username, + avatar: member.user.avatar, + createdAt: new Date(), + updatedAt: new Date(), + }) + .onConflictDoNothing() + .returning(); + + if (!user) { + console.error(`failed to save user for member ${member.user.id}`); + continue; + } + + await db + .insert(actors) + .values({ + userId: user.id, + discordId: member.user.id, + displayName: member.nick ?? member.user.global_name, + username: member.user.username, + avatar: member.user.avatar, + guildId: GUILD_ID, + + createdAt: new Date(), + updatedAt: new Date(), + }) + .onConflictDoNothing(); +} + +for (const user of allUsers) { + if (!user) { + console.error(`failed to save user ${user.id}`); + continue; + } + + const [dbUser] = await db + .insert(users) + .values({ + discordId: user.id, + displayName: user.global_name, + username: user.username, + avatar: user.avatar, + createdAt: new Date(), + updatedAt: new Date(), + }) + .onConflictDoNothing() + .returning(); + + if (!dbUser) { + console.error(`failed to save user ${user.id}`); + continue; + } + + const [dbActor] = await db + .insert(actors) + .values({ + userId: dbUser.id, + discordId: user.id, + displayName: user.global_name, + username: user.username, + avatar: user.avatar, + guildId: GUILD_ID, + createdAt: new Date(), + updatedAt: new Date(), + }) + .onConflictDoNothing() + .returning(); + + if (!dbActor) { + console.error(`failed to save actor for user ${user.id}`); + } +}
D apps/migration/src/actors.json

@@ -1,26 +0,0 @@

-[ - { - id: 511, - discordId: "1350116449611677770", - displayName: "Stealth Developers", - username: "Stealth Developers", - avatar: "https://images-ext-1.discordapp.net/external/j7H9Ep50cCN5igmEKGwFnv7frj2590Xg2Z4hvHGFPPo/%3Fsize%3D4096/https/cdn.discordapp.com/icons/895998762630127616/36c6e14d0933eb338826599b632df818.png?format=webp&quality=lossless&width=852&height=852", - acceptedPrivacyPolicy: false, - acceptedTermsOfService: false, - createdAt: "2026-06-28T19:13:33.000Z", - updatedAt: "2026-06-28T19:13:33.000Z", - } - { - id: 511, - guildId: 1, - userId: 511, - discordId: "1350116449611677770", - displayName: "Stealth Developers", - username: "Stealth Developers", - avatar: "https://images-ext-1.discordapp.net/external/j7H9Ep50cCN5igmEKGwFnv7frj2590Xg2Z4hvHGFPPo/%3Fsize%3D4096/https/cdn.discordapp.com/icons/895998762630127616/36c6e14d0933eb338826599b632df818.png?format=webp&quality=lossless&width=852&height=852", - moderator: true, - createdAt: "2026-06-28T19:13:33.000Z", - updatedAt: "2026-06-28T19:13:33.000Z", - deletedAt: null, - } -]
D apps/migration/src/collect-users.ts

@@ -1,39 +0,0 @@

-import type { GuildMemberResponse } from "@purrkit/types"; - -import fs from "node:fs"; - -import { db, tickets } from "./old-db"; - -const users = await db - .select({ - subjectId: tickets.authorId, - closerId: tickets.closedBy, - }) - .from(tickets) - .execute(); // { subjectId, closerId }[] - -const activeUserIds = new Set<string>(); -for (const row of users) { - if (row.subjectId) activeUserIds.add(row.subjectId); - if (row.closerId) activeUserIds.add(row.closerId); -} - -const membersArray = JSON.parse( - fs.readFileSync("members/members.json", "utf-8"), -) as GuildMemberResponse[]; - -const allMembersLookup = new Map<string, GuildMemberResponse>( - membersArray.map((member) => [member.user.id, member]), -); - -const membersMap: Record<string, GuildMemberResponse> = {}; -const missingUserIds: string[] = []; - -for (const id of activeUserIds) { - const member = allMembersLookup.get(id); - if (member) { - membersMap[id] = member; - } else { - missingUserIds.push(id); - } -}
M apps/migration/src/constants.tsapps/migration/src/constants.ts

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

-export const SYSTEM_USER_ID = 511; -export const SYSTEM_ACTOR_ID = 511; +export const SYSTEM_USER_ID = 1; +export const SYSTEM_ACTOR_ID = 1; export const GUILD_ID = 1; +export const TOKEN = ""; +export const DISCORD_GUILD_ID = "895998762630127616";
D apps/migration/src/fetch-missing-users.ts

@@ -1,122 +0,0 @@

-import type { UserResponse } from "@purrkit/types"; - -import { RateLimitManager } from "@purrkit/ratelimit"; -import { RESTClient } from "@purrkit/rest"; - -const limiter = new RateLimitManager({}); - -const TOKEN = process.env.TOKEN!; - -const rest = RESTClient({ - token: TOKEN, - fetch: limiter.fetch, -}); - -const missingUserIds: string[] = [ - "1782593884", - "795061786255687711", - "904144997559975937", - "578420334110179333", - "1413504630586998997", - "1437781069779832954", - "1029008522588987443", - "835909579497799691", - "1223644746896117791", - "493127296270467072", - "1329636666763116628", - "1324912280890118174", - "1350148294151503984", - "1258548326912098477", - "1475011243722018968", - "466382971251982357", - "1048430495094022205", - "1400279059765657630", - "717297081215352872", - "1431735081705275506", - "550840432997957633", - "504437490711527424", - "1281578414951632928", - "1438881999338213457", - "1243327172714958861", - "1455011612472643667", - "835703930091864094", - "786644500030029835", - "1069335764166524978", - "1365420920423841873", - "1319940559506378763", - "709796139611979879", - "1252497096129642506", - "872560135258669100", - "926198568715436063", - "1388237404376269016", - "733250938378125374", - "1346451794557009972", - "1457403097008832512", - "1387565913041010688", - "1338023988181467267", - "1482884859872673813", - "1484755143378473101", - "908069295337111572", - "1104329197742194739", - "836829335628414997", - "772837851255275530", - "805911623968489492", - "1307061098159013971", - "211104933028560896", - "1162930129954144256", - "993513174408114176", - "840586677516369951", - "1482060412076949597", - "1111315394406387765", - "866852794443366411", - "345391810472050689", - "722126325048934522", - "602335935543115776", - "1037074050100506634", - "573585328288563213", - "957637851569356902", - "988651720756301824", - "1298713042330062879", - "796477858037366784", - "496036367151661066", - "1471423782983041074", - "1091124167736299551", - "1115003796595163216", - "1130254575551664341", - "1069248360114028544", - "1388100438326378498", - "723208823765991490", - "1497641855343530198", - "1012951474629783614", - "1509220340637761556", - "792016188338077747", - "1073360202583253075", - "164569267243712512", - "525936042012835841", - "1343299975291080734", - "1386813477318037665", - "1213514366910074900", - "1265039531132850247", - "1162445027826475089", - "1120382975591338004", - "1241411863829549221", - "768001665054801930", -]; -// const users: UserResponse[] = []; -const out: { users: UserResponse[]; errors: any[] } = { - users: [], - errors: [], -}; - -for (const userId of missingUserIds) { - const user = await rest.users(userId).get(); - if (user.ok) { - out.users.push(user.data); - console.log(`Fetched user ${userId}`); - } else { - console.error(`Failed to fetch user ${userId}:`, user.status, user.data); - out.errors.push({ userId, status: user.status, data: user.data }); - } -} - -console.log(JSON.stringify(out, null, 2));
D apps/migration/src/fetch-users.ts

@@ -1,39 +0,0 @@

-import { RESTClient } from "@purrkit/rest"; -import fs from "node:fs"; - -const TOKEN = process.env.TOKEN!; -const GUILD_ID = process.env.GUILD_ID!; - -const rest = RESTClient({ - token: TOKEN, -}); - -let lastUserId: number | undefined; -const allMembers: unknown[] = []; - -while (true) { - const response = await rest.guilds(GUILD_ID).members.get({ - query: { - limit: 1000, - after: lastUserId, - }, - }); - - if (!response.ok || response.data.length === 0) { - console.log(JSON.stringify(response.data)); - break; - } - - lastUserId = response.data[response.data.length - 1]!.user.id; - - fs.writeFileSync(`members/members.${lastUserId}.json`, JSON.stringify(response.data)); - allMembers.push(...response.data); - - console.log( - `fetched ${response.data.length} members page (total ${allMembers.length + response.data.length})`, - ); - - if (response.data.length < 1000) break; -} - -fs.writeFileSync(`members/members.json`, JSON.stringify(allMembers));
A apps/migration/src/lib/rest.ts

@@ -0,0 +1,11 @@

+import { RateLimitManager } from "@purrkit/ratelimit"; +import { RESTClient } from "@purrkit/rest"; + +import { TOKEN } from "../constants"; + +const limiter = new RateLimitManager({}); + +export const rest = RESTClient({ + token: TOKEN, + fetch: limiter.fetch, +});
D apps/migration/src/save-users.ts

@@ -1,91 +0,0 @@

-import type { GuildMemberResponse, Snowflake, UserResponse } from "@purrkit/types"; - -import db, { actors, users } from "@stealth-developers/db"; -import fs from "node:fs"; - -type Data = Record<Snowflake, GuildMemberResponse>; - -const file = fs.readFileSync("./members/filtered-map.json", "utf-8"); -const memberData: Data = JSON.parse(file); -const GUILD_ID = 1; - -for (const member of Object.values(memberData)) { - const [user] = await db - .insert(users) - .values({ - discordId: member.user.id, - displayName: member.nick ?? member.user.global_name, - username: member.user.username, - avatar: member.user.avatar, - createdAt: new Date(), - updatedAt: new Date(), - }) - .onConflictDoNothing() - .returning(); - if (user) { - const [actor] = await db - .insert(actors) - .values({ - userId: user.id, - guildId: GUILD_ID, - - discordId: user.discordId, - username: user.username, - displayName: user.displayName, - avatar: user.avatar, - - createdAt: new Date(), - updatedAt: new Date(), - }) - .onConflictDoNothing() - .returning(); - - if (actor) { - console.log(`actor inserted for member ${member.user.id}`); - } - } else { - console.warn(`no user inserted for member ${member.user.id}`); - } -} - -const userFile = fs.readFileSync("./members/filtered-users.json", "utf-8"); -const userData: UserResponse[] = JSON.parse(userFile); - -for (const user of userData) { - const [dbUser] = await db - .insert(users) - .values({ - discordId: user.id, - displayName: user.global_name, - username: user.username, - avatar: user.avatar, - createdAt: new Date(), - updatedAt: new Date(), - }) - .onConflictDoNothing() - .returning(); - if (dbUser) { - const [actor] = await db - .insert(actors) - .values({ - userId: dbUser.id, - guildId: GUILD_ID, - discordId: dbUser.discordId, - username: dbUser.username, - displayName: dbUser.displayName, - avatar: dbUser.avatar, - createdAt: new Date(), - updatedAt: new Date(), - }) - .onConflictDoNothing() - .returning(); - - if (actor) { - console.log(`actor inserted for member ${user.id}`); - } else { - console.warn(`no actor inserted for member ${user.id}`); - } - } else { - console.warn(`no user inserted for member ${user.id}`); - } -}
M apps/migration/src/tickets.tsapps/migration/src/5.save-tickets.ts

@@ -264,12 +264,12 @@

console.log(`Inserting ${ticketsToInsert.length} tickets...`); const processedTicketIds = new Set<string>(); let insertedTickets = 0; - let skippedTickets = 0; + let skippedTickets = []; for (const ticket of ticketsToInsert) { if (processedTicketIds.has(ticket.id)) { console.warn(`[DEDUPE] skipping duplicate ticket ID in memory: ${ticket.id}`); - skippedTickets++; + skippedTickets.push(ticket.id); continue; } processedTicketIds.add(ticket.id);

@@ -280,7 +280,7 @@ insertedTickets++;

} catch (err: any) { if (isUniqueConstraintError(err)) { console.warn(`[WARNING] skipping ticket ID conflict in DB: ${ticket.id}`); - skippedTickets++; + skippedTickets.push(ticket.id); continue; } console.error(

@@ -292,8 +292,9 @@ throw err;

} } console.log( - `tickets status: migrated ${insertedTickets}, skipped ${skippedTickets} duplicate/conflict rows.`, + `tickets status: migrated ${insertedTickets}, skipped ${skippedTickets.length} duplicate/conflict rows.`, ); + console.log(skippedTickets); console.log(`inserting ${participantsToInsert.length} ticket participants...`); let insertedParticipants = 0;
M apps/web/src/views/TicketDetailView.vueapps/web/src/views/TicketDetailView.vue

@@ -24,9 +24,6 @@ const messages = ref<SanitisedTicketMessage[]>([]);

const attachments = ref<SanitisedTicketAttachment[]>([]); const comments = ref<SanitisedTicketComment[]>([]); -const uploading = ref(false); -const uploadProgress = ref(0); -const uploadError = ref<string | null>(null); const highlightUploadButton = ref(false); async function loadTicket() {

@@ -313,27 +310,18 @@ </div> -->

</div> </div> - <!-- Staff Comments Section (fetched separately) --> <div class="ticket-comments" v-if="comments.length > 0"> <h2>Staff Comments</h2> - <div class="comments-grid"> - <div v-for="comment in comments" :key="comment.id" class="comment-card"> - <div class="comment-header"> - <img :src="getAvatarUrl(comment.actors?.author)" class="comment-avatar" alt="" /> - <div class="comment-meta"> - <span class="comment-author">{{ - comment.actors?.author?.displayName || comment.actors?.author?.username || "Staff" - }}</span> - <span class="comment-time"><TimeAgo :time="comment.createdAt" /></span> - </div> - </div> + <div class="comments-list"> + <div v-for="comment in comments" :key="comment.id" class="comment"> + <UserChip :user="comment.actors?.author" /> <p class="comment-text">{{ comment.content }}</p> </div> </div> </div> - <!-- Messages Section (fetched separately) --> <div class="ticket-messages"> + <h2>Messages</h2> <div class="messages-container" v-if="groupedMessages.length > 0"> <div v-for="(group, index) in groupedMessages"

@@ -683,69 +671,35 @@ }

} .ticket-comments { - display: flex; - flex-direction: column; - gap: 1rem; - padding-bottom: 1.5rem; - border-bottom: 2px solid hsla(var(--surface0) / 0.5); + display: flex; + flex-direction: column; + background-color: hsl(var(--crust)); + border-radius: 1.5rem; + overflow: hidden; + margin-bottom: 0.5rem; h2 { + padding: 0.5rem 1rem; font-size: 1.5rem; font-weight: 800; color: hsl(var(--text)); } - .comments-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(min(100%, 340px), 1fr)); - gap: 1rem; - } - - .comment-card { - display: flex; - flex-direction: column; - gap: 0.75rem; - padding: 1rem; - border-radius: 1rem; - background-color: hsla(var(--base) / 0.5); - border: 1px solid hsla(var(--surface0) / 0.4); + .comments-list { + display: flex; + flex-direction: column; + background-color: hsl(var(--crust)); - .comment-header { + .comment { display: flex; - align-items: center; + flex-direction: column; gap: 0.75rem; + padding: 0.5rem 1rem; - .comment-avatar { - width: 2rem; - height: 2rem; - border-radius: 50%; - object-fit: cover; - background-color: hsla(var(--surface0)); + &:hover { + background-color: hsla(var(--surface0) / 0.25); + transition: none; } - - .comment-meta { - display: flex; - flex-direction: column; - - .comment-author { - font-weight: 700; - font-size: 0.9rem; - color: hsl(var(--text)); - } - - .comment-time { - font-size: 0.75rem; - color: hsl(var(--subtext0)); - } - } - } - - .comment-text { - font-size: 0.9rem; - line-height: 1.5; - color: hsl(var(--text)); - white-space: pre-wrap; - word-break: break-word; } } }

@@ -759,10 +713,10 @@ border-radius: 1.5rem;

overflow: hidden; h2 { + padding: 0.5rem 1rem; font-size: 1.5rem; font-weight: 800; color: hsl(var(--text)); - margin-bottom: 0.5rem; } .messages-container {
M bun.lockbun.lock

@@ -46,6 +46,7 @@ "@sapphire/discord.js-utilities": "7.3.2",

"@sapphire/framework": "^5.5.0", "@sapphire/plugin-logger": "^4.1.0", "@sapphire/plugin-subcommands": "^7.0.1", + "@sapphire/result": "^2.8.0", "@sapphire/utilities": "^3.18.2", "@stealth-developers/config": "workspace:*", "@stealth-developers/db": "workspace:*",