diff --git a/api/src/routes/channels/#channel_id/messages/#message_id/index.ts b/api/src/routes/channels/#channel_id/messages/#message_id/index.ts
index a27c71e1..6d2bf185 100644
--- a/api/src/routes/channels/#channel_id/messages/#message_id/index.ts
+++ b/api/src/routes/channels/#channel_id/messages/#message_id/index.ts
@@ -1,5 +1,18 @@
-import { Channel, emitEvent, getPermission, getRights, MessageDeleteEvent, Message, MessageUpdateEvent } from "@fosscord/util";
+import {
+ Attachment,
+ Channel,
+ Embed,
+ emitEvent,
+ getPermission,
+ getRights,
+ Message,
+ MessageCreateEvent,
+ MessageDeleteEvent,
+ MessageUpdateEvent,
+ uploadFile
+} from "@fosscord/util";
import { Router, Response, Request } from "express";
+import multer from "multer";
import { route } from "@fosscord/api";
import { handleMessage, postHandleMessage } from "@fosscord/api";
import { MessageCreateSchema } from "../index";
@@ -7,6 +20,15 @@ import { MessageCreateSchema } from "../index";
const router = Router();
// TODO: message content/embed string length limit
+const messageUpload = multer({
+ limits: {
+ fileSize: 1024 * 1024 * 100,
+ fields: 10,
+ files: 1
+ },
+ storage: multer.memoryStorage()
+}); // max upload 50 mb
+
router.patch("/", route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES", right: "SEND_MESSAGES" }), async (req: Request, res: Response) => {
const { message_id, channel_id } = req.params;
var body = req.body as MessageCreateSchema;
@@ -51,6 +73,68 @@ router.patch("/", route({ body: "MessageCreateSchema", permission: "SEND_MESSAGE
return res.json(message);
});
+
+// Backfill message with specific timestamp
+router.put(
+ "/",
+ messageUpload.single("file"),
+ async (req, res, next) => {
+ if (req.body.payload_json) {
+ req.body = JSON.parse(req.body.payload_json);
+ }
+
+ next();
+ },
+ route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES", right: "SEND_BACKDATED_EVENTS" }),
+ async (req: Request, res: Response) => {
+ const { channel_id, message_id } = req.params;
+ var body = req.body as MessageCreateSchema;
+ const attachments: Attachment[] = [];
+
+ if (req.file) {
+ try {
+ const file = await uploadFile(`/attachments/${req.params.channel_id}`, req.file);
+ attachments.push({ ...file, proxy_url: file.url });
+ } catch (error) {
+ return res.status(400).json(error);
+ }
+ }
+ const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients", "recipients.user"] });
+
+ // TODO: check the ID is not from the future, to prevent future-faking of channel histories
+
+ const embeds = body.embeds || [];
+ if (body.embed) embeds.push(body.embed);
+ let message = await handleMessage({
+ ...body,
+ type: 0,
+ pinned: false,
+ author_id: req.user_id,
+ id: message_id,
+ embeds,
+ channel_id,
+ attachments,
+ edited_timestamp: undefined,
+ timestamp: undefined, // FIXME: calculate timestamp from snowflake
+ });
+
+ channel.last_message_id = message.id;
+
+ //Fix for the client bug
+ delete message.member
+
+ await Promise.all([
+ message.save(),
+ emitEvent({ event: "MESSAGE_CREATE", channel_id: channel_id, data: message } as MessageCreateEvent),
+ channel.save()
+ ]);
+
+ postHandleMessage(message).catch((e) => { }); // no await as it shouldnt block the message send function and silently catch error
+
+ return res.json(message);
+ }
+);
+
router.get("/", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => {
const { message_id, channel_id } = req.params;
diff --git a/api/src/util/handlers/Message.ts b/api/src/util/handlers/Message.ts
index 5a5ac666..e9f0ac55 100644
--- a/api/src/util/handlers/Message.ts
+++ b/api/src/util/handlers/Message.ts
@@ -91,7 +91,8 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
if (opts.message_reference.channel_id !== opts.channel_id) throw new HTTPError("You can only reference messages from this channel");
}
}
- // Q: should be checked if the referenced message exists? ANSWER: NO
+ /** Q: should be checked if the referenced message exists? ANSWER: NO
+ otherwise backfilling won't work **/
// @ts-ignore
message.type = MessageType.REPLY;
}
diff --git a/util/src/entities/Channel.ts b/util/src/entities/Channel.ts
index c516e6a1..bf72994f 100644
--- a/util/src/entities/Channel.ts
+++ b/util/src/entities/Channel.ts
@@ -3,7 +3,7 @@ import { BaseClass } from "./BaseClass";
import { Guild } from "./Guild";
import { PublicUserProjection, User } from "./User";
import { HTTPError } from "lambert-server";
-import { containsAll, emitEvent, getPermission, Snowflake, trimSpecial, InvisibleCharacters } from "../util";
+import { containsAll, emitEvent, getPermission, Snowflake, trimSpecial, InvisibleCharacters, ChannelTypes } from "../util";
import { ChannelCreateEvent, ChannelRecipientRemoveEvent } from "../interfaces";
import { Recipient } from "./Recipient";
import { Message } from "./Message";
@@ -147,7 +147,7 @@ export class Channel extends BaseClass {
orphanedRowAction: "delete",
})
webhooks?: Webhook[];
-
+
// TODO: DM channel
static async createChannel(
channel: Partial<Channel>,
@@ -173,12 +173,20 @@ export class Channel extends BaseClass {
if (channel.name.includes(character))
throw new HTTPError("Channel name cannot include invalid characters", 403);
- if (channel.name.match(/\-\-+/g))
- throw new HTTPError("Channel name cannot include multiple adjacent dashes.", 403)
+ // Categories skip these checks on discord.com
+ if (channel.type !== ChannelType.GUILD_CATEGORY) {
+ if (channel.name.includes(" "))
+ throw new HTTPError("Channel name cannot include invalid characters", 403);
+
+ if (channel.name.match(/\-\-+/g))
+ throw new HTTPError("Channel name cannot include multiple adjacent dashes.", 403);
- if (channel.name.charAt(0) === "-" ||
- channel.name.charAt(channel.name.length - 1) === "-")
- throw new HTTPError("Channel name cannot start/end with dash.", 403)
+ if (channel.name.charAt(0) === "-" ||
+ channel.name.charAt(channel.name.length - 1) === "-")
+ throw new HTTPError("Channel name cannot start/end with dash.", 403);
+ }
+ else
+ channel.name = channel.name.trim(); //category names are trimmed client side on discord.com
}
if (!guild.features.includes("ALLOW_UNNAMED_CHANNELS")) {
@@ -297,7 +305,7 @@ export class Channel extends BaseClass {
await emitEvent({ event: "CHANNEL_CREATE", data: channel_dto, user_id: creator_user_id });
}
- if (recipients.length === 1) return channel_dto;
+ if (recipients.length === 1) return channel_dto;
else return channel_dto.excludedRecipients([creator_user_id]);
}
@@ -357,7 +365,6 @@ export class Channel extends BaseClass {
isWritable() {
const disallowedChannelTypes = [
ChannelType.GUILD_CATEGORY,
- ChannelType.GUILD_VOICE, // TODO: Remove this when clients can send messages to voice channels on discord.com
ChannelType.GUILD_STAGE_VOICE,
ChannelType.VOICELESS_WHITEBOARD,
];
diff --git a/util/src/util/InvisibleCharacters.ts b/util/src/util/InvisibleCharacters.ts
index 2b014e14..a48cfab0 100644
--- a/util/src/util/InvisibleCharacters.ts
+++ b/util/src/util/InvisibleCharacters.ts
@@ -1,7 +1,7 @@
// List from https://invisible-characters.com/
export const InvisibleCharacters = [
'\u{9}', //Tab
- '\u{20}', //Space
+ //'\u{20}', //Space //categories can have spaces in them
'\u{ad}', //Soft hyphen
'\u{34f}', //Combining grapheme joiner
'\u{61c}', //Arabic letter mark
|