mirror of
https://github.com/spacebarchat/server.git
synced 2026-07-26 03:41:16 +00:00
Add webhook message get/update/delete
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Spacebar.Models.Generic;
|
||||
using Spacebar.Sdk.Core;
|
||||
using Spacebar.Tests.Abstractions;
|
||||
@@ -19,6 +20,7 @@ public class WebhookTests(ITestOutputHelper testOutputHelper, TestFixture fixtur
|
||||
|
||||
private static SpacebarClientChannel? Channel = null!;
|
||||
private static Webhook? Webhook = null!;
|
||||
private static Message? WebhookMessage = null!;
|
||||
|
||||
public async ValueTask InitializeAsync() {
|
||||
Client = await _userAbstraction.GetSharedUser();
|
||||
@@ -48,6 +50,12 @@ public class WebhookTests(ITestOutputHelper testOutputHelper, TestFixture fixtur
|
||||
Assert.StringNotNullOrWhitespace(w.Result.Url);
|
||||
Webhook = w.Result;
|
||||
});
|
||||
|
||||
|
||||
if (WebhookMessage is null)
|
||||
WebhookMessage = await (await Assert.SuccessfullyHttpPostAsJsonAsync(Webhook.Url + "?wait=true", new JsonObject() {
|
||||
{ "content", "meow" }
|
||||
})).Content.ReadFromJsonAsync<Message>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -160,4 +168,23 @@ public class WebhookTests(ITestOutputHelper testOutputHelper, TestFixture fixtur
|
||||
{ "avatar_url", Client.ClientWellKnown.Api.BaseUrl + "/static/logo.png" }
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetWebhookMessageById() {
|
||||
var msg = await Assert.SuccessfullyHttpGetAsync(Webhook.Url + "/messages/" + WebhookMessage.Id);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(WebhookExecuteCombinations))]
|
||||
public async Task EditWebhookMessageByIdWithData(string content, string? username, string? avatarUrl, bool? tts, int? flags) {
|
||||
var payload = new JsonObject() {
|
||||
{ "content", content }
|
||||
};
|
||||
if (username != null) payload.Add("username", username);
|
||||
if (avatarUrl != null) payload.Add("avatar_url", avatarUrl);
|
||||
if (tts != null) payload.Add("tts", tts);
|
||||
if (flags != null) payload.Add("flags", flags);
|
||||
|
||||
await Assert.SuccessfullyHttpPostAsJsonAsync(Webhook.Url + "?wait=true", payload);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
|
||||
Copyright (C) 2026 Spacebar and Spacebar Contributors
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Request, Response, Router } from "express";
|
||||
import { HTTPError } from "lambert-server/HTTPError";
|
||||
import multer from "multer";
|
||||
import { handleMessage, postHandleMessage, route } from "@spacebar/api/util";
|
||||
import { Attachment, Channel, Message, Webhook } from "@spacebar/database";
|
||||
import {
|
||||
MessageCreateEvent,
|
||||
MessageDeleteEvent,
|
||||
MessageUpdateEvent,
|
||||
Snowflake,
|
||||
SpacebarApiErrors,
|
||||
emitEvent,
|
||||
getPermission,
|
||||
getRights,
|
||||
uploadFile,
|
||||
NewUrlUserSignatureData,
|
||||
DiscordApiErrors,
|
||||
} from "@spacebar/util";
|
||||
import { MessageCreateAttachment, MessageCreateCloudAttachment, MessageCreateSchema, MessageEditSchema, ChannelType } from "@spacebar/schemas";
|
||||
|
||||
const router = Router({ mergeParams: true });
|
||||
// TODO: message content/embed string length limit
|
||||
|
||||
async function assertValidWebhookAuth(webhookId: string, webhookToken: string, messageId: string) {
|
||||
const webhook = await Webhook.findOne({ where: { id: webhookId } });
|
||||
if (!webhook) throw DiscordApiErrors.UNKNOWN_WEBHOOK;
|
||||
if (webhook.token != webhookToken) throw DiscordApiErrors.INVALID_WEBHOOK_TOKEN_PROVIDED;
|
||||
|
||||
// TODO: fix error responses
|
||||
const message = await Message.findOne({ where: { id: messageId } });
|
||||
if (!message) throw new HTTPError(`No message found with ID ${messageId}`, 404);
|
||||
if (webhook.id != message?.webhook_id) throw new HTTPError(`Message does not belong to webhook ${message.webhook_id}`, 401);
|
||||
if (webhook.channel_id != message?.channel_id) throw new HTTPError(`Message does not belong to webhook channel ${message.channel_id}`, 401);
|
||||
}
|
||||
|
||||
const messageUpload = multer({
|
||||
limits: {
|
||||
fileSize: 1024 * 1024 * 100,
|
||||
fields: 10,
|
||||
files: 1,
|
||||
},
|
||||
storage: multer.memoryStorage(),
|
||||
}); // max upload 50 mb
|
||||
|
||||
router.patch(
|
||||
"/",
|
||||
route({
|
||||
requestBody: "MessageEditSchema",
|
||||
responses: {
|
||||
200: {
|
||||
body: "Message",
|
||||
},
|
||||
400: {
|
||||
body: "APIErrorResponse",
|
||||
},
|
||||
403: {},
|
||||
404: {},
|
||||
},
|
||||
}),
|
||||
async (req: Request, res: Response) => {
|
||||
const { webhook_id, webhook_token, message_id } = req.params as { [key: string]: string };
|
||||
const body = req.body as MessageEditSchema;
|
||||
|
||||
await assertValidWebhookAuth(webhook_id, webhook_token, message_id);
|
||||
|
||||
const message = await Message.findOneOrFail({
|
||||
where: { id: message_id, webhook_id: webhook_id },
|
||||
relations: { attachments: true },
|
||||
});
|
||||
|
||||
const new_message = await handleMessage({
|
||||
...message,
|
||||
// TODO: should message_reference be overridable?
|
||||
message_reference: message.message_reference,
|
||||
...body,
|
||||
author_id: message.author_id,
|
||||
webhook_id: message.webhook_id,
|
||||
channel_id: message.channel_id,
|
||||
id: message_id,
|
||||
edited_timestamp: new Date(),
|
||||
});
|
||||
|
||||
await new_message.save();
|
||||
await emitEvent({
|
||||
event: "MESSAGE_UPDATE",
|
||||
channel_id: message.channel_id,
|
||||
data: {
|
||||
...new_message.toJSON(),
|
||||
nonce: undefined,
|
||||
},
|
||||
} satisfies MessageUpdateEvent);
|
||||
|
||||
postHandleMessage(new_message).catch((e) => console.error("[Message] post-message handler failed", e));
|
||||
|
||||
// TODO: a DTO?
|
||||
return res.json({
|
||||
...new_message.toJSON(),
|
||||
id: new_message.id,
|
||||
type: new_message.type,
|
||||
channel_id: new_message.channel_id,
|
||||
member: new_message.member?.toPublicMember(),
|
||||
author: new_message.author?.toPublicUser(),
|
||||
attachments: new_message.attachments,
|
||||
embeds: new_message.embeds,
|
||||
mentions: new_message.embeds,
|
||||
mention_roles: new_message.mention_roles,
|
||||
mention_everyone: new_message.mention_everyone,
|
||||
pinned: new_message.pinned,
|
||||
timestamp: new_message.timestamp,
|
||||
edited_timestamp: new_message.edited_timestamp,
|
||||
|
||||
// these are not in the Discord.com response
|
||||
mention_channels: new_message.mention_channels,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/",
|
||||
route({
|
||||
responses: {
|
||||
200: {
|
||||
body: "Message",
|
||||
},
|
||||
400: {
|
||||
body: "APIErrorResponse",
|
||||
},
|
||||
403: {},
|
||||
404: {},
|
||||
},
|
||||
}),
|
||||
async (req: Request, res: Response) => {
|
||||
const { webhook_id, webhook_token, message_id } = req.params as { [key: string]: string };
|
||||
|
||||
await assertValidWebhookAuth(webhook_id, webhook_token, message_id);
|
||||
|
||||
const message = await Message.findOneOrFail({
|
||||
where: { id: message_id, webhook_id: webhook_id },
|
||||
relations: {
|
||||
attachments: true,
|
||||
author: true,
|
||||
},
|
||||
});
|
||||
|
||||
return res.json(message);
|
||||
},
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/",
|
||||
route({
|
||||
responses: {
|
||||
204: {},
|
||||
400: {
|
||||
body: "APIErrorResponse",
|
||||
},
|
||||
404: {},
|
||||
},
|
||||
}),
|
||||
async (req: Request, res: Response) => {
|
||||
const { webhook_id, webhook_token, message_id } = req.params as { [key: string]: string };
|
||||
|
||||
await assertValidWebhookAuth(webhook_id, webhook_token, message_id);
|
||||
|
||||
const message = await Message.findOneOrFail({
|
||||
where: { id: message_id, webhook_id: webhook_id },
|
||||
});
|
||||
|
||||
const channel = await Channel.findOneOrFail({
|
||||
where: { id: message.channel_id },
|
||||
});
|
||||
|
||||
if (channel.type === ChannelType.GUILD_PUBLIC_THREAD) {
|
||||
if (channel.message_count !== undefined) channel.message_count--;
|
||||
await channel.save();
|
||||
}
|
||||
|
||||
await Message.delete({ id: message_id, webhook_id: webhook_id });
|
||||
|
||||
await emitEvent({
|
||||
event: "MESSAGE_DELETE",
|
||||
channel_id: message.channel_id,
|
||||
data: {
|
||||
id: message_id,
|
||||
channel_id: message.channel_id!,
|
||||
guild_id: channel.guild_id,
|
||||
},
|
||||
} satisfies MessageDeleteEvent);
|
||||
|
||||
res.sendStatus(204);
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user