diff options
Diffstat (limited to 'src/util/entities/Member.ts')
-rw-r--r-- | src/util/entities/Member.ts | 430 |
1 files changed, 430 insertions, 0 deletions
diff --git a/src/util/entities/Member.ts b/src/util/entities/Member.ts new file mode 100644 index 00000000..d7bcefea --- /dev/null +++ b/src/util/entities/Member.ts @@ -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 +}; |