Merge branch 'master' into feat/local-image-proxy

This commit is contained in:
TomatoCake 2024-07-18 16:08:06 +02:00 committed by GitHub
commit d79669f771
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
69 changed files with 41560 additions and 6268 deletions

View File

@ -1,7 +1,6 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@ -23,32 +22,39 @@
width: 100%;
}
</style>
</head>
</head>
<body>
<div style="background-color: #202225;">
<img src="https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/svg/Spacebar__Logo-Blue.svg"
alt="Branding" style="
<body>
<div style="background-color: #202225">
<img
src="https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/svg/Spacebar__Logo-Blue.svg"
alt="Branding"
style="
width: 100%;
max-width: 200px;
margin: 0 auto;
display: block;
padding: 20px;
" />
<div style="
"
/>
<div
style="
width: 100%;
max-width: 500px;
margin: 0 auto;
padding: 40px 50px;
background-color: #32353b;
border-radius: 5px;
">
<p style="
"
>
<p
style="
font-weight: 600;
font-size: 20px;
letter-spacing: 0.27px;
line-height: 24px;
">
"
>
Hey {userUsername},
</p>
<p>
@ -65,12 +71,17 @@
{locationCountryName}
</p>
<div>
<div style="
<div
style="
text-align: center;
justify-content: center;
padding-bottom: 10px;
">
<a href="{actionUrl}" target="_blank" style="
"
>
<a
href="{actionUrl}"
target="_blank"
style="
font-size: 15px;
border: none;
text-decoration: none;
@ -79,23 +90,31 @@
padding: 15px 19px;
background-color: #0185ff;
border-radius: 5px;
">Verify Login</a>
"
>Verify Login</a
>
</div>
<hr />
<div style="
<div
style="
text-align: center;
justify-content: center;
padding-bottom: 10px;
">
"
>
<p>
Alternatively, you can directly paste this link into
your browser:
</p>
<a href="{actionUrl}" target="_blank" style="word-wrap: break-word;">{actionUrl}</a>
<a
href="{actionUrl}"
target="_blank"
style="word-wrap: break-word"
>{actionUrl}</a
>
</div>
</div>
</div>
</div>
</body>
</body>
</html>

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
@ -22,7 +22,7 @@
</style>
</head>
<body>
<div style="background-color: #202225;">
<div style="background-color: #202225">
<img
src="https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/svg/Spacebar__Logo-Blue.svg"
alt="Branding"

View File

@ -1,7 +1,6 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@ -23,32 +22,39 @@
width: 100%;
}
</style>
</head>
</head>
<body>
<div style="background-color: #202225;">
<img src="https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/svg/Spacebar__Logo-Blue.svg"
alt="Branding" style="
<body>
<div style="background-color: #202225">
<img
src="https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/svg/Spacebar__Logo-Blue.svg"
alt="Branding"
style="
width: 100%;
max-width: 200px;
margin: 0 auto;
display: block;
padding: 20px;
" />
<div style="
"
/>
<div
style="
width: 100%;
max-width: 500px;
margin: 0 auto;
padding: 40px 50px;
background-color: #32353b;
border-radius: 5px;
">
<p style="
"
>
<p
style="
font-weight: 600;
font-size: 20px;
letter-spacing: 0.27px;
line-height: 24px;
">
"
>
Hey {userUsername},
</p>
<p>
@ -57,12 +63,17 @@
ignore this email.
</p>
<div>
<div style="
<div
style="
text-align: center;
justify-content: center;
padding-bottom: 10px;
">
<a href="{actionUrl}" target="_blank" style="
"
>
<a
href="{actionUrl}"
target="_blank"
style="
font-size: 15px;
border: none;
text-decoration: none;
@ -71,7 +82,9 @@
padding: 15px 19px;
background-color: #ff5f00;
border-radius: 5px;
">Reset Password</a>
"
>Reset Password</a
>
</div>
<hr />
<div style="text-align: center">
@ -79,11 +92,15 @@
Alternatively, you can directly paste this link into
your browser:
</p>
<a href="{actionUrl}" target="_blank" style="word-wrap: break-word;">{actionUrl}</a>
<a
href="{actionUrl}"
target="_blank"
style="word-wrap: break-word"
>{actionUrl}</a
>
</div>
</div>
</div>
</div>
</body>
</body>
</html>

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
@ -22,7 +22,7 @@
</style>
</head>
<body>
<div style="background-color: #202225;">
<div style="background-color: #202225">
<img
src="https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/svg/Spacebar__Logo-Blue.svg"
alt="Branding"

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
@ -22,7 +22,7 @@
</style>
</head>
<body>
<div style="background-color: #202225;">
<div style="background-color: #202225">
<img
src="https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/svg/Spacebar__Logo-Blue.svg"
alt="Branding"
@ -69,7 +69,7 @@
>
<a
class="btn"
href="{emailVerificationUrl}"
href="{actionUrl}"
target="_blank"
style="
font-size: 15px;
@ -90,8 +90,11 @@
Alternatively, you can directly paste this link into
your browser:
</p>
<a href="{emailVerificationUrl}" target="_blank" style="word-wrap: break-word;"
>{emailVerificationUrl}</a
<a
href="{actionUrl}"
target="_blank"
style="word-wrap: break-word"
>{actionUrl}</a
>
</div>
</div>

View File

