import { PublicUser, User } from "./User"; import { BaseClass } from "./BaseClass"; import { Column, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany, RelationId } from "typeorm"; import { Guild } from "./Guild"; import { Config, emitEvent } from "../util"; import { GuildCreateEvent, GuildDeleteEvent, GuildMemberAddEvent, GuildMemberRemoveEvent, GuildMemberUpdateEvent, } from "../interfaces"; import { HTTPError } from "lambert-server"; import { Role } from "./Role"; import { ReadState } from "./ReadState"; @Entity("members") export class Member extends BaseClass { @JoinColumn({ name: "id" }) @ManyToOne(() => User) user: User; @Column({ nullable: true }) @RelationId((member: Member) => member.guild) guild_id: string; @JoinColumn({ name: "guild_id" }) @ManyToOne(() => Guild) guild: Guild; @Column({ nullable: true }) nick?: string; @JoinTable({ name: "member_roles" }) @ManyToMany(() => Role) roles: Role[]; @Column() joined_at: Date; @Column({ nullable: true }) premium_since?: number; @Column() deaf: boolean; @Column() mute: boolean; @Column() pending: boolean; @Column({ type: "simple-json" }) settings: UserGuildSettings; // TODO: update // @Column({ type: "simple-json" }) // read_state: ReadState; static async IsInGuildOrFail(user_id: string, guild_id: string) { if (await Member.count({ 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_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: guild_id, user: member.user, }, guild_id: guild_id, } as GuildMemberRemoveEvent), ]); } static async addRole(user_id: string, guild_id: string, role_id: string) { const [member] = await Promise.all([ // @ts-ignore Member.findOneOrFail({ where: { id: user_id, guild_id: guild_id }, relations: ["user", "roles"], // we don't want to load the role objects just the ids select: ["roles.id"], }), await Role.findOneOrFail({ id: role_id, guild_id: guild_id }), ]); member.roles.push(new Role({ id: role_id })); await Promise.all([ member.save(), emitEvent({ event: "GUILD_MEMBER_UPDATE", data: { guild_id: guild_id, user: member.user, roles: member.roles.map((x) => x.id), }, guild_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: guild_id }, relations: ["user", "roles"], // we don't want to load the role objects just the ids select: ["roles.id"], }), await Role.findOneOrFail({ id: role_id, guild_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: guild_id, user: member.user, roles: member.roles.map((x) => x.id), }, guild_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: guild_id, }, relations: ["user"], }); member.nick = nickname; await Promise.all([ member.save(), emitEvent({ event: "GUILD_MEMBER_UPDATE", data: { guild_id: guild_id, user: member.user, nick: nickname, }, guild_id: guild_id, } as GuildMemberUpdateEvent), ]); } static async addToGuild(user_id: string, guild_id: string) { const user = await User.getPublicUser(user_id); const { maxGuilds } = Config.get().limits.user; const guild_count = await Member.count({ 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: ["channels", "emojis", "members", "roles", "stickers"], }); if (await Member.count({ 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: guild_id, nick: undefined, roles: [guild_id], // @everyone role joined_at: new Date(), premium_since: undefined, deaf: false, mute: false, pending: false, }; // @ts-ignore guild.joined_at = member.joined_at.toISOString(); await Promise.all([ Member.insert({ ...member, roles: undefined, // read_state: {}, settings: { channel_overrides: [], message_notifications: 0, mobile_push: true, muted: false, suppress_everyone: false, suppress_roles: false, version: 0, }, }), Guild.increment({ id: guild_id }, "member_count", 1), emitEvent({ event: "GUILD_MEMBER_ADD", data: { ...member, user, guild_id: guild_id, }, guild_id: guild_id, } as GuildMemberAddEvent), emitEvent({ event: "GUILD_CREATE", data: { ...guild, members: [...guild.members, member] }, 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 };