Merge branch 'master' into fix/reaction-removing

This commit is contained in:
Cyber 2024-08-10 20:22:59 +02:00 committed by GitHub
commit d9b4b569bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 9143 additions and 1082 deletions

View File

@ -2413,7 +2413,7 @@
"attachments": { "attachments": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/components/schemas/Attachment_1" "$ref": "#/components/schemas/Attachment"
} }
}, },
"embeds": { "embeds": {
@ -2510,6 +2510,12 @@
"poll": { "poll": {
"$ref": "#/components/schemas/Poll" "$ref": "#/components/schemas/Poll"
}, },
"username": {
"type": "string"
},
"avatar": {
"type": "string"
},
"id": { "id": {
"type": "string" "type": "string"
} }
@ -2817,6 +2823,15 @@
"source_guild": { "source_guild": {
"$ref": "#/components/schemas/Guild" "$ref": "#/components/schemas/Guild"
}, },
"source_channel_id": {
"type": "string"
},
"source_channel": {
"$ref": "#/components/schemas/Channel"
},
"url": {
"type": "string"
},
"id": { "id": {
"type": "string" "type": "string"
} }
@ -2824,14 +2839,15 @@
"required": [ "required": [
"application", "application",
"application_id", "application_id",
"avatar",
"channel", "channel",
"channel_id", "channel_id",
"guild",
"guild_id",
"id", "id",
"source_guild", "name",
"source_guild_id", "source_channel",
"source_channel_id",
"type", "type",
"url",
"user", "user",
"user_id" "user_id"
] ]
@ -3167,7 +3183,7 @@
], ],
"type": "number" "type": "number"
}, },
"Attachment_1": { "Attachment": {
"type": "object", "type": "object",
"properties": { "properties": {
"filename": { "filename": {
@ -3686,7 +3702,7 @@
"attachments": { "attachments": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/components/schemas/Attachment_1" "$ref": "#/components/schemas/Attachment"
} }
}, },
"embeds": { "embeds": {
@ -6956,6 +6972,107 @@
"name" "name"
] ]
}, },
"WebhookExecuteSchema": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"username": {
"type": "string"
},
"avatar_url": {
"type": "string"
},
"tts": {
"type": "boolean"
},
"embeds": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Embed"
}
},
"allowed_mentions": {
"type": "object",
"properties": {
"parse": {
"type": "array",
"items": {
"type": "string"
}
},
"roles": {
"type": "array",
"items": {
"type": "string"
}
},
"users": {
"type": "array",
"items": {
"type": "string"
}
},
"replied_user": {
"type": "boolean"
}
},
"additionalProperties": false
},
"components": {
"type": "array",
"items": {}
},
"file": {
"type": "object",
"properties": {
"filename": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"filename"
]
},
"payload_json": {
"type": "string"
},
"attachments": {
"description": "TODO: we should create an interface for attachments\nTODO: OpenWAAO<-->attachment-style metadata conversion",
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"filename": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"filename",
"id"
]
}
},
"flags": {
"type": "integer"
},
"thread_name": {
"type": "string"
},
"applied_tags": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"WidgetModifySchema": { "WidgetModifySchema": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -8864,9 +8981,175 @@
}, },
{ {
"name": "voice" "name": "voice"
},
{
"name": "webhooks"
} }
], ],
"paths": { "paths": {
"/webhooks/{webhook_id}/": {
"get": {
"security": [
{
"bearer": []
}
],
"description": "Returns a webhook object for the given id. Requires the MANAGE_WEBHOOKS permission or to be the owner of the webhook.",
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIWebhook"
}
}
}
},
"404": {
"description": "No description available"
}
},
"parameters": [
{
"name": "webhook_id",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "webhook_id"
}
],
"tags": [
"webhooks"
]
}
},
"/webhooks/{webhook_id}/{token}/": {
"get": {
"security": [
{
"bearer": []
}
],
"description": "Returns a webhook object for the given id and token.",
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIWebhook"
}
}
}
},
"404": {
"description": "No description available"
}
},
"parameters": [
{
"name": "webhook_id",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "webhook_id"
},
{
"name": "token",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "token"
}
],
"tags": [
"webhooks"
]
},
"post": {
"security": [
{
"bearer": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WebhookExecuteSchema"
}
}
}
},
"responses": {
"204": {
"description": "No description available"
},
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
},
"404": {
"description": "No description available"
}
},
"parameters": [
{
"name": "webhook_id",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "webhook_id"
},
{
"name": "token",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "token"
},
{
"name": "wait",
"in": "query",
"required": false,
"schema": {
"type": "boolean"
},
"description": "waits for server confirmation of message send before response, and returns the created message body"
},
{
"name": "thread_id",
"in": "query",
"required": false,
"schema": {
"type": "string"
},
"description": "Send a message to the specified thread within a webhook's channel."
}
],
"tags": [
"webhooks"
]
}
},
"/voice/regions/": { "/voice/regions/": {
"get": { "get": {
"security": [ "security": [
@ -11739,14 +12022,23 @@
}, },
"/guilds/{guild_id}/webhooks/": { "/guilds/{guild_id}/webhooks/": {
"get": { "get": {
"x-permission-required": "MANAGE_WEBHOOKS",
"security": [ "security": [
{ {
"bearer": [] "bearer": []
} }
], ],
"description": "Returns a list of guild webhook objects. Requires the MANAGE_WEBHOOKS permission.",
"responses": { "responses": {
"default": { "200": {
"description": "No description available" "description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIWebhookArray"
}
}
}
} }
}, },
"parameters": [ "parameters": [
@ -15249,11 +15541,13 @@
}, },
"/channels/{channel_id}/webhooks/": { "/channels/{channel_id}/webhooks/": {
"get": { "get": {
"x-permission-required": "MANAGE_WEBHOOKS",
"security": [ "security": [
{ {
"bearer": [] "bearer": []
} }
], ],
"description": "Returns a list of channel webhook objects. Requires the MANAGE_WEBHOOKS permission.",
"responses": { "responses": {
"200": { "200": {
"description": "", "description": "",

File diff suppressed because it is too large Load Diff

View File

@ -16,8 +16,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { checkToken, Rights } from "@spacebar/util";
import * as Sentry from "@sentry/node"; import * as Sentry from "@sentry/node";
import { checkToken, Rights } from "@spacebar/util";
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
@ -32,7 +32,7 @@ export const NO_AUTHORIZATION_ROUTES = [
"/auth/forgot", "/auth/forgot",
"/auth/reset", "/auth/reset",
// Routes with a seperate auth system // Routes with a seperate auth system
"/webhooks/", /\/webhooks\/\d+\/\w+\/?/, // no token requires auth
// Public information endpoints // Public information endpoints
"/ping", "/ping",
"/gateway", "/gateway",

View File

@ -26,8 +26,8 @@ import {
WebhookCreateSchema, WebhookCreateSchema,
WebhookType, WebhookType,
handleFile, handleFile,
trimSpecial,
isTextChannel, isTextChannel,
trimSpecial,
} from "@spacebar/util"; } from "@spacebar/util";
import crypto from "crypto"; import crypto from "crypto";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
@ -35,10 +35,12 @@ import { HTTPError } from "lambert-server";
const router: Router = Router(); const router: Router = Router();
//TODO: implement webhooks
router.get( router.get(
"/", "/",
route({ route({
description:
"Returns a list of channel webhook objects. Requires the MANAGE_WEBHOOKS permission.",
permission: "MANAGE_WEBHOOKS",
responses: { responses: {
200: { 200: {
body: "APIWebhookArray", body: "APIWebhookArray",
@ -46,7 +48,32 @@ router.get(
}, },
}), }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
res.json([]); const { channel_id } = req.params;
const webhooks = await Webhook.find({
where: { channel_id },
relations: [
"user",
"channel",
"source_channel",
"guild",
"source_guild",
"application",
],
});
const instanceUrl =
Config.get().api.endpointPublic || "http://localhost:3001";
return res.json(
webhooks.map((webhook) => ({
...webhook,
url:
instanceUrl +
"/webhooks/" +
webhook.id +
"/" +
webhook.token,
})),
);
}, },
); );
@ -89,15 +116,15 @@ router.post(
if (avatar) avatar = await handleFile(`/avatars/${channel_id}`, avatar); if (avatar) avatar = await handleFile(`/avatars/${channel_id}`, avatar);
const hook = Webhook.create({ const hook = await Webhook.create({
type: WebhookType.Incoming, type: WebhookType.Incoming,
name, name,
avatar, avatar,
guild_id: channel.guild_id, guild_id: channel.guild_id,
channel_id: channel.id, channel_id: channel.id,
user_id: req.user_id, user_id: req.user_id,
token: crypto.randomBytes(24).toString("base64"), token: crypto.randomBytes(24).toString("base64url"),
}); }).save();
const user = await User.getPublicUser(req.user_id); const user = await User.getPublicUser(req.user_id);

View File

@ -16,12 +16,51 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Router, Response, Request } from "express";
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { Config, Webhook } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router(); const router = Router();
//TODO: implement webhooks router.get(
router.get("/", route({}), async (req: Request, res: Response) => { "/",
res.json([]); route({
description:
"Returns a list of guild webhook objects. Requires the MANAGE_WEBHOOKS permission.",
permission: "MANAGE_WEBHOOKS",
responses: {
200: {
body: "APIWebhookArray",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const webhooks = await Webhook.find({
where: { guild_id },
relations: [
"user",
"channel",
"source_channel",
"guild",
"source_guild",
"application",
],
}); });
const instanceUrl =
Config.get().api.endpointPublic || "http://localhost:3001";
return res.json(
webhooks.map((webhook) => ({
...webhook,
url:
instanceUrl +
"/webhooks/" +
webhook.id +
"/" +
webhook.token,
})),
);
},
);
export default router; export default router;

View File

@ -0,0 +1,251 @@
import { handleMessage, postHandleMessage, route } from "@spacebar/api";
import {
Attachment,
Config,
DiscordApiErrors,
FieldErrors,
Message,
MessageCreateEvent,
Webhook,
WebhookExecuteSchema,
emitEvent,
uploadFile,
} from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
import multer from "multer";
import { MoreThan } from "typeorm";
const router = Router();
router.get(
"/",
route({
description: "Returns a webhook object for the given id and token.",
responses: {
200: {
body: "APIWebhook",
},
404: {},
},
}),
async (req: Request, res: Response) => {
const { webhook_id, token } = req.params;
const webhook = await Webhook.findOne({
where: {
id: webhook_id,
},
relations: [
"user",
"channel",
"source_channel",
"guild",
"source_guild",
"application",
],
});
if (!webhook) {
throw DiscordApiErrors.UNKNOWN_WEBHOOK;
}
if (webhook.token !== token) {
throw DiscordApiErrors.INVALID_WEBHOOK_TOKEN_PROVIDED;
}
const instanceUrl =
Config.get().api.endpointPublic || "http://localhost:3001";
return res.json({
...webhook,
url: instanceUrl + "/webhooks/" + webhook.id + "/" + webhook.token,
});
},
);
// TODO: config max upload size
const messageUpload = multer({
limits: {
fileSize: Config.get().limits.message.maxAttachmentSize,
fields: 10,
// files: 1
},
storage: multer.memoryStorage(),
}); // max upload 50 mb
// https://discord.com/developers/docs/resources/webhook#execute-webhook
// TODO: GitHub/Slack compatible hooks
router.post(
"/",
messageUpload.any(),
(req, res, next) => {
if (req.body.payload_json) {
req.body = JSON.parse(req.body.payload_json);
}
next();
},
route({
requestBody: "WebhookExecuteSchema",
query: {
wait: {
type: "boolean",
required: false,
description:
"waits for server confirmation of message send before response, and returns the created message body",
},
thread_id: {
type: "string",
required: false,
description:
"Send a message to the specified thread within a webhook's channel.",
},
},
responses: {
204: {},
400: {
body: "APIErrorResponse",
},
404: {},
},
}),
async (req: Request, res: Response) => {
const { wait } = req.query;
if (!wait) return res.status(204).send();
const { webhook_id, token } = req.params;
const body = req.body as WebhookExecuteSchema;
const attachments: Attachment[] = [];
// ensure one of content, embeds, components, or file is present
if (
!body.content &&
!body.embeds &&
!body.components &&
!body.file &&
!body.attachments
) {
throw DiscordApiErrors.CANNOT_SEND_EMPTY_MESSAGE;
}
// block username from containing certain words
// TODO: configurable additions
const blockedContains = ["discord", "clyde", "spacebar"];
for (const word of blockedContains) {
if (body.username?.toLowerCase().includes(word)) {
return res.status(400).json({
username: [`Username cannot contain "${word}"`],
});
}
}
// block username from being certain words
// TODO: configurable additions
const blockedEquals = ["everyone", "here"];
for (const word of blockedEquals) {
if (body.username?.toLowerCase() === word) {
return res.status(400).json({
username: [`Username cannot be "${word}"`],
});
}
}
const webhook = await Webhook.findOne({
where: {
id: webhook_id,
},
relations: ["channel", "guild", "application"],
});
if (!webhook) {
throw DiscordApiErrors.UNKNOWN_WEBHOOK;
}
if (!webhook.channel.isWritable()) {
throw new HTTPError(
`Cannot send messages to channel of type ${webhook.channel.type}`,
400,
);
}
if (webhook.token !== token) {
throw DiscordApiErrors.INVALID_WEBHOOK_TOKEN_PROVIDED;
}
// TODO: creating messages by users checks if the user can bypass rate limits, we cant do that on webhooks, but maybe we could check the application if there is one?
const limits = Config.get().limits;
if (limits.absoluteRate.register.enabled) {
const count = await Message.count({
where: {
channel_id: webhook.channel_id,
timestamp: MoreThan(
new Date(
Date.now() - limits.absoluteRate.sendMessage.window,
),
),
},
});
if (count >= limits.absoluteRate.sendMessage.limit)
throw FieldErrors({
channel_id: {
code: "TOO_MANY_MESSAGES",
message: req.t("common:toomany.MESSAGE"),
},
});
}
const files = (req.files as Express.Multer.File[]) ?? [];
for (const currFile of files) {
try {
const file = await uploadFile(
`/attachments/${webhook.channel.id}`,
currFile,
);
attachments.push(
Attachment.create({ ...file, proxy_url: file.url }),
);
} catch (error) {
return res.status(400).json({ message: error?.toString() });
}
}
// TODO: set username and avatar based on body
const embeds = body.embeds || [];
const message = await handleMessage({
...body,
type: 0,
pinned: false,
webhook_id: webhook.id,
application_id: webhook.application?.id,
embeds,
// TODO: Support thread_id/thread_name once threads are implemented
channel_id: webhook.channel_id,
attachments,
timestamp: new Date(),
});
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore dont care2
message.edited_timestamp = null;
webhook.channel.last_message_id = message.id;
await Promise.all([
message.save(),
emitEvent({
event: "MESSAGE_CREATE",
channel_id: webhook.channel_id,
data: message,
} as MessageCreateEvent),
]);
// no await as it shouldnt block the message send function and silently catch error
postHandleMessage(message).catch((e) =>
console.error("[Message] post-message handler failed", e),
);
return res.json(message);
},
);
export default router;

View File

@ -0,0 +1,57 @@
import { route } from "@spacebar/api";
import {
Config,
DiscordApiErrors,
getPermission,
Webhook,
} from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router();
router.get(
"/",
route({
description:
"Returns a webhook object for the given id. Requires the MANAGE_WEBHOOKS permission or to be the owner of the webhook.",
responses: {
200: {
body: "APIWebhook",
},
404: {},
},
}),
async (req: Request, res: Response) => {
const { webhook_id } = req.params;
const webhook = await Webhook.findOneOrFail({
where: { id: webhook_id },
relations: [
"user",
"channel",
"source_channel",
"guild",
"source_guild",
"application",
],
});
if (webhook.guild_id) {
const permission = await getPermission(
req.user_id,
webhook.guild_id,
);
if (!permission.has("MANAGE_WEBHOOKS"))
throw DiscordApiErrors.UNKNOWN_WEBHOOK;
} else if (webhook.user_id != req.user_id)
throw DiscordApiErrors.UNKNOWN_WEBHOOK;
const instanceUrl =
Config.get().api.endpointPublic || "http://localhost:3001";
return res.json({
...webhook,
url: instanceUrl + "/webhooks/" + webhook.id + "/" + webhook.token,
});
},
);
export default router;

View File

@ -43,9 +43,12 @@ import {
//CHANNEL_MENTION, //CHANNEL_MENTION,
USER_MENTION, USER_MENTION,
Webhook, Webhook,
handleFile,
Permissions,
} from "@spacebar/util"; } from "@spacebar/util";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { In } from "typeorm"; import { In } from "typeorm";
import fetch from "node-fetch";
const allow_empty = false; const allow_empty = false;
// TODO: check webhook, application, system author, stickers // TODO: check webhook, application, system author, stickers
// TODO: embed gifs/videos/images // TODO: embed gifs/videos/images
@ -93,13 +96,63 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
where: { id: opts.application_id }, where: { id: opts.application_id },
}); });
} }
let permission: undefined | Permissions;
if (opts.webhook_id) { if (opts.webhook_id) {
message.webhook = await Webhook.findOneOrFail({ message.webhook = await Webhook.findOneOrFail({
where: { id: opts.webhook_id }, where: { id: opts.webhook_id },
}); });
message.author =
(await User.findOne({
where: { id: opts.webhook_id },
})) || undefined;
if (!message.author) {
message.author = User.create({
id: opts.webhook_id,
username: message.webhook.name,
discriminator: "0000",
avatar: message.webhook.avatar,
public_flags: 0,
premium: false,
premium_type: 0,
bot: true,
created_at: new Date(),
verified: true,
rights: "0",
data: {
valid_tokens_since: new Date(),
},
});
await message.author.save();
} }
const permission = await getPermission( if (opts.username) {
message.username = opts.username;
message.author.username = message.username;
}
if (opts.avatar_url) {
const avatarData = await fetch(opts.avatar_url);
const base64 = await avatarData
.buffer()
.then((x) => x.toString("base64"));
const dataUri =
"data:" +
avatarData.headers.get("content-type") +
";base64," +
base64;
message.avatar = await handleFile(
`/avatars/${opts.webhook_id}`,
dataUri as string,
);
message.author.avatar = message.avatar;
}
} else {
permission = await getPermission(
opts.author_id, opts.author_id,
channel.guild_id, channel.guild_id,
opts.channel_id, opts.channel_id,
@ -117,7 +170,6 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
const guild = await Guild.findOneOrFail({ const guild = await Guild.findOneOrFail({
where: { id: channel.guild_id }, where: { id: channel.guild_id },
}); });
if (!opts.message_reference.guild_id) if (!opts.message_reference.guild_id)
opts.message_reference.guild_id = channel.guild_id; opts.message_reference.guild_id = channel.guild_id;
if (!opts.message_reference.channel_id) if (!opts.message_reference.channel_id)
@ -140,6 +192,7 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
otherwise backfilling won't work **/ otherwise backfilling won't work **/
message.type = MessageType.REPLY; message.type = MessageType.REPLY;
} }
}
// TODO: stickers/activity // TODO: stickers/activity
if ( if (
@ -183,14 +236,18 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
const role = await Role.findOneOrFail({ const role = await Role.findOneOrFail({
where: { id: mention, guild_id: channel.guild_id }, where: { id: mention, guild_id: channel.guild_id },
}); });
if (role.mentionable || permission.has("MANAGE_ROLES")) { if (
role.mentionable ||
opts.webhook_id ||
permission?.has("MANAGE_ROLES")
) {
mention_role_ids.push(mention); mention_role_ids.push(mention);
} }
}, },
), ),
); );
if (permission.has("MENTION_EVERYONE")) { if (opts.webhook_id || permission?.has("MENTION_EVERYONE")) {
mention_everyone = mention_everyone =
!!content.match(EVERYONE_MENTION) || !!content.match(EVERYONE_MENTION) ||
!!content.match(HERE_MENTION); !!content.match(HERE_MENTION);
@ -316,4 +373,6 @@ interface MessageOptions extends MessageCreateSchema {
attachments?: Attachment[]; attachments?: Attachment[];
edited_timestamp?: Date; edited_timestamp?: Date;
timestamp?: Date; timestamp?: Date;
username?: string;
avatar_url?: string;
} }

View File

@ -221,6 +221,12 @@ export class Message extends BaseClass {
@Column({ type: "simple-json", nullable: true }) @Column({ type: "simple-json", nullable: true })
poll?: Poll; poll?: Poll;
@Column({ nullable: true })
username?: string;
@Column({ nullable: true })
avatar?: string;
toJSON(): Message { toJSON(): Message {
return { return {
...this, ...this,
@ -237,7 +243,12 @@ export class Message extends BaseClass {
reactions: this.reactions ?? undefined, reactions: this.reactions ?? undefined,
sticker_items: this.sticker_items ?? undefined, sticker_items: this.sticker_items ?? undefined,
message_reference: this.message_reference ?? undefined, message_reference: this.message_reference ?? undefined,
author: this.author?.toPublicUser() ?? undefined, author: {
...(this.author?.toPublicUser() ?? undefined),
// Webhooks
username: this.username ?? this.author?.username,
avatar: this.avatar ?? this.author?.avatar,
},
activity: this.activity ?? undefined, activity: this.activity ?? undefined,
application: this.application ?? undefined, application: this.application ?? undefined,
components: this.components ?? undefined, components: this.components ?? undefined,

View File

@ -35,23 +35,23 @@ export class Webhook extends BaseClass {
type: WebhookType; type: WebhookType;
@Column({ nullable: true }) @Column({ nullable: true })
name?: string; name: string;
@Column({ nullable: true }) @Column({ nullable: true })
avatar?: string; avatar: string;
@Column({ nullable: true }) @Column({ nullable: true })
token?: string; token?: string;
@Column({ nullable: true }) @Column({ nullable: true })
@RelationId((webhook: Webhook) => webhook.guild) @RelationId((webhook: Webhook) => webhook.guild)
guild_id: string; guild_id?: string;
@JoinColumn({ name: "guild_id" }) @JoinColumn({ name: "guild_id" })
@ManyToOne(() => Guild, { @ManyToOne(() => Guild, {
onDelete: "CASCADE", onDelete: "CASCADE",
}) })
guild: Guild; guild?: Guild;
@Column({ nullable: true }) @Column({ nullable: true })
@RelationId((webhook: Webhook) => webhook.channel) @RelationId((webhook: Webhook) => webhook.channel)
@ -85,11 +85,23 @@ export class Webhook extends BaseClass {
@Column({ nullable: true }) @Column({ nullable: true })
@RelationId((webhook: Webhook) => webhook.guild) @RelationId((webhook: Webhook) => webhook.guild)
source_guild_id: string; source_guild_id?: string;
@JoinColumn({ name: "source_guild_id" }) @JoinColumn({ name: "source_guild_id" })
@ManyToOne(() => Guild, { @ManyToOne(() => Guild, {
onDelete: "CASCADE", onDelete: "CASCADE",
}) })
source_guild: Guild; source_guild?: Guild;
@Column({ nullable: true })
@RelationId((webhook: Webhook) => webhook.channel)
source_channel_id: string;
@JoinColumn({ name: "source_channel_id" })
@ManyToOne(() => Channel, {
onDelete: "CASCADE",
})
source_channel: Channel;
url: string;
} }

View File

@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class WebhookMessageProperties1721298824927
implements MigrationInterface
{
name = "WebhookMessageProperties1721298824927";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
"ALTER TABLE `messages` ADD `username` text NULL",
);
await queryRunner.query(
"ALTER TABLE `messages` ADD `avatar` text NULL",
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
"ALTER TABLE `messages` DROP COLUMN `username`",
);
await queryRunner.query("ALTER TABLE `messages` DROP COLUMN `avatar`");
}
}

View File

@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class WebhookMessageProperties1721298824927
implements MigrationInterface
{
name = "WebhookMessageProperties1721298824927";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
"ALTER TABLE `messages` ADD `username` text NULL",
);
await queryRunner.query(
"ALTER TABLE `messages` ADD `avatar` text NULL",
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
"ALTER TABLE `messages` DROP COLUMN `username`",
);
await queryRunner.query("ALTER TABLE `messages` DROP COLUMN `avatar`");
}
}

View File

@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class WebhookMessageProperties1721298824927
implements MigrationInterface
{
name = "WebhookMessageProperties1721298824927";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query("ALTER TABLE messages ADD username text NULL");
await queryRunner.query("ALTER TABLE messages ADD avatar text NULL");
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query("ALTER TABLE messages DROP COLUMN username");
await queryRunner.query("ALTER TABLE messages DROP COLUMN avatar");
}
}

View File

@ -23,7 +23,7 @@ import {
PollMedia, PollMedia,
} from "@spacebar/util"; } from "@spacebar/util";
type Attachment = { export type MessageCreateAttachment = {
id: string; id: string;
filename: string; filename: string;
}; };
@ -57,7 +57,7 @@ export interface MessageCreateSchema {
TODO: we should create an interface for attachments TODO: we should create an interface for attachments
TODO: OpenWAAO<-->attachment-style metadata conversion TODO: OpenWAAO<-->attachment-style metadata conversion
**/ **/
attachments?: Attachment[]; attachments?: MessageCreateAttachment[];
sticker_ids?: string[]; sticker_ids?: string[];
components?: ActionRowComponent[]; components?: ActionRowComponent[];
// TODO: Fix TypeScript errors in src\api\util\handlers\Message.ts once this is enabled // TODO: Fix TypeScript errors in src\api\util\handlers\Message.ts once this is enabled

View File

@ -16,7 +16,6 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
// TODO: webhooks
export interface WebhookCreateSchema { export interface WebhookCreateSchema {
/** /**
* @maxLength 80 * @maxLength 80

View File

@ -0,0 +1,46 @@
/*
Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
Copyright (C) 2023 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 { Embed } from "../entities";
import { MessageCreateAttachment } from "./MessageCreateSchema";
export interface WebhookExecuteSchema {
content?: string;
username?: string;
avatar_url?: string;
tts?: boolean;
embeds?: Embed[];
allowed_mentions?: {
parse?: string[];
roles?: string[];
users?: string[];
replied_user?: boolean;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
components?: any[];
file?: { filename: string };
payload_json?: string;
/**
TODO: we should create an interface for attachments
TODO: OpenWAAO<-->attachment-style metadata conversion
**/
attachments?: MessageCreateAttachment[];
flags?: number;
thread_name?: string;
applied_tags?: string[];
}

View File

@ -79,5 +79,6 @@ export * from "./VoiceStateUpdateSchema";
export * from "./VoiceVideoSchema"; export * from "./VoiceVideoSchema";
export * from "./WebAuthnSchema"; export * from "./WebAuthnSchema";
export * from "./WebhookCreateSchema"; export * from "./WebhookCreateSchema";
export * from "./WebhookExecuteSchema";
export * from "./WidgetModifySchema"; export * from "./WidgetModifySchema";
export * from "./responses"; export * from "./responses";

View File

@ -578,7 +578,7 @@ export const DiscordApiErrors = {
UNKNOWN_TOKEN: new ApiError("Unknown token", 10012), UNKNOWN_TOKEN: new ApiError("Unknown token", 10012),
UNKNOWN_USER: new ApiError("Unknown user", 10013), UNKNOWN_USER: new ApiError("Unknown user", 10013),
UNKNOWN_EMOJI: new ApiError("Unknown emoji", 10014), UNKNOWN_EMOJI: new ApiError("Unknown emoji", 10014),
UNKNOWN_WEBHOOK: new ApiError("Unknown webhook", 10015), UNKNOWN_WEBHOOK: new ApiError("Unknown webhook", 10015, 404),
UNKNOWN_WEBHOOK_SERVICE: new ApiError("Unknown webhook service", 10016), UNKNOWN_WEBHOOK_SERVICE: new ApiError("Unknown webhook service", 10016),
UNKNOWN_CONNECTION: new ApiError("Unknown connection", 10017, 400), UNKNOWN_CONNECTION: new ApiError("Unknown connection", 10017, 400),
UNKNOWN_SESSION: new ApiError("Unknown session", 10020), UNKNOWN_SESSION: new ApiError("Unknown session", 10020),