diff --git a/src/routes/guilds/#id/bans.ts b/src/routes/guilds/#id/bans.ts
new file mode 100644
index 00000000..5133ee3c
--- /dev/null
+++ b/src/routes/guilds/#id/bans.ts
@@ -0,0 +1,93 @@
+import { Request, Response, Router } from "express";
+import { BanModel, getPermission, GuildBanAddEvent, GuildBanRemoveEvent, GuildModel } from "fosscord-server-util";
+import { HTTPError } from "lambert-server";
+import { getIpAdress } from "../../../../../middlewares/GlobalRateLimit";
+import { BanCreateSchema } from "../../../../../schema/Ban";
+import { emitEvent } from "../../../../../util/Event";
+import { check } from "../../../../../util/instanceOf";
+import { removeMember } from "../../../../../util/Member";
+import { getPublicUser } from "../../../../../util/User";
+
+const router: Router = Router();
+
+router.get("/", async (req: Request, res: Response) => {
+ const guild_id = BigInt(req.params.id);
+
+ const guild = await GuildModel.findOne({ id: guild_id }).exec();
+ if (!guild) throw new HTTPError("Guild not found", 404);
+
+ var bans = await BanModel.find({ guild_id: guild_id }).exec();
+ return res.json(bans);
+});
+
+router.get("/:user", async (req: Request, res: Response) => {
+ const guild_id = BigInt(req.params.id);
+ const user_id = BigInt(req.params.ban);
+
+ var ban = await BanModel.findOne({ guild_id: guild_id, user_id: user_id }).exec();
+ if (!ban) throw new HTTPError("Ban not found", 404);
+ return res.json(ban);
+});
+
+router.post("/:user_id", check(BanCreateSchema), async (req: Request, res: Response) => {
+ const guild_id = BigInt(req.params.id);
+ const banned_user_id = BigInt(req.params.user_id);
+
+ const banned_user = await getPublicUser(banned_user_id);
+ const perms = await getPermission(req.user_id, guild_id);
+ if (!perms.has("BAN_MEMBERS")) throw new HTTPError("You don't have the permission to ban members", 403);
+ if (req.user_id === banned_user_id) throw new HTTPError("You can't ban yourself", 400);
+
+ await removeMember(banned_user_id, guild_id);
+
+ const ban = await new BanModel({
+ user_id: banned_user_id,
+ guild_id: guild_id,
+ ip: getIpAdress(req),
+ executor_id: req.user_id,
+ reason: req.body.reason, // || otherwise empty
+ }).save();
+
+ await emitEvent({
+ event: "GUILD_BAN_ADD",
+ data: {
+ guild_id: guild_id,
+ user: banned_user,
+ },
+ guild_id: guild_id,
+ } as GuildBanAddEvent);
+
+ return res.json(ban).send();
+});
+
+router.delete("/:user_id", async (req: Request, res: Response) => {
+ var guild_id = BigInt(req.params.id);
+ var banned_user_id = BigInt(req.params.user_id);
+
+ const banned_user = await getPublicUser(banned_user_id);
+ const guild = await GuildModel.findOne({ id: guild_id }, { id: true }).exec();
+ if (!guild) throw new HTTPError("Guild not found", 404);
+
+ const perms = await getPermission(req.user_id, guild.id);
+ if (!perms.has("BAN_MEMBERS")) {
+ throw new HTTPError("No permissions", 403);
+ }
+
+ await BanModel.deleteOne({
+ user_id: banned_user_id,
+ guild_id: guild.id,
+ }).exec();
+
+ await emitEvent({
+ event: "GUILD_BAN_REMOVE",
+ data: {
+ guild_id: guild.id,
+ user: banned_user,
+ },
+ guild_id: guild.id,
+ } as GuildBanRemoveEvent);
+
+ return res.status(204).send();
+});
+
+export default router;
diff --git a/src/routes/guilds/#id/channels.ts b/src/routes/guilds/#id/channels.ts
new file mode 100644
index 00000000..1316a2ca
--- /dev/null
+++ b/src/routes/guilds/#id/channels.ts
@@ -0,0 +1,51 @@
+import { Router } from "express";
+import { ChannelModel, ChannelType, GuildModel, Snowflake } from "fosscord-server-util";
+import { HTTPError } from "lambert-server";
+import { ChannelModifySchema } from "../../../../../schema/Channel";
+import { check } from "../../../../../util/instanceOf";
+const router = Router();
+
+router.get("/", async (req, res) => {
+ const guild_id = BigInt(req.params.id);
+ const channels = await ChannelModel.find({ guild_id }).exec();
+
+ res.json(channels);
+});
+
+router.post("/", check(ChannelModifySchema), async (req, res) => {
+ const guild_id = BigInt(req.params.id);
+ const body = req.body as ChannelModifySchema;
+ if (!body.permission_overwrites) body.permission_overwrites = [];
+ if (!body.topic) body.topic = "";
+ if (!body.rate_limit_per_user) body.rate_limit_per_user = 0;
+ switch (body.type) {
+ case ChannelType.DM:
+ case ChannelType.GROUP_DM:
+ throw new HTTPError("You can't create a dm channel in a guild");
+ // TODO:
+ case ChannelType.GUILD_STORE:
+ throw new HTTPError("Not yet supported");
+ case ChannelType.GUILD_NEWS:
+ // TODO: check if guild is community server
+ }
+
+ if (body.parent_id) {
+ const exists = ChannelModel.findOne({ channel_id: body.parent_id }).exec();
+ if (!exists) throw new HTTPError("Parent id channel doesn't exist", 400);
+ }
+
+ const guild = await GuildModel.findOne({ id: guild_id }, { id: true }).exec();
+ if (!guild) throw new HTTPError("Guild not found", 4040);
+
+ const channel = {
+ ...body,
+ id: Snowflake.generate(),
+ created_at: new Date(),
+ guild_id,
+ };
+ await new ChannelModel(channel).save();
+
+ res.json(channel);
+});
+
+export default router;
diff --git a/src/routes/guilds/#id/index.ts b/src/routes/guilds/#id/index.ts
new file mode 100644
index 00000000..e86d9416
--- /dev/null
+++ b/src/routes/guilds/#id/index.ts
@@ -0,0 +1,73 @@
+import { Request, Response, Router } from "express";
+import {
+ ChannelModel,
+ EmojiModel,
+ getPermission,
+ GuildDeleteEvent,
+ GuildModel,
+ InviteModel,
+ MemberModel,
+ MessageModel,
+ RoleModel,
+ UserModel,
+} from "fosscord-server-util";
+import { HTTPError } from "lambert-server";
+import { GuildUpdateSchema } from "../../../../../schema/Guild";
+import { emitEvent } from "../../../../../util/Event";
+import { check } from "../../../../../util/instanceOf";
+
+const router = Router();
+
+router.get("/", async (req: Request, res: Response) => {
+ const guild_id = BigInt(req.params.id);
+
+ const guild = await GuildModel.findOne({ id: guild_id }).exec();
+ if (!guild) throw new HTTPError("Guild does not exist", 404);
+
+ const member = await MemberModel.findOne({ guild_id: guild_id, id: req.user_id }, "id").exec();
+ if (!member) throw new HTTPError("You are not a member of the guild you are trying to access", 401);
+
+ return res.json(guild);
+});
+
+router.patch("/", check(GuildUpdateSchema), async (req: Request, res: Response) => {
+ const body = req.body as GuildUpdateSchema;
+ const guild_id = BigInt(req.params.id);
+
+ const guild = await GuildModel.findOne({ id: guild_id }).exec();
+ if (!guild) throw new HTTPError("This guild does not exist", 404);
+
+ const perms = await getPermission(req.user_id, guild_id);
+ if (!perms.has("MANAGE_GUILD")) throw new HTTPError("You do not have the MANAGE_GUILD permission", 401);
+
+ await GuildModel.updateOne({ id: guild_id }, body).exec();
+ return res.status(204);
+});
+
+router.delete("/", async (req: Request, res: Response) => {
+ var guild_id = BigInt(req.params.id);
+
+ const guild = await GuildModel.findOne({ id: guild_id }, "owner_id").exec();
+ if (!guild) throw new HTTPError("This guild does not exist", 404);
+ if (guild.owner_id !== req.user_id) throw new HTTPError("You are not the owner of this guild", 401);
+
+ await emitEvent({
+ event: "GUILD_DELETE",
+ data: {
+ id: guild_id,
+ },
+ guild_id: guild_id,
+ } as GuildDeleteEvent);
+
+ await GuildModel.deleteOne({ id: guild_id }).exec();
+ await UserModel.updateMany({ guilds: guild_id }, { $pull: { guilds: guild_id } }).exec();
+ await RoleModel.deleteMany({ guild_id }).exec();
+ await ChannelModel.deleteMany({ guild_id }).exec();
+ await EmojiModel.deleteMany({ guild_id }).exec();
+ await InviteModel.deleteMany({ guild_id }).exec();
+ await MessageModel.deleteMany({ guild_id }).exec();
+
+ return res.status(204).send();
+});
+
+export default router;
diff --git a/src/routes/guilds/#id/members.ts b/src/routes/guilds/#id/members.ts
new file mode 100644
index 00000000..0aed61ae
--- /dev/null
+++ b/src/routes/guilds/#id/members.ts
@@ -0,0 +1,54 @@
+import { Request, Response, Router } from "express";
+import { GuildModel, MemberModel } from "fosscord-server-util";
+import { HTTPError } from "lambert-server";
+import { instanceOf, Length } from "../../../../../util/instanceOf";
+import { PublicMemberProjection } from "../../../../../util/Member";
+import { PublicUserProjection } from "../../../../../util/User";
+
+const router = Router();
+
+// TODO: not allowed for user -> only allowed for bots with privileged intents
+// TODO: send over websocket
+router.get("/", async (req: Request, res: Response) => {
+ const guild_id = BigInt(req.params.id);
+ const guild = await GuildModel.findOne({ id: guild_id }).exec();
+ if (!guild) throw new HTTPError("Guild not found", 404);
+
+ try {
+ instanceOf({ $limit: new Length(Number, 1, 1000), $after: BigInt }, req.query, {
+ path: "query",
+ req,
+ ref: { obj: null, key: "" },
+ });
+ } catch (error) {
+ return res.status(400).json({ code: 50035, message: "Invalid Query", success: false, errors: error });
+ }
+
+ // @ts-ignore
+ if (!req.query.limit) req.query.limit = 1;
+ const { limit, after } = (<unknown>req.query) as { limit: number; after: bigint };
+ const query = after ? { id: { $gt: after } } : {};
+
+ var members = await MemberModel.find({ guild_id, ...query }, PublicMemberProjection)
+ .limit(limit)
+ .populate({ path: "user", select: PublicUserProjection })
+ .exec();
+
+ return res.json(members);
+});
+
+router.get("/:member", async (req: Request, res: Response) => {
+ const guild_id = BigInt(req.params.id);
+ const user_id = BigInt(req.params.member);
+
+ const member = await MemberModel.findOne({ id: user_id, guild_id }).populate({ path: "user", select: PublicUserProjection }).exec();
+ if (!member) throw new HTTPError("Member not found", 404);
+
+ return res.json(member);
+});
+
+router.put("/:member", async (req: Request, res: Response) => {
+ // https://discord.com/developers/docs/resources/guild#add-guild-member
+});
+
+export default router;
diff --git a/src/routes/guilds/index.ts b/src/routes/guilds/index.ts
new file mode 100644
index 00000000..319184ad
--- /dev/null
+++ b/src/routes/guilds/index.ts
@@ -0,0 +1,81 @@
+import { Router, Request, Response } from "express";
+import { RoleModel, GuildModel, Snowflake, Guild } from "fosscord-server-util";
+import { HTTPError } from "lambert-server";
+import { check } from "./../../../../util/instanceOf";
+import { GuildCreateSchema } from "../../../../schema/Guild";
+import Config from "../../../../util/Config";
+import { getPublicUser } from "../../../../util/User";
+import { addMember } from "../../../../util/Member";
+
+const router: Router = Router();
+
+router.post("/", check(GuildCreateSchema), async (req: Request, res: Response) => {
+ const body = req.body as GuildCreateSchema;
+
+ const { maxGuilds } = Config.get().limits.user;
+ const user = await getPublicUser(req.user_id, { guilds: true });
+
+ if (user.guilds.length >= maxGuilds) {
+ throw new HTTPError(`Maximum number of guilds reached ${maxGuilds}`, 403);
+ }
+
+ const guild_id = Snowflake.generate();
+ const guild: Guild = {
+ name: body.name,
+ region: body.region || "en-US",
+ owner_id: req.user_id,
+ icon: undefined,
+ afk_channel_id: undefined,
+ afk_timeout: 300,
+ application_id: undefined,
+ banner: undefined,
+ default_message_notifications: undefined,
+ description: undefined,
+ splash: undefined,
+ discovery_splash: undefined,
+ explicit_content_filter: undefined,
+ features: [],
+ id: guild_id,
+ large: undefined,
+ max_members: 250000,
+ max_presences: 250000,
+ max_video_channel_users: 25,
+ presence_count: 0,
+ member_count: 0, // will automatically be increased by addMember()
+ mfa_level: 0,
+ preferred_locale: "en-US",
+ premium_subscription_count: 0,
+ premium_tier: 0,
+ public_updates_channel_id: undefined,
+ rules_channel_id: undefined,
+ system_channel_flags: undefined,
+ system_channel_id: undefined,
+ unavailable: false,
+ vanity_url_code: undefined,
+ verification_level: undefined,
+ welcome_screen: [],
+ widget_channel_id: undefined,
+ widget_enabled: false,
+ };
+
+ await Promise.all([
+ new GuildModel(guild).save(),
+ new RoleModel({
+ id: guild_id,
+ guild_id: guild_id,
+ color: 0,
+ hoist: false,
+ managed: true,
+ mentionable: true,
+ name: "@everyone",
+ permissions: 2251804225n,
+ position: 0,
+ tags: null,
+ }).save(),
+ ]);
+ await addMember(req.user_id, guild_id, { guild });
+
+ res.status(201).json({ id: guild.id });
+});
+
+export default router;
diff --git a/src/routes/guilds/templates/index.ts b/src/routes/guilds/templates/index.ts
new file mode 100644
index 00000000..9a4e81fa
--- /dev/null
+++ b/src/routes/guilds/templates/index.ts
@@ -0,0 +1,4 @@
+import { Router } from "express";
+const router: Router = Router();
+
+export default router;
|