diff --git a/api/src/routes/channels/#channel_id/messages/index.ts b/api/src/routes/channels/#channel_id/messages/index.ts
index 26bb9e5d..b5a2d334 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";
@@ -186,33 +187,34 @@ router.post(
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();
- })
+ 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);
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..92809985
--- /dev/null
+++ b/api/src/routes/guilds/#guild_id/prune.ts
@@ -0,0 +1,82 @@
+import { Router, Request, Response } from "express";
+import { Guild, Member, Snowflake } from "@fosscord/util";
+import { LessThan, IsNull } 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())
+ },
+ {
+ 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)));
+
+ 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 } });
+
+ 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("/", 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
+
+ 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];
+
+ const { guild_id } = req.params;
+ const members = await inactiveMembers(guild_id, req.user_id, days, roles as string[]);
+
+ 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 7d7ac40a..12b0b49a 100644
--- a/util/src/entities/Member.ts
+++ b/util/src/entities/Member.ts
@@ -84,6 +84,9 @@ export class Member extends BaseClassWithoutId {
@Column({ type: "simple-json" })
settings: UserGuildSettings;
+ @Column({ nullable: true })
+ last_message_id?: string;
+
// TODO: update
// @Column({ type: "simple-json" })
// read_state: ReadState;
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++;
|