diff --git a/assets/openapi.json b/assets/openapi.json
index 913920f5..25833b97 100644
--- a/assets/openapi.json
+++ b/assets/openapi.json
@@ -12327,7 +12327,7 @@
"mentions": {
"type": "array",
"items": {
- "type": "string"
+ "$ref": "#/components/schemas/PartialUser"
}
},
"mention_roles": {
@@ -12391,6 +12391,92 @@
"message"
]
},
+ "PartialUser": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "A Twitter-like snowflake, except the epoch is 2015-01-01T00:00:00.000Z\n```\nIf we have a snowflake '266241948824764416' we can represent it as binary:\n\n64 22 17 12 0\n 000000111011000111100001101001000101000000 00001 00000 000000000000\n number of ms since Discord epoch worker pid increment\n```",
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ },
+ "discriminator": {
+ "type": "string"
+ },
+ "global_name": {
+ "type": "string",
+ "nullable": true
+ },
+ "avatar": {
+ "type": "string",
+ "nullable": true
+ },
+ "avatar_decoration_data": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/AvatarDecorationData"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "collectibles": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/Collectibles"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "display_name_styles": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/DisplayNameStyle"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "primary_guild": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/PrimaryGuild"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "bot": {
+ "type": "boolean"
+ },
+ "system": {
+ "type": "boolean"
+ },
+ "banner": {
+ "type": "string",
+ "nullable": true
+ },
+ "accent_color": {
+ "type": "integer",
+ "nullable": true
+ },
+ "public_flags": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "avatar",
+ "discriminator",
+ "id",
+ "username"
+ ]
+ },
"MessageComponent": {
"type": "object",
"properties": {
@@ -12882,92 +12968,6 @@
"type"
]
},
- "PartialUser": {
- "type": "object",
- "properties": {
- "id": {
- "description": "A Twitter-like snowflake, except the epoch is 2015-01-01T00:00:00.000Z\n```\nIf we have a snowflake '266241948824764416' we can represent it as binary:\n\n64 22 17 12 0\n 000000111011000111100001101001000101000000 00001 00000 000000000000\n number of ms since Discord epoch worker pid increment\n```",
- "type": "string"
- },
- "username": {
- "type": "string"
- },
- "discriminator": {
- "type": "string"
- },
- "global_name": {
- "type": "string",
- "nullable": true
- },
- "avatar": {
- "type": "string",
- "nullable": true
- },
- "avatar_decoration_data": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/AvatarDecorationData"
- },
- {
- "type": "null"
- }
- ]
- },
- "collectibles": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/Collectibles"
- },
- {
- "type": "null"
- }
- ]
- },
- "display_name_styles": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/DisplayNameStyle"
- },
- {
- "type": "null"
- }
- ]
- },
- "primary_guild": {
- "anyOf": [
- {
- "$ref": "#/components/schemas/PrimaryGuild"
- },
- {
- "type": "null"
- }
- ]
- },
- "bot": {
- "type": "boolean"
- },
- "system": {
- "type": "boolean"
- },
- "banner": {
- "type": "string",
- "nullable": true
- },
- "accent_color": {
- "type": "integer",
- "nullable": true
- },
- "public_flags": {
- "type": "integer"
- }
- },
- "required": [
- "avatar",
- "discriminator",
- "id",
- "username"
- ]
- },
"HubGuild": {
"type": "object",
"properties": {
diff --git a/assets/schemas.json b/assets/schemas.json
index 3d9692a6..a7acf2b0 100644
--- a/assets/schemas.json
+++ b/assets/schemas.json
@@ -13081,7 +13081,7 @@
"mentions": {
"type": "array",
"items": {
- "type": "string"
+ "$ref": "#/definitions/PartialUser"
}
},
"mention_roles": {
@@ -13147,6 +13147,102 @@
],
"$schema": "http://json-schema.org/draft-07/schema#"
},
+ "PartialUser": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "A Twitter-like snowflake, except the epoch is 2015-01-01T00:00:00.000Z\n```\nIf we have a snowflake '266241948824764416' we can represent it as binary:\n\n64 22 17 12 0\n 000000111011000111100001101001000101000000 00001 00000 000000000000\n number of ms since Discord epoch worker pid increment\n```",
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ },
+ "discriminator": {
+ "type": "string"
+ },
+ "global_name": {
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "avatar": {
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "avatar_decoration_data": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/AvatarDecorationData"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "collectibles": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/Collectibles"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "display_name_styles": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/DisplayNameStyle"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "primary_guild": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/PrimaryGuild"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "bot": {
+ "type": "boolean"
+ },
+ "system": {
+ "type": "boolean"
+ },
+ "banner": {
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "accent_color": {
+ "type": [
+ "null",
+ "integer"
+ ]
+ },
+ "public_flags": {
+ "type": "integer"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "avatar",
+ "discriminator",
+ "id",
+ "username"
+ ],
+ "$schema": "http://json-schema.org/draft-07/schema#"
+ },
"MessageComponent": {
"type": "object",
"properties": {
@@ -13661,102 +13757,6 @@
],
"$schema": "http://json-schema.org/draft-07/schema#"
},
- "PartialUser": {
- "type": "object",
- "properties": {
- "id": {
- "description": "A Twitter-like snowflake, except the epoch is 2015-01-01T00:00:00.000Z\n```\nIf we have a snowflake '266241948824764416' we can represent it as binary:\n\n64 22 17 12 0\n 000000111011000111100001101001000101000000 00001 00000 000000000000\n number of ms since Discord epoch worker pid increment\n```",
- "type": "string"
- },
- "username": {
- "type": "string"
- },
- "discriminator": {
- "type": "string"
- },
- "global_name": {
- "type": [
- "null",
- "string"
- ]
- },
- "avatar": {
- "type": [
- "null",
- "string"
- ]
- },
- "avatar_decoration_data": {
- "anyOf": [
- {
- "$ref": "#/definitions/AvatarDecorationData"
- },
- {
- "type": "null"
- }
- ]
- },
- "collectibles": {
- "anyOf": [
- {
- "$ref": "#/definitions/Collectibles"
- },
- {
- "type": "null"
- }
- ]
- },
- "display_name_styles": {
- "anyOf": [
- {
- "$ref": "#/definitions/DisplayNameStyle"
- },
- {
- "type": "null"
- }
- ]
- },
- "primary_guild": {
- "anyOf": [
- {
- "$ref": "#/definitions/PrimaryGuild"
- },
- {
- "type": "null"
- }
- ]
- },
- "bot": {
- "type": "boolean"
- },
- "system": {
- "type": "boolean"
- },
- "banner": {
- "type": [
- "null",
- "string"
- ]
- },
- "accent_color": {
- "type": [
- "null",
- "integer"
- ]
- },
- "public_flags": {
- "type": "integer"
- }
- },
- "additionalProperties": false,
- "required": [
- "avatar",
- "discriminator",
- "id",
- "username"
- ],
- "$schema": "http://json-schema.org/draft-07/schema#"
- },
"HubGuild": {
"type": "object",
"properties": {
diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts
index 964f48f0..ea42283f 100644
--- a/src/api/util/handlers/Message.ts
+++ b/src/api/util/handlers/Message.ts
@@ -50,6 +50,7 @@ import {
MessageFlags,
FieldErrors,
Snowflake,
+ getDatabase,
} from "@spacebar/util";
import { HTTPError } from "lambert-server";
import { In, Or, Equal, IsNull } from "typeorm";
@@ -321,16 +322,6 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
}
const stickers = opts.sticker_ids ? await Sticker.find({ where: { id: In(opts.sticker_ids) } }) : undefined;
- // cloud attachments with indexes
- const cloudAttachments = opts.attachments?.reduce(
- (acc, att, index) => {
- if ("uploaded_filename" in att) {
- acc.push({ attachment: att, index });
- }
- return acc;
- },
- [] as { attachment: MessageCreateCloudAttachment; index: number }[],
- );
const message = Message.create({
...opts,
@@ -339,7 +330,7 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
sticker_items: stickers,
guild_id: channel.guild_id,
channel_id: opts.channel_id,
- attachments: opts.attachments || [],
+ attachments: [],
embeds: opts.embeds || [],
reactions: opts.reactions || [],
type: opts.type ?? 0,
@@ -347,10 +338,7 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
components: opts.components ?? undefined, // Fix Discord-Go?
});
message.channel = channel;
- message.attachments?.forEach((att) => {
- att.message_id = message.id;
- att.save();
- });
+ await processMessageOptionAttachments(opts, message);
if (opts.author_id) {
message.author = await User.findOneOrFail({
@@ -372,52 +360,7 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
await channel.save();
}
- if (cloudAttachments && cloudAttachments.length > 0) {
- console.log("[Message] Processing attachments for message", message.id, ":", message.attachments);
- handle?.(message.id, message.author as User, message.channel);
- const uploadedAttachments = await Promise.all(
- cloudAttachments.map(async (att) => {
- const cAtt = att.attachment;
- const attEnt = await CloudAttachment.findOneOrFail({
- where: {
- uploadFilename: cAtt.uploaded_filename,
- },
- });
-
- const cloneResponse = await fetch(`${conf.cdn.endpointPrivate}/attachments/${attEnt.uploadFilename}/clone_to_message/${message.id}`, {
- method: "POST",
- headers: {
- signature: conf.security.requestSignature || "",
- },
- });
-
- if (!cloneResponse.ok) {
- console.error(`[Message] Failed to clone attachment ${attEnt.userFilename} to message ${message.id}`);
- throw new HTTPError("Failed to process attachment: " + (await cloneResponse.text()), 500);
- }
-
- const cloneRespBody = (await cloneResponse.json()) as { success: boolean; new_path: string };
-
- const realAtt = Attachment.create({
- filename: attEnt.userFilename,
- size: attEnt.size,
- height: attEnt.height,
- width: attEnt.width,
- content_type: attEnt.contentType || attEnt.userOriginalContentType,
- channel_id: channel.id,
- message_id: message.id,
- });
- await realAtt.save();
- return { attachment: realAtt, index: att.index };
- }),
- );
- console.log("[Message] Processed attachments for message", message.id, ":", message.attachments);
-
- for (const att of uploadedAttachments) {
- message.attachments![att.index] = att.attachment;
- }
- }
- // else console.log("[Message] No cloud attachments to process for message", message.id, ":", message.attachments);
+ // TODO: Removed cloud attachment handling being inline - handle components!
if (message.content && message.content.length > conf.limits.message.maxCharacters) {
throw new HTTPError("Content length over max character limit");
@@ -562,6 +505,7 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
if (content) {
// TODO: explicit-only mentions
+ // TODO: make mentions lazy
message.content = content.trim();
content = content.replace(/ *`[^)]*` */g, ""); // remove codeblocks
// root@Rory - 20/02/2023 - This breaks channel mentions in test client. We're not sure this was used in older clients.
@@ -596,6 +540,10 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
id: message.message_reference.message_id,
channel_id: message.channel_id,
},
+ relations: {
+ mentions: true,
+ mention_roles: true,
+ },
});
if (referencedMessage && referencedMessage.author_id !== message.author_id) {
message.mentions.push(
@@ -609,27 +557,8 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
message.type = MessageType.DEFAULT;
if (message.referenced_message) {
- const mention_roles: string[] = [];
- const mentions: string[] = [];
-
// TODO: mention_roles and mentions arrays - not needed it seems, but discord still returns that
-
- message.message_snapshots = [
- {
- message: {
- attachments: message.referenced_message.attachments,
- components: message.referenced_message.components,
- content: message.referenced_message.content!,
- edited_timestamp: message.referenced_message.edited_timestamp,
- embeds: message.referenced_message.embeds,
- flags: message.referenced_message.flags,
- mention_roles,
- mentions,
- timestamp: message.referenced_message.timestamp,
- type: message.referenced_message.type,
- },
- },
- ];
+ message.message_snapshots = [message.referenced_message.toSnapshot()];
}
}
}
@@ -792,9 +721,12 @@ export async function sendMessage(opts: MessageOptions) {
const message = await handleMessage({ ...opts, timestamp: new Date() });
const ephemeral = (message.flags & Number(MessageFlags.FLAGS.EPHEMERAL)) !== 0;
+ await getDatabase()?.transaction(async (entityManager) => {
+ await entityManager.save(message);
+ await entityManager.save(message.channel);
+ if (message.attachments && message.attachments.length > 0) await entityManager.save(message.attachments);
+ });
await Promise.all([
- message.insert(),
- message.channel.save(),
emitEvent({
event: "MESSAGE_CREATE",
...(ephemeral ? { user_id: message.interaction_metadata?.user_id } : { channel_id: message.channel_id }),
@@ -808,6 +740,7 @@ export async function sendMessage(opts: MessageOptions) {
return message;
}
+type MessageOptionAttachment = MessageCreateAttachment | MessageCreateCloudAttachment | Attachment;
interface MessageOptions extends MessageCreateSchema {
id?: string;
type?: MessageType;
@@ -824,3 +757,63 @@ interface MessageOptions extends MessageCreateSchema {
username?: string;
avatar_url?: string;
}
+
+// Makes for concise code, inspired by Nix' lib.trace
+function logPassthru<T>(obj: T, ...data: unknown[]) {
+ console.log(...data);
+ return obj;
+}
+export async function processMessageOptionAttachments(source: MessageOptions, destination: Message) {
+ if (!source.attachments || source.attachments.length == 0) return;
+ const logp = `[Message/${destination.id}/Attachments]`;
+ console.log("[Message] Processing attachments for message", source.id, "->", source.attachments);
+ const tasks = source.attachments?.map(async (src): Promise<Attachment> => {
+ if (src instanceof Attachment) return logPassthru(src, logp, `Got Attachment instance`);
+ if (isCloudAttachment(src))
+ return logPassthru(await convertCloudAttachmentToAttachment(src, destination.channel_id!, destination.id), logp, "Got MessageCreateCloudAttachment contents");
+ throw new Error(logp + " Unhandled attachment: " + JSON.stringify(src));
+ });
+
+ destination.attachments = [];
+ for (const task of tasks) {
+ destination.attachments.push(await task);
+ }
+}
+
+export function isCloudAttachment(attachment: MessageOptionAttachment) {
+ return "uploaded_filename" in attachment;
+}
+
+export async function convertCloudAttachmentToAttachment(cAtt: MessageCreateCloudAttachment, destinationChannelId: string, destinationMessageId: string) {
+ const attEnt = await CloudAttachment.findOneOrFail({
+ where: {
+ uploadFilename: cAtt.uploaded_filename,
+ },
+ });
+
+ const cloneResponse = await fetch(`${Config.get().cdn.endpointPrivate}/attachments/${attEnt.uploadFilename}/clone_to_message/${destinationMessageId}`, {
+ method: "POST",
+ headers: {
+ signature: Config.get().security.requestSignature || "",
+ },
+ });
+
+ if (!cloneResponse.ok) {
+ console.error(`[Message] Failed to clone attachment ${attEnt.userFilename} to message ${destinationMessageId}`);
+ throw new HTTPError("Failed to process attachment: " + (await cloneResponse.text()), 500);
+ }
+
+ const cloneRespBody = (await cloneResponse.json()) as { success: boolean; new_path: string };
+
+ const realAtt = Attachment.create({
+ filename: attEnt.userFilename,
+ size: attEnt.size,
+ height: attEnt.height,
+ width: attEnt.width,
+ content_type: attEnt.contentType || attEnt.userOriginalContentType,
+ channel_id: destinationChannelId,
+ message_id: destinationMessageId,
+ });
+ console.log("[Message] Converted cloud attachment to", realAtt);
+ return realAtt;
+}
diff --git a/src/schemas/api/messages/Message.ts b/src/schemas/api/messages/Message.ts
index 515514ee..ee2b9a8b 100644
--- a/src/schemas/api/messages/Message.ts
+++ b/src/schemas/api/messages/Message.ts
@@ -149,7 +149,7 @@ export interface MessageSnapshot {
content: string;
timestamp: Date;
edited_timestamp?: Date | null;
- mentions: Snowflake[];
+ mentions: PartialUser[];
mention_roles: Snowflake[];
attachments?: Attachment[];
embeds: Embed[];
diff --git a/src/util/entities/Message.ts b/src/util/entities/Message.ts
index 7c5d6b6d..217f1da0 100644
--- a/src/util/entities/Message.ts
+++ b/src/util/entities/Message.ts
@@ -328,6 +328,23 @@ export class Message extends BaseClass {
};
}
+ toSnapshot(): MessageSnapshot {
+ return {
+ message: {
+ attachments: this.attachments,
+ components: this.components,
+ content: this.content!,
+ edited_timestamp: this.edited_timestamp,
+ embeds: this.embeds,
+ flags: this.flags,
+ mention_roles: this.mention_roles?.map((x) => x.id),
+ mentions: this.mentions.map((x) => x.toPublicUser() as unknown as PartialUser), // TODO: write a proper method for this
+ timestamp: this.timestamp,
+ type: this.type,
+ },
+ };
+ }
+
withSignedAttachments(data: NewUrlUserSignatureData) {
function signMedia(media: UnfurledMediaItem) {
Object.assign(media, Attachment.prototype.signUrls.call(media, data));
|