Resolve some TODOs

This commit is contained in:
Rory& committed 2026-11-01 22:11:49 +01:00
1 parent 340c2efac5
commit 0cd981e500
41 files changed
+628 -152

No files matched your search

BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
-2
View File
@@ -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") || "*");
@@ -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.`,
},
});
}
@@ -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.`,
},
});
}
@@ -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.`,
},
});
}
@@ -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.`,
},
});
}
-1
View File
@@ -31,7 +31,6 @@ router.post(
spacebarOnly: false, // Not part of public openapi
}),
(req: Request, res: Response) => {
// TODO:
res.sendStatus(204);
},
);
+9 -10
View File
@@ -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());
},
);
@@ -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 });
@@ -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<string, { first_message: null | PublicMessage; owner: null | Member }> } = { 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);
},
);
@@ -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()),
);
},
);
@@ -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,
+1
View File
@@ -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(
"/",
-1
View File
@@ -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) {
@@ -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")));
},
);
-2
View File
@@ -24,8 +24,6 @@ import { GuildCreateSchema } from "@spacebar/schemas";
const router: Router = Router({ mergeParams: true });
//TODO: create default channel
router.post(
"/",
route({
-2
View File
@@ -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) => {
-1
View File
@@ -30,7 +30,6 @@ router.post(
authentication: "never",
}),
(req: Request, res: Response) => {
// TODO:
res.sendStatus(204);
},
);
+1 -1
View File
@@ -84,7 +84,7 @@ router.post(
role: TeamMemberRole.ADMIN,
}).save();
res.json(team.toJSON() satisfies Team);
res.json(team.toJSON());
},
);
@@ -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);
});
-2
View File
@@ -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 } });
@@ -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({
-1
View File
@@ -153,7 +153,6 @@ router.patch(
}
if (body.discriminator) {
// TODO: HACK - maybe make this optional?
if (!/^\d{4}$/.test(body.discriminator)) {
throw FieldErrors({
discriminator: {
+1 -2
View File
@@ -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);
+5 -10
View File
@@ -558,7 +558,6 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
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()];
}
}
+1 -3
View File
@@ -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
+7 -4
View File
@@ -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,
};
}
+22 -1
View File
@@ -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()) {
+1 -1
View File
@@ -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,
},
+4 -3
View File
@@ -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,
};
}
}
+17 -21
View File
@@ -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
+30 -3
View File
@@ -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;
}
+285 -1
View File
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<number, ApplicationIntegrationTypeConfiguration | null>;
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<string, EmbeddedActivityPlatformConfig>;
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",
}
+19 -1
View File
@@ -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;
}
+89
View File
@@ -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 <https://www.gnu.org/licenses/>.
*/
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,
}
+1
View File
@@ -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";
+11 -1
View File
@@ -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 <https://www.gnu.org/licenses/>.
*/
import { PublicMember, PublicMessage, Snowflake } from "@spacebar/schemas";
export interface PostDataSchema {
thread_ids: string[];
}
export interface PostDataResponse {
threads: Record<Snowflake, PostData>;
}
export interface PostData {
owner: PublicMember | null;
first_message: PublicMessage | null;
}
@@ -17,5 +17,10 @@
*/
export class ApplicationLimits {
maxEmojis: number = 100;
maxEmojis: number = 1000;
commandLimits: CommandLimits = new CommandLimits();
}
export class CommandLimits {
maxNameLength: number = 32;
}
+4 -5
View File
@@ -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<Invite, "guild" | "channel"> & {
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[];
};
}
+13 -3
View File
@@ -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;
}