mirror of
https://github.com/spacebarchat/server.git
synced 2026-05-25 12:04:39 +00:00
9aeca6c03d
Welcome messages
373 lines
9.0 KiB
TypeScript
373 lines
9.0 KiB
TypeScript
import { Column, Entity, Index, JoinColumn, JoinTable, ManyToMany, ManyToOne, PrimaryGeneratedColumn, RelationId } from "typeorm";
|
|
import { Ban, PublicGuildRelations, Message } from ".";
|
|
import { GuildCreateEvent, GuildDeleteEvent, GuildMemberAddEvent, GuildMemberRemoveEvent, GuildMemberUpdateEvent, MessageCreateEvent } 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: [],
|
|
embedded_activities: [],
|
|
},
|
|
user_id
|
|
} as GuildCreateEvent)
|
|
]);
|
|
|
|
if (guild.system_channel_id) {
|
|
// send welcome message
|
|
const message = OrmUtils.mergeDeep(new Message(), {
|
|
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 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
|
|
};
|