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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@ -22,7 +22,7 @@
</style> </style>
</head> </head>
<body> <body>
<div style="background-color: #202225;"> <div style="background-color: #202225">
<img <img
src="https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/svg/Spacebar__Logo-Blue.svg" src="https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/svg/Spacebar__Logo-Blue.svg"
alt="Branding" alt="Branding"
@ -69,7 +69,7 @@
> >
<a <a
class="btn" class="btn"
href="{emailVerificationUrl}" href="{actionUrl}"
target="_blank" target="_blank"
style=" style="
font-size: 15px; font-size: 15px;
@ -90,8 +90,11 @@
Alternatively, you can directly paste this link into Alternatively, you can directly paste this link into
your browser: your browser:
</p> </p>
<a href="{emailVerificationUrl}" target="_blank" style="word-wrap: break-word;" <a
>{emailVerificationUrl}</a href="{actionUrl}"
target="_blank"
style="word-wrap: break-word"
>{actionUrl}</a
> >
</div> </div>
</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": { "ChannelOverride": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -1193,7 +1301,8 @@
"$ref": "#/components/schemas/Guild" "$ref": "#/components/schemas/Guild"
}, },
"parent_id": { "parent_id": {
"type": "string" "type": "string",
"nullable": true
}, },
"parent": { "parent": {
"$ref": "#/components/schemas/Channel" "$ref": "#/components/schemas/Channel"
@ -1802,6 +1911,10 @@
"type": "integer", "type": "integer",
"default": 0 "default": 0
}, },
"friend_discovery_flags": {
"type": "integer",
"default": 0
},
"friend_source_flags": { "friend_source_flags": {
"$ref": "#/components/schemas/FriendSourceFlags" "$ref": "#/components/schemas/FriendSourceFlags"
}, },
@ -1892,6 +2005,10 @@
"timezone_offset": { "timezone_offset": {
"type": "integer", "type": "integer",
"default": 0 "default": 0
},
"view_nsfw_guilds": {
"type": "boolean",
"default": true
} }
}, },
"required": [ "required": [
@ -1908,6 +2025,7 @@
"disable_games_tab", "disable_games_tab",
"enable_tts_command", "enable_tts_command",
"explicit_content_filter", "explicit_content_filter",
"friend_discovery_flags",
"friend_source_flags", "friend_source_flags",
"gateway_connected", "gateway_connected",
"gif_auto_play", "gif_auto_play",
@ -1926,7 +2044,8 @@
"status", "status",
"stream_notifications_enabled", "stream_notifications_enabled",
"theme", "theme",
"timezone_offset" "timezone_offset",
"view_nsfw_guilds"
] ]
}, },
"SecurityKey": { "SecurityKey": {
@ -2240,6 +2359,9 @@
"$ref": "#/components/schemas/MessageComponent" "$ref": "#/components/schemas/MessageComponent"
} }
}, },
"poll": {
"$ref": "#/components/schemas/Poll"
},
"id": { "id": {
"type": "string" "type": "string"
} }
@ -2963,23 +3085,6 @@
"user_ids" "user_ids"
] ]
}, },
"PartialEmoji": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"animated": {
"type": "boolean"
}
},
"required": [
"name"
]
},
"MessageType": { "MessageType": {
"enum": [ "enum": [
0, 0,
@ -3018,40 +3123,71 @@
], ],
"type": "number" "type": "number"
}, },
"MessageComponent": { "Poll": {
"type": "object", "type": "object",
"properties": { "properties": {
"type": { "question": {
"type": "integer" "$ref": "#/components/schemas/PollMedia"
}, },
"style": { "answers": {
"type": "integer"
},
"label": {
"type": "string"
},
"emoji": {
"$ref": "#/components/schemas/PartialEmoji"
},
"custom_id": {
"type": "string"
},
"url": {
"type": "string"
},
"disabled": {
"type": "boolean"
},
"components": {
"type": "array", "type": "array",
"items": { "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": [ "required": [
"components", "answer_counts",
"type" "is_finalized"
]
},
"PollAnswerCount": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"count": {
"type": "integer"
},
"me_voted": {
"type": "boolean"
}
},
"required": [
"count",
"id",
"me_voted"
] ]
}, },
"VoiceState": { "VoiceState": {
@ -3444,7 +3580,12 @@
}, },
"components": { "components": {
"type": "array", "type": "array",
"items": {} "items": {
"$ref": "#/components/schemas/MessageComponent"
}
},
"poll": {
"$ref": "#/components/schemas/Poll"
}, },
"hit": { "hit": {
"type": "boolean", "type": "boolean",
@ -3466,6 +3607,7 @@
"mention_roles", "mention_roles",
"mentions", "mentions",
"pinned", "pinned",
"poll",
"timestamp", "timestamp",
"tts", "tts",
"type" "type"
@ -5212,7 +5354,27 @@
}, },
"components": { "components": {
"type": "array", "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": { "components": {
"type": "array", "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": { "explicit_content_filter": {
"type": "integer" "type": "integer"
}, },
"friend_discovery_flags": {
"type": "integer"
},
"friend_source_flags": { "friend_source_flags": {
"$ref": "#/components/schemas/FriendSourceFlags" "$ref": "#/components/schemas/FriendSourceFlags"
}, },
@ -5982,6 +6167,9 @@
}, },
"timezone_offset": { "timezone_offset": {
"type": "integer" "type": "integer"
},
"view_nsfw_guilds": {
"type": "boolean"
} }
} }
}, },
@ -7660,6 +7848,9 @@
"$ref": "#/components/schemas/StickerPack" "$ref": "#/components/schemas/StickerPack"
} }
}, },
"APIConnectionsConfiguration": {
"type": "object"
},
"UpdatesResponse": { "UpdatesResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -7917,6 +8108,23 @@
"user" "user"
] ]
}, },
"BulkBanSchema": {
"type": "object",
"properties": {
"user_ids": {
"type": "array",
"items": {
"type": "string"
}
},
"delete_message_seconds": {
"type": "integer"
}
},
"required": [
"user_ids"
]
},
"BulkDeleteSchema": { "BulkDeleteSchema": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -13729,12 +13937,25 @@
}, },
"/guilds/{guild_id}/bulk-ban/": { "/guilds/{guild_id}/bulk-ban/": {
"post": { "post": {
"x-permission-required": "BAN_MEMBERS", "x-permission-required": [
"BAN_MEMBERS",
"MANAGE_GUILD"
],
"security": [ "security": [
{ {
"bearer": [] "bearer": []
} }
], ],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BulkBanSchema"
}
}
}
},
"responses": { "responses": {
"200": { "200": {
"description": "", "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/": { "/connections/{connection_name}/callback/": {
"post": { "post": {
"security": [ "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 { import {
Config, Config,
Email,
initDatabase,
initEvent,
JSONReplacer,
registerRoutes,
Sentry,
WebAuthn,
ConnectionConfig, ConnectionConfig,
ConnectionLoader, ConnectionLoader,
Email,
JSONReplacer,
Sentry,
WebAuthn,
initDatabase,
initEvent,
registerRoutes,
} from "@spacebar/util"; } from "@spacebar/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { Server, ServerOptions } from "lambert-server"; import { Server, ServerOptions } from "lambert-server";
@ -143,6 +143,10 @@ export class SpacebarServer extends Server {
res.sendFile(path.join(PUBLIC_ASSETS_FOLDER, "index.html")), 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); this.app.use(ErrorHandler);
Sentry.errorHandler(this.app); Sentry.errorHandler(this.app);

View File

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

View File

@ -50,7 +50,13 @@ router.get(
const channel = await Channel.findOneOrFail({ const channel = await Channel.findOneOrFail({
where: { id: channel_id }, 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); return res.send(channel);
}, },
); );

View File

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

View File

@ -91,11 +91,10 @@ router.patch(
} }
} else rights.hasThrow("SELF_EDIT_MESSAGES"); } 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({ const new_message = await handleMessage({
...message, ...message,
// TODO: should message_reference be overridable? // TODO: should message_reference be overridable?
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
message_reference: message.message_reference, message_reference: message.message_reference,
...body, ...body,
author_id: message.author_id, author_id: message.author_id,

View File

@ -183,9 +183,17 @@ router.get(
const uri = y.proxy_url.startsWith("http") const uri = y.proxy_url.startsWith("http")
? y.proxy_url ? y.proxy_url
: `https://example.org${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 }, where: { id: channel_id },
}); });
if (!channel.guild_id) throw new HTTPError("Channel not found", 404); 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 (body.type === 0) {
if (!(await Role.count({ where: { id: overwrite_id } }))) 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 { guild_id } = req.params;
const channels = await Channel.find({ where: { guild_id } }); 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); res.json(channels);
}, },
); );
@ -71,6 +80,11 @@ router.post(
{ ...body, guild_id }, { ...body, guild_id },
req.user_id, req.user_id,
); );
channel.position = await Channel.calculatePosition(
channel.id,
guild_id,
channel.guild,
);
res.status(201).json(channel); res.status(201).json(channel);
}, },

View File

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

View File

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

View File

@ -18,6 +18,7 @@
import { route } from "@spacebar/api"; import { route } from "@spacebar/api";
import { import {
Badge,
Member, Member,
PrivateUserProjection, PrivateUserProjection,
User, User,
@ -98,6 +99,9 @@ router.get(
bio: guild_member?.bio || "", bio: guild_member?.bio || "",
guild_id, guild_id,
}; };
const badges = await Badge.find();
res.json({ res.json({
connected_accounts: user.connected_accounts.filter( connected_accounts: user.connected_accounts.filter(
(x) => x.visibility != 0, (x) => x.visibility != 0,
@ -111,6 +115,7 @@ router.get(
user_profile: userProfile, user_profile: userProfile,
guild_member: guild_member?.toPublicMember(), guild_member: guild_member?.toPublicMember(),
guild_member_profile: guild_id && guildMemberProfile, 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/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as Sentry from "@sentry/node";
import { EmbedHandlers } from "@spacebar/api";
import { import {
Application,
Attachment,
Channel, Channel,
Config,
Embed, Embed,
EmbedCache,
emitEvent, emitEvent,
Guild, EVERYONE_MENTION,
Message,
MessageCreateEvent,
MessageUpdateEvent,
getPermission, getPermission,
getRights, getRights,
Guild,
HERE_MENTION,
Message,
MessageCreateEvent,
MessageCreateSchema,
MessageType,
MessageUpdateEvent,
Role,
ROLE_MENTION,
Sticker,
User,
//CHANNEL_MENTION, //CHANNEL_MENTION,
USER_MENTION, USER_MENTION,
ROLE_MENTION,
Role,
EVERYONE_MENTION,
HERE_MENTION,
MessageType,
User,
Application,
Webhook, Webhook,
Attachment,
Config,
Sticker,
MessageCreateSchema,
EmbedCache,
} 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 { EmbedHandlers } from "@spacebar/api";
import * as Sentry from "@sentry/node";
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
const LINK_REGEX = 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> { export async function handleMessage(opts: MessageOptions): Promise<Message> {
const channel = await Channel.findOneOrFail({ const channel = await Channel.findOneOrFail({
@ -66,6 +66,7 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
: undefined; : undefined;
const message = Message.create({ const message = Message.create({
...opts, ...opts,
poll: opts.poll,
sticker_items: stickers, sticker_items: stickers,
guild_id: channel.guild_id, guild_id: channel.guild_id,
channel_id: opts.channel_id, channel_id: opts.channel_id,
@ -116,6 +117,12 @@ 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)
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 (!guild.features.includes("CROSS_CHANNEL_REPLIES")) {
if (opts.message_reference.guild_id !== channel.guild_id) if (opts.message_reference.guild_id !== channel.guild_id)
throw new HTTPError( throw new HTTPError(
@ -126,6 +133,8 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
"You can only reference messages from this channel", "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 /** Q: should be checked if the referenced message exists? ANSWER: NO
otherwise backfilling won't work **/ otherwise backfilling won't work **/
@ -138,7 +147,9 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
!opts.content && !opts.content &&
!opts.embeds?.length && !opts.embeds?.length &&
!opts.attachments?.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); throw new HTTPError("Empty messages are not allowed", 50006);
} }
@ -213,6 +224,9 @@ export async function postHandleMessage(message: Message) {
const cachePromises = []; const cachePromises = [];
for (const link of links) { for (const link of links) {
// Don't embed links in <>
if (link.startsWith("<") && link.endsWith(">")) continue;
const url = new URL(link); const url = new URL(link);
const cached = await EmbedCache.findOne({ where: { 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"; import { join } from "path";
const defaultAvatarHashMap = new Map([ const defaultAvatarHashMap = new Map([
["0", "823a3de61c4dc2415cc4dbc38fca4299"], ["0", "4a8562cf00887030c416d3ec2d46385a"],
["1", "e56a89224be0b2b1f7c04eca975be468"], ["1", "9b0bb198936784c45c72833cc426cc55"],
["2", "0c8138dcc0dfe2689cdd73f7952c2475"], ["2", "22341bdb500c7b63a93bbce957d1601e"],
["3", "5ac2728593bb455250d11b848a0c36c6"], ["3", "d9977836b82058bf2f74eebd50edc095"],
["4", "addd2f3268df46459e1d6012ad8e75bd"], ["4", "9d6ddb4e4d899a533a8cc617011351c9"],
["5", "c4e0c8300fa491d94acfd2a1fb26cea8"], ["5", "7213ab6677377974697dfdfbaf5f6a6f"],
]); ]);
const defaultGroupDMAvatarHashMap = new Map([ const defaultGroupDMAvatarHashMap = new Map([

View File

@ -47,13 +47,15 @@ export default class BattleNetConnection extends Connection {
settings: BattleNetSettings = new BattleNetSettings(); settings: BattleNetSettings = new BattleNetSettings();
init(): void { init(): void {
const settings = this.settings = ConnectionLoader.getConnectionConfig<BattleNetSettings>(
ConnectionLoader.getConnectionConfig<BattleNetSettings>(
this.id, this.id,
this.settings, 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}`); 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(); settings: DiscordSettings = new DiscordSettings();
init(): void { init(): void {
const settings = ConnectionLoader.getConnectionConfig<DiscordSettings>( this.settings = ConnectionLoader.getConnectionConfig<DiscordSettings>(
this.id, this.id,
this.settings, 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}`); 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(); settings: EpicGamesSettings = new EpicGamesSettings();
init(): void { init(): void {
const settings = this.settings = ConnectionLoader.getConnectionConfig<EpicGamesSettings>(
ConnectionLoader.getConnectionConfig<EpicGamesSettings>(
this.id, this.id,
this.settings, 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}`); 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(); settings: FacebookSettings = new FacebookSettings();
init(): void { init(): void {
const settings = ConnectionLoader.getConnectionConfig<FacebookSettings>( this.settings = ConnectionLoader.getConnectionConfig<FacebookSettings>(
this.id, this.id,
this.settings, 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}`); 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(); settings: GitHubSettings = new GitHubSettings();
init(): void { init(): void {
const settings = ConnectionLoader.getConnectionConfig<GitHubSettings>( this.settings = ConnectionLoader.getConnectionConfig<GitHubSettings>(
this.id, this.id,
this.settings, 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}`); 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(); settings: RedditSettings = new RedditSettings();
init(): void { init(): void {
const settings = ConnectionLoader.getConnectionConfig<RedditSettings>( this.settings = ConnectionLoader.getConnectionConfig<RedditSettings>(
this.id, this.id,
this.settings, 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}`); 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. * So to prevent spamming the spotify api we disable the ability to refresh.
*/ */
this.refreshEnabled = false; this.refreshEnabled = false;
const settings = ConnectionLoader.getConnectionConfig<SpotifySettings>(
this.settings = ConnectionLoader.getConnectionConfig<SpotifySettings>(
this.id, this.id,
this.settings, 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}`); 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(); settings: TwitchSettings = new TwitchSettings();
init(): void { init(): void {
const settings = ConnectionLoader.getConnectionConfig<TwitchSettings>( this.settings = ConnectionLoader.getConnectionConfig<TwitchSettings>(
this.id, this.id,
this.settings, 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}`); 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(); settings: TwitterSettings = new TwitterSettings();
init(): void { init(): void {
const settings = ConnectionLoader.getConnectionConfig<TwitterSettings>( this.settings = ConnectionLoader.getConnectionConfig<TwitterSettings>(
this.id, this.id,
this.settings, 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}`); 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(); settings: XboxSettings = new XboxSettings();
init(): void { init(): void {
const settings = ConnectionLoader.getConnectionConfig<XboxSettings>( this.settings = ConnectionLoader.getConnectionConfig<XboxSettings>(
this.id, this.id,
this.settings, 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}`); 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(); settings: YoutubeSettings = new YoutubeSettings();
init(): void { init(): void {
const settings = ConnectionLoader.getConnectionConfig<YoutubeSettings>( this.settings = ConnectionLoader.getConnectionConfig<YoutubeSettings>(
this.id, this.id,
this.settings, 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}`); throw new Error(`Invalid settings for connection ${this.id}`);
} }

View File

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

View File

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

View File

@ -24,6 +24,7 @@ export class MinimalPublicUserDTO {
id: string; id: string;
public_flags: number; public_flags: number;
username: string; username: string;
badge_ids?: string[] | null;
constructor(user: User) { constructor(user: User) {
this.avatar = user.avatar; this.avatar = user.avatar;
@ -31,5 +32,6 @@ export class MinimalPublicUserDTO {
this.id = user.id; this.id = user.id;
this.public_flags = user.public_flags; this.public_flags = user.public_flags;
this.username = user.username; 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, PrimaryGeneratedColumn,
RelationId, RelationId,
} from "typeorm"; } from "typeorm";
import { Ban, PublicGuildRelations } from "."; import { Ban, Channel, PublicGuildRelations } from ".";
import { ReadyGuildDTO } from "../dtos"; import { ReadyGuildDTO } from "../dtos";
import { import {
GuildCreateEvent, GuildCreateEvent,
@ -330,6 +330,13 @@ export class Member extends BaseClassWithoutId {
relationLoadStrategy: "query", 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 memberCount = await Member.count({ where: { guild_id } });
const memberPreview = ( const memberPreview = (

View File

@ -218,6 +218,9 @@ export class Message extends BaseClass {
@Column({ type: "simple-json", nullable: true }) @Column({ type: "simple-json", nullable: true })
components?: MessageComponent[]; components?: MessageComponent[];
@Column({ type: "simple-json", nullable: true })
poll?: Poll;
toJSON(): Message { toJSON(): Message {
return { return {
...this, ...this,
@ -238,6 +241,7 @@ export class Message extends BaseClass {
activity: this.activity ?? undefined, activity: this.activity ?? undefined,
application: this.application ?? undefined, application: this.application ?? undefined,
components: this.components ?? undefined, components: this.components ?? undefined,
poll: this.poll ?? undefined,
content: this.content ?? "", content: this.content ?? "",
}; };
} }
@ -249,6 +253,7 @@ export interface MessageComponent {
label?: string; label?: string;
emoji?: PartialEmoji; emoji?: PartialEmoji;
custom_id?: string; custom_id?: string;
sku_id?: string;
url?: string; url?: string;
disabled?: boolean; disabled?: boolean;
components: MessageComponent[]; components: MessageComponent[];
@ -327,3 +332,32 @@ export interface AllowedMentions {
users?: string[]; users?: string[];
replied_user?: boolean; 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, premium_type,
theme_colors, theme_colors,
pronouns, pronouns,
badge_ids,
} }
export type PublicUserKeys = keyof typeof PublicUserEnum; export type PublicUserKeys = keyof typeof PublicUserEnum;
@ -231,6 +232,9 @@ export class User extends BaseClass {
@OneToMany(() => SecurityKey, (key: SecurityKey) => key.user) @OneToMany(() => SecurityKey, (key: SecurityKey) => key.user)
security_keys: SecurityKey[]; security_keys: SecurityKey[];
@Column({ type: "simple-array", nullable: true })
badge_ids?: string[];
// TODO: I don't like this method? // TODO: I don't like this method?
validate() { validate() {
if (this.discriminator) { if (this.discriminator) {

View File

@ -20,6 +20,7 @@ export * from "./Application";
export * from "./Attachment"; export * from "./Attachment";
export * from "./AuditLog"; export * from "./AuditLog";
export * from "./BackupCodes"; export * from "./BackupCodes";
export * from "./Badge";
export * from "./Ban"; export * from "./Ban";
export * from "./BaseClass"; export * from "./BaseClass";
export * from "./Categories"; 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/>. 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 = { type Attachment = {
id: string; id: string;
@ -42,7 +42,7 @@ export interface MessageCreateSchema {
}; };
message_reference?: { message_reference?: {
message_id: string; message_id: string;
channel_id: string; channel_id?: string;
guild_id?: string; guild_id?: string;
fail_if_not_exists?: boolean; fail_if_not_exists?: boolean;
}; };
@ -54,6 +54,21 @@ export interface MessageCreateSchema {
**/ **/
attachments?: Attachment[]; attachments?: Attachment[];
sticker_ids?: string[]; sticker_ids?: string[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any components?: MessageComponent[];
components?: any[]; // 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 { import {
Attachment, Attachment,
Embed, Embed,
MessageComponent,
MessageType, MessageType,
Poll,
PublicUser, PublicUser,
Role, Role,
} from "../../entities"; } from "../../entities";
@ -40,7 +42,8 @@ export interface GuildMessagesSearchMessage {
timestamp: string; timestamp: string;
edited_timestamp: string | null; edited_timestamp: string | null;
flags: number; flags: number;
components: unknown[]; components: MessageComponent[];
poll: Poll;
hit: true; hit: true;
} }

View File

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

View File

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

View File

@ -141,8 +141,9 @@ export const Email: {
*/ */
generateLink: async function (type, id, email) { generateLink: async function (type, id, email) {
const token = (await generateToken(id, email)) as string; 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 = const instanceUrl =
Config.get().general.frontPage || "http://localhost:3001"; Config.get().api.endpointPublic || "http://localhost:3001";
const link = `${instanceUrl}/${type}#token=${token}`; const link = `${instanceUrl}/${type}#token=${token}`;
return link; return link;
}, },
@ -187,7 +188,9 @@ export const Email: {
const message = { const message = {
from: from:
Config.get().general.correspondenceEmail || "noreply@localhost", Config.get().email.senderAddress ||
Config.get().general.correspondenceEmail ||
"noreply@localhost",
to: email, to: email,
subject, subject,
html, html,

View File

@ -27,9 +27,12 @@ export default async function () {
if (!host || !port || secure === null || !username || !password) if (!host || !port || secure === null || !username || !password)
return console.error("[Email] SMTP has not been configured correctly."); 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( 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 // construct the transporter