diff options
Diffstat (limited to 'src/util/entities/Member.ts')
-rw-r--r-- | src/util/entities/Member.ts | 360 |
1 files changed, 360 insertions, 0 deletions
diff --git a/src/util/entities/Member.ts b/src/util/entities/Member.ts new file mode 100644 index 00000000..baac58ed --- /dev/null +++ b/src/util/entities/Member.ts @@ -0,0 +1,360 @@ +import { PublicUser, User } from "./User"; +import { BaseClass } from "./BaseClass"; +import { + Column, + Entity, + Index, + JoinColumn, + JoinTable, + ManyToMany, + ManyToOne, + PrimaryGeneratedColumn, + RelationId, +} from "typeorm"; +import { Guild } from "./Guild"; +import { Config, emitEvent } from "../util"; +import { + GuildCreateEvent, + GuildDeleteEvent, + GuildMemberAddEvent, + GuildMemberRemoveEvent, + GuildMemberUpdateEvent, +} from "../interfaces"; +import { HTTPError } from "../util/imports/HTTPError"; +import { Role } from "./Role"; +import { BaseClassWithoutId } from "./BaseClass"; +import { Ban, PublicGuildRelations } from "."; +import { DiscordApiErrors } from "../util/Constants"; +import { OrmUtils } from "../util/imports/OrmUtils"; + +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; + + // 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, + }; + //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<Member, Omit<PublicMemberKeys, "roles">> & { + user: PublicUser; + roles: string[]; // only role ids not objects +}; |