Merge branch 'fosscord:master' into master
This commit is contained in:
		
						commit
						edd5cf651c
					
				| @ -37,7 +37,11 @@ export function isTextChannel(type: ChannelType): boolean { | ||||
| 		case ChannelType.GUILD_PUBLIC_THREAD: | ||||
| 		case ChannelType.GUILD_PRIVATE_THREAD: | ||||
| 		case ChannelType.GUILD_TEXT: | ||||
| 		case ChannelType.ENCRYPTED: | ||||
| 		case ChannelType.ENCRYPTED_THREAD: | ||||
| 			return true; | ||||
| 		default: | ||||
| 			throw new HTTPError("unimplemented", 400); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| @ -87,7 +91,7 @@ router.get("/", async (req: Request, res: Response) => { | ||||
| 	permissions.hasThrow("VIEW_CHANNEL"); | ||||
| 	if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]); | ||||
| 
 | ||||
| 	var query: FindManyOptions<Message> & { where: { id?: any } } = { | ||||
| 	var query: FindManyOptions<Message> & { where: { id?: any; }; } = { | ||||
| 		order: { id: "DESC" }, | ||||
| 		take: limit, | ||||
| 		where: { channel_id }, | ||||
| @ -216,7 +220,7 @@ router.post( | ||||
| 			channel.save() | ||||
| 		]); | ||||
| 
 | ||||
| 		postHandleMessage(message).catch((e) => {}); // no await as it shouldnt block the message send function and silently catch error
 | ||||
| 		postHandleMessage(message).catch((e) => { }); // no await as it shouldnt block the message send function and silently catch error
 | ||||
| 
 | ||||
| 		return res.json(message); | ||||
| 	} | ||||
|  | ||||
| @ -1,332 +1,357 @@ | ||||
| import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm"; | ||||
| import { BaseClass } from "./BaseClass"; | ||||
| import { Guild } from "./Guild"; | ||||
| import { PublicUserProjection, User } from "./User"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { containsAll, emitEvent, getPermission, Snowflake, trimSpecial } from "../util"; | ||||
| import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces"; | ||||
| import { Recipient } from "./Recipient"; | ||||
| import { Message } from "./Message"; | ||||
| import { ReadState } from "./ReadState"; | ||||
| import { Invite } from "./Invite"; | ||||
| import { VoiceState } from "./VoiceState"; | ||||
| import { Webhook } from "./Webhook"; | ||||
| import { DmChannelDTO } from "../dtos"; | ||||
| 
 | ||||
| export enum ChannelType { | ||||
| 	GUILD_TEXT = 0, // a text channel within a server
 | ||||
| 	DM = 1, // a direct message between users
 | ||||
| 	GUILD_VOICE = 2, // a voice channel within a server
 | ||||
| 	GROUP_DM = 3, // a direct message between multiple users
 | ||||
| 	GUILD_CATEGORY = 4, // an organizational category that contains up to 50 channels
 | ||||
| 	GUILD_NEWS = 5, // a channel that users can follow and crosspost into their own server
 | ||||
| 	GUILD_STORE = 6, // a channel in which game developers can sell their game on Discord
 | ||||
| 	// TODO: what are channel types between 7-9?
 | ||||
| 	GUILD_NEWS_THREAD = 10, // a temporary sub-channel within a GUILD_NEWS channel
 | ||||
| 	GUILD_PUBLIC_THREAD = 11, // a temporary sub-channel within a GUILD_TEXT channel
 | ||||
| 	GUILD_PRIVATE_THREAD = 12, // a temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission
 | ||||
| 	GUILD_STAGE_VOICE = 13, // a voice channel for hosting events with an audience
 | ||||
| } | ||||
| 
 | ||||
| @Entity("channels") | ||||
| export class Channel extends BaseClass { | ||||
| 	@Column() | ||||
| 	created_at: Date; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	name?: string; | ||||
| 
 | ||||
| 	@Column({ type: "text", nullable: true }) | ||||
| 	icon?: string | null; | ||||
| 
 | ||||
| 	@Column({ type: "int" }) | ||||
| 	type: ChannelType; | ||||
| 
 | ||||
| 	@OneToMany(() => Recipient, (recipient: Recipient) => recipient.channel, { | ||||
| 		cascade: true, | ||||
| 		orphanedRowAction: "delete", | ||||
| 	}) | ||||
| 	recipients?: Recipient[]; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	last_message_id: string; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	@RelationId((channel: Channel) => channel.guild) | ||||
| 	guild_id?: string; | ||||
| 
 | ||||
| 	@JoinColumn({ name: "guild_id" }) | ||||
| 	@ManyToOne(() => Guild, { | ||||
| 		onDelete: "CASCADE", | ||||
| 	}) | ||||
| 	guild: Guild; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	@RelationId((channel: Channel) => channel.parent) | ||||
| 	parent_id: string; | ||||
| 
 | ||||
| 	@JoinColumn({ name: "parent_id" }) | ||||
| 	@ManyToOne(() => Channel) | ||||
| 	parent?: Channel; | ||||
| 
 | ||||
| 	// only for group dms
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	@RelationId((channel: Channel) => channel.owner) | ||||
| 	owner_id: string; | ||||
| 
 | ||||
| 	@JoinColumn({ name: "owner_id" }) | ||||
| 	@ManyToOne(() => User) | ||||
| 	owner: User; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	last_pin_timestamp?: number; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	default_auto_archive_duration?: number; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	position?: number; | ||||
| 
 | ||||
| 	@Column({ type: "simple-json", nullable: true }) | ||||
| 	permission_overwrites?: ChannelPermissionOverwrite[]; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	video_quality_mode?: number; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	bitrate?: number; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	user_limit?: number; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	nsfw?: boolean; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	rate_limit_per_user?: number; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	topic?: string; | ||||
| 
 | ||||
| 	@OneToMany(() => Invite, (invite: Invite) => invite.channel, { | ||||
| 		cascade: true, | ||||
| 		orphanedRowAction: "delete", | ||||
| 	}) | ||||
| 	invites?: Invite[]; | ||||
| 
 | ||||
| 	@OneToMany(() => Message, (message: Message) => message.channel, { | ||||
| 		cascade: true, | ||||
| 		orphanedRowAction: "delete", | ||||
| 	}) | ||||
| 	messages?: Message[]; | ||||
| 
 | ||||
| 	@OneToMany(() => VoiceState, (voice_state: VoiceState) => voice_state.channel, { | ||||
| 		cascade: true, | ||||
| 		orphanedRowAction: "delete", | ||||
| 	}) | ||||
| 	voice_states?: VoiceState[]; | ||||
| 
 | ||||
| 	@OneToMany(() => ReadState, (read_state: ReadState) => read_state.channel, { | ||||
| 		cascade: true, | ||||
| 		orphanedRowAction: "delete", | ||||
| 	}) | ||||
| 	read_states?: ReadState[]; | ||||
| 
 | ||||
| 	@OneToMany(() => Webhook, (webhook: Webhook) => webhook.channel, { | ||||
| 		cascade: true, | ||||
| 		orphanedRowAction: "delete", | ||||
| 	}) | ||||
| 	webhooks?: Webhook[]; | ||||
| 
 | ||||
| 	// TODO: DM channel
 | ||||
| 	static async createChannel( | ||||
| 		channel: Partial<Channel>, | ||||
| 		user_id: string = "0", | ||||
| 		opts?: { | ||||
| 			keepId?: boolean; | ||||
| 			skipExistsCheck?: boolean; | ||||
| 			skipPermissionCheck?: boolean; | ||||
| 			skipEventEmit?: boolean; | ||||
| 		} | ||||
| 	) { | ||||
| 		if (!opts?.skipPermissionCheck) { | ||||
| 			// Always check if user has permission first
 | ||||
| 			const permissions = await getPermission(user_id, channel.guild_id); | ||||
| 			permissions.hasThrow("MANAGE_CHANNELS"); | ||||
| 		} | ||||
| 
 | ||||
| 		switch (channel.type) { | ||||
| 			case ChannelType.GUILD_TEXT: | ||||
| 			case ChannelType.GUILD_VOICE: | ||||
| 				if (channel.parent_id && !opts?.skipExistsCheck) { | ||||
| 					const exists = await Channel.findOneOrFail({ id: channel.parent_id }); | ||||
| 					if (!exists) throw new HTTPError("Parent id channel doesn't exist", 400); | ||||
| 					if (exists.guild_id !== channel.guild_id) | ||||
| 						throw new HTTPError("The category channel needs to be in the guild"); | ||||
| 				} | ||||
| 				break; | ||||
| 			case ChannelType.GUILD_CATEGORY: | ||||
| 				break; | ||||
| 			case ChannelType.DM: | ||||
| 			case ChannelType.GROUP_DM: | ||||
| 				throw new HTTPError("You can't create a dm channel in a guild"); | ||||
| 			// TODO: check if guild is community server
 | ||||
| 			case ChannelType.GUILD_STORE: | ||||
| 			case ChannelType.GUILD_NEWS: | ||||
| 			default: | ||||
| 				throw new HTTPError("Not yet supported"); | ||||
| 		} | ||||
| 
 | ||||
| 		if (!channel.permission_overwrites) channel.permission_overwrites = []; | ||||
| 		// TODO: auto generate position
 | ||||
| 
 | ||||
| 		channel = { | ||||
| 			...channel, | ||||
| 			...(!opts?.keepId && { id: Snowflake.generate() }), | ||||
| 			created_at: new Date(), | ||||
| 			position: channel.position || 0, | ||||
| 		}; | ||||
| 
 | ||||
| 		await Promise.all([ | ||||
| 			new Channel(channel).save(), | ||||
| 			!opts?.skipEventEmit | ||||
| 				? emitEvent({ | ||||
| 						event: "CHANNEL_CREATE", | ||||
| 						data: channel, | ||||
| 						guild_id: channel.guild_id, | ||||
| 				  } as ChannelCreateEvent) | ||||
| 				: Promise.resolve(), | ||||
| 		]); | ||||
| 
 | ||||
| 		return channel; | ||||
| 	} | ||||
| 
 | ||||
| 	static async createDMChannel(recipients: string[], creator_user_id: string, name?: string) { | ||||
| 		recipients = recipients.unique().filter((x) => x !== creator_user_id); | ||||
| 		const otherRecipientsUsers = await User.find({ where: recipients.map((x) => ({ id: x })) }); | ||||
| 
 | ||||
| 		// TODO: check config for max number of recipients
 | ||||
| 		if (otherRecipientsUsers.length !== recipients.length) { | ||||
| 			throw new HTTPError("Recipient/s not found"); | ||||
| 		} | ||||
| 
 | ||||
| 		const type = recipients.length === 1 ? ChannelType.DM : ChannelType.GROUP_DM; | ||||
| 
 | ||||
| 		let channel = null; | ||||
| 
 | ||||
| 		const channelRecipients = [...recipients, creator_user_id]; | ||||
| 
 | ||||
| 		const userRecipients = await Recipient.find({ | ||||
| 			where: { user_id: creator_user_id }, | ||||
| 			relations: ["channel", "channel.recipients"], | ||||
| 		}); | ||||
| 
 | ||||
| 		for (let ur of userRecipients) { | ||||
| 			let re = ur.channel.recipients!.map((r) => r.user_id); | ||||
| 			if (re.length === channelRecipients.length) { | ||||
| 				if (containsAll(re, channelRecipients)) { | ||||
| 					if (channel == null) { | ||||
| 						channel = ur.channel; | ||||
| 						await ur.assign({ closed: false }).save(); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if (channel == null) { | ||||
| 			name = trimSpecial(name); | ||||
| 
 | ||||
| 			channel = await new Channel({ | ||||
| 				name, | ||||
| 				type, | ||||
| 				owner_id: type === ChannelType.DM ? undefined : creator_user_id, | ||||
| 				created_at: new Date(), | ||||
| 				last_message_id: null, | ||||
| 				recipients: channelRecipients.map( | ||||
| 					(x) => | ||||
| 						new Recipient({ user_id: x, closed: !(type === ChannelType.GROUP_DM || x === creator_user_id) }) | ||||
| 				), | ||||
| 			}).save(); | ||||
| 		} | ||||
| 
 | ||||
| 		const channel_dto = await DmChannelDTO.from(channel); | ||||
| 
 | ||||
| 		if (type === ChannelType.GROUP_DM) { | ||||
| 			for (let recipient of channel.recipients!) { | ||||
| 				await emitEvent({ | ||||
| 					event: "CHANNEL_CREATE", | ||||
| 					data: channel_dto.excludedRecipients([recipient.user_id]), | ||||
| 					user_id: recipient.user_id, | ||||
| 				}); | ||||
| 			} | ||||
| 		} else { | ||||
| 			await emitEvent({ event: "CHANNEL_CREATE", data: channel_dto, user_id: creator_user_id }); | ||||
| 		} | ||||
| 
 | ||||
| 		return channel_dto.excludedRecipients([creator_user_id]); | ||||
| 	} | ||||
| 
 | ||||
| 	static async removeRecipientFromChannel(channel: Channel, user_id: string) { | ||||
| 		await Recipient.delete({ channel_id: channel.id, user_id: user_id }); | ||||
| 		channel.recipients = channel.recipients?.filter((r) => r.user_id !== user_id); | ||||
| 
 | ||||
| 		if (channel.recipients?.length === 0) { | ||||
| 			await Channel.deleteChannel(channel); | ||||
| 			await emitEvent({ | ||||
| 				event: "CHANNEL_DELETE", | ||||
| 				data: await DmChannelDTO.from(channel, [user_id]), | ||||
| 				user_id: user_id, | ||||
| 			}); | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		await emitEvent({ | ||||
| 			event: "CHANNEL_DELETE", | ||||
| 			data: await DmChannelDTO.from(channel, [user_id]), | ||||
| 			user_id: user_id, | ||||
| 		}); | ||||
| 
 | ||||
| 		//If the owner leave we make the first recipient in the list the new owner
 | ||||
| 		if (channel.owner_id === user_id) { | ||||
| 			channel.owner_id = channel.recipients!.find((r) => r.user_id !== user_id)!.user_id; //Is there a criteria to choose the new owner?
 | ||||
| 			await emitEvent({ | ||||
| 				event: "CHANNEL_UPDATE", | ||||
| 				data: await DmChannelDTO.from(channel, [user_id]), | ||||
| 				channel_id: channel.id, | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		await channel.save(); | ||||
| 
 | ||||
| 		await emitEvent({ | ||||
| 			event: "CHANNEL_RECIPIENT_REMOVE", | ||||
| 			data: { | ||||
| 				channel_id: channel.id, | ||||
| 				user: await User.findOneOrFail({ where: { id: user_id }, select: PublicUserProjection }), | ||||
| 			}, | ||||
| 			channel_id: channel.id, | ||||
| 		} as ChannelRecipientRemoveEvent); | ||||
| 	} | ||||
| 
 | ||||
| 	static async deleteChannel(channel: Channel) { | ||||
| 		await Message.delete({ channel_id: channel.id }); //TODO we should also delete the attachments from the cdn but to do that we need to move cdn.ts in util
 | ||||
| 		//TODO before deleting the channel we should check and delete other relations
 | ||||
| 		await Channel.delete({ id: channel.id }); | ||||
| 	} | ||||
| 
 | ||||
| 	isDm() { | ||||
| 		return this.type === ChannelType.DM || this.type === ChannelType.GROUP_DM; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export interface ChannelPermissionOverwrite { | ||||
| 	allow: string; | ||||
| 	deny: string; | ||||
| 	id: string; | ||||
| 	type: ChannelPermissionOverwriteType; | ||||
| } | ||||
| 
 | ||||
| export enum ChannelPermissionOverwriteType { | ||||
| 	role = 0, | ||||
| 	member = 1, | ||||
| } | ||||
| import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm"; | ||||
| import { BaseClass } from "./BaseClass"; | ||||
| import { Guild } from "./Guild"; | ||||
| import { PublicUserProjection, User } from "./User"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { containsAll, emitEvent, getPermission, Snowflake, trimSpecial, InvisibleCharacters } from "../util"; | ||||
| import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces"; | ||||
| import { Recipient } from "./Recipient"; | ||||
| import { Message } from "./Message"; | ||||
| import { ReadState } from "./ReadState"; | ||||
| import { Invite } from "./Invite"; | ||||
| import { VoiceState } from "./VoiceState"; | ||||
| import { Webhook } from "./Webhook"; | ||||
| import { DmChannelDTO } from "../dtos"; | ||||
| 
 | ||||
| export enum ChannelType { | ||||
| 	GUILD_TEXT = 0, // a text channel within a server
 | ||||
| 	DM = 1, // a direct message between users
 | ||||
| 	GUILD_VOICE = 2, // a voice channel within a server
 | ||||
| 	GROUP_DM = 3, // a direct message between multiple users
 | ||||
| 	GUILD_CATEGORY = 4, // an organizational category that contains up to 50 channels
 | ||||
| 	GUILD_NEWS = 5, // a channel that users can follow and crosspost into their own server
 | ||||
| 	GUILD_STORE = 6, // a channel in which game developers can sell their game on Discord
 | ||||
| 	ENCRYPTED = 7, // end-to-end encrypted channel
 | ||||
| 	ENCRYPTED_THREAD = 8, // end-to-end encrypted thread channel
 | ||||
| 	GUILD_NEWS_THREAD = 10, // a temporary sub-channel within a GUILD_NEWS channel
 | ||||
| 	GUILD_PUBLIC_THREAD = 11, // a temporary sub-channel within a GUILD_TEXT channel
 | ||||
| 	GUILD_PRIVATE_THREAD = 12, // a temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission
 | ||||
| 	GUILD_STAGE_VOICE = 13, // a voice channel for hosting events with an audience
 | ||||
| 	CUSTOM_START = 64, // start custom channel types from here
 | ||||
| 	UNHANDLED = 255 // unhandled unowned pass-through channel type
 | ||||
| } | ||||
| 
 | ||||
| @Entity("channels") | ||||
| export class Channel extends BaseClass { | ||||
| 	@Column() | ||||
| 	created_at: Date; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	name?: string; | ||||
| 
 | ||||
| 	@Column({ type: "text", nullable: true }) | ||||
| 	icon?: string | null; | ||||
| 
 | ||||
| 	@Column({ type: "int" }) | ||||
| 	type: ChannelType; | ||||
| 
 | ||||
| 	@OneToMany(() => Recipient, (recipient: Recipient) => recipient.channel, { | ||||
| 		cascade: true, | ||||
| 		orphanedRowAction: "delete", | ||||
| 	}) | ||||
| 	recipients?: Recipient[]; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	last_message_id: string; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	@RelationId((channel: Channel) => channel.guild) | ||||
| 	guild_id?: string; | ||||
| 
 | ||||
| 	@JoinColumn({ name: "guild_id" }) | ||||
| 	@ManyToOne(() => Guild, { | ||||
| 		onDelete: "CASCADE", | ||||
| 	}) | ||||
| 	guild: Guild; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	@RelationId((channel: Channel) => channel.parent) | ||||
| 	parent_id: string; | ||||
| 
 | ||||
| 	@JoinColumn({ name: "parent_id" }) | ||||
| 	@ManyToOne(() => Channel) | ||||
| 	parent?: Channel; | ||||
| 
 | ||||
| 	// only for group dms
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	@RelationId((channel: Channel) => channel.owner) | ||||
| 	owner_id: string; | ||||
| 
 | ||||
| 	@JoinColumn({ name: "owner_id" }) | ||||
| 	@ManyToOne(() => User) | ||||
| 	owner: User; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	last_pin_timestamp?: number; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	default_auto_archive_duration?: number; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	position?: number; | ||||
| 
 | ||||
| 	@Column({ type: "simple-json", nullable: true }) | ||||
| 	permission_overwrites?: ChannelPermissionOverwrite[]; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	video_quality_mode?: number; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	bitrate?: number; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	user_limit?: number; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	nsfw?: boolean; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	rate_limit_per_user?: number; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	topic?: string; | ||||
| 
 | ||||
| 	@OneToMany(() => Invite, (invite: Invite) => invite.channel, { | ||||
| 		cascade: true, | ||||
| 		orphanedRowAction: "delete", | ||||
| 	}) | ||||
| 	invites?: Invite[]; | ||||
| 
 | ||||
| 	@OneToMany(() => Message, (message: Message) => message.channel, { | ||||
| 		cascade: true, | ||||
| 		orphanedRowAction: "delete", | ||||
| 	}) | ||||
| 	messages?: Message[]; | ||||
| 
 | ||||
| 	@OneToMany(() => VoiceState, (voice_state: VoiceState) => voice_state.channel, { | ||||
| 		cascade: true, | ||||
| 		orphanedRowAction: "delete", | ||||
| 	}) | ||||
| 	voice_states?: VoiceState[]; | ||||
| 
 | ||||
| 	@OneToMany(() => ReadState, (read_state: ReadState) => read_state.channel, { | ||||
| 		cascade: true, | ||||
| 		orphanedRowAction: "delete", | ||||
| 	}) | ||||
| 	read_states?: ReadState[]; | ||||
| 
 | ||||
| 	@OneToMany(() => Webhook, (webhook: Webhook) => webhook.channel, { | ||||
| 		cascade: true, | ||||
| 		orphanedRowAction: "delete", | ||||
| 	}) | ||||
| 	webhooks?: Webhook[]; | ||||
| 
 | ||||
| 	// TODO: DM channel
 | ||||
| 	static async createChannel( | ||||
| 		channel: Partial<Channel>, | ||||
| 		user_id: string = "0", | ||||
| 		opts?: { | ||||
| 			keepId?: boolean; | ||||
| 			skipExistsCheck?: boolean; | ||||
| 			skipPermissionCheck?: boolean; | ||||
| 			skipEventEmit?: boolean; | ||||
| 			skipNameChecks?: boolean; | ||||
| 		} | ||||
| 	) { | ||||
| 		if (!opts?.skipPermissionCheck) { | ||||
| 			// Always check if user has permission first
 | ||||
| 			const permissions = await getPermission(user_id, channel.guild_id); | ||||
| 			permissions.hasThrow("MANAGE_CHANNELS"); | ||||
| 		} | ||||
| 
 | ||||
| 		if (!opts?.skipNameChecks) { | ||||
| 			const guild = await Guild.findOneOrFail({ id: channel.guild_id }); | ||||
| 			if (!guild.features.includes("ALLOW_INVALID_CHANNEL_NAMES") && channel.name) { | ||||
| 				for (var character of InvisibleCharacters) | ||||
| 					if (channel.name.includes(character)) | ||||
| 						throw new HTTPError("Channel name cannot include invalid characters", 403); | ||||
| 
 | ||||
| 				if (channel.name.match(/\-\-+/g)) | ||||
| 					throw new HTTPError("Channel name cannot include multiple adjacent dashes.", 403) | ||||
| 
 | ||||
| 				if (channel.name.charAt(0) === "-" || | ||||
| 					channel.name.charAt(channel.name.length - 1) === "-") | ||||
| 					throw new HTTPError("Channel name cannot start/end with dash.", 403) | ||||
| 			} | ||||
| 
 | ||||
| 			if (!guild.features.includes("ALLOW_UNNAMED_CHANNELS")) { | ||||
| 				if (!channel.name) | ||||
| 					throw new HTTPError("Channel name cannot be empty.", 403); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		switch (channel.type) { | ||||
| 			case ChannelType.GUILD_TEXT: | ||||
| 			case ChannelType.GUILD_VOICE: | ||||
| 				if (channel.parent_id && !opts?.skipExistsCheck) { | ||||
| 					const exists = await Channel.findOneOrFail({ id: channel.parent_id }); | ||||
| 					if (!exists) throw new HTTPError("Parent id channel doesn't exist", 400); | ||||
| 					if (exists.guild_id !== channel.guild_id) | ||||
| 						throw new HTTPError("The category channel needs to be in the guild"); | ||||
| 				} | ||||
| 				break; | ||||
| 			case ChannelType.GUILD_CATEGORY: | ||||
| 				break; | ||||
| 			case ChannelType.DM: | ||||
| 			case ChannelType.GROUP_DM: | ||||
| 				throw new HTTPError("You can't create a dm channel in a guild"); | ||||
| 			// TODO: check if guild is community server
 | ||||
| 			case ChannelType.GUILD_STORE: | ||||
| 			case ChannelType.GUILD_NEWS: | ||||
| 			default: | ||||
| 				throw new HTTPError("Not yet supported"); | ||||
| 		} | ||||
| 
 | ||||
| 		if (!channel.permission_overwrites) channel.permission_overwrites = []; | ||||
| 		// TODO: auto generate position
 | ||||
| 
 | ||||
| 		channel = { | ||||
| 			...channel, | ||||
| 			...(!opts?.keepId && { id: Snowflake.generate() }), | ||||
| 			created_at: new Date(), | ||||
| 			position: channel.position || 0, | ||||
| 		}; | ||||
| 
 | ||||
| 		await Promise.all([ | ||||
| 			new Channel(channel).save(), | ||||
| 			!opts?.skipEventEmit | ||||
| 				? emitEvent({ | ||||
| 					event: "CHANNEL_CREATE", | ||||
| 					data: channel, | ||||
| 					guild_id: channel.guild_id, | ||||
| 				} as ChannelCreateEvent) | ||||
| 				: Promise.resolve(), | ||||
| 		]); | ||||
| 
 | ||||
| 		return channel; | ||||
| 	} | ||||
| 
 | ||||
| 	static async createDMChannel(recipients: string[], creator_user_id: string, name?: string) { | ||||
| 		recipients = recipients.unique().filter((x) => x !== creator_user_id); | ||||
| 		const otherRecipientsUsers = await User.find({ where: recipients.map((x) => ({ id: x })) }); | ||||
| 
 | ||||
| 		// TODO: check config for max number of recipients
 | ||||
| 		if (otherRecipientsUsers.length !== recipients.length) { | ||||
| 			throw new HTTPError("Recipient/s not found"); | ||||
| 		} | ||||
| 
 | ||||
| 		const type = recipients.length === 1 ? ChannelType.DM : ChannelType.GROUP_DM; | ||||
| 
 | ||||
| 		let channel = null; | ||||
| 
 | ||||
| 		const channelRecipients = [...recipients, creator_user_id]; | ||||
| 
 | ||||
| 		const userRecipients = await Recipient.find({ | ||||
| 			where: { user_id: creator_user_id }, | ||||
| 			relations: ["channel", "channel.recipients"], | ||||
| 		}); | ||||
| 
 | ||||
| 		for (let ur of userRecipients) { | ||||
| 			let re = ur.channel.recipients!.map((r) => r.user_id); | ||||
| 			if (re.length === channelRecipients.length) { | ||||
| 				if (containsAll(re, channelRecipients)) { | ||||
| 					if (channel == null) { | ||||
| 						channel = ur.channel; | ||||
| 						await ur.assign({ closed: false }).save(); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if (channel == null) { | ||||
| 			name = trimSpecial(name); | ||||
| 
 | ||||
| 			channel = await new Channel({ | ||||
| 				name, | ||||
| 				type, | ||||
| 				owner_id: type === ChannelType.DM ? undefined : null, // 1:1 DMs are ownerless in fosscord-server
 | ||||
| 				created_at: new Date(), | ||||
| 				last_message_id: null, | ||||
| 				recipients: channelRecipients.map( | ||||
| 					(x) => | ||||
| 						new Recipient({ user_id: x, closed: !(type === ChannelType.GROUP_DM || x === creator_user_id) }) | ||||
| 				), | ||||
| 			}).save(); | ||||
| 		} | ||||
| 
 | ||||
| 		const channel_dto = await DmChannelDTO.from(channel); | ||||
| 
 | ||||
| 		if (type === ChannelType.GROUP_DM) { | ||||
| 			for (let recipient of channel.recipients!) { | ||||
| 				await emitEvent({ | ||||
| 					event: "CHANNEL_CREATE", | ||||
| 					data: channel_dto.excludedRecipients([recipient.user_id]), | ||||
| 					user_id: recipient.user_id, | ||||
| 				}); | ||||
| 			} | ||||
| 		} else { | ||||
| 			await emitEvent({ event: "CHANNEL_CREATE", data: channel_dto, user_id: creator_user_id }); | ||||
| 		} | ||||
| 
 | ||||
| 		return channel_dto.excludedRecipients([creator_user_id]); | ||||
| 	} | ||||
| 
 | ||||
| 	static async removeRecipientFromChannel(channel: Channel, user_id: string) { | ||||
| 		await Recipient.delete({ channel_id: channel.id, user_id: user_id }); | ||||
| 		channel.recipients = channel.recipients?.filter((r) => r.user_id !== user_id); | ||||
| 
 | ||||
| 		if (channel.recipients?.length === 0) { | ||||
| 			await Channel.deleteChannel(channel); | ||||
| 			await emitEvent({ | ||||
| 				event: "CHANNEL_DELETE", | ||||
| 				data: await DmChannelDTO.from(channel, [user_id]), | ||||
| 				user_id: user_id, | ||||
| 			}); | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		await emitEvent({ | ||||
| 			event: "CHANNEL_DELETE", | ||||
| 			data: await DmChannelDTO.from(channel, [user_id]), | ||||
| 			user_id: user_id, | ||||
| 		}); | ||||
| 
 | ||||
| 		//If the owner leave the server user is the new owner
 | ||||
| 		if (channel.owner_id === user_id) { | ||||
| 			channel.owner_id = "1"; // The channel is now owned by the server user
 | ||||
| 			await emitEvent({ | ||||
| 				event: "CHANNEL_UPDATE", | ||||
| 				data: await DmChannelDTO.from(channel, [user_id]), | ||||
| 				channel_id: channel.id, | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		await channel.save(); | ||||
| 
 | ||||
| 		await emitEvent({ | ||||
| 			event: "CHANNEL_RECIPIENT_REMOVE", | ||||
| 			data: { | ||||
| 				channel_id: channel.id, | ||||
| 				user: await User.findOneOrFail({ where: { id: user_id }, select: PublicUserProjection }), | ||||
| 			}, | ||||
| 			channel_id: channel.id, | ||||
| 		} as ChannelRecipientRemoveEvent); | ||||
| 	} | ||||
| 
 | ||||
| 	static async deleteChannel(channel: Channel) { | ||||
| 		await Message.delete({ channel_id: channel.id }); //TODO we should also delete the attachments from the cdn but to do that we need to move cdn.ts in util
 | ||||
| 		//TODO before deleting the channel we should check and delete other relations
 | ||||
| 		await Channel.delete({ id: channel.id }); | ||||
| 	} | ||||
| 
 | ||||
| 	isDm() { | ||||
| 		return this.type === ChannelType.DM || this.type === ChannelType.GROUP_DM; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export interface ChannelPermissionOverwrite { | ||||
| 	allow: string; | ||||
| 	deny: string; | ||||
| 	id: string; | ||||
| 	type: ChannelPermissionOverwriteType; | ||||
| } | ||||
| 
 | ||||
| export enum ChannelPermissionOverwriteType { | ||||
| 	role = 0, | ||||
| 	member = 1, | ||||
| } | ||||
|  | ||||
							
								
								
									
										56
									
								
								util/src/util/InvisibleCharacters.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								util/src/util/InvisibleCharacters.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,56 @@ | ||||
| // List from https://invisible-characters.com/
 | ||||
| export const InvisibleCharacters = [ | ||||
| 	'\u{9}',			//Tab
 | ||||
| 	'\u{20}',			//Space
 | ||||
| 	'\u{ad}',			//Soft hyphen
 | ||||
| 	'\u{34f}',			//Combining grapheme joiner
 | ||||
| 	'\u{61c}',			//Arabic letter mark
 | ||||
| 	'\u{115f}',			//Hangul choseong filler
 | ||||
| 	'\u{1160}',			//Hangul jungseong filler
 | ||||
| 	'\u{17b4}',			//Khmer vowel inherent AQ
 | ||||
| 	'\u{17b5}',			//Khmer vowel inherent AA
 | ||||
| 	'\u{180e}',			//Mongolian vowel separator
 | ||||
| 	'\u{2000}',			//En quad
 | ||||
| 	'\u{2001}',			//Em quad
 | ||||
| 	'\u{2002}',			//En space
 | ||||
| 	'\u{2003}',			//Em space
 | ||||
| 	'\u{2004}',			//Three-per-em space
 | ||||
| 	'\u{2005}',			//Four-per-em space
 | ||||
| 	'\u{2006}',			//Six-per-em space
 | ||||
| 	'\u{2007}',			//Figure space
 | ||||
| 	'\u{2008}',			//Punctuation space
 | ||||
| 	'\u{2009}',			//Thin space
 | ||||
| 	'\u{200a}',			//Hair space
 | ||||
| 	'\u{200b}',			//Zero width space
 | ||||
| 	'\u{200c}',			//Zero width non-joiner
 | ||||
| 	'\u{200d}',			//Zero width joiner
 | ||||
| 	'\u{200e}',			//Left-to-right mark
 | ||||
| 	'\u{200f}',			//Right-to-left mark
 | ||||
| 	'\u{202f}',			//Narrow no-break space
 | ||||
| 	'\u{205f}',			//Medium mathematical space
 | ||||
| 	'\u{2060}',			//Word joiner
 | ||||
| 	'\u{2061}',			//Function application
 | ||||
| 	'\u{2062}',			//Invisible times
 | ||||
| 	'\u{2063}',			//Invisible separator
 | ||||
| 	'\u{2064}',			//Invisible plus
 | ||||
| 	'\u{206a}',			//Inhibit symmetric swapping
 | ||||
| 	'\u{206b}',			//Activate symmetric swapping
 | ||||
| 	'\u{206c}',			//Inhibit arabic form shaping
 | ||||
| 	'\u{206d}',			//Activate arabic form shaping
 | ||||
| 	'\u{206e}',			//National digit shapes
 | ||||
| 	'\u{206f}',			//Nominal digit shapes
 | ||||
| 	'\u{3000}',			//Ideographic space
 | ||||
| 	'\u{2800}',			//Braille pattern blank
 | ||||
| 	'\u{3164}',			//Hangul filler
 | ||||
| 	'\u{feff}',			//Zero width no-break space
 | ||||
| 	'\u{ffa0}',			//Haldwidth hangul filler
 | ||||
| 	'\u{1d159}',		//Musical symbol null notehead
 | ||||
| 	'\u{1d173}',		//Musical symbol begin beam 
 | ||||
| 	'\u{1d174}',		//Musical symbol end beam
 | ||||
| 	'\u{1d175}',		//Musical symbol begin tie
 | ||||
| 	'\u{1d176}',		//Musical symbol end tie
 | ||||
| 	'\u{1d177}',		//Musical symbol begin slur
 | ||||
| 	'\u{1d178}',		//Musical symbol end slur
 | ||||
| 	'\u{1d179}',		//Musical symbol begin phrase
 | ||||
| 	'\u{1d17a}'			//Musical symbol end phrase
 | ||||
| ];  | ||||
| @ -18,3 +18,4 @@ export * from "./Snowflake"; | ||||
| export * from "./String"; | ||||
| export * from "./Array"; | ||||
| export * from "./TraverseDirectory"; | ||||
| export * from "./InvisibleCharacters"; | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Featyre
						Featyre