@ -734,6 +734,114 @@
}
}
},
"MessageComponent": {
"type": "object",
"properties": {
"type": {
"type": "integer"
},
"style": {
"type": "integer"
},
"label": {
"type": "string"
},
"emoji": {
"$ref": "#/components/schemas/PartialEmoji"
},
"custom_id": {
"type": "string"
},
"sku_id": {
"type": "string"
},
"url": {
"type": "string"
},
"disabled": {
"type": "boolean"
},
"components": {
"type": "array",
"items": {
"$ref": "#/components/schemas/MessageComponent"
}
}
},
"required": [
"components",
"type"
]
},
"PartialEmoji": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"animated": {
"type": "boolean"
}
},
"required": [
"name"
]
},
"PollCreationSchema": {
"type": "object",
"properties": {
"question": {
"$ref": "#/components/schemas/PollMedia"
},
"answers": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PollAnswer"
}
},
"duration": {
"type": "integer"
},
"allow_multiselect": {
"type": "boolean"
},
"layout_type": {
"type": "integer"
}
},
"required": [
"answers",
"question"
]
},
"PollMedia": {
"type": "object",
"properties": {
"text": {
"type": "string"
},
"emoji": {
"$ref": "#/components/schemas/PartialEmoji"
}
}
},
"PollAnswer": {
"type": "object",
"properties": {
"answer_id": {
"type": "string"
},
"poll_media": {
"$ref": "#/components/schemas/PollMedia"
}
},
"required": [
"poll_media"
]
},
"ChannelOverride": {
"type": "object",
"properties": {
@ -1193,7 +1301,8 @@
"$ref": "#/components/schemas/Guild"
},
"parent_id": {
"type": "string"
"type": "string",
"nullable": true
},
"parent": {
"$ref": "#/components/schemas/Channel"
@ -1802,6 +1911,10 @@
"type": "integer",
"default": 0
},
"friend_discovery_flags": {
"type": "integer",
"default": 0
},
"friend_source_flags": {
"$ref": "#/components/schemas/FriendSourceFlags"
},
@ -1892,6 +2005,10 @@
"timezone_offset": {
"type": "integer",
"default": 0
},
"view_nsfw_guilds": {
"type": "boolean",
"default": true
}
},
"required": [
@ -1908,6 +2025,7 @@
"disable_games_tab",
"enable_tts_command",
"explicit_content_filter",
"friend_discovery_flags",
"friend_source_flags",
"gateway_connected",
"gif_auto_play",
@ -1926,7 +2044,8 @@
"status",
"stream_notifications_enabled",
"theme",
"timezone_offset"
"timezone_offset",
"view_nsfw_guilds"
]
},
"SecurityKey": {
@ -2240,6 +2359,9 @@
"$ref": "#/components/schemas/MessageComponent"
}
},
"poll": {
"$ref": "#/components/schemas/Poll"
},
"id": {
"type": "string"
}
@ -2963,23 +3085,6 @@
"user_ids"
]
},
"PartialEmoji": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"animated": {
"type": "boolean"
}
},
"required": [
"name"
]
},
"MessageType": {
"enum": [
0,
@ -3018,40 +3123,71 @@
],
"type": "number"
},
"MessageComponent": {
"Poll": {
"type": "object",
"properties": {
"type": {
"type": "integer"
"question": {
"$ref": "#/components/schemas/PollMedia"
},
"style": {
"type": "integer"
},
"label": {
"type": "string"
},
"emoji": {
"$ref": "#/components/schemas/PartialEmoji"
},
"custom_id": {
"type": "string"
},
"url": {
"type": "string"
},
"disabled": {
"type": "boolean"
},
"components": {
"answers": {
"type": "array",
"items": {
"$ref": "#/components/schemas/MessageComponent"
"$ref": "#/components/schemas/PollAnswer"
}
},
"expiry": {
"type": "string",
"format": "date-time"
},
"allow_multiselect": {
"type": "boolean"
},
"results": {
"$ref": "#/components/schemas/PollResult"
}
},
"required": [
"allow_multiselect",
"answers",
"expiry",
"question"
]
},
"PollResult": {
"type": "object",
"properties": {
"is_finalized": {
"type": "boolean"
},
"answer_counts": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PollAnswerCount"
}
}
},
"required": [
"components",
"type"
"answer_counts",
"is_finalized"
]
},
"PollAnswerCount": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"count": {
"type": "integer"
},
"me_voted": {
"type": "boolean"
}
},
"required": [
"count",
"id",
"me_voted"
]
},
"VoiceState": {
@ -3444,7 +3580,12 @@
},
"components": {
"type": "array",
"items": {}
"items": {
"$ref": "#/components/schemas/MessageComponent"
}
},
"poll": {
"$ref": "#/components/schemas/Poll"
},
"hit": {
"type": "boolean",
@ -3466,6 +3607,7 @@
"mention_roles",
"mentions",
"pinned",
"poll",
"timestamp",
"tts",
"type"
@ -5212,7 +5354,27 @@
},
"components": {
"type": "array",
"items": {}
"items": {
"$ref": "#/components/schemas/MessageComponent"
}
},
"poll": {
"$ref": "#/components/schemas/PollCreationSchema"
},
"enforce_nonce": {
"type": "boolean"
},
"applied_tags": {
"type": "array",
"items": {
"type": "string"
}
},
"thread_name": {
"type": "string"
},
"avatar_url": {
"type": "string"
}
}
},
@ -5338,7 +5500,27 @@
},
"components": {
"type": "array",
"items": {}
"items": {
"$ref": "#/components/schemas/MessageComponent"
}
},
"poll": {
"$ref": "#/components/schemas/PollCreationSchema"
},
"enforce_nonce": {
"type": "boolean"
},
"applied_tags": {
"type": "array",
"items": {
"type": "string"
}
},
"thread_name": {
"type": "string"
},
"avatar_url": {
"type": "string"
}
}
},
@ -5919,6 +6101,9 @@
"explicit_content_filter": {
"type": "integer"
},
"friend_discovery_flags": {
"type": "integer"
},
"friend_source_flags": {
"$ref": "#/components/schemas/FriendSourceFlags"
},
@ -5982,6 +6167,9 @@
},
"timezone_offset": {
"type": "integer"
},
"view_nsfw_guilds": {
"type": "boolean"
}
}
},
@ -7660,6 +7848,9 @@
"$ref": "#/components/schemas/StickerPack"
}
},
"APIConnectionsConfiguration": {
"type": "object"
},
"UpdatesResponse": {
"type": "object",
"properties": {
@ -7917,6 +8108,23 @@
"user"
]
},
"BulkBanSchema": {
"type": "object",
"properties": {
"user_ids": {
"type": "array",
"items": {
"type": "string"
}
},
"delete_message_seconds": {
"type": "integer"
}
},
"required": [
"user_ids"
]
},
"BulkDeleteSchema": {
"type": "object",
"properties": {
@ -13729,12 +13937,25 @@
},
"/guilds/{guild_id}/bulk-ban/": {
"post": {
"x-permission-required": "BAN_MEMBERS",
"x-permission-required": [
"BAN_MEMBERS",
"MANAGE_GUILD"
],
"security": [
{
"bearer": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BulkBanSchema"
}
}
}
},
"responses": {
"200": {
"description": "",
@ -14322,6 +14543,30 @@
]
}
},
"/connections/": {
"get": {
"security": [
{
"bearer": []
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIConnectionsConfiguration"
}
}
}
}
},
"tags": [
"connections"
]
}
},
"/connections/{connection_name}/callback/": {
"post": {
"security": [

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

147
assets/public/verify.html Normal file
View File

@ -0,0 +1,147 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Spacebar Server</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Montserrat&display=swap"
rel="stylesheet"
/>
<style>
body {
font-family: "Montserrat", sans-serif;
background-color: rgb(10, 10, 10);
color: white;
font-size: 1.1rem;
height: 100vh;
}
* {
padding: 0;
margin: 0;
}
p {
margin-top: 10px;
}
#wordmark {
width: min(200px, 50%);
margin: 20px;
position: absolute;
top: 20px;
left: 20px;
}
.title {
font-size: 1.5rem;
font-weight: 600;
}
.subtitle {
font-size: 1.1rem;
font-weight: 400;
}
.container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.box {
width: 22vw;
padding: 32px;
border-radius: 8px;
background-color: rgb(32, 32, 32);
align-items: center;
display: flex;
flex-direction: column;
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<img
alt="Spacebar Logo"
id="wordmark"
src="https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/svg/Spacebar__Logo-Blue.svg"
/>
<div class="box">
<p id="title" class="title">Verifying your email</p>
<p id="subtitle" class="subtitle">Please wait...</p>
</div>
</div>
<script>
window.onload = verify;
function verify() {
const title = document.getElementById("title");
const subtitle = document.getElementById("subtitle");
// if no fragment identifier in URL, error
if (!window.location.hash) {
title.innerText = "Invalid Link";
subtitle.innerText = "Please check the link and try again.";
return;
}
// convert fragment to a key-value pair
const fragment = window.location.hash.substring(1);
const pairs = fragment.split("&");
const values = {};
pairs.forEach((pair) => {
const [key, value] = pair.split("=");
values[key] = value;
});
// ensure token key is present
if (!values.token) {
title.innerText = "Invalid Link";
subtitle.innerText = "Please check the link and try again.";
return;
}
// make request to server
const token = values.token;
fetch("/api/auth/verify", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
token,
}),
})
.then((response) => response.json())
.then((data) => {
// check for an error response
if ("message" in data) {
title.innerText = "Email Verification Link Expired";
subtitle.innerText =
"Please request a new verification link.";
return;
}
title.innerText = "Email Verified";
subtitle.innerText = "You can now login.";
})
.catch((error) => {
title.innerText = "Email Verification Failed";
subtitle.innerText = error;
});
}
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -18,15 +18,15 @@
import {
Config,
Email,
initDatabase,
initEvent,
JSONReplacer,
registerRoutes,
Sentry,
WebAuthn,
ConnectionConfig,
ConnectionLoader,
Email,
JSONReplacer,
Sentry,
WebAuthn,
initDatabase,
initEvent,
registerRoutes,
} from "@spacebar/util";
import { Request, Response, Router } from "express";
import { Server, ServerOptions } from "lambert-server";
@ -143,6 +143,10 @@ export class SpacebarServer extends Server {
res.sendFile(path.join(PUBLIC_ASSETS_FOLDER, "index.html")),
);
app.get("/verify", (req, res) =>
res.sendFile(path.join(PUBLIC_ASSETS_FOLDER, "verify.html")),
);
this.app.use(ErrorHandler);
Sentry.errorHandler(this.app);

