diff --git a/api/src/Server.ts b/api/src/Server.ts
index 259c2a6b..4cf0917d 100644
--- a/api/src/Server.ts
+++ b/api/src/Server.ts
@@ -10,9 +10,9 @@ import { initRateLimits } from "./middlewares/RateLimit";
import TestClient from "./middlewares/TestClient";
import { initTranslation } from "./middlewares/Translation";
import morgan from "morgan";
-import { initInstance } from "./util/Instance";
+import { initInstance } from "./util/handlers/Instance";
import { registerRoutes } from "@fosscord/util";
-import { red } from "nanocolors"
+import { red } from "picocolors"
export interface FosscordServerOptions extends ServerOptions {}
diff --git a/api/src/routes/channels/#channel_id/messages/index.ts b/api/src/routes/channels/#channel_id/messages/index.ts
index d3c0a409..1ae9d676 100644
--- a/api/src/routes/channels/#channel_id/messages/index.ts
+++ b/api/src/routes/channels/#channel_id/messages/index.ts
@@ -37,7 +37,11 @@ export function isTextChannel(type: ChannelType): boolean {
case ChannelType.GUILD_PUBLIC_THREAD:
case ChannelType.GUILD_PRIVATE_THREAD:
case ChannelType.GUILD_TEXT:
+ case ChannelType.ENCRYPTED:
+ case ChannelType.ENCRYPTED_THREAD:
return true;
+ default:
+ throw new HTTPError("unimplemented", 400);
}
}
@@ -87,7 +91,7 @@ router.get("/", async (req: Request, res: Response) => {
permissions.hasThrow("VIEW_CHANNEL");
if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]);
- var query: FindManyOptions<Message> & { where: { id?: any } } = {
+ var query: FindManyOptions<Message> & { where: { id?: any; }; } = {
order: { id: "DESC" },
take: limit,
where: { channel_id },
@@ -107,7 +111,7 @@ router.get("/", async (req: Request, res: Response) => {
const endpoint = Config.get().cdn.endpointPublic;
return res.json(
- messages.map((x) => {
+ messages.map((x: any) => {
(x.reactions || []).forEach((x: any) => {
// @ts-ignore
if ((x.user_ids || []).includes(req.user_id)) x.me = true;
@@ -116,10 +120,10 @@ router.get("/", async (req: Request, res: Response) => {
});
// @ts-ignore
if (!x.author) x.author = { discriminator: "0000", username: "Deleted User", public_flags: "0", avatar: null };
- x.attachments?.forEach((x) => {
+ x.attachments?.forEach((y: any) => {
// dynamically set attachment proxy_url in case the endpoint changed
- const uri = x.proxy_url.startsWith("http") ? x.proxy_url : `https://example.org${x.proxy_url}`;
- x.proxy_url = `${endpoint == null ? "" : endpoint}${new URL(uri).pathname}`;
+ const uri = y.proxy_url.startsWith("http") ? y.proxy_url : `https://example.org${y.proxy_url}`;
+ y.proxy_url = `${endpoint == null ? "" : endpoint}${new URL(uri).pathname}`;
});
return x;
@@ -172,7 +176,7 @@ router.post(
}
const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients", "recipients.user"] });
- const embeds = [];
+ const embeds = body.embeds || [];
if (body.embed) embeds.push(body.embed);
let message = await handleMessage({
...body,
@@ -216,7 +220,7 @@ router.post(
channel.save()
]);
- postHandleMessage(message).catch((e) => {}); // no await as it shouldnt block the message send function and silently catch error
+ 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/channels/#channel_id/webhooks.ts b/api/src/routes/channels/#channel_id/webhooks.ts
index 7b894455..92895da6 100644
--- a/api/src/routes/channels/#channel_id/webhooks.ts
+++ b/api/src/routes/channels/#channel_id/webhooks.ts
@@ -14,6 +14,10 @@ export interface WebhookCreateSchema {
name: string;
avatar: string;
}
+//TODO: implement webhooks
+router.get("/", route({}), async (req: Request, res: Response) => {
+ res.json([]);
+});
// TODO: use Image Data Type for avatar instead of String
router.post("/", route({ body: "WebhookCreateSchema", permission: "MANAGE_WEBHOOKS" }), async (req: Request, res: Response) => {
diff --git a/api/src/routes/guilds/#guild_id/audit-logs.ts b/api/src/routes/guilds/#guild_id/audit-logs.ts
new file mode 100644
index 00000000..a4f2f800
--- /dev/null
+++ b/api/src/routes/guilds/#guild_id/audit-logs.ts
@@ -0,0 +1,20 @@
+import { Router, Response, Request } from "express";
+import { Channel, ChannelUpdateEvent, getPermission, emitEvent } from "@fosscord/util";
+import { HTTPError } from "lambert-server";
+import { route } from "@fosscord/api";
+import { ChannelModifySchema } from "../../channels/#channel_id";
+const router = Router();
+
+//TODO: implement audit logs
+router.get("/", route({}), async (req: Request, res: Response) => {
+ res.json({
+ audit_log_entries: [],
+ users: [],
+ integrations: [],
+ webhooks: [],
+ guild_scheduled_events: [],
+ threads: [],
+ application_commands: []
+ });
+});
+export default router;
diff --git a/api/src/routes/guilds/#guild_id/bans.ts b/api/src/routes/guilds/#guild_id/bans.ts
index e7d46898..1e09a38d 100644
--- a/api/src/routes/guilds/#guild_id/bans.ts
+++ b/api/src/routes/guilds/#guild_id/bans.ts
@@ -6,13 +6,32 @@ import { getIpAdress, route } from "@fosscord/api";
export interface BanCreateSchema {
delete_message_days?: string;
reason?: string;
-}
+};
+
+export interface BanRegistrySchema {
+ id: string;
+ user_id: string;
+ guild_id: string;
+ executor_id: string;
+ ip?: string;
+ reason?: string | undefined;
+};
const router: Router = Router();
+
+/* TODO: Deleting the secrets is just a temporary go-around. Views should be implemented for both safety and better handling. */
+
router.get("/", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => {
const { guild_id } = req.params;
- var bans = await Ban.find({ guild_id: guild_id });
+ let bans = await Ban.find({ guild_id: guild_id });
+
+ /* Filter secret from database registry.*/
+
+ bans.forEach((registry: BanRegistrySchema) => {
+ delete registry.ip;
+ });
+
return res.json(bans);
});
@@ -20,7 +39,12 @@ router.get("/:user", route({ permission: "BAN_MEMBERS" }), async (req: Request,
const { guild_id } = req.params;
const user_id = req.params.ban;
- var ban = await Ban.findOneOrFail({ guild_id: guild_id, user_id: user_id });
+ let ban = await Ban.findOneOrFail({ guild_id: guild_id, user_id: user_id }) as BanRegistrySchema;
+
+ /* Filter secret from registry. */
+
+ delete ban.ip
+
return res.json(ban);
});
diff --git a/api/src/routes/guilds/#guild_id/integrations.ts b/api/src/routes/guilds/#guild_id/integrations.ts
new file mode 100644
index 00000000..abf997c9
--- /dev/null
+++ b/api/src/routes/guilds/#guild_id/integrations.ts
@@ -0,0 +1,12 @@
+import { Router, Response, Request } from "express";
+import { Channel, ChannelUpdateEvent, getPermission, emitEvent } from "@fosscord/util";
+import { HTTPError } from "lambert-server";
+import { route } from "@fosscord/api";
+import { ChannelModifySchema } from "../../channels/#channel_id";
+const router = Router();
+
+//TODO: implement integrations list
+router.get("/", route({}), async (req: Request, res: Response) => {
+ res.json([]);
+});
+export default router;
diff --git a/api/src/routes/guilds/#guild_id/roles.ts b/api/src/routes/guilds/#guild_id/roles.ts
index b1875598..b6894e3f 100644
--- a/api/src/routes/guilds/#guild_id/roles.ts
+++ b/api/src/routes/guilds/#guild_id/roles.ts
@@ -8,7 +8,8 @@ import {
GuildRoleDeleteEvent,
emitEvent,
Config,
- DiscordApiErrors
+ DiscordApiErrors,
+ handleFile
} from "@fosscord/util";
import { HTTPError } from "lambert-server";
import { route } from "@fosscord/api";
@@ -22,6 +23,8 @@ export interface RoleModifySchema {
hoist?: boolean; // whether the role should be displayed separately in the sidebar
mentionable?: boolean; // whether the role should be mentionable
position?: number;
+ icon?: string;
+ unicode_emoji?: string;
}
export type RolePositionUpdateSchema = {
@@ -58,7 +61,9 @@ router.post("/", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" })
guild_id: guild_id,
managed: false,
permissions: String(req.permission!.bitfield & BigInt(body.permissions || "0")),
- tags: undefined
+ tags: undefined,
+ icon: null,
+ unicode_emoji: null
});
await Promise.all([
@@ -105,6 +110,8 @@ router.patch("/:role_id", route({ body: "RoleModifySchema", permission: "MANAGE_
const { role_id, guild_id } = req.params;
const body = req.body as RoleModifySchema;
+ if (body.icon) body.icon = await handleFile(`/role-icons/${role_id}`, body.icon as string);
+
const role = new Role({
...body,
id: role_id,
diff --git a/api/src/routes/guilds/#guild_id/webhooks.ts b/api/src/routes/guilds/#guild_id/webhooks.ts
new file mode 100644
index 00000000..8b2febea
--- /dev/null
+++ b/api/src/routes/guilds/#guild_id/webhooks.ts
@@ -0,0 +1,12 @@
+import { Router, Response, Request } from "express";
+import { Channel, ChannelUpdateEvent, getPermission, emitEvent } from "@fosscord/util";
+import { HTTPError } from "lambert-server";
+import { route } from "@fosscord/api";
+import { ChannelModifySchema } from "../../channels/#channel_id";
+const router = Router();
+
+//TODO: implement webhooks
+router.get("/", route({}), async (req: Request, res: Response) => {
+ res.json([]);
+});
+export default router;
diff --git a/api/src/routes/invites/index.ts b/api/src/routes/invites/index.ts
index a2cf4cb5..37e9e05a 100644
--- a/api/src/routes/invites/index.ts
+++ b/api/src/routes/invites/index.ts
@@ -19,7 +19,8 @@ router.post("/:code", route({}), async (req: Request, res: Response) => {
const { features } = await Guild.findOneOrFail({ id: guild_id});
const { public_flags } = await User.findOneOrFail({ id: req.user_id });
- if(features.includes("INTERNAL_EMPLOYEE_ONLY") && (public_flags & 1) !== 1) throw new HTTPError("You are not allowed to join this guild.", 401)
+ if(features.includes("INTERNAL_EMPLOYEE_ONLY") && (public_flags & 1) !== 1) throw new HTTPError("Only intended for the staff of this server.", 401);
+ if(features.includes("INVITES_CLOSED")) throw new HTTPError("Sorry, this guild has joins closed.", 403);
const invite = await Invite.joinGuild(req.user_id, code);
diff --git a/api/src/routes/stop.ts b/api/src/routes/stop.ts
new file mode 100644
index 00000000..7f8b78ba
--- /dev/null
+++ b/api/src/routes/stop.ts
@@ -0,0 +1,26 @@
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+import { User } from "@fosscord/util";
+
+const router: Router = Router();
+
+router.post("/", route({}), async (req: Request, res: Response) => {
+ //EXPERIMENTAL: have an "OPERATOR" platform permission implemented for this API route
+ const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["rights"] });
+ if((Number(user.rights) << Number(0))%Number(2)==Number(1)) {
+ console.log("user that POSTed to the API was ALLOWED");
+ console.log(user.rights);
+ res.sendStatus(200)
+ process.kill(process.pid, 'SIGTERM')
+ }
+ else {
+ console.log("operation failed");
+ console.log(user.rights);
+ res.sendStatus(403)
+ }
+});
+
+export default router;
+
+//THIS API CAN ONLY BE USED BY USERS WITH THE 'OPERATOR' RIGHT (which is the value of 1) ONLY IF ANY OTHER RIGHTS ARE ADDED OR IF THE USER DOESNT HAVE PERMISSION,
+//THE REQUEST WILL RETURN 403 'FORBIDDEN'
diff --git a/api/src/util/blockedEmailDomains.txt b/api/src/util/entities/blockedEmailDomains.txt
index eb88305d..eb88305d 100644
--- a/api/src/util/blockedEmailDomains.txt
+++ b/api/src/util/entities/blockedEmailDomains.txt
diff --git a/api/src/util/trustedEmailDomains.txt b/api/src/util/entities/trustedEmailDomains.txt
index 38ffa4fa..38ffa4fa 100644
--- a/api/src/util/trustedEmailDomains.txt
+++ b/api/src/util/entities/trustedEmailDomains.txt
diff --git a/api/src/util/Instance.ts b/api/src/util/handlers/Instance.ts
index 6bddfa98..6bddfa98 100644
--- a/api/src/util/Instance.ts
+++ b/api/src/util/handlers/Instance.ts
diff --git a/api/src/util/Message.ts b/api/src/util/handlers/Message.ts
index 4ba93edd..21664368 100644
--- a/api/src/util/Message.ts
+++ b/api/src/util/handlers/Message.ts
@@ -2,6 +2,7 @@ import {
Channel,
Embed,
emitEvent,
+ Guild,
Message,
MessageCreateEvent,
MessageUpdateEvent,
@@ -17,13 +18,14 @@ import {
User,
Application,
Webhook,
- Attachment
+ Attachment,
+ Config,
} from "@fosscord/util";
import { HTTPError } from "lambert-server";
import fetch from "node-fetch";
import cheerio from "cheerio";
-import { MessageCreateSchema } from "../routes/channels/#channel_id/messages";
-
+import { MessageCreateSchema } from "../../routes/channels/#channel_id/messages";
+const allow_empty = false;
// TODO: check webhook, application, system author, stickers
// TODO: embed gifs/videos/images
@@ -55,6 +57,10 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
type: opts.type ?? 0
});
+ if (message.content && message.content.length > Config.get().limits.message.maxCharacters) {
+ throw new HTTPError("Content length over max character limit")
+ }
+
// TODO: are tts messages allowed in dm channels? should permission be checked?
if (opts.author_id) {
message.author = await User.getPublicUser(opts.author_id);
@@ -67,7 +73,7 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
}
const permission = await getPermission(opts.author_id, channel.guild_id, opts.channel_id);
- permission.hasThrow("SEND_MESSAGES");
+ permission.hasThrow("SEND_MESSAGES"); // TODO: add the rights check
if (permission.cache.member) {
message.member = permission.cache.member;
}
@@ -75,15 +81,19 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
if (opts.tts) permission.hasThrow("SEND_TTS_MESSAGES");
if (opts.message_reference) {
permission.hasThrow("READ_MESSAGE_HISTORY");
- if (opts.message_reference.guild_id !== channel.guild_id) throw new HTTPError("You can only reference messages from this guild");
- if (opts.message_reference.channel_id !== opts.channel_id) throw new HTTPError("You can only reference messages from this channel");
+ // code below has to be redone when we add custom message routing and cross-channel replies
+ const guild = await Guild.findOneOrFail({ id: channel.guild_id });
+ if (!guild.features.includes("CROSS_CHANNEL_REPLIES")) {
+ if (opts.message_reference.guild_id !== channel.guild_id) throw new HTTPError("You can only reference messages from this guild");
+ if (opts.message_reference.channel_id !== opts.channel_id) throw new HTTPError("You can only reference messages from this channel");
+ }
// TODO: should be checked if the referenced message exists?
// @ts-ignore
message.type = MessageType.REPLY;
}
// TODO: stickers/activity
- if (!opts.content && !opts.embeds?.length && !opts.attachments?.length && !opts.sticker_ids?.length) {
+ if (!allow_empty && (!opts.content && !opts.embeds?.length && !opts.attachments?.length && !opts.sticker_ids?.length)) {
throw new HTTPError("Empty messages are not allowed", 50006);
}
@@ -93,7 +103,7 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
var mention_user_ids = [] as string[];
var mention_everyone = false;
- if (content) {
+ if (content) { // TODO: explicit-only mentions
message.content = content.trim();
for (const [_, mention] of content.matchAll(CHANNEL_MENTION)) {
if (!mention_channel_ids.includes(mention)) mention_channel_ids.push(mention);
@@ -135,7 +145,7 @@ export async function postHandleMessage(message: Message) {
const data = { ...message };
data.embeds = data.embeds.filter((x) => x.type !== "link");
- links = links.slice(0, 5); // embed max 5 links
+ links = links.slice(0, 20); // embed max 20 links — TODO: make this configurable with instance policies
for (const link of links) {
try {
@@ -188,7 +198,7 @@ export async function sendMessage(opts: MessageOptions) {
emitEvent({ event: "MESSAGE_CREATE", channel_id: opts.channel_id, data: message.toJSON() } as MessageCreateEvent)
]);
- postHandleMessage(message).catch((e) => {}); // no await as it shouldnt block the message send function and silently catch error
+ postHandleMessage(message).catch((e) => {}); // no await as it should catch error non-blockingly
return message;
}
diff --git a/api/src/util/Voice.ts b/api/src/util/handlers/Voice.ts
index f06b1aaa..4d60eb91 100644
--- a/api/src/util/Voice.ts
+++ b/api/src/util/handlers/Voice.ts
@@ -1,5 +1,5 @@
import { Config } from "@fosscord/util";
-import { distanceBetweenLocations, IPAnalysis } from "./ipAddress";
+import { distanceBetweenLocations, IPAnalysis } from "../utility/ipAddress";
export async function getVoiceRegions(ipAddress: string, vip: boolean) {
const regions = Config.get().regions;
diff --git a/api/src/util/route.ts b/api/src/util/handlers/route.ts
index e4794eb5..05658ad3 100644
--- a/api/src/util/route.ts
+++ b/api/src/util/handlers/route.ts
@@ -18,8 +18,9 @@ import Ajv from "ajv";
import { AnyValidateFunction } from "ajv/dist/core";
import addFormats from "ajv-formats";
-const SchemaPath = path.join(__dirname, "..", "..", "assets", "schemas.json");
+const SchemaPath = path.join(__dirname, "..", "..", "..", "assets", "schemas.json");
const schemas = JSON.parse(fs.readFileSync(SchemaPath, { encoding: "utf8" }));
+
export const ajv = new Ajv({
allErrors: true,
parseDate: true,
@@ -30,6 +31,7 @@ export const ajv = new Ajv({
strict: true,
strictRequired: true
});
+
addFormats(ajv);
declare global {
diff --git a/api/src/util/index.ts b/api/src/util/index.ts
index 238787c9..ffbcf24e 100644
--- a/api/src/util/index.ts
+++ b/api/src/util/index.ts
@@ -1,8 +1,8 @@
-export * from "./Base64";
-export * from "./ipAddress";
-export * from "./Message";
-export * from "./passwordStrength";
-export * from "./RandomInviteID";
-export * from "./route";
-export * from "./String";
-export * from "./Voice";
+export * from "./utility/Base64";
+export * from "./utility/ipAddress";
+export * from "./handlers/Message";
+export * from "./utility/passwordStrength";
+export * from "./utility/RandomInviteID";
+export * from "./handlers/route";
+export * from "./utility/String";
+export * from "./handlers/Voice";
diff --git a/api/src/util/Base64.ts b/api/src/util/utility/Base64.ts
index 46cff77a..46cff77a 100644
--- a/api/src/util/Base64.ts
+++ b/api/src/util/utility/Base64.ts
diff --git a/api/src/util/RandomInviteID.ts b/api/src/util/utility/RandomInviteID.ts
index 7ea344e0..7ea344e0 100644
--- a/api/src/util/RandomInviteID.ts
+++ b/api/src/util/utility/RandomInviteID.ts
diff --git a/api/src/util/String.ts b/api/src/util/utility/String.ts
index 982b7e11..982b7e11 100644
--- a/api/src/util/String.ts
+++ b/api/src/util/utility/String.ts
diff --git a/api/src/util/ipAddress.ts b/api/src/util/utility/ipAddress.ts
index 13cc9603..13cc9603 100644
--- a/api/src/util/ipAddress.ts
+++ b/api/src/util/utility/ipAddress.ts
diff --git a/api/src/util/passwordStrength.ts b/api/src/util/utility/passwordStrength.ts
index 047df008..047df008 100644
--- a/api/src/util/passwordStrength.ts
+++ b/api/src/util/utility/passwordStrength.ts
|