summary refs log tree commit diff
diff options
context:
space:
mode:
authorHayden Young <hi@hbjy.dev>2021-10-15 10:33:02 +0000
committerGitHub <noreply@github.com>2021-10-15 10:33:02 +0000
commit4d04e6fcc5142ed197ead89eceb01b2da2bed6dd (patch)
tree5ba4c9b6e839faae5cdfab16005aaa95ab19c3fa
parentchore: update ts-node during npm run setup (diff)
parentMerge pull request #460 from fosscord/dev (diff)
downloadserver-4d04e6fcc5142ed197ead89eceb01b2da2bed6dd.tar.xz
Merge branch 'master' into cdn-s3
-rw-r--r--api/src/middlewares/Authentication.ts2
-rw-r--r--api/src/routes/-/healthz.ts17
-rw-r--r--api/src/routes/-/readyz.ts17
-rw-r--r--api/src/routes/channels/#channel_id/messages/index.ts42
-rw-r--r--api/src/routes/guilds/#guild_id/prune.ts82
-rw-r--r--api/src/util/Instance.ts8
-rw-r--r--bundle/scripts/build.js25
-rw-r--r--util/src/entities/Member.ts3
-rw-r--r--util/src/util/Snowflake.ts2
9 files changed, 161 insertions, 37 deletions
diff --git a/api/src/middlewares/Authentication.ts b/api/src/middlewares/Authentication.ts
index 5a082751..59a181e6 100644
--- a/api/src/middlewares/Authentication.ts
+++ b/api/src/middlewares/Authentication.ts
@@ -9,6 +9,8 @@ export const NO_AUTHORIZATION_ROUTES = [
 	"/ping",
 	"/gateway",
 	"/experiments",
+	"/-/readyz",
+	"/-/healthz",
 	/\/guilds\/\d+\/widget\.(json|png)/
 ];
 
diff --git a/api/src/routes/-/healthz.ts b/api/src/routes/-/healthz.ts
new file mode 100644
index 00000000..a42575f8
--- /dev/null
+++ b/api/src/routes/-/healthz.ts
@@ -0,0 +1,17 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+import { getConnection } from "typeorm";
+
+const router = Router();
+
+router.get("/", route({}), (req: Request, res: Response) => {
+	try {
+		// test that the database is alive & responding
+		getConnection();
+		return res.sendStatus(200);
+	} catch(e) {
+		res.sendStatus(503);
+	}
+});
+
+export default router;
diff --git a/api/src/routes/-/readyz.ts b/api/src/routes/-/readyz.ts
new file mode 100644
index 00000000..a42575f8
--- /dev/null
+++ b/api/src/routes/-/readyz.ts
@@ -0,0 +1,17 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+import { getConnection } from "typeorm";
+
+const router = Router();
+
+router.get("/", route({}), (req: Request, res: Response) => {
+	try {
+		// test that the database is alive & responding
+		getConnection();
+		return res.sendStatus(200);
+	} catch(e) {
+		res.sendStatus(503);
+	}
+});
+
+export default router;
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/api/src/util/Instance.ts b/api/src/util/Instance.ts
index d1d9e1ab..7dcd126e 100644
--- a/api/src/util/Instance.ts
+++ b/api/src/util/Instance.ts
@@ -10,9 +10,9 @@ export async function initInstance() {
 
 	if (autoJoin.enabled && !autoJoin.guilds?.length) {
 		let guild = await Guild.findOne({});
-		if (!guild) guild = await Guild.createGuild({});
-
-		// @ts-ignore
-		await Config.set({ guild: { autoJoin: { guilds: [guild.id] } } });
+		if (guild) {
+			// @ts-ignore
+			await Config.set({ guild: { autoJoin: { guilds: [guild.id] } } });
+		}
 	}
 }
diff --git a/bundle/scripts/build.js b/bundle/scripts/build.js
index dfbaec15..dbc305a9 100644
--- a/bundle/scripts/build.js
+++ b/bundle/scripts/build.js
@@ -8,12 +8,12 @@ const dirs = ["api", "util", "cdn", "gateway", "bundle"];
 
 const verbose = argv.includes("verbose") || argv.includes("v");
 
-if(argv.includes("clean")){
-	dirs.forEach(a=>{
-		var d = "../"+a+"/dist";
-		if(fse.existsSync(d)) {
-			fse.rmSync(d,{recursive: true});
-			if(verbose) console.log(`Deleted ${d}!`);
+if (argv.includes("clean")) {
+	dirs.forEach((a) => {
+		var d = "../" + a + "/dist";
+		if (fse.existsSync(d)) {
+			fse.rmSync(d, { recursive: true });
+			if (verbose) console.log(`Deleted ${d}!`);
 		}
 	});
 }
@@ -24,9 +24,9 @@ fse.copySync(
 	path.join(__dirname, "..", "dist", "api", "client_test")
 );
 fse.copySync(path.join(__dirname, "..", "..", "api", "locales"), path.join(__dirname, "..", "dist", "api", "locales"));
-dirs.forEach(a=>{
-	fse.copySync("../"+a+"/src", "dist/"+a+"/src");
-	if(verbose) console.log(`Copied ${"../"+a+"/dist"} -> ${"dist/"+a+"/src"}!`);
+dirs.forEach((a) => {
+	fse.copySync("../" + a + "/src", "dist/" + a + "/src");
+	if (verbose) console.log(`Copied ${"../" + a + "/dist"} -> ${"dist/" + a + "/src"}!`);
 });
 
 console.log("Copying src files done");
@@ -34,10 +34,11 @@ console.log("Compiling src files ...");
 
 console.log(
 	execSync(
-		"node \"" +
+		'node "' +
 			path.join(__dirname, "..", "node_modules", "typescript", "lib", "tsc.js") +
-			"\" -p \"" +
-			path.join(__dirname, "..") + "\"",
+			'" -p "' +
+			path.join(__dirname, "..") +
+			'"',
 		{
 			cwd: path.join(__dirname, ".."),
 			shell: true,
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++;