View File

@ -85,7 +85,7 @@ router.post(
user = userTokenData.user;
} catch {
throw FieldErrors({
password: {
token: {
message: req.t("auth:password_reset.INVALID_TOKEN"),
code: "INVALID_TOKEN",
},

View File

@ -50,7 +50,13 @@ router.get(
const channel = await Channel.findOneOrFail({
where: { id: channel_id },
});
if (!channel.guild_id) return res.send(channel);
channel.position = await Channel.calculatePosition(
channel_id,
channel.guild_id,
channel.guild,
);
return res.send(channel);
},
);

View File

@ -56,6 +56,7 @@ router.post(
edited_timestamp: null,
flags: 1,
components: [],
poll: {},
}).status(200);
},
);

View File

@ -91,11 +91,10 @@ router.patch(
}
} else rights.hasThrow("SELF_EDIT_MESSAGES");
// @ts-expect-error Something is wrong with message_reference here, TS complains since "channel_id" is optional in MessageCreateSchema
const new_message = await handleMessage({
...message,
// TODO: should message_reference be overridable?
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
message_reference: message.message_reference,
...body,
author_id: message.author_id,

View File

@ -183,9 +183,17 @@ router.get(
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
}`;
let pathname = new URL(uri).pathname;
while (
pathname.split("/")[0] != "attachments" &&
pathname.length > 30
) {
pathname = pathname.split("/").slice(1).join("/");
}
if (!endpoint?.endsWith("/")) pathname = "/" + pathname;
y.proxy_url = `${endpoint == null ? "" : endpoint}${pathname}`;
});
/**

View File

@ -53,6 +53,11 @@ router.put(
where: { id: channel_id },
});
if (!channel.guild_id) throw new HTTPError("Channel not found", 404);
channel.position = await Channel.calculatePosition(
channel_id,
channel.guild_id,
channel.guild,
);
if (body.type === 0) {
if (!(await Role.count({ where: { id: overwrite_id } })))

View File

@ -0,0 +1,45 @@
/*
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 { route } from "@spacebar/api";
import { ConnectionConfig } from "@spacebar/util";
import { Request, Response, Router } from "express";
const router = Router();
router.get(
"/",
route({
responses: {
200: {
body: "APIConnectionsConfiguration",
},
},
}),
async (req: Request, res: Response) => {
const config = ConnectionConfig.get();
Object.keys(config).forEach((key) => {
delete config[key].clientId;
delete config[key].clientSecret;
});
res.json(config);
},
);
export default router;

View File

@ -41,6 +41,15 @@ router.get(
const { guild_id } = req.params;
const channels = await Channel.find({ where: { guild_id } });
for await (const channel of channels) {
channel.position = await Channel.calculatePosition(
channel.id,
guild_id,
channel.guild,
);
}
channels.sort((a, b) => a.position - b.position);
res.json(channels);
},
);
@ -71,6 +80,11 @@ router.post(
{ ...body, guild_id },
req.user_id,
);
channel.position = await Channel.calculatePosition(
channel.id,
guild_id,
channel.guild,
);
res.status(201).json(channel);
},

View File

@ -125,6 +125,7 @@ router.post(
const user = await User.findOneOrFail({ where: { id: req.user_id } });
body.image = (await handleFile(`/emojis/${id}`, body.image)) as string;
const mimeType = body.image.split(":")[1].split(";")[0];
const emoji = await Emoji.create({
id: id,
guild_id: guild_id,
@ -132,7 +133,10 @@ router.post(
require_colons: body.require_colons ?? undefined, // schema allows nulls, db does not
user: user,
managed: false,
animated: false, // TODO: Add support animated emojis
animated:
mimeType == "image/gif" ||
mimeType == "image/apng" ||
mimeType == "video/webm",
available: true,
roles: [],
}).save();

View File

@ -162,6 +162,7 @@ router.get(
edited_timestamp: x.edited_timestamp,
flags: x.flags,
components: x.components,
poll: x.poll,
hit: true,
},
]);

View File

@ -18,6 +18,7 @@
import { route } from "@spacebar/api";
import {
Badge,
Member,
PrivateUserProjection,
User,
@ -98,6 +99,9 @@ router.get(
bio: guild_member?.bio || "",
guild_id,
};
const badges = await Badge.find();
res.json({
connected_accounts: user.connected_accounts.filter(
(x) => x.visibility != 0,
@ -111,6 +115,7 @@ router.get(
user_profile: userProfile,
guild_member: guild_member?.toPublicMember(),
guild_member_profile: guild_id && guildMemberProfile,
badges: badges.filter((x) => user.badge_ids?.includes(x.id)),
});
},
);

View File

@ -16,42 +16,42 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as Sentry from "@sentry/node";
import { EmbedHandlers } from "@spacebar/api";
import {
Application,
Attachment,
Channel,
Config,
Embed,
EmbedCache,
emitEvent,
Guild,
Message,
MessageCreateEvent,
MessageUpdateEvent,
EVERYONE_MENTION,
getPermission,
getRights,
Guild,
HERE_MENTION,
Message,
MessageCreateEvent,
MessageCreateSchema,
MessageType,
MessageUpdateEvent,
Role,
ROLE_MENTION,
Sticker,
User,
//CHANNEL_MENTION,
USER_MENTION,
ROLE_MENTION,
Role,
EVERYONE_MENTION,
HERE_MENTION,
MessageType,
User,
Application,
Webhook,
Attachment,
Config,
Sticker,
MessageCreateSchema,
EmbedCache,
} from "@spacebar/util";
import { HTTPError } from "lambert-server";
import { In } from "typeorm";
import { EmbedHandlers } from "@spacebar/api";
import * as Sentry from "@sentry/node";
const allow_empty = false;
// TODO: check webhook, application, system author, stickers
// TODO: embed gifs/videos/images
const LINK_REGEX =
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/g;
/<?https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)>?/g;
export async function handleMessage(opts: MessageOptions): Promise<Message> {
const channel = await Channel.findOneOrFail({
@ -66,6 +66,7 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
: undefined;
const message = Message.create({
...opts,
poll: opts.poll,
sticker_items: stickers,
guild_id: channel.guild_id,
channel_id: opts.channel_id,
@ -116,6 +117,12 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
const guild = await Guild.findOneOrFail({
where: { id: channel.guild_id },
});
if (!opts.message_reference.guild_id)
opts.message_reference.guild_id = channel.guild_id;
if (!opts.message_reference.channel_id)
opts.message_reference.channel_id = opts.channel_id;
if (!guild.features.includes("CROSS_CHANNEL_REPLIES")) {
if (opts.message_reference.guild_id !== channel.guild_id)
throw new HTTPError(
@ -126,6 +133,8 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
"You can only reference messages from this channel",
);
}
message.message_reference = opts.message_reference;
}
/** Q: should be checked if the referenced message exists? ANSWER: NO
otherwise backfilling won't work **/
@ -138,7 +147,9 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
!opts.content &&
!opts.embeds?.length &&
!opts.attachments?.length &&
!opts.sticker_ids?.length
!opts.sticker_ids?.length &&
!opts.poll &&
!opts.components?.length
) {
throw new HTTPError("Empty messages are not allowed", 50006);
}
@ -213,6 +224,9 @@ export async function postHandleMessage(message: Message) {
const cachePromises = [];
for (const link of links) {
// Don't embed links in <>
if (link.startsWith("<") && link.endsWith(">")) continue;
const url = new URL(link);
const cached = await EmbedCache.findOne({ where: { url: link } });

View File

@ -0,0 +1,40 @@
/*
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 { Router, Response, Request } from "express";
import { storage } from "../util/Storage";
import FileType from "file-type";
import { HTTPError } from "lambert-server";
const router = Router();
router.get("/:badge_id", async (req: Request, res: Response) => {
const { badge_id } = req.params;
const path = `badge-icons/${badge_id}`;
const file = await storage.get(path);
if (!file) throw new HTTPError("not found", 404);
const type = await FileType.fromBuffer(file);
res.set("Content-Type", type?.mime);
res.set("Cache-Control", "public, max-age=31536000, must-revalidate");
return res.send(file);
});
export default router;

View File

@ -23,12 +23,12 @@ import { HTTPError } from "lambert-server";
import { join } from "path";
const defaultAvatarHashMap = new Map([
["0", "823a3de61c4dc2415cc4dbc38fca4299"],
["1", "e56a89224be0b2b1f7c04eca975be468"],
["2", "0c8138dcc0dfe2689cdd73f7952c2475"],
["3", "5ac2728593bb455250d11b848a0c36c6"],
["4", "addd2f3268df46459e1d6012ad8e75bd"],
["5", "c4e0c8300fa491d94acfd2a1fb26cea8"],
["0", "4a8562cf00887030c416d3ec2d46385a"],
["1", "9b0bb198936784c45c72833cc426cc55"],
["2", "22341bdb500c7b63a93bbce957d1601e"],
["3", "d9977836b82058bf2f74eebd50edc095"],
["4", "9d6ddb4e4d899a533a8cc617011351c9"],
["5", "7213ab6677377974697dfdfbaf5f6a6f"],
]);
const defaultGroupDMAvatarHashMap = new Map([

View File

@ -47,13 +47,15 @@ export default class BattleNetConnection extends Connection {
settings: BattleNetSettings = new BattleNetSettings();
init(): void {
const settings =
ConnectionLoader.getConnectionConfig<BattleNetSettings>(
this.settings = ConnectionLoader.getConnectionConfig<BattleNetSettings>(
this.id,
this.settings,
);
if (settings.enabled && (!settings.clientId || !settings.clientSecret))
if (
this.settings.enabled &&
(!this.settings.clientId || !this.settings.clientSecret)
)
throw new Error(`Invalid settings for connection ${this.id}`);
}

View File

@ -43,12 +43,15 @@ export default class DiscordConnection extends Connection {
settings: DiscordSettings = new DiscordSettings();
init(): void {
const settings = ConnectionLoader.getConnectionConfig<DiscordSettings>(
this.settings = ConnectionLoader.getConnectionConfig<DiscordSettings>(
this.id,
this.settings,
);
if (settings.enabled && (!settings.clientId || !settings.clientSecret))
if (
this.settings.enabled &&
(!this.settings.clientId || !this.settings.clientSecret)
)
throw new Error(`Invalid settings for connection ${this.id}`);
}

View File

@ -53,13 +53,15 @@ export default class EpicGamesConnection extends Connection {
settings: EpicGamesSettings = new EpicGamesSettings();
init(): void {
const settings =
ConnectionLoader.getConnectionConfig<EpicGamesSettings>(
this.settings = ConnectionLoader.getConnectionConfig<EpicGamesSettings>(
this.id,
this.settings,
);
if (settings.enabled && (!settings.clientId || !settings.clientSecret))
if (
this.settings.enabled &&
(!this.settings.clientId || !this.settings.clientSecret)
)
throw new Error(`Invalid settings for connection ${this.id}`);
}

View File

@ -52,12 +52,15 @@ export default class FacebookConnection extends Connection {
settings: FacebookSettings = new FacebookSettings();
init(): void {
const settings = ConnectionLoader.getConnectionConfig<FacebookSettings>(
this.settings = ConnectionLoader.getConnectionConfig<FacebookSettings>(
this.id,
this.settings,
);
if (settings.enabled && (!settings.clientId || !settings.clientSecret))
if (
this.settings.enabled &&
(!this.settings.clientId || !this.settings.clientSecret)
)
throw new Error(`Invalid settings for connection ${this.id}`);
}

View File

@ -42,12 +42,15 @@ export default class GitHubConnection extends Connection {
settings: GitHubSettings = new GitHubSettings();
init(): void {
const settings = ConnectionLoader.getConnectionConfig<GitHubSettings>(
this.settings = ConnectionLoader.getConnectionConfig<GitHubSettings>(
this.id,
this.settings,
);
if (settings.enabled && (!settings.clientId || !settings.clientSecret))
if (
this.settings.enabled &&
(!this.settings.clientId || !this.settings.clientSecret)
)
throw new Error(`Invalid settings for connection ${this.id}`);
}

View File

@ -54,12 +54,15 @@ export default class RedditConnection extends Connection {
settings: RedditSettings = new RedditSettings();
init(): void {
const settings = ConnectionLoader.getConnectionConfig<RedditSettings>(
this.settings = ConnectionLoader.getConnectionConfig<RedditSettings>(
this.id,
this.settings,
);
if (settings.enabled && (!settings.clientId || !settings.clientSecret))
if (
this.settings.enabled &&
(!this.settings.clientId || !this.settings.clientSecret)
)
throw new Error(`Invalid settings for connection ${this.id}`);
}

View File

@ -63,12 +63,16 @@ export default class SpotifyConnection extends RefreshableConnection {
* So to prevent spamming the spotify api we disable the ability to refresh.
*/
this.refreshEnabled = false;
const settings = ConnectionLoader.getConnectionConfig<SpotifySettings>(
this.settings = ConnectionLoader.getConnectionConfig<SpotifySettings>(
this.id,
this.settings,
);
if (settings.enabled && (!settings.clientId || !settings.clientSecret))
if (
this.settings.enabled &&
(!this.settings.clientId || !this.settings.clientSecret)
)
throw new Error(`Invalid settings for connection ${this.id}`);
}

View File

@ -55,12 +55,15 @@ export default class TwitchConnection extends RefreshableConnection {
settings: TwitchSettings = new TwitchSettings();
init(): void {
const settings = ConnectionLoader.getConnectionConfig<TwitchSettings>(
this.settings = ConnectionLoader.getConnectionConfig<TwitchSettings>(
this.id,
this.settings,
);
if (settings.enabled && (!settings.clientId || !settings.clientSecret))
if (
this.settings.enabled &&
(!this.settings.clientId || !this.settings.clientSecret)
)
throw new Error(`Invalid settings for connection ${this.id}`);
}

View File

@ -55,12 +55,15 @@ export default class TwitterConnection extends RefreshableConnection {
settings: TwitterSettings = new TwitterSettings();
init(): void {
const settings = ConnectionLoader.getConnectionConfig<TwitterSettings>(
this.settings = ConnectionLoader.getConnectionConfig<TwitterSettings>(
this.id,
this.settings,
);
if (settings.enabled && (!settings.clientId || !settings.clientSecret))
if (
this.settings.enabled &&
(!this.settings.clientId || !this.settings.clientSecret)
)
throw new Error(`Invalid settings for connection ${this.id}`);
}

View File

@ -62,12 +62,15 @@ export default class XboxConnection extends Connection {
settings: XboxSettings = new XboxSettings();
init(): void {
const settings = ConnectionLoader.getConnectionConfig<XboxSettings>(
this.settings = ConnectionLoader.getConnectionConfig<XboxSettings>(
this.id,
this.settings,
);
if (settings.enabled && (!settings.clientId || !settings.clientSecret))
if (
this.settings.enabled &&
(!this.settings.clientId || !this.settings.clientSecret)
)
throw new Error(`Invalid settings for connection ${this.id}`);
}

View File

@ -62,12 +62,15 @@ export default class YoutubeConnection extends Connection {
settings: YoutubeSettings = new YoutubeSettings();
init(): void {
const settings = ConnectionLoader.getConnectionConfig<YoutubeSettings>(
this.settings = ConnectionLoader.getConnectionConfig<YoutubeSettings>(
this.id,
this.settings,
);
if (settings.enabled && (!settings.clientId || !settings.clientSecret))
if (
this.settings.enabled &&
(!this.settings.clientId || !this.settings.clientSecret)
)
throw new Error(`Invalid settings for connection ${this.id}`);
}

View File

@ -25,6 +25,7 @@ import { SendGridConfiguration } from "./subconfigurations/email/SendGrid";
export class EmailConfiguration {
provider: string | null = null;
senderAddress: string | null = null;
smtp: SMTPConfiguration = new SMTPConfiguration();
mailgun: MailGunConfiguration = new MailGunConfiguration();
mailjet: MailJetConfiguration = new MailJetConfiguration();

View File

@ -43,7 +43,7 @@ export abstract class Connection {
*/
getRedirectUri() {
const endpointPublic =
Config.get().api.endpointPublic ?? "http://localhost:3001";
Config.get().general.frontPage ?? "http://localhost:3001";
return `${endpointPublic}/connections/${this.id}/callback`;
}

View File

@ -24,6 +24,7 @@ export class MinimalPublicUserDTO {
id: string;
public_flags: number;
username: string;
badge_ids?: string[] | null;
constructor(user: User) {
this.avatar = user.avatar;
@ -31,5 +32,6 @@ export class MinimalPublicUserDTO {
this.id = user.id;
this.public_flags = user.public_flags;
this.username = user.username;
this.badge_ids = user.badge_ids;
}
}

View File

@ -0,0 +1,35 @@
/*
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 { Column, Entity } from "typeorm";
import { BaseClassWithoutId } from "./BaseClass";
@Entity("badges")
export class Badge extends BaseClassWithoutId {
@Column({ primary: true })
id: string;
@Column()
description: string;
@Column()
icon: string;
@Column({ nullable: true })
link?: string;
}

View File

@ -31,7 +31,7 @@ import {
PrimaryGeneratedColumn,
RelationId,
} from "typeorm";
import { Ban, PublicGuildRelations } from ".";
import { Ban, Channel, PublicGuildRelations } from ".";
import { ReadyGuildDTO } from "../dtos";
import {
GuildCreateEvent,
@ -330,6 +330,13 @@ export class Member extends BaseClassWithoutId {
relationLoadStrategy: "query",
});
for await (const channel of guild.channels) {
channel.position = await Channel.calculatePosition(
channel.id,
guild_id,
);
}
const memberCount = await Member.count({ where: { guild_id } });
const memberPreview = (

View File

@ -218,6 +218,9 @@ export class Message extends BaseClass {
@Column({ type: "simple-json", nullable: true })
components?: MessageComponent[];
@Column({ type: "simple-json", nullable: true })
poll?: Poll;
toJSON(): Message {
return {
...this,
@ -238,6 +241,7 @@ export class Message extends BaseClass {
activity: this.activity ?? undefined,
application: this.application ?? undefined,
components: this.components ?? undefined,
poll: this.poll ?? undefined,
content: this.content ?? "",
};
}
@ -249,6 +253,7 @@ export interface MessageComponent {
label?: string;
emoji?: PartialEmoji;
custom_id?: string;
sku_id?: string;
url?: string;
disabled?: boolean;
components: MessageComponent[];
@ -327,3 +332,32 @@ export interface AllowedMentions {
users?: string[];
replied_user?: boolean;
}
export interface Poll {
question: PollMedia;
answers: PollAnswer[];
expiry: Date;
allow_multiselect: boolean;
results?: PollResult;
}
export interface PollMedia {
text?: string;
emoji?: PartialEmoji;
}
export interface PollAnswer {
answer_id?: string;
poll_media: PollMedia;
}
export interface PollResult {
is_finalized: boolean;
answer_counts: PollAnswerCount[];
}
export interface PollAnswerCount {
id: string;
count: number;
me_voted: boolean;
}

View File

@ -49,6 +49,7 @@ export enum PublicUserEnum {
premium_type,
theme_colors,
pronouns,
badge_ids,
}
export type PublicUserKeys = keyof typeof PublicUserEnum;
@ -231,6 +232,9 @@ export class User extends BaseClass {
@OneToMany(() => SecurityKey, (key: SecurityKey) => key.user)
security_keys: SecurityKey[];
@Column({ type: "simple-array", nullable: true })
badge_ids?: string[];
// TODO: I don't like this method?
validate() {
if (this.discriminator) {

View File

@ -20,6 +20,7 @@ export * from "./Application";
export * from "./Attachment";
export * from "./AuditLog";
export * from "./BackupCodes";
export * from "./Badge";
export * from "./Ban";
export * from "./BaseClass";
export * from "./Categories";

View File

@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class NewUserSettings1719776735000 implements MigrationInterface {
name = "NewUserSettings1719776735000";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
"ALTER TABLE `user_settings` ADD friend_discovery_flags integer NULL DEFAULT 0;",
);
await queryRunner.query(
"ALTER TABLE `user_settings` ADD view_nsfw_guilds tinyint NULL DEFAULT 1;",
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
"ALTER TABLE `user_settings` DROP COLUMN friend_discovery_flags;",
);
await queryRunner.query(
"ALTER TABLE `user_settings` DROP COLUMN view_nsfw_guilds;",
);
}
}

View File

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

View File

@ -0,0 +1,21 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class Badges1720628601997 implements MigrationInterface {
name = "Badges1720628601997";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE \`badges\` (\`id\` varchar(255) NOT NULL, \`description\` varchar(255) NOT NULL, \`icon\` varchar(255) NOT NULL, \`link\` varchar(255) NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`,
);
await queryRunner.query(
`ALTER TABLE \`users\` ADD \`badge_ids\` text NULL`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE \`users\` DROP COLUMN \`badge_ids\``,
);
await queryRunner.query(`DROP TABLE \`badges\``);
}
}

