import { Column, Entity, Index, JoinColumn, JoinTable, ManyToMany, ManyToOne, PrimaryGeneratedColumn, RelationId } from "typeorm"; import { Ban, PublicGuildRelations } from "."; import { GuildCreateEvent, GuildDeleteEvent, GuildMemberAddEvent, GuildMemberRemoveEvent, GuildMemberUpdateEvent } from "../interfaces"; import { Config, emitEvent } from "../util"; import { DiscordApiErrors } from "../util/Constants"; import { HTTPError } from "../util/imports/HTTPError"; import { OrmUtils } from "../util/imports/OrmUtils"; import { BaseClassWithoutId } from "./BaseClass"; import { Guild } from "./Guild"; import { Role } from "./Role"; import { PublicUser, User } from "./User"; 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({ nullable: true }) premium_since?: Date; @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; @Column({ nullable: true }) avatar: string; @Column({ nullable: true }) banner: string; @Column() bio: string; @Column({ nullable: true }) communication_disabled_until: Date; // TODO: add this when we have proper read receipts // @Column({ type: "simple-json" }) // read_state: ReadState; 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", "member_count"], 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 //TODO: check for bugs if (guild.member_count) guild.member_count--; 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 select: ["index"] }), Role.findOneOrFail({ where: { id: role_id, guild_id }, select: ["id"] }) ]); member.roles.push(OrmUtils.mergeDeep(new Role(), { 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 select: ["index"] }), 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 }); 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: null, deaf: false, mute: false, pending: false, avatar: null, banner: null, bio: "", communication_disabled_until: null }; //TODO: check for bugs if (guild.member_count) guild.member_count++; await Promise.all([ OrmUtils.mergeDeep(new Member(), { ...member, roles: [OrmUtils.mergeDeep(new Role(), { id: guild_id })], // read_state: {}, settings: { 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) ]); } } export interface UserGuildSettings { channel_overrides: { channel_id: string; message_notifications: number; mute_config: MuteConfig; muted: boolean; }[]; message_notifications: number; mobile_push: boolean; mute_config: MuteConfig; muted: boolean; suppress_everyone: boolean; suppress_roles: boolean; version: number; } 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> & { user: PublicUser; roles: string[]; // only role ids not objects };