From 1c01ecd773023a1267e776f29a9d3dd42a471b8b Mon Sep 17 00:00:00 2001 From: Rory& Date: Mon, 14 Sep 2026 19:26:10 +0200 Subject: [PATCH] Invite schemas? --- assets/openapi.json | Bin 1073593 -> 1085097 bytes assets/schemas.json | Bin 492697 -> 502700 bytes extra/admin-api/Spacebar.Offload/Program.cs | 4 -- .../routes/channels/#channel_id/invites.ts | 15 +++-- src/api/routes/guilds/#guild_id/invites.ts | 5 +- src/database/entities/Guild.ts | 58 +++++++++++++++++- src/database/entities/Invite.ts | 23 ++++++- .../api/guilds/GuildProfileResponse.ts | 8 ++- src/schemas/api/guilds/Invite.ts | 22 +++++-- src/schemas/responses/TypedResponses.ts | 1 - 10 files changed, 111 insertions(+), 25 deletions(-) diff --git a/assets/openapi.json b/assets/openapi.json index cced6947ff7fb74aed5a6420a9eb3435b2bc4694..22e9257e339aeb81e9d8186a6ddb6dd12d2d5a59 100644 GIT binary patch delta 2673 zcmZ`*eNa@_6`%Xwz3;vS7T2hN2(knyYPBrJZ;V-J#h@ZfK#kP0yXYcBVV7O}n8^gz zNepA!MD$JgHSHKz9gWUpbSs@)QB!H9A2v?VCqI_b5PDk8flp0VdPVe)<%nvO@zM?M=5_7{ zlvYTqweALMuRZw!!M0;^rRJKt0G$Wq8l|FjLvyzN>$j}cy0@r3zgvd*33T&^)%Mn?|OyZ|Y$Qenq99oW| z6Kpp6t4R~Ax1|**8l*m)e}a|m*g6HSWh@Ey8sb9LO!ik;nPP(Ph90Bo2Cav^o-#4` zL$X#v_Ysxi(NgAyV+-fPb&Bjy;q6`{X;gng$MtMBnY7?v=2`gz&eJ`HZ;Zl2-za532na@#UCkIn0Ss%G7TB`EI_E6jKiCi%q*Pe z$h0)E@CvzW`hTVD(kr5uQ;?$gWW(YmA6M*JY6P|)E581U1?I1jiJa&9&D>{sL zhY?H9(AKVD#6FCi8A7zqArDUu@7T5}37_F9ERkJK*-)A#TM~liTai$S+>|nnN;tBK zac^)8nkOJwU}mr$=e<3Jsv=xF<$YAMU3}Q&@OxUC*SFL*;|6CgM5js&zZ}al;%d;< zQeodKryKwJ_sHuI3CW|xQ+wr6l6dccykQos$D|y&%^QH#NAu$E1l{wL4me`+6!C{k z@_)TT@4oyD@eh?zDso4&y^`iSn}UPe*%&QKOMt)lmW{bsT{s5(!;PUk{;E%M=7xQ)H&4X&5?_hsj!vs0tDSsW1>sV zZ}M!T7_QNB4!W&UN4y~sT1viWbh#G&iox~;D}lF>DY&tlPe*C7WQQ+}FwAtN6u>c{ zW@0c$u_N@F!f{=bGc|ir2D<%Rfz>HxBXVAu4@aMx2FoGIgsW$iG{dJQCi%6ZKCZpm zH%W_>&d_XqN$Bg4=JtG_%}R8AoQ@LFx6Mx(kN$(?2k5*?Q;>0$eyqhhI6}4R1kLqY zJ#F=DbmCr}oWJAFqo{g^G4lqGzqzq_mAk#!+v>!&?aCxroa!t~YxAnLt?m|&b5)bO zb&bcZU3$A-k0Y4{oKJZhaOp)_W~6Jbb^F(N+Ls1A{=h;nc3bJF_%lgEWM*m_9Gaf8 z;{%DJUy;)h^idVgrD|r6pG}kXE5UDD=rQAh#NhAwE4eVQU|f{=S%s7kUrks3oDiXl zqtH24VL07FkBC!W(mb=cx|sh>#F09vyu9Y5< zMbmM1F-aow zF!bjE<{q)^ffusavjNo$navm=&rtDWs zjIq&kktorMF04T#GLWG;SO+J{J9L z+=J-nG%xNZS92VuaC||FzaD<~4=cEpOVWoOQVitj|8#oiX!(*BqHB6`8Y-*EXw)ub zBN6tHyp#vi!O4@V9?3OH@8(6k9Kpj%k#ScKFk1B{W`!%0%}nU&qoLkVq4AI(Fc9Am zu{YGSL}Z^(>!@lJ)ANKEBO9gkI7x}fT6wMn`v}RYjbGV-h$&gcy*H#M;}XM&TRaD` zpOai95oNlbbr&uimFB?mg4z|=P!Q2^#FnwlL?xna%mk~f6pc=}`bH-<;N3E^5>;6w z-}qj6C{AzR(%8O}P1AOL$gtx&ai{qF-c_DJz_Z%9J^;5tky!; zCgQPrH5s;B>^wq$SE#hkgI_hO`QNdxIR+SUqI#vQQ>jJjxvyW`#>T_9f+fSZm#4?o zn6pLq3;WMVCVU}ElkSJ%!RC507E7{-iUYg#sigkbR_h%y;Eo14kCgjMw{H zwhpx)28ipOi{O3iNOh}G#_ z8y+EZOr9dEV%|S_?>k+a`c-|I^p&KqWU;9~rGO+xKwGhCj|euJYY9R{DO%TrffRk2 h#e{*>56bVWJR^dZ5p@#61*x_!o;pJCRLhZ?{{foB>Nx-a delta 1049 zcmaKrZBUd|6vyX2&wblhmbTp`1lvF&qZX*i3{w`zaA=HOst@Sw3{Nv@k&`a&q!d%( z^g=NSP)_(O}S$rI45Q zSq(+skjr|_nFx3v;I$BI5?U$|7>VwMwvDO}1_*f!Vm6Th@o$C1bMs_Kf#I{X8XXB@ zb;9^*;=tMw;-b)SlB7d_C-FdU3$sFLJ39(J(?sI2aq_&2R&3i@3sI`P1g&w(L3@MU z0*+0J9fFcLu{cTQO#j>26o>dn%nX6e)C1vBH4FTHl|f;S@aX<|3EDWRX=1Q^>=hx|daMUg&#JW|A4|c0jst??_6WXlG`h@_4Ej?;KZ0l5Sh2`H1 z4`^4JfbN5;6@7dOE~$4&nRvP3sDTw z8N~xJAwGk%2UIr%ewOucyi3hc?k|F}ge+X7Xw4U!Xj9FUG-NVlHe|urq-8ltEj|ui vwp1$+ykhk=xu>m$Z0Mf0pDGt+!HSeKj0g2tq@^`0O*c!@@MekQ@Du+6F@TpB diff --git a/assets/schemas.json b/assets/schemas.json index fe58930aa8659893eed81144962f3193c4d5f883..72ac7694b5d29cca9323dd64c132c96fb905bbb4 100644 GIT binary patch delta 2530 zcmai0eQ;FO6`%X=$J=!ib^|HoJDUU<1ck-OSNIAdfka3GNHCDPyCjsWR!0}FH0^Tp@Y5jkVezFi^tA)2oZ=Q}X)4>C(TcTu3}6shU&&;7g?mERY<|2O|>%E7=5a!4EA`#dV& zU;-6hb*=W*(a#V%$R6%FFPdaDLW()U`7{;FUS(YR^T^yyi0xsga0g^HV)MyU=-r`Y zV{|WbAu^%}MB8~l4(uR(1iKHi)upLp!2os04-%@TG;-B5bCa?d5TvcrG6RpAeMnCopkVzPRfu!KdVPYQ#)$%q;h3f!G zLf|O7B+nipPfe8XpCUHP|4Z0DJz;v%l*>(;=c9#i`sw{}T~elKVa3vu6aQ)zrD?_< z73URmq3uoxvI~SmbDwf(=MsXNb*cld+ls}o*M<_(wPR-XviL=TgaETOZbD&~A#s(O z=kS8QL6;i81;-85D2=O*=>P^IUN?|D&n0YrJsuaoA2S*pTU;MoWCj!G!|#YV@-0I( zhTOeB9x|?9=Jsy`IaU_8t+<^~e=%k^`mQq3%uUo!GZ7mkHaOo?Cc@>VxrlwNlg%?z zXGyeLFr??v334clo~D{Bc`{s62)9- zsMUqy>0}CP3AVX$b(+XP@jKj#P=MTz-V1CwV!x%?(zS`6berWS^>T|&l1=N`2$lb; zVNWV><*+0)rSfF0`OqTV-o%9bSrsc2vdqo)Q!Scjkq6(T{_n{xx7dI{c!Ln!WHC|} z@e;VYm2^Y~L`_0Cr-e;L^rB8-&qoz_^>0chMvpNodI!lfh&(QGWXP$!OAvaS2;{!Z zW{#D(dmb-<>@p0_ab%%j>&4Z7CJhQHTyc< zMzv?b@kc&Oi`22??Y`E=`o>ysdt78 znef(*HeYL-`|03T9KJ>iQ{#31gtxW9*S^tEmFb#w3&k!YVN6?^=Rj|a-iIwUOsA5A zZcd~x+{U~JU7;JzL`&<-;$${Hba7QKe2ondsV_SfLy!T-75a1JHZxVuTE&*w;F-lO z@@%0fr5K&fZSuI_JuGpyhkZ3k-aO5;1)4jcY7cF*YOmGaCx@=kY}#{BVY2&E_Q4dH zL3t;Qw_nRnHX}>=i$pe)g*HB`02cy^iiwN(%EX&rA`fXU%gT61?0Si>kmr_(H<{Uw z<%_%chy`a)>XhHHj3>*St-K=>yK9+>lb!rAL{`(1Z+j`a&Iv1AKjteCo=Z#Q7yEdN zYFf*`Y-OE9mK5?!s$^5e2P*FLDXQ$P=Jl4ir~b!87RT^gT-7!?lM%~M=i|;yWsT9! zRfOk@b(kGhtlG0nA2k*Nfg|EUj6!!sZ}Su2v{Q${%(AvKbi7qiOooAGf z(Gw1(1kpY{L)|TEAtF`!T#P+Hd(Gd2169flBk7wl&?%loB%42g;Sn}1ejr#x^i!?s zntK+p_T{rHvHNQ#G5iAx&Y@Dwff$Udv39;|9J-ahhNb1M1FIX&ghVpb*#uP goj+hBMCyNC{fo`Az~Z-rMgB^Pqy6$kn|jmnU!6gjj{pDw delta 968 zcmZ{jZETZO7>0A+^PKZ`AN_C}Rv7K-9EjaQDk@^#NBM|fQ7M=>t2SVCBD%T#*hX}j zR1FyfHXEAs&dkh4AV{W=%FFT?;^<}sF-v3`!V)5Jkr*^=t_cQX$Tr_G<3InLA168I zx$o;-=kp1F?>>LVDzkow|2_cUu)NC}E2FS=dalKfT$|Z}(CgY`MZSJmAU7)4z;lP( z$tO;U?-g1elqDEAEC_X5$%90xZ#(HF$ln)-CLHu4^_HW~ca{Yx!{k+Y@S&!b7N*;dQEmMzk5tyUCfI+JH#gZG-&g$ z`Er4+{TzH{khYY5I#Wv}uosh0t)#5rnXQysm!1~H!#ybx{zaG zN)wvrg+}^}bYaq~*5Z>e?dQN4#mez)*q)NTPOS*VX&FZP#Qkth(-JgIQ8Q2vM zHu&!Ubc1uVcHqKux*}bsi_rg-DCEp6l`iAwZ_sa!MO;66)nx}?iYvDt^Lu0wcPCU= zJ0{zu;y>?G=S)=1sXX@mr7oB}x0a%kr{+|fyVMI5;s)m(*<+xGJocE4KgD z`C^U_YO5N%XKkPI^XRaTJ=gRRi%)xWRw!G#9Su120j)>UL1h^4Q`LyxOJnG~sWu>H d*fc%wn^D~8R7DVvh!^>tBE6K@`q=#2^AD_ [i, true])), //TODO: cleanup - }); + const invites = ( + await Invite.find({ + where: { guild_id, channel_id }, + relations: Object.fromEntries(PublicInviteRelation.map((i) => [i, true])), //TODO: cleanup + }) + ).map((x) => x.toPublicJSON()); - res.status(200).send(invites); + res.status(200).send(invites satisfies InviteListResponse); }, ); diff --git a/src/api/routes/guilds/#guild_id/invites.ts b/src/api/routes/guilds/#guild_id/invites.ts index 89ac00c88..aa1ccb225 100644 --- a/src/api/routes/guilds/#guild_id/invites.ts +++ b/src/api/routes/guilds/#guild_id/invites.ts @@ -19,6 +19,7 @@ import { Request, Response, Router } from "express"; import { route } from "@spacebar/api/middlewares"; import { Invite, PublicInviteRelation } from "@spacebar/database"; +import { InviteListResponse } from "@spacebar/schemas/api/guilds/Invite"; const router = Router({ mergeParams: true }); @@ -28,7 +29,7 @@ router.get( permission: "MANAGE_GUILD", responses: { 200: { - body: "APIInviteArray", + body: "InviteListResponse", }, }, }), @@ -48,7 +49,7 @@ router.get( }), ); - return res.json(invites.filter((i) => !i.isExpired())); + return res.json(invites.filter((i) => !i.isExpired()).map((x) => x.toPublicJSON()) satisfies InviteListResponse); }, ); diff --git a/src/database/entities/Guild.ts b/src/database/entities/Guild.ts index f7de10327..514fccca7 100644 --- a/src/database/entities/Guild.ts +++ b/src/database/entities/Guild.ts @@ -18,8 +18,17 @@ import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm"; import { arrayRemove } from "@spacebar/extensions"; -import { Config, Snowflake, handleFile } from "@spacebar/util"; -import { DiscoverableGuild, GuildWelcomeScreen, IntegrationGuild } from "@spacebar/schemas"; +import { Config, handleFile, Snowflake } from "@spacebar/util"; +import { + DiscoverableGuild, + GuildNsfwLevel, + GuildPremiumTier, + GuildProfileResponse, + GuildVerificationLevel, + GuildVisibilityLevel, + GuildWelcomeScreen, + IntegrationGuild, +} from "@spacebar/schemas"; import { Ban } from "./Ban"; import { BaseClass } from "./BaseClass"; import { Channel } from "./Channel"; @@ -33,6 +42,7 @@ import { User } from "./User"; import { VoiceState } from "./VoiceState"; import { Webhook } from "./Webhook"; import { Categories } from "./Categories"; +import { InviteGuild } from "@spacebar/schemas/api/guilds/Invite"; // TODO: application_command_count, application_command_counts: {1: 0, 2: 0, 3: 0} // TODO: guild_scheduled_events // TODO: stage_instances @@ -512,4 +522,48 @@ export class Guild extends BaseClass { presence_count: undefined, }; } + + toInviteGuild(): InviteGuild { + return { + id: this.id, + name: this.name, + icon: this.icon ?? null, + description: this.description ?? null, + banner: this.banner ?? null, + splash: this.splash ?? null, + verification_level: this.verification_level ?? GuildVerificationLevel.NONE, + features: this.features, + vanity_url_code: null, //this.vanity_url_code, // TODO: store this in db? + premium_subscription_count: this.premium_subscription_count, + premium_tier: this.premium_tier ?? GuildPremiumTier.NONE, + nsfw: this.nsfw, + nsfw_level: this.nsfw_level ?? GuildNsfwLevel.DEFAULT, + } satisfies InviteGuild; + } + + toGuildProfile(): GuildProfileResponse { + return { + id: this.id, + name: this.name, + icon_hash: this.icon ?? null, + member_count: this.member_count ?? 0, + online_count: this.presence_count ?? 0, + description: this.description ?? "", + brand_color_primary: undefined, // TODO + game_application_ids: [], // TODO + game_activity: {}, // TODO + tag: null, // TODO + badge: null, // TODO + badge_color_primary: "", // TODO + badge_color_secondary: "", // TODO + badge_hash: "", // TODO + traits: [], // TODO + features: this.features, // TODO: should we filter this? + visibility: GuildVisibilityLevel.PUBLIC, // TODO + custom_banner_hash: this.discovery_splash ?? null, + premium_subscription_count: this.premium_subscription_count ?? 0, + premium_tier: this.premium_tier ?? GuildPremiumTier.NONE, + banner_hash: null, // Deprecated, TODO: clan banner hash + } satisfies GuildProfileResponse; + } } diff --git a/src/database/entities/Invite.ts b/src/database/entities/Invite.ts index 15522449b..4c338f4e2 100644 --- a/src/database/entities/Invite.ts +++ b/src/database/entities/Invite.ts @@ -22,6 +22,7 @@ import { Channel } from "./Channel"; import { Guild } from "./Guild"; import { Member } from "./Member"; import { User } from "./User"; +import { InviteType, PublicInvite } from "@spacebar/schemas/api/guilds/Invite"; export const PublicInviteRelation = ["inviter", "guild", "channel"]; @@ -104,10 +105,26 @@ export class Invite extends BaseClassWithoutId { if (this.max_uses !== 0 && this.uses >= this.max_uses) return true; return false; } - toPublicJSON() { + toPublicJSON(): PublicInvite { return { - ...this, - inviter: this.inviter.toPublicUser(), + code: this.code, + type: InviteType.GUILD, // TODO: support other invite types + channel: this.channel.toJSON(), + guild_id: this.guild_id, + guild: this.guild.toInviteGuild(), + profile: this.guild.toGuildProfile(), + inviter: this.inviter.toPartialUser(), + flags: this.flags, + expires_at: this.expires_at?.toString() ?? null, + approximate_member_count: this.guild.member_count, + approximate_presence_count: this.guild.presence_count, + is_nickname_changeable: true, // TODO + new_member: true, // TODO + roles: [], // TODO + show_verification_form: false, // TODO + target_type: undefined, // TODO + target_user: undefined, // TODO + target_users_job_status: undefined, // TODO }; } diff --git a/src/schemas/api/guilds/GuildProfileResponse.ts b/src/schemas/api/guilds/GuildProfileResponse.ts index a71a18a9f..c31887374 100644 --- a/src/schemas/api/guilds/GuildProfileResponse.ts +++ b/src/schemas/api/guilds/GuildProfileResponse.ts @@ -16,6 +16,8 @@ along with this program. If not, see . */ +import { GuildPremiumTier } from "@spacebar/schemas"; + export interface GuildProfileResponse { id: string; name: string; @@ -23,12 +25,12 @@ export interface GuildProfileResponse { member_count: number; online_count: number; description: string; - brand_color_primary: string; + brand_color_primary?: string; banner_hash: string | null; game_application_ids: string[]; game_activity: { [id: string]: GameActivity }; tag: string | null; - badge: GuildBadgeType; + badge: GuildBadgeType | null; badge_color_primary: string; badge_color_secondary: string; badge_hash: string; @@ -37,7 +39,7 @@ export interface GuildProfileResponse { visibility: GuildVisibilityLevel; custom_banner_hash: string | null; premium_subscription_count: number; - premium_tier: number; + premium_tier: GuildPremiumTier; } export interface GameActivity { diff --git a/src/schemas/api/guilds/Invite.ts b/src/schemas/api/guilds/Invite.ts index 35e458592..efa000f82 100644 --- a/src/schemas/api/guilds/Invite.ts +++ b/src/schemas/api/guilds/Invite.ts @@ -20,17 +20,18 @@ import { Snowflake } from "../../Identifiers"; import { PartialUser } from "../users"; import { GuildNsfwLevel, GuildPremiumTier, GuildVerificationLevel } from "./GuildSchema"; import { GuildProfileResponse, PublicChannel, RoleColors } from "@spacebar/schemas"; -import { Role } from "@spacebar/database"; -export interface Invite { +export type InviteListResponse = PublicInvite[]; + +export interface PublicInvite { code: string; - type: number; + type: InviteType; channel: PublicChannel | null; // TODO: Figure out what fields this is *supposed* to have as it's labeled as "Partial channel object" guild_id?: Snowflake; guild?: InviteGuild; profile?: GuildProfileResponse; inviter?: PartialUser; - flags?: number; + flags?: InviteFlags; target_type?: number; target_user?: PartialUser; // target_application?: PartialApplication; // TODO - voice activities @@ -45,6 +46,19 @@ export interface Invite { target_users_job_status?: InviteTargetUsersJob; // TODO } +export enum InviteType { + GUILD = 0, + GROUP_DM = 1, + FRIEND = 2, +} + +export enum InviteFlags { + IS_GUEST_INVITE = 1 << 0, // temporary voice channel access + IS_VIEWED = 1 << 1, // TODO + IS_ENHANCED = 1 << 2, // Unknown + IS_APPLICATION_BYPASS = 1 << 3, // Bypass guild join requests -> pending=false +} + export interface InviteTargetUsersJob { status: InviteTargetUsersJobStatus; total_users: number; diff --git a/src/schemas/responses/TypedResponses.ts b/src/schemas/responses/TypedResponses.ts index b37c4b741..243fb0706 100644 --- a/src/schemas/responses/TypedResponses.ts +++ b/src/schemas/responses/TypedResponses.ts @@ -37,7 +37,6 @@ export type ApplicationDetectableResponse = unknown[]; export type ApplicationEntitlementsResponse = unknown[]; export type ApplicationSkusResponse = unknown[]; export type APIApplicationArray = Application[]; -export type APIInviteArray = Invite[]; export type APIDiscoveryCategoryArray = Categories[]; export type APIChannelArray = Channel[];