Merge pull request #369 from AlTech98/fix-dm
This commit is contained in:
		
						commit
						03afedc627
					
				
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,6 +1,7 @@ | ||||
| import { ChannelDeleteEvent, Channel, ChannelUpdateEvent, emitEvent, ChannelType, ChannelPermissionOverwriteType } from "@fosscord/util"; | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { route } from "@fosscord/api"; | ||||
| import { Channel, ChannelDeleteEvent, ChannelPermissionOverwriteType, ChannelType, ChannelUpdateEvent, emitEvent, Recipient } from "@fosscord/util"; | ||||
| import { Request, Response, Router } from "express"; | ||||
| import { handleFile, route } from "@fosscord/api"; | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| // TODO: delete channel
 | ||||
| // TODO: Get channel
 | ||||
| @ -16,23 +17,37 @@ router.get("/", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: | ||||
| router.delete("/", route({ permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { | ||||
| 	const { channel_id } = req.params; | ||||
| 
 | ||||
| 	const channel = await Channel.findOneOrFail({ id: channel_id }); | ||||
| 	const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients"] }); | ||||
| 
 | ||||
| 	// TODO: Dm channel "close" not delete
 | ||||
| 	const data = channel; | ||||
| 	if (channel.type === ChannelType.DM) { | ||||
| 		const recipient = await Recipient.findOneOrFail({ where: { channel_id: channel_id, user_id: req.user_id } }) | ||||
| 		recipient.closed = true | ||||
| 		await Promise.all([ | ||||
| 			recipient.save(), | ||||
| 			emitEvent({ event: "CHANNEL_DELETE", data: channel, user_id: req.user_id } as ChannelDeleteEvent) | ||||
| 		]); | ||||
| 
 | ||||
| 	await Promise.all([emitEvent({ event: "CHANNEL_DELETE", data, channel_id } as ChannelDeleteEvent), Channel.delete({ id: channel_id })]); | ||||
| 	} else if (channel.type === ChannelType.GROUP_DM) { | ||||
| 		await Channel.removeRecipientFromChannel(channel, req.user_id) | ||||
| 	} else { | ||||
| 		//TODO messages in this channel should be deleted before deleting the channel
 | ||||
| 		await Promise.all([ | ||||
| 			Channel.delete({ id: channel_id }), | ||||
| 			emitEvent({ event: "CHANNEL_DELETE", data: channel, channel_id } as ChannelDeleteEvent) | ||||
| 		]); | ||||
| 	} | ||||
| 
 | ||||
| 	res.send(data); | ||||
| 	res.send(channel); | ||||
| }); | ||||
| 
 | ||||
| export interface ChannelModifySchema { | ||||
| 	/** | ||||
| 	 * @maxLength 100 | ||||
| 	 */ | ||||
| 	name: string; | ||||
| 	type: ChannelType; | ||||
| 	name?: string; | ||||
| 	type?: ChannelType; | ||||
| 	topic?: string; | ||||
| 	icon?: string | null; | ||||
| 	bitrate?: number; | ||||
| 	user_limit?: number; | ||||
| 	rate_limit_per_user?: number; | ||||
| @ -53,6 +68,7 @@ export interface ChannelModifySchema { | ||||
| router.patch("/", route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { | ||||
| 	var payload = req.body as ChannelModifySchema; | ||||
| 	const { channel_id } = req.params; | ||||
| 	if (payload.icon) payload.icon = await handleFile(`/channel-icons/${channel_id}`, payload.icon); | ||||
| 
 | ||||
| 	const channel = await Channel.findOneOrFail({ id: channel_id }); | ||||
| 	channel.assign(payload); | ||||
|  | ||||
| @ -26,7 +26,7 @@ router.post("/", route({ body: "MessageAcknowledgeSchema" }), async (req: Reques | ||||
| 		data: { | ||||
| 			channel_id, | ||||
| 			message_id, | ||||
| 			version: 496 | ||||
| 			version: 3763 | ||||
| 		} | ||||
| 	} as MessageAckEvent); | ||||
| 
 | ||||
|  | ||||
| @ -1,9 +1,8 @@ | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { Attachment, Channel, ChannelType, Embed, getPermission, Message } from "@fosscord/util"; | ||||
| import { Attachment, Channel, ChannelType, DmChannelDTO, Embed, emitEvent, getPermission, Message, MessageCreateEvent, Recipient } from "@fosscord/util"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { route } from "@fosscord/api"; | ||||
| import { handleMessage, postHandleMessage, route } from "@fosscord/api"; | ||||
| import multer from "multer"; | ||||
| import { sendMessage } from "@fosscord/api"; | ||||
| import { uploadFile } from "@fosscord/api"; | ||||
| import { FindManyOptions, LessThan, MoreThan } from "typeorm"; | ||||
| 
 | ||||
| @ -62,9 +61,9 @@ router.get("/", async (req: Request, res: Response) => { | ||||
| 	if (!channel) throw new HTTPError("Channel not found", 404); | ||||
| 
 | ||||
| 	isTextChannel(channel.type); | ||||
| 	const around = `${req.query.around}`; | ||||
| 	const before = `${req.query.before}`; | ||||
| 	const after = `${req.query.after}`; | ||||
| 	const around = req.query.around ? `${req.query.around}` : undefined; | ||||
| 	const before = req.query.before ? `${req.query.before}` : undefined; | ||||
| 	const after = req.query.after ? `${req.query.after}` : undefined; | ||||
| 	const limit = Number(req.query.limit) || 50; | ||||
| 	if (limit < 1 || limit > 100) throw new HTTPError("limit must be between 1 and 100"); | ||||
| 
 | ||||
| @ -151,10 +150,11 @@ router.post( | ||||
| 				return res.status(400).json(error); | ||||
| 			} | ||||
| 		} | ||||
| 		const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients", "recipients.user"] }) | ||||
| 
 | ||||
| 		const embeds = []; | ||||
| 		if (body.embed) embeds.push(body.embed); | ||||
| 		const data = await sendMessage({ | ||||
| 		let message = await handleMessage({ | ||||
| 			...body, | ||||
| 			type: 0, | ||||
| 			pinned: false, | ||||
| @ -162,9 +162,37 @@ router.post( | ||||
| 			embeds, | ||||
| 			channel_id, | ||||
| 			attachments, | ||||
| 			edited_timestamp: undefined | ||||
| 			edited_timestamp: undefined, | ||||
| 			timestamp: new Date() | ||||
| 		}); | ||||
| 
 | ||||
| 		return res.json(data); | ||||
| 		message = await message.save() | ||||
| 
 | ||||
| 		await channel.assign({ last_message_id: message.id }).save() | ||||
| 
 | ||||
| 		if (channel.isDm()) { | ||||
| 			const channel_dto = await DmChannelDTO.from(channel) | ||||
| 
 | ||||
| 			for (let recipient of channel.recipients!) { | ||||
| 				if (recipient.closed) { | ||||
| 					await emitEvent({ | ||||
| 						event: "CHANNEL_CREATE", | ||||
| 						data: channel_dto.excludedRecipients([recipient.user_id]), | ||||
| 						user_id: recipient.user_id | ||||
| 					}) | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			//Only one recipients should be closed here, since in group DMs the recipient is deleted not closed
 | ||||
| 			await Promise.all(channel.recipients!.filter(r => r.closed).map(async r => { | ||||
| 				r.closed = false; | ||||
| 				return await r.save() | ||||
| 			})); | ||||
| 		} | ||||
| 
 | ||||
| 		await emitEvent({ event: "MESSAGE_CREATE", channel_id: channel_id, data: message } as MessageCreateEvent) | ||||
| 		postHandleMessage(message).catch((e) => {}); // no await as it shouldnt block the message send function and silently catch error
 | ||||
| 
 | ||||
| 		return res.json(message); | ||||
| 	} | ||||
| ); | ||||
|  | ||||
| @ -1,5 +1,57 @@ | ||||
| import { Router, Response, Request } from "express"; | ||||
| import { Request, Response, Router } from "express"; | ||||
| import { Channel, ChannelRecipientAddEvent, ChannelType, DiscordApiErrors, DmChannelDTO, emitEvent, PublicUserProjection, Recipient, User } from "@fosscord/util"; | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| // TODO:
 | ||||
| 
 | ||||
| router.put("/:user_id", async (req: Request, res: Response) => { | ||||
| 	const { channel_id, user_id } = req.params; | ||||
| 	const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients"] }); | ||||
| 
 | ||||
| 	if (channel.type !== ChannelType.GROUP_DM) { | ||||
| 		const recipients = [ | ||||
| 			...channel.recipients!.map(r => r.user_id), | ||||
| 			user_id | ||||
| 		].unique() | ||||
| 
 | ||||
| 		const new_channel = await Channel.createDMChannel(recipients, req.user_id) | ||||
| 		return res.status(201).json(new_channel); | ||||
| 	} else { | ||||
| 		if (channel.recipients!.map(r => r.user_id).includes(user_id)) { | ||||
| 			throw DiscordApiErrors.INVALID_RECIPIENT //TODO is this the right error?
 | ||||
| 		} | ||||
| 
 | ||||
| 		channel.recipients!.push(new Recipient({ channel_id: channel_id, user_id: user_id })); | ||||
| 		await channel.save() | ||||
| 
 | ||||
| 		await emitEvent({ | ||||
| 			event: "CHANNEL_CREATE", | ||||
| 			data: await DmChannelDTO.from(channel, [user_id]), | ||||
| 			user_id: user_id | ||||
| 		}); | ||||
| 
 | ||||
| 		await emitEvent({ | ||||
| 			event: "CHANNEL_RECIPIENT_ADD", data: { | ||||
| 				channel_id: channel_id, | ||||
| 				user: await User.findOneOrFail({ where: { id: user_id }, select: PublicUserProjection }) | ||||
| 			}, channel_id: channel_id | ||||
| 		} as ChannelRecipientAddEvent); | ||||
| 		return res.sendStatus(204); | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
| router.delete("/:user_id", async (req: Request, res: Response) => { | ||||
| 	const { channel_id, user_id } = req.params; | ||||
| 	const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients"] }); | ||||
| 	if (!(channel.type === ChannelType.GROUP_DM && (channel.owner_id === req.user_id || user_id === req.user_id))) | ||||
| 		throw DiscordApiErrors.MISSING_PERMISSIONS | ||||
| 
 | ||||
| 	if (!channel.recipients!.map(r => r.user_id).includes(user_id)) { | ||||
| 		throw DiscordApiErrors.INVALID_RECIPIENT //TODO is this the right error?
 | ||||
| 	} | ||||
| 
 | ||||
| 	await Channel.removeRecipientFromChannel(channel, user_id) | ||||
| 
 | ||||
| 	return res.sendStatus(204); | ||||
| }); | ||||
| 
 | ||||
| export default router; | ||||
|  | ||||
							
								
								
									
										18
									
								
								api/src/routes/sticker-packs/#id/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								api/src/routes/sticker-packs/#id/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | ||||
| import { Request, Response, Router } from "express"; | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| 
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| 	//TODO
 | ||||
| 	res.json({ | ||||
| 		id: "", | ||||
| 		stickers: [], | ||||
| 		name: "", | ||||
| 		sku_id: "", | ||||
| 		cover_sticker_id: "", | ||||
| 		description: "", | ||||
| 		banner_asset_id: "" | ||||
| 	}).status(200); | ||||
| }); | ||||
| 
 | ||||
| export default router; | ||||
							
								
								
									
										10
									
								
								api/src/routes/sticker-packs/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								api/src/routes/sticker-packs/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| import { Request, Response, Router } from "express"; | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| 
 | ||||
| router.get("/", async (req: Request, res: Response) => { | ||||
| 	//TODO
 | ||||
| 	res.json({ sticker_packs: [] }).status(200); | ||||
| }); | ||||
| 
 | ||||
| export default router; | ||||
| @ -19,6 +19,7 @@ router.get("/", route({ response: { body: "UserProfileResponse" } }), async (req | ||||
| 		connected_accounts: user.connected_accounts, | ||||
| 		premium_guild_since: null, // TODO
 | ||||
| 		premium_since: null, // TODO
 | ||||
| 		mutual_guilds: [], // TODO {id: "", nick: null} when ?with_mutual_guilds=true
 | ||||
| 		user: { | ||||
| 			username: user.username, | ||||
| 			discriminator: user.discriminator, | ||||
|  | ||||
| @ -1,15 +1,12 @@ | ||||
| import { Router, Request, Response } from "express"; | ||||
| import { Channel, ChannelCreateEvent, ChannelType, Snowflake, trimSpecial, User, emitEvent, Recipient } from "@fosscord/util"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { Request, Response, Router } from "express"; | ||||
| import { Recipient, DmChannelDTO, Channel } from "@fosscord/util"; | ||||
| import { route } from "@fosscord/api"; | ||||
| import { In } from "typeorm"; | ||||
| 
 | ||||
| const router: Router = Router(); | ||||
| 
 | ||||
| router.get("/", route({}), async (req: Request, res: Response) => { | ||||
| 	const recipients = await Recipient.find({ where: { user_id: req.user_id }, relations: ["channel"] }); | ||||
| 
 | ||||
| 	res.json(recipients.map((x) => x.channel)); | ||||
| 	const recipients = await Recipient.find({ where: { user_id: req.user_id, closed: false }, relations: ["channel", "channel.recipients"] }); | ||||
| 	res.json(await Promise.all(recipients.map(r => DmChannelDTO.from(r.channel, [req.user_id])))); | ||||
| }); | ||||
| 
 | ||||
| export interface DmChannelCreateSchema { | ||||
| @ -19,30 +16,7 @@ export interface DmChannelCreateSchema { | ||||
| 
 | ||||
| router.post("/", route({ body: "DmChannelCreateSchema" }), async (req: Request, res: Response) => { | ||||
| 	const body = req.body as DmChannelCreateSchema; | ||||
| 
 | ||||
| 	body.recipients = body.recipients.filter((x) => x !== req.user_id).unique(); | ||||
| 
 | ||||
| 	const recipients = await User.find({ where: body.recipients.map((x) => ({ id: x })) }); | ||||
| 
 | ||||
| 	if (recipients.length !== body.recipients.length) { | ||||
| 		throw new HTTPError("Recipient/s not found"); | ||||
| 	} | ||||
| 
 | ||||
| 	const type = body.recipients.length === 1 ? ChannelType.DM : ChannelType.GROUP_DM; | ||||
| 	const name = trimSpecial(body.name); | ||||
| 
 | ||||
| 	const channel = await new Channel({ | ||||
| 		name, | ||||
| 		type, | ||||
| 		// owner_id only for group dm channels
 | ||||
| 		created_at: new Date(), | ||||
| 		last_message_id: null, | ||||
| 		recipients: [...body.recipients.map((x) => new Recipient({ user_id: x })), new Recipient({ user_id: req.user_id })] | ||||
| 	}).save(); | ||||
| 
 | ||||
| 	await emitEvent({ event: "CHANNEL_CREATE", data: channel, user_id: req.user_id } as ChannelCreateEvent); | ||||
| 
 | ||||
| 	res.json(channel); | ||||
| 	res.json(await Channel.createDMChannel(body.recipients, req.user_id, body.name)); | ||||
| }); | ||||
| 
 | ||||
| export default router; | ||||
|  | ||||
| @ -18,9 +18,19 @@ const router = Router(); | ||||
| const userProjection: (keyof User)[] = ["relationships", ...PublicUserProjection]; | ||||
| 
 | ||||
| router.get("/", route({}), async (req: Request, res: Response) => { | ||||
| 	const user = await User.findOneOrFail({ where: { id: req.user_id }, relations: ["relationships"] }); | ||||
| 	const user = await User.findOneOrFail({ where: { id: req.user_id }, relations: ["relationships", "relationships.to"] }); | ||||
| 
 | ||||
| 	return res.json(user.relationships); | ||||
| 	//TODO DTO
 | ||||
| 	const related_users = user.relationships.map(r => { | ||||
| 		return { | ||||
| 			id: r.to.id, | ||||
| 			type: r.type, | ||||
| 			nickname: null, | ||||
| 			user: r.to.toPublicUser(), | ||||
| 		} | ||||
| 	}) | ||||
| 
 | ||||
| 	return res.json(related_users); | ||||
| }); | ||||
| 
 | ||||
| export interface RelationshipPutSchema { | ||||
| @ -48,7 +58,10 @@ router.post("/", route({ body: "RelationshipPostSchema" }), async (req: Request, | ||||
| 		await User.findOneOrFail({ | ||||
| 			relations: ["relationships", "relationships.to"], | ||||
| 			select: userProjection, | ||||
| 			where: req.body as { discriminator: string; username: string } | ||||
| 			where: { | ||||
| 				discriminator: String(req.body.discriminator,).padStart(4, '0'), //Discord send the discriminator as integer, we need to add leading zeroes
 | ||||
| 				username: req.body.username | ||||
| 			} | ||||
| 		}), | ||||
| 		req.body.type | ||||
| 	); | ||||
|  | ||||
| @ -58,6 +58,9 @@ export class CDNServer extends Server { | ||||
| 		this.app.use("/team-icons/", avatarsRoute); | ||||
| 		this.log("verbose", "[Server] Route /team-icons registered"); | ||||
| 
 | ||||
| 		this.app.use("/channel-icons/", avatarsRoute); | ||||
| 		this.log("verbose", "[Server] Route /channel-icons registered"); | ||||
| 
 | ||||
| 		return super.start(); | ||||
| 	} | ||||
| 
 | ||||
|  | ||||
| @ -32,7 +32,7 @@ export async function setupListener(this: WebSocket) { | ||||
| 	}); | ||||
| 	const guilds = members.map((x) => x.guild); | ||||
| 	const recipients = await Recipient.find({ | ||||
| 		where: { user_id: this.user_id }, | ||||
| 		where: { user_id: this.user_id, closed: false }, | ||||
| 		relations: ["channel"], | ||||
| 	}); | ||||
| 	const dm_channels = recipients.map((x) => x.channel); | ||||
| @ -116,7 +116,7 @@ async function consume(this: WebSocket, opts: EventOpts) { | ||||
| 					.has("VIEW_CHANNEL") | ||||
| 			) | ||||
| 				return; | ||||
| 		// TODO: check if user has permission to channel
 | ||||
| 			//No break needed here, we need to call the listenEvent function below
 | ||||
| 		case "GUILD_CREATE": | ||||
| 			this.events[id] = await listenEvent(id, consumer, listenOpts); | ||||
| 			break; | ||||
|  | ||||
| @ -88,20 +88,17 @@ export async function onIdentify(this: WebSocket, data: Payload) { | ||||
| 	const user_guild_settings_entries = members.map((x) => x.settings); | ||||
| 
 | ||||
| 	const recipients = await Recipient.find({ | ||||
| 		where: { user_id: this.user_id }, | ||||
| 		where: { user_id: this.user_id, closed: false }, | ||||
| 		relations: ["channel", "channel.recipients", "channel.recipients.user"], | ||||
| 		// TODO: public user selection
 | ||||
| 	}); | ||||
| 	const channels = recipients.map((x) => { | ||||
| 		// @ts-ignore
 | ||||
| 		x.channel.recipients = x.channel.recipients?.map((x) => x.user); | ||||
| 		// @ts-ignore
 | ||||
| 		users = users.concat(x.channel.recipients); | ||||
| 		if (x.channel.type === ChannelType.DM) { | ||||
| 			x.channel.recipients = [ | ||||
| 				// @ts-ignore
 | ||||
| 				x.channel.recipients.find((x) => x.id !== this.user_id), | ||||
| 			]; | ||||
| 		//TODO is this needed? check if users in group dm that are not friends are sent in the READY event
 | ||||
| 		//users = users.concat(x.channel.recipients);
 | ||||
| 		if (x.channel.isDm()) { | ||||
| 			x.channel.recipients = x.channel.recipients!.filter((x) => x.id !== this.user_id); | ||||
| 		} | ||||
| 		return x.channel; | ||||
| 	}); | ||||
| @ -111,16 +108,19 @@ export async function onIdentify(this: WebSocket, data: Payload) { | ||||
| 	}); | ||||
| 	if (!user) return this.close(CLOSECODES.Authentication_failed); | ||||
| 
 | ||||
| 	const public_user = { | ||||
| 		username: user.username, | ||||
| 		discriminator: user.discriminator, | ||||
| 		id: user.id, | ||||
| 		public_flags: user.public_flags, | ||||
| 		avatar: user.avatar, | ||||
| 		bot: user.bot, | ||||
| 		bio: user.bio, | ||||
| 	}; | ||||
| 	users.push(public_user); | ||||
| 	for (let relation of user.relationships) { | ||||
| 		const related_user = relation.to | ||||
| 		const public_related_user = { | ||||
| 			username: related_user.username, | ||||
| 			discriminator: related_user.discriminator, | ||||
| 			id: related_user.id, | ||||
| 			public_flags: related_user.public_flags, | ||||
| 			avatar: related_user.avatar, | ||||
| 			bot: related_user.bot, | ||||
| 			bio: related_user.bio, | ||||
| 		}; | ||||
| 		users.push(public_related_user); | ||||
| 	} | ||||
| 
 | ||||
| 	const session_id = genSessionId(); | ||||
| 	this.session_id = session_id; //Set the session of the WebSocket object
 | ||||
| @ -201,7 +201,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { | ||||
| 		// @ts-ignore
 | ||||
| 		experiments: experiments, // TODO
 | ||||
| 		guild_join_requests: [], // TODO what is this?
 | ||||
| 		users: users.unique(), // TODO
 | ||||
| 		users: users.unique(), | ||||
| 		merged_members: merged_members, | ||||
| 		// shard // TODO: only for bots sharding
 | ||||
| 		// application // TODO for applications
 | ||||
|  | ||||
| @ -21,5 +21,6 @@ export default { | ||||
| 	8: onRequestGuildMembers, | ||||
| 	// 9: Invalid Session
 | ||||
| 	// 10: Hello
 | ||||
| 	// 13: Dm_update
 | ||||
| 	14: onLazyRequest, | ||||
| }; | ||||
|  | ||||
							
								
								
									
										35
									
								
								util/src/dtos/DmChannelDTO.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								util/src/dtos/DmChannelDTO.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | ||||
| import { MinimalPublicUserDTO } from "./UserDTO"; | ||||
| import { Channel, PublicUserProjection, User } from "../entities"; | ||||
| 
 | ||||
| export class DmChannelDTO { | ||||
| 	icon: string | null; | ||||
| 	id: string; | ||||
| 	last_message_id: string | null; | ||||
| 	name: string | null; | ||||
| 	origin_channel_id: string | null; | ||||
| 	owner_id?: string; | ||||
| 	recipients: MinimalPublicUserDTO[]; | ||||
| 	type: number; | ||||
| 
 | ||||
| 	static async from(channel: Channel, excluded_recipients: string[] = [], origin_channel_id?: string) { | ||||
| 		const obj = new DmChannelDTO() | ||||
| 		obj.icon = channel.icon || null | ||||
| 		obj.id = channel.id | ||||
| 		obj.last_message_id = channel.last_message_id || null | ||||
| 		obj.name = channel.name || null | ||||
| 		obj.origin_channel_id = origin_channel_id || null | ||||
| 		obj.owner_id = channel.owner_id | ||||
| 		obj.type = channel.type | ||||
| 		obj.recipients = (await Promise.all(channel.recipients!.filter(r => !excluded_recipients.includes(r.user_id)).map(async r => { | ||||
| 			return await User.findOneOrFail({ where: { id: r.user_id }, select: PublicUserProjection }) | ||||
| 		}))).map(u => new MinimalPublicUserDTO(u)) | ||||
| 		return obj | ||||
| 	} | ||||
| 
 | ||||
| 	excludedRecipients(excluded_recipients: string[]): DmChannelDTO { | ||||
| 		return { | ||||
| 			...this, | ||||
| 			recipients: this.recipients.filter(r => !excluded_recipients.includes(r.id)) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										17
									
								
								util/src/dtos/UserDTO.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								util/src/dtos/UserDTO.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| import { User } from "../entities"; | ||||
| 
 | ||||
| export class MinimalPublicUserDTO { | ||||
| 	avatar?: string | null; | ||||
| 	discriminator: string; | ||||
| 	id: string; | ||||
| 	public_flags: number; | ||||
| 	username: string; | ||||
| 
 | ||||
| 	constructor(user: User) { | ||||
| 		this.avatar = user.avatar | ||||
| 		this.discriminator = user.discriminator | ||||
| 		this.id = user.id | ||||
| 		this.public_flags = user.public_flags | ||||
| 		this.username = user.username | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										2
									
								
								util/src/dtos/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								util/src/dtos/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| export * from "./DmChannelDTO"; | ||||
| export * from "./UserDTO"; | ||||
| @ -1,12 +1,13 @@ | ||||
| import { Column, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany, RelationId } from "typeorm"; | ||||
| import { Column, Entity, JoinColumn, ManyToOne, OneToMany, RelationId } from "typeorm"; | ||||
| import { BaseClass } from "./BaseClass"; | ||||
| import { Guild } from "./Guild"; | ||||
| import { Message } from "./Message"; | ||||
| import { User } from "./User"; | ||||
| import { PublicUserProjection, User } from "./User"; | ||||
| import { HTTPError } from "lambert-server"; | ||||
| import { emitEvent, getPermission, Snowflake } from "../util"; | ||||
| import { ChannelCreateEvent } from "../interfaces"; | ||||
| import { containsAll, emitEvent, getPermission, Snowflake, trimSpecial } from "../util"; | ||||
| import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces"; | ||||
| import { Recipient } from "./Recipient"; | ||||
| import { DmChannelDTO } from "../dtos"; | ||||
| import { Message } from "./Message"; | ||||
| 
 | ||||
| export enum ChannelType { | ||||
| 	GUILD_TEXT = 0, // a text channel within a server
 | ||||
| @ -31,6 +32,9 @@ export class Channel extends BaseClass { | ||||
| 	@Column({ nullable: true }) | ||||
| 	name?: string; | ||||
| 
 | ||||
| 	@Column({ type: 'text', nullable: true }) | ||||
| 	icon?: string | null; | ||||
| 
 | ||||
| 	@Column({ type: "simple-enum", enum: ChannelType }) | ||||
| 	type: ChannelType; | ||||
| 
 | ||||
| @ -38,13 +42,8 @@ export class Channel extends BaseClass { | ||||
| 	recipients?: Recipient[]; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	@RelationId((channel: Channel) => channel.last_message) | ||||
| 	last_message_id: string; | ||||
| 
 | ||||
| 	@JoinColumn({ name: "last_message_id" }) | ||||
| 	@ManyToOne(() => Message) | ||||
| 	last_message?: Message; | ||||
| 
 | ||||
| 	@Column({ nullable: true }) | ||||
| 	@RelationId((channel: Channel) => channel.guild) | ||||
| 	guild_id?: string; | ||||
| @ -100,7 +99,6 @@ export class Channel extends BaseClass { | ||||
| 	@Column({ nullable: true }) | ||||
| 	topic?: string; | ||||
| 
 | ||||
| 	// TODO: DM channel
 | ||||
| 	static async createChannel( | ||||
| 		channel: Partial<Channel>, | ||||
| 		user_id: string = "0", | ||||
| @ -153,15 +151,126 @@ export class Channel extends BaseClass { | ||||
| 			new Channel(channel).save(), | ||||
| 			!opts?.skipEventEmit | ||||
| 				? emitEvent({ | ||||
| 						event: "CHANNEL_CREATE", | ||||
| 						data: channel, | ||||
| 						guild_id: channel.guild_id, | ||||
| 				  } as ChannelCreateEvent) | ||||
| 					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 { | ||||
|  | ||||
| @ -19,5 +19,8 @@ export class Recipient extends BaseClass { | ||||
| 	@ManyToOne(() => require("./User").User) | ||||
| 	user: import("./User").User; | ||||
| 
 | ||||
| 	@Column({ default: false }) | ||||
| 	closed: boolean; | ||||
| 
 | ||||
| 	// TODO: settings/mute/nick/added at/encryption keys/read_state
 | ||||
| } | ||||
|  | ||||
| @ -124,7 +124,7 @@ export class User extends BaseClass { | ||||
| 	flags: string; // UserFlags
 | ||||
| 
 | ||||
| 	@Column() | ||||
| 	public_flags: string; | ||||
| 	public_flags: number; | ||||
| 
 | ||||
| 	@JoinColumn({ name: "relationship_ids" }) | ||||
| 	@OneToMany(() => Relationship, (relationship: Relationship) => relationship.from) | ||||
|  | ||||
| @ -4,6 +4,7 @@ import "reflect-metadata"; | ||||
| export * from "./util/index"; | ||||
| export * from "./interfaces/index"; | ||||
| export * from "./entities/index"; | ||||
| export * from "./dtos/index"; | ||||
| 
 | ||||
| // import Config from "../util/Config";
 | ||||
| // import db, { MongooseCache, toObject } from "./util/Database";
 | ||||
|  | ||||
| @ -127,6 +127,22 @@ export interface ChannelPinsUpdateEvent extends Event { | ||||
| 	}; | ||||
| } | ||||
| 
 | ||||
| export interface ChannelRecipientAddEvent extends Event { | ||||
| 	event: "CHANNEL_RECIPIENT_ADD"; | ||||
| 	data: { | ||||
| 		channel_id: string; | ||||
| 		user: User; | ||||
| 	}; | ||||
| } | ||||
| 
 | ||||
| export interface ChannelRecipientRemoveEvent extends Event { | ||||
| 	event: "CHANNEL_RECIPIENT_REMOVE"; | ||||
| 	data: { | ||||
| 		channel_id: string; | ||||
| 		user: User; | ||||
| 	}; | ||||
| } | ||||
| 
 | ||||
| export interface GuildCreateEvent extends Event { | ||||
| 	event: "GUILD_CREATE"; | ||||
| 	data: Guild & { | ||||
| @ -436,6 +452,8 @@ export type EventData = | ||||
| 	| ChannelUpdateEvent | ||||
| 	| ChannelDeleteEvent | ||||
| 	| ChannelPinsUpdateEvent | ||||
| 	| ChannelRecipientAddEvent | ||||
| 	| ChannelRecipientRemoveEvent | ||||
| 	| GuildCreateEvent | ||||
| 	| GuildUpdateEvent | ||||
| 	| GuildDeleteEvent | ||||
| @ -482,6 +500,8 @@ export enum EVENTEnum { | ||||
| 	ChannelUpdate = "CHANNEL_UPDATE", | ||||
| 	ChannelDelete = "CHANNEL_DELETE", | ||||
| 	ChannelPinsUpdate = "CHANNEL_PINS_UPDATE", | ||||
| 	ChannelRecipientAdd = "CHANNEL_RECIPIENT_ADD", | ||||
| 	ChannelRecipientRemove = "CHANNEL_RECIPIENT_REMOVE", | ||||
| 	GuildCreate = "GUILD_CREATE", | ||||
| 	GuildUpdate = "GUILD_UPDATE", | ||||
| 	GuildDelete = "GUILD_DELETE", | ||||
| @ -525,6 +545,8 @@ export type EVENT = | ||||
| 	| "CHANNEL_UPDATE" | ||||
| 	| "CHANNEL_DELETE" | ||||
| 	| "CHANNEL_PINS_UPDATE" | ||||
| 	| "CHANNEL_RECIPIENT_ADD" | ||||
| 	| "CHANNEL_RECIPIENT_REMOVE" | ||||
| 	| "GUILD_CREATE" | ||||
| 	| "GUILD_UPDATE" | ||||
| 	| "GUILD_DELETE" | ||||
|  | ||||
							
								
								
									
										3
									
								
								util/src/util/Array.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								util/src/util/Array.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| export function containsAll(arr: any[], target: any[]) { | ||||
| 	return target.every(v => arr.includes(v)); | ||||
| } | ||||
| @ -92,6 +92,7 @@ export class Permissions extends BitField { | ||||
| 	} | ||||
| 
 | ||||
| 	overwriteChannel(overwrites: ChannelPermissionOverwrite[]) { | ||||
| 		if (!overwrites) return this | ||||
| 		if (!this.cache) throw new Error("permission chache not available"); | ||||
| 		overwrites = overwrites.filter((x) => { | ||||
| 			if (x.type === 0 && this.cache.roles?.some((r) => r.id === x.id)) return true; | ||||
|  | ||||
| @ -12,3 +12,4 @@ export * from "./RabbitMQ"; | ||||
| export * from "./Regex"; | ||||
| export * from "./Snowflake"; | ||||
| export * from "./String"; | ||||
| export * from "./Array"; | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Flam3rboy
						Flam3rboy