mirror of
https://github.com/spacebarchat/server.git
synced 2026-06-03 17:31:26 +00:00
Refactor to mono-repo + upgrade packages
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import { MinimalPublicUserDTO } from "./UserDTO";
|
||||
import { Channel, PublicUserProjection, User } from "../entities";
|
||||
|
||||
export class DmChannelDTO {
|
||||
icon: string | null;
|
||||
id: string;
|
||||
last_message_id: string | null;
|
||||
name: string | null;
|
||||
origin_channel_id: string | null;
|
||||
owner_id?: string;
|
||||
recipients: MinimalPublicUserDTO[];
|
||||
type: number;
|
||||
|
||||
static async from(channel: Channel, excluded_recipients: string[] = [], origin_channel_id?: string) {
|
||||
const obj = new DmChannelDTO();
|
||||
obj.icon = channel.icon || null;
|
||||
obj.id = channel.id;
|
||||
obj.last_message_id = channel.last_message_id || null;
|
||||
obj.name = channel.name || null;
|
||||
obj.origin_channel_id = origin_channel_id || null;
|
||||
obj.owner_id = channel.owner_id;
|
||||
obj.type = channel.type;
|
||||
obj.recipients = (
|
||||
await Promise.all(
|
||||
channel
|
||||
.recipients!.filter((r) => !excluded_recipients.includes(r.user_id))
|
||||
.map(async (r) => {
|
||||
return await User.findOneOrFail({ where: { id: r.user_id }, select: PublicUserProjection });
|
||||
})
|
||||
)
|
||||
).map((u) => new MinimalPublicUserDTO(u));
|
||||
return obj;
|
||||
}
|
||||
|
||||
excludedRecipients(excluded_recipients: string[]): DmChannelDTO {
|
||||
return {
|
||||
...this,
|
||||
recipients: this.recipients.filter((r) => !excluded_recipients.includes(r.id)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { User } from "../entities";
|
||||
|
||||
export class MinimalPublicUserDTO {
|
||||
avatar?: string | null;
|
||||
discriminator: string;
|
||||
id: string;
|
||||
public_flags: number;
|
||||
username: string;
|
||||
|
||||
constructor(user: User) {
|
||||
this.avatar = user.avatar;
|
||||
this.discriminator = user.discriminator;
|
||||
this.id = user.id;
|
||||
this.public_flags = user.public_flags;
|
||||
this.username = user.username;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./DmChannelDTO";
|
||||
export * from "./UserDTO";
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { Guild } from "./Guild";
|
||||
import { Team } from "./Team";
|
||||
import { User } from "./User";
|
||||
|
||||
@Entity("applications")
|
||||
export class Application extends BaseClass {
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
icon?: string;
|
||||
|
||||
@Column()
|
||||
description: string;
|
||||
|
||||
@Column({ type: "simple-array", nullable: true })
|
||||
rpc_origins?: string[];
|
||||
|
||||
@Column()
|
||||
bot_public: boolean;
|
||||
|
||||
@Column()
|
||||
bot_require_code_grant: boolean;
|
||||
|
||||
@Column({ nullable: true })
|
||||
terms_of_service_url?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
privacy_policy_url?: string;
|
||||
|
||||
@JoinColumn({ name: "owner_id" })
|
||||
@ManyToOne(() => User)
|
||||
owner?: User;
|
||||
|
||||
@Column({ nullable: true })
|
||||
summary?: string;
|
||||
|
||||
@Column()
|
||||
verify_key: string;
|
||||
|
||||
@JoinColumn({ name: "team_id" })
|
||||
@ManyToOne(() => Team, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
team?: Team;
|
||||
|
||||
@JoinColumn({ name: "guild_id" })
|
||||
@ManyToOne(() => Guild)
|
||||
guild: Guild; // if this application is a game sold, this field will be the guild to which it has been linked
|
||||
|
||||
@Column({ nullable: true })
|
||||
primary_sku_id?: string; // if this application is a game sold, this field will be the id of the "Game SKU" that is created,
|
||||
|
||||
@Column({ nullable: true })
|
||||
slug?: string; // if this application is a game sold, this field will be the URL slug that links to the store page
|
||||
|
||||
@Column({ nullable: true })
|
||||
cover_image?: string; // the application's default rich presence invite cover image hash
|
||||
|
||||
@Column()
|
||||
flags: string; // the application's public flags
|
||||
}
|
||||
|
||||
export interface ApplicationCommand {
|
||||
id: string;
|
||||
application_id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
options?: ApplicationCommandOption[];
|
||||
}
|
||||
|
||||
export interface ApplicationCommandOption {
|
||||
type: ApplicationCommandOptionType;
|
||||
name: string;
|
||||
description: string;
|
||||
required?: boolean;
|
||||
choices?: ApplicationCommandOptionChoice[];
|
||||
options?: ApplicationCommandOption[];
|
||||
}
|
||||
|
||||
export interface ApplicationCommandOptionChoice {
|
||||
name: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export enum ApplicationCommandOptionType {
|
||||
SUB_COMMAND = 1,
|
||||
SUB_COMMAND_GROUP = 2,
|
||||
STRING = 3,
|
||||
INTEGER = 4,
|
||||
BOOLEAN = 5,
|
||||
USER = 6,
|
||||
CHANNEL = 7,
|
||||
ROLE = 8,
|
||||
}
|
||||
|
||||
export interface ApplicationCommandInteractionData {
|
||||
id: string;
|
||||
name: string;
|
||||
options?: ApplicationCommandInteractionDataOption[];
|
||||
}
|
||||
|
||||
export interface ApplicationCommandInteractionDataOption {
|
||||
name: string;
|
||||
value?: any;
|
||||
options?: ApplicationCommandInteractionDataOption[];
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { BeforeRemove, Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||
import { URL } from "url";
|
||||
import { deleteFile } from "../util/cdn";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
|
||||
@Entity("attachments")
|
||||
export class Attachment extends BaseClass {
|
||||
@Column()
|
||||
filename: string; // name of file attached
|
||||
|
||||
@Column()
|
||||
size: number; // size of file in bytes
|
||||
|
||||
@Column()
|
||||
url: string; // source url of file
|
||||
|
||||
@Column()
|
||||
proxy_url: string; // a proxied url of file
|
||||
|
||||
@Column({ nullable: true })
|
||||
height?: number; // height of file (if image)
|
||||
|
||||
@Column({ nullable: true })
|
||||
width?: number; // width of file (if image)
|
||||
|
||||
@Column({ nullable: true })
|
||||
content_type?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((attachment: Attachment) => attachment.message)
|
||||
message_id: string;
|
||||
|
||||
@JoinColumn({ name: "message_id" })
|
||||
@ManyToOne(() => require("./Message").Message, (message: import("./Message").Message) => message.attachments, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
message: import("./Message").Message;
|
||||
|
||||
@BeforeRemove()
|
||||
onDelete() {
|
||||
return deleteFile(new URL(this.url).pathname);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { ChannelPermissionOverwrite } from "./Channel";
|
||||
import { User } from "./User";
|
||||
|
||||
export enum AuditLogEvents {
|
||||
// guild level
|
||||
GUILD_UPDATE = 1,
|
||||
GUILD_IMPORT = 2,
|
||||
GUILD_EXPORTED = 3,
|
||||
GUILD_ARCHIVE = 4,
|
||||
GUILD_UNARCHIVE = 5,
|
||||
// join-leave
|
||||
USER_JOIN = 6,
|
||||
USER_LEAVE = 7,
|
||||
// channels
|
||||
CHANNEL_CREATE = 10,
|
||||
CHANNEL_UPDATE = 11,
|
||||
CHANNEL_DELETE = 12,
|
||||
// permission overrides
|
||||
CHANNEL_OVERWRITE_CREATE = 13,
|
||||
CHANNEL_OVERWRITE_UPDATE = 14,
|
||||
CHANNEL_OVERWRITE_DELETE = 15,
|
||||
// kick and ban
|
||||
MEMBER_KICK = 20,
|
||||
MEMBER_PRUNE = 21,
|
||||
MEMBER_BAN_ADD = 22,
|
||||
MEMBER_BAN_REMOVE = 23,
|
||||
// member updates
|
||||
MEMBER_UPDATE = 24,
|
||||
MEMBER_ROLE_UPDATE = 25,
|
||||
MEMBER_MOVE = 26,
|
||||
MEMBER_DISCONNECT = 27,
|
||||
BOT_ADD = 28,
|
||||
// roles
|
||||
ROLE_CREATE = 30,
|
||||
ROLE_UPDATE = 31,
|
||||
ROLE_DELETE = 32,
|
||||
ROLE_SWAP = 33,
|
||||
// invites
|
||||
INVITE_CREATE = 40,
|
||||
INVITE_UPDATE = 41,
|
||||
INVITE_DELETE = 42,
|
||||
// webhooks
|
||||
WEBHOOK_CREATE = 50,
|
||||
WEBHOOK_UPDATE = 51,
|
||||
WEBHOOK_DELETE = 52,
|
||||
WEBHOOK_SWAP = 53,
|
||||
// custom emojis
|
||||
EMOJI_CREATE = 60,
|
||||
EMOJI_UPDATE = 61,
|
||||
EMOJI_DELETE = 62,
|
||||
EMOJI_SWAP = 63,
|
||||
// deletion
|
||||
MESSAGE_CREATE = 70, // messages sent using non-primary seat of the user only
|
||||
MESSAGE_EDIT = 71, // non-self edits only
|
||||
MESSAGE_DELETE = 72,
|
||||
MESSAGE_BULK_DELETE = 73,
|
||||
// pinning
|
||||
MESSAGE_PIN = 74,
|
||||
MESSAGE_UNPIN = 75,
|
||||
// integrations
|
||||
INTEGRATION_CREATE = 80,
|
||||
INTEGRATION_UPDATE = 81,
|
||||
INTEGRATION_DELETE = 82,
|
||||
// stage actions
|
||||
STAGE_INSTANCE_CREATE = 83,
|
||||
STAGE_INSTANCE_UPDATE = 84,
|
||||
STAGE_INSTANCE_DELETE = 85,
|
||||
// stickers
|
||||
STICKER_CREATE = 90,
|
||||
STICKER_UPDATE = 91,
|
||||
STICKER_DELETE = 92,
|
||||
STICKER_SWAP = 93,
|
||||
// threads
|
||||
THREAD_CREATE = 110,
|
||||
THREAD_UPDATE = 111,
|
||||
THREAD_DELETE = 112,
|
||||
// application commands
|
||||
APPLICATION_COMMAND_PERMISSION_UPDATE = 121,
|
||||
// automod
|
||||
POLICY_CREATE = 140,
|
||||
POLICY_UPDATE = 141,
|
||||
POLICY_DELETE = 142,
|
||||
MESSAGE_BLOCKED_BY_POLICIES = 143, // in fosscord, blocked messages are stealth-dropped
|
||||
// instance policies affecting the guild
|
||||
GUILD_AFFECTED_BY_POLICIES = 216,
|
||||
// message moves
|
||||
IN_GUILD_MESSAGE_MOVE = 223,
|
||||
CROSS_GUILD_MESSAGE_MOVE = 224,
|
||||
// message routing
|
||||
ROUTE_CREATE = 225,
|
||||
ROUTE_UPDATE = 226,
|
||||
}
|
||||
|
||||
@Entity("audit_logs")
|
||||
export class AuditLog extends BaseClass {
|
||||
@JoinColumn({ name: "target_id" })
|
||||
@ManyToOne(() => User)
|
||||
target?: User;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((auditlog: AuditLog) => auditlog.user)
|
||||
user_id: string;
|
||||
|
||||
@JoinColumn({ name: "user_id" })
|
||||
@ManyToOne(() => User, (user: User) => user.id)
|
||||
user: User;
|
||||
|
||||
@Column({ type: "int" })
|
||||
action_type: AuditLogEvents;
|
||||
|
||||
@Column({ type: "simple-json", nullable: true })
|
||||
options?: {
|
||||
delete_member_days?: string;
|
||||
members_removed?: string;
|
||||
channel_id?: string;
|
||||
messaged_id?: string;
|
||||
count?: string;
|
||||
id?: string;
|
||||
type?: string;
|
||||
role_name?: string;
|
||||
};
|
||||
|
||||
@Column()
|
||||
@Column({ type: "simple-json" })
|
||||
changes: AuditLogChange[];
|
||||
|
||||
@Column({ nullable: true })
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface AuditLogChange {
|
||||
new_value?: AuditLogChangeValue;
|
||||
old_value?: AuditLogChangeValue;
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface AuditLogChangeValue {
|
||||
name?: string;
|
||||
description?: string;
|
||||
icon_hash?: string;
|
||||
splash_hash?: string;
|
||||
discovery_splash_hash?: string;
|
||||
banner_hash?: string;
|
||||
owner_id?: string;
|
||||
region?: string;
|
||||
preferred_locale?: string;
|
||||
afk_channel_id?: string;
|
||||
afk_timeout?: number;
|
||||
rules_channel_id?: string;
|
||||
public_updates_channel_id?: string;
|
||||
mfa_level?: number;
|
||||
verification_level?: number;
|
||||
explicit_content_filter?: number;
|
||||
default_message_notifications?: number;
|
||||
vanity_url_code?: string;
|
||||
$add?: {}[];
|
||||
$remove?: {}[];
|
||||
prune_delete_days?: number;
|
||||
widget_enabled?: boolean;
|
||||
widget_channel_id?: string;
|
||||
system_channel_id?: string;
|
||||
position?: number;
|
||||
topic?: string;
|
||||
bitrate?: number;
|
||||
permission_overwrites?: ChannelPermissionOverwrite[];
|
||||
nsfw?: boolean;
|
||||
application_id?: string;
|
||||
rate_limit_per_user?: number;
|
||||
permissions?: string;
|
||||
color?: number;
|
||||
hoist?: boolean;
|
||||
mentionable?: boolean;
|
||||
allow?: string;
|
||||
deny?: string;
|
||||
code?: string;
|
||||
channel_id?: string;
|
||||
inviter_id?: string;
|
||||
max_uses?: number;
|
||||
uses?: number;
|
||||
max_age?: number;
|
||||
temporary?: boolean;
|
||||
deaf?: boolean;
|
||||
mute?: boolean;
|
||||
nick?: string;
|
||||
avatar_hash?: string;
|
||||
id?: string;
|
||||
type?: number;
|
||||
enable_emoticons?: boolean;
|
||||
expire_behavior?: number;
|
||||
expire_grace_period?: number;
|
||||
user_limit?: number;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { User } from "./User";
|
||||
import crypto from "crypto";
|
||||
|
||||
@Entity("backup_codes")
|
||||
export class BackupCode extends BaseClass {
|
||||
@JoinColumn({ name: "user_id" })
|
||||
@ManyToOne(() => User, { onDelete: "CASCADE" })
|
||||
user: User;
|
||||
|
||||
@Column()
|
||||
code: string;
|
||||
|
||||
@Column()
|
||||
consumed: boolean;
|
||||
|
||||
@Column()
|
||||
expired: boolean;
|
||||
}
|
||||
|
||||
export function generateMfaBackupCodes(user_id: string) {
|
||||
let backup_codes: BackupCode[] = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const code = BackupCode.create({
|
||||
user: { id: user_id },
|
||||
code: crypto.randomBytes(4).toString("hex"), // 8 characters
|
||||
consumed: false,
|
||||
expired: false,
|
||||
});
|
||||
backup_codes.push(code);
|
||||
}
|
||||
|
||||
return backup_codes;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { Guild } from "./Guild";
|
||||
import { User } from "./User";
|
||||
|
||||
@Entity("bans")
|
||||
export class Ban extends BaseClass {
|
||||
@Column({ nullable: true })
|
||||
@RelationId((ban: Ban) => ban.user)
|
||||
user_id: string;
|
||||
|
||||
@JoinColumn({ name: "user_id" })
|
||||
@ManyToOne(() => User, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
user: User;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((ban: Ban) => ban.guild)
|
||||
guild_id: string;
|
||||
|
||||
@JoinColumn({ name: "guild_id" })
|
||||
@ManyToOne(() => Guild, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
guild: Guild;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((ban: Ban) => ban.executor)
|
||||
executor_id: string;
|
||||
|
||||
@JoinColumn({ name: "executor_id" })
|
||||
@ManyToOne(() => User)
|
||||
executor: User;
|
||||
|
||||
@Column()
|
||||
ip: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
reason?: string;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import "reflect-metadata";
|
||||
import { BaseEntity, BeforeInsert, BeforeUpdate, FindOptionsWhere, ObjectIdColumn, PrimaryColumn } from "typeorm";
|
||||
import { Snowflake } from "../util/Snowflake";
|
||||
import "missing-native-js-functions";
|
||||
import { getDatabase } from "..";
|
||||
import { OrmUtils } from "@fosscord/util";
|
||||
|
||||
export class BaseClassWithoutId extends BaseEntity {
|
||||
private get construct(): any {
|
||||
return this.constructor;
|
||||
}
|
||||
|
||||
private get metadata() {
|
||||
return getDatabase()?.getMetadata(this.construct);
|
||||
}
|
||||
|
||||
assign(props: any) {
|
||||
OrmUtils.mergeDeep(this, props);
|
||||
return this;
|
||||
}
|
||||
|
||||
toJSON(): any {
|
||||
return Object.fromEntries(
|
||||
this.metadata!.columns // @ts-ignore
|
||||
.map((x) => [x.propertyName, this[x.propertyName]]) // @ts-ignore
|
||||
.concat(this.metadata.relations.map((x) => [x.propertyName, this[x.propertyName]]))
|
||||
);
|
||||
}
|
||||
|
||||
static increment<T extends BaseClass>(conditions: FindOptionsWhere<T>, propertyPath: string, value: number | string) {
|
||||
const repository = this.getRepository();
|
||||
return repository.increment(conditions, propertyPath, value);
|
||||
}
|
||||
|
||||
static decrement<T extends BaseClass>(conditions: FindOptionsWhere<T>, propertyPath: string, value: number | string) {
|
||||
const repository = this.getRepository();
|
||||
return repository.decrement(conditions, propertyPath, value);
|
||||
}
|
||||
}
|
||||
|
||||
export const PrimaryIdColumn = process.env.DATABASE?.startsWith("mongodb") ? ObjectIdColumn : PrimaryColumn;
|
||||
|
||||
export class BaseClass extends BaseClassWithoutId {
|
||||
@PrimaryIdColumn()
|
||||
id: string;
|
||||
|
||||
@BeforeUpdate()
|
||||
@BeforeInsert()
|
||||
do_validate() {
|
||||
if (!this.id) this.id = Snowflake.generate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { PrimaryColumn, Column, Entity} from "typeorm";
|
||||
import { BaseClassWithoutId } from "./BaseClass";
|
||||
|
||||
// TODO: categories:
|
||||
// [{
|
||||
// "id": 16,
|
||||
// "default": "Anime & Manga",
|
||||
// "localizations": {
|
||||
// "de": "Anime & Manga",
|
||||
// "fr": "Anim\u00e9s et mangas",
|
||||
// "ru": "\u0410\u043d\u0438\u043c\u0435 \u0438 \u043c\u0430\u043d\u0433\u0430"
|
||||
// }
|
||||
// },
|
||||
// "is_primary": false/true
|
||||
// }]
|
||||
// Also populate discord default categories
|
||||
|
||||
@Entity("categories")
|
||||
export class Categories extends BaseClassWithoutId { // Not using snowflake
|
||||
|
||||
@PrimaryColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
name: string;
|
||||
|
||||
@Column({ type: "simple-json" })
|
||||
localizations: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
is_primary: boolean;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { Guild } from "./Guild";
|
||||
import { PublicUserProjection, User } from "./User";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { containsAll, emitEvent, getPermission, Snowflake, trimSpecial, InvisibleCharacters, ChannelTypes } from "../util";
|
||||
import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces";
|
||||
import { Recipient } from "./Recipient";
|
||||
import { Message } from "./Message";
|
||||
import { ReadState } from "./ReadState";
|
||||
import { Invite } from "./Invite";
|
||||
import { VoiceState } from "./VoiceState";
|
||||
import { Webhook } from "./Webhook";
|
||||
import { DmChannelDTO } from "../dtos";
|
||||
|
||||
export enum ChannelType {
|
||||
GUILD_TEXT = 0, // a text channel within a guild
|
||||
DM = 1, // a direct message between users
|
||||
GUILD_VOICE = 2, // a voice channel within a guild
|
||||
GROUP_DM = 3, // a direct message between multiple users
|
||||
GUILD_CATEGORY = 4, // an organizational category that contains zero or more channels
|
||||
GUILD_NEWS = 5, // a channel that users can follow and crosspost into a guild or route
|
||||
GUILD_STORE = 6, // a channel in which game developers can sell their things
|
||||
ENCRYPTED = 7, // end-to-end encrypted channel
|
||||
ENCRYPTED_THREAD = 8, // end-to-end encrypted thread channel
|
||||
TRANSACTIONAL = 9, // event chain style transactional channel
|
||||
GUILD_NEWS_THREAD = 10, // a temporary sub-channel within a GUILD_NEWS channel
|
||||
GUILD_PUBLIC_THREAD = 11, // a temporary sub-channel within a GUILD_TEXT channel
|
||||
GUILD_PRIVATE_THREAD = 12, // a temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission
|
||||
GUILD_STAGE_VOICE = 13, // a voice channel for hosting events with an audience
|
||||
DIRECTORY = 14, // guild directory listing channel
|
||||
GUILD_FORUM = 15, // forum composed of IM threads
|
||||
TICKET_TRACKER = 33, // ticket tracker, individual ticket items shall have type 12
|
||||
KANBAN = 34, // confluence like kanban board
|
||||
VOICELESS_WHITEBOARD = 35, // whiteboard but without voice (whiteboard + voice is the same as stage)
|
||||
CUSTOM_START = 64, // start custom channel types from here
|
||||
UNHANDLED = 255 // unhandled unowned pass-through channel type
|
||||
}
|
||||
|
||||
@Entity("channels")
|
||||
export class Channel extends BaseClass {
|
||||
@Column()
|
||||
created_at: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
name?: string;
|
||||
|
||||
@Column({ type: "text", nullable: true })
|
||||
icon?: string | null;
|
||||
|
||||
@Column({ type: "int" })
|
||||
type: ChannelType;
|
||||
|
||||
@OneToMany(() => Recipient, (recipient: Recipient) => recipient.channel, {
|
||||
cascade: true,
|
||||
orphanedRowAction: "delete",
|
||||
})
|
||||
recipients?: Recipient[];
|
||||
|
||||
@Column({ nullable: true })
|
||||
last_message_id?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((channel: Channel) => channel.guild)
|
||||
guild_id?: string;
|
||||
|
||||
@JoinColumn({ name: "guild_id" })
|
||||
@ManyToOne(() => Guild, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
guild: Guild;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((channel: Channel) => channel.parent)
|
||||
parent_id: string;
|
||||
|
||||
@JoinColumn({ name: "parent_id" })
|
||||
@ManyToOne(() => Channel)
|
||||
parent?: Channel;
|
||||
|
||||
// for group DMs and owned custom channel types
|
||||
@Column({ nullable: true })
|
||||
@RelationId((channel: Channel) => channel.owner)
|
||||
owner_id?: string;
|
||||
|
||||
@JoinColumn({ name: "owner_id" })
|
||||
@ManyToOne(() => User)
|
||||
owner: User;
|
||||
|
||||
@Column({ nullable: true })
|
||||
last_pin_timestamp?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
default_auto_archive_duration?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
position?: number;
|
||||
|
||||
@Column({ type: "simple-json", nullable: true })
|
||||
permission_overwrites?: ChannelPermissionOverwrite[];
|
||||
|
||||
@Column({ nullable: true })
|
||||
video_quality_mode?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
bitrate?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
user_limit?: number;
|
||||
|
||||
@Column()
|
||||
nsfw: boolean = false;
|
||||
|
||||
@Column({ nullable: true })
|
||||
rate_limit_per_user?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
topic?: string;
|
||||
|
||||
@OneToMany(() => Invite, (invite: Invite) => invite.channel, {
|
||||
cascade: true,
|
||||
orphanedRowAction: "delete",
|
||||
})
|
||||
invites?: Invite[];
|
||||
|
||||
@Column({ nullable: true })
|
||||
retention_policy_id?: string;
|
||||
|
||||
@OneToMany(() => Message, (message: Message) => message.channel, {
|
||||
cascade: true,
|
||||
orphanedRowAction: "delete",
|
||||
})
|
||||
messages?: Message[];
|
||||
|
||||
@OneToMany(() => VoiceState, (voice_state: VoiceState) => voice_state.channel, {
|
||||
cascade: true,
|
||||
orphanedRowAction: "delete",
|
||||
})
|
||||
voice_states?: VoiceState[];
|
||||
|
||||
@OneToMany(() => ReadState, (read_state: ReadState) => read_state.channel, {
|
||||
cascade: true,
|
||||
orphanedRowAction: "delete",
|
||||
})
|
||||
read_states?: ReadState[];
|
||||
|
||||
@OneToMany(() => Webhook, (webhook: Webhook) => webhook.channel, {
|
||||
cascade: true,
|
||||
orphanedRowAction: "delete",
|
||||
})
|
||||
webhooks?: Webhook[];
|
||||
|
||||
// TODO: DM channel
|
||||
static async createChannel(
|
||||
channel: Partial<Channel>,
|
||||
user_id: string = "0",
|
||||
opts?: {
|
||||
keepId?: boolean;
|
||||
skipExistsCheck?: boolean;
|
||||
skipPermissionCheck?: boolean;
|
||||
skipEventEmit?: boolean;
|
||||
skipNameChecks?: boolean;
|
||||
}
|
||||
) {
|
||||
if (!opts?.skipPermissionCheck) {
|
||||
// Always check if user has permission first
|
||||
const permissions = await getPermission(user_id, channel.guild_id);
|
||||
permissions.hasThrow("MANAGE_CHANNELS");
|
||||
}
|
||||
|
||||
if (!opts?.skipNameChecks) {
|
||||
const guild = await Guild.findOneOrFail({ where: { id: channel.guild_id } });
|
||||
if (!guild.features.includes("ALLOW_INVALID_CHANNEL_NAMES") && channel.name) {
|
||||
for (var character of InvisibleCharacters)
|
||||
if (channel.name.includes(character))
|
||||
throw new HTTPError("Channel name cannot include invalid characters", 403);
|
||||
|
||||
// Categories skip these checks on discord.com
|
||||
if (channel.type !== ChannelType.GUILD_CATEGORY) {
|
||||
if (channel.name.includes(" "))
|
||||
throw new HTTPError("Channel name cannot include invalid characters", 403);
|
||||
|
||||
if (channel.name.match(/\-\-+/g))
|
||||
throw new HTTPError("Channel name cannot include multiple adjacent dashes.", 403);
|
||||
|
||||
if (channel.name.charAt(0) === "-" ||
|
||||
channel.name.charAt(channel.name.length - 1) === "-")
|
||||
throw new HTTPError("Channel name cannot start/end with dash.", 403);
|
||||
}
|
||||
else
|
||||
channel.name = channel.name.trim(); //category names are trimmed client side on discord.com
|
||||
}
|
||||
|
||||
if (!guild.features.includes("ALLOW_UNNAMED_CHANNELS")) {
|
||||
if (!channel.name)
|
||||
throw new HTTPError("Channel name cannot be empty.", 403);
|
||||
}
|
||||
}
|
||||
|
||||
switch (channel.type) {
|
||||
case ChannelType.GUILD_TEXT:
|
||||
case ChannelType.GUILD_NEWS:
|
||||
case ChannelType.GUILD_VOICE:
|
||||
if (channel.parent_id && !opts?.skipExistsCheck) {
|
||||
const exists = await Channel.findOneOrFail({ where: { id: channel.parent_id } });
|
||||
if (!exists) throw new HTTPError("Parent id channel doesn't exist", 400);
|
||||
if (exists.guild_id !== channel.guild_id)
|
||||
throw new HTTPError("The category channel needs to be in the guild");
|
||||
}
|
||||
break;
|
||||
case ChannelType.GUILD_CATEGORY:
|
||||
case ChannelType.UNHANDLED:
|
||||
break;
|
||||
case ChannelType.DM:
|
||||
case ChannelType.GROUP_DM:
|
||||
throw new HTTPError("You can't create a dm channel in a guild");
|
||||
case ChannelType.GUILD_STORE:
|
||||
default:
|
||||
throw new HTTPError("Not yet supported");
|
||||
}
|
||||
|
||||
if (!channel.permission_overwrites) channel.permission_overwrites = [];
|
||||
// TODO: eagerly auto generate position of all guild channels
|
||||
|
||||
channel = {
|
||||
...channel,
|
||||
...(!opts?.keepId && { id: Snowflake.generate() }),
|
||||
created_at: new Date(),
|
||||
position: (channel.type === ChannelType.UNHANDLED ? 0 : channel.position) || 0,
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
Channel.create(channel).save(),
|
||||
!opts?.skipEventEmit
|
||||
? emitEvent({
|
||||
event: "CHANNEL_CREATE",
|
||||
data: channel,
|
||||
guild_id: channel.guild_id,
|
||||
} as ChannelCreateEvent)
|
||||
: Promise.resolve(),
|
||||
]);
|
||||
|
||||
return channel;
|
||||
}
|
||||
|
||||
static async createDMChannel(recipients: string[], creator_user_id: string, name?: string) {
|
||||
recipients = recipients.unique().filter((x) => x !== creator_user_id);
|
||||
//@ts-ignore some typeorm typescript issue
|
||||
const otherRecipientsUsers = await User.find({ where: recipients.map((x) => ({ id: x })) });
|
||||
|
||||
// TODO: check config for max number of recipients
|
||||
/** if you want to disallow note to self channels, uncomment the conditional below
|
||||
if (otherRecipientsUsers.length !== recipients.length) {
|
||||
throw new HTTPError("Recipient/s not found");
|
||||
}
|
||||
**/
|
||||
|
||||
const type = recipients.length > 1 ? ChannelType.GROUP_DM : ChannelType.DM;
|
||||
|
||||
let channel = null;
|
||||
|
||||
const channelRecipients = [...recipients, creator_user_id];
|
||||
|
||||
const userRecipients = await Recipient.find({
|
||||
where: { user_id: creator_user_id },
|
||||
relations: ["channel", "channel.recipients"],
|
||||
});
|
||||
|
||||
for (let ur of userRecipients) {
|
||||
let re = ur.channel.recipients!.map((r) => r.user_id);
|
||||
if (re.length === channelRecipients.length) {
|
||||
if (containsAll(re, channelRecipients)) {
|
||||
if (channel == null) {
|
||||
channel = ur.channel;
|
||||
await ur.assign({ closed: false }).save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (channel == null) {
|
||||
name = trimSpecial(name);
|
||||
|
||||
channel = await Channel.create({
|
||||
name,
|
||||
type,
|
||||
owner_id: undefined,
|
||||
created_at: new Date(),
|
||||
last_message_id: undefined,
|
||||
recipients: channelRecipients.map(
|
||||
(x) =>
|
||||
Recipient.create({ user_id: x, closed: !(type === ChannelType.GROUP_DM || x === creator_user_id) })
|
||||
),
|
||||
nsfw: false,
|
||||
}).save();
|
||||
}
|
||||
|
||||
const channel_dto = await DmChannelDTO.from(channel);
|
||||
|
||||
if (type === ChannelType.GROUP_DM) {
|
||||
for (let recipient of channel.recipients!) {
|
||||
await emitEvent({
|
||||
event: "CHANNEL_CREATE",
|
||||
data: channel_dto.excludedRecipients([recipient.user_id]),
|
||||
user_id: recipient.user_id,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await emitEvent({ event: "CHANNEL_CREATE", data: channel_dto, user_id: creator_user_id });
|
||||
}
|
||||
|
||||
if (recipients.length === 1) return channel_dto;
|
||||
else return channel_dto.excludedRecipients([creator_user_id]);
|
||||
}
|
||||
|
||||
static async removeRecipientFromChannel(channel: Channel, user_id: string) {
|
||||
await Recipient.delete({ channel_id: channel.id, user_id: user_id });
|
||||
channel.recipients = channel.recipients?.filter((r) => r.user_id !== user_id);
|
||||
|
||||
if (channel.recipients?.length === 0) {
|
||||
await Channel.deleteChannel(channel);
|
||||
await emitEvent({
|
||||
event: "CHANNEL_DELETE",
|
||||
data: await DmChannelDTO.from(channel, [user_id]),
|
||||
user_id: user_id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await emitEvent({
|
||||
event: "CHANNEL_DELETE",
|
||||
data: await DmChannelDTO.from(channel, [user_id]),
|
||||
user_id: user_id,
|
||||
});
|
||||
|
||||
//If the owner leave the server user is the new owner
|
||||
if (channel.owner_id === user_id) {
|
||||
channel.owner_id = "1"; // The channel is now owned by the server user
|
||||
await emitEvent({
|
||||
event: "CHANNEL_UPDATE",
|
||||
data: await DmChannelDTO.from(channel, [user_id]),
|
||||
channel_id: channel.id,
|
||||
});
|
||||
}
|
||||
|
||||
await channel.save();
|
||||
|
||||
await emitEvent({
|
||||
event: "CHANNEL_RECIPIENT_REMOVE",
|
||||
data: {
|
||||
channel_id: channel.id,
|
||||
user: await User.findOneOrFail({ where: { id: user_id }, select: PublicUserProjection }),
|
||||
},
|
||||
channel_id: channel.id,
|
||||
} as ChannelRecipientRemoveEvent);
|
||||
}
|
||||
|
||||
static async deleteChannel(channel: Channel) {
|
||||
await Message.delete({ channel_id: channel.id }); //TODO we should also delete the attachments from the cdn but to do that we need to move cdn.ts in util
|
||||
//TODO before deleting the channel we should check and delete other relations
|
||||
await Channel.delete({ id: channel.id });
|
||||
}
|
||||
|
||||
isDm() {
|
||||
return this.type === ChannelType.DM || this.type === ChannelType.GROUP_DM;
|
||||
}
|
||||
|
||||
// Does the channel support sending messages ( eg categories do not )
|
||||
isWritable() {
|
||||
const disallowedChannelTypes = [
|
||||
ChannelType.GUILD_CATEGORY,
|
||||
ChannelType.GUILD_STAGE_VOICE,
|
||||
ChannelType.VOICELESS_WHITEBOARD,
|
||||
];
|
||||
return disallowedChannelTypes.indexOf(this.type) == -1;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ChannelPermissionOverwrite {
|
||||
allow: string;
|
||||
deny: string;
|
||||
id: string;
|
||||
type: ChannelPermissionOverwriteType;
|
||||
}
|
||||
|
||||
export enum ChannelPermissionOverwriteType {
|
||||
role = 0,
|
||||
member = 1,
|
||||
group = 2,
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Column, Entity} from "typeorm";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
|
||||
@Entity("client_release")
|
||||
export class Release extends BaseClass {
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column()
|
||||
pub_date: string;
|
||||
|
||||
@Column()
|
||||
url: string;
|
||||
|
||||
@Column()
|
||||
deb_url: string;
|
||||
|
||||
@Column()
|
||||
osx_url: string;
|
||||
|
||||
@Column()
|
||||
win_url: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
notes?: string;
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
import { Column, Entity } from "typeorm";
|
||||
import { BaseClassWithoutId, PrimaryIdColumn } from "./BaseClass";
|
||||
import crypto from "crypto";
|
||||
import { Snowflake } from "../util/Snowflake";
|
||||
import { SessionsReplace } from "..";
|
||||
import { hostname } from "os";
|
||||
import { Rights } from "../util/Rights";
|
||||
|
||||
@Entity("config")
|
||||
export class ConfigEntity extends BaseClassWithoutId {
|
||||
@PrimaryIdColumn()
|
||||
key: string;
|
||||
|
||||
@Column({ type: "simple-json", nullable: true })
|
||||
value: number | boolean | null | string | undefined;
|
||||
}
|
||||
|
||||
export interface RateLimitOptions {
|
||||
bot?: number;
|
||||
count: number;
|
||||
window: number;
|
||||
onyIp?: boolean;
|
||||
}
|
||||
|
||||
export interface Region {
|
||||
id: string;
|
||||
name: string;
|
||||
endpoint: string;
|
||||
location?: {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
};
|
||||
vip: boolean;
|
||||
custom: boolean;
|
||||
deprecated: boolean;
|
||||
}
|
||||
|
||||
export interface KafkaBroker {
|
||||
ip: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export interface ConfigValue {
|
||||
gateway: {
|
||||
endpointClient: string | null;
|
||||
endpointPrivate: string | null;
|
||||
endpointPublic: string | null;
|
||||
};
|
||||
cdn: {
|
||||
endpointClient: string | null;
|
||||
endpointPublic: string | null;
|
||||
endpointPrivate: string | null;
|
||||
resizeHeightMax: number | null;
|
||||
resizeWidthMax: number | null;
|
||||
};
|
||||
api: {
|
||||
defaultVersion: string;
|
||||
activeVersions: string[];
|
||||
useFosscordEnhancements: boolean;
|
||||
};
|
||||
general: {
|
||||
instanceName: string;
|
||||
instanceDescription: string | null;
|
||||
frontPage: string | null;
|
||||
tosPage: string | null;
|
||||
correspondenceEmail: string | null;
|
||||
correspondenceUserID: string | null;
|
||||
image: string | null;
|
||||
instanceId: string;
|
||||
};
|
||||
limits: {
|
||||
user: {
|
||||
maxGuilds: number;
|
||||
maxUsername: number;
|
||||
maxFriends: number;
|
||||
};
|
||||
guild: {
|
||||
maxRoles: number;
|
||||
maxEmojis: number;
|
||||
maxMembers: number;
|
||||
maxChannels: number;
|
||||
maxChannelsInCategory: number;
|
||||
hideOfflineMember: number;
|
||||
};
|
||||
message: {
|
||||
maxCharacters: number;
|
||||
maxTTSCharacters: number;
|
||||
maxReactions: number;
|
||||
maxAttachmentSize: number;
|
||||
maxBulkDelete: number;
|
||||
maxEmbedDownloadSize: number;
|
||||
};
|
||||
channel: {
|
||||
maxPins: number;
|
||||
maxTopic: number;
|
||||
maxWebhooks: number;
|
||||
};
|
||||
rate: {
|
||||
disabled: boolean;
|
||||
ip: Omit<RateLimitOptions, "bot_count">;
|
||||
global: RateLimitOptions;
|
||||
error: RateLimitOptions;
|
||||
routes: {
|
||||
guild: RateLimitOptions;
|
||||
webhook: RateLimitOptions;
|
||||
channel: RateLimitOptions;
|
||||
auth: {
|
||||
login: RateLimitOptions;
|
||||
register: RateLimitOptions;
|
||||
};
|
||||
// TODO: rate limit configuration for all routes
|
||||
};
|
||||
};
|
||||
};
|
||||
security: {
|
||||
autoUpdate: boolean | number;
|
||||
requestSignature: string;
|
||||
jwtSecret: string;
|
||||
forwadedFor: string | null; // header to get the real user ip address
|
||||
captcha: {
|
||||
enabled: boolean;
|
||||
service: "recaptcha" | "hcaptcha" | null; // TODO: hcaptcha, custom
|
||||
sitekey: string | null;
|
||||
secret: string | null;
|
||||
};
|
||||
ipdataApiKey: string | null;
|
||||
defaultRights: string;
|
||||
};
|
||||
login: {
|
||||
requireCaptcha: boolean;
|
||||
};
|
||||
register: {
|
||||
email: {
|
||||
required: boolean;
|
||||
allowlist: boolean;
|
||||
blocklist: boolean;
|
||||
domains: string[];
|
||||
};
|
||||
dateOfBirth: {
|
||||
required: boolean;
|
||||
minimum: number; // in years
|
||||
};
|
||||
disabled: boolean;
|
||||
requireCaptcha: boolean;
|
||||
requireInvite: boolean;
|
||||
guestsRequireInvite: boolean;
|
||||
allowNewRegistration: boolean;
|
||||
allowMultipleAccounts: boolean;
|
||||
blockProxies: boolean;
|
||||
password: {
|
||||
required: boolean;
|
||||
minLength: number;
|
||||
minNumbers: number;
|
||||
minUpperCase: number;
|
||||
minSymbols: number;
|
||||
};
|
||||
incrementingDiscriminators: boolean; // random otherwise
|
||||
};
|
||||
regions: {
|
||||
default: string;
|
||||
useDefaultAsOptimal: boolean;
|
||||
available: Region[];
|
||||
};
|
||||
guild: {
|
||||
discovery: {
|
||||
showAllGuilds: boolean;
|
||||
useRecommendation: boolean; // TODO: Recommendation, privacy concern?
|
||||
offset: number;
|
||||
limit: number;
|
||||
};
|
||||
autoJoin: {
|
||||
enabled: boolean;
|
||||
guilds: string[];
|
||||
canLeave: boolean;
|
||||
};
|
||||
defaultFeatures: string[];
|
||||
};
|
||||
gif: {
|
||||
enabled: boolean;
|
||||
provider: "tenor"; // more coming soon
|
||||
apiKey?: string;
|
||||
};
|
||||
rabbitmq: {
|
||||
host: string | null;
|
||||
};
|
||||
kafka: {
|
||||
brokers: KafkaBroker[] | null;
|
||||
};
|
||||
templates: {
|
||||
enabled: Boolean;
|
||||
allowTemplateCreation: Boolean;
|
||||
allowDiscordTemplates: Boolean;
|
||||
allowRaws: Boolean;
|
||||
},
|
||||
client: {
|
||||
useTestClient: Boolean;
|
||||
releases: {
|
||||
useLocalRelease: Boolean; //TODO
|
||||
upstreamVersion: string;
|
||||
};
|
||||
},
|
||||
metrics: {
|
||||
timeout: number;
|
||||
},
|
||||
sentry: {
|
||||
enabled: boolean;
|
||||
endpoint: string;
|
||||
traceSampleRate: number;
|
||||
environment: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const DefaultConfigOptions: ConfigValue = {
|
||||
gateway: {
|
||||
endpointClient: null,
|
||||
endpointPrivate: null,
|
||||
endpointPublic: null,
|
||||
},
|
||||
cdn: {
|
||||
endpointClient: null,
|
||||
endpointPrivate: null,
|
||||
endpointPublic: null,
|
||||
resizeHeightMax: 1000,
|
||||
resizeWidthMax: 1000,
|
||||
},
|
||||
api: {
|
||||
defaultVersion: "9",
|
||||
activeVersions: ["6", "7", "8", "9"],
|
||||
useFosscordEnhancements: true,
|
||||
},
|
||||
general: {
|
||||
instanceName: "Fosscord Instance",
|
||||
instanceDescription: "This is a Fosscord instance made in pre-release days",
|
||||
frontPage: null,
|
||||
tosPage: null,
|
||||
correspondenceEmail: "noreply@localhost.local",
|
||||
correspondenceUserID: null,
|
||||
image: null,
|
||||
instanceId: Snowflake.generate(),
|
||||
},
|
||||
limits: {
|
||||
user: {
|
||||
maxGuilds: 1048576,
|
||||
maxUsername: 127,
|
||||
maxFriends: 5000,
|
||||
},
|
||||
guild: {
|
||||
maxRoles: 1000,
|
||||
maxEmojis: 2000,
|
||||
maxMembers: 25000000,
|
||||
maxChannels: 65535,
|
||||
maxChannelsInCategory: 65535,
|
||||
hideOfflineMember: 3,
|
||||
},
|
||||
message: {
|
||||
maxCharacters: 1048576,
|
||||
maxTTSCharacters: 160,
|
||||
maxReactions: 2048,
|
||||
maxAttachmentSize: 1024 * 1024 * 1024,
|
||||
maxEmbedDownloadSize: 1024 * 1024 * 5,
|
||||
maxBulkDelete: 1000,
|
||||
},
|
||||
channel: {
|
||||
maxPins: 500,
|
||||
maxTopic: 1024,
|
||||
maxWebhooks: 100,
|
||||
},
|
||||
rate: {
|
||||
disabled: true,
|
||||
ip: {
|
||||
count: 500,
|
||||
window: 5,
|
||||
},
|
||||
global: {
|
||||
count: 250,
|
||||
window: 5,
|
||||
},
|
||||
error: {
|
||||
count: 10,
|
||||
window: 5,
|
||||
},
|
||||
routes: {
|
||||
guild: {
|
||||
count: 5,
|
||||
window: 5,
|
||||
},
|
||||
webhook: {
|
||||
count: 10,
|
||||
window: 5,
|
||||
},
|
||||
channel: {
|
||||
count: 10,
|
||||
window: 5,
|
||||
},
|
||||
auth: {
|
||||
login: {
|
||||
count: 5,
|
||||
window: 60,
|
||||
},
|
||||
register: {
|
||||
count: 2,
|
||||
window: 60 * 60 * 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
security: {
|
||||
autoUpdate: true,
|
||||
requestSignature: crypto.randomBytes(32).toString("base64"),
|
||||
jwtSecret: crypto.randomBytes(256).toString("base64"),
|
||||
forwadedFor: null,
|
||||
// forwadedFor: "X-Forwarded-For" // nginx/reverse proxy
|
||||
// forwadedFor: "CF-Connecting-IP" // cloudflare:
|
||||
captcha: {
|
||||
enabled: false,
|
||||
service: null,
|
||||
sitekey: null,
|
||||
secret: null,
|
||||
},
|
||||
ipdataApiKey: "eca677b284b3bac29eb72f5e496aa9047f26543605efe99ff2ce35c9",
|
||||
defaultRights: "30644591655936", // See util/scripts/rights.js
|
||||
},
|
||||
login: {
|
||||
requireCaptcha: false,
|
||||
},
|
||||
register: {
|
||||
email: {
|
||||
required: false,
|
||||
allowlist: false,
|
||||
blocklist: true,
|
||||
domains: [], // TODO: efficiently save domain blocklist in database
|
||||
// domains: fs.readFileSync(__dirname + "/blockedEmailDomains.txt", { encoding: "utf8" }).split("\n"),
|
||||
},
|
||||
dateOfBirth: {
|
||||
required: true,
|
||||
minimum: 13,
|
||||
},
|
||||
disabled: false,
|
||||
requireInvite: false,
|
||||
guestsRequireInvite: true,
|
||||
requireCaptcha: true,
|
||||
allowNewRegistration: true,
|
||||
allowMultipleAccounts: true,
|
||||
blockProxies: true,
|
||||
password: {
|
||||
required: false,
|
||||
minLength: 8,
|
||||
minNumbers: 2,
|
||||
minUpperCase: 2,
|
||||
minSymbols: 0,
|
||||
},
|
||||
incrementingDiscriminators: false,
|
||||
},
|
||||
regions: {
|
||||
default: "fosscord",
|
||||
useDefaultAsOptimal: true,
|
||||
available: [
|
||||
{
|
||||
id: "fosscord",
|
||||
name: "Fosscord",
|
||||
endpoint: "127.0.0.1:3004",
|
||||
vip: false,
|
||||
custom: false,
|
||||
deprecated: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
guild: {
|
||||
discovery: {
|
||||
showAllGuilds: false,
|
||||
useRecommendation: false,
|
||||
offset: 0,
|
||||
limit: 24,
|
||||
},
|
||||
autoJoin: {
|
||||
enabled: true,
|
||||
canLeave: true,
|
||||
guilds: [],
|
||||
},
|
||||
defaultFeatures: [],
|
||||
},
|
||||
gif: {
|
||||
enabled: true,
|
||||
provider: "tenor",
|
||||
apiKey: "LIVDSRZULELA",
|
||||
},
|
||||
rabbitmq: {
|
||||
host: null,
|
||||
},
|
||||
kafka: {
|
||||
brokers: null,
|
||||
},
|
||||
templates: {
|
||||
enabled: true,
|
||||
allowTemplateCreation: true,
|
||||
allowDiscordTemplates: true,
|
||||
allowRaws: false
|
||||
},
|
||||
client: {
|
||||
useTestClient: true,
|
||||
releases: {
|
||||
useLocalRelease: true,
|
||||
upstreamVersion: "0.0.264"
|
||||
}
|
||||
},
|
||||
metrics: {
|
||||
timeout: 30000
|
||||
},
|
||||
sentry: {
|
||||
enabled: false,
|
||||
endpoint: "https://05e8e3d005f34b7d97e920ae5870a5e5@sentry.thearcanebrony.net/6",
|
||||
traceSampleRate: 1.0,
|
||||
environment: hostname()
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { User } from "./User";
|
||||
|
||||
export interface PublicConnectedAccount extends Pick<ConnectedAccount, "name" | "type" | "verified"> {}
|
||||
|
||||
@Entity("connected_accounts")
|
||||
export class ConnectedAccount extends BaseClass {
|
||||
@Column({ nullable: true })
|
||||
@RelationId((account: ConnectedAccount) => account.user)
|
||||
user_id: string;
|
||||
|
||||
@JoinColumn({ name: "user_id" })
|
||||
@ManyToOne(() => User, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
user: User;
|
||||
|
||||
@Column({ select: false })
|
||||
access_token: string;
|
||||
|
||||
@Column({ select: false })
|
||||
friend_sync: boolean;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column({ select: false })
|
||||
revoked: boolean;
|
||||
|
||||
@Column({ select: false })
|
||||
show_activity: boolean;
|
||||
|
||||
@Column()
|
||||
type: string;
|
||||
|
||||
@Column()
|
||||
verified: boolean;
|
||||
|
||||
@Column({ select: false })
|
||||
visibility: number;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||
import { User } from ".";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { Guild } from "./Guild";
|
||||
import { Role } from "./Role";
|
||||
|
||||
@Entity("emojis")
|
||||
export class Emoji extends BaseClass {
|
||||
@Column()
|
||||
animated: boolean;
|
||||
|
||||
@Column()
|
||||
available: boolean; // whether this emoji can be used, may be false due to various reasons
|
||||
|
||||
@Column()
|
||||
guild_id: string;
|
||||
|
||||
@JoinColumn({ name: "guild_id" })
|
||||
@ManyToOne(() => Guild, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
guild: Guild;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((emoji: Emoji) => emoji.user)
|
||||
user_id: string;
|
||||
|
||||
@JoinColumn({ name: "user_id" })
|
||||
@ManyToOne(() => User)
|
||||
user: User;
|
||||
|
||||
@Column()
|
||||
managed: boolean;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column()
|
||||
require_colons: boolean;
|
||||
|
||||
@Column({ type: "simple-array" })
|
||||
roles: string[]; // roles this emoji is whitelisted to (new discord feature?)
|
||||
|
||||
@Column({ type: "simple-array", nullable: true })
|
||||
groups: string[]; // user groups this emoji is whitelisted to (Fosscord extension)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { Guild } from "./Guild";
|
||||
import { PublicUserProjection, User } from "./User";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { containsAll, emitEvent, getPermission, Snowflake, trimSpecial, InvisibleCharacters } from "../util";
|
||||
import { BitField, BitFieldResolvable, BitFlag } from "../util/BitField";
|
||||
import { Recipient } from "./Recipient";
|
||||
import { Message } from "./Message";
|
||||
import { ReadState } from "./ReadState";
|
||||
import { Invite } from "./Invite";
|
||||
import { DmChannelDTO } from "../dtos";
|
||||
|
||||
@Entity("security_settings")
|
||||
export class SecuritySettings extends BaseClass {
|
||||
|
||||
@Column({ nullable: true })
|
||||
guild_id: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
channel_id: string;
|
||||
|
||||
@Column()
|
||||
encryption_permission_mask: number;
|
||||
|
||||
@Column({ type: "simple-array" })
|
||||
allowed_algorithms: string[];
|
||||
|
||||
@Column()
|
||||
current_algorithm: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
used_since_message: string;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
import { Column, Entity, JoinColumn, ManyToMany, ManyToOne, OneToMany, OneToOne, RelationId } from "typeorm";
|
||||
import { Config, handleFile, Snowflake } from "..";
|
||||
import { Ban } from "./Ban";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { Channel } from "./Channel";
|
||||
import { Emoji } from "./Emoji";
|
||||
import { Invite } from "./Invite";
|
||||
import { Member } from "./Member";
|
||||
import { Role } from "./Role";
|
||||
import { Sticker } from "./Sticker";
|
||||
import { Template } from "./Template";
|
||||
import { User } from "./User";
|
||||
import { VoiceState } from "./VoiceState";
|
||||
import { Webhook } from "./Webhook";
|
||||
|
||||
// TODO: application_command_count, application_command_counts: {1: 0, 2: 0, 3: 0}
|
||||
// TODO: guild_scheduled_events
|
||||
// TODO: stage_instances
|
||||
// TODO: threads
|
||||
// TODO:
|
||||
// "keywords": [
|
||||
// "Genshin Impact",
|
||||
// "Paimon",
|
||||
// "Honkai Impact",
|
||||
// "ARPG",
|
||||
// "Open-World",
|
||||
// "Waifu",
|
||||
// "Anime",
|
||||
// "Genshin",
|
||||
// "miHoYo",
|
||||
// "Gacha"
|
||||
// ],
|
||||
|
||||
export const PublicGuildRelations = [
|
||||
"channels",
|
||||
"emojis",
|
||||
"members",
|
||||
"roles",
|
||||
"stickers",
|
||||
"voice_states",
|
||||
"members.user",
|
||||
];
|
||||
|
||||
@Entity("guilds")
|
||||
export class Guild extends BaseClass {
|
||||
@Column({ nullable: true })
|
||||
@RelationId((guild: Guild) => guild.afk_channel)
|
||||
afk_channel_id?: string;
|
||||
|
||||
@JoinColumn({ name: "afk_channel_id" })
|
||||
@ManyToOne(() => Channel)
|
||||
afk_channel?: Channel;
|
||||
|
||||
@Column({ nullable: true })
|
||||
afk_timeout?: number;
|
||||
|
||||
// * commented out -> use owner instead
|
||||
// application id of the guild creator if it is bot-created
|
||||
// @Column({ nullable: true })
|
||||
// application?: string;
|
||||
|
||||
@JoinColumn({ name: "ban_ids" })
|
||||
@OneToMany(() => Ban, (ban: Ban) => ban.guild, {
|
||||
cascade: true,
|
||||
orphanedRowAction: "delete",
|
||||
})
|
||||
bans: Ban[];
|
||||
|
||||
@Column({ nullable: true })
|
||||
banner?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
default_message_notifications?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
discovery_splash?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
explicit_content_filter?: number;
|
||||
|
||||
@Column({ type: "simple-array" })
|
||||
features: string[]; //TODO use enum
|
||||
//TODO: https://discord.com/developers/docs/resources/guild#guild-object-guild-features
|
||||
|
||||
@Column({ nullable: true })
|
||||
primary_category_id?: string; // TODO: this was number?
|
||||
|
||||
@Column({ nullable: true })
|
||||
icon?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
large?: boolean;
|
||||
|
||||
@Column({ nullable: true })
|
||||
max_members?: number; // e.g. default 100.000
|
||||
|
||||
@Column({ nullable: true })
|
||||
max_presences?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
max_video_channel_users?: number; // ? default: 25, is this max 25 streaming or watching
|
||||
|
||||
@Column({ nullable: true })
|
||||
member_count?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
presence_count?: number; // users online
|
||||
|
||||
@OneToMany(() => Member, (member: Member) => member.guild, {
|
||||
cascade: true,
|
||||
orphanedRowAction: "delete",
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
members: Member[];
|
||||
|
||||
@JoinColumn({ name: "role_ids" })
|
||||
@OneToMany(() => Role, (role: Role) => role.guild, {
|
||||
cascade: true,
|
||||
orphanedRowAction: "delete",
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
roles: Role[];
|
||||
|
||||
@JoinColumn({ name: "channel_ids" })
|
||||
@OneToMany(() => Channel, (channel: Channel) => channel.guild, {
|
||||
cascade: true,
|
||||
orphanedRowAction: "delete",
|
||||
})
|
||||
channels: Channel[];
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((guild: Guild) => guild.template)
|
||||
template_id?: string;
|
||||
|
||||
@JoinColumn({ name: "template_id", referencedColumnName: "id" })
|
||||
@ManyToOne(() => Template)
|
||||
template: Template;
|
||||
|
||||
@JoinColumn({ name: "emoji_ids" })
|
||||
@OneToMany(() => Emoji, (emoji: Emoji) => emoji.guild, {
|
||||
cascade: true,
|
||||
orphanedRowAction: "delete",
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
emojis: Emoji[];
|
||||
|
||||
@JoinColumn({ name: "sticker_ids" })
|
||||
@OneToMany(() => Sticker, (sticker: Sticker) => sticker.guild, {
|
||||
cascade: true,
|
||||
orphanedRowAction: "delete",
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
stickers: Sticker[];
|
||||
|
||||
@JoinColumn({ name: "invite_ids" })
|
||||
@OneToMany(() => Invite, (invite: Invite) => invite.guild, {
|
||||
cascade: true,
|
||||
orphanedRowAction: "delete",
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
invites: Invite[];
|
||||
|
||||
@JoinColumn({ name: "voice_state_ids" })
|
||||
@OneToMany(() => VoiceState, (voicestate: VoiceState) => voicestate.guild, {
|
||||
cascade: true,
|
||||
orphanedRowAction: "delete",
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
voice_states: VoiceState[];
|
||||
|
||||
@JoinColumn({ name: "webhook_ids" })
|
||||
@OneToMany(() => Webhook, (webhook: Webhook) => webhook.guild, {
|
||||
cascade: true,
|
||||
orphanedRowAction: "delete",
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
webhooks: Webhook[];
|
||||
|
||||
@Column({ nullable: true })
|
||||
mfa_level?: number;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((guild: Guild) => guild.owner)
|
||||
owner_id?: string; // optional to allow for ownerless guilds
|
||||
|
||||
@JoinColumn({ name: "owner_id", referencedColumnName: "id" })
|
||||
@ManyToOne(() => User)
|
||||
owner?: User; // optional to allow for ownerless guilds
|
||||
|
||||
@Column({ nullable: true })
|
||||
preferred_locale?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
premium_subscription_count?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
premium_tier?: number; // crowd premium level
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((guild: Guild) => guild.public_updates_channel)
|
||||
public_updates_channel_id: string;
|
||||
|
||||
@JoinColumn({ name: "public_updates_channel_id" })
|
||||
@ManyToOne(() => Channel)
|
||||
public_updates_channel?: Channel;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((guild: Guild) => guild.rules_channel)
|
||||
rules_channel_id?: string;
|
||||
|
||||
@JoinColumn({ name: "rules_channel_id" })
|
||||
@ManyToOne(() => Channel)
|
||||
rules_channel?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
region?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
splash?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((guild: Guild) => guild.system_channel)
|
||||
system_channel_id?: string;
|
||||
|
||||
@JoinColumn({ name: "system_channel_id" })
|
||||
@ManyToOne(() => Channel)
|
||||
system_channel?: Channel;
|
||||
|
||||
@Column({ nullable: true })
|
||||
system_channel_flags?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
unavailable?: boolean;
|
||||
|
||||
@Column({ nullable: true })
|
||||
verification_level?: number;
|
||||
|
||||
@Column({ type: "simple-json" })
|
||||
welcome_screen: {
|
||||
enabled: boolean;
|
||||
description: string;
|
||||
welcome_channels: {
|
||||
description: string;
|
||||
emoji_id?: string;
|
||||
emoji_name?: string;
|
||||
channel_id: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((guild: Guild) => guild.widget_channel)
|
||||
widget_channel_id?: string;
|
||||
|
||||
@JoinColumn({ name: "widget_channel_id" })
|
||||
@ManyToOne(() => Channel)
|
||||
widget_channel?: Channel;
|
||||
|
||||
@Column({ nullable: true })
|
||||
widget_enabled?: boolean;
|
||||
|
||||
@Column({ nullable: true })
|
||||
nsfw_level?: number;
|
||||
|
||||
@Column()
|
||||
nsfw: boolean;
|
||||
|
||||
// TODO: nested guilds
|
||||
@Column({ nullable: true })
|
||||
parent?: string;
|
||||
|
||||
// only for developer portal
|
||||
permissions?: number;
|
||||
|
||||
static async createGuild(body: {
|
||||
name?: string;
|
||||
icon?: string | null;
|
||||
owner_id?: string;
|
||||
channels?: Partial<Channel>[];
|
||||
}) {
|
||||
const guild_id = Snowflake.generate();
|
||||
|
||||
const guild = await Guild.create({
|
||||
name: body.name || "Fosscord",
|
||||
icon: await handleFile(`/icons/${guild_id}`, body.icon as string),
|
||||
region: Config.get().regions.default,
|
||||
owner_id: body.owner_id, // TODO: need to figure out a way for ownerless guilds and multiply-owned guilds
|
||||
afk_timeout: 300,
|
||||
default_message_notifications: 1, // defaults effect: setting the push default at mentions-only will save a lot
|
||||
explicit_content_filter: 0,
|
||||
features: Config.get().guild.defaultFeatures,
|
||||
primary_category_id: undefined,
|
||||
id: guild_id,
|
||||
max_members: 250000,
|
||||
max_presences: 250000,
|
||||
max_video_channel_users: 200,
|
||||
presence_count: 0,
|
||||
member_count: 0, // will automatically be increased by addMember()
|
||||
mfa_level: 0,
|
||||
preferred_locale: "en-US",
|
||||
premium_subscription_count: 0,
|
||||
premium_tier: 0,
|
||||
system_channel_flags: 4, // defaults effect: suppress the setup tips to save performance
|
||||
unavailable: false,
|
||||
nsfw: false,
|
||||
nsfw_level: 0,
|
||||
verification_level: 0,
|
||||
welcome_screen: {
|
||||
enabled: false,
|
||||
description: "Fill in your description",
|
||||
welcome_channels: [],
|
||||
},
|
||||
widget_enabled: true, // NB: don't set it as false to prevent artificial restrictions
|
||||
}).save();
|
||||
|
||||
// we have to create the role _after_ the guild because else we would get a "SQLITE_CONSTRAINT: FOREIGN KEY constraint failed" error
|
||||
// TODO: make the @everyone a pseudorole that is dynamically generated at runtime so we can save storage
|
||||
await Role.create({
|
||||
id: guild_id,
|
||||
guild_id: guild_id,
|
||||
color: 0,
|
||||
hoist: false,
|
||||
managed: false,
|
||||
// NB: in Fosscord, every role will be non-managed, as we use user-groups instead of roles for managed groups
|
||||
mentionable: false,
|
||||
name: "@everyone",
|
||||
permissions: String("2251804225"),
|
||||
position: 0,
|
||||
icon: undefined,
|
||||
unicode_emoji: undefined
|
||||
}).save();
|
||||
|
||||
if (!body.channels || !body.channels.length) body.channels = [{ id: "01", type: 0, name: "general", nsfw: false }];
|
||||
|
||||
const ids = new Map();
|
||||
|
||||
body.channels.forEach((x) => {
|
||||
if (x.id) {
|
||||
ids.set(x.id, Snowflake.generate());
|
||||
}
|
||||
});
|
||||
|
||||
for (const channel of body.channels?.sort((a, b) => (a.parent_id ? 1 : -1))) {
|
||||
var id = ids.get(channel.id) || Snowflake.generate();
|
||||
|
||||
var parent_id = ids.get(channel.parent_id);
|
||||
|
||||
await Channel.createChannel({ ...channel, guild_id, id, parent_id }, body.owner_id, {
|
||||
keepId: true,
|
||||
skipExistsCheck: true,
|
||||
skipPermissionCheck: true,
|
||||
skipEventEmit: true,
|
||||
});
|
||||
}
|
||||
|
||||
return guild;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne, RelationId, PrimaryColumn } from "typeorm";
|
||||
import { Member } from "./Member";
|
||||
import { BaseClassWithoutId } from "./BaseClass";
|
||||
import { Channel } from "./Channel";
|
||||
import { Guild } from "./Guild";
|
||||
import { User } from "./User";
|
||||
|
||||
export const PublicInviteRelation = ["inviter", "guild", "channel"];
|
||||
|
||||
@Entity("invites")
|
||||
export class Invite extends BaseClassWithoutId {
|
||||
@PrimaryColumn()
|
||||
code: string;
|
||||
|
||||
@Column()
|
||||
temporary: boolean;
|
||||
|
||||
@Column()
|
||||
uses: number;
|
||||
|
||||
@Column()
|
||||
max_uses: number;
|
||||
|
||||
@Column()
|
||||
max_age: number;
|
||||
|
||||
@Column()
|
||||
created_at: Date;
|
||||
|
||||
@Column()
|
||||
expires_at: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((invite: Invite) => invite.guild)
|
||||
guild_id: string;
|
||||
|
||||
@JoinColumn({ name: "guild_id" })
|
||||
@ManyToOne(() => Guild, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
guild: Guild;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((invite: Invite) => invite.channel)
|
||||
channel_id: string;
|
||||
|
||||
@JoinColumn({ name: "channel_id" })
|
||||
@ManyToOne(() => Channel, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
channel: Channel;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((invite: Invite) => invite.inviter)
|
||||
inviter_id?: string;
|
||||
|
||||
@JoinColumn({ name: "inviter_id" })
|
||||
@ManyToOne(() => User)
|
||||
inviter: User;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((invite: Invite) => invite.target_user)
|
||||
target_user_id: string;
|
||||
|
||||
@JoinColumn({ name: "target_user_id" })
|
||||
@ManyToOne(() => User, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
target_user?: string; // could be used for "User specific invites" https://github.com/fosscord/fosscord/issues/62
|
||||
|
||||
@Column({ nullable: true })
|
||||
target_user_type?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
vanity_url?: boolean;
|
||||
|
||||
static async joinGuild(user_id: string, code: string) {
|
||||
const invite = await Invite.findOneOrFail({ where: { code } });
|
||||
if (invite.uses++ >= invite.max_uses && invite.max_uses !== 0) await Invite.delete({ code });
|
||||
else await invite.save();
|
||||
|
||||
await Member.addToGuild(user_id, invite.guild_id);
|
||||
return invite;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
import { PublicUser, User } from "./User";
|
||||
import { Message } from "./Message";
|
||||
import {
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
RelationId,
|
||||
} from "typeorm";
|
||||
import { Guild } from "./Guild";
|
||||
import { Config, emitEvent, BannedWords, FieldErrors } from "../util";
|
||||
import {
|
||||
GuildCreateEvent,
|
||||
GuildDeleteEvent,
|
||||
GuildMemberAddEvent,
|
||||
GuildMemberRemoveEvent,
|
||||
GuildMemberUpdateEvent,
|
||||
MessageCreateEvent,
|
||||
|
||||
} from "../interfaces";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { Role } from "./Role";
|
||||
import { BaseClassWithoutId } from "./BaseClass";
|
||||
import { Ban, PublicGuildRelations } from ".";
|
||||
import { DiscordApiErrors } from "../util/Constants";
|
||||
|
||||
export const MemberPrivateProjection: (keyof Member)[] = [
|
||||
"id",
|
||||
"guild",
|
||||
"guild_id",
|
||||
"deaf",
|
||||
"joined_at",
|
||||
"last_message_id",
|
||||
"mute",
|
||||
"nick",
|
||||
"pending",
|
||||
"premium_since",
|
||||
"roles",
|
||||
"settings",
|
||||
"user",
|
||||
];
|
||||
|
||||
@Entity("members")
|
||||
@Index(["id", "guild_id"], { unique: true })
|
||||
export class Member extends BaseClassWithoutId {
|
||||
@PrimaryGeneratedColumn()
|
||||
index: string;
|
||||
|
||||
@Column()
|
||||
@RelationId((member: Member) => member.user)
|
||||
id: string;
|
||||
|
||||
@JoinColumn({ name: "id" })
|
||||
@ManyToOne(() => User, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
user: User;
|
||||
|
||||
@Column()
|
||||
@RelationId((member: Member) => member.guild)
|
||||
guild_id: string;
|
||||
|
||||
@JoinColumn({ name: "guild_id" })
|
||||
@ManyToOne(() => Guild, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
guild: Guild;
|
||||
|
||||
@Column({ nullable: true })
|
||||
nick?: string;
|
||||
|
||||
@JoinTable({
|
||||
name: "member_roles",
|
||||
joinColumn: { name: "index", referencedColumnName: "index" },
|
||||
inverseJoinColumn: {
|
||||
name: "role_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
})
|
||||
@ManyToMany(() => Role, { cascade: true })
|
||||
roles: Role[];
|
||||
|
||||
@Column()
|
||||
joined_at: Date;
|
||||
|
||||
@Column({ type: "bigint", nullable: true })
|
||||
premium_since?: number;
|
||||
|
||||
@Column()
|
||||
deaf: boolean;
|
||||
|
||||
@Column()
|
||||
mute: boolean;
|
||||
|
||||
@Column()
|
||||
pending: boolean;
|
||||
|
||||
@Column({ type: "simple-json", select: false })
|
||||
settings: UserGuildSettings;
|
||||
|
||||
@Column({ nullable: true })
|
||||
last_message_id?: string;
|
||||
|
||||
/**
|
||||
@JoinColumn({ name: "id" })
|
||||
@ManyToOne(() => User, {
|
||||
onDelete: "DO NOTHING",
|
||||
// do not auto-kick force-joined members just because their joiners left the server
|
||||
}) **/
|
||||
@Column({ nullable: true })
|
||||
joined_by?: string;
|
||||
|
||||
// TODO: add this when we have proper read receipts
|
||||
// @Column({ type: "simple-json" })
|
||||
// read_state: ReadState;
|
||||
|
||||
@BeforeUpdate()
|
||||
@BeforeInsert()
|
||||
validate() {
|
||||
if (this.nick) {
|
||||
this.nick = this.nick.split("\n").join("");
|
||||
this.nick = this.nick.split("\t").join("");
|
||||
if (BannedWords.find(this.nick)) throw FieldErrors({ nick: { message: "Bad nickname", code: "INVALID_NICKNAME" } });
|
||||
}
|
||||
}
|
||||
|
||||
static async IsInGuildOrFail(user_id: string, guild_id: string) {
|
||||
if (await Member.count({ where: { id: user_id, guild: { id: guild_id } } })) return true;
|
||||
throw new HTTPError("You are not member of this guild", 403);
|
||||
}
|
||||
|
||||
static async removeFromGuild(user_id: string, guild_id: string) {
|
||||
const guild = await Guild.findOneOrFail({ select: ["owner_id"], where: { id: guild_id } });
|
||||
if (guild.owner_id === user_id) throw new Error("The owner cannot be removed of the guild");
|
||||
const member = await Member.findOneOrFail({ where: { id: user_id, guild_id }, relations: ["user"] });
|
||||
|
||||
// use promise all to execute all promises at the same time -> save time
|
||||
return Promise.all([
|
||||
Member.delete({
|
||||
id: user_id,
|
||||
guild_id,
|
||||
}),
|
||||
Guild.decrement({ id: guild_id }, "member_count", -1),
|
||||
|
||||
emitEvent({
|
||||
event: "GUILD_DELETE",
|
||||
data: {
|
||||
id: guild_id,
|
||||
},
|
||||
user_id: user_id,
|
||||
} as GuildDeleteEvent),
|
||||
emitEvent({
|
||||
event: "GUILD_MEMBER_REMOVE",
|
||||
data: { guild_id, user: member.user },
|
||||
guild_id,
|
||||
} as GuildMemberRemoveEvent),
|
||||
]);
|
||||
}
|
||||
|
||||
static async addRole(user_id: string, guild_id: string, role_id: string) {
|
||||
const [member, role] = await Promise.all([
|
||||
// @ts-ignore
|
||||
Member.findOneOrFail({
|
||||
where: { id: user_id, guild_id },
|
||||
relations: ["user", "roles"], // we don't want to load the role objects just the ids
|
||||
//@ts-ignore
|
||||
select: ["index", "roles.id"], // TODO fix type
|
||||
}),
|
||||
Role.findOneOrFail({ where: { id: role_id, guild_id }, select: ["id"] }),
|
||||
]);
|
||||
member.roles.push(Role.create({ id: role_id }));
|
||||
|
||||
await Promise.all([
|
||||
member.save(),
|
||||
emitEvent({
|
||||
event: "GUILD_MEMBER_UPDATE",
|
||||
data: {
|
||||
guild_id,
|
||||
user: member.user,
|
||||
roles: member.roles.map((x) => x.id),
|
||||
},
|
||||
guild_id,
|
||||
} as GuildMemberUpdateEvent),
|
||||
]);
|
||||
}
|
||||
|
||||
static async removeRole(user_id: string, guild_id: string, role_id: string) {
|
||||
const [member] = await Promise.all([
|
||||
// @ts-ignore
|
||||
Member.findOneOrFail({
|
||||
where: { id: user_id, guild_id },
|
||||
relations: ["user", "roles"], // we don't want to load the role objects just the ids
|
||||
//@ts-ignore
|
||||
select: ["roles.id", "index"], // TODO: fix type
|
||||
}),
|
||||
await Role.findOneOrFail({ where: { id: role_id, guild_id } }),
|
||||
]);
|
||||
member.roles = member.roles.filter((x) => x.id == role_id);
|
||||
|
||||
await Promise.all([
|
||||
member.save(),
|
||||
emitEvent({
|
||||
event: "GUILD_MEMBER_UPDATE",
|
||||
data: {
|
||||
guild_id,
|
||||
user: member.user,
|
||||
roles: member.roles.map((x) => x.id),
|
||||
},
|
||||
guild_id,
|
||||
} as GuildMemberUpdateEvent),
|
||||
]);
|
||||
}
|
||||
|
||||
static async changeNickname(user_id: string, guild_id: string, nickname: string) {
|
||||
const member = await Member.findOneOrFail({
|
||||
where: {
|
||||
id: user_id,
|
||||
guild_id,
|
||||
},
|
||||
relations: ["user"],
|
||||
});
|
||||
member.nick = nickname;
|
||||
|
||||
await Promise.all([
|
||||
member.save(),
|
||||
|
||||
emitEvent({
|
||||
event: "GUILD_MEMBER_UPDATE",
|
||||
data: {
|
||||
guild_id,
|
||||
user: member.user,
|
||||
nick: nickname,
|
||||
},
|
||||
guild_id,
|
||||
} as GuildMemberUpdateEvent),
|
||||
]);
|
||||
}
|
||||
|
||||
static async addToGuild(user_id: string, guild_id: string) {
|
||||
const user = await User.getPublicUser(user_id);
|
||||
const isBanned = await Ban.count({ where: { guild_id, user_id } });
|
||||
if (isBanned) {
|
||||
throw DiscordApiErrors.USER_BANNED;
|
||||
}
|
||||
const { maxGuilds } = Config.get().limits.user;
|
||||
const guild_count = await Member.count({ where: { id: user_id } });
|
||||
if (guild_count >= maxGuilds) {
|
||||
throw new HTTPError(`You are at the ${maxGuilds} server limit.`, 403);
|
||||
}
|
||||
|
||||
const guild = await Guild.findOneOrFail({
|
||||
where: {
|
||||
id: guild_id,
|
||||
},
|
||||
relations: [...PublicGuildRelations, "system_channel"],
|
||||
});
|
||||
|
||||
if (await Member.count({ where: { id: user.id, guild: { id: guild_id } } }))
|
||||
throw new HTTPError("You are already a member of this guild", 400);
|
||||
|
||||
const member = {
|
||||
id: user_id,
|
||||
guild_id,
|
||||
nick: undefined,
|
||||
roles: [guild_id], // @everyone role
|
||||
joined_at: new Date(),
|
||||
premium_since: (new Date()).getTime(),
|
||||
deaf: false,
|
||||
mute: false,
|
||||
pending: false,
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
Member.create({
|
||||
...member,
|
||||
roles: [Role.create({ id: guild_id })],
|
||||
// read_state: {},
|
||||
settings: {
|
||||
guild_id: null,
|
||||
mute_config: null,
|
||||
mute_scheduled_events: false,
|
||||
flags: 0,
|
||||
hide_muted_channels: false,
|
||||
notify_highlights: 0,
|
||||
channel_overrides: {},
|
||||
message_notifications: 0,
|
||||
mobile_push: true,
|
||||
muted: false,
|
||||
suppress_everyone: false,
|
||||
suppress_roles: false,
|
||||
version: 0,
|
||||
},
|
||||
// Member.save is needed because else the roles relations wouldn't be updated
|
||||
}).save(),
|
||||
Guild.increment({ id: guild_id }, "member_count", 1),
|
||||
emitEvent({
|
||||
event: "GUILD_MEMBER_ADD",
|
||||
data: {
|
||||
...member,
|
||||
user,
|
||||
guild_id,
|
||||
},
|
||||
guild_id,
|
||||
} as GuildMemberAddEvent),
|
||||
emitEvent({
|
||||
event: "GUILD_CREATE",
|
||||
data: {
|
||||
...guild,
|
||||
members: [...guild.members, { ...member, user }],
|
||||
member_count: (guild.member_count || 0) + 1,
|
||||
guild_hashes: {},
|
||||
guild_scheduled_events: [],
|
||||
joined_at: member.joined_at,
|
||||
presences: [],
|
||||
stage_instances: [],
|
||||
threads: [],
|
||||
},
|
||||
user_id,
|
||||
} as GuildCreateEvent),
|
||||
]);
|
||||
|
||||
if (guild.system_channel_id) {
|
||||
// send welcome message
|
||||
const message = Message.create({
|
||||
type: 7,
|
||||
guild_id: guild.id,
|
||||
channel_id: guild.system_channel_id,
|
||||
author: user,
|
||||
timestamp: new Date(),
|
||||
reactions: [],
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
sticker_items: [],
|
||||
edited_timestamp: undefined,
|
||||
});
|
||||
await Promise.all([
|
||||
message.save(),
|
||||
emitEvent({ event: "MESSAGE_CREATE", channel_id: message.channel_id, data: message } as MessageCreateEvent)
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ChannelOverride {
|
||||
message_notifications: number;
|
||||
mute_config: MuteConfig;
|
||||
muted: boolean;
|
||||
channel_id: string | null;
|
||||
}
|
||||
|
||||
export interface UserGuildSettings {
|
||||
// channel_overrides: {
|
||||
// channel_id: string;
|
||||
// message_notifications: number;
|
||||
// mute_config: MuteConfig;
|
||||
// muted: boolean;
|
||||
// }[];
|
||||
|
||||
channel_overrides: {
|
||||
[channel_id: string]: ChannelOverride;
|
||||
} | null,
|
||||
message_notifications: number;
|
||||
mobile_push: boolean;
|
||||
mute_config: MuteConfig | null;
|
||||
muted: boolean;
|
||||
suppress_everyone: boolean;
|
||||
suppress_roles: boolean;
|
||||
version: number;
|
||||
guild_id: string | null;
|
||||
flags: number;
|
||||
mute_scheduled_events: boolean;
|
||||
hide_muted_channels: boolean;
|
||||
notify_highlights: 0;
|
||||
}
|
||||
|
||||
export const DefaultUserGuildSettings: UserGuildSettings = {
|
||||
channel_overrides: null,
|
||||
message_notifications: 1,
|
||||
flags: 0,
|
||||
hide_muted_channels: false,
|
||||
mobile_push: true,
|
||||
mute_config: null,
|
||||
mute_scheduled_events: false,
|
||||
muted: false,
|
||||
notify_highlights: 0,
|
||||
suppress_everyone: false,
|
||||
suppress_roles: false,
|
||||
version: 453, // ?
|
||||
guild_id: null,
|
||||
};
|
||||
|
||||
export interface MuteConfig {
|
||||
end_time: number;
|
||||
selected_time_window: number;
|
||||
}
|
||||
|
||||
export type PublicMemberKeys =
|
||||
| "id"
|
||||
| "guild_id"
|
||||
| "nick"
|
||||
| "roles"
|
||||
| "joined_at"
|
||||
| "pending"
|
||||
| "deaf"
|
||||
| "mute"
|
||||
| "premium_since";
|
||||
|
||||
export const PublicMemberProjection: PublicMemberKeys[] = [
|
||||
"id",
|
||||
"guild_id",
|
||||
"nick",
|
||||
"roles",
|
||||
"joined_at",
|
||||
"pending",
|
||||
"deaf",
|
||||
"mute",
|
||||
"premium_since",
|
||||
];
|
||||
|
||||
// @ts-ignore
|
||||
export type PublicMember = Pick<Member, Omit<PublicMemberKeys, "roles">> & {
|
||||
user: PublicUser;
|
||||
roles: string[]; // only role ids not objects
|
||||
};
|
||||
@@ -0,0 +1,296 @@
|
||||
import { User } from "./User";
|
||||
import { Member } from "./Member";
|
||||
import { Role } from "./Role";
|
||||
import { Channel } from "./Channel";
|
||||
import { InteractionType } from "../interfaces/Interaction";
|
||||
import { Application } from "./Application";
|
||||
import {
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
RelationId,
|
||||
} from "typeorm";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { Guild } from "./Guild";
|
||||
import { Webhook } from "./Webhook";
|
||||
import { Sticker } from "./Sticker";
|
||||
import { Attachment } from "./Attachment";
|
||||
import { BannedWords } from "../util";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import { ValidatorConstraint } from "class-validator";
|
||||
|
||||
export enum MessageType {
|
||||
DEFAULT = 0,
|
||||
RECIPIENT_ADD = 1,
|
||||
RECIPIENT_REMOVE = 2,
|
||||
CALL = 3,
|
||||
CHANNEL_NAME_CHANGE = 4,
|
||||
CHANNEL_ICON_CHANGE = 5,
|
||||
CHANNEL_PINNED_MESSAGE = 6,
|
||||
GUILD_MEMBER_JOIN = 7,
|
||||
USER_PREMIUM_GUILD_SUBSCRIPTION = 8,
|
||||
USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1 = 9,
|
||||
USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2 = 10,
|
||||
USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 = 11,
|
||||
CHANNEL_FOLLOW_ADD = 12,
|
||||
ACTION = 13, // /me messages
|
||||
GUILD_DISCOVERY_DISQUALIFIED = 14,
|
||||
GUILD_DISCOVERY_REQUALIFIED = 15,
|
||||
ENCRYPTED = 16,
|
||||
REPLY = 19,
|
||||
APPLICATION_COMMAND = 20, // application command or self command invocation
|
||||
ROUTE_ADDED = 41, // custom message routing: new route affecting that channel
|
||||
ROUTE_DISABLED = 42, // custom message routing: given route no longer affecting that channel
|
||||
SELF_COMMAND_SCRIPT = 43, // self command scripts
|
||||
ENCRYPTION = 50,
|
||||
CUSTOM_START = 63,
|
||||
UNHANDLED = 255
|
||||
}
|
||||
|
||||
@Entity("messages")
|
||||
@Index(["channel_id", "id"], { unique: true })
|
||||
export class Message extends BaseClass {
|
||||
@Column({ nullable: true })
|
||||
@RelationId((message: Message) => message.channel)
|
||||
@Index()
|
||||
channel_id?: string;
|
||||
|
||||
@JoinColumn({ name: "channel_id" })
|
||||
@ManyToOne(() => Channel, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
channel: Channel;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((message: Message) => message.guild)
|
||||
guild_id?: string;
|
||||
|
||||
@JoinColumn({ name: "guild_id" })
|
||||
@ManyToOne(() => Guild, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
guild?: Guild;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((message: Message) => message.author)
|
||||
@Index()
|
||||
author_id?: string;
|
||||
|
||||
@JoinColumn({ name: "author_id", referencedColumnName: "id" })
|
||||
@ManyToOne(() => User, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
author?: User;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((message: Message) => message.member)
|
||||
member_id?: string;
|
||||
|
||||
@JoinColumn({ name: "member_id", referencedColumnName: "id" })
|
||||
@ManyToOne(() => User, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
member?: Member;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((message: Message) => message.webhook)
|
||||
webhook_id?: string;
|
||||
|
||||
@JoinColumn({ name: "webhook_id" })
|
||||
@ManyToOne(() => Webhook)
|
||||
webhook?: Webhook;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((message: Message) => message.application)
|
||||
application_id?: string;
|
||||
|
||||
@JoinColumn({ name: "application_id" })
|
||||
@ManyToOne(() => Application)
|
||||
application?: Application;
|
||||
|
||||
@Column({ nullable: true, type: process.env.PRODUCTION ? "longtext" : undefined })
|
||||
content?: string;
|
||||
|
||||
@Column()
|
||||
@CreateDateColumn()
|
||||
timestamp: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
edited_timestamp?: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
tts?: boolean;
|
||||
|
||||
@Column({ nullable: true })
|
||||
mention_everyone?: boolean;
|
||||
|
||||
@JoinTable({ name: "message_user_mentions" })
|
||||
@ManyToMany(() => User)
|
||||
mentions: User[];
|
||||
|
||||
@JoinTable({ name: "message_role_mentions" })
|
||||
@ManyToMany(() => Role)
|
||||
mention_roles: Role[];
|
||||
|
||||
@JoinTable({ name: "message_channel_mentions" })
|
||||
@ManyToMany(() => Channel)
|
||||
mention_channels: Channel[];
|
||||
|
||||
@JoinTable({ name: "message_stickers" })
|
||||
@ManyToMany(() => Sticker, { cascade: true, onDelete: "CASCADE" })
|
||||
sticker_items?: Sticker[];
|
||||
|
||||
@OneToMany(() => Attachment, (attachment: Attachment) => attachment.message, {
|
||||
cascade: true,
|
||||
orphanedRowAction: "delete",
|
||||
})
|
||||
attachments?: Attachment[];
|
||||
|
||||
@Column({ type: "simple-json" })
|
||||
embeds: Embed[];
|
||||
|
||||
@Column({ type: "simple-json" })
|
||||
reactions: Reaction[];
|
||||
|
||||
@Column({ type: "text", nullable: true })
|
||||
nonce?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
pinned?: boolean;
|
||||
|
||||
@Column({ type: "int" })
|
||||
type: MessageType;
|
||||
|
||||
@Column({ type: "simple-json", nullable: true })
|
||||
activity?: {
|
||||
type: number;
|
||||
party_id: string;
|
||||
};
|
||||
|
||||
@Column({ nullable: true })
|
||||
flags?: string;
|
||||
|
||||
@Column({ type: "simple-json", nullable: true })
|
||||
message_reference?: {
|
||||
message_id: string;
|
||||
channel_id?: string;
|
||||
guild_id?: string;
|
||||
};
|
||||
|
||||
@JoinColumn({ name: "message_reference_id" })
|
||||
@ManyToOne(() => Message)
|
||||
referenced_message?: Message;
|
||||
|
||||
@Column({ type: "simple-json", nullable: true })
|
||||
interaction?: {
|
||||
id: string;
|
||||
type: InteractionType;
|
||||
name: string;
|
||||
user_id: string; // the user who invoked the interaction
|
||||
// user: User; // TODO: autopopulate user
|
||||
};
|
||||
|
||||
@Column({ type: "simple-json", nullable: true })
|
||||
components?: MessageComponent[];
|
||||
|
||||
@BeforeUpdate()
|
||||
@BeforeInsert()
|
||||
validate() {
|
||||
if (this.content) {
|
||||
if (BannedWords.find(this.content)) throw new HTTPError("Message was blocked by automatic moderation", 200000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface MessageComponent {
|
||||
type: number;
|
||||
style?: number;
|
||||
label?: string;
|
||||
emoji?: PartialEmoji;
|
||||
custom_id?: string;
|
||||
url?: string;
|
||||
disabled?: boolean;
|
||||
components: MessageComponent[];
|
||||
}
|
||||
|
||||
export enum MessageComponentType {
|
||||
Script = 0, // self command script
|
||||
ActionRow = 1,
|
||||
Button = 2,
|
||||
}
|
||||
|
||||
export interface Embed {
|
||||
title?: string; //title of embed
|
||||
type?: EmbedType; // type of embed (always "rich" for webhook embeds)
|
||||
description?: string; // description of embed
|
||||
url?: string; // url of embed
|
||||
timestamp?: Date; // timestamp of embed content
|
||||
color?: number; // color code of the embed
|
||||
footer?: {
|
||||
text: string;
|
||||
icon_url?: string;
|
||||
proxy_icon_url?: string;
|
||||
}; // footer object footer information
|
||||
image?: EmbedImage; // image object image information
|
||||
thumbnail?: EmbedImage; // thumbnail object thumbnail information
|
||||
video?: EmbedImage; // video object video information
|
||||
provider?: {
|
||||
name?: string;
|
||||
url?: string;
|
||||
}; // provider object provider information
|
||||
author?: {
|
||||
name?: string;
|
||||
url?: string;
|
||||
icon_url?: string;
|
||||
proxy_icon_url?: string;
|
||||
}; // author object author information
|
||||
fields?: {
|
||||
name: string;
|
||||
value: string;
|
||||
inline?: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
export enum EmbedType {
|
||||
rich = "rich",
|
||||
image = "image",
|
||||
video = "video",
|
||||
gifv = "gifv",
|
||||
article = "article",
|
||||
link = "link",
|
||||
}
|
||||
|
||||
export interface EmbedImage {
|
||||
url?: string;
|
||||
proxy_url?: string;
|
||||
height?: number;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export interface Reaction {
|
||||
count: number;
|
||||
//// not saved in the database // me: boolean; // whether the current user reacted using this emoji
|
||||
emoji: PartialEmoji;
|
||||
user_ids: string[];
|
||||
}
|
||||
|
||||
export interface PartialEmoji {
|
||||
id?: string;
|
||||
name: string;
|
||||
animated?: boolean;
|
||||
}
|
||||
|
||||
export interface AllowedMentions {
|
||||
parse?: ("users" | "roles" | "everyone")[];
|
||||
roles?: string[];
|
||||
users?: string[];
|
||||
replied_user?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Column, Entity, ObjectIdColumn, PrimaryGeneratedColumn } from "typeorm";
|
||||
import { BaseClassWithoutId } from ".";
|
||||
|
||||
export const PrimaryIdAutoGenerated = process.env.DATABASE?.startsWith("mongodb")
|
||||
? ObjectIdColumn
|
||||
: PrimaryGeneratedColumn;
|
||||
|
||||
@Entity("migrations")
|
||||
export class Migration extends BaseClassWithoutId {
|
||||
@PrimaryIdAutoGenerated()
|
||||
id: number;
|
||||
|
||||
@Column({ type: "bigint" })
|
||||
timestamp: number;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne, Unique } from "typeorm";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { User } from "./User";
|
||||
|
||||
@Entity("notes")
|
||||
@Unique(["owner", "target"])
|
||||
export class Note extends BaseClass {
|
||||
@JoinColumn({ name: "owner_id" })
|
||||
@ManyToOne(() => User, { onDelete: "CASCADE" })
|
||||
owner: User;
|
||||
|
||||
@JoinColumn({ name: "target_id" })
|
||||
@ManyToOne(() => User, { onDelete: "CASCADE" })
|
||||
target: User;
|
||||
|
||||
@Column()
|
||||
content: string;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Column, Entity } from "typeorm";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
|
||||
@Entity("rate_limits")
|
||||
export class RateLimit extends BaseClass {
|
||||
@Column() // no relation as it also
|
||||
executor_id: string;
|
||||
|
||||
@Column()
|
||||
hits: number;
|
||||
|
||||
@Column()
|
||||
blocked: boolean;
|
||||
|
||||
@Column()
|
||||
expires_at: Date;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { Channel } from "./Channel";
|
||||
import { Message } from "./Message";
|
||||
import { User } from "./User";
|
||||
|
||||
// for read receipts
|
||||
// notification cursor and public read receipt need to be forwards-only (the former to prevent re-pinging when marked as unread, and the latter to be acceptable as a legal acknowledgement in criminal proceedings), and private read marker needs to be advance-rewind capable
|
||||
// public read receipt ≥ notification cursor ≥ private fully read marker
|
||||
|
||||
@Entity("read_states")
|
||||
@Index(["channel_id", "user_id"], { unique: true })
|
||||
export class ReadState extends BaseClass {
|
||||
@Column()
|
||||
@RelationId((read_state: ReadState) => read_state.channel)
|
||||
channel_id: string;
|
||||
|
||||
@JoinColumn({ name: "channel_id" })
|
||||
@ManyToOne(() => Channel, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
channel: Channel;
|
||||
|
||||
@Column()
|
||||
@RelationId((read_state: ReadState) => read_state.user)
|
||||
user_id: string;
|
||||
|
||||
@JoinColumn({ name: "user_id" })
|
||||
@ManyToOne(() => User, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
user: User;
|
||||
|
||||
// fully read marker
|
||||
@Column({ nullable: true })
|
||||
last_message_id: string;
|
||||
|
||||
// public read receipt
|
||||
@Column({ nullable: true })
|
||||
public_ack: string;
|
||||
|
||||
// notification cursor / private read receipt
|
||||
@Column({ nullable: true })
|
||||
notifications_cursor: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
last_pin_timestamp?: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
mention_count: number;
|
||||
|
||||
// @Column({ nullable: true })
|
||||
// TODO: derive this from (last_message_id=notifications_cursor=public_ack)=true
|
||||
manual: boolean;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
|
||||
@Entity("recipients")
|
||||
export class Recipient extends BaseClass {
|
||||
@Column()
|
||||
@RelationId((recipient: Recipient) => recipient.channel)
|
||||
channel_id: string;
|
||||
|
||||
@JoinColumn({ name: "channel_id" })
|
||||
@ManyToOne(() => require("./Channel").Channel, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
channel: import("./Channel").Channel;
|
||||
|
||||
@Column()
|
||||
@RelationId((recipient: Recipient) => recipient.user)
|
||||
user_id: string;
|
||||
|
||||
@JoinColumn({ name: "user_id" })
|
||||
@ManyToOne(() => require("./User").User, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
user: import("./User").User;
|
||||
|
||||
@Column({ default: false })
|
||||
closed: boolean;
|
||||
|
||||
// TODO: settings/mute/nick/added at/encryption keys/read_state
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { User } from "./User";
|
||||
|
||||
export enum RelationshipType {
|
||||
outgoing = 4,
|
||||
incoming = 3,
|
||||
blocked = 2,
|
||||
friends = 1,
|
||||
}
|
||||
|
||||
@Entity("relationships")
|
||||
@Index(["from_id", "to_id"], { unique: true })
|
||||
export class Relationship extends BaseClass {
|
||||
@Column({})
|
||||
@RelationId((relationship: Relationship) => relationship.from)
|
||||
from_id: string;
|
||||
|
||||
@JoinColumn({ name: "from_id" })
|
||||
@ManyToOne(() => User, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
from: User;
|
||||
|
||||
@Column({})
|
||||
@RelationId((relationship: Relationship) => relationship.to)
|
||||
to_id: string;
|
||||
|
||||
@JoinColumn({ name: "to_id" })
|
||||
@ManyToOne(() => User, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
to: User;
|
||||
|
||||
@Column({ nullable: true })
|
||||
nickname?: string;
|
||||
|
||||
@Column({ type: "int" })
|
||||
type: RelationshipType;
|
||||
|
||||
toPublicRelationship() {
|
||||
return {
|
||||
id: this.to?.id || this.to_id,
|
||||
type: this.type,
|
||||
nickname: this.nickname,
|
||||
user: this.to?.toPublicUser(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { Guild } from "./Guild";
|
||||
|
||||
@Entity("roles")
|
||||
export class Role extends BaseClass {
|
||||
@Column({ nullable: true })
|
||||
@RelationId((role: Role) => role.guild)
|
||||
guild_id: string;
|
||||
|
||||
@JoinColumn({ name: "guild_id" })
|
||||
@ManyToOne(() => Guild, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
guild: Guild;
|
||||
|
||||
@Column()
|
||||
color: number;
|
||||
|
||||
@Column()
|
||||
hoist: boolean;
|
||||
|
||||
@Column()
|
||||
managed: boolean;
|
||||
|
||||
@Column()
|
||||
mentionable: boolean;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column()
|
||||
permissions: string;
|
||||
|
||||
@Column()
|
||||
position: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
icon?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
unicode_emoji?: string;
|
||||
|
||||
@Column({ type: "simple-json", nullable: true })
|
||||
tags?: {
|
||||
bot_id?: string;
|
||||
integration_id?: string;
|
||||
premium_subscriber?: boolean;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { User } from "./User";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||
import { Status } from "../interfaces/Status";
|
||||
import { Activity } from "../interfaces/Activity";
|
||||
|
||||
//TODO we need to remove all sessions on server start because if the server crashes without closing websockets it won't delete them
|
||||
|
||||
@Entity("sessions")
|
||||
export class Session extends BaseClass {
|
||||
@Column({ nullable: true })
|
||||
@RelationId((session: Session) => session.user)
|
||||
user_id: string;
|
||||
|
||||
@JoinColumn({ name: "user_id" })
|
||||
@ManyToOne(() => User, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
user: User;
|
||||
|
||||
//TODO check, should be 32 char long hex string
|
||||
@Column({ nullable: false, select: false })
|
||||
session_id: string;
|
||||
|
||||
@Column({ type: "simple-json", nullable: true })
|
||||
activities: Activity[];
|
||||
|
||||
// TODO client_status
|
||||
@Column({ type: "simple-json", select: false })
|
||||
client_info: {
|
||||
client: string;
|
||||
os: string;
|
||||
version: number;
|
||||
};
|
||||
|
||||
@Column({ nullable: false, type: "varchar" })
|
||||
status: Status; //TODO enum
|
||||
}
|
||||
|
||||
export const PrivateSessionProjection: (keyof Session)[] = [
|
||||
"user_id",
|
||||
"session_id",
|
||||
"activities",
|
||||
"client_info",
|
||||
"status",
|
||||
];
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||
import { User } from "./User";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { Guild } from "./Guild";
|
||||
|
||||
export enum StickerType {
|
||||
STANDARD = 1,
|
||||
GUILD = 2,
|
||||
}
|
||||
|
||||
export enum StickerFormatType {
|
||||
GIF = 0, // gif is a custom format type and not in discord spec
|
||||
PNG = 1,
|
||||
APNG = 2,
|
||||
LOTTIE = 3,
|
||||
}
|
||||
|
||||
@Entity("stickers")
|
||||
export class Sticker extends BaseClass {
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
available?: boolean;
|
||||
|
||||
@Column({ nullable: true })
|
||||
tags?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((sticker: Sticker) => sticker.pack)
|
||||
pack_id?: string;
|
||||
|
||||
@JoinColumn({ name: "pack_id" })
|
||||
@ManyToOne(() => require("./StickerPack").StickerPack, {
|
||||
onDelete: "CASCADE",
|
||||
nullable: true,
|
||||
})
|
||||
pack: import("./StickerPack").StickerPack;
|
||||
|
||||
@Column({ nullable: true })
|
||||
guild_id?: string;
|
||||
|
||||
@JoinColumn({ name: "guild_id" })
|
||||
@ManyToOne(() => Guild, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
guild?: Guild;
|
||||
|
||||
@Column({ nullable: true })
|
||||
user_id?: string;
|
||||
|
||||
@JoinColumn({ name: "user_id" })
|
||||
@ManyToOne(() => User, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
user?: User;
|
||||
|
||||
@Column({ type: "int" })
|
||||
type: StickerType;
|
||||
|
||||
@Column({ type: "int" })
|
||||
format_type: StickerFormatType;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne, OneToMany, OneToOne, RelationId } from "typeorm";
|
||||
import { Sticker } from ".";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
|
||||
@Entity("sticker_packs")
|
||||
export class StickerPack extends BaseClass {
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
banner_asset_id?: string;
|
||||
|
||||
@OneToMany(() => Sticker, (sticker: Sticker) => sticker.pack, {
|
||||
cascade: true,
|
||||
orphanedRowAction: "delete",
|
||||
})
|
||||
stickers: Sticker[];
|
||||
|
||||
// sku_id: string
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((pack: StickerPack) => pack.cover_sticker)
|
||||
cover_sticker_id?: string;
|
||||
|
||||
@ManyToOne(() => Sticker, { nullable: true })
|
||||
@JoinColumn()
|
||||
cover_sticker?: Sticker;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Column, Entity, JoinColumn, ManyToMany, ManyToOne, OneToMany, RelationId } from "typeorm";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { TeamMember } from "./TeamMember";
|
||||
import { User } from "./User";
|
||||
|
||||
@Entity("teams")
|
||||
export class Team extends BaseClass {
|
||||
@Column({ nullable: true })
|
||||
icon?: string;
|
||||
|
||||
@JoinColumn({ name: "member_ids" })
|
||||
@OneToMany(() => TeamMember, (member: TeamMember) => member.team, {
|
||||
orphanedRowAction: "delete",
|
||||
})
|
||||
members: TeamMember[];
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((team: Team) => team.owner_user)
|
||||
owner_user_id: string;
|
||||
|
||||
@JoinColumn({ name: "owner_user_id" })
|
||||
@ManyToOne(() => User)
|
||||
owner_user: User;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { User } from "./User";
|
||||
|
||||
export enum TeamMemberState {
|
||||
INVITED = 1,
|
||||
ACCEPTED = 2,
|
||||
}
|
||||
|
||||
@Entity("team_members")
|
||||
export class TeamMember extends BaseClass {
|
||||
@Column({ type: "int" })
|
||||
membership_state: TeamMemberState;
|
||||
|
||||
@Column({ type: "simple-array" })
|
||||
permissions: string[];
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((member: TeamMember) => member.team)
|
||||
team_id: string;
|
||||
|
||||
@JoinColumn({ name: "team_id" })
|
||||
@ManyToOne(() => require("./Team").Team, (team: import("./Team").Team) => team.members, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
team: import("./Team").Team;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((member: TeamMember) => member.user)
|
||||
user_id: string;
|
||||
|
||||
@JoinColumn({ name: "user_id" })
|
||||
@ManyToOne(() => User, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
user: User;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { Guild } from "./Guild";
|
||||
import { User } from "./User";
|
||||
|
||||
@Entity("templates")
|
||||
export class Template extends BaseClass {
|
||||
@Column({ unique: true })
|
||||
code: string;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
usage_count?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((template: Template) => template.creator)
|
||||
creator_id: string;
|
||||
|
||||
@JoinColumn({ name: "creator_id" })
|
||||
@ManyToOne(() => User)
|
||||
creator: User;
|
||||
|
||||
@Column()
|
||||
created_at: Date;
|
||||
|
||||
@Column()
|
||||
updated_at: Date;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((template: Template) => template.source_guild)
|
||||
source_guild_id: string;
|
||||
|
||||
@JoinColumn({ name: "source_guild_id" })
|
||||
@ManyToOne(() => Guild)
|
||||
source_guild: Guild;
|
||||
|
||||
@Column({ type: "simple-json" })
|
||||
serialized_source_guild: Guild;
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
import { BeforeInsert, BeforeUpdate, Column, Entity, FindOneOptions, JoinColumn, OneToMany } from "typeorm";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { BitField } from "../util/BitField";
|
||||
import { Relationship } from "./Relationship";
|
||||
import { ConnectedAccount } from "./ConnectedAccount";
|
||||
import { Config, FieldErrors, Snowflake, trimSpecial, BannedWords, adjustEmail } from "..";
|
||||
import { Member, Session } from ".";
|
||||
|
||||
export enum PublicUserEnum {
|
||||
username,
|
||||
discriminator,
|
||||
id,
|
||||
public_flags,
|
||||
avatar,
|
||||
accent_color,
|
||||
banner,
|
||||
bio,
|
||||
bot,
|
||||
premium_since,
|
||||
}
|
||||
export type PublicUserKeys = keyof typeof PublicUserEnum;
|
||||
|
||||
export enum PrivateUserEnum {
|
||||
flags,
|
||||
mfa_enabled,
|
||||
email,
|
||||
phone,
|
||||
verified,
|
||||
nsfw_allowed,
|
||||
premium,
|
||||
premium_type,
|
||||
purchased_flags,
|
||||
premium_usage_flags,
|
||||
disabled,
|
||||
settings,
|
||||
// locale
|
||||
}
|
||||
export type PrivateUserKeys = keyof typeof PrivateUserEnum | PublicUserKeys;
|
||||
|
||||
export const PublicUserProjection = Object.values(PublicUserEnum).filter(
|
||||
(x) => typeof x === "string"
|
||||
) as PublicUserKeys[];
|
||||
export const PrivateUserProjection = [
|
||||
...PublicUserProjection,
|
||||
...Object.values(PrivateUserEnum).filter((x) => typeof x === "string"),
|
||||
] as PrivateUserKeys[];
|
||||
|
||||
// Private user data that should never get sent to the client
|
||||
export type PublicUser = Pick<User, PublicUserKeys>;
|
||||
|
||||
export interface UserPublic extends Pick<User, PublicUserKeys> { }
|
||||
|
||||
export interface UserPrivate extends Pick<User, PrivateUserKeys> {
|
||||
locale: string;
|
||||
}
|
||||
|
||||
@Entity("users")
|
||||
export class User extends BaseClass {
|
||||
@Column()
|
||||
username: string; // username max length 32, min 2 (should be configurable)
|
||||
|
||||
@Column()
|
||||
discriminator: string; // opaque string: 4 digits on discord.com
|
||||
|
||||
@Column({ nullable: true })
|
||||
avatar?: string; // hash of the user avatar
|
||||
|
||||
@Column({ nullable: true })
|
||||
accent_color?: number; // banner color of user
|
||||
|
||||
@Column({ nullable: true })
|
||||
banner?: string; // hash of the user banner
|
||||
|
||||
@Column({ nullable: true, select: false })
|
||||
phone?: string; // phone number of the user
|
||||
|
||||
@Column({ select: false })
|
||||
desktop: boolean; // if the user has desktop app installed
|
||||
|
||||
@Column({ select: false })
|
||||
mobile: boolean; // if the user has mobile app installed
|
||||
|
||||
@Column()
|
||||
premium: boolean; // if user bought individual premium
|
||||
|
||||
@Column()
|
||||
premium_type: number; // individual premium level
|
||||
|
||||
@Column()
|
||||
bot: boolean; // if user is bot
|
||||
|
||||
@Column()
|
||||
bio: string; // short description of the user (max 190 chars -> should be configurable)
|
||||
|
||||
@Column()
|
||||
system: boolean; // shouldn't be used, the api sends this field type true, if the generated message comes from a system generated author
|
||||
|
||||
@Column({ select: false })
|
||||
nsfw_allowed: boolean; // if the user can do age-restricted actions (NSFW channels/guilds/commands)
|
||||
|
||||
@Column({ select: false })
|
||||
mfa_enabled: boolean; // if multi factor authentication is enabled
|
||||
|
||||
@Column({ select: false, nullable: true })
|
||||
totp_secret?: string;
|
||||
|
||||
@Column({ nullable: true, select: false })
|
||||
totp_last_ticket?: string;
|
||||
|
||||
@Column()
|
||||
created_at: Date; // registration date
|
||||
|
||||
@Column({ nullable: true })
|
||||
premium_since: Date; // premium date
|
||||
|
||||
@Column({ select: false })
|
||||
verified: boolean; // if the user is offically verified
|
||||
|
||||
@Column()
|
||||
disabled: boolean; // if the account is disabled
|
||||
|
||||
@Column()
|
||||
deleted: boolean; // if the user was deleted
|
||||
|
||||
@Column({ nullable: true, select: false })
|
||||
email?: string; // email of the user
|
||||
|
||||
@Column()
|
||||
flags: string; // UserFlags
|
||||
|
||||
@Column()
|
||||
public_flags: number;
|
||||
|
||||
@Column()
|
||||
purchased_flags: number;
|
||||
|
||||
@Column()
|
||||
premium_usage_flags: number;
|
||||
|
||||
@Column({ type: "bigint" })
|
||||
rights: string; // Rights
|
||||
|
||||
@OneToMany(() => Session, (session: Session) => session.user)
|
||||
sessions: Session[];
|
||||
|
||||
@JoinColumn({ name: "relationship_ids" })
|
||||
@OneToMany(() => Relationship, (relationship: Relationship) => relationship.from, {
|
||||
cascade: true,
|
||||
orphanedRowAction: "delete",
|
||||
})
|
||||
relationships: Relationship[];
|
||||
|
||||
@JoinColumn({ name: "connected_account_ids" })
|
||||
@OneToMany(() => ConnectedAccount, (account: ConnectedAccount) => account.user, {
|
||||
cascade: true,
|
||||
orphanedRowAction: "delete",
|
||||
})
|
||||
connected_accounts: ConnectedAccount[];
|
||||
|
||||
@Column({ type: "simple-json", select: false })
|
||||
data: {
|
||||
valid_tokens_since: Date; // all tokens with a previous issue date are invalid
|
||||
hash?: string; // hash of the password, salt is saved in password (bcrypt)
|
||||
};
|
||||
|
||||
@Column({ type: "simple-array", select: false })
|
||||
fingerprints: string[]; // array of fingerprints -> used to prevent multiple accounts
|
||||
|
||||
@Column({ type: "simple-json", select: false })
|
||||
settings: UserSettings;
|
||||
|
||||
// workaround to prevent fossord-unaware clients from deleting settings not used by them
|
||||
@Column({ type: "simple-json", select: false })
|
||||
extended_settings: string;
|
||||
|
||||
@BeforeUpdate()
|
||||
@BeforeInsert()
|
||||
validate() {
|
||||
this.email = adjustEmail(this.email);
|
||||
if (!this.email) throw FieldErrors({ email: { message: "Invalid email", code: "EMAIL_INVALID" } });
|
||||
if (!this.email.match(/([a-z\d.-]{3,})@([a-z\d.-]+).([a-z]{2,})/g)) throw FieldErrors({ email: { message: "Invalid email", code: "EMAIL_INVALID" } });
|
||||
|
||||
const discrim = Number(this.discriminator);
|
||||
if (this.discriminator.length > 4) throw FieldErrors({ email: { message: "Discriminator cannot be more than 4 digits.", code: "DISCRIMINATOR_INVALID" } });
|
||||
if (isNaN(discrim)) throw FieldErrors({ email: { message: "Discriminator must be a number.", code: "DISCRIMINATOR_INVALID" } });
|
||||
if (discrim <= 0 || discrim >= 10000) throw FieldErrors({ email: { message: "Discriminator must be a number.", code: "DISCRIMINATOR_INVALID" } });
|
||||
this.discriminator = discrim.toString().padStart(4, "0");
|
||||
|
||||
if (BannedWords.find(this.username)) throw FieldErrors({ username: { message: "Bad username", code: "INVALID_USERNAME" } });
|
||||
}
|
||||
|
||||
toPublicUser() {
|
||||
const user: any = {};
|
||||
PublicUserProjection.forEach((x) => {
|
||||
user[x] = this[x];
|
||||
});
|
||||
return user as PublicUser;
|
||||
}
|
||||
|
||||
static async getPublicUser(user_id: string, opts?: FindOneOptions<User>) {
|
||||
return await User.findOneOrFail({
|
||||
where: { id: user_id },
|
||||
...opts,
|
||||
//@ts-ignore
|
||||
select: [...PublicUserProjection, ...(opts?.select || [])], // TODO: fix
|
||||
});
|
||||
}
|
||||
|
||||
private static async generateDiscriminator(username: string): Promise<string | undefined> {
|
||||
if (Config.get().register.incrementingDiscriminators) {
|
||||
// discriminator will be incrementally generated
|
||||
|
||||
// First we need to figure out the currently highest discrimnator for the given username and then increment it
|
||||
const users = await User.find({ where: { username }, select: ["discriminator"] });
|
||||
const highestDiscriminator = Math.max(0, ...users.map((u) => Number(u.discriminator)));
|
||||
|
||||
const discriminator = highestDiscriminator + 1;
|
||||
if (discriminator >= 10000) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return discriminator.toString().padStart(4, "0");
|
||||
} else {
|
||||
// discriminator will be randomly generated
|
||||
|
||||
// randomly generates a discriminator between 1 and 9999 and checks max five times if it already exists
|
||||
// TODO: is there any better way to generate a random discriminator only once, without checking if it already exists in the database?
|
||||
for (let tries = 0; tries < 5; tries++) {
|
||||
const discriminator = Math.randomIntBetween(1, 9999).toString().padStart(4, "0");
|
||||
const exists = await User.findOne({ where: { discriminator, username: username }, select: ["id"] });
|
||||
if (!exists) return discriminator;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
static async register({
|
||||
email,
|
||||
username,
|
||||
password,
|
||||
date_of_birth,
|
||||
req,
|
||||
}: {
|
||||
username: string;
|
||||
password?: string;
|
||||
email?: string;
|
||||
date_of_birth?: Date; // "2000-04-03"
|
||||
req?: any;
|
||||
}) {
|
||||
// trim special uf8 control characters -> Backspace, Newline, ...
|
||||
username = trimSpecial(username);
|
||||
|
||||
const discriminator = await User.generateDiscriminator(username);
|
||||
if (!discriminator) {
|
||||
// We've failed to generate a valid and unused discriminator
|
||||
throw FieldErrors({
|
||||
username: {
|
||||
code: "USERNAME_TOO_MANY_USERS",
|
||||
message: req.t("auth:register.USERNAME_TOO_MANY_USERS"),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: save date_of_birth
|
||||
// appearently discord doesn't save the date of birth and just calculate if nsfw is allowed
|
||||
// if nsfw_allowed is null/undefined it'll require date_of_birth to set it to true/false
|
||||
const language = req.language === "en" ? "en-US" : req.language || "en-US";
|
||||
|
||||
const user = User.create({
|
||||
created_at: new Date(),
|
||||
username: username,
|
||||
discriminator,
|
||||
id: Snowflake.generate(),
|
||||
bot: false,
|
||||
system: false,
|
||||
premium_since: new Date(),
|
||||
desktop: false,
|
||||
mobile: false,
|
||||
premium: true,
|
||||
premium_type: 2,
|
||||
bio: "",
|
||||
mfa_enabled: false,
|
||||
verified: true,
|
||||
disabled: false,
|
||||
deleted: false,
|
||||
email: email,
|
||||
rights: Config.get().security.defaultRights,
|
||||
nsfw_allowed: true, // TODO: depending on age
|
||||
public_flags: 0,
|
||||
flags: "0", // TODO: generate
|
||||
data: {
|
||||
hash: password,
|
||||
valid_tokens_since: new Date(),
|
||||
},
|
||||
settings: { ...defaultSettings, locale: language },
|
||||
purchased_flags: 5, // TODO: idk what the values for this are
|
||||
premium_usage_flags: 2, // TODO: idk what the values for this are
|
||||
extended_settings: "", // TODO: was {}
|
||||
fingerprints: [],
|
||||
});
|
||||
|
||||
await user.save();
|
||||
|
||||
setImmediate(async () => {
|
||||
if (Config.get().guild.autoJoin.enabled) {
|
||||
for (const guild of Config.get().guild.autoJoin.guilds || []) {
|
||||
await Member.addToGuild(user.id, guild).catch((e) => { });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
export const defaultSettings: UserSettings = {
|
||||
afk_timeout: 3600,
|
||||
allow_accessibility_detection: true,
|
||||
animate_emoji: true,
|
||||
animate_stickers: 0,
|
||||
contact_sync_enabled: false,
|
||||
convert_emoticons: false,
|
||||
custom_status: null,
|
||||
default_guilds_restricted: false,
|
||||
detect_platform_accounts: false,
|
||||
developer_mode: true,
|
||||
disable_games_tab: true,
|
||||
enable_tts_command: false,
|
||||
explicit_content_filter: 0,
|
||||
friend_source_flags: { all: true },
|
||||
gateway_connected: false,
|
||||
gif_auto_play: true,
|
||||
guild_folders: [],
|
||||
guild_positions: [],
|
||||
inline_attachment_media: true,
|
||||
inline_embed_media: true,
|
||||
locale: "en-US",
|
||||
message_display_compact: false,
|
||||
native_phone_integration_enabled: true,
|
||||
render_embeds: true,
|
||||
render_reactions: true,
|
||||
restricted_guilds: [],
|
||||
show_current_game: true,
|
||||
status: "online",
|
||||
stream_notifications_enabled: false,
|
||||
theme: "dark",
|
||||
timezone_offset: 0, // TODO: timezone from request
|
||||
|
||||
banner_color: null,
|
||||
friend_discovery_flags: 0,
|
||||
view_nsfw_guilds: true,
|
||||
passwordless: false,
|
||||
};
|
||||
|
||||
export interface UserSettings {
|
||||
afk_timeout: number;
|
||||
allow_accessibility_detection: boolean;
|
||||
animate_emoji: boolean;
|
||||
animate_stickers: number;
|
||||
contact_sync_enabled: boolean;
|
||||
convert_emoticons: boolean;
|
||||
custom_status: {
|
||||
emoji_id?: string;
|
||||
emoji_name?: string;
|
||||
expires_at?: number;
|
||||
text?: string;
|
||||
} | null;
|
||||
default_guilds_restricted: boolean;
|
||||
detect_platform_accounts: boolean;
|
||||
developer_mode: boolean;
|
||||
disable_games_tab: boolean;
|
||||
enable_tts_command: boolean;
|
||||
explicit_content_filter: number;
|
||||
friend_source_flags: { all: boolean; };
|
||||
gateway_connected: boolean;
|
||||
gif_auto_play: boolean;
|
||||
// every top guild is displayed as a "folder"
|
||||
guild_folders: {
|
||||
color: number;
|
||||
guild_ids: string[];
|
||||
id: number;
|
||||
name: string;
|
||||
}[];
|
||||
guild_positions: string[]; // guild ids ordered by position
|
||||
inline_attachment_media: boolean;
|
||||
inline_embed_media: boolean;
|
||||
locale: string; // en_US
|
||||
message_display_compact: boolean;
|
||||
native_phone_integration_enabled: boolean;
|
||||
render_embeds: boolean;
|
||||
render_reactions: boolean;
|
||||
restricted_guilds: string[];
|
||||
show_current_game: boolean;
|
||||
status: "online" | "offline" | "dnd" | "idle" | "invisible";
|
||||
stream_notifications_enabled: boolean;
|
||||
theme: "dark" | "white"; // dark
|
||||
timezone_offset: number; // e.g -60
|
||||
banner_color: string | null;
|
||||
friend_discovery_flags: number;
|
||||
view_nsfw_guilds: boolean;
|
||||
passwordless: boolean;
|
||||
}
|
||||
|
||||
export const CUSTOM_USER_FLAG_OFFSET = BigInt(1) << BigInt(32);
|
||||
|
||||
export class UserFlags extends BitField {
|
||||
static FLAGS = {
|
||||
DISCORD_EMPLOYEE: BigInt(1) << BigInt(0),
|
||||
PARTNERED_SERVER_OWNER: BigInt(1) << BigInt(1),
|
||||
HYPESQUAD_EVENTS: BigInt(1) << BigInt(2),
|
||||
BUGHUNTER_LEVEL_1: BigInt(1) << BigInt(3),
|
||||
MFA_SMS: BigInt(1) << BigInt(4),
|
||||
PREMIUM_PROMO_DISMISSED: BigInt(1) << BigInt(5),
|
||||
HOUSE_BRAVERY: BigInt(1) << BigInt(6),
|
||||
HOUSE_BRILLIANCE: BigInt(1) << BigInt(7),
|
||||
HOUSE_BALANCE: BigInt(1) << BigInt(8),
|
||||
EARLY_SUPPORTER: BigInt(1) << BigInt(9),
|
||||
TEAM_USER: BigInt(1) << BigInt(10),
|
||||
TRUST_AND_SAFETY: BigInt(1) << BigInt(11),
|
||||
SYSTEM: BigInt(1) << BigInt(12),
|
||||
HAS_UNREAD_URGENT_MESSAGES: BigInt(1) << BigInt(13),
|
||||
BUGHUNTER_LEVEL_2: BigInt(1) << BigInt(14),
|
||||
UNDERAGE_DELETED: BigInt(1) << BigInt(15),
|
||||
VERIFIED_BOT: BigInt(1) << BigInt(16),
|
||||
EARLY_VERIFIED_BOT_DEVELOPER: BigInt(1) << BigInt(17),
|
||||
CERTIFIED_MODERATOR: BigInt(1) << BigInt(18),
|
||||
BOT_HTTP_INTERACTIONS: BigInt(1) << BigInt(19),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { Channel } from "./Channel";
|
||||
import { Guild } from "./Guild";
|
||||
import { User } from "./User";
|
||||
import { Member } from "./Member";
|
||||
|
||||
//https://gist.github.com/vassjozsef/e482c65df6ee1facaace8b3c9ff66145#file-voice_state-ex
|
||||
@Entity("voice_states")
|
||||
export class VoiceState extends BaseClass {
|
||||
@Column({ nullable: true })
|
||||
@RelationId((voice_state: VoiceState) => voice_state.guild)
|
||||
guild_id: string;
|
||||
|
||||
@JoinColumn({ name: "guild_id" })
|
||||
@ManyToOne(() => Guild, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
guild?: Guild;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((voice_state: VoiceState) => voice_state.channel)
|
||||
channel_id: string;
|
||||
|
||||
@JoinColumn({ name: "channel_id" })
|
||||
@ManyToOne(() => Channel, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
channel: Channel;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((voice_state: VoiceState) => voice_state.user)
|
||||
user_id: string;
|
||||
|
||||
@JoinColumn({ name: "user_id" })
|
||||
@ManyToOne(() => User, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
user: User;
|
||||
|
||||
// @JoinColumn([{ name: "user_id", referencedColumnName: "id" },{ name: "guild_id", referencedColumnName: "guild_id" }])
|
||||
// @ManyToOne(() => Member, {
|
||||
// onDelete: "CASCADE",
|
||||
// })
|
||||
//TODO find a way to make it work without breaking Guild.voice_states
|
||||
member: Member;
|
||||
|
||||
@Column()
|
||||
session_id: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
token: string;
|
||||
|
||||
@Column()
|
||||
deaf: boolean;
|
||||
|
||||
@Column()
|
||||
mute: boolean;
|
||||
|
||||
@Column()
|
||||
self_deaf: boolean;
|
||||
|
||||
@Column()
|
||||
self_mute: boolean;
|
||||
|
||||
@Column({ nullable: true })
|
||||
self_stream?: boolean;
|
||||
|
||||
@Column()
|
||||
self_video: boolean;
|
||||
|
||||
@Column()
|
||||
suppress: boolean; // whether this user is muted by the current user
|
||||
|
||||
@Column({ nullable: true, default: null })
|
||||
request_to_speak_timestamp?: Date;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
||||
import { Application } from "./Application";
|
||||
import { BaseClass } from "./BaseClass";
|
||||
import { Channel } from "./Channel";
|
||||
import { Guild } from "./Guild";
|
||||
import { User } from "./User";
|
||||
|
||||
export enum WebhookType {
|
||||
Incoming = 1,
|
||||
ChannelFollower = 2,
|
||||
}
|
||||
|
||||
@Entity("webhooks")
|
||||
export class Webhook extends BaseClass {
|
||||
@Column({ type: "int" })
|
||||
type: WebhookType;
|
||||
|
||||
@Column({ nullable: true })
|
||||
name?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
avatar?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
token?: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((webhook: Webhook) => webhook.guild)
|
||||
guild_id: string;
|
||||
|
||||
@JoinColumn({ name: "guild_id" })
|
||||
@ManyToOne(() => Guild, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
guild: Guild;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((webhook: Webhook) => webhook.channel)
|
||||
channel_id: string;
|
||||
|
||||
@JoinColumn({ name: "channel_id" })
|
||||
@ManyToOne(() => Channel, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
channel: Channel;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((webhook: Webhook) => webhook.application)
|
||||
application_id: string;
|
||||
|
||||
@JoinColumn({ name: "application_id" })
|
||||
@ManyToOne(() => Application, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
application: Application;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((webhook: Webhook) => webhook.user)
|
||||
user_id: string;
|
||||
|
||||
@JoinColumn({ name: "user_id" })
|
||||
@ManyToOne(() => User, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
user: User;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@RelationId((webhook: Webhook) => webhook.guild)
|
||||
source_guild_id: string;
|
||||
|
||||
@JoinColumn({ name: "source_guild_id" })
|
||||
@ManyToOne(() => Guild, {
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
source_guild: Guild;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
export * from "./Application";
|
||||
export * from "./Attachment";
|
||||
export * from "./AuditLog";
|
||||
export * from "./Ban";
|
||||
export * from "./BaseClass";
|
||||
export * from "./Categories";
|
||||
export * from "./Channel";
|
||||
export * from "./Config";
|
||||
export * from "./ConnectedAccount";
|
||||
export * from "./Emoji";
|
||||
export * from "./Guild";
|
||||
export * from "./Invite";
|
||||
export * from "./Member";
|
||||
export * from "./Message";
|
||||
export * from "./Migration";
|
||||
export * from "./RateLimit";
|
||||
export * from "./ReadState";
|
||||
export * from "./Recipient";
|
||||
export * from "./Relationship";
|
||||
export * from "./Role";
|
||||
export * from "./Session";
|
||||
export * from "./Sticker";
|
||||
export * from "./StickerPack";
|
||||
export * from "./Team";
|
||||
export * from "./TeamMember";
|
||||
export * from "./Template";
|
||||
export * from "./User";
|
||||
export * from "./VoiceState";
|
||||
export * from "./Webhook";
|
||||
export * from "./ClientRelease";
|
||||
export * from "./BackupCodes";
|
||||
export * from "./Note";
|
||||
@@ -0,0 +1,96 @@
|
||||
//source: https://github.com/typeorm/typeorm/blob/master/src/util/OrmUtils.ts
|
||||
export class OrmUtils {
|
||||
// Checks if it's an object made by Object.create(null), {} or new Object()
|
||||
private static isPlainObject(item: any) {
|
||||
if (item === null || item === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !item.constructor || item.constructor === Object;
|
||||
}
|
||||
|
||||
private static mergeArrayKey(target: any, key: number, value: any, memo: Map<any, any>) {
|
||||
// Have we seen this before? Prevent infinite recursion.
|
||||
if (memo.has(value)) {
|
||||
target[key] = memo.get(value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value instanceof Promise) {
|
||||
// Skip promises entirely.
|
||||
// This is a hold-over from the old code & is because we don't want to pull in
|
||||
// the lazy fields. Ideally we'd remove these promises via another function first
|
||||
// but for now we have to do it here.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isPlainObject(value) && !Array.isArray(value)) {
|
||||
target[key] = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target[key]) {
|
||||
target[key] = Array.isArray(value) ? [] : {};
|
||||
}
|
||||
|
||||
memo.set(value, target[key]);
|
||||
this.merge(target[key], value, memo);
|
||||
memo.delete(value);
|
||||
}
|
||||
|
||||
private static mergeObjectKey(target: any, key: string, value: any, memo: Map<any, any>) {
|
||||
// Have we seen this before? Prevent infinite recursion.
|
||||
if (memo.has(value)) {
|
||||
Object.assign(target, { [key]: memo.get(value) });
|
||||
return;
|
||||
}
|
||||
|
||||
if (value instanceof Promise) {
|
||||
// Skip promises entirely.
|
||||
// This is a hold-over from the old code & is because we don't want to pull in
|
||||
// the lazy fields. Ideally we'd remove these promises via another function first
|
||||
// but for now we have to do it here.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isPlainObject(value) && !Array.isArray(value)) {
|
||||
Object.assign(target, { [key]: value });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target[key]) {
|
||||
Object.assign(target, { [key]: value });
|
||||
}
|
||||
|
||||
memo.set(value, target[key]);
|
||||
this.merge(target[key], value, memo);
|
||||
memo.delete(value);
|
||||
}
|
||||
|
||||
private static merge(target: any, source: any, memo: Map<any, any> = new Map()): any {
|
||||
if (Array.isArray(target) && Array.isArray(source)) {
|
||||
for (let key = 0; key < source.length; key++) {
|
||||
this.mergeArrayKey(target, key, source[key], memo);
|
||||
}
|
||||
} else {
|
||||
for (const key of Object.keys(source)) {
|
||||
this.mergeObjectKey(target, key, source[key], memo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep Object.assign.
|
||||
*/
|
||||
static mergeDeep(target: any, ...sources: any[]): any {
|
||||
if (!sources.length) {
|
||||
return target;
|
||||
}
|
||||
|
||||
for (const source of sources) {
|
||||
OrmUtils.merge(target, source);
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./OrmUtils";
|
||||
@@ -0,0 +1,8 @@
|
||||
import "reflect-metadata";
|
||||
|
||||
export * from "./util/index";
|
||||
export * from "./interfaces/index";
|
||||
export * from "./entities/index";
|
||||
export * from "./dtos/index";
|
||||
export * from "./schemas";
|
||||
export * from "./imports";
|
||||
@@ -0,0 +1,53 @@
|
||||
export interface Activity {
|
||||
name: string; // the activity's name
|
||||
type: ActivityType; // activity type // TODO: check if its between range 0-5
|
||||
url?: string; // stream url, is validated when type is 1
|
||||
created_at?: number; // unix timestamp of when the activity was added to the user's session
|
||||
timestamps?: {
|
||||
// unix timestamps for start and/or end of the game
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
application_id?: string; // application id for the game
|
||||
details?: string;
|
||||
state?: string;
|
||||
emoji?: {
|
||||
name: string;
|
||||
id?: string;
|
||||
animated: boolean;
|
||||
};
|
||||
party?: {
|
||||
id?: string;
|
||||
size?: [number]; // used to show the party's current and maximum size // TODO: array length 2
|
||||
};
|
||||
assets?: {
|
||||
large_image?: string; // the id for a large asset of the activity, usually a snowflake
|
||||
large_text?: string; // text displayed when hovering over the large image of the activity
|
||||
small_image?: string; // the id for a small asset of the activity, usually a snowflake
|
||||
small_text?: string; // text displayed when hovering over the small image of the activity
|
||||
};
|
||||
secrets?: {
|
||||
join?: string; // the secret for joining a party
|
||||
spectate?: string; // the secret for spectating a game
|
||||
match?: string; // the secret for a specific instanced match
|
||||
};
|
||||
instance?: boolean;
|
||||
flags: string; // activity flags OR d together, describes what the payload includes
|
||||
|
||||
id?: string;
|
||||
sync_id?: string;
|
||||
metadata?: { // spotify
|
||||
context_uri?: string;
|
||||
album_id: string;
|
||||
artist_ids: string[];
|
||||
};
|
||||
session_id: string;
|
||||
}
|
||||
|
||||
export enum ActivityType {
|
||||
GAME = 0,
|
||||
STREAMING = 1,
|
||||
LISTENING = 2,
|
||||
CUSTOM = 4,
|
||||
COMPETING = 5,
|
||||
}
|
||||
@@ -0,0 +1,641 @@
|
||||
import { PublicUser, User, UserSettings } from "../entities/User";
|
||||
import { Channel } from "../entities/Channel";
|
||||
import { Guild } from "../entities/Guild";
|
||||
import { Member, PublicMember, UserGuildSettings } from "../entities/Member";
|
||||
import { Emoji } from "../entities/Emoji";
|
||||
import { Role } from "../entities/Role";
|
||||
import { Invite } from "../entities/Invite";
|
||||
import { Message, PartialEmoji } from "../entities/Message";
|
||||
import { VoiceState } from "../entities/VoiceState";
|
||||
import { ApplicationCommand } from "../entities/Application";
|
||||
import { Interaction } from "./Interaction";
|
||||
import { ConnectedAccount } from "../entities/ConnectedAccount";
|
||||
import { Relationship, RelationshipType } from "../entities/Relationship";
|
||||
import { Presence } from "./Presence";
|
||||
import { Sticker } from "..";
|
||||
import { Activity, Status } from ".";
|
||||
|
||||
export interface Event {
|
||||
guild_id?: string;
|
||||
user_id?: string;
|
||||
channel_id?: string;
|
||||
created_at?: Date;
|
||||
event: EVENT;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
// ! Custom Events that shouldn't get sent to the client but processed by the server
|
||||
|
||||
export interface InvalidatedEvent extends Event {
|
||||
event: "INVALIDATED";
|
||||
}
|
||||
|
||||
export interface PublicRelationship {
|
||||
id: string;
|
||||
user: PublicUser;
|
||||
type: RelationshipType;
|
||||
}
|
||||
|
||||
// ! END Custom Events that shouldn't get sent to the client but processed by the server
|
||||
|
||||
export interface ReadyEventData {
|
||||
v: number;
|
||||
user: PublicUser & {
|
||||
mobile: boolean;
|
||||
desktop: boolean;
|
||||
email: string | undefined;
|
||||
flags: string;
|
||||
mfa_enabled: boolean;
|
||||
nsfw_allowed: boolean;
|
||||
phone: string | undefined;
|
||||
premium: boolean;
|
||||
premium_type: number;
|
||||
verified: boolean;
|
||||
bot: boolean;
|
||||
};
|
||||
private_channels: Channel[]; // this will be empty for bots
|
||||
session_id: string; // resuming
|
||||
guilds: Guild[];
|
||||
analytics_token?: string;
|
||||
connected_accounts?: ConnectedAccount[];
|
||||
consents?: {
|
||||
personalization?: {
|
||||
consented?: boolean;
|
||||
};
|
||||
};
|
||||
country_code?: string; // e.g. DE
|
||||
friend_suggestion_count?: number;
|
||||
geo_ordered_rtc_regions?: string[]; // ["europe","russie","india","us-east","us-central"]
|
||||
experiments?: [number, number, number, number, number][];
|
||||
guild_experiments?: [
|
||||
// ? what are guild_experiments?
|
||||
// this is the structure of it:
|
||||
number,
|
||||
null,
|
||||
number,
|
||||
[[number, { e: number; s: number }[]]],
|
||||
[number, [[number, [number, number]]]],
|
||||
{ b: number; k: bigint[] }[]
|
||||
][];
|
||||
guild_join_requests?: any[]; // ? what is this? this is new
|
||||
shard?: [number, number];
|
||||
user_settings?: UserSettings;
|
||||
relationships?: PublicRelationship[]; // TODO
|
||||
read_state: {
|
||||
entries: any[]; // TODO
|
||||
partial: boolean;
|
||||
version: number;
|
||||
};
|
||||
user_guild_settings?: {
|
||||
entries: UserGuildSettings[];
|
||||
version: number;
|
||||
partial: boolean;
|
||||
};
|
||||
application?: {
|
||||
id: string;
|
||||
flags: string;
|
||||
};
|
||||
merged_members?: PublicMember[][];
|
||||
// probably all users who the user is in contact with
|
||||
users?: PublicUser[];
|
||||
sessions: any[];
|
||||
}
|
||||
|
||||
export interface ReadyEvent extends Event {
|
||||
event: "READY";
|
||||
data: ReadyEventData;
|
||||
}
|
||||
|
||||
export interface ChannelCreateEvent extends Event {
|
||||
event: "CHANNEL_CREATE";
|
||||
data: Channel;
|
||||
}
|
||||
|
||||
export interface ChannelUpdateEvent extends Event {
|
||||
event: "CHANNEL_UPDATE";
|
||||
data: Channel;
|
||||
}
|
||||
|
||||
export interface ChannelDeleteEvent extends Event {
|
||||
event: "CHANNEL_DELETE";
|
||||
data: Channel;
|
||||
}
|
||||
|
||||
export interface ChannelPinsUpdateEvent extends Event {
|
||||
event: "CHANNEL_PINS_UPDATE";
|
||||
data: {
|
||||
guild_id?: string;
|
||||
channel_id: string;
|
||||
last_pin_timestamp?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ChannelRecipientAddEvent extends Event {
|
||||
event: "CHANNEL_RECIPIENT_ADD";
|
||||
data: {
|
||||
channel_id: string;
|
||||
user: User;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ChannelRecipientRemoveEvent extends Event {
|
||||
event: "CHANNEL_RECIPIENT_REMOVE";
|
||||
data: {
|
||||
channel_id: string;
|
||||
user: User;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GuildCreateEvent extends Event {
|
||||
event: "GUILD_CREATE";
|
||||
data: Guild & {
|
||||
joined_at: Date;
|
||||
// TODO: add them to guild
|
||||
guild_scheduled_events: never[];
|
||||
guild_hashes: {};
|
||||
presences: never[];
|
||||
stage_instances: never[];
|
||||
threads: never[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface GuildUpdateEvent extends Event {
|
||||
event: "GUILD_UPDATE";
|
||||
data: Guild;
|
||||
}
|
||||
|
||||
export interface GuildDeleteEvent extends Event {
|
||||
event: "GUILD_DELETE";
|
||||
data: {
|
||||
id: string;
|
||||
unavailable?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GuildBanAddEvent extends Event {
|
||||
event: "GUILD_BAN_ADD";
|
||||
data: {
|
||||
guild_id: string;
|
||||
user: User;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GuildBanRemoveEvent extends Event {
|
||||
event: "GUILD_BAN_REMOVE";
|
||||
data: {
|
||||
guild_id: string;
|
||||
user: User;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GuildEmojisUpdateEvent extends Event {
|
||||
event: "GUILD_EMOJIS_UPDATE";
|
||||
data: {
|
||||
guild_id: string;
|
||||
emojis: Emoji[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface GuildStickersUpdateEvent extends Event {
|
||||
event: "GUILD_STICKERS_UPDATE";
|
||||
data: {
|
||||
guild_id: string;
|
||||
stickers: Sticker[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface GuildIntegrationUpdateEvent extends Event {
|
||||
event: "GUILD_INTEGRATIONS_UPDATE";
|
||||
data: {
|
||||
guild_id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GuildMemberAddEvent extends Event {
|
||||
event: "GUILD_MEMBER_ADD";
|
||||
data: PublicMember & {
|
||||
guild_id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GuildMemberRemoveEvent extends Event {
|
||||
event: "GUILD_MEMBER_REMOVE";
|
||||
data: {
|
||||
guild_id: string;
|
||||
user: User;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GuildMemberUpdateEvent extends Event {
|
||||
event: "GUILD_MEMBER_UPDATE";
|
||||
data: {
|
||||
guild_id: string;
|
||||
roles: string[];
|
||||
user: User;
|
||||
nick?: string;
|
||||
joined_at?: Date;
|
||||
premium_since?: number;
|
||||
pending?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GuildMembersChunkEvent extends Event {
|
||||
event: "GUILD_MEMBERS_CHUNK";
|
||||
data: {
|
||||
guild_id: string;
|
||||
members: PublicMember[];
|
||||
chunk_index: number;
|
||||
chunk_count: number;
|
||||
not_found: string[];
|
||||
presences: Presence[];
|
||||
nonce?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GuildRoleCreateEvent extends Event {
|
||||
event: "GUILD_ROLE_CREATE";
|
||||
data: {
|
||||
guild_id: string;
|
||||
role: Role;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GuildRoleUpdateEvent extends Event {
|
||||
event: "GUILD_ROLE_UPDATE";
|
||||
data: {
|
||||
guild_id: string;
|
||||
role: Role;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GuildRoleDeleteEvent extends Event {
|
||||
event: "GUILD_ROLE_DELETE";
|
||||
data: {
|
||||
guild_id: string;
|
||||
role_id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InviteCreateEvent extends Event {
|
||||
event: "INVITE_CREATE";
|
||||
data: Omit<Invite, "guild" | "channel"> & {
|
||||
channel_id: string;
|
||||
guild_id?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InviteDeleteEvent extends Event {
|
||||
event: "INVITE_DELETE";
|
||||
data: {
|
||||
channel_id: string;
|
||||
guild_id?: string;
|
||||
code: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MessageCreateEvent extends Event {
|
||||
event: "MESSAGE_CREATE";
|
||||
data: Message;
|
||||
}
|
||||
|
||||
export interface MessageUpdateEvent extends Event {
|
||||
event: "MESSAGE_UPDATE";
|
||||
data: Message;
|
||||
}
|
||||
|
||||
export interface MessageDeleteEvent extends Event {
|
||||
event: "MESSAGE_DELETE";
|
||||
data: {
|
||||
id: string;
|
||||
channel_id: string;
|
||||
guild_id?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MessageDeleteBulkEvent extends Event {
|
||||
event: "MESSAGE_DELETE_BULK";
|
||||
data: {
|
||||
ids: string[];
|
||||
channel_id: string;
|
||||
guild_id?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MessageReactionAddEvent extends Event {
|
||||
event: "MESSAGE_REACTION_ADD";
|
||||
data: {
|
||||
user_id: string;
|
||||
channel_id: string;
|
||||
message_id: string;
|
||||
guild_id?: string;
|
||||
member?: PublicMember;
|
||||
emoji: PartialEmoji;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MessageReactionRemoveEvent extends Event {
|
||||
event: "MESSAGE_REACTION_REMOVE";
|
||||
data: {
|
||||
user_id: string;
|
||||
channel_id: string;
|
||||
message_id: string;
|
||||
guild_id?: string;
|
||||
emoji: PartialEmoji;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MessageReactionRemoveAllEvent extends Event {
|
||||
event: "MESSAGE_REACTION_REMOVE_ALL";
|
||||
data: {
|
||||
channel_id: string;
|
||||
message_id: string;
|
||||
guild_id?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MessageReactionRemoveEmojiEvent extends Event {
|
||||
event: "MESSAGE_REACTION_REMOVE_EMOJI";
|
||||
data: {
|
||||
channel_id: string;
|
||||
message_id: string;
|
||||
guild_id?: string;
|
||||
emoji: PartialEmoji;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PresenceUpdateEvent extends Event {
|
||||
event: "PRESENCE_UPDATE";
|
||||
data: Presence;
|
||||
}
|
||||
|
||||
export interface TypingStartEvent extends Event {
|
||||
event: "TYPING_START";
|
||||
data: {
|
||||
channel_id: string;
|
||||
user_id: string;
|
||||
timestamp: number;
|
||||
guild_id?: string;
|
||||
member?: PublicMember;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UserUpdateEvent extends Event {
|
||||
event: "USER_UPDATE";
|
||||
data: User;
|
||||
}
|
||||
|
||||
export interface VoiceStateUpdateEvent extends Event {
|
||||
event: "VOICE_STATE_UPDATE";
|
||||
data: VoiceState & {
|
||||
member: PublicMember;
|
||||
};
|
||||
}
|
||||
|
||||
export interface VoiceServerUpdateEvent extends Event {
|
||||
event: "VOICE_SERVER_UPDATE";
|
||||
data: {
|
||||
token: string;
|
||||
guild_id: string;
|
||||
endpoint: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface WebhooksUpdateEvent extends Event {
|
||||
event: "WEBHOOKS_UPDATE";
|
||||
data: {
|
||||
guild_id: string;
|
||||
channel_id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type ApplicationCommandPayload = ApplicationCommand & {
|
||||
guild_id: string;
|
||||
};
|
||||
|
||||
export interface ApplicationCommandCreateEvent extends Event {
|
||||
event: "APPLICATION_COMMAND_CREATE";
|
||||
data: ApplicationCommandPayload;
|
||||
}
|
||||
|
||||
export interface ApplicationCommandUpdateEvent extends Event {
|
||||
event: "APPLICATION_COMMAND_UPDATE";
|
||||
data: ApplicationCommandPayload;
|
||||
}
|
||||
|
||||
export interface ApplicationCommandDeleteEvent extends Event {
|
||||
event: "APPLICATION_COMMAND_DELETE";
|
||||
data: ApplicationCommandPayload;
|
||||
}
|
||||
|
||||
export interface InteractionCreateEvent extends Event {
|
||||
event: "INTERACTION_CREATE";
|
||||
data: Interaction;
|
||||
}
|
||||
|
||||
export interface MessageAckEvent extends Event {
|
||||
event: "MESSAGE_ACK";
|
||||
data: {
|
||||
channel_id: string;
|
||||
message_id: string;
|
||||
version?: number;
|
||||
manual?: boolean;
|
||||
mention_count?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RelationshipAddEvent extends Event {
|
||||
event: "RELATIONSHIP_ADD";
|
||||
data: PublicRelationship & {
|
||||
should_notify?: boolean;
|
||||
user: PublicUser;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RelationshipRemoveEvent extends Event {
|
||||
event: "RELATIONSHIP_REMOVE";
|
||||
data: Omit<PublicRelationship, "nickname">;
|
||||
}
|
||||
|
||||
export interface SessionsReplace extends Event {
|
||||
event: "SESSIONS_REPLACE";
|
||||
data: {
|
||||
activities: Activity[];
|
||||
client_info: {
|
||||
version: number;
|
||||
os: string;
|
||||
client: string;
|
||||
};
|
||||
status: Status;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface GuildMemberListUpdate extends Event {
|
||||
event: "GUILD_MEMBER_LIST_UPDATE";
|
||||
data: {
|
||||
groups: { id: string; count: number }[];
|
||||
guild_id: string;
|
||||
id: string;
|
||||
member_count: number;
|
||||
online_count: number;
|
||||
ops: {
|
||||
index: number;
|
||||
item: {
|
||||
member?: PublicMember & { presence: Presence };
|
||||
group?: { id: string; count: number }[];
|
||||
};
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
export type EventData =
|
||||
| InvalidatedEvent
|
||||
| ReadyEvent
|
||||
| ChannelCreateEvent
|
||||
| ChannelUpdateEvent
|
||||
| ChannelDeleteEvent
|
||||
| ChannelPinsUpdateEvent
|
||||
| ChannelRecipientAddEvent
|
||||
| ChannelRecipientRemoveEvent
|
||||
| GuildCreateEvent
|
||||
| GuildUpdateEvent
|
||||
| GuildDeleteEvent
|
||||
| GuildBanAddEvent
|
||||
| GuildBanRemoveEvent
|
||||
| GuildEmojisUpdateEvent
|
||||
| GuildIntegrationUpdateEvent
|
||||
| GuildMemberAddEvent
|
||||
| GuildMemberRemoveEvent
|
||||
| GuildMemberUpdateEvent
|
||||
| GuildMembersChunkEvent
|
||||
| GuildMemberListUpdate
|
||||
| GuildRoleCreateEvent
|
||||
| GuildRoleUpdateEvent
|
||||
| GuildRoleDeleteEvent
|
||||
| InviteCreateEvent
|
||||
| InviteDeleteEvent
|
||||
| MessageCreateEvent
|
||||
| MessageUpdateEvent
|
||||
| MessageDeleteEvent
|
||||
| MessageDeleteBulkEvent
|
||||
| MessageReactionAddEvent
|
||||
| MessageReactionRemoveEvent
|
||||
| MessageReactionRemoveAllEvent
|
||||
| MessageReactionRemoveEmojiEvent
|
||||
| PresenceUpdateEvent
|
||||
| TypingStartEvent
|
||||
| UserUpdateEvent
|
||||
| VoiceStateUpdateEvent
|
||||
| VoiceServerUpdateEvent
|
||||
| WebhooksUpdateEvent
|
||||
| ApplicationCommandCreateEvent
|
||||
| ApplicationCommandUpdateEvent
|
||||
| ApplicationCommandDeleteEvent
|
||||
| InteractionCreateEvent
|
||||
| MessageAckEvent
|
||||
| RelationshipAddEvent
|
||||
| RelationshipRemoveEvent;
|
||||
|
||||
// located in collection events
|
||||
|
||||
export enum EVENTEnum {
|
||||
Ready = "READY",
|
||||
ChannelCreate = "CHANNEL_CREATE",
|
||||
ChannelUpdate = "CHANNEL_UPDATE",
|
||||
ChannelDelete = "CHANNEL_DELETE",
|
||||
ChannelPinsUpdate = "CHANNEL_PINS_UPDATE",
|
||||
ChannelRecipientAdd = "CHANNEL_RECIPIENT_ADD",
|
||||
ChannelRecipientRemove = "CHANNEL_RECIPIENT_REMOVE",
|
||||
GuildCreate = "GUILD_CREATE",
|
||||
GuildUpdate = "GUILD_UPDATE",
|
||||
GuildDelete = "GUILD_DELETE",
|
||||
GuildBanAdd = "GUILD_BAN_ADD",
|
||||
GuildBanRemove = "GUILD_BAN_REMOVE",
|
||||
GuildEmojUpdate = "GUILD_EMOJI_UPDATE",
|
||||
GuildIntegrationsUpdate = "GUILD_INTEGRATIONS_UPDATE",
|
||||
GuildMemberAdd = "GUILD_MEMBER_ADD",
|
||||
GuildMemberRempve = "GUILD_MEMBER_REMOVE",
|
||||
GuildMemberUpdate = "GUILD_MEMBER_UPDATE",
|
||||
GuildMemberSpeaking = "GUILD_MEMBER_SPEAKING",
|
||||
GuildMembersChunk = "GUILD_MEMBERS_CHUNK",
|
||||
GuildMemberListUpdate = "GUILD_MEMBER_LIST_UPDATE",
|
||||
GuildRoleCreate = "GUILD_ROLE_CREATE",
|
||||
GuildRoleDelete = "GUILD_ROLE_DELETE",
|
||||
GuildRoleUpdate = "GUILD_ROLE_UPDATE",
|
||||
InviteCreate = "INVITE_CREATE",
|
||||
InviteDelete = "INVITE_DELETE",
|
||||
MessageCreate = "MESSAGE_CREATE",
|
||||
MessageUpdate = "MESSAGE_UPDATE",
|
||||
MessageDelete = "MESSAGE_DELETE",
|
||||
MessageDeleteBulk = "MESSAGE_DELETE_BULK",
|
||||
MessageReactionAdd = "MESSAGE_REACTION_ADD",
|
||||
MessageReactionRemove = "MESSAGE_REACTION_REMOVE",
|
||||
MessageReactionRemoveAll = "MESSAGE_REACTION_REMOVE_ALL",
|
||||
MessageReactionRemoveEmoji = "MESSAGE_REACTION_REMOVE_EMOJI",
|
||||
PresenceUpdate = "PRESENCE_UPDATE",
|
||||
TypingStart = "TYPING_START",
|
||||
UserUpdate = "USER_UPDATE",
|
||||
WebhooksUpdate = "WEBHOOKS_UPDATE",
|
||||
InteractionCreate = "INTERACTION_CREATE",
|
||||
VoiceStateUpdate = "VOICE_STATE_UPDATE",
|
||||
VoiceServerUpdate = "VOICE_SERVER_UPDATE",
|
||||
ApplicationCommandCreate = "APPLICATION_COMMAND_CREATE",
|
||||
ApplicationCommandUpdate = "APPLICATION_COMMAND_UPDATE",
|
||||
ApplicationCommandDelete = "APPLICATION_COMMAND_DELETE",
|
||||
SessionsReplace = "SESSIONS_REPLACE",
|
||||
}
|
||||
|
||||
export type EVENT =
|
||||
| "READY"
|
||||
| "CHANNEL_CREATE"
|
||||
| "CHANNEL_UPDATE"
|
||||
| "CHANNEL_DELETE"
|
||||
| "CHANNEL_PINS_UPDATE"
|
||||
| "CHANNEL_RECIPIENT_ADD"
|
||||
| "CHANNEL_RECIPIENT_REMOVE"
|
||||
| "GUILD_CREATE"
|
||||
| "GUILD_UPDATE"
|
||||
| "GUILD_DELETE"
|
||||
| "GUILD_BAN_ADD"
|
||||
| "GUILD_BAN_REMOVE"
|
||||
| "GUILD_EMOJIS_UPDATE"
|
||||
| "GUILD_STICKERS_UPDATE"
|
||||
| "GUILD_INTEGRATIONS_UPDATE"
|
||||
| "GUILD_MEMBER_ADD"
|
||||
| "GUILD_MEMBER_REMOVE"
|
||||
| "GUILD_MEMBER_UPDATE"
|
||||
| "GUILD_MEMBER_SPEAKING"
|
||||
| "GUILD_MEMBERS_CHUNK"
|
||||
| "GUILD_MEMBER_LIST_UPDATE"
|
||||
| "GUILD_ROLE_CREATE"
|
||||
| "GUILD_ROLE_DELETE"
|
||||
| "GUILD_ROLE_UPDATE"
|
||||
| "INVITE_CREATE"
|
||||
| "INVITE_DELETE"
|
||||
| "MESSAGE_CREATE"
|
||||
| "MESSAGE_UPDATE"
|
||||
| "MESSAGE_DELETE"
|
||||
| "MESSAGE_DELETE_BULK"
|
||||
| "MESSAGE_REACTION_ADD"
|
||||
// TODO: add a new event: bulk add reaction:
|
||||
// | "MESSAGE_REACTION_BULK_ADD"
|
||||
| "MESSAGE_REACTION_REMOVE"
|
||||
| "MESSAGE_REACTION_REMOVE_ALL"
|
||||
| "MESSAGE_REACTION_REMOVE_EMOJI"
|
||||
| "PRESENCE_UPDATE"
|
||||
| "TYPING_START"
|
||||
| "USER_UPDATE"
|
||||
| "USER_NOTE_UPDATE"
|
||||
| "WEBHOOKS_UPDATE"
|
||||
| "INTERACTION_CREATE"
|
||||
| "VOICE_STATE_UPDATE"
|
||||
| "VOICE_SERVER_UPDATE"
|
||||
| "APPLICATION_COMMAND_CREATE"
|
||||
| "APPLICATION_COMMAND_UPDATE"
|
||||
| "APPLICATION_COMMAND_DELETE"
|
||||
| "MESSAGE_ACK"
|
||||
| "RELATIONSHIP_ADD"
|
||||
| "RELATIONSHIP_REMOVE"
|
||||
| "SESSIONS_REPLACE"
|
||||
| CUSTOMEVENTS;
|
||||
|
||||
export type CUSTOMEVENTS = "INVALIDATED" | "RATELIMIT";
|
||||
@@ -0,0 +1,34 @@
|
||||
import { AllowedMentions, Embed } from "../entities/Message";
|
||||
|
||||
export interface Interaction {
|
||||
id: string;
|
||||
type: InteractionType;
|
||||
data?: {};
|
||||
guild_id: string;
|
||||
channel_id: string;
|
||||
member_id: string;
|
||||
token: string;
|
||||
version: number;
|
||||
}
|
||||
|
||||
export enum InteractionType {
|
||||
SelfCommand = 0,
|
||||
Ping = 1,
|
||||
ApplicationCommand = 2,
|
||||
}
|
||||
|
||||
export enum InteractionResponseType {
|
||||
SelfCommandResponse = 0,
|
||||
Pong = 1,
|
||||
Acknowledge = 2,
|
||||
ChannelMessage = 3,
|
||||
ChannelMessageWithSource = 4,
|
||||
AcknowledgeWithSource = 5,
|
||||
}
|
||||
|
||||
export interface InteractionApplicationCommandCallbackData {
|
||||
tts?: boolean;
|
||||
content: string;
|
||||
embeds?: Embed[];
|
||||
allowed_mentions?: AllowedMentions;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ClientStatus, Status } from "./Status";
|
||||
import { Activity } from "./Activity";
|
||||
import { PublicUser } from "../entities/User";
|
||||
|
||||
export interface Presence {
|
||||
user: PublicUser;
|
||||
guild_id?: string;
|
||||
status: Status;
|
||||
activities: Activity[];
|
||||
client_status: ClientStatus;
|
||||
// TODO: game
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export type Status = "idle" | "dnd" | "online" | "offline" | "invisible";
|
||||
|
||||
export interface ClientStatus {
|
||||
desktop?: string; // e.g. Windows/Linux/Mac
|
||||
mobile?: string; // e.g. iOS/Android
|
||||
web?: string; // e.g. browser, bot account
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./Activity";
|
||||
export * from "./Presence";
|
||||
export * from "./Interaction";
|
||||
export * from "./Event";
|
||||
export * from "./Status";
|
||||
@@ -0,0 +1,13 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class EmojiRoles1633864260873 implements MigrationInterface {
|
||||
name = "EmojiRoles1633864260873";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "emojis" ADD "roles" text NOT NULL DEFAULT ''`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "emojis" DROP COLUMN column_name "roles"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class EmojiUser1633864669243 implements MigrationInterface {
|
||||
name = "EmojiUser1633864669243";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "emojis" ADD "user_id" varchar`);
|
||||
try {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "emojis" ADD CONSTRAINT FK_fa7ddd5f9a214e28ce596548421 FOREIGN KEY (user_id) REFERENCES users(id)`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"sqlite doesn't support altering foreign keys: https://stackoverflow.com/questions/1884818/how-do-i-add-a-foreign-key-to-an-existing-sqlite-table"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "emojis" DROP COLUMN column_name "user_id"`);
|
||||
await queryRunner.query(`ALTER TABLE "emojis" DROP CONSTRAINT FK_fa7ddd5f9a214e28ce596548421`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class VanityInvite1633881705509 implements MigrationInterface {
|
||||
name = "VanityInvite1633881705509";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
try {
|
||||
await queryRunner.query(`ALTER TABLE "emojis" DROP COLUMN vanity_url_code`);
|
||||
await queryRunner.query(`ALTER TABLE "emojis" DROP CONSTRAINT FK_c2c1809d79eb120ea0cb8d342ad`);
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "emojis" ADD vanity_url_code varchar`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "emojis" ADD CONSTRAINT FK_c2c1809d79eb120ea0cb8d342ad FOREIGN KEY ("vanity_url_code") REFERENCES "invites"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { MigrationInterface, QueryRunner, Table, TableColumn, TableForeignKey } from "typeorm";
|
||||
|
||||
export class Stickers1634308884591 implements MigrationInterface {
|
||||
name = "Stickers1634308884591";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropForeignKey("read_states", "FK_6f255d873cfbfd7a93849b7ff74");
|
||||
await queryRunner.changeColumn(
|
||||
"stickers",
|
||||
"tags",
|
||||
new TableColumn({ name: "tags", type: "varchar", isNullable: true })
|
||||
);
|
||||
await queryRunner.changeColumn(
|
||||
"stickers",
|
||||
"pack_id",
|
||||
new TableColumn({ name: "pack_id", type: "varchar", isNullable: true })
|
||||
);
|
||||
await queryRunner.changeColumn("stickers", "type", new TableColumn({ name: "type", type: "integer" }));
|
||||
await queryRunner.changeColumn(
|
||||
"stickers",
|
||||
"format_type",
|
||||
new TableColumn({ name: "format_type", type: "integer" })
|
||||
);
|
||||
await queryRunner.changeColumn(
|
||||
"stickers",
|
||||
"available",
|
||||
new TableColumn({ name: "available", type: "boolean", isNullable: true })
|
||||
);
|
||||
await queryRunner.changeColumn(
|
||||
"stickers",
|
||||
"user_id",
|
||||
new TableColumn({ name: "user_id", type: "boolean", isNullable: true })
|
||||
);
|
||||
await queryRunner.createForeignKey(
|
||||
"stickers",
|
||||
new TableForeignKey({
|
||||
name: "FK_8f4ee73f2bb2325ff980502e158",
|
||||
columnNames: ["user_id"],
|
||||
referencedColumnNames: ["id"],
|
||||
referencedTableName: "users",
|
||||
onDelete: "CASCADE",
|
||||
})
|
||||
);
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: "sticker_packs",
|
||||
columns: [
|
||||
new TableColumn({ name: "id", type: "varchar", isPrimary: true }),
|
||||
new TableColumn({ name: "name", type: "varchar" }),
|
||||
new TableColumn({ name: "description", type: "varchar", isNullable: true }),
|
||||
new TableColumn({ name: "banner_asset_id", type: "varchar", isNullable: true }),
|
||||
new TableColumn({ name: "cover_sticker_id", type: "varchar", isNullable: true }),
|
||||
],
|
||||
foreignKeys: [
|
||||
new TableForeignKey({
|
||||
columnNames: ["cover_sticker_id"],
|
||||
referencedColumnNames: ["id"],
|
||||
referencedTableName: "stickers",
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
|
||||
|
||||
export class Presence1634424361103 implements MigrationInterface {
|
||||
name = "Presence1634424361103";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
queryRunner.addColumn("sessions", new TableColumn({ name: "activites", type: "text" }));
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
|
||||
|
||||
export class MigrationTimestamp1634426540271 implements MigrationInterface {
|
||||
name = "MigrationTimestamp1634426540271";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.changeColumn(
|
||||
"migrations",
|
||||
"timestamp",
|
||||
new TableColumn({ name: "timestampe", type: "bigint", isNullable: false })
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class ReleaseTypo1648643945733 implements MigrationInterface {
|
||||
name = "ReleaseTypo1648643945733";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
//drop table first because typeorm creates it before migrations run
|
||||
await queryRunner.dropTable("client_release", true);
|
||||
await queryRunner.renameTable("client_relase", "client_release");
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropTable("client_relase", true);
|
||||
await queryRunner.renameTable("client_release", "client_relase");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class opencordFixes1660678870706 implements MigrationInterface {
|
||||
name = 'opencordFixes1660678870706'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE \`users\`
|
||||
ADD \`purchased_flags\` int NOT NULL
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE \`users\`
|
||||
ADD \`premium_usage_flags\` int NOT NULL
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE \`user_settings\`
|
||||
ADD \`friend_discovery_flags\` int NOT NULL
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE \`user_settings\`
|
||||
ADD \`view_nsfw_guilds\` tinyint NOT NULL
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE \`user_settings\`
|
||||
ADD \`passwordless\` tinyint NOT NULL
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE \`users\` CHANGE \`mfa_enabled\` \`mfa_enabled\` tinyint NOT NULL
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE \`users\` CHANGE \`mfa_enabled\` \`mfa_enabled\` tinyint NULL
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE \`user_settings\` DROP COLUMN \`passwordless\`
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE \`user_settings\` DROP COLUMN \`view_nsfw_guilds\`
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE \`user_settings\` DROP COLUMN \`friend_discovery_flags\`
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE \`users\` DROP COLUMN \`premium_usage_flags\`
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE \`users\` DROP COLUMN \`purchased_flags\`
|
||||
`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class mobileFixes21660689892073 implements MigrationInterface {
|
||||
name = 'mobileFixes21660689892073'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE \`user_settings\`
|
||||
ADD \`banner_color\` varchar(255) NULL
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
UPDATE \`channels\` SET \`nsfw\` = 0 WHERE \`nsfw\` = NULL
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE \`channels\` CHANGE \`nsfw\` \`nsfw\` tinyint NOT NULL
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
UPDATE \`guilds\` SET \`nsfw\` = 0 WHERE \`nsfw\` = NULL
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE \`guilds\` CHANGE \`nsfw\` \`nsfw\` tinyint NOT NULL
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE \`guilds\` CHANGE \`nsfw\` \`nsfw\` tinyint NULL
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE \`channels\` CHANGE \`nsfw\` \`nsfw\` tinyint NULL
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE \`user_settings\` DROP COLUMN \`banner_color\`
|
||||
`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import Ajv from "ajv";
|
||||
import addFormats from "ajv-formats";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const SchemaPath = path.join(__dirname, "..", "..", "..", "assets", "schemas.json");
|
||||
const schemas = JSON.parse(fs.readFileSync(SchemaPath, { encoding: "utf8" }));
|
||||
|
||||
export const ajv = new Ajv({
|
||||
allErrors: true,
|
||||
parseDate: true,
|
||||
allowDate: true,
|
||||
schemas,
|
||||
coerceTypes: true,
|
||||
messages: true,
|
||||
strict: true,
|
||||
strictRequired: true
|
||||
});
|
||||
|
||||
addFormats(ajv);
|
||||
|
||||
export function validateSchema<G>(schema: string, data: G): G {
|
||||
const valid = ajv.validate(schema, normalizeBody(data));
|
||||
if (!valid) throw ajv.errors;
|
||||
return data;
|
||||
}
|
||||
|
||||
// Normalizer is introduced to workaround https://github.com/ajv-validator/ajv/issues/1287
|
||||
// this removes null values as ajv doesn't treat them as undefined
|
||||
// normalizeBody allows to handle circular structures without issues
|
||||
// taken from https://github.com/serverless/serverless/blob/master/lib/classes/ConfigSchemaHandler/index.js#L30 (MIT license)
|
||||
export const normalizeBody = (body: any = {}) => {
|
||||
const normalizedObjectsSet = new WeakSet();
|
||||
const normalizeObject = (object: any) => {
|
||||
if (normalizedObjectsSet.has(object)) return;
|
||||
normalizedObjectsSet.add(object);
|
||||
if (Array.isArray(object)) {
|
||||
for (const [index, value] of object.entries()) {
|
||||
if (typeof value === "object") normalizeObject(value);
|
||||
}
|
||||
} else {
|
||||
for (const [key, value] of Object.entries(object)) {
|
||||
if (value == null) {
|
||||
if (key === "icon" || key === "avatar" || key === "banner" || key === "splash" || key === "discovery_splash") continue;
|
||||
delete object[key];
|
||||
} else if (typeof value === "object") {
|
||||
normalizeObject(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
normalizeObject(body);
|
||||
return body;
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./Validator";
|
||||
export * from "./voice";
|
||||
@@ -0,0 +1,69 @@
|
||||
export interface VoiceVideoSchema {
|
||||
audio_ssrc: number;
|
||||
video_ssrc: number;
|
||||
rtx_ssrc?: number;
|
||||
user_id?: string;
|
||||
streams?: {
|
||||
type: "video" | "audio";
|
||||
rid: string;
|
||||
ssrc: number;
|
||||
active: boolean;
|
||||
quality: number;
|
||||
rtx_ssrc: number;
|
||||
max_bitrate: number;
|
||||
max_framerate: number;
|
||||
max_resolution: { type: string; width: number; height: number; };
|
||||
}[];
|
||||
}
|
||||
|
||||
export const VoiceStateUpdateSchema = {
|
||||
$guild_id: String,
|
||||
$channel_id: String,
|
||||
self_mute: Boolean,
|
||||
self_deaf: Boolean,
|
||||
self_video: Boolean
|
||||
};
|
||||
|
||||
//TODO need more testing when community guild and voice stage channel are working
|
||||
export interface VoiceStateUpdateSchema {
|
||||
channel_id: string;
|
||||
guild_id?: string;
|
||||
suppress?: boolean;
|
||||
request_to_speak_timestamp?: Date;
|
||||
self_mute?: boolean;
|
||||
self_deaf?: boolean;
|
||||
self_video?: boolean;
|
||||
}
|
||||
|
||||
export interface VoiceIdentifySchema {
|
||||
server_id: string;
|
||||
user_id: string;
|
||||
session_id: string;
|
||||
token: string;
|
||||
video?: boolean;
|
||||
streams?: {
|
||||
type: string;
|
||||
rid: string;
|
||||
quality: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface SelectProtocolSchema {
|
||||
protocol: "webrtc" | "udp";
|
||||
data:
|
||||
| string
|
||||
| {
|
||||
address: string;
|
||||
port: number;
|
||||
mode: string;
|
||||
};
|
||||
sdp?: string;
|
||||
codecs?: {
|
||||
name: "opus" | "VP8" | "VP9" | "H264";
|
||||
type: "audio" | "video";
|
||||
priority: number;
|
||||
payload_type: number;
|
||||
rtx_payload_type?: number | null;
|
||||
}[];
|
||||
rtc_connection_id?: string; // uuid
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
readonly message: string,
|
||||
public readonly code: number,
|
||||
public readonly httpStatus: number = 400,
|
||||
public readonly defaultParams?: string[]
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
withDefaultParams(): ApiError {
|
||||
if (this.defaultParams)
|
||||
return new ApiError(applyParamsToString(this.message, this.defaultParams), this.code, this.httpStatus);
|
||||
return this;
|
||||
}
|
||||
|
||||
withParams(...params: (string | number)[]): ApiError {
|
||||
return new ApiError(applyParamsToString(this.message, params), this.code, this.httpStatus);
|
||||
}
|
||||
}
|
||||
|
||||
export function applyParamsToString(s: string, params: (string | number)[]): string {
|
||||
let newString = s;
|
||||
params.forEach((a) => {
|
||||
newString = newString.replace("{}", "" + a);
|
||||
});
|
||||
return newString;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function containsAll(arr: any[], target: any[]) {
|
||||
return target.every((v) => arr.includes(v));
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import "missing-native-js-functions";
|
||||
import fetch from "node-fetch";
|
||||
import ProxyAgent from 'proxy-agent';
|
||||
import readline from "readline";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
export function enableAutoUpdate(opts: {
|
||||
checkInterval: number | boolean;
|
||||
packageJsonLink: string;
|
||||
path: string;
|
||||
downloadUrl: string;
|
||||
downloadType?: "zip";
|
||||
}) {
|
||||
if (!opts.checkInterval) return;
|
||||
var interval = 1000 * 60 * 60 * 24;
|
||||
if (typeof opts.checkInterval === "number") opts.checkInterval = 1000 * interval;
|
||||
|
||||
const i = setInterval(async () => {
|
||||
const currentVersion = await getCurrentVersion(opts.path);
|
||||
const latestVersion = await getLatestVersion(opts.packageJsonLink);
|
||||
if (currentVersion !== latestVersion) {
|
||||
clearInterval(i);
|
||||
console.log(`[Auto Update] Current version (${currentVersion}) is out of date, updating ...`);
|
||||
await download(opts.downloadUrl, opts.path);
|
||||
}
|
||||
}, interval);
|
||||
setImmediate(async () => {
|
||||
const currentVersion = await getCurrentVersion(opts.path);
|
||||
const latestVersion = await getLatestVersion(opts.packageJsonLink);
|
||||
if (currentVersion !== latestVersion) {
|
||||
rl.question(
|
||||
`[Auto Update] Current version (${currentVersion}) is out of date, would you like to update? (yes/no)`,
|
||||
(answer) => {
|
||||
if (answer.toBoolean()) {
|
||||
console.log(`[Auto update] updating ...`);
|
||||
download(opts.downloadUrl, opts.path);
|
||||
} else {
|
||||
console.log(`[Auto update] aborted`);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function download(url: string, dir: string) {
|
||||
try {
|
||||
// TODO: use file stream instead of buffer (to prevent crash because of high memory usage for big files)
|
||||
// TODO check file hash
|
||||
const agent = new ProxyAgent();
|
||||
const response = await fetch(url, { agent });
|
||||
const buffer = await response.buffer();
|
||||
const tempDir = await fs.mkdtemp("fosscord");
|
||||
fs.writeFile(path.join(tempDir, "Fosscord.zip"), buffer);
|
||||
} catch (error) {
|
||||
console.error(`[Auto Update] download failed`, error);
|
||||
}
|
||||
}
|
||||
|
||||
async function getCurrentVersion(dir: string) {
|
||||
try {
|
||||
const content = await fs.readFile(path.join(dir, "package.json"), { encoding: "utf8" });
|
||||
return JSON.parse(content).version;
|
||||
} catch (error) {
|
||||
throw new Error("[Auto update] couldn't get current version in " + dir);
|
||||
}
|
||||
}
|
||||
|
||||
async function getLatestVersion(url: string) {
|
||||
try {
|
||||
const agent = new ProxyAgent();
|
||||
const response = await fetch(url, { agent });
|
||||
const content = await response.json() as any; // TODO: types
|
||||
return content.version;
|
||||
} catch (error) {
|
||||
throw new Error("[Auto update] check failed for " + url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
var words: string[];
|
||||
|
||||
export const BannedWords = {
|
||||
init: async function init() {
|
||||
if (words) return words;
|
||||
const file = (await fs.readFile(path.join(process.cwd(), "bannedWords"))).toString();
|
||||
if (!file) {
|
||||
words = [];
|
||||
return [];
|
||||
}
|
||||
words = file.trim().split("\n");
|
||||
return words;
|
||||
},
|
||||
|
||||
get: () => words,
|
||||
|
||||
find: (val: string) => {
|
||||
return words.some(x => val.indexOf(x) != -1);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
"use strict";
|
||||
|
||||
// https://github.com/discordjs/discord.js/blob/master/src/util/BitField.js
|
||||
// Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah
|
||||
|
||||
export type BitFieldResolvable = number | BigInt | BitField | string | BitFieldResolvable[];
|
||||
|
||||
/**
|
||||
* Data structure that makes it easy to interact with a bitfield.
|
||||
*/
|
||||
export class BitField {
|
||||
public bitfield: bigint = BigInt(0);
|
||||
|
||||
public static FLAGS: Record<string, bigint> = {};
|
||||
|
||||
constructor(bits: BitFieldResolvable = 0) {
|
||||
this.bitfield = BitField.resolve.call(this, bits);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the bitfield has a bit, or any of multiple bits.
|
||||
*/
|
||||
any(bit: BitFieldResolvable): boolean {
|
||||
return (this.bitfield & BitField.resolve.call(this, bit)) !== BigInt(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this bitfield equals another
|
||||
*/
|
||||
equals(bit: BitFieldResolvable): boolean {
|
||||
return this.bitfield === BitField.resolve.call(this, bit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the bitfield has a bit, or multiple bits.
|
||||
*/
|
||||
has(bit: BitFieldResolvable): boolean {
|
||||
if (Array.isArray(bit)) return bit.every((p) => this.has(p));
|
||||
const BIT = BitField.resolve.call(this, bit);
|
||||
return (this.bitfield & BIT) === BIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all given bits that are missing from the bitfield.
|
||||
*/
|
||||
missing(bits: BitFieldResolvable) {
|
||||
if (!Array.isArray(bits)) bits = new BitField(bits).toArray();
|
||||
return bits.filter((p) => !this.has(p));
|
||||
}
|
||||
|
||||
/**
|
||||
* Freezes these bits, making them immutable.
|
||||
*/
|
||||
freeze(): Readonly<BitField> {
|
||||
return Object.freeze(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds bits to these ones.
|
||||
* @param {...BitFieldResolvable} [bits] Bits to add
|
||||
* @returns {BitField} These bits or new BitField if the instance is frozen.
|
||||
*/
|
||||
add(...bits: BitFieldResolvable[]): BitField {
|
||||
let total = BigInt(0);
|
||||
for (const bit of bits) {
|
||||
total |= BitField.resolve.call(this, bit);
|
||||
}
|
||||
if (Object.isFrozen(this)) return new BitField(this.bitfield | total);
|
||||
this.bitfield |= total;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes bits from these.
|
||||
* @param {...BitFieldResolvable} [bits] Bits to remove
|
||||
*/
|
||||
remove(...bits: BitFieldResolvable[]) {
|
||||
let total = BigInt(0);
|
||||
for (const bit of bits) {
|
||||
total |= BitField.resolve.call(this, bit);
|
||||
}
|
||||
if (Object.isFrozen(this)) return new BitField(this.bitfield & ~total);
|
||||
this.bitfield &= ~total;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an object mapping field names to a {@link boolean} indicating whether the
|
||||
* bit is available.
|
||||
* @param {...*} hasParams Additional parameters for the has method, if any
|
||||
*/
|
||||
serialize() {
|
||||
const serialized: Record<string, boolean> = {};
|
||||
for (const [flag, bit] of Object.entries(BitField.FLAGS)) serialized[flag] = this.has(bit);
|
||||
return serialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an {@link Array} of bitfield names based on the bits available.
|
||||
*/
|
||||
toArray(): string[] {
|
||||
return Object.keys(BitField.FLAGS).filter((bit) => this.has(bit));
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return this.bitfield;
|
||||
}
|
||||
|
||||
valueOf() {
|
||||
return this.bitfield;
|
||||
}
|
||||
|
||||
*[Symbol.iterator]() {
|
||||
yield* this.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Data that can be resolved to give a bitfield. This can be:
|
||||
* * A bit number (this can be a number literal or a value taken from {@link BitField.FLAGS})
|
||||
* * An instance of BitField
|
||||
* * An Array of BitFieldResolvable
|
||||
* @typedef {number|BitField|BitFieldResolvable[]} BitFieldResolvable
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves bitfields to their numeric form.
|
||||
* @param {BitFieldResolvable} [bit=0] - bit(s) to resolve
|
||||
* @returns {number}
|
||||
*/
|
||||
static resolve(bit: BitFieldResolvable = BigInt(0)): bigint {
|
||||
// @ts-ignore
|
||||
const FLAGS = this.FLAGS || this.constructor?.FLAGS;
|
||||
if ((typeof bit === "number" || typeof bit === "bigint") && bit >= BigInt(0)) return BigInt(bit);
|
||||
if (bit instanceof BitField) return bit.bitfield;
|
||||
if (Array.isArray(bit)) {
|
||||
// @ts-ignore
|
||||
const resolve = this.constructor?.resolve || this.resolve;
|
||||
return bit.map((p) => resolve.call(this, p)).reduce((prev, p) => BigInt(prev) | BigInt(p), BigInt(0));
|
||||
}
|
||||
if (typeof bit === "string" && typeof FLAGS[bit] !== "undefined") return FLAGS[bit];
|
||||
throw new RangeError("BITFIELD_INVALID: " + bit);
|
||||
}
|
||||
}
|
||||
|
||||
export function BitFlag(x: bigint | number) {
|
||||
return BigInt(1) << BigInt(x);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
//TODO: populate default discord categories + init, get and set methods
|
||||
@@ -0,0 +1,81 @@
|
||||
import "missing-native-js-functions";
|
||||
import { ConfigValue, ConfigEntity, DefaultConfigOptions } from "../entities/Config";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
// TODO: yaml instead of json
|
||||
// const overridePath = path.join(process.cwd(), "config.json");
|
||||
|
||||
var config: ConfigValue;
|
||||
var pairs: ConfigEntity[];
|
||||
|
||||
// TODO: use events to inform about config updates
|
||||
// Config keys are separated with _
|
||||
|
||||
export const Config = {
|
||||
init: async function init() {
|
||||
if (config) return config;
|
||||
pairs = await ConfigEntity.find();
|
||||
config = pairsToConfig(pairs);
|
||||
config = (config || {}).merge(DefaultConfigOptions);
|
||||
|
||||
// try {
|
||||
// const overrideConfig = JSON.parse(fs.readFileSync(overridePath, { encoding: "utf8" }));
|
||||
// config = overrideConfig.merge(config);
|
||||
// } catch (error) {
|
||||
// fs.writeFileSync(overridePath, JSON.stringify(config, null, 4));
|
||||
// }
|
||||
|
||||
return this.set(config);
|
||||
},
|
||||
get: function get() {
|
||||
return config;
|
||||
},
|
||||
set: function set(val: Partial<ConfigValue>) {
|
||||
if (!config || !val) return;
|
||||
config = val.merge(config);
|
||||
|
||||
return applyConfig(config);
|
||||
},
|
||||
};
|
||||
|
||||
function applyConfig(val: ConfigValue) {
|
||||
async function apply(obj: any, key = ""): Promise<any> {
|
||||
if (typeof obj === "object" && obj !== null)
|
||||
return Promise.all(Object.keys(obj).map((k) => apply(obj[k], key ? `${key}_${k}` : k)));
|
||||
|
||||
let pair = pairs.find((x) => x.key === key);
|
||||
if (!pair) pair = new ConfigEntity();
|
||||
|
||||
pair.key = key;
|
||||
pair.value = obj;
|
||||
return pair.save();
|
||||
}
|
||||
// fs.writeFileSync(overridePath, JSON.stringify(val, null, 4));
|
||||
|
||||
return apply(val);
|
||||
}
|
||||
|
||||
function pairsToConfig(pairs: ConfigEntity[]) {
|
||||
var value: any = {};
|
||||
|
||||
pairs.forEach((p) => {
|
||||
const keys = p.key.split("_");
|
||||
let obj = value;
|
||||
let prev = "";
|
||||
let prevObj = obj;
|
||||
let i = 0;
|
||||
|
||||
for (const key of keys) {
|
||||
if (!isNaN(Number(key)) && !prevObj[prev]?.length) prevObj[prev] = obj = [];
|
||||
if (i++ === keys.length - 1) obj[key] = p.value;
|
||||
else if (!obj[key]) obj[key] = {};
|
||||
|
||||
prev = key;
|
||||
prevObj = obj;
|
||||
obj = obj[key];
|
||||
}
|
||||
});
|
||||
|
||||
return value as ConfigValue;
|
||||
}
|
||||
@@ -0,0 +1,796 @@
|
||||
import { ApiError } from "./ApiError";
|
||||
|
||||
export const WSCodes = {
|
||||
1000: "WS_CLOSE_REQUESTED",
|
||||
4004: "TOKEN_INVALID",
|
||||
4010: "SHARDING_INVALID",
|
||||
4011: "SHARDING_REQUIRED",
|
||||
4013: "INVALID_INTENTS",
|
||||
4014: "DISALLOWED_INTENTS",
|
||||
};
|
||||
|
||||
/**
|
||||
* The current status of the client. Here are the available statuses:
|
||||
* * READY: 0
|
||||
* * CONNECTING: 1
|
||||
* * RECONNECTING: 2
|
||||
* * IDLE: 3
|
||||
* * NEARLY: 4
|
||||
* * DISCONNECTED: 5
|
||||
* * WAITING_FOR_GUILDS: 6
|
||||
* * IDENTIFYING: 7
|
||||
* * RESUMING: 8
|
||||
* @typedef {number} Status
|
||||
*/
|
||||
export const WsStatus = {
|
||||
READY: 0,
|
||||
CONNECTING: 1,
|
||||
RECONNECTING: 2,
|
||||
IDLE: 3,
|
||||
NEARLY: 4,
|
||||
DISCONNECTED: 5,
|
||||
WAITING_FOR_GUILDS: 6,
|
||||
IDENTIFYING: 7,
|
||||
RESUMING: 8,
|
||||
};
|
||||
|
||||
/**
|
||||
* The current status of a voice connection. Here are the available statuses:
|
||||
* * CONNECTED: 0
|
||||
* * CONNECTING: 1
|
||||
* * AUTHENTICATING: 2
|
||||
* * RECONNECTING: 3
|
||||
* * DISCONNECTED: 4
|
||||
* @typedef {number} VoiceStatus
|
||||
*/
|
||||
export const VoiceStatus = {
|
||||
CONNECTED: 0,
|
||||
CONNECTING: 1,
|
||||
AUTHENTICATING: 2,
|
||||
RECONNECTING: 3,
|
||||
DISCONNECTED: 4,
|
||||
};
|
||||
|
||||
export const OPCodes = {
|
||||
DISPATCH: 0,
|
||||
HEARTBEAT: 1,
|
||||
IDENTIFY: 2,
|
||||
STATUS_UPDATE: 3,
|
||||
VOICE_STATE_UPDATE: 4,
|
||||
VOICE_GUILD_PING: 5,
|
||||
RESUME: 6,
|
||||
RECONNECT: 7,
|
||||
REQUEST_GUILD_MEMBERS: 8,
|
||||
INVALID_SESSION: 9,
|
||||
HELLO: 10,
|
||||
HEARTBEAT_ACK: 11,
|
||||
};
|
||||
|
||||
export const VoiceOPCodes = {
|
||||
IDENTIFY: 0,
|
||||
SELECT_PROTOCOL: 1,
|
||||
READY: 2,
|
||||
HEARTBEAT: 3,
|
||||
SESSION_DESCRIPTION: 4,
|
||||
SPEAKING: 5,
|
||||
HEARTBEAT_ACK: 6,
|
||||
RESUME: 7,
|
||||
HELLO: 8,
|
||||
RESUMED: 9,
|
||||
CLIENT_CONNECT: 12, // incorrect, op 12 is probably used for video
|
||||
CLIENT_DISCONNECT: 13, // incorrect
|
||||
VERSION: 16, //not documented
|
||||
};
|
||||
|
||||
export const Events = {
|
||||
RATE_LIMIT: "rateLimit",
|
||||
CLIENT_READY: "ready",
|
||||
GUILD_CREATE: "guildCreate",
|
||||
GUILD_DELETE: "guildDelete",
|
||||
GUILD_UPDATE: "guildUpdate",
|
||||
GUILD_UNAVAILABLE: "guildUnavailable",
|
||||
GUILD_AVAILABLE: "guildAvailable",
|
||||
GUILD_MEMBER_ADD: "guildMemberAdd",
|
||||
GUILD_MEMBER_REMOVE: "guildMemberRemove",
|
||||
GUILD_MEMBER_UPDATE: "guildMemberUpdate",
|
||||
GUILD_MEMBER_AVAILABLE: "guildMemberAvailable",
|
||||
GUILD_MEMBER_SPEAKING: "guildMemberSpeaking",
|
||||
GUILD_MEMBERS_CHUNK: "guildMembersChunk",
|
||||
GUILD_INTEGRATIONS_UPDATE: "guildIntegrationsUpdate",
|
||||
GUILD_ROLE_CREATE: "roleCreate",
|
||||
GUILD_ROLE_DELETE: "roleDelete",
|
||||
INVITE_CREATE: "inviteCreate",
|
||||
INVITE_DELETE: "inviteDelete",
|
||||
GUILD_ROLE_UPDATE: "roleUpdate",
|
||||
GUILD_EMOJI_CREATE: "emojiCreate",
|
||||
GUILD_EMOJI_DELETE: "emojiDelete",
|
||||
GUILD_EMOJI_UPDATE: "emojiUpdate",
|
||||
GUILD_BAN_ADD: "guildBanAdd",
|
||||
GUILD_BAN_REMOVE: "guildBanRemove",
|
||||
CHANNEL_CREATE: "channelCreate",
|
||||
CHANNEL_DELETE: "channelDelete",
|
||||
CHANNEL_UPDATE: "channelUpdate",
|
||||
CHANNEL_PINS_UPDATE: "channelPinsUpdate",
|
||||
MESSAGE_CREATE: "message",
|
||||
MESSAGE_DELETE: "messageDelete",
|
||||
MESSAGE_UPDATE: "messageUpdate",
|
||||
MESSAGE_BULK_DELETE: "messageDeleteBulk",
|
||||
MESSAGE_REACTION_ADD: "messageReactionAdd",
|
||||
MESSAGE_REACTION_REMOVE: "messageReactionRemove",
|
||||
MESSAGE_REACTION_REMOVE_ALL: "messageReactionRemoveAll",
|
||||
MESSAGE_REACTION_REMOVE_EMOJI: "messageReactionRemoveEmoji",
|
||||
USER_UPDATE: "userUpdate",
|
||||
PRESENCE_UPDATE: "presenceUpdate",
|
||||
VOICE_SERVER_UPDATE: "voiceServerUpdate",
|
||||
VOICE_STATE_UPDATE: "voiceStateUpdate",
|
||||
VOICE_BROADCAST_SUBSCRIBE: "subscribe",
|
||||
VOICE_BROADCAST_UNSUBSCRIBE: "unsubscribe",
|
||||
TYPING_START: "typingStart",
|
||||
TYPING_STOP: "typingStop",
|
||||
WEBHOOKS_UPDATE: "webhookUpdate",
|
||||
ERROR: "error",
|
||||
WARN: "warn",
|
||||
DEBUG: "debug",
|
||||
SHARD_DISCONNECT: "shardDisconnect",
|
||||
SHARD_ERROR: "shardError",
|
||||
SHARD_RECONNECTING: "shardReconnecting",
|
||||
SHARD_READY: "shardReady",
|
||||
SHARD_RESUME: "shardResume",
|
||||
INVALIDATED: "invalidated",
|
||||
RAW: "raw",
|
||||
};
|
||||
|
||||
export const ShardEvents = {
|
||||
CLOSE: "close",
|
||||
DESTROYED: "destroyed",
|
||||
INVALID_SESSION: "invalidSession",
|
||||
READY: "ready",
|
||||
RESUMED: "resumed",
|
||||
ALL_READY: "allReady",
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of Structure allowed to be a partial:
|
||||
* * USER
|
||||
* * CHANNEL (only affects DMChannels)
|
||||
* * GUILD_MEMBER
|
||||
* * MESSAGE
|
||||
* * REACTION
|
||||
* <warn>Partials require you to put checks in place when handling data, read the Partials topic listed in the
|
||||
* sidebar for more information.</warn>
|
||||
* @typedef {string} PartialType
|
||||
*/
|
||||
export const PartialTypes = keyMirror(["USER", "CHANNEL", "GUILD_MEMBER", "MESSAGE", "REACTION"]);
|
||||
|
||||
/**
|
||||
* The type of a websocket message event, e.g. `MESSAGE_CREATE`. Here are the available events:
|
||||
* * READY
|
||||
* * RESUMED
|
||||
* * GUILD_CREATE
|
||||
* * GUILD_DELETE
|
||||
* * GUILD_UPDATE
|
||||
* * INVITE_CREATE
|
||||
* * INVITE_DELETE
|
||||
* * GUILD_MEMBER_ADD
|
||||
* * GUILD_MEMBER_REMOVE
|
||||
* * GUILD_MEMBER_UPDATE
|
||||
* * GUILD_MEMBERS_CHUNK
|
||||
* * GUILD_INTEGRATIONS_UPDATE
|
||||
* * GUILD_ROLE_CREATE
|
||||
* * GUILD_ROLE_DELETE
|
||||
* * GUILD_ROLE_UPDATE
|
||||
* * GUILD_BAN_ADD
|
||||
* * GUILD_BAN_REMOVE
|
||||
* * GUILD_EMOJIS_UPDATE
|
||||
* * CHANNEL_CREATE
|
||||
* * CHANNEL_DELETE
|
||||
* * CHANNEL_UPDATE
|
||||
* * CHANNEL_PINS_UPDATE
|
||||
* * MESSAGE_CREATE
|
||||
* * MESSAGE_DELETE
|
||||
* * MESSAGE_UPDATE
|
||||
* * MESSAGE_DELETE_BULK
|
||||
* * MESSAGE_REACTION_ADD
|
||||
* * MESSAGE_REACTION_REMOVE
|
||||
* * MESSAGE_REACTION_REMOVE_ALL
|
||||
* * MESSAGE_REACTION_REMOVE_EMOJI
|
||||
* * USER_UPDATE
|
||||
* * PRESENCE_UPDATE
|
||||
* * TYPING_START
|
||||
* * VOICE_STATE_UPDATE
|
||||
* * VOICE_SERVER_UPDATE
|
||||
* * WEBHOOKS_UPDATE
|
||||
* @typedef {string} WSEventType
|
||||
*/
|
||||
export const WSEvents = keyMirror([
|
||||
"READY",
|
||||
"RESUMED",
|
||||
"GUILD_CREATE",
|
||||
"GUILD_DELETE",
|
||||
"GUILD_UPDATE",
|
||||
"INVITE_CREATE",
|
||||
"INVITE_DELETE",
|
||||
"GUILD_MEMBER_ADD",
|
||||
"GUILD_MEMBER_REMOVE",
|
||||
"GUILD_MEMBER_UPDATE",
|
||||
"GUILD_MEMBERS_CHUNK",
|
||||
"GUILD_INTEGRATIONS_UPDATE",
|
||||
"GUILD_ROLE_CREATE",
|
||||
"GUILD_ROLE_DELETE",
|
||||
"GUILD_ROLE_UPDATE",
|
||||
"GUILD_BAN_ADD",
|
||||
"GUILD_BAN_REMOVE",
|
||||
"GUILD_EMOJIS_UPDATE",
|
||||
"CHANNEL_CREATE",
|
||||
"CHANNEL_DELETE",
|
||||
"CHANNEL_UPDATE",
|
||||
"CHANNEL_PINS_UPDATE",
|
||||
"MESSAGE_CREATE",
|
||||
"MESSAGE_DELETE",
|
||||
"MESSAGE_UPDATE",
|
||||
"MESSAGE_DELETE_BULK",
|
||||
"MESSAGE_REACTION_ADD",
|
||||
"MESSAGE_REACTION_REMOVE",
|
||||
"MESSAGE_REACTION_REMOVE_ALL",
|
||||
"MESSAGE_REACTION_REMOVE_EMOJI",
|
||||
"USER_UPDATE",
|
||||
"PRESENCE_UPDATE",
|
||||
"TYPING_START",
|
||||
"VOICE_STATE_UPDATE",
|
||||
"VOICE_SERVER_UPDATE",
|
||||
"WEBHOOKS_UPDATE",
|
||||
]);
|
||||
|
||||
/**
|
||||
* The type of a message, e.g. `DEFAULT`. Here are the available types:
|
||||
* * DEFAULT
|
||||
* * RECIPIENT_ADD
|
||||
* * RECIPIENT_REMOVE
|
||||
* * CALL
|
||||
* * CHANNEL_NAME_CHANGE
|
||||
* * CHANNEL_ICON_CHANGE
|
||||
* * PINS_ADD
|
||||
* * GUILD_MEMBER_JOIN
|
||||
* * USER_PREMIUM_GUILD_SUBSCRIPTION
|
||||
* * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1
|
||||
* * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2
|
||||
* * USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3
|
||||
* * CHANNEL_FOLLOW_ADD
|
||||
* * GUILD_DISCOVERY_DISQUALIFIED
|
||||
* * GUILD_DISCOVERY_REQUALIFIED
|
||||
* * REPLY
|
||||
* @typedef {string} MessageType
|
||||
*/
|
||||
export const MessageTypes = [
|
||||
"DEFAULT",
|
||||
"RECIPIENT_ADD",
|
||||
"RECIPIENT_REMOVE",
|
||||
"CALL",
|
||||
"CHANNEL_NAME_CHANGE",
|
||||
"CHANNEL_ICON_CHANGE",
|
||||
"PINS_ADD",
|
||||
"GUILD_MEMBER_JOIN",
|
||||
"USER_PREMIUM_GUILD_SUBSCRIPTION",
|
||||
"USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1",
|
||||
"USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2",
|
||||
"USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3",
|
||||
"CHANNEL_FOLLOW_ADD",
|
||||
null,
|
||||
"GUILD_DISCOVERY_DISQUALIFIED",
|
||||
"GUILD_DISCOVERY_REQUALIFIED",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"REPLY",
|
||||
];
|
||||
|
||||
/**
|
||||
* The types of messages that are `System`. The available types are `MessageTypes` excluding:
|
||||
* * DEFAULT
|
||||
* * REPLY
|
||||
* @typedef {string} SystemMessageType
|
||||
*/
|
||||
export const SystemMessageTypes = MessageTypes.filter(
|
||||
(type: string | null) => type && type !== "DEFAULT" && type !== "REPLY"
|
||||
);
|
||||
|
||||
/**
|
||||
* <info>Bots cannot set a `CUSTOM_STATUS`, it is only for custom statuses received from users</info>
|
||||
* The type of an activity of a users presence, e.g. `PLAYING`. Here are the available types:
|
||||
* * PLAYING
|
||||
* * STREAMING
|
||||
* * LISTENING
|
||||
* * WATCHING
|
||||
* * CUSTOM_STATUS
|
||||
* * COMPETING
|
||||
* @typedef {string} ActivityType
|
||||
*/
|
||||
export const ActivityTypes = ["PLAYING", "STREAMING", "LISTENING", "WATCHING", "CUSTOM_STATUS", "COMPETING"];
|
||||
|
||||
export const ChannelTypes = {
|
||||
TEXT: 0,
|
||||
DM: 1,
|
||||
VOICE: 2,
|
||||
GROUP: 3,
|
||||
CATEGORY: 4,
|
||||
NEWS: 5,
|
||||
STORE: 6,
|
||||
};
|
||||
|
||||
export const ClientApplicationAssetTypes = {
|
||||
SMALL: 1,
|
||||
BIG: 2,
|
||||
};
|
||||
|
||||
export const Colors = {
|
||||
DEFAULT: 0x000000,
|
||||
WHITE: 0xffffff,
|
||||
AQUA: 0x1abc9c,
|
||||
GREEN: 0x2ecc71,
|
||||
BLUE: 0x3498db,
|
||||
YELLOW: 0xffff00,
|
||||
PURPLE: 0x9b59b6,
|
||||
LUMINOUS_VIVID_PINK: 0xe91e63,
|
||||
GOLD: 0xf1c40f,
|
||||
ORANGE: 0xe67e22,
|
||||
RED: 0xe74c3c,
|
||||
GREY: 0x95a5a6,
|
||||
NAVY: 0x34495e,
|
||||
DARK_AQUA: 0x11806a,
|
||||
DARK_GREEN: 0x1f8b4c,
|
||||
DARK_BLUE: 0x206694,
|
||||
DARK_PURPLE: 0x71368a,
|
||||
DARK_VIVID_PINK: 0xad1457,
|
||||
DARK_GOLD: 0xc27c0e,
|
||||
DARK_ORANGE: 0xa84300,
|
||||
DARK_RED: 0x992d22,
|
||||
DARK_GREY: 0x979c9f,
|
||||
DARKER_GREY: 0x7f8c8d,
|
||||
LIGHT_GREY: 0xbcc0c0,
|
||||
DARK_NAVY: 0x2c3e50,
|
||||
BLURPLE: 0x7289da,
|
||||
GREYPLE: 0x99aab5,
|
||||
DARK_BUT_NOT_BLACK: 0x2c2f33,
|
||||
NOT_QUITE_BLACK: 0x23272a,
|
||||
};
|
||||
|
||||
/**
|
||||
* The value set for the explicit content filter levels for a guild:
|
||||
* * DISABLED
|
||||
* * MEMBERS_WITHOUT_ROLES
|
||||
* * ALL_MEMBERS
|
||||
* @typedef {string} ExplicitContentFilterLevel
|
||||
*/
|
||||
export const ExplicitContentFilterLevels = ["DISABLED", "MEMBERS_WITHOUT_ROLES", "ALL_MEMBERS"];
|
||||
|
||||
/**
|
||||
* The value set for the verification levels for a guild:
|
||||
* * NONE
|
||||
* * LOW
|
||||
* * MEDIUM
|
||||
* * HIGH
|
||||
* * VERY_HIGH
|
||||
* @typedef {string} VerificationLevel
|
||||
*/
|
||||
export const VerificationLevels = ["NONE", "LOW", "MEDIUM", "HIGH", "VERY_HIGH"];
|
||||
|
||||
/**
|
||||
* An error encountered while performing an API request. Here are the potential errors:
|
||||
* * GENERAL_ERROR
|
||||
* * UNKNOWN_ACCOUNT
|
||||
* * UNKNOWN_APPLICATION
|
||||
* * UNKNOWN_CHANNEL
|
||||
* * UNKNOWN_GUILD
|
||||
* * UNKNOWN_INTEGRATION
|
||||
* * UNKNOWN_INVITE
|
||||
* * UNKNOWN_MEMBER
|
||||
* * UNKNOWN_MESSAGE
|
||||
* * UNKNOWN_OVERWRITE
|
||||
* * UNKNOWN_PROVIDER
|
||||
* * UNKNOWN_ROLE
|
||||
* * UNKNOWN_TOKEN
|
||||
* * UNKNOWN_USER
|
||||
* * UNKNOWN_EMOJI
|
||||
* * UNKNOWN_WEBHOOK
|
||||
* * UNKNOWN_WEBHOOK_SERVICE
|
||||
* * UNKNOWN_SESSION
|
||||
* * UNKNOWN_BAN
|
||||
* * UNKNOWN_SKU
|
||||
* * UNKNOWN_STORE_LISTING
|
||||
* * UNKNOWN_ENTITLEMENT
|
||||
* * UNKNOWN_BUILD
|
||||
* * UNKNOWN_LOBBY
|
||||
* * UNKNOWN_BRANCH
|
||||
* * UNKNOWN_STORE_DIRECTORY_LAYOUT
|
||||
* * UNKNOWN_REDISTRIBUTABLE
|
||||
* * UNKNOWN_GIFT_CODE
|
||||
* * UNKNOWN_STREAM
|
||||
* * UNKNOWN_PREMIUM_SERVER_SUBSCRIBE_COOLDOWN
|
||||
* * UNKNOWN_GUILD_TEMPLATE
|
||||
* * UNKNOWN_DISCOVERABLE_SERVER_CATEGORY
|
||||
* * UNKNOWN_STICKER
|
||||
* * UNKNOWN_INTERACTION
|
||||
* * UNKNOWN_APPLICATION_COMMAND
|
||||
* * UNKNOWN_APPLICATION_COMMAND_PERMISSIONS
|
||||
* * UNKNOWN_STAGE_INSTANCE
|
||||
* * UNKNOWN_GUILD_MEMBER_VERIFICATION_FORM
|
||||
* * UNKNOWN_GUILD_WELCOME_SCREEN
|
||||
* * UNKNOWN_GUILD_SCHEDULED_EVENT
|
||||
* * UNKNOWN_GUILD_SCHEDULED_EVENT_USER
|
||||
* * BOT_PROHIBITED_ENDPOINT
|
||||
* * BOT_ONLY_ENDPOINT
|
||||
* * EXPLICIT_CONTENT_CANNOT_BE_SENT_TO_RECIPIENT
|
||||
* * ACTION_NOT_AUTHORIZED_ON_APPLICATION
|
||||
* * SLOWMODE_RATE_LIMIT
|
||||
* * ONLY_OWNER
|
||||
* * ANNOUNCEMENT_RATE_LIMITS
|
||||
* * CHANNEL_WRITE_RATELIMIT
|
||||
* * WORDS_NOT_ALLOWED
|
||||
* * GUILD_PREMIUM_LEVEL_TOO_LOW
|
||||
* * MAXIMUM_GUILDS
|
||||
* * MAXIMUM_FRIENDS
|
||||
* * MAXIMUM_PINS
|
||||
* * MAXIMUM_NUMBER_OF_RECIPIENTS_REACHED
|
||||
* * MAXIMUM_ROLES
|
||||
* * MAXIMUM_WEBHOOKS
|
||||
* * MAXIMUM_NUMBER_OF_EMOJIS_REACHED
|
||||
* * MAXIMUM_REACTIONS
|
||||
* * MAXIMUM_CHANNELS
|
||||
* * MAXIMUM_ATTACHMENTS
|
||||
* * MAXIMUM_INVITES
|
||||
* * MAXIMUM_ANIMATED_EMOJIS
|
||||
* * MAXIMUM_SERVER_MEMBERS
|
||||
* * MAXIMUM_SERVER_CATEGORIES
|
||||
* * GUILD_ALREADY_HAS_TEMPLATE
|
||||
* * MAXIMUM_THREAD_PARTICIPANTS
|
||||
* * MAXIMUM_BANS_FOR_NON_GUILD_MEMBERS
|
||||
* * MAXIMUM_BANS_FETCHES
|
||||
* * MAXIMUM_STICKERS
|
||||
* * MAXIMUM_PRUNE_REQUESTS
|
||||
* * UNAUTHORIZED
|
||||
* * ACCOUNT_VERIFICATION_REQUIRED
|
||||
* * OPENING_DIRECT_MESSAGES_TOO_FAST
|
||||
* * REQUEST_ENTITY_TOO_LARGE
|
||||
* * FEATURE_TEMPORARILY_DISABLED
|
||||
* * USER_BANNED
|
||||
* * TARGET_USER_IS_NOT_CONNECTED_TO_VOICE
|
||||
* * ALREADY_CROSSPOSTED
|
||||
* * APPLICATION_COMMAND_ALREADY_EXISTS
|
||||
* * MISSING_ACCESS
|
||||
* * INVALID_ACCOUNT_TYPE
|
||||
* * CANNOT_EXECUTE_ON_DM
|
||||
* * EMBED_DISABLED
|
||||
* * CANNOT_EDIT_MESSAGE_BY_OTHER
|
||||
* * CANNOT_SEND_EMPTY_MESSAGE
|
||||
* * CANNOT_MESSAGE_USER
|
||||
* * CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL
|
||||
* * CHANNEL_VERIFICATION_LEVEL_TOO_HIGH
|
||||
* * OAUTH2_APPLICATION_BOT_ABSENT
|
||||
* * MAXIMUM_OAUTH2_APPLICATIONS
|
||||
* * INVALID_OAUTH_STATE
|
||||
* * MISSING_PERMISSIONS
|
||||
* * INVALID_AUTHENTICATION_TOKEN
|
||||
* * NOTE_TOO_LONG
|
||||
* * INVALID_BULK_DELETE_QUANTITY
|
||||
* * CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL
|
||||
* * INVALID_OR_TAKEN_INVITE_CODE
|
||||
* * CANNOT_EXECUTE_ON_SYSTEM_MESSAGE
|
||||
* * CANNOT_EXECUTE_ON_THIS_CHANNEL_TYPE
|
||||
* * INVALID_OAUTH_TOKEN
|
||||
* * MISSING_REQUIRED_OAUTH2_SCOPE
|
||||
* * INVALID_WEBHOOK_TOKEN_PROVIDED
|
||||
* * INVALID_ROLE
|
||||
* * INVALID_RECIPIENT
|
||||
* * BULK_DELETE_MESSAGE_TOO_OLD
|
||||
* * INVALID_FORM_BODY
|
||||
* * INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT
|
||||
* * INVALID_API_VERSION
|
||||
* * FILE_EXCEEDS_MAXIMUM_SIZE
|
||||
* * INVALID_FILE_UPLOADED
|
||||
* * CANNOT_SELF_REDEEM_GIFT
|
||||
* * PAYMENT_SOURCE_REQUIRED
|
||||
* * CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL
|
||||
* * INVALID_STICKER_SENT
|
||||
* * CANNOT_EDIT_ARCHIVED_THREAD
|
||||
* * INVALID_THREAD_NOTIFICATION_SETTINGS
|
||||
* * BEFORE_EARLIER_THAN_THREAD_CREATION_DATE
|
||||
* * SERVER_NOT_AVAILABLE_IN_YOUR_LOCATION
|
||||
* * SERVER_NEEDS_MONETIZATION_ENABLED
|
||||
* * TWO_FACTOR_REQUIRED
|
||||
* * NO_USERS_WITH_DISCORDTAG_EXIST
|
||||
* * REACTION_BLOCKED
|
||||
* * RESOURCE_OVERLOADED
|
||||
* * STAGE_ALREADY_OPEN
|
||||
* * THREAD_ALREADY_CREATED_FOR_THIS_MESSAGE
|
||||
* * THREAD_IS_LOCKED
|
||||
* * MAXIMUM_NUMBER_OF_ACTIVE_THREADS
|
||||
* * MAXIMUM_NUMBER_OF_ACTIVE_ANNOUNCEMENT_THREADS
|
||||
* * INVALID_JSON_FOR_UPLOADED_LOTTIE_FILE
|
||||
* * LOTTIES_CANNOT_CONTAIN_RASTERIZED_IMAGES
|
||||
* * STICKER_MAXIMUM_FRAMERATE
|
||||
* * STICKER_MAXIMUM_FRAME_COUNT
|
||||
* * LOTTIE_ANIMATION_MAXIMUM_DIMENSIONS
|
||||
* * STICKER_FRAME_RATE_TOO_SMALL_OR_TOO_LARGE
|
||||
* * STICKER_ANIMATION_DURATION_MAXIMUM
|
||||
* * UNKNOWN_VOICE_STATE
|
||||
* @typedef {string} APIError
|
||||
*/
|
||||
export const DiscordApiErrors = {
|
||||
//https://discord.com/developers/docs/topics/opcodes-and-status-codes#json-json-error-codes
|
||||
GENERAL_ERROR: new ApiError("General error (such as a malformed request body, amongst other things)", 0),
|
||||
UNKNOWN_ACCOUNT: new ApiError("Unknown account", 10001),
|
||||
UNKNOWN_APPLICATION: new ApiError("Unknown application", 10002),
|
||||
UNKNOWN_CHANNEL: new ApiError("Unknown channel", 10003),
|
||||
UNKNOWN_GUILD: new ApiError("Unknown guild", 10004),
|
||||
UNKNOWN_INTEGRATION: new ApiError("Unknown integration", 10005),
|
||||
UNKNOWN_INVITE: new ApiError("Unknown invite", 10006),
|
||||
UNKNOWN_MEMBER: new ApiError("Unknown member", 10007),
|
||||
UNKNOWN_MESSAGE: new ApiError("Unknown message", 10008),
|
||||
UNKNOWN_OVERWRITE: new ApiError("Unknown permission overwrite", 10009),
|
||||
UNKNOWN_PROVIDER: new ApiError("Unknown provider", 10010),
|
||||
UNKNOWN_ROLE: new ApiError("Unknown role", 10011),
|
||||
UNKNOWN_TOKEN: new ApiError("Unknown token", 10012),
|
||||
UNKNOWN_USER: new ApiError("Unknown user", 10013),
|
||||
UNKNOWN_EMOJI: new ApiError("Unknown emoji", 10014),
|
||||
UNKNOWN_WEBHOOK: new ApiError("Unknown webhook", 10015),
|
||||
UNKNOWN_WEBHOOK_SERVICE: new ApiError("Unknown webhook service", 10016),
|
||||
UNKNOWN_SESSION: new ApiError("Unknown session", 10020),
|
||||
UNKNOWN_BAN: new ApiError("Unknown ban", 10026),
|
||||
UNKNOWN_SKU: new ApiError("Unknown SKU", 10027),
|
||||
UNKNOWN_STORE_LISTING: new ApiError("Unknown Store Listing", 10028),
|
||||
UNKNOWN_ENTITLEMENT: new ApiError("Unknown entitlement", 10029),
|
||||
UNKNOWN_BUILD: new ApiError("Unknown build", 10030),
|
||||
UNKNOWN_LOBBY: new ApiError("Unknown lobby", 10031),
|
||||
UNKNOWN_BRANCH: new ApiError("Unknown branch", 10032),
|
||||
UNKNOWN_STORE_DIRECTORY_LAYOUT: new ApiError("Unknown store directory layout", 10033),
|
||||
UNKNOWN_REDISTRIBUTABLE: new ApiError("Unknown redistributable", 10036),
|
||||
UNKNOWN_GIFT_CODE: new ApiError("Unknown gift code", 10038),
|
||||
UNKNOWN_STREAM: new ApiError("Unknown stream", 10049),
|
||||
UNKNOWN_PREMIUM_SERVER_SUBSCRIBE_COOLDOWN: new ApiError("Unknown premium server subscribe cooldown", 10050),
|
||||
UNKNOWN_GUILD_TEMPLATE: new ApiError("Unknown guild template", 10057),
|
||||
UNKNOWN_DISCOVERABLE_SERVER_CATEGORY: new ApiError("Unknown discoverable server category", 10059),
|
||||
UNKNOWN_STICKER: new ApiError("Unknown sticker", 10060),
|
||||
UNKNOWN_INTERACTION: new ApiError("Unknown interaction", 10062),
|
||||
UNKNOWN_APPLICATION_COMMAND: new ApiError("Unknown application command", 10063),
|
||||
UNKNOWN_APPLICATION_COMMAND_PERMISSIONS: new ApiError("Unknown application command permissions", 10066),
|
||||
UNKNOWN_STAGE_INSTANCE: new ApiError("Unknown Stage Instance", 10067),
|
||||
UNKNOWN_GUILD_MEMBER_VERIFICATION_FORM: new ApiError("Unknown Guild Member Verification Form", 10068),
|
||||
UNKNOWN_GUILD_WELCOME_SCREEN: new ApiError("Unknown Guild Welcome Screen", 10069),
|
||||
UNKNOWN_GUILD_SCHEDULED_EVENT: new ApiError("Unknown Guild Scheduled Event", 10070),
|
||||
UNKNOWN_GUILD_SCHEDULED_EVENT_USER: new ApiError("Unknown Guild Scheduled Event User", 10071),
|
||||
BOT_PROHIBITED_ENDPOINT: new ApiError("Bots cannot use this endpoint", 20001),
|
||||
BOT_ONLY_ENDPOINT: new ApiError("Only bots can use this endpoint", 20002),
|
||||
EXPLICIT_CONTENT_CANNOT_BE_SENT_TO_RECIPIENT: new ApiError(
|
||||
"Explicit content cannot be sent to the desired recipient(s)",
|
||||
20009
|
||||
),
|
||||
ACTION_NOT_AUTHORIZED_ON_APPLICATION: new ApiError(
|
||||
"You are not authorized to perform this action on this application",
|
||||
20012
|
||||
),
|
||||
SLOWMODE_RATE_LIMIT: new ApiError("This action cannot be performed due to slowmode rate limit", 20016),
|
||||
ONLY_OWNER: new ApiError("Only the owner of this account can perform this action", 20018),
|
||||
ANNOUNCEMENT_RATE_LIMITS: new ApiError("This message cannot be edited due to announcement rate limits", 20022),
|
||||
CHANNEL_WRITE_RATELIMIT: new ApiError("The channel you are writing has hit the write rate limit", 20028),
|
||||
WORDS_NOT_ALLOWED: new ApiError(
|
||||
"Your Stage topic, server name, server description, or channel names contain words that are not allowed",
|
||||
20031
|
||||
),
|
||||
GUILD_PREMIUM_LEVEL_TOO_LOW: new ApiError("Guild premium subscription level too low", 20035),
|
||||
MAXIMUM_GUILDS: new ApiError("Maximum number of guilds reached ({})", 30001, undefined, ["100"]),
|
||||
MAXIMUM_FRIENDS: new ApiError("Maximum number of friends reached ({})", 30002, undefined, ["1000"]),
|
||||
MAXIMUM_PINS: new ApiError("Maximum number of pins reached for the channel ({})", 30003, undefined, ["50"]),
|
||||
MAXIMUM_NUMBER_OF_RECIPIENTS_REACHED: new ApiError("Maximum number of recipients reached ({})", 30004, undefined, [
|
||||
"10",
|
||||
]),
|
||||
MAXIMUM_ROLES: new ApiError("Maximum number of guild roles reached ({})", 30005, undefined, ["250"]),
|
||||
MAXIMUM_WEBHOOKS: new ApiError("Maximum number of webhooks reached ({})", 30007, undefined, ["10"]),
|
||||
MAXIMUM_NUMBER_OF_EMOJIS_REACHED: new ApiError("Maximum number of emojis reached", 30008),
|
||||
MAXIMUM_REACTIONS: new ApiError("Maximum number of reactions reached ({})", 30010, undefined, ["20"]),
|
||||
MAXIMUM_CHANNELS: new ApiError("Maximum number of guild channels reached ({})", 30013, undefined, ["500"]),
|
||||
MAXIMUM_ATTACHMENTS: new ApiError("Maximum number of attachments in a message reached ({})", 30015, undefined, [
|
||||
"10",
|
||||
]),
|
||||
MAXIMUM_INVITES: new ApiError("Maximum number of invites reached ({})", 30016, undefined, ["1000"]),
|
||||
MAXIMUM_ANIMATED_EMOJIS: new ApiError("Maximum number of animated emojis reached", 30018),
|
||||
MAXIMUM_SERVER_MEMBERS: new ApiError("Maximum number of server members reached", 30019),
|
||||
MAXIMUM_SERVER_CATEGORIES: new ApiError(
|
||||
"Maximum number of server categories has been reached ({})",
|
||||
30030,
|
||||
undefined,
|
||||
["5"]
|
||||
),
|
||||
GUILD_ALREADY_HAS_TEMPLATE: new ApiError("Guild already has a template", 30031),
|
||||
MAXIMUM_THREAD_PARTICIPANTS: new ApiError("Max number of thread participants has been reached", 30033),
|
||||
MAXIMUM_BANS_FOR_NON_GUILD_MEMBERS: new ApiError(
|
||||
"Maximum number of bans for non-guild members have been exceeded",
|
||||
30035
|
||||
),
|
||||
MAXIMUM_BANS_FETCHES: new ApiError("Maximum number of bans fetches has been reached", 30037),
|
||||
MAXIMUM_STICKERS: new ApiError("Maximum number of stickers reached", 30039),
|
||||
MAXIMUM_PRUNE_REQUESTS: new ApiError("Maximum number of prune requests has been reached. Try again later", 30040),
|
||||
UNAUTHORIZED: new ApiError("Unauthorized. Provide a valid token and try again", 40001),
|
||||
ACCOUNT_VERIFICATION_REQUIRED: new ApiError(
|
||||
"You need to verify your account in order to perform this action",
|
||||
40002
|
||||
),
|
||||
OPENING_DIRECT_MESSAGES_TOO_FAST: new ApiError("You are opening direct messages too fast", 40003),
|
||||
REQUEST_ENTITY_TOO_LARGE: new ApiError("Request entity too large. Try sending something smaller in size", 40005),
|
||||
FEATURE_TEMPORARILY_DISABLED: new ApiError("This feature has been temporarily disabled server-side", 40006),
|
||||
USER_BANNED: new ApiError("The user is banned from this guild", 40007),
|
||||
TARGET_USER_IS_NOT_CONNECTED_TO_VOICE: new ApiError("Target user is not connected to voice", 40032),
|
||||
ALREADY_CROSSPOSTED: new ApiError("This message has already been crossposted", 40033),
|
||||
APPLICATION_COMMAND_ALREADY_EXISTS: new ApiError("An application command with that name already exists", 40041),
|
||||
MISSING_ACCESS: new ApiError("Missing access", 50001),
|
||||
INVALID_ACCOUNT_TYPE: new ApiError("Invalid account type", 50002),
|
||||
CANNOT_EXECUTE_ON_DM: new ApiError("Cannot execute action on a DM channel", 50003),
|
||||
EMBED_DISABLED: new ApiError("Guild widget disabled", 50004),
|
||||
CANNOT_EDIT_MESSAGE_BY_OTHER: new ApiError("Cannot edit a message authored by another user", 50005),
|
||||
CANNOT_SEND_EMPTY_MESSAGE: new ApiError("Cannot send an empty message", 50006),
|
||||
CANNOT_MESSAGE_USER: new ApiError("Cannot send messages to this user", 50007),
|
||||
CANNOT_SEND_MESSAGES_IN_VOICE_CHANNEL: new ApiError("Cannot send messages in a voice channel", 50008),
|
||||
CHANNEL_VERIFICATION_LEVEL_TOO_HIGH: new ApiError(
|
||||
"Channel verification level is too high for you to gain access",
|
||||
50009
|
||||
),
|
||||
OAUTH2_APPLICATION_BOT_ABSENT: new ApiError("OAuth2 application does not have a bot", 50010),
|
||||
MAXIMUM_OAUTH2_APPLICATIONS: new ApiError("OAuth2 application limit reached", 50011),
|
||||
INVALID_OAUTH_STATE: new ApiError("Invalid OAuth2 state", 50012),
|
||||
MISSING_PERMISSIONS: new ApiError("You lack permissions to perform that action ({})", 50013, undefined, [""]),
|
||||
INVALID_AUTHENTICATION_TOKEN: new ApiError("Invalid authentication token provided", 50014),
|
||||
NOTE_TOO_LONG: new ApiError("Note was too long", 50015),
|
||||
INVALID_BULK_DELETE_QUANTITY: new ApiError(
|
||||
"Provided too few or too many messages to delete. Must provide at least {} and fewer than {} messages to delete",
|
||||
50016,
|
||||
undefined,
|
||||
["2", "100"]
|
||||
),
|
||||
CANNOT_PIN_MESSAGE_IN_OTHER_CHANNEL: new ApiError(
|
||||
"A message can only be pinned to the channel it was sent in",
|
||||
50019
|
||||
),
|
||||
INVALID_OR_TAKEN_INVITE_CODE: new ApiError("Invite code was either invalid or taken", 50020),
|
||||
CANNOT_EXECUTE_ON_SYSTEM_MESSAGE: new ApiError("Cannot execute action on a system message", 50021),
|
||||
CANNOT_EXECUTE_ON_THIS_CHANNEL_TYPE: new ApiError("Cannot execute action on this channel type", 50024),
|
||||
INVALID_OAUTH_TOKEN: new ApiError("Invalid OAuth2 access token provided", 50025),
|
||||
MISSING_REQUIRED_OAUTH2_SCOPE: new ApiError("Missing required OAuth2 scope", 50026),
|
||||
INVALID_WEBHOOK_TOKEN_PROVIDED: new ApiError("Invalid webhook token provided", 50027),
|
||||
INVALID_ROLE: new ApiError("Invalid role", 50028),
|
||||
INVALID_RECIPIENT: new ApiError("Invalid Recipient(s)", 50033),
|
||||
BULK_DELETE_MESSAGE_TOO_OLD: new ApiError("A message provided was too old to bulk delete", 50034),
|
||||
INVALID_FORM_BODY: new ApiError(
|
||||
"Invalid form body (returned for both application/json and multipart/form-data bodies), or invalid Content-Type provided",
|
||||
50035
|
||||
),
|
||||
INVITE_ACCEPTED_TO_GUILD_NOT_CONTAINING_BOT: new ApiError(
|
||||
"An invite was accepted to a guild the application's bot is not in",
|
||||
50036
|
||||
),
|
||||
INVALID_API_VERSION: new ApiError("Invalid API version provided", 50041),
|
||||
FILE_EXCEEDS_MAXIMUM_SIZE: new ApiError("File uploaded exceeds the maximum size", 50045),
|
||||
INVALID_FILE_UPLOADED: new ApiError("Invalid file uploaded", 50046),
|
||||
CANNOT_SELF_REDEEM_GIFT: new ApiError("Cannot self-redeem this gift", 50054),
|
||||
PAYMENT_SOURCE_REQUIRED: new ApiError("Payment source required to redeem gift", 50070),
|
||||
CANNOT_DELETE_COMMUNITY_REQUIRED_CHANNEL: new ApiError(
|
||||
"Cannot delete a channel required for Community guilds",
|
||||
50074
|
||||
),
|
||||
INVALID_STICKER_SENT: new ApiError("Invalid sticker sent", 50081),
|
||||
CANNOT_EDIT_ARCHIVED_THREAD: new ApiError(
|
||||
"Tried to perform an operation on an archived thread, such as editing a message or adding a user to the thread",
|
||||
50083
|
||||
),
|
||||
INVALID_THREAD_NOTIFICATION_SETTINGS: new ApiError("Invalid thread notification settings", 50084),
|
||||
BEFORE_EARLIER_THAN_THREAD_CREATION_DATE: new ApiError(
|
||||
"before value is earlier than the thread creation date",
|
||||
50085
|
||||
),
|
||||
SERVER_NOT_AVAILABLE_IN_YOUR_LOCATION: new ApiError("This server is not available in your location", 50095),
|
||||
SERVER_NEEDS_MONETIZATION_ENABLED: new ApiError(
|
||||
"This server needs monetization enabled in order to perform this action",
|
||||
50097
|
||||
),
|
||||
TWO_FACTOR_REQUIRED: new ApiError("Two factor is required for this operation", 60003),
|
||||
NO_USERS_WITH_DISCORDTAG_EXIST: new ApiError("No users with DiscordTag exist", 80004),
|
||||
REACTION_BLOCKED: new ApiError("Reaction was blocked", 90001),
|
||||
RESOURCE_OVERLOADED: new ApiError("API resource is currently overloaded. Try again a little later", 130000),
|
||||
STAGE_ALREADY_OPEN: new ApiError("The Stage is already open", 150006),
|
||||
THREAD_ALREADY_CREATED_FOR_THIS_MESSAGE: new ApiError("A thread has already been created for this message", 160004),
|
||||
THREAD_IS_LOCKED: new ApiError("Thread is locked", 160005),
|
||||
MAXIMUM_NUMBER_OF_ACTIVE_THREADS: new ApiError("Maximum number of active threads reached", 160006),
|
||||
MAXIMUM_NUMBER_OF_ACTIVE_ANNOUNCEMENT_THREADS: new ApiError(
|
||||
"Maximum number of active announcement threads reached",
|
||||
160007
|
||||
),
|
||||
INVALID_JSON_FOR_UPLOADED_LOTTIE_FILE: new ApiError("Invalid JSON for uploaded Lottie file", 170001),
|
||||
LOTTIES_CANNOT_CONTAIN_RASTERIZED_IMAGES: new ApiError(
|
||||
"Uploaded Lotties cannot contain rasterized images such as PNG or JPEG",
|
||||
170002
|
||||
),
|
||||
STICKER_MAXIMUM_FRAMERATE: new ApiError("Sticker maximum framerate exceeded", 170003),
|
||||
STICKER_MAXIMUM_FRAME_COUNT: new ApiError("Sticker frame count exceeds maximum of {} frames", 170004, undefined, [
|
||||
"1000",
|
||||
]),
|
||||
LOTTIE_ANIMATION_MAXIMUM_DIMENSIONS: new ApiError("Lottie animation maximum dimensions exceeded", 170005),
|
||||
STICKER_FRAME_RATE_TOO_SMALL_OR_TOO_LARGE: new ApiError(
|
||||
"Sticker frame rate is either too small or too large",
|
||||
170006
|
||||
),
|
||||
STICKER_ANIMATION_DURATION_MAXIMUM: new ApiError(
|
||||
"Sticker animation duration exceeds maximum of {} seconds",
|
||||
170007,
|
||||
undefined,
|
||||
["5"]
|
||||
),
|
||||
|
||||
//Other errors
|
||||
UNKNOWN_VOICE_STATE: new ApiError("Unknown Voice State", 10065, 404),
|
||||
};
|
||||
|
||||
/**
|
||||
* An error encountered while performing an API request (Fosscord only). Here are the potential errors:
|
||||
*/
|
||||
export const FosscordApiErrors = {
|
||||
MANUALLY_TRIGGERED_ERROR: new ApiError("This is an artificial error", 1, 500),
|
||||
PREMIUM_DISABLED_FOR_GUILD: new ApiError("This guild cannot be boosted", 25001),
|
||||
NO_FURTHER_PREMIUM: new ApiError("This guild does not receive further boosts", 25002),
|
||||
GUILD_PREMIUM_DISABLED_FOR_YOU: new ApiError("This guild cannot be boosted by you", 25003, 403),
|
||||
CANNOT_FRIEND_SELF: new ApiError("Cannot friend oneself", 25009),
|
||||
USER_SPECIFIC_INVITE_WRONG_RECIPIENT: new ApiError("This invite is not meant for you", 25010),
|
||||
USER_SPECIFIC_INVITE_FAILED: new ApiError("Failed to invite user", 25011),
|
||||
CANNOT_MODIFY_USER_GROUP: new ApiError("This user cannot manipulate this group", 25050, 403),
|
||||
CANNOT_REMOVE_SELF_FROM_GROUP: new ApiError("This user cannot remove oneself from user group", 25051),
|
||||
CANNOT_BAN_OPERATOR: new ApiError("Non-OPERATOR cannot ban OPERATOR from instance", 25052),
|
||||
CANNOT_LEAVE_GUILD: new ApiError("You are not allowed to leave guilds that you joined by yourself", 25059, 403),
|
||||
EDITS_DISABLED: new ApiError("You are not allowed to edit your own messages", 25060, 403),
|
||||
DELETE_MESSAGE_DISABLED: new ApiError("You are not allowed to delete your own messages", 25061, 403),
|
||||
FEATURE_PERMANENTLY_DISABLED: new ApiError("This feature has been disabled server-side", 45006, 501),
|
||||
MISSING_RIGHTS: new ApiError("You lack rights to perform that action ({})", 50013, undefined, [""]),
|
||||
CANNOT_REPLACE_BY_BACKFILL: new ApiError("Cannot backfill to message ID that already exists", 55002, 409),
|
||||
CANNOT_BACKFILL_TO_THE_FUTURE: new ApiError("You cannot backfill messages in the future", 55003),
|
||||
CANNOT_GRANT_PERMISSIONS_EXCEEDING_RIGHTS: new ApiError("You cannot grant permissions exceeding your own rights", 50050),
|
||||
ROUTES_LOOPING: new ApiError("Loops in the route definition ({})", 50060, undefined, [""]),
|
||||
CANNOT_REMOVE_ROUTE: new ApiError("Cannot remove message route while it is in effect and being used", 50061),
|
||||
};
|
||||
|
||||
/**
|
||||
* The value set for a guild's default message notifications, e.g. `ALL`. Here are the available types:
|
||||
* * ALL
|
||||
* * MENTIONS
|
||||
* * MUTED (Fosscord extension)
|
||||
* @typedef {string} DefaultMessageNotifications
|
||||
*/
|
||||
export const DefaultMessageNotifications = ["ALL", "MENTIONS", "MUTED"];
|
||||
|
||||
/**
|
||||
* The value set for a team members's membership state:
|
||||
* * INVITED
|
||||
* * ACCEPTED
|
||||
* * INSERTED (Fosscord extension)
|
||||
* @typedef {string} MembershipStates
|
||||
*/
|
||||
export const MembershipStates = [
|
||||
"INSERTED",
|
||||
"INVITED",
|
||||
"ACCEPTED",
|
||||
];
|
||||
|
||||
/**
|
||||
* The value set for a webhook's type:
|
||||
* * Incoming
|
||||
* * Channel Follower
|
||||
* * Custom (Fosscord extension)
|
||||
* @typedef {string} WebhookTypes
|
||||
*/
|
||||
export const WebhookTypes = [
|
||||
"Custom",
|
||||
"Incoming",
|
||||
"Channel Follower",
|
||||
];
|
||||
|
||||
function keyMirror(arr: string[]) {
|
||||
let tmp = Object.create(null);
|
||||
for (const value of arr) tmp[value] = value;
|
||||
return tmp;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import path from "path";
|
||||
import "reflect-metadata";
|
||||
import { DataSource } from "typeorm";
|
||||
import * as Models from "../entities";
|
||||
import { Migration } from "../entities/Migration";
|
||||
import { yellow, green, red } from "picocolors";
|
||||
|
||||
// UUID extension option is only supported with postgres
|
||||
// We want to generate all id's with Snowflakes that's why we have our own BaseEntity class
|
||||
|
||||
var dbConnection: DataSource | undefined;
|
||||
let dbConnectionString = process.env.DATABASE || path.join(process.cwd(), "database.db");
|
||||
|
||||
export function getDatabase(): DataSource | null {
|
||||
// if (!dbConnection) throw new Error("Tried to get database before it was initialised");
|
||||
if (!dbConnection) return null;
|
||||
return dbConnection;
|
||||
}
|
||||
|
||||
export async function initDatabase(): Promise<DataSource> {
|
||||
if (dbConnection) return dbConnection;
|
||||
|
||||
const type = dbConnectionString.includes("://") ? dbConnectionString.split(":")[0]?.replace("+srv", "") : "sqlite";
|
||||
const isSqlite = type.includes("sqlite");
|
||||
|
||||
console.log(`[Database] ${yellow(`connecting to ${type} db`)}`);
|
||||
if (isSqlite) {
|
||||
console.log(`[Database] ${red(`You are running sqlite! Please keep in mind that we recommend setting up a dedicated database!`)}`);
|
||||
}
|
||||
|
||||
const dataSource = new DataSource({
|
||||
//@ts-ignore
|
||||
type,
|
||||
charset: 'utf8mb4',
|
||||
url: isSqlite ? undefined : dbConnectionString,
|
||||
database: isSqlite ? dbConnectionString : undefined,
|
||||
entities: ["dist/util/entities/*.js"],
|
||||
synchronize: type !== "mongodb",
|
||||
logging: false,
|
||||
bigNumberStrings: false,
|
||||
supportBigNumbers: true,
|
||||
name: "default",
|
||||
// migrations: [path.join(__dirname, "..", "migrations", "*.js")],
|
||||
});
|
||||
|
||||
dbConnection = await dataSource.initialize();
|
||||
|
||||
// // @ts-ignore
|
||||
// promise = createConnection({
|
||||
// type,
|
||||
// charset: 'utf8mb4',
|
||||
// url: isSqlite ? undefined : dbConnectionString,
|
||||
// database: isSqlite ? dbConnectionString : undefined,
|
||||
// // @ts-ignore
|
||||
// entities: Object.values(Models).filter((x) => x?.constructor?.name !== "Object" && x?.name),
|
||||
// synchronize: type !== "mongodb",
|
||||
// logging: false,
|
||||
// // cache: { // cache is used only by query builder and entity manager
|
||||
// // duration: 1000 * 30,
|
||||
// // type: "redis",
|
||||
// // options: {
|
||||
// // host: "localhost",
|
||||
// // port: 6379,
|
||||
// // },
|
||||
// // },
|
||||
// bigNumberStrings: false,
|
||||
// supportBigNumbers: true,
|
||||
// name: "default",
|
||||
// migrations: [path.join(__dirname, "..", "migrations", "*.js")],
|
||||
// });
|
||||
|
||||
// // run migrations, and if it is a new fresh database, set it to the last migration
|
||||
// if (dbConnection.migrations.length) {
|
||||
// if (!(await Migration.findOne({ }))) {
|
||||
// let i = 0;
|
||||
|
||||
// await Migration.insert(
|
||||
// dbConnection.migrations.map((x) => ({
|
||||
// id: i++,
|
||||
// name: x.name,
|
||||
// timestamp: Date.now(),
|
||||
// }))
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
await dbConnection.runMigrations();
|
||||
console.log(`[Database] ${green("connected")}`);
|
||||
|
||||
return dbConnection;
|
||||
}
|
||||
|
||||
export { dbConnection };
|
||||
|
||||
export function closeDatabase() {
|
||||
dbConnection?.destroy();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export const EMAIL_REGEX =
|
||||
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
|
||||
export function adjustEmail(email?: string): string | undefined {
|
||||
if (!email) return email;
|
||||
// body parser already checked if it is a valid email
|
||||
const parts = <RegExpMatchArray>email.match(EMAIL_REGEX);
|
||||
// @ts-ignore
|
||||
if (!parts || parts.length < 5) return undefined;
|
||||
const domain = parts[5];
|
||||
const user = parts[1];
|
||||
|
||||
// TODO: check accounts with uncommon email domains
|
||||
if (domain === "gmail.com" || domain === "googlemail.com") {
|
||||
// replace .dots and +alternatives -> Gmail Dot Trick https://support.google.com/mail/answer/7436150 and https://generator.email/blog/gmail-generator
|
||||
let v = user.replace(/[.]|(\+.*)/g, "") + "@gmail.com";
|
||||
}
|
||||
|
||||
if (domain === "google.com") {
|
||||
// replace .dots and +alternatives -> Google Staff GMail Dot Trick
|
||||
let v = user.replace(/[.]|(\+.*)/g, "") + "@google.com";
|
||||
}
|
||||
|
||||
return email;
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { Channel } from "amqplib";
|
||||
import { RabbitMQ } from "./RabbitMQ";
|
||||
import EventEmitter from "events";
|
||||
import { EVENT, Event } from "../interfaces";
|
||||
export const events = new EventEmitter();
|
||||
|
||||
export async function emitEvent(payload: Omit<Event, "created_at">) {
|
||||
const id = (payload.channel_id || payload.user_id || payload.guild_id) as string;
|
||||
if (!id) return console.error("event doesn't contain any id", payload);
|
||||
|
||||
if (RabbitMQ.connection) {
|
||||
const data = typeof payload.data === "object" ? JSON.stringify(payload.data) : payload.data; // use rabbitmq for event transmission
|
||||
await RabbitMQ.channel?.assertExchange(id, "fanout", { durable: false });
|
||||
|
||||
// assertQueue isn't needed, because a queue will automatically created if it doesn't exist
|
||||
const successful = RabbitMQ.channel?.publish(id, "", Buffer.from(`${data}`), { type: payload.event });
|
||||
if (!successful) throw new Error("failed to send event");
|
||||
} else if (process.env.EVENT_TRANSMISSION === "process") {
|
||||
process.send?.({ type: "event", event: payload, id } as ProcessEvent);
|
||||
} else {
|
||||
events.emit(id, payload);
|
||||
}
|
||||
}
|
||||
|
||||
export async function initEvent() {
|
||||
await RabbitMQ.init(); // does nothing if rabbitmq is not setup
|
||||
if (RabbitMQ.connection) {
|
||||
} else {
|
||||
// use event emitter
|
||||
// use process messages
|
||||
}
|
||||
}
|
||||
|
||||
export interface EventOpts extends Event {
|
||||
acknowledge?: Function;
|
||||
channel?: Channel;
|
||||
cancel: Function;
|
||||
}
|
||||
|
||||
export interface ListenEventOpts {
|
||||
channel?: Channel;
|
||||
acknowledge?: boolean;
|
||||
}
|
||||
|
||||
export interface ProcessEvent {
|
||||
type: "event";
|
||||
event: Event;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export async function listenEvent(event: string, callback: (event: EventOpts) => any, opts?: ListenEventOpts) {
|
||||
if (RabbitMQ.connection) {
|
||||
// @ts-ignore
|
||||
return rabbitListen(opts?.channel || RabbitMQ.channel, event, callback, { acknowledge: opts?.acknowledge });
|
||||
} else if (process.env.EVENT_TRANSMISSION === "process") {
|
||||
const cancel = () => {
|
||||
process.removeListener("message", listener);
|
||||
process.setMaxListeners(process.getMaxListeners() - 1);
|
||||
};
|
||||
|
||||
const listener = (msg: ProcessEvent) => {
|
||||
msg.type === "event" && msg.id === event && callback({ ...msg.event, cancel });
|
||||
};
|
||||
|
||||
//@ts-ignore apparently theres no function addListener with this signature
|
||||
process.addListener("message", listener);
|
||||
process.setMaxListeners(process.getMaxListeners() + 1);
|
||||
|
||||
return cancel;
|
||||
} else {
|
||||
const listener = (opts: any) => callback({ ...opts, cancel });
|
||||
const cancel = () => {
|
||||
events.removeListener(event, listener);
|
||||
events.setMaxListeners(events.getMaxListeners() - 1);
|
||||
};
|
||||
events.setMaxListeners(events.getMaxListeners() + 1);
|
||||
events.addListener(event, listener);
|
||||
|
||||
return cancel;
|
||||
}
|
||||
}
|
||||
|
||||
async function rabbitListen(
|
||||
channel: Channel,
|
||||
id: string,
|
||||
callback: (event: EventOpts) => any,
|
||||
opts?: { acknowledge?: boolean }
|
||||
) {
|
||||
await channel.assertExchange(id, "fanout", { durable: false });
|
||||
const q = await channel.assertQueue("", { exclusive: true, autoDelete: true });
|
||||
|
||||
const cancel = () => {
|
||||
channel.cancel(q.queue);
|
||||
channel.unbindQueue(q.queue, id, "");
|
||||
};
|
||||
|
||||
channel.bindQueue(q.queue, id, "");
|
||||
channel.consume(
|
||||
q.queue,
|
||||
(opts) => {
|
||||
if (!opts) return;
|
||||
|
||||
const data = JSON.parse(opts.content.toString());
|
||||
const event = opts.properties.type as EVENT;
|
||||
|
||||
callback({
|
||||
event,
|
||||
data,
|
||||
acknowledge() {
|
||||
channel.ack(opts);
|
||||
},
|
||||
channel,
|
||||
cancel,
|
||||
});
|
||||
// rabbitCh.ack(opts);
|
||||
},
|
||||
{
|
||||
noAck: !opts?.acknowledge,
|
||||
}
|
||||
);
|
||||
|
||||
return cancel;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import "missing-native-js-functions";
|
||||
|
||||
export function FieldErrors(fields: Record<string, { code?: string; message: string }>) {
|
||||
return new FieldError(
|
||||
50035,
|
||||
"Invalid Form Body",
|
||||
fields.map(({ message, code }) => ({
|
||||
_errors: [
|
||||
{
|
||||
message,
|
||||
code: code || "BASE_TYPE_INVALID",
|
||||
},
|
||||
],
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: implement Image data type: Data URI scheme that supports JPG, GIF, and PNG formats. An example Data URI format is: data:image/jpeg;base64,BASE64_ENCODED_JPEG_IMAGE_DATA
|
||||
// Ensure you use the proper content type (image/jpeg, image/png, image/gif) that matches the image data being provided.
|
||||
|
||||
export class FieldError extends Error {
|
||||
constructor(public code: string | number, public message: string, public errors?: any) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BitField } from "./BitField";
|
||||
|
||||
export class Intents extends BitField {
|
||||
static FLAGS = {
|
||||
GUILDS: BigInt(1) << BigInt(0), // guilds and guild merge-split events affecting the user
|
||||
GUILD_MEMBERS: BigInt(1) << BigInt(1), // memberships
|
||||
GUILD_BANS: BigInt(1) << BigInt(2), // bans and ban lists
|
||||
GUILD_EMOJIS: BigInt(1) << BigInt(3), // custom emojis
|
||||
GUILD_INTEGRATIONS: BigInt(1) << BigInt(4), // applications
|
||||
GUILD_WEBHOOKS: BigInt(1) << BigInt(5), // webhooks
|
||||
GUILD_INVITES: BigInt(1) << BigInt(6), // mass invites (no user can receive user specific invites of another user)
|
||||
GUILD_VOICE_STATES: BigInt(1) << BigInt(7), // voice updates
|
||||
GUILD_PRESENCES: BigInt(1) << BigInt(8), // presence updates
|
||||
GUILD_MESSAGES_METADATA: BigInt(1) << BigInt(9), // guild message metadata
|
||||
GUILD_MESSAGE_REACTIONS: BigInt(1) << BigInt(10), // guild message reactions
|
||||
GUILD_MESSAGE_TYPING: BigInt(1) << BigInt(11), // guild channel typing notifications
|
||||
DIRECT_MESSAGES: BigInt(1) << BigInt(12), // DM or orphan channels
|
||||
DIRECT_MESSAGE_REACTIONS: BigInt(1) << BigInt(13), // DM or orphan channel message reactions
|
||||
DIRECT_MESSAGE_TYPING: BigInt(1) << BigInt(14), // DM typing notifications
|
||||
GUILD_MESSAGES_CONTENT: BigInt(1) << BigInt(15), // guild message content
|
||||
GUILD_POLICIES: BigInt(1) << BigInt(20), // guild policies
|
||||
GUILD_POLICY_EXECUTION: BigInt(1) << BigInt(21), // guild policy execution
|
||||
LIVE_MESSAGE_COMPOSITION: BigInt(1) << BigInt(32), // allow composing messages using the gateway
|
||||
GUILD_ROUTES: BigInt(1) << BigInt(41), // message routes affecting the guild
|
||||
DIRECT_MESSAGES_THREADS: BigInt(1) << BigInt(42), // direct message threads
|
||||
JUMBO_EVENTS: BigInt(1) << BigInt(43), // jumbo events (size limits to be defined later)
|
||||
LOBBIES: BigInt(1) << BigInt(44), // lobbies
|
||||
INSTANCE_ROUTES: BigInt(1) << BigInt(60), // all message route changes
|
||||
INSTANCE_GUILD_CHANGES: BigInt(1) << BigInt(61), // all guild create, guild object patch, split, merge and delete events
|
||||
INSTANCE_POLICY_UPDATES: BigInt(1) << BigInt(62), // all instance policy updates
|
||||
INSTANCE_USER_UPDATES: BigInt(1) << BigInt(63) // all instance user updates
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// List from https://invisible-characters.com/
|
||||
export const InvisibleCharacters = [
|
||||
'\u{9}', //Tab
|
||||
//'\u{20}', //Space //categories can have spaces in them
|
||||
'\u{ad}', //Soft hyphen
|
||||
'\u{34f}', //Combining grapheme joiner
|
||||
'\u{61c}', //Arabic letter mark
|
||||
'\u{115f}', //Hangul choseong filler
|
||||
'\u{1160}', //Hangul jungseong filler
|
||||
'\u{17b4}', //Khmer vowel inherent AQ
|
||||
'\u{17b5}', //Khmer vowel inherent AA
|
||||
'\u{180e}', //Mongolian vowel separator
|
||||
'\u{2000}', //En quad
|
||||
'\u{2001}', //Em quad
|
||||
'\u{2002}', //En space
|
||||
'\u{2003}', //Em space
|
||||
'\u{2004}', //Three-per-em space
|
||||
'\u{2005}', //Four-per-em space
|
||||
'\u{2006}', //Six-per-em space
|
||||
'\u{2007}', //Figure space
|
||||
'\u{2008}', //Punctuation space
|
||||
'\u{2009}', //Thin space
|
||||
'\u{200a}', //Hair space
|
||||
'\u{200b}', //Zero width space
|
||||
'\u{200c}', //Zero width non-joiner
|
||||
'\u{200d}', //Zero width joiner
|
||||
'\u{200e}', //Left-to-right mark
|
||||
'\u{200f}', //Right-to-left mark
|
||||
'\u{202f}', //Narrow no-break space
|
||||
'\u{205f}', //Medium mathematical space
|
||||
'\u{2060}', //Word joiner
|
||||
'\u{2061}', //Function application
|
||||
'\u{2062}', //Invisible times
|
||||
'\u{2063}', //Invisible separator
|
||||
'\u{2064}', //Invisible plus
|
||||
'\u{206a}', //Inhibit symmetric swapping
|
||||
'\u{206b}', //Activate symmetric swapping
|
||||
'\u{206c}', //Inhibit arabic form shaping
|
||||
'\u{206d}', //Activate arabic form shaping
|
||||
'\u{206e}', //National digit shapes
|
||||
'\u{206f}', //Nominal digit shapes
|
||||
'\u{3000}', //Ideographic space
|
||||
'\u{2800}', //Braille pattern blank
|
||||
'\u{3164}', //Hangul filler
|
||||
'\u{feff}', //Zero width no-break space
|
||||
'\u{ffa0}', //Haldwidth hangul filler
|
||||
'\u{1d159}', //Musical symbol null notehead
|
||||
'\u{1d173}', //Musical symbol begin beam
|
||||
'\u{1d174}', //Musical symbol end beam
|
||||
'\u{1d175}', //Musical symbol begin tie
|
||||
'\u{1d176}', //Musical symbol end tie
|
||||
'\u{1d177}', //Musical symbol begin slur
|
||||
'\u{1d178}', //Musical symbol end slur
|
||||
'\u{1d179}', //Musical symbol begin phrase
|
||||
'\u{1d17a}' //Musical symbol end phrase
|
||||
];
|
||||
@@ -0,0 +1,20 @@
|
||||
// based on https://github.com/discordjs/discord.js/blob/master/src/util/MessageFlags.js
|
||||
// Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah, 2022 Erkin Alp Güney
|
||||
|
||||
import { BitField } from "./BitField";
|
||||
|
||||
export class MessageFlags extends BitField {
|
||||
static FLAGS = {
|
||||
CROSSPOSTED: BigInt(1) << BigInt(0),
|
||||
IS_CROSSPOST: BigInt(1) << BigInt(1),
|
||||
SUPPRESS_EMBEDS: BigInt(1) << BigInt(2),
|
||||
// SOURCE_MESSAGE_DELETED: BigInt(1) << BigInt(3), // fosscord will delete them from destination too, making this redundant
|
||||
URGENT: BigInt(1) << BigInt(4),
|
||||
// HAS_THREAD: BigInt(1) << BigInt(5) // does not apply to fosscord due to infrastructural differences
|
||||
PRIVATE_ROUTE: BigInt(1) << BigInt(6), // it that has been routed to only some of the users that can see the channel
|
||||
INTERACTION_WAIT: BigInt(1) << BigInt(7), // discord.com calls this LOADING
|
||||
// FAILED_TO_MENTION_SOME_ROLES_IN_THREAD: BigInt(1) << BigInt(8)
|
||||
SCRIPT_WAIT: BigInt(1) << BigInt(24), // waiting for the self command to complete
|
||||
IMPORT_WAIT: BigInt(1) << BigInt(25), // latest message of a bulk import, waiting for the rest of the channel to be backfilled
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
// https://github.com/discordjs/discord.js/blob/master/src/util/Permissions.js
|
||||
// Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah
|
||||
import { Channel, ChannelPermissionOverwrite, Guild, Member, Role } from "../entities";
|
||||
import { BitField } from "./BitField";
|
||||
import "missing-native-js-functions";
|
||||
import { BitFieldResolvable, BitFlag } from "./BitField";
|
||||
|
||||
var HTTPError: any;
|
||||
|
||||
try {
|
||||
HTTPError = require("lambert-server").HTTPError;
|
||||
} catch (e) {
|
||||
HTTPError = Error;
|
||||
}
|
||||
|
||||
export type PermissionResolvable = bigint | number | Permissions | PermissionResolvable[] | PermissionString;
|
||||
|
||||
type PermissionString = keyof typeof Permissions.FLAGS;
|
||||
|
||||
// BigInt doesn't have a bit limit (https://stackoverflow.com/questions/53335545/whats-the-biggest-bigint-value-in-js-as-per-spec)
|
||||
const CUSTOM_PERMISSION_OFFSET = BigInt(1) << BigInt(64); // 27 permission bits left for discord to add new ones
|
||||
|
||||
export class Permissions extends BitField {
|
||||
cache: PermissionCache = {};
|
||||
|
||||
constructor(bits: BitFieldResolvable = 0) {
|
||||
super(bits);
|
||||
if (this.bitfield & Permissions.FLAGS.ADMINISTRATOR) {
|
||||
this.bitfield = ALL_PERMISSIONS;
|
||||
}
|
||||
}
|
||||
|
||||
static FLAGS = {
|
||||
CREATE_INSTANT_INVITE: BitFlag(0),
|
||||
KICK_MEMBERS: BitFlag(1),
|
||||
BAN_MEMBERS: BitFlag(2),
|
||||
ADMINISTRATOR: BitFlag(3),
|
||||
MANAGE_CHANNELS: BitFlag(4),
|
||||
MANAGE_GUILD: BitFlag(5),
|
||||
ADD_REACTIONS: BitFlag(6),
|
||||
VIEW_AUDIT_LOG: BitFlag(7),
|
||||
PRIORITY_SPEAKER: BitFlag(8),
|
||||
STREAM: BitFlag(9),
|
||||
VIEW_CHANNEL: BitFlag(10),
|
||||
SEND_MESSAGES: BitFlag(11),
|
||||
SEND_TTS_MESSAGES: BitFlag(12),
|
||||
MANAGE_MESSAGES: BitFlag(13),
|
||||
EMBED_LINKS: BitFlag(14),
|
||||
ATTACH_FILES: BitFlag(15),
|
||||
READ_MESSAGE_HISTORY: BitFlag(16),
|
||||
MENTION_EVERYONE: BitFlag(17),
|
||||
USE_EXTERNAL_EMOJIS: BitFlag(18),
|
||||
VIEW_GUILD_INSIGHTS: BitFlag(19),
|
||||
CONNECT: BitFlag(20),
|
||||
SPEAK: BitFlag(21),
|
||||
MUTE_MEMBERS: BitFlag(22),
|
||||
DEAFEN_MEMBERS: BitFlag(23),
|
||||
MOVE_MEMBERS: BitFlag(24),
|
||||
USE_VAD: BitFlag(25),
|
||||
CHANGE_NICKNAME: BitFlag(26),
|
||||
MANAGE_NICKNAMES: BitFlag(27),
|
||||
MANAGE_ROLES: BitFlag(28),
|
||||
MANAGE_WEBHOOKS: BitFlag(29),
|
||||
MANAGE_EMOJIS_AND_STICKERS: BitFlag(30),
|
||||
USE_APPLICATION_COMMANDS: BitFlag(31),
|
||||
REQUEST_TO_SPEAK: BitFlag(32),
|
||||
// TODO: what is permission 33?
|
||||
MANAGE_THREADS: BitFlag(34),
|
||||
USE_PUBLIC_THREADS: BitFlag(35),
|
||||
USE_PRIVATE_THREADS: BitFlag(36),
|
||||
USE_EXTERNAL_STICKERS: BitFlag(37),
|
||||
|
||||
/**
|
||||
* CUSTOM PERMISSIONS ideas:
|
||||
* - allow user to dm members
|
||||
* - allow user to pin messages (without MANAGE_MESSAGES)
|
||||
* - allow user to publish messages (without MANAGE_MESSAGES)
|
||||
*/
|
||||
// CUSTOM_PERMISSION: BigInt(1) << BigInt(0) + CUSTOM_PERMISSION_OFFSET
|
||||
};
|
||||
|
||||
any(permission: PermissionResolvable, checkAdmin = true) {
|
||||
return (checkAdmin && super.any(Permissions.FLAGS.ADMINISTRATOR)) || super.any(permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the bitfield has a permission, or multiple permissions.
|
||||
*/
|
||||
has(permission: PermissionResolvable, checkAdmin = true) {
|
||||
return (checkAdmin && super.has(Permissions.FLAGS.ADMINISTRATOR)) || super.has(permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the bitfield has a permission, or multiple permissions, but throws an Error if user fails to match auth criteria.
|
||||
*/
|
||||
hasThrow(permission: PermissionResolvable) {
|
||||
if (this.has(permission) && this.has("VIEW_CHANNEL")) return true;
|
||||
// @ts-ignore
|
||||
throw new HTTPError(`You are missing the following permissions ${permission}`, 403);
|
||||
}
|
||||
|
||||
overwriteChannel(overwrites: ChannelPermissionOverwrite[]) {
|
||||
if (!overwrites) return this;
|
||||
if (!this.cache) throw new Error("permission chache not available");
|
||||
overwrites = overwrites.filter((x) => {
|
||||
if (x.type === 0 && this.cache.roles?.some((r) => r.id === x.id)) return true;
|
||||
if (x.type === 1 && x.id == this.cache.user_id) return true;
|
||||
return false;
|
||||
});
|
||||
return new Permissions(Permissions.channelPermission(overwrites, this.bitfield));
|
||||
}
|
||||
|
||||
static channelPermission(overwrites: ChannelPermissionOverwrite[], init?: bigint) {
|
||||
// TODO: do not deny any permissions if admin
|
||||
return overwrites.reduce((permission, overwrite) => {
|
||||
// apply disallowed permission
|
||||
// * permission: current calculated permission (e.g. 010)
|
||||
// * deny contains all denied permissions (e.g. 011)
|
||||
// * allow contains all explicitly allowed permisions (e.g. 100)
|
||||
return (permission & ~BigInt(overwrite.deny)) | BigInt(overwrite.allow);
|
||||
// ~ operator inverts deny (e.g. 011 -> 100)
|
||||
// & operator only allows 1 for both ~deny and permission (e.g. 010 & 100 -> 000)
|
||||
// | operators adds both together (e.g. 000 + 100 -> 100)
|
||||
}, init || BigInt(0));
|
||||
}
|
||||
|
||||
static rolePermission(roles: Role[]) {
|
||||
// adds all permissions of all roles together (Bit OR)
|
||||
return roles.reduce((permission, role) => permission | BigInt(role.permissions), BigInt(0));
|
||||
}
|
||||
|
||||
static finalPermission({
|
||||
user,
|
||||
guild,
|
||||
channel,
|
||||
}: {
|
||||
user: { id: string; roles: string[] };
|
||||
guild: { roles: Role[] };
|
||||
channel?: {
|
||||
overwrites?: ChannelPermissionOverwrite[];
|
||||
recipient_ids?: string[] | null;
|
||||
owner_id?: string;
|
||||
};
|
||||
}) {
|
||||
if (user.id === "0") return new Permissions("ADMINISTRATOR"); // system user id
|
||||
|
||||
let roles = guild.roles.filter((x) => user.roles.includes(x.id));
|
||||
let permission = Permissions.rolePermission(roles);
|
||||
|
||||
if (channel?.overwrites) {
|
||||
let overwrites = channel.overwrites.filter((x) => {
|
||||
if (x.type === 0 && user.roles.includes(x.id)) return true;
|
||||
if (x.type === 1 && x.id == user.id) return true;
|
||||
return false;
|
||||
});
|
||||
permission = Permissions.channelPermission(overwrites, permission);
|
||||
}
|
||||
|
||||
if (channel?.recipient_ids) {
|
||||
if (channel?.owner_id === user.id) return new Permissions("ADMINISTRATOR");
|
||||
if (channel.recipient_ids.includes(user.id)) {
|
||||
// Default dm permissions
|
||||
return new Permissions([
|
||||
"VIEW_CHANNEL",
|
||||
"SEND_MESSAGES",
|
||||
"STREAM",
|
||||
"ADD_REACTIONS",
|
||||
"EMBED_LINKS",
|
||||
"ATTACH_FILES",
|
||||
"READ_MESSAGE_HISTORY",
|
||||
"MENTION_EVERYONE",
|
||||
"USE_EXTERNAL_EMOJIS",
|
||||
"CONNECT",
|
||||
"SPEAK",
|
||||
"MANAGE_CHANNELS",
|
||||
]);
|
||||
}
|
||||
|
||||
return new Permissions();
|
||||
}
|
||||
|
||||
return new Permissions(permission);
|
||||
}
|
||||
}
|
||||
|
||||
const ALL_PERMISSIONS = Object.values(Permissions.FLAGS).reduce((total, val) => total | val, BigInt(0));
|
||||
|
||||
export type PermissionCache = {
|
||||
channel?: Channel | undefined;
|
||||
member?: Member | undefined;
|
||||
guild?: Guild | undefined;
|
||||
roles?: Role[] | undefined;
|
||||
user_id?: string;
|
||||
};
|
||||
|
||||
export async function getPermission(
|
||||
user_id?: string,
|
||||
guild_id?: string,
|
||||
channel_id?: string,
|
||||
opts: {
|
||||
guild_select?: (keyof Guild)[];
|
||||
guild_relations?: string[];
|
||||
channel_select?: (keyof Channel)[];
|
||||
channel_relations?: string[];
|
||||
member_select?: (keyof Member)[];
|
||||
member_relations?: string[];
|
||||
} = {}
|
||||
) {
|
||||
if (!user_id) throw new HTTPError("User not found");
|
||||
var channel: Channel | undefined;
|
||||
var member: Member | undefined;
|
||||
var guild: Guild | undefined;
|
||||
|
||||
if (channel_id) {
|
||||
channel = await Channel.findOneOrFail({
|
||||
where: { id: channel_id },
|
||||
relations: ["recipients", ...(opts.channel_relations || [])],
|
||||
select: [
|
||||
"id",
|
||||
"recipients",
|
||||
"permission_overwrites",
|
||||
"owner_id",
|
||||
"guild_id",
|
||||
// @ts-ignore
|
||||
...(opts.channel_select || []),
|
||||
],
|
||||
});
|
||||
if (channel.guild_id) guild_id = channel.guild_id; // derive guild_id from the channel
|
||||
}
|
||||
|
||||
if (guild_id) {
|
||||
guild = await Guild.findOneOrFail({
|
||||
where: { id: guild_id },
|
||||
select: [
|
||||
"id",
|
||||
"owner_id",
|
||||
// @ts-ignore
|
||||
...(opts.guild_select || []),
|
||||
],
|
||||
relations: opts.guild_relations,
|
||||
});
|
||||
if (guild.owner_id === user_id) return new Permissions(Permissions.FLAGS.ADMINISTRATOR);
|
||||
|
||||
member = await Member.findOneOrFail({
|
||||
where: { guild_id, id: user_id },
|
||||
relations: ["roles", ...(opts.member_relations || [])],
|
||||
select: [
|
||||
"id",
|
||||
"roles",
|
||||
// @ts-ignore
|
||||
...(opts.member_select || []),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
let recipient_ids: any = channel?.recipients?.map((x) => x.user_id);
|
||||
if (!recipient_ids?.length) recipient_ids = null;
|
||||
|
||||
// TODO: remove guild.roles and convert recipient_ids to recipients
|
||||
var permission = Permissions.finalPermission({
|
||||
user: {
|
||||
id: user_id,
|
||||
roles: member?.roles.map((x) => x.id) || [],
|
||||
},
|
||||
guild: {
|
||||
roles: member?.roles || [],
|
||||
},
|
||||
channel: {
|
||||
overwrites: channel?.permission_overwrites,
|
||||
owner_id: channel?.owner_id,
|
||||
recipient_ids,
|
||||
},
|
||||
});
|
||||
|
||||
const obj = new Permissions(permission);
|
||||
|
||||
// pass cache to permission for possible future getPermission calls
|
||||
obj.cache = { guild, member, channel, roles: member?.roles, user_id };
|
||||
|
||||
return obj;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import amqp, { Connection, Channel } from "amqplib";
|
||||
// import Config from "./Config";
|
||||
|
||||
export const RabbitMQ: { connection: Connection | null; channel: Channel | null; init: () => Promise<void> } = {
|
||||
connection: null,
|
||||
channel: null,
|
||||
init: async function () {
|
||||
return;
|
||||
// const host = Config.get().rabbitmq.host;
|
||||
// if (!host) return;
|
||||
// console.log(`[RabbitMQ] connect: ${host}`);
|
||||
// this.connection = await amqp.connect(host, {
|
||||
// timeout: 1000 * 60,
|
||||
// });
|
||||
// console.log(`[RabbitMQ] connected`);
|
||||
// this.channel = await this.connection.createChannel();
|
||||
// console.log(`[RabbitMQ] channel created`);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
export const DOUBLE_WHITE_SPACE = /\s\s+/g;
|
||||
export const SPECIAL_CHAR = /[@#`:\r\n\t\f\v\p{C}]/gu;
|
||||
export const CHANNEL_MENTION = /<#(\d+)>/g;
|
||||
export const USER_MENTION = /<@!?(\d+)>/g;
|
||||
export const ROLE_MENTION = /<@&(\d+)>/g;
|
||||
export const EVERYONE_MENTION = /@everyone/g;
|
||||
export const HERE_MENTION = /@here/g;
|
||||
@@ -0,0 +1,102 @@
|
||||
import { BitField } from "./BitField";
|
||||
import "missing-native-js-functions";
|
||||
import { BitFieldResolvable, BitFlag } from "./BitField";
|
||||
import { User } from "../entities";
|
||||
|
||||
var HTTPError: any;
|
||||
|
||||
try {
|
||||
HTTPError = require("lambert-server").HTTPError;
|
||||
} catch (e) {
|
||||
HTTPError = Error;
|
||||
}
|
||||
|
||||
export type RightResolvable = bigint | number | Rights | RightResolvable[] | RightString;
|
||||
|
||||
type RightString = keyof typeof Rights.FLAGS;
|
||||
// TODO: just like roles for members, users should have privilidges which combine multiple rights into one and make it easy to assign
|
||||
|
||||
export class Rights extends BitField {
|
||||
constructor(bits: BitFieldResolvable = 0) {
|
||||
super(bits);
|
||||
if (this.bitfield & Rights.FLAGS.OPERATOR) {
|
||||
this.bitfield = ALL_RIGHTS;
|
||||
}
|
||||
}
|
||||
|
||||
static FLAGS = {
|
||||
OPERATOR: BitFlag(0), // has all rights
|
||||
MANAGE_APPLICATIONS: BitFlag(1),
|
||||
MANAGE_GUILDS: BitFlag(2),
|
||||
MANAGE_MESSAGES: BitFlag(3), // Can't see other messages but delete/edit them in channels that they can see
|
||||
MANAGE_RATE_LIMITS: BitFlag(4),
|
||||
MANAGE_ROUTING: BitFlag(5), // can create custom message routes to any channel/guild
|
||||
MANAGE_TICKETS: BitFlag(6), // can respond to and resolve support tickets
|
||||
MANAGE_USERS: BitFlag(7),
|
||||
ADD_MEMBERS: BitFlag(8), // can manually add any members in their guilds
|
||||
BYPASS_RATE_LIMITS: BitFlag(9),
|
||||
CREATE_APPLICATIONS: BitFlag(10),
|
||||
CREATE_CHANNELS: BitFlag(11), // can create guild channels or threads in the guilds that they have permission
|
||||
CREATE_DMS: BitFlag(12),
|
||||
CREATE_DM_GROUPS: BitFlag(13), // can create group DMs or custom orphan channels
|
||||
CREATE_GUILDS: BitFlag(14),
|
||||
CREATE_INVITES: BitFlag(15), // can create mass invites in the guilds that they have CREATE_INSTANT_INVITE
|
||||
CREATE_ROLES: BitFlag(16),
|
||||
CREATE_TEMPLATES: BitFlag(17),
|
||||
CREATE_WEBHOOKS: BitFlag(18),
|
||||
JOIN_GUILDS: BitFlag(19),
|
||||
PIN_MESSAGES: BitFlag(20),
|
||||
SELF_ADD_REACTIONS: BitFlag(21),
|
||||
SELF_DELETE_MESSAGES: BitFlag(22),
|
||||
SELF_EDIT_MESSAGES: BitFlag(23),
|
||||
SELF_EDIT_NAME: BitFlag(24),
|
||||
SEND_MESSAGES: BitFlag(25),
|
||||
USE_ACTIVITIES: BitFlag(26), // use (game) activities in voice channels (e.g. Watch together)
|
||||
USE_VIDEO: BitFlag(27),
|
||||
USE_VOICE: BitFlag(28),
|
||||
INVITE_USERS: BitFlag(29), // can create user-specific invites in the guilds that they have INVITE_USERS
|
||||
SELF_DELETE_DISABLE: BitFlag(30), // can disable/delete own account
|
||||
DEBTABLE: BitFlag(31), // can use pay-to-use features
|
||||
CREDITABLE: BitFlag(32), // can receive money from monetisation related features
|
||||
KICK_BAN_MEMBERS: BitFlag(33),
|
||||
// can kick or ban guild or group DM members in the guilds/groups that they have KICK_MEMBERS, or BAN_MEMBERS
|
||||
SELF_LEAVE_GROUPS: BitFlag(34),
|
||||
// can leave the guilds or group DMs that they joined on their own (one can always leave a guild or group DMs they have been force-added)
|
||||
PRESENCE: BitFlag(35),
|
||||
// inverts the presence confidentiality default (OPERATOR's presence is not routed by default, others' are) for a given user
|
||||
SELF_ADD_DISCOVERABLE: BitFlag(36), // can mark discoverable guilds that they have permissions to mark as discoverable
|
||||
MANAGE_GUILD_DIRECTORY: BitFlag(37), // can change anything in the primary guild directory
|
||||
POGGERS: BitFlag(38), // can send confetti, screenshake, random user mention (@someone)
|
||||
USE_ACHIEVEMENTS: BitFlag(39), // can use achievements and cheers
|
||||
INITIATE_INTERACTIONS: BitFlag(40), // can initiate interactions
|
||||
RESPOND_TO_INTERACTIONS: BitFlag(41), // can respond to interactions
|
||||
SEND_BACKDATED_EVENTS: BitFlag(42), // can send backdated events
|
||||
USE_MASS_INVITES: BitFlag(43), // added per @xnacly's request — can accept mass invites
|
||||
ACCEPT_INVITES: BitFlag(44) // added per @xnacly's request — can accept user-specific invites and DM requests
|
||||
};
|
||||
|
||||
any(permission: RightResolvable, checkOperator = true) {
|
||||
return (checkOperator && super.any(Rights.FLAGS.OPERATOR)) || super.any(permission);
|
||||
}
|
||||
|
||||
has(permission: RightResolvable, checkOperator = true) {
|
||||
return (checkOperator && super.has(Rights.FLAGS.OPERATOR)) || super.has(permission);
|
||||
}
|
||||
|
||||
hasThrow(permission: RightResolvable) {
|
||||
if (this.has(permission)) return true;
|
||||
// @ts-ignore
|
||||
throw new HTTPError(`You are missing the following rights ${permission}`, 403);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const ALL_RIGHTS = Object.values(Rights.FLAGS).reduce((total, val) => total | val, BigInt(0));
|
||||
|
||||
export async function getRights( user_id: string
|
||||
/**, opts: {
|
||||
in_behalf?: (keyof User)[];
|
||||
} = {} **/) {
|
||||
let user = await User.findOneOrFail({ where: { id: user_id } });
|
||||
return new Rights(user.rights);
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// @ts-nocheck
|
||||
import * as cluster from "cluster";
|
||||
|
||||
// https://github.com/discordjs/discord.js/blob/master/src/util/Snowflake.js
|
||||
// Apache License Version 2.0 Copyright 2015 - 2021 Amish Shah
|
||||
("use strict");
|
||||
|
||||
// Discord epoch (2015-01-01T00:00:00.000Z)
|
||||
|
||||
/**
|
||||
* A container for useful snowflake-related methods.
|
||||
*/
|
||||
export class Snowflake {
|
||||
static readonly EPOCH = 1420070400000;
|
||||
static INCREMENT = 0n; // max 4095
|
||||
static processId = BigInt(process.pid % 31); // max 31
|
||||
static workerId = BigInt((cluster.worker?.id || 0) % 31); // max 31
|
||||
|
||||
constructor() {
|
||||
throw new Error(`The ${this.constructor.name} class may not be instantiated.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* A Twitter-like snowflake, except the epoch is 2015-01-01T00:00:00.000Z
|
||||
* ```
|
||||
* If we have a snowflake '266241948824764416' we can represent it as binary:
|
||||
*
|
||||
* 64 22 17 12 0
|
||||
* 000000111011000111100001101001000101000000 00001 00000 000000000000
|
||||
* number of ms since Discord epoch worker pid increment
|
||||
* ```
|
||||
* @typedef {string} Snowflake
|
||||
*/
|
||||
|
||||
/**
|
||||
* Transforms a snowflake from a decimal string to a bit string.
|
||||
* @param {Snowflake} num Snowflake to be transformed
|
||||
* @returns {string}
|
||||
* @private
|
||||
*/
|
||||
static idToBinary(num) {
|
||||
let bin = "";
|
||||
let high = parseInt(num.slice(0, -10)) || 0;
|
||||
let low = parseInt(num.slice(-10));
|
||||
while (low > 0 || high > 0) {
|
||||
bin = String(low & 1) + bin;
|
||||
low = Math.floor(low / 2);
|
||||
if (high > 0) {
|
||||
low += 5000000000 * (high % 2);
|
||||
high = Math.floor(high / 2);
|
||||
}
|
||||
}
|
||||
return bin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a snowflake from a bit string to a decimal string.
|
||||
* @param {string} num Bit string to be transformed
|
||||
* @returns {Snowflake}
|
||||
* @private
|
||||
*/
|
||||
static binaryToID(num) {
|
||||
let dec = "";
|
||||
|
||||
while (num.length > 50) {
|
||||
const high = parseInt(num.slice(0, -32), 2);
|
||||
const low = parseInt((high % 10).toString(2) + num.slice(-32), 2);
|
||||
|
||||
dec = (low % 10).toString() + dec;
|
||||
num =
|
||||
Math.floor(high / 10).toString(2) +
|
||||
Math.floor(low / 10)
|
||||
.toString(2)
|
||||
.padStart(32, "0");
|
||||
}
|
||||
|
||||
num = parseInt(num, 2);
|
||||
while (num > 0) {
|
||||
dec = (num % 10).toString() + dec;
|
||||
num = Math.floor(num / 10);
|
||||
}
|
||||
|
||||
return dec;
|
||||
}
|
||||
|
||||
static generateWorkerProcess() { // worker process - returns a number
|
||||
var time = BigInt(Date.now() - Snowflake.EPOCH) << BigInt(22);
|
||||
var worker = Snowflake.workerId << 17n;
|
||||
var process = Snowflake.processId << 12n;
|
||||
var increment = Snowflake.INCREMENT++;
|
||||
return BigInt(time | worker | process | increment);
|
||||
}
|
||||
|
||||
static generate() {
|
||||
return Snowflake.generateWorkerProcess().toString();
|
||||
}
|
||||
/**
|
||||
* A deconstructed snowflake.
|
||||
* @typedef {Object} DeconstructedSnowflake
|
||||
* @property {number} timestamp Timestamp the snowflake was created
|
||||
* @property {Date} date Date the snowflake was created
|
||||
* @property {number} workerID Worker ID in the snowflake
|
||||
* @property {number} processID Process ID in the snowflake
|
||||
* @property {number} increment Increment in the snowflake
|
||||
* @property {string} binary Binary representation of the snowflake
|
||||
*/
|
||||
|
||||
/**
|
||||
* Deconstructs a Discord snowflake.
|
||||
* @param {Snowflake} snowflake Snowflake to deconstruct
|
||||
* @returns {DeconstructedSnowflake} Deconstructed snowflake
|
||||
*/
|
||||
static deconstruct(snowflake) {
|
||||
const BINARY = Snowflake.idToBinary(snowflake).toString(2).padStart(64, "0");
|
||||
const res = {
|
||||
timestamp: parseInt(BINARY.substring(0, 42), 2) + Snowflake.EPOCH,
|
||||
workerID: parseInt(BINARY.substring(42, 47), 2),
|
||||
processID: parseInt(BINARY.substring(47, 52), 2),
|
||||
increment: parseInt(BINARY.substring(52, 64), 2),
|
||||
binary: BINARY,
|
||||
};
|
||||
Object.defineProperty(res, "date", {
|
||||
get: function get() {
|
||||
return new Date(this.timestamp);
|
||||
},
|
||||
enumerable: true,
|
||||
});
|
||||
return res;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { SPECIAL_CHAR } from "./Regex";
|
||||
|
||||
export function trimSpecial(str?: string): string {
|
||||
// @ts-ignore
|
||||
if (!str) return;
|
||||
return str.replace(SPECIAL_CHAR, "").trim();
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import jwt, { VerifyOptions } from "jsonwebtoken";
|
||||
import { Config } from "./Config";
|
||||
import { User } from "../entities";
|
||||
|
||||
export const JWTOptions: VerifyOptions = { algorithms: ["HS256"] };
|
||||
|
||||
export function checkToken(token: string, jwtSecret: string): Promise<any> {
|
||||
return new Promise((res, rej) => {
|
||||
token = token.replace("Bot ", "");
|
||||
/**
|
||||
in fosscord, even with instances that have bot distinction; we won't enforce "Bot" prefix,
|
||||
as we don't really have separate pathways for bots
|
||||
**/
|
||||
|
||||
jwt.verify(token, jwtSecret, JWTOptions, async (err, decoded: any) => {
|
||||
if (err || !decoded) return rej("Invalid Token");
|
||||
|
||||
const user = await User.findOne({
|
||||
where: { id: decoded.id },
|
||||
select: ["data", "bot", "disabled", "deleted", "rights"]
|
||||
});
|
||||
if (!user) return rej("Invalid Token");
|
||||
// we need to round it to seconds as it saved as seconds in jwt iat and valid_tokens_since is stored in milliseconds
|
||||
if (decoded.iat * 1000 < new Date(user.data.valid_tokens_since).setSeconds(0, 0))
|
||||
return rej("Invalid Token");
|
||||
if (user.disabled) return rej("User disabled");
|
||||
if (user.deleted) return rej("User not found");
|
||||
|
||||
return res({ decoded, user });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function generateToken(id: string) {
|
||||
const iat = Math.floor(Date.now() / 1000);
|
||||
const algorithm = "HS256";
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
jwt.sign(
|
||||
{ id: id, iat },
|
||||
Config.get().security.jwtSecret,
|
||||
{
|
||||
algorithm,
|
||||
},
|
||||
(err, token) => {
|
||||
if (err) return rej(err);
|
||||
return res(token);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Server, traverseDirectory } from "lambert-server";
|
||||
|
||||
//if we're using ts-node, use ts files instead of js
|
||||
const extension = Symbol.for("ts-node.register.instance") in process ? "ts" : "js"
|
||||
|
||||
const DEFAULT_FILTER = new RegExp("^([^\.].*)(?<!\.d)\.(" + extension + ")$");
|
||||
|
||||
export function registerRoutes(server: Server, root: string) {
|
||||
return traverseDirectory(
|
||||
{ dirname: root, recursive: true, filter: DEFAULT_FILTER },
|
||||
server.registerRoute.bind(server, root)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import FormData from "form-data";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import fetch from "node-fetch";
|
||||
import { Attachment } from "../entities";
|
||||
import { Config } from "./Config";
|
||||
|
||||
export async function uploadFile(path: string, file?: Express.Multer.File): Promise<Attachment> {
|
||||
if (!file?.buffer) throw new HTTPError("Missing file in body");
|
||||
|
||||
const form = new FormData();
|
||||
form.append("file", file.buffer, {
|
||||
contentType: file.mimetype,
|
||||
filename: file.originalname,
|
||||
});
|
||||
|
||||
const response = await fetch(`${Config.get().cdn.endpointPrivate || "http://localhost:3003"}${path}`, {
|
||||
headers: {
|
||||
signature: Config.get().security.requestSignature,
|
||||
...form.getHeaders(),
|
||||
},
|
||||
method: "POST",
|
||||
body: form,
|
||||
});
|
||||
const result = await response.json() as Attachment;
|
||||
|
||||
if (response.status !== 200) throw result;
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function handleFile(path: string, body?: string): Promise<string | undefined> {
|
||||
if (!body || !body.startsWith("data:")) return undefined;
|
||||
try {
|
||||
const mimetype = body.split(":")[1].split(";")[0];
|
||||
const buffer = Buffer.from(body.split(",")[1], "base64");
|
||||
|
||||
// @ts-ignore
|
||||
const { id } = await uploadFile(path, { buffer, mimetype, originalname: "banner" });
|
||||
return id;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new HTTPError("Invalid " + path);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteFile(path: string) {
|
||||
const response = await fetch(`${Config.get().cdn.endpointPrivate || "http://localhost:3003"}${path}`, {
|
||||
headers: {
|
||||
signature: Config.get().security.requestSignature,
|
||||
},
|
||||
method: "DELETE",
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (response.status !== 200) throw result;
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export * from "./ApiError";
|
||||
export * from "./BitField";
|
||||
export * from "./Token";
|
||||
//export * from "./Categories";
|
||||
export * from "./cdn";
|
||||
export * from "./Config";
|
||||
export * from "./Constants";
|
||||
export * from "./Database";
|
||||
export * from "./Email";
|
||||
export * from "./Event";
|
||||
export * from "./FieldError";
|
||||
export * from "./Intents";
|
||||
export * from "./MessageFlags";
|
||||
export * from "./Permissions";
|
||||
export * from "./RabbitMQ";
|
||||
export * from "./Regex";
|
||||
export * from "./Rights";
|
||||
export * from "./Snowflake";
|
||||
export * from "./String";
|
||||
export * from "./Array";
|
||||
export * from "./TraverseDirectory";
|
||||
export * from "./InvisibleCharacters";
|
||||
export * from "./BannedWords";
|
||||
Reference in New Issue
Block a user