diff --git a/assets/openapi.json b/assets/openapi.json index 62b53f9a5..463269130 100644 Binary files a/assets/openapi.json and b/assets/openapi.json differ diff --git a/assets/schemas.json b/assets/schemas.json index 6891629d1..f7b875759 100644 Binary files a/assets/schemas.json and b/assets/schemas.json differ diff --git a/src/api/middlewares/CORS.ts b/src/api/middlewares/CORS.ts index 6cb2b1d8c..c4aa9cc08 100644 --- a/src/api/middlewares/CORS.ts +++ b/src/api/middlewares/CORS.ts @@ -18,8 +18,6 @@ import { NextFunction, Request, Response } from "express"; -// TODO: config settings - export function CORS(req: Request, res: Response, next: NextFunction) { res.set("Access-Control-Allow-Credentials", "true"); res.set("Access-Control-Allow-Headers", req.header("Access-Control-Request-Headers") || "*"); diff --git a/src/api/routes/applications/#application_id/commands/#command_id/index.ts b/src/api/routes/applications/#application_id/commands/#command_id/index.ts index 6fc4e28ed..3e4df62ff 100644 --- a/src/api/routes/applications/#application_id/commands/#command_id/index.ts +++ b/src/api/routes/applications/#application_id/commands/#command_id/index.ts @@ -19,7 +19,7 @@ import { Request, Response, Router } from "express"; import { route } from "@spacebar/api/middlewares"; import { Application, ApplicationCommand } from "@spacebar/database"; -import { FieldErrors, Snowflake } from "@spacebar/util"; +import { Config, FieldErrors, Snowflake } from "@spacebar/util"; import { ApplicationCommandCreateSchema, ApplicationCommandSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); @@ -68,12 +68,11 @@ router.patch( body.type = 1; } - if (body.name.trim().length < 1 || body.name.trim().length > 32) { - // TODO: configurable? + if (body.name.trim().length < 1 || body.name.trim().length > Config.get().limits.application.commandLimits.maxNameLength) { throw FieldErrors({ name: { code: "BASE_TYPE_BAD_LENGTH", - message: `Must be between 1 and 32 in length.`, + message: `Must be between 1 and ${Config.get().limits.application.commandLimits.maxNameLength} in length.`, }, }); } diff --git a/src/api/routes/applications/#application_id/commands/index.ts b/src/api/routes/applications/#application_id/commands/index.ts index 4891a180f..538ea7ea8 100644 --- a/src/api/routes/applications/#application_id/commands/index.ts +++ b/src/api/routes/applications/#application_id/commands/index.ts @@ -21,7 +21,7 @@ import { IsNull } from "typeorm"; import { ApplicationCommandCreateSchema, ApplicationCommandSchema } from "@spacebar/schemas"; import { route } from "@spacebar/api/middlewares"; import { Application, ApplicationCommand } from "@spacebar/database"; -import { FieldErrors, Snowflake } from "@spacebar/util"; +import { Config, FieldErrors, Snowflake } from "@spacebar/util"; const router = Router({ mergeParams: true }); @@ -56,12 +56,11 @@ router.post( body.type = 1; } - if (body.name.trim().length < 1 || body.name.trim().length > 32) { - // TODO: configurable? + if (body.name.trim().length < 1 || body.name.trim().length > Config.get().limits.application.commandLimits.maxNameLength) { throw FieldErrors({ name: { code: "BASE_TYPE_BAD_LENGTH", - message: `Must be between 1 and 32 in length.`, + message: `Must be between 1 and ${Config.get().limits.application.commandLimits.maxNameLength} in length.`, }, }); } @@ -127,12 +126,11 @@ router.put( command.type = 1; } - if (command.name.trim().length < 1 || command.name.trim().length > 32) { - // TODO: configurable? + if (command.name.trim().length < 1 || command.name.trim().length > Config.get().limits.application.commandLimits.maxNameLength) { throw FieldErrors({ name: { code: "BASE_TYPE_BAD_LENGTH", - message: `Must be between 1 and 32 in length.`, + message: `Must be between 1 and ${Config.get().limits.application.commandLimits.maxNameLength} in length.`, }, }); } diff --git a/src/api/routes/applications/#application_id/guilds/#guild_id/commands/#command_id/index.ts b/src/api/routes/applications/#application_id/guilds/#guild_id/commands/#command_id/index.ts index 6cbd4ae06..eea17006f 100644 --- a/src/api/routes/applications/#application_id/guilds/#guild_id/commands/#command_id/index.ts +++ b/src/api/routes/applications/#application_id/guilds/#guild_id/commands/#command_id/index.ts @@ -19,7 +19,7 @@ import { Request, Response, Router } from "express"; import { route } from "@spacebar/api/middlewares"; import { Application, ApplicationCommand, Guild, Member } from "@spacebar/database"; -import { FieldErrors, Snowflake } from "@spacebar/util"; +import { Config, FieldErrors, Snowflake } from "@spacebar/util"; import { ApplicationCommandCreateSchema, ApplicationCommandSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); @@ -87,12 +87,11 @@ router.patch( body.type = 1; } - if (body.name.trim().length < 1 || body.name.trim().length > 32) { - // TODO: configurable? + if (body.name.trim().length < 1 || body.name.trim().length > Config.get().limits.application.commandLimits.maxNameLength) { throw FieldErrors({ name: { code: "BASE_TYPE_BAD_LENGTH", - message: `Must be between 1 and 32 in length.`, + message: `Must be between 1 and ${Config.get().limits.application.commandLimits.maxNameLength} in length.`, }, }); } diff --git a/src/api/routes/applications/#application_id/guilds/#guild_id/commands/index.ts b/src/api/routes/applications/#application_id/guilds/#guild_id/commands/index.ts index 19444d3a8..520ca9995 100644 --- a/src/api/routes/applications/#application_id/guilds/#guild_id/commands/index.ts +++ b/src/api/routes/applications/#application_id/guilds/#guild_id/commands/index.ts @@ -19,7 +19,7 @@ import { Request, Response, Router } from "express"; import { route } from "@spacebar/api/middlewares"; import { Application, ApplicationCommand, Guild, Member } from "@spacebar/database"; -import { FieldErrors, Snowflake } from "@spacebar/util"; +import { Config, FieldErrors, Snowflake } from "@spacebar/util"; import { ApplicationCommandCreateSchema, ApplicationCommandSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); @@ -79,12 +79,11 @@ router.post( body.type = 1; } - if (body.name.trim().length < 1 || body.name.trim().length > 32) { - // TODO: configurable? + if (body.name.trim().length < 1 || body.name.trim().length > Config.get().limits.application.commandLimits.maxNameLength) { throw FieldErrors({ name: { code: "BASE_TYPE_BAD_LENGTH", - message: `Must be between 1 and 32 in length.`, + message: `Must be between 1 and ${Config.get().limits.application.commandLimits.maxNameLength} in length.`, }, }); } @@ -165,12 +164,11 @@ router.put( command.type = 1; } - if (command.name.trim().length < 1 || command.name.trim().length > 32) { - // TODO: configurable? + if (command.name.trim().length < 1 || command.name.trim().length > Config.get().limits.application.commandLimits.maxNameLength) { throw FieldErrors({ name: { code: "BASE_TYPE_BAD_LENGTH", - message: `Must be between 1 and 32 in length.`, + message: `Must be between 1 and ${Config.get().limits.application.commandLimits.maxNameLength} in length.`, }, }); } diff --git a/src/api/routes/beaker.ts b/src/api/routes/beaker.ts index 88cc72c09..393c89d64 100644 --- a/src/api/routes/beaker.ts +++ b/src/api/routes/beaker.ts @@ -31,7 +31,6 @@ router.post( spacebarOnly: false, // Not part of public openapi }), (req: Request, res: Response) => { - // TODO: res.sendStatus(204); }, ); diff --git a/src/api/routes/channels/#channel_id/invites.ts b/src/api/routes/channels/#channel_id/invites.ts index c16b9f955..d57b4f2be 100644 --- a/src/api/routes/channels/#channel_id/invites.ts +++ b/src/api/routes/channels/#channel_id/invites.ts @@ -21,9 +21,9 @@ import { HTTPError } from "lambert-server/HTTPError"; import { route } from "@spacebar/api/middlewares"; import { Channel, Guild, Invite, PublicInviteRelation, User } from "@spacebar/database"; import { InviteCreateEvent, emitEvent } from "@spacebar/util"; -import { InviteCreateSchema, isTextChannel } from "@spacebar/schemas"; +import { InviteCreateSchema, isTextChannel, PublicUserProjection } from "@spacebar/schemas"; import { Random } from "@spacebar/extensions"; -import { InviteListResponse } from "@spacebar/schemas/api/guilds/Invite"; +import { InviteListResponse, PublicInvite } from "@spacebar/schemas/api/guilds/Invite"; const router: Router = Router({ mergeParams: true }); @@ -35,7 +35,7 @@ router.post( right: "CREATE_INVITES", responses: { 201: { - body: "Invite", + body: "PublicInvite", }, 404: {}, 400: { @@ -71,21 +71,20 @@ router.post( guild_id, channel_id: channel_id, inviter_id: user_id, + inviter: await User.findOneOrFail({ + where: { id: req.user_id }, + select: Object.fromEntries(PublicUserProjection.map((i) => [i, true])), // TODO: clean up + }), flags: body.flags ?? 0, }).save(); - const data = invite.toJSON(); - data.inviter = await User.getPublicUser(req.user_id); - data.guild = await Guild.findOne({ where: { id: guild_id } }); - data.channel = channel; - await emitEvent({ event: "INVITE_CREATE", - data, + data: invite.toGatewayInviteCreateData(), guild_id, } satisfies InviteCreateEvent); - res.status(201).send(data); + res.status(201).send(invite.toPublicJSON()); }, ); diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts index 1816aa6db..d85b05119 100644 --- a/src/api/routes/channels/#channel_id/messages/index.ts +++ b/src/api/routes/channels/#channel_id/messages/index.ts @@ -18,7 +18,7 @@ import { handleMessage, postHandleMessage } from "@spacebar/api/util"; import { route } from "@spacebar/api/middlewares"; -import { Attachment, Channel, Member, Message, ReadState, Relationship, User, ThreadMember, ThreadMemberFlags } from "@spacebar/database"; +import { Attachment, Channel, Member, Message, ReadState, Relationship, User, ThreadMember } from "@spacebar/database"; import { Config, DiscordApiErrors, @@ -52,6 +52,7 @@ import { Reaction, ReadStateType, RelationshipType, + ThreadMemberFlags, } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); diff --git a/src/api/routes/channels/#channel_id/post-data.ts b/src/api/routes/channels/#channel_id/post-data.ts index 202cce99b..72d582546 100644 --- a/src/api/routes/channels/#channel_id/post-data.ts +++ b/src/api/routes/channels/#channel_id/post-data.ts @@ -21,7 +21,7 @@ import { messageUpload } from "./messages"; import { In } from "typeorm"; import { route } from "@spacebar/api/middlewares"; import { Channel, Member, Message } from "@spacebar/database"; -import { PostDataSchema, PublicMessage } from "@spacebar/schemas"; +import { PostDataResponse, PostDataSchema, PublicMessage } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); @@ -43,7 +43,9 @@ router.post( requestBody: "PostDataSchema", permission: "VIEW_CHANNEL", responses: { - 200: {}, + 200: { + body: "PostDataResponse", + }, 403: {}, }, }), @@ -54,6 +56,7 @@ router.post( id: In(body), }, }); + const [messages, members] = await Promise.all([ Message.find({ where: { @@ -81,17 +84,18 @@ router.post( }, }), ]); + await Message.fillReplies(messages); - const objRet: { threads: Record } = { threads: {} }; + const threadPostData: PostDataResponse = { threads: {} }; for (const thread of threads) { - const owner = members.find(({ id }) => id === thread.owner_id)?.toJSON() || null; - const first_message = messages.find(({ channel_id }) => channel_id === thread.id)?.toJSON() || null; - objRet.threads[thread.id] = { + const owner = members.find(({ id }) => id === thread.owner_id)?.toPublicMember() ?? null; + const first_message = messages.find(({ channel_id }) => channel_id === thread.id)?.toJSON() ?? null; + threadPostData.threads[thread.id] = { owner, first_message, }; } - return res.json(objRet); + return res.json(threadPostData); }, ); diff --git a/src/api/routes/channels/#channel_id/thread-members.ts b/src/api/routes/channels/#channel_id/thread-members.ts index f1d2a6cf1..f6ed628ec 100644 --- a/src/api/routes/channels/#channel_id/thread-members.ts +++ b/src/api/routes/channels/#channel_id/thread-members.ts @@ -19,9 +19,9 @@ import { Request, Response, Router } from "express"; import { MoreThan } from "typeorm"; import { route } from "@spacebar/api/middlewares"; -import { Channel, Member, ThreadMember, ThreadMemberFlags } from "@spacebar/database"; +import { Channel, Member, ThreadMember } from "@spacebar/database"; import { emitEvent, Permissions, ThreadCreateEvent, ThreadDeleteEvent, ThreadMembersUpdateEvent } from "@spacebar/util"; -import { ChannelType, Snowflake } from "@spacebar/schemas"; +import { ChannelType, Snowflake, ThreadMemberFlags } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); @@ -29,7 +29,9 @@ router.get( "/", route({ responses: { - 200: {}, + 200: { + body: "PublicThreadMembersResponse", + }, 403: {}, }, permission: "VIEW_CHANNEL", @@ -51,12 +53,14 @@ router.get( } return res.send( - await ThreadMember.find({ - where: { channel: { id: channel_id }, ...(after ? { user_id: MoreThan(after) } : {}) }, - take: limit ? parseInt(limit) : 50, - order: { member_idx: "ASC" }, - relations: { ...(with_member ? { member: true } : {}) }, - }), + ( + await ThreadMember.find({ + where: { channel: { id: channel_id }, ...(after ? { user_id: MoreThan(after) } : {}) }, + take: limit ? parseInt(limit) : 50, + order: { member_idx: "ASC" }, + relations: { ...(with_member ? { member: true } : {}) }, + }) + ).map((m) => m.toJSON()), ); }, ); diff --git a/src/api/routes/guilds/#guild_id/application-command-index.ts b/src/api/routes/guilds/#guild_id/application-command-index.ts index 758392fe8..10f404e47 100644 --- a/src/api/routes/guilds/#guild_id/application-command-index.ts +++ b/src/api/routes/guilds/#guild_id/application-command-index.ts @@ -64,10 +64,11 @@ router.get("/", route({}), async (req: Request, res: Response) => { guild_id: command.guild_id, name: command.name, name_localizations: command.name_localizations, - // name_localized: // TODO: make this work + name_localized: (command.name_localizations && req.user.settings?.locale ? command.name_localizations[req.user.settings.locale] : null) ?? command.name, description: command.description, description_localizations: command.description_localizations, - // description_localized: // TODO: make this work + description_localized: + (command.description_localizations && req.user.settings?.locale ? command.description_localizations[req.user.settings.locale] : null) ?? command.description, options: command.type === ApplicationCommandType.CHAT_INPUT ? command.options : undefined, default_member_permissions: command.default_member_permissions, dm_permission: command.dm_permission, diff --git a/src/api/routes/guilds/#guild_id/bans.ts b/src/api/routes/guilds/#guild_id/bans.ts index 821746654..1a4890b8a 100644 --- a/src/api/routes/guilds/#guild_id/bans.ts +++ b/src/api/routes/guilds/#guild_id/bans.ts @@ -26,6 +26,7 @@ import { BanCreateSchema, BanRegistrySchema, GuildBanResponse, GuildBansResponse const router: Router = Router({ mergeParams: true }); /* TODO: Deleting the secrets is just a temporary go-around. Views should be implemented for both safety and better handling. */ +// Emma [it/its] @ Rory& - 2026-11-01 - what secrets? router.get( "/", diff --git a/src/api/routes/guilds/#guild_id/index.ts b/src/api/routes/guilds/#guild_id/index.ts index 9fa61a4d3..c4eb05515 100644 --- a/src/api/routes/guilds/#guild_id/index.ts +++ b/src/api/routes/guilds/#guild_id/index.ts @@ -109,7 +109,6 @@ router.patch( if (body.features) { const diff = guild.features.filter((x) => !body.features?.includes(x)).concat(body.features.filter((x) => !guild.features.includes(x))); - // TODO move these const MUTABLE_FEATURES = ["COMMUNITY", "INVITES_DISABLED", "DISCOVERABLE"]; for (const feature of diff) { diff --git a/src/api/routes/guilds/#guild_id/regions.ts b/src/api/routes/guilds/#guild_id/regions.ts index 025ea7c44..8709c9c00 100644 --- a/src/api/routes/guilds/#guild_id/regions.ts +++ b/src/api/routes/guilds/#guild_id/regions.ts @@ -38,7 +38,6 @@ router.get( async (req: Request, res: Response) => { const { guild_id } = req.params as { [key: string]: string }; const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); - //TODO we should use an enum for guild's features and not hardcoded strings return res.json(await getVoiceRegions(req.ip!, guild.features.includes("VIP_REGIONS"))); }, ); diff --git a/src/api/routes/guilds/index.ts b/src/api/routes/guilds/index.ts index 231ba9041..6e6bf2099 100644 --- a/src/api/routes/guilds/index.ts +++ b/src/api/routes/guilds/index.ts @@ -24,8 +24,6 @@ import { GuildCreateSchema } from "@spacebar/schemas"; const router: Router = Router({ mergeParams: true }); -//TODO: create default channel - router.post( "/", route({ diff --git a/src/api/routes/read-states/ack-bulk.ts b/src/api/routes/read-states/ack-bulk.ts index 5a1cef184..07fcec962 100644 --- a/src/api/routes/read-states/ack-bulk.ts +++ b/src/api/routes/read-states/ack-bulk.ts @@ -37,8 +37,6 @@ router.post( async (req: Request, res: Response) => { const body = req.body as AckBulkSchema; - // TODO: what is read_state_type ? - await Promise.all([ // for every new state ...body.read_states.map(async (x) => { diff --git a/src/api/routes/science.ts b/src/api/routes/science.ts index 34f342883..012a78989 100644 --- a/src/api/routes/science.ts +++ b/src/api/routes/science.ts @@ -30,7 +30,6 @@ router.post( authentication: "never", }), (req: Request, res: Response) => { - // TODO: res.sendStatus(204); }, ); diff --git a/src/api/routes/teams.ts b/src/api/routes/teams.ts index 16fd5d3a5..1370c1eea 100644 --- a/src/api/routes/teams.ts +++ b/src/api/routes/teams.ts @@ -84,7 +84,7 @@ router.post( role: TeamMemberRole.ADMIN, }).save(); - res.json(team.toJSON() satisfies Team); + res.json(team.toJSON()); }, ); diff --git a/src/api/routes/users/@me/billing/payment-sources.ts b/src/api/routes/users/@me/billing/payment-sources.ts index 3c6008cf7..ec68efd12 100644 --- a/src/api/routes/users/@me/billing/payment-sources.ts +++ b/src/api/routes/users/@me/billing/payment-sources.ts @@ -47,34 +47,63 @@ const example = { default: false, }; -router.get("/", route({}), (req: Request, res: Response) => { - // TODO: schema - res.json([example]).status(200); -}); +router.get( + "/", + route({ + responses: { + 200: { body: "PaymentSourceListResponse" }, + }, + }), + (req: Request, res: Response) => { + res.json([example]).status(200); + }, +); -router.post("/", route({}), (req: Request, res: Response) => { - // TODO: schema - res.json([example]).status(200); -}); +router.post( + "/", + route({ + responses: { + 200: { body: "PaymentSourceListResponse" }, + }, + }), + // TODO: request schema + (req: Request, res: Response) => { + res.json([example]).status(200); + }, +); -router.get("/:payment_source_id", route({}), (req: Request, res: Response) => { - // TODO: schema - res.json({ - ...example, - id: req.route.payment_source_id, - }).status(200); -}); +router.get( + "/:payment_source_id", + route({ + responses: { + 200: { body: "PaymentSource" }, + }, + }), + (req: Request, res: Response) => { + res.json({ + ...example, + id: req.route.payment_source_id, + }).status(200); + }, +); -router.patch("/:payment_source_id", route({}), (req: Request, res: Response) => { - // TODO: schema - res.json({ - ...example, - id: req.route.payment_source_id, - }).status(200); -}); +router.patch( + "/:payment_source_id", + route({ + responses: { + 200: { body: "PaymentSourceListResponse" }, + }, + }), + (req: Request, res: Response) => { + // TODO: request schema + res.json({ + ...example, + id: req.route.payment_source_id, + }).status(200); + }, +); router.delete("/:payment_source_id", route({}), (req: Request, res: Response) => { - // TODO: schema res.status(204); }); diff --git a/src/api/routes/users/@me/delete.ts b/src/api/routes/users/@me/delete.ts index e0887900a..ec29a5fa6 100644 --- a/src/api/routes/users/@me/delete.ts +++ b/src/api/routes/users/@me/delete.ts @@ -52,8 +52,6 @@ router.post( } } - // TODO: decrement guild member count - if (correctpass) { // Check if the user owns any guilds. const ownedGuilds = await Guild.findOne({ where: { owner_id: req.user_id } }); diff --git a/src/api/routes/users/@me/guilds/#guild_id/settings.ts b/src/api/routes/users/@me/guilds/#guild_id/settings.ts index eb3694b59..fb2f794ed 100644 --- a/src/api/routes/users/@me/guilds/#guild_id/settings.ts +++ b/src/api/routes/users/@me/guilds/#guild_id/settings.ts @@ -21,6 +21,8 @@ import { route } from "@spacebar/api/middlewares"; import { Channel, Member } from "@spacebar/database"; import { OrmUtils } from "@spacebar/util"; import { UserGuildSettingsSchema } from "@spacebar/schemas"; +import { In } from "typeorm"; +import { HTTPError } from "lambert-server"; const router = Router({ mergeParams: true }); @@ -60,10 +62,8 @@ router.patch( const body = req.body as UserGuildSettingsSchema; if (body.channel_overrides) { - // TODO: rewrite to a single query? - for (const channel in body.channel_overrides) { - await Channel.findOneOrFail({ where: { id: channel } }); - } + const channels = await Channel.find({ where: { id: In(Object.keys(body.channel_overrides)), guild_id: req.params.guild_id as string } }); + if (channels.length != Object.keys(body.channel_overrides).length) throw new HTTPError("Could not find one or more channels.", 404); } const user = await Member.findOneOrFail({ diff --git a/src/api/routes/users/@me/index.ts b/src/api/routes/users/@me/index.ts index 341dbf1f3..59565480e 100644 --- a/src/api/routes/users/@me/index.ts +++ b/src/api/routes/users/@me/index.ts @@ -153,7 +153,6 @@ router.patch( } if (body.discriminator) { - // TODO: HACK - maybe make this optional? if (!/^\d{4}$/.test(body.discriminator)) { throw FieldErrors({ discriminator: { diff --git a/src/api/routes/users/@me/mfa/totp/enable.ts b/src/api/routes/users/@me/mfa/totp/enable.ts index a55a9ff65..660778c8e 100644 --- a/src/api/routes/users/@me/mfa/totp/enable.ts +++ b/src/api/routes/users/@me/mfa/totp/enable.ts @@ -51,12 +51,11 @@ router.post( select: { id: true, data: true, email: true }, }); - // TODO: Are guests allowed to enable 2fa? if (user.data.hash) { if (!(await bcrypt.compare(body.password, user.data.hash))) { throw new HTTPError(req.t("auth:login.INVALID_PASSWORD")); } - } + } else throw new HTTPError("Guests cannot enable 2FA, please set a password first!", 403); if (!body.secret) throw new HTTPError(req.t("auth:login.INVALID_TOTP_SECRET"), 60005); diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts index 2940b3064..3be73067e 100644 --- a/src/api/util/handlers/Message.ts +++ b/src/api/util/handlers/Message.ts @@ -558,7 +558,6 @@ export async function handleMessage(opts: MessageOptions): Promise { return message; } -// TODO: cache link result in db export async function postHandleMessage(message: Message) { message.clean_data(); @@ -773,25 +772,21 @@ async function handleMessageMentionsAsync(message: Message) { mention_roles: true, }, }); + if (referencedMessage && referencedMessage.author_id !== message.author_id) { - message.mentions.push( - // @ts-expect-error it does not like the .toPublicUser() lol - (await User.findOne({ where: { id: referencedMessage.author_id } }))!.toPublicUser(), - ); + const user = await User.findOne({ where: { id: referencedMessage.author_id } }); + if (user) message.mentions.push(user); } if (message.embeds[0]?.type === EmbedType.poll_result) { - message.mentions.push( - // @ts-expect-error it does not like the .toPublicUser() lol - (await User.findOne({ where: { id: message.author_id } }))!.toPublicUser(), - ); + const user = await User.findOne({ where: { id: message.author_id } }); + if (user) message.mentions.push(user); } if (message.message_reference.type === MessageReferenceType.FORWARD) { message.type = MessageType.DEFAULT; if (message.referenced_message) { - // TODO: mention_roles and mentions arrays - not needed it seems, but discord still returns that message.message_snapshots = [message.referenced_message.toSnapshot()]; } } diff --git a/src/database/entities/BaseClass.ts b/src/database/entities/BaseClass.ts index 730a769f4..6d4bfdaf8 100644 --- a/src/database/entities/BaseClass.ts +++ b/src/database/entities/BaseClass.ts @@ -76,9 +76,7 @@ export class BaseClassWithoutId extends BaseEntity { return this; } - // TODO: fix eslint - // eslint-disable-next-line @typescript-eslint/no-explicit-any - toJSON(): any { + toJSON(): unknown { this.clean_data(); return Object.fromEntries( // eslint-disable-next-line @typescript-eslint/ban-ts-comment diff --git a/src/database/entities/Guild.ts b/src/database/entities/Guild.ts index d325a1caf..3526c1cfd 100644 --- a/src/database/entities/Guild.ts +++ b/src/database/entities/Guild.ts @@ -21,6 +21,8 @@ import { arrayRemove } from "@spacebar/extensions"; import { Config, handleFile, Snowflake } from "@spacebar/util"; import { DiscoverableGuild, + DiscoveryCategory, + EmojiResponse, GuildNsfwLevel, GuildPremiumTier, GuildProfileResponse, @@ -28,6 +30,7 @@ import { GuildVisibilityLevel, GuildWelcomeScreen, IntegrationGuild, + StickerResponse, } from "@spacebar/schemas"; import { Ban } from "./Ban"; import { BaseClass } from "./BaseClass"; @@ -342,9 +345,9 @@ export class Guild extends BaseClass { }, }, }),*/ - emojis: this.emojis?.map((e) => e.toJSON()) ?? undefined, + emojis: (this.emojis?.map((e) => e.toJSON()) as EmojiResponse[]) ?? undefined, emoji_count: this.emojis ? this.emojis.length : undefined, - stickers: this.stickers?.map((s) => s.toJSON()) ?? undefined, + stickers: (this.stickers?.map((s) => s.toJSON()) as StickerResponse[]) ?? undefined, sticker_count: this.stickers ? this.stickers.length : undefined, auto_removed: false, primary_category_id: this.primary_category_id!, @@ -353,13 +356,13 @@ export class Guild extends BaseClass { reasons_to_join: [], created_at: new Date(Snowflake.deconstruct(this.id).timestamp).toISOString(), // TODO: make column primary_category: this.primary_category_id - ? ( + ? (( await Categories.findOneOrFail({ where: { id: this.primary_category_id!, }, }) - ).toJSON() + ).toJSON() as DiscoveryCategory) : undefined, }; } diff --git a/src/database/entities/Invite.ts b/src/database/entities/Invite.ts index 4c338f4e2..6de0d8ff8 100644 --- a/src/database/entities/Invite.ts +++ b/src/database/entities/Invite.ts @@ -22,7 +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"; +import { InviteCreateEventData, InviteType, PublicInvite } from "@spacebar/schemas/api/guilds/Invite"; export const PublicInviteRelation = ["inviter", "guild", "channel"]; @@ -105,6 +105,7 @@ export class Invite extends BaseClassWithoutId { if (this.max_uses !== 0 && this.uses >= this.max_uses) return true; return false; } + toPublicJSON(): PublicInvite { return { code: this.code, @@ -128,6 +129,26 @@ export class Invite extends BaseClassWithoutId { }; } + toGatewayInviteCreateData(): InviteCreateEventData { + return { + code: this.code, + type: InviteType.GUILD, // TODO: support other invite types + channel_id: this.channel_id ?? this.channel.id, + guild_id: this.guild_id ?? this.channel.guild_id, + inviter: this.inviter.toPartialUser(), + expires_at: this.expires_at?.toString() ?? null, + target_type: undefined, // TODO + target_user: undefined, // TODO + created_at: this.created_at.toISOString(), + max_age: this.max_age, + max_uses: this.max_uses, + temporary: this.temporary, + uses: this.uses, + role_ids: [], // TODO + target_application: undefined, // TODO + } satisfies InviteCreateEventData; + } + static async joinGuild(user_id: string, code: string) { const invite = await Invite.findOneOrFail({ where: { code } }); if (invite.isExpired()) { diff --git a/src/database/entities/Message.ts b/src/database/entities/Message.ts index 629e68741..34920e713 100644 --- a/src/database/entities/Message.ts +++ b/src/database/entities/Message.ts @@ -390,7 +390,7 @@ export class Message extends BaseClass { embeds: this.embeds, flags: this.flags, mention_roles: this.mention_roles?.map((x) => x.id), - mentions: this.mentions.map((x) => x.toPublicUser() as unknown as PartialUser), // TODO: write a proper method for this + mentions: this.mentions.map((x) => x.toPartialUser()), timestamp: this.timestamp, type: this.type, }, diff --git a/src/database/entities/Tag.ts b/src/database/entities/Tag.ts index 611122a98..08113e969 100644 --- a/src/database/entities/Tag.ts +++ b/src/database/entities/Tag.ts @@ -17,6 +17,7 @@ */ import { Column, Entity, JoinColumn, ManyToOne } from "typeorm"; +import { Tag as Tag_ } from "@spacebar/schemas"; import { BaseClass } from "./BaseClass"; import { Channel } from "./Channel"; @@ -45,13 +46,13 @@ export class Tag extends BaseClass { @Column({ nullable: true }) emoji_name?: string; - toJSON() { + toJSON(): Tag_ { return { name: this.name, id: this.id, moderated: this.moderated, - emoji_id: this.emoji_id, - emoji_name: this.emoji_name, + emoji_id: this.emoji_id ?? null, + emoji_name: this.emoji_name ?? null, }; } } diff --git a/src/database/entities/ThreadMember.ts b/src/database/entities/ThreadMember.ts index de7e881c7..ded086af7 100644 --- a/src/database/entities/ThreadMember.ts +++ b/src/database/entities/ThreadMember.ts @@ -18,27 +18,13 @@ import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn, RelationId } from "typeorm"; import { ThreadMembersUpdateEvent } from "../../util/interfaces"; +import { HTTPError } from "lambert-server/HTTPError"; import { emitEvent } from "@spacebar/util/util"; +import { MuteConfig, PublicThreadMember, ThreadMemberFlags } from "@spacebar/schemas"; import { BaseClassWithoutId } from "./BaseClass"; import { Channel } from "./Channel"; -import { HTTPError } from "lambert-server/HTTPError"; import { Member } from "./Member"; -// TODO: move -interface ThreadMemberMuteConfig { - end_time?: Date; - selected_time_window?: number; -} - -// TODO: move -export enum ThreadMemberFlags { - NONE = 0, - HAS_INTERACTED = 1 << 0, - ALL_MESSAGES = 1 << 1, - ONLY_MENTIONS = 1 << 2, - NO_MESSAGES = 1 << 3, -} - @Entity("thread_members") @Index(["id", "member_idx"], { unique: true }) export class ThreadMember extends BaseClassWithoutId { @@ -72,7 +58,7 @@ export class ThreadMember extends BaseClassWithoutId { muted: boolean; @Column({ nullable: true, type: "jsonb" }) - mute_config?: ThreadMemberMuteConfig; + mute_config?: MuteConfig; @Column() flags: ThreadMemberFlags; @@ -93,20 +79,18 @@ export class ThreadMember extends BaseClassWithoutId { })) ) throw new HTTPError("You are not member of this thread", 403); - // // use promise all to execute all promises at the same time -> save time - // TODO: check for bugs + if (channel.member_count) channel.member_count--; return Promise.all([ ThreadMember.delete({ id: thread_id, member_idx: member_id, }), - // //Guild.decrement({ id: guild_id }, "member_count", -1), emitEvent({ event: "THREAD_MEMBERS_UPDATE", data: { - guild_id: channel.guild_id!, // TODO: is this the right fix? + guild_id: channel.guild_id!, id: channel.id, member_count: channel.member_count ?? 0, removed_member_ids: [member_id], @@ -116,6 +100,18 @@ export class ThreadMember extends BaseClassWithoutId { ]); } + toJSON(): PublicThreadMember { + return { + id: this.id, + flags: this.flags, + join_timestamp: this.join_timestamp.toISOString(), + member: this.member.toPublicMember(), + mute_config: this.mute_config, + muted: this.muted, + user_id: this.member.id, + } satisfies PublicThreadMember; + } + // static async addRole(user_id: string, guild_id: string, role_id: string) { // const [member, role] = await Promise.all([ // // @ts-ignore diff --git a/src/schemas/api/channels/Channel.ts b/src/schemas/api/channels/Channel.ts index 33514b82c..90bcdad60 100644 --- a/src/schemas/api/channels/Channel.ts +++ b/src/schemas/api/channels/Channel.ts @@ -17,10 +17,10 @@ */ // TODO: remove entity imports -import { Channel, Recipient, Tag, ThreadMember } from "@spacebar/database"; +import { Channel, Recipient } from "@spacebar/database"; import { HTTPError } from "lambert-server/HTTPError"; import { Snowflake } from "../../Identifiers"; -import { PartialUser, PublicMember } from "../users"; +import { MuteConfig, PartialUser, PublicMember } from "../users"; export enum ChannelType { GUILD_TEXT = 0, // a text channel within a guild @@ -127,7 +127,7 @@ export interface PublicChannel { member_count?: number; member_ids_preview?: Snowflake[]; thread_metadata?: ThreadMetadata; - member?: ThreadMember; + member?: PublicThreadMember; default_auto_archive_duration?: number | null; default_thread_rate_limit_per_user?: number; permissions?: string; @@ -150,3 +150,30 @@ export interface PublicChannel { is_linkable?: boolean; is_viewable_and_writeable_by_all_members?: boolean; } + +export type PublicThreadMembersResponse = PublicThreadMember[]; +export interface PublicThreadMember { + id?: Snowflake; + user_id?: Snowflake; + join_timestamp: string; + flags: ThreadMemberFlags; + muted?: boolean; + mute_config?: MuteConfig | null; + member?: PublicMember; +} + +export enum ThreadMemberFlags { + NONE = 0, + HAS_INTERACTED = 1 << 0, + ALL_MESSAGES = 1 << 1, + ONLY_MENTIONS = 1 << 2, + NO_MESSAGES = 1 << 3, +} + +export interface Tag { + id: Snowflake; + name: string; + moderated: boolean; + emoji_id: Snowflake | null; + emoji_name: string | null; +} diff --git a/src/schemas/api/developers/Application.ts b/src/schemas/api/developers/Application.ts index 10e49a6bd..b2ccd1e46 100644 --- a/src/schemas/api/developers/Application.ts +++ b/src/schemas/api/developers/Application.ts @@ -1,6 +1,6 @@ /* Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 Spacebar and Spacebar Contributors + Copyright (C) 2026 Spacebar and Spacebar Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published @@ -16,6 +16,8 @@ along with this program. If not, see . */ +import { ApplicationSku, PartialUser, Snowflake, Team } from "@spacebar/schemas"; + export interface ApplicationCommand { id: string; application_id: string; @@ -60,3 +62,285 @@ export interface ApplicationCommandInteractionDataOption { value?: unknown; options?: ApplicationCommandInteractionDataOption[]; } + +export interface Application { + id: Snowflake; + name: string; + description: string; + icon: string | null; + cover_image?: string; + splash?: string; + type: ApplicationType | null; + flags: number; // ApplicationFlags + flags_new: string; // ApplicationFlags + primary_sku_id?: Snowflake; + verify_key: string; + guild_id?: Snowflake; + eula_id?: Snowflake; + slug?: string; + aliases?: string[]; + executables?: ApplicationExecutable[]; + third_party_skus?: ApplicationSku[]; + hook: boolean; + overlay?: boolean; + overlay_methods?: number; // OverlayMethodFlags + overlay_warn?: boolean; + overlay_compatibility_hook?: boolean; + bot?: PartialUser; + owner: PartialUser; + team?: Team | null; + developers?: Company[]; + publishers?: Company[]; + rpc_origins?: string[]; + redirect_uris: string[]; + deeplink_uri?: string; + integration_public: boolean; + integration_require_code_grant: boolean; + bot_public?: boolean; + bot_require_code_grant?: boolean; + bot_disabled?: boolean; + bot_quarantined?: boolean; + bot_approximate_guild_count?: number; + approximate_guild_count?: number; + approximate_user_install_count: number; + approximate_user_authorization_count: number; + internal_guild_restriction: InternalGuildRestriction; + terms_of_service_url?: string; + privacy_policy_url?: string; + role_connections_verification_url: string | null; + interactions_endpoint_url: string; + interactions_version: number; + interactions_event_types: string[]; + event_webhooks_status?: EventWebhooksStatus; + event_webhooks_url?: string; + event_webhooks_types?: string[]; + explicit_content_filter: number; + tags?: string[]; + install_params?: ApplicationInstallParams; + custom_install_url?: string; + integration_types_config?: Record; + connection_entrypoint_url?: string; + is_verified: boolean; + verification_state: ApplicationVerificationState; + store_application_state: StoreApplicationState; + rpc_application_state: RpcApplicationState; + creator_monetization_state: number; // unknown enum + is_discoverable: boolean; + discoverability_state: ApplicationDiscoverabilityState; + discovery_eligibility_flags: number; // ApplicationDiscoveryEligibilityFlags + is_monetized: boolean; + storefront_available: boolean; + monetization_state: ApplicationMonetizationState; + monetization_eligibility_flags?: number; // ApplicationMonetizationEligibilityFlags + max_participants?: number; + embedded_activity_config?: EmbeddedActivityConfig; + approved_consoles: ApprovableConsoleType[]; + pricing_localization_strategy: string; +} + +export enum ApplicationType { + DEPRECATED_GAME = 1, + TICKETED_EVENTS = 3, + CREATOR_MONETIZATION = 4, + GAME = 5, +} +const OverlayMethodFlags = { + OUT_OF_PROCESS: 1n << 0n, +} as const; + +export enum InternalGuildRestriction { + JOIN_ALL = 1, + JOIN_EXTERNAL_ONLY = 2, + JOIN_INTERNAL_ONLY = 3, +} +export enum EventWebhooksStatus { + DISABLED = 1, + ENABLED = 2, +} + +export enum ApplicationVerificationState { + INELIGIBLE = 1, + UNSUBMITTED = 2, + SUBMITTED = 3, + APPROVED_MANUALLY = 4, + BLOCKED = 5, + APPROVED_AUTOMATICALLY = 6, +} + +export enum StoreApplicationState { + NONE = 1, + PAID = 2, + SUBMITTED = 3, + APPROVED = 4, + REJECTED = 5, +} + +export enum RpcApplicationState { + DISABLED = 0, + UNSUBMITTED = 1, + SUBMITTED = 2, + APPROVED = 3, + REJECTED = 4, +} + +export enum ApplicationDiscoverabilityState { + INELIGIBLE = 1, + NOT_DISCOVERABLE = 2, + DISCOVERABLE = 3, + FEATUREABLE = 4, + BLOCKED = 5, +} + +const ApplicationDiscoveryEligibilityFlags = { + VERIFIED: 1n << 0n, + TAG: 1n << 1n, + DESCRIPTION: 1n << 2n, + TERMS_OF_SERVICE: 1n << 3n, + PRIVACY_POLICY: 1n << 4n, + INSTALL_PARAMS: 1n << 5n, + SAFE_NAME: 1n << 6n, + SAFE_DESCRIPTION: 1n << 7n, + APPROVED_COMMANDS: 1n << 8n, + SUPPORT_GUILD: 1n << 9n, + SAFE_COMMANDS: 1n << 10n, + MFA: 1n << 11n, + SAFE_DIRECTORY_OVERVIEW: 1n << 12n, + SUPPORTED_LOCALES: 1n << 13n, + SAFE_SHORT_DESCRIPTION: 1n << 14n, + SAFE_ROLE_CONNECTIONS: 1n << 15n, +} as const; + +export enum ApplicationMonetizationState { + NONE = 1, + ENABLED = 2, + BLOCKED = 3, +} + +const ApplicationMonetizationEligibilityFlags = { + VERIFIED: 1n << 0n, + HAS_TEAM: 1n << 1n, + APPROVED_COMMANDS: 1n << 2n, + TERMS_OF_SERVICE: 1n << 3n, + PRIVACY_POLICY: 1n << 4n, + SAFE_NAME: 1n << 5n, + SAFE_DESCRIPTION: 1n << 6n, + SAFE_ROLE_CONNECTIONS: 1n << 7n, + USER_IS_TEAM_OWNER: 1n << 8n, + NOT_QUARANTINED: 1n << 9n, + USER_LOCALE_SUPPORTED: 1n << 10n, + USER_AGE_SUPPORTED: 1n << 11n, + USER_DATE_OF_BIRTH_DEFINED: 1n << 12n, + USER_MFA_ENABLED: 1n << 13n, + USER_EMAIL_VERIFIED: 1n << 14n, + TEAM_MEMBERS_EMAIL_VERIFIED: 1n << 15n, + TEAM_MEMBERS_MFA_ENABLED: 1n << 16n, + NO_BLOCKING_ISSUES: 1n << 17n, + VALID_PAYOUT_STATUS: 1n << 18n, +} as const; + +export enum ApprovableConsoleType { + XBOX = 1, + PLAYSTATION_5 = 2, + PLAYSTATION_4 = 3, +} + +const ApplicationFlags = { + EMBEDDED_RELEASED: 1n << 1n, + MANAGED_EMOJI: 1n << 2n, + EMBEDDED_IAP: 1n << 3n, + GROUP_DM_CREATE: 1n << 4n, + RPC_PRIVATE_BETA: 1n << 5n, + AUTO_MODERATION_RULE_CREATE_BADGE: 1n << 6n, + GAME_PROFILE_DISABLED: 1n << 7n, + PUBLIC_OAUTH2_CLIENT: 1n << 8n, + CONTEXTLESS_ACTIVITY: 1n << 9n, + SOCIAL_LAYER_INTEGRATION_LIMITED: 1n << 10n, + CLOUD_GAMING_DEMO: 1n << 11n, + GATEWAY_PRESENCE: 1n << 12n, + GATEWAY_PRESENCE_LIMITED: 1n << 13n, + GATEWAY_GUILD_MEMBERS: 1n << 14n, + GATEWAY_GUILD_MEMBERS_LIMITED: 1n << 15n, + VERIFICATION_PENDING_GUILD_LIMIT: 1n << 16n, + EMBEDDED: 1n << 17n, + GATEWAY_MESSAGE_CONTENT: 1n << 18n, + GATEWAY_MESSAGE_CONTENT_LIMITED: 1n << 19n, + EMBEDDED_FIRST_PARTY: 1n << 20n, + APPLICATION_COMMAND_MIGRATED: 1n << 21n, + APPLICATION_COMMAND_BADGE: 1n << 23n, + ACTIVE: 1n << 24n, + ACTIVE_GRACE_PERIOD: 1n << 25n, + IFRAME_MODAL: 1n << 26n, + SOCIAL_LAYER_INTEGRATION: 1n << 27n, + PROMOTED: 1n << 29n, + PARTNER: 1n << 30n, + PARENT: 1n << 33n, + DISABLE_RELATIONSHIP_ACCESS: 1n << 34n, + STOREFRONT_ELIGIBLE: 1n << 35n, +} as const; + +export interface ApplicationExecutable { + os: string; + name: string; + is_launcher: boolean; +} + +export interface Company { + id: Snowflake; + name: string; +} + +export interface ApplicationInstallParams { + scopes: string[]; + permissions: string; +} + +export interface ApplicationIntegrationTypeConfiguration { + oauth2_install_params?: ApplicationInstallParams; +} + +export interface EmbeddedActivityConfig { + application_id?: Snowflake; + activity_preview_video_asset_id: Snowflake | null; + supported_platforms: string[]; + default_orientation_lock_state: EmbeddedActivityOrientationLockStateType; + tablet_default_orientation_lock_state: EmbeddedActivityOrientationLockStateType; + requires_age_gate: boolean; + legacy_responsive_aspect_ratio: boolean; + premium_tier_requirement: number | null; + free_period_starts_at: string | null; + free_period_ends_at: string | null; + client_platform_config: Record; + shelf_rank: number; + has_csp_exception: boolean; + displays_advertisements: boolean; + supported_locales: string[]; + blocked_locales: string[]; +} + +export enum EmbeddedActivityOrientationLockStateType { + UNLOCKED = 1, + PORTRAIT = 2, + LANDSCAPE = 3, +} + +export interface EmbeddedActivityPlatformConfig { + label_type: EmbeddedActivityPlatformConfigLabelType; + label_until?: string | null; + release_phase: EmbeddedActivityReleasePhase; + omit_badge_from_surfaces: string[]; +} + +export enum EmbeddedActivityPlatformConfigLabelType { + NONE = 0, + NEW = 1, + UPDATED = 2, +} + +export enum EmbeddedActivityReleasePhase { + IN_DEVELOPMENT = "in_development", + ACTIVITIES_TEAM = "activities_team", + EMPLOYEE_RELEASE = "employee_release", + SOFT_LAUNCH = "soft_launch", + SOFT_LAUNCH_MULTI_GEO = "soft_launch_multi_geo", + GLOBAL_LAUNCH = "global_launch", +} diff --git a/src/schemas/api/guilds/Invite.ts b/src/schemas/api/guilds/Invite.ts index efa000f82..51b2e7999 100644 --- a/src/schemas/api/guilds/Invite.ts +++ b/src/schemas/api/guilds/Invite.ts @@ -19,7 +19,7 @@ import { Snowflake } from "../../Identifiers"; import { PartialUser } from "../users"; import { GuildNsfwLevel, GuildPremiumTier, GuildVerificationLevel } from "./GuildSchema"; -import { GuildProfileResponse, PublicChannel, RoleColors } from "@spacebar/schemas"; +import { Application, GuildProfileResponse, PublicChannel, RoleColors } from "@spacebar/schemas"; export type InviteListResponse = PublicInvite[]; @@ -100,3 +100,21 @@ export interface InviteRole { icon?: string | null; unicode_emoji?: string | null; } + +export interface InviteCreateEventData { + code: string; + type: InviteType; + channel_id: Snowflake; + guild_id: Snowflake; + inviter?: PartialUser; + target_type?: number; + target_user?: PartialUser; + target_application?: Application; // PartialApplication + role_ids?: Snowflake[]; + expires_at: string | null; + created_at: string; + uses: number; + max_uses: number; + max_age: number; + temporary: boolean; +} diff --git a/src/schemas/api/users/PaymentSource.ts b/src/schemas/api/users/PaymentSource.ts new file mode 100644 index 000000000..1f7d5f15f --- /dev/null +++ b/src/schemas/api/users/PaymentSource.ts @@ -0,0 +1,89 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2026 Spacebar and Spacebar Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import { Snowflake } from "@spacebar/schemas"; + +export type PaymentSourceListResponse = PaymentSource[]; +export interface PaymentSource { + id: Snowflake; + brand?: string; + country?: string; + last_4?: string; + billing_address: BillingAddress; + type: PaymentSourceType; + payment_gateway: PaymentGateway; + payment_gateway_source_id?: string; + default: boolean; + invalid: boolean; + flags: PaymentSourceFlags; + deleted_at: string | null; + expires_month?: number; + expires_year?: number; + email?: string; + bank?: string; + username?: string; +} + +export interface BillingAddress { + line_1: string; + line_2?: string | null; + name: string; + postal_code?: string; + city: string; + state?: string; + country: string; +} +export enum PaymentSourceType { + CARD = 1, + PAYPAL = 2, + GIROPAY = 3, + SOFORT = 4, + PRZELEWY24 = 5, + SEPA_DEBIT = 6, + PAYSAFE_CARD = 7, + GCASH = 8, + GRABPAY_MY = 9, + MOMO_WALLET = 10, + VENMO = 11, + GOPAY_WALLET = 12, + KAKAOPAY = 13, + BANCONTACT = 14, + EPS = 15, + IDEAL = 16, + CASH_APP = 17, + APPLE = 18, + TDS_WALLET = 19, +} + +export enum PaymentGateway { + STRIPE = 1, + BRAINTREE = 2, + APPLE = 3, + GOOGLE = 4, + ADYEN = 5, + APPLE_PARTNER = 6, + AMAZON = 7, + VIRTUAL_CURRENCY = 8, + APPLE_ADVANCED_COMMERCE = 9, + TDS = 10, +} + +export enum PaymentSourceFlags { + NEW = 1 << 0, + SUCCESSFUL_PAYMENT = 1 << 1, +} diff --git a/src/schemas/api/users/index.ts b/src/schemas/api/users/index.ts index f49cfc54e..9051ea354 100644 --- a/src/schemas/api/users/index.ts +++ b/src/schemas/api/users/index.ts @@ -19,6 +19,7 @@ export * from "./ConnectedAccount"; export * from "./InstanceUserDeleteSchema"; export * from "./Member"; +export * from "./PaymentSource"; export * from "./RelationshipSchemas"; export * from "./SessionsSchemas"; export * from "./Status"; diff --git a/src/schemas/uncategorised/PostDataSchema.ts b/src/schemas/uncategorised/PostDataSchema.ts index b380610d3..c8abd02f1 100644 --- a/src/schemas/uncategorised/PostDataSchema.ts +++ b/src/schemas/uncategorised/PostDataSchema.ts @@ -1,6 +1,6 @@ /* Spacebar: A FOSS re-implementation and extension of the Discord.com backend. - Copyright (C) 2023 Spacebar and Spacebar Contributors + Copyright (C) 2026 Spacebar and Spacebar Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published @@ -16,6 +16,16 @@ along with this program. If not, see . */ +import { PublicMember, PublicMessage, Snowflake } from "@spacebar/schemas"; + export interface PostDataSchema { thread_ids: string[]; } + +export interface PostDataResponse { + threads: Record; +} +export interface PostData { + owner: PublicMember | null; + first_message: PublicMessage | null; +} diff --git a/src/util/config/types/subconfigurations/limits/ApplicationLimits.ts b/src/util/config/types/subconfigurations/limits/ApplicationLimits.ts index 47e09386f..52d492607 100644 --- a/src/util/config/types/subconfigurations/limits/ApplicationLimits.ts +++ b/src/util/config/types/subconfigurations/limits/ApplicationLimits.ts @@ -17,5 +17,10 @@ */ export class ApplicationLimits { - maxEmojis: number = 100; + maxEmojis: number = 1000; + commandLimits: CommandLimits = new CommandLimits(); +} + +export class CommandLimits { + maxNameLength: number = 32; } diff --git a/src/util/interfaces/Event.ts b/src/util/interfaces/Event.ts index 66a25e6db..fc24dc729 100644 --- a/src/util/interfaces/Event.ts +++ b/src/util/interfaces/Event.ts @@ -30,12 +30,14 @@ import { PublicChannel, PublicMember, PublicMessage, + PublicThreadMember, PublicUser, PublicVoiceState, RelationshipSchema, RelationshipType, UserPrivate, } from "@spacebar/schemas"; +import { InviteCreateEventData } from "@spacebar/schemas/api/guilds/Invite"; export interface Event { guild_id?: string; @@ -320,10 +322,7 @@ export interface GuildRoleDeleteEvent extends Event { export interface InviteCreateEvent extends Event { event: "INVITE_CREATE"; - data: Omit & { - channel_id: string; - guild_id?: string; - }; + data: InviteCreateEventData; } export interface InviteDeleteEvent extends Event { @@ -665,7 +664,7 @@ export interface ThreadMembersUpdateEvent extends Event { id: string; guild_id: string; member_count: number; - added_members?: (ThreadMember & { user_id: string })[]; + added_members?: PublicThreadMember[]; removed_member_ids?: string[]; }; } diff --git a/src/util/util/ipc/writer/UnixSocketWriter.ts b/src/util/util/ipc/writer/UnixSocketWriter.ts index ae143f7e1..99a2dc5a8 100644 --- a/src/util/util/ipc/writer/UnixSocketWriter.ts +++ b/src/util/util/ipc/writer/UnixSocketWriter.ts @@ -108,7 +108,7 @@ export class UnixSocketWriter extends BaseEventWriter { delete this.clients[fullPath]; } - console.error("[UnixSocketWriter] Unix socket client error on", fullPath, ":", err); + console.error("[UnixSocketWriter] Unix socket client error on", fullPath, ":", summariseError(err)); rej(err); }); @@ -127,7 +127,7 @@ export class UnixSocketWriter extends BaseEventWriter { let tries = 0; let lastError: Error | undefined = undefined; - while (tries++ < 10) { + while (tries++ < 9) { if (tries > 1) { const delay = Math.pow(tries, 3.5); console.warn("[UnixSocketWriter] Attemption connects to", fullPath, "have been failing... Trying again in", delay, "millis..."); @@ -143,7 +143,7 @@ export class UnixSocketWriter extends BaseEventWriter { } if (tries >= 10 && lastError !== undefined) { - console.error("[UnixSocketWriter] Exhausted all", tries, "attempts at connecting to", fullPath, "- Error:", lastError); + console.error("[UnixSocketWriter] Exhausted all", tries, "attempts at connecting to", fullPath, "-", summariseError(lastError)); if ("code" in lastError && lastError.code == "ECONNREFUSED") { console.error("[UnixSocketWriter] Got ECONNREFUSED for socket", fullPath, " - assuming it is orphaned..."); fs.unlinkSync(fullPath); @@ -286,3 +286,13 @@ function getPidCmdline(pid: number): string | null { return null; } } + +function summariseError(err: Error) { + // we dont care about these: + if ("syscall" in err && err.syscall == "connect") { + if ("code" in err && err.code == "ECONNREFUSED") return "Error: " + err.message; + if ("code" in err && err.code == "ENOENT") return "Error: " + err.message; + } + + return err; +}