From 808bf0a74c974d365fb83d31de370dfebb6d0367 Mon Sep 17 00:00:00 2001 From: Maddy Date: Wed, 13 Oct 2021 16:07:33 +1100 Subject: added route /guilds/:id/prune GET count and POST prune. Edited Member entity to include last_message_id to avoid searching for every members last message when pruning When a message is sent through /channels/:id/messages last_message_id of sending Member is updated. --- .../routes/channels/#channel_id/messages/index.ts | 10 ++- api/src/routes/guilds/#guild_id/prune.ts | 74 ++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 api/src/routes/guilds/#guild_id/prune.ts (limited to 'api') diff --git a/api/src/routes/channels/#channel_id/messages/index.ts b/api/src/routes/channels/#channel_id/messages/index.ts index 26bb9e5d..399fa7bb 100644 --- a/api/src/routes/channels/#channel_id/messages/index.ts +++ b/api/src/routes/channels/#channel_id/messages/index.ts @@ -10,7 +10,8 @@ import { getPermission, Message, MessageCreateEvent, - uploadFile + uploadFile, + Member, } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import { handleMessage, postHandleMessage, route } from "@fosscord/api"; @@ -188,6 +189,13 @@ router.post( await channel.assign({ last_message_id: message.id }).save(); + //gosh + var member = await Member.findOneOrFail({ + where: { id: req.user_id }, + }); + await member.assign({ last_message_id: message.id }) + await member.save(); //why does member.assign here return void? + if (channel.isDm()) { const channel_dto = await DmChannelDTO.from(channel); diff --git a/api/src/routes/guilds/#guild_id/prune.ts b/api/src/routes/guilds/#guild_id/prune.ts new file mode 100644 index 00000000..cddbcee7 --- /dev/null +++ b/api/src/routes/guilds/#guild_id/prune.ts @@ -0,0 +1,74 @@ +import { Router, Request, Response } from "express"; +import { Guild, Member, Snowflake } from "@fosscord/util"; +import { HTTPError } from "lambert-server"; +import { LessThan } from "typeorm"; +import { route } from "@fosscord/api"; +const router = Router(); + +//Returns all inactive members, respecting role hierarchy +export const inactiveMembers = async (guild_id: string, user_id: string, days: number, roles: string[] = []) => { + var date = new Date(); + date.setDate(date.getDate() - days); + //Snowflake should have `generateFromTime` method? Or similar? + var minId = BigInt(date.valueOf() - Snowflake.EPOCH) << BigInt(22); + + var members = await Member.find({ + where: [ + { + guild_id, + last_message_id: LessThan(minId.toString()), + }, + ], + relations: ["roles"] + }); + if (!members.length) return []; + + //I'm sure I can do this in the above db query ( and it would probably be better to do so ), but oh well. + if (roles.length && members.length) + members = members.filter(user => user.roles?.some(role => roles.includes(role.id))); + + const me = await Member.findOne({ id: user_id, guild_id }, { relations: ["roles"] }); + if (!me) throw new HTTPError("You are not member of this guild", 403); + const myHighestRole = Math.max(...(me.roles?.map(x => x.position) || [])) + + const guild = await Guild.findOneOrFail({ where: { id: guild_id } }) + + members = members.filter(member => + member.id !== guild.owner_id && //can't kick owner + member.roles?.some(role => + role.position < myHighestRole || //roles higher than me can't be kicked + me.id === guild.owner_id //owner can kick anyone + ) + ) + + return members; +} + +router.get("/", async (req: Request, res: Response) => { + const days = parseInt(req.query.days as string); + + var roles = req.query.include_roles; + if (typeof roles === 'string') roles = [roles]; //express will return array otherwise + + const members = await inactiveMembers(req.params.guild_id, req.user_id, days, roles as string[]); + + res.send({ pruned: members.length }); +}); + +router.post("/", route({ permission: "KICK_MEMBERS" }), async (req: Request, res: Response) => { + const days = parseInt(req.body.days); + + var roles = req.query.include_roles; + if (typeof roles === 'string') roles = [roles]; + + const { guild_id } = req.params; + const members = await inactiveMembers(guild_id, req.user_id, days, roles as string[]); + + for (var curr of members) { + await Member.removeFromGuild(curr.id, guild_id); + } + + res.send({ purged: members.length }); +}) + +export default router; -- cgit 1.4.1 From f38f79b70c3daa2ed45b27803fb01e6bc55adef9 Mon Sep 17 00:00:00 2001 From: Flam3rboy <34555296+Flam3rboy@users.noreply.github.com> Date: Fri, 15 Oct 2021 00:35:01 +0200 Subject: :zap: speed up message query --- .../routes/channels/#channel_id/messages/index.ts | 48 ++++++++++------------ 1 file changed, 21 insertions(+), 27 deletions(-) (limited to 'api') diff --git a/api/src/routes/channels/#channel_id/messages/index.ts b/api/src/routes/channels/#channel_id/messages/index.ts index 399fa7bb..b5a2d334 100644 --- a/api/src/routes/channels/#channel_id/messages/index.ts +++ b/api/src/routes/channels/#channel_id/messages/index.ts @@ -11,7 +11,7 @@ import { Message, MessageCreateEvent, uploadFile, - Member, + Member } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import { handleMessage, postHandleMessage, route } from "@fosscord/api"; @@ -187,40 +187,34 @@ router.post( message = await message.save(); - await channel.assign({ last_message_id: message.id }).save(); - - //gosh - var member = await Member.findOneOrFail({ - where: { id: req.user_id }, - }); - await member.assign({ last_message_id: message.id }) - await member.save(); //why does member.assign here return void? - 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(); - }) + channel.recipients!.map((recipient) => { + if (recipient.closed) { + recipient.closed = false; + return Promise.all([ + recipient.save(), + emitEvent({ + event: "CHANNEL_CREATE", + data: channel_dto.excludedRecipients([recipient.user_id]), + user_id: recipient.user_id + }) + ]); + } + }) ); } - await emitEvent({ event: "MESSAGE_CREATE", channel_id: channel_id, data: message } as MessageCreateEvent); + await Promise.all([ + channel.assign({ last_message_id: message.id }).save(), + new Member({ id: req.user_id, last_message_id: message.id }).save(), + 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); -- cgit 1.4.1 From 0994b47a6702c8957ba8ffcfe1720d3c70fabebe Mon Sep 17 00:00:00 2001 From: Flam3rboy <34555296+Flam3rboy@users.noreply.github.com> Date: Fri, 15 Oct 2021 00:35:18 +0200 Subject: :bug: fix prune --- api/src/routes/guilds/#guild_id/prune.ts | 56 ++++++++++++++++++-------------- util/src/entities/Member.ts | 5 ++- util/src/util/Snowflake.ts | 2 +- 3 files changed, 35 insertions(+), 28 deletions(-) (limited to 'api') diff --git a/api/src/routes/guilds/#guild_id/prune.ts b/api/src/routes/guilds/#guild_id/prune.ts index cddbcee7..92809985 100644 --- a/api/src/routes/guilds/#guild_id/prune.ts +++ b/api/src/routes/guilds/#guild_id/prune.ts @@ -1,7 +1,6 @@ import { Router, Request, Response } from "express"; import { Guild, Member, Snowflake } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; -import { LessThan } from "typeorm"; +import { LessThan, IsNull } from "typeorm"; import { route } from "@fosscord/api"; const router = Router(); @@ -16,59 +15,68 @@ export const inactiveMembers = async (guild_id: string, user_id: string, days: n where: [ { guild_id, - last_message_id: LessThan(minId.toString()), + last_message_id: LessThan(minId.toString()) }, + { + last_message_id: IsNull() + } ], relations: ["roles"] }); + console.log(members); if (!members.length) return []; //I'm sure I can do this in the above db query ( and it would probably be better to do so ), but oh well. - if (roles.length && members.length) - members = members.filter(user => user.roles?.some(role => roles.includes(role.id))); + if (roles.length && members.length) members = members.filter((user) => user.roles?.some((role) => roles.includes(role.id))); - const me = await Member.findOne({ id: user_id, guild_id }, { relations: ["roles"] }); - if (!me) throw new HTTPError("You are not member of this guild", 403); - const myHighestRole = Math.max(...(me.roles?.map(x => x.position) || [])) + const me = await Member.findOneOrFail({ id: user_id, guild_id }, { relations: ["roles"] }); + const myHighestRole = Math.max(...(me.roles?.map((x) => x.position) || [])); - const guild = await Guild.findOneOrFail({ where: { id: guild_id } }) + const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); - members = members.filter(member => - member.id !== guild.owner_id && //can't kick owner - member.roles?.some(role => - role.position < myHighestRole || //roles higher than me can't be kicked - me.id === guild.owner_id //owner can kick anyone - ) - ) + members = members.filter( + (member) => + member.id !== guild.owner_id && //can't kick owner + member.roles?.some( + (role) => + role.position < myHighestRole || //roles higher than me can't be kicked + me.id === guild.owner_id //owner can kick anyone + ) + ); return members; -} +}; -router.get("/", async (req: Request, res: Response) => { +router.get("/", route({ permission: "KICK_MEMBERS" }), async (req: Request, res: Response) => { const days = parseInt(req.query.days as string); var roles = req.query.include_roles; - if (typeof roles === 'string') roles = [roles]; //express will return array otherwise + if (typeof roles === "string") roles = [roles]; //express will return array otherwise const members = await inactiveMembers(req.params.guild_id, req.user_id, days, roles as string[]); res.send({ pruned: members.length }); }); +export interface PruneSchema { + /** + * @min 0 + */ + days: number; +} + router.post("/", route({ permission: "KICK_MEMBERS" }), async (req: Request, res: Response) => { const days = parseInt(req.body.days); var roles = req.query.include_roles; - if (typeof roles === 'string') roles = [roles]; + if (typeof roles === "string") roles = [roles]; const { guild_id } = req.params; const members = await inactiveMembers(guild_id, req.user_id, days, roles as string[]); - for (var curr of members) { - await Member.removeFromGuild(curr.id, guild_id); - } + await Promise.all(members.map((x) => Member.removeFromGuild(x.id, guild_id))); res.send({ purged: members.length }); -}) +}); export default router; diff --git a/util/src/entities/Member.ts b/util/src/entities/Member.ts index 19747318..12b0b49a 100644 --- a/util/src/entities/Member.ts +++ b/util/src/entities/Member.ts @@ -84,8 +84,8 @@ export class Member extends BaseClassWithoutId { @Column({ type: "simple-json" }) settings: UserGuildSettings; - @Column() - last_message_id: string; + @Column({ nullable: true }) + last_message_id?: string; // TODO: update // @Column({ type: "simple-json" }) @@ -233,7 +233,6 @@ export class Member extends BaseClassWithoutId { deaf: false, mute: false, pending: false, - last_message_id: "", }; await Promise.all([ diff --git a/util/src/util/Snowflake.ts b/util/src/util/Snowflake.ts index f7a13388..3f6e3c63 100644 --- a/util/src/util/Snowflake.ts +++ b/util/src/util/Snowflake.ts @@ -84,7 +84,7 @@ export class Snowflake { } static generate() { - var time = BigInt(Date.now() - Snowflake.EPOCH) << 22n; + var time = BigInt(Date.now() - Snowflake.EPOCH) << BigInt(22); var worker = Snowflake.workerId << 17n; var process = Snowflake.processId << 12n; var increment = Snowflake.INCREMENT++; -- cgit 1.4.1