View File

@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class NewUserSettings1719776735000 implements MigrationInterface {
name = "NewUserSettings1719776735000";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
"ALTER TABLE `user_settings` ADD friend_discovery_flags integer NULL DEFAULT 0;",
);
await queryRunner.query(
"ALTER TABLE `user_settings` ADD view_nsfw_guilds tinyint NULL DEFAULT 1;",
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
"ALTER TABLE `user_settings` DROP COLUMN friend_discovery_flags;",
);
await queryRunner.query(
"ALTER TABLE `user_settings` DROP COLUMN view_nsfw_guilds;",
);
}
}

View File

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

View File

@ -0,0 +1,21 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class Badges1720628601997 implements MigrationInterface {
name = "Badges1720628601997";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE \`badges\` (\`id\` varchar(255) NOT NULL, \`description\` varchar(255) NOT NULL, \`icon\` varchar(255) NOT NULL, \`link\` varchar(255) NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`,
);
await queryRunner.query(
`ALTER TABLE \`users\` ADD \`badge_ids\` text NULL`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE \`users\` DROP COLUMN \`badge_ids\``,
);
await queryRunner.query(`DROP TABLE \`badges\``);
}
}

View File

@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class NewUserSettings1719776735000 implements MigrationInterface {
name = "NewUserSettings1719776735000";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
"ALTER TABLE user_settings ADD COLUMN friend_discovery_flags integer DEFAULT 0;",
);
await queryRunner.query(
"ALTER TABLE user_settings ADD COLUMN view_nsfw_guilds boolean DEFAULT true;",
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
"ALTER TABLE user_settings DROP COLUMN friend_discovery_flags;",
);
await queryRunner.query(
"ALTER TABLE user_settings DROP COLUMN view_nsfw_guilds;",
);
}
}

