diff --git a/assets/openapi.json b/assets/openapi.json index cced6947f..22e9257e3 100644 Binary files a/assets/openapi.json and b/assets/openapi.json differ diff --git a/assets/schemas.json b/assets/schemas.json index fe58930aa..72ac7694b 100644 Binary files a/assets/schemas.json and b/assets/schemas.json differ diff --git a/extra/admin-api/Spacebar.Offload/Program.cs b/extra/admin-api/Spacebar.Offload/Program.cs index 59cc3d0c2..87246c00a 100644 --- a/extra/admin-api/Spacebar.Offload/Program.cs +++ b/extra/admin-api/Spacebar.Offload/Program.cs @@ -1,14 +1,10 @@ -using System.Diagnostics.Metrics; -using System.Text.Json; using System.Text.Json.Serialization; -using ArcaneLibs.Extensions; using Microsoft.EntityFrameworkCore; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using Spacebar.Interop.Authentication; using Spacebar.Interop.Authentication.AspNetCore; using Spacebar.Models.Db.Contexts; -using Spacebar.Models.Generic; var builder = WebApplication.CreateBuilder(args); if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("APPSETTINGS_PATH"))) diff --git a/src/api/routes/channels/#channel_id/invites.ts b/src/api/routes/channels/#channel_id/invites.ts index 984a3c812..c16b9f955 100644 --- a/src/api/routes/channels/#channel_id/invites.ts +++ b/src/api/routes/channels/#channel_id/invites.ts @@ -23,6 +23,7 @@ import { Channel, Guild, Invite, PublicInviteRelation, User } from "@spacebar/da import { InviteCreateEvent, emitEvent } from "@spacebar/util"; import { InviteCreateSchema, isTextChannel } from "@spacebar/schemas"; import { Random } from "@spacebar/extensions"; +import { InviteListResponse } from "@spacebar/schemas/api/guilds/Invite"; const router: Router = Router({ mergeParams: true }); @@ -94,7 +95,7 @@ router.get( permission: "MANAGE_CHANNELS", responses: { 200: { - body: "APIInviteArray", + body: "InviteListResponse", }, 404: {}, }, @@ -110,12 +111,14 @@ router.get( } const { guild_id } = channel; - const invites = await Invite.find({ - where: { guild_id, channel_id }, - relations: Object.fromEntries(PublicInviteRelation.map((i) => [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[];