View File

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

View File

@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class Badges1720628601997 implements MigrationInterface {
name = "Badges1720628601997";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "badges" ("id" character varying NOT NULL, "description" character varying NOT NULL, "icon" character varying NOT NULL, "link" character varying, CONSTRAINT "PK_8a651318b8de577e8e217676466" PRIMARY KEY ("id"))`,
);
await queryRunner.query(`ALTER TABLE "users" ADD "badge_ids" text`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "badge_ids"`);
}
}

View File

@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Embed } from "@spacebar/util";
import { Embed, MessageComponent, PollAnswer, PollMedia } from "@spacebar/util";
type Attachment = {
id: string;
@ -42,7 +42,7 @@ export interface MessageCreateSchema {
};
message_reference?: {
message_id: string;
channel_id: string;
channel_id?: string;
guild_id?: string;
fail_if_not_exists?: boolean;
};
@ -54,6 +54,21 @@ export interface MessageCreateSchema {
**/
attachments?: Attachment[];
sticker_ids?: string[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
components?: any[];
components?: MessageComponent[];
// TODO: Fix TypeScript errors in src\api\util\handlers\Message.ts once this is enabled
poll?: PollCreationSchema;
enforce_nonce?: boolean; // For Discord compatibility, it's the default behavior here
applied_tags?: string[]; // Not implemented yet, for webhooks in forums
thread_name?: string; // Not implemented yet, for webhooks
avatar_url?: string; // Not implemented yet, for webhooks
}
// TypeScript complains once this is used above
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface PollCreationSchema {
question: PollMedia;
answers: PollAnswer[];
duration?: number;
allow_multiselect?: boolean;
layout_type?: number;
}

View File

@ -19,7 +19,9 @@
import {
Attachment,
Embed,
MessageComponent,
MessageType,
Poll,
PublicUser,
Role,
} from "../../entities";
@ -40,7 +42,8 @@ export interface GuildMessagesSearchMessage {
timestamp: string;
edited_timestamp: string | null;
flags: number;
components: unknown[];
components: MessageComponent[];
poll: Poll;
hit: true;
}

View File

@ -104,3 +104,10 @@ export type APIGuildVoiceRegion = GuildVoiceRegion[];
export type APILimitsConfiguration = LimitsConfiguration;
export type APIStickerPackArray = StickerPack[];
export type APIConnectionsConfiguration = Record<
string,
{
enabled: boolean;
}
>;

View File

@ -17,6 +17,7 @@
*/
import {
Badge,
Member,
PublicConnectedAccount,
PublicMember,
@ -52,4 +53,5 @@ export interface UserProfileResponse {
user_profile: UserProfile;
guild_member?: PublicMember;
guild_member_profile?: PublicMemberProfile;
badges: Badge[];
}

View File

@ -141,8 +141,9 @@ export const Email: {
*/
generateLink: async function (type, id, email) {
const token = (await generateToken(id, email)) as string;
// puyodead1: this is set to api endpoint because the verification page is on the server since no clients have one, and not all 3rd party clients will have one
const instanceUrl =
Config.get().general.frontPage || "http://localhost:3001";
Config.get().api.endpointPublic || "http://localhost:3001";
const link = `${instanceUrl}/${type}#token=${token}`;
return link;
},
@ -187,7 +188,9 @@ export const Email: {
const message = {
from:
Config.get().general.correspondenceEmail || "noreply@localhost",
Config.get().email.senderAddress ||
Config.get().general.correspondenceEmail ||
"noreply@localhost",
to: email,
subject,
html,

View File

@ -27,9 +27,12 @@ export default async function () {
if (!host || !port || secure === null || !username || !password)
return console.error("[Email] SMTP has not been configured correctly.");
if (!Config.get().general.correspondenceEmail)
if (
!Config.get().email.senderAddress &&
!Config.get().general.correspondenceEmail
)
return console.error(
"[Email] Correspondence email has not been configured! This is used as the sender email address.",
'[Email] You have to configure either "email_senderAddress" or "general_correspondenceEmail" for emails to work. The configured value is used as the sender address.',
);
// construct the transporter