Merge branch 'master' of https://github.com/DEVTomatoCake/spacebar-server into feat/webhooks-3

This commit is contained in:
TomatoCake 2024-07-18 14:41:14 +02:00
commit 128b81bc6a
140 changed files with 2052 additions and 707 deletions

3
.github/CODEOWNERS vendored Normal file
View File

@ -0,0 +1,3 @@
# Nix stuff is owned by Rory& - we& want to be notified if these are changed.
/flake.nix root@rory.gay
/nix-update.sh root@rory.gay

18
.github/workflows/nix-build.yml vendored Normal file
View File

@ -0,0 +1,18 @@
name: Nix build
on:
push:
pull_request:
jobs:
build-nix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v25
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: DeterminateSystems/magic-nix-cache-action@v2
- run: nix build -L
- run: nix develop --command echo OK

1
.gitignore vendored
View File

@ -19,3 +19,4 @@ build
*.tmp *.tmp
tmp/ tmp/
dump/ dump/
result

View File

@ -1,4 +1,28 @@
#!/usr/bin/env sh #!/usr/bin/env sh
#!nix-shell -i "bash" -p bash prefetch-npm-deps jq nodejs nix-output-monitor
. "$(dirname -- "$0")/_/husky.sh" . "$(dirname -- "$0")/_/husky.sh"
# Check if nix is available
if [ -x "$(/usr/bin/env which nix-shell 2>/dev/null)" ]; then
# Check if we haven't re-executed ourselves yet
if [ ! "$HOOK_NIX_SHELL" ]; then
echo "Nix is available, updating nix flake..."
export HOOK_NIX_SHELL=1
nix-shell $0
exit $?
else
nix flake update
# run ./nix-update.sh if package lock has changed and has no unstaged changes
if [ -n "$(git status --porcelain=v1 2>/dev/null | grep -E '^(MM| M) package-lock.json')" ]; then
echo "package-lock.json has unstaged changes. Skipping update of nix dependencies."
elif [ ! -n "$(git status --porcelain=v1 2>/dev/null | grep -E '^M package-lock.json')" ]; then
echo "package-lock.json has no changes. Skipping update of nix dependencies."
else
./nix-update.sh || exit $?
fi
fi
else
echo "You do not appear to have nix installed. Skipping update of nix dependencies."
fi
npx -y lint-staged npx -y lint-staged

View File

@ -1,6 +1,5 @@
<!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" />
@ -26,29 +25,36 @@
</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,38 +71,50 @@
{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;
border-radius: 3px;
text-decoration: none; text-decoration: none;
color: white; color: white;
cursor: pointer; cursor: pointer;
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,6 +1,5 @@
<!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" />
@ -26,29 +25,36 @@
</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,22 +63,28 @@
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;
border-radius: 3px;
text-decoration: none; text-decoration: none;
color: white; color: white;
cursor: pointer; cursor: pointer;
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">
@ -80,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,12 +69,11 @@
> >
<a <a
class="btn" class="btn"
href="{emailVerificationUrl}" href="{actionUrl}"
target="_blank" target="_blank"
style=" style="
font-size: 15px; font-size: 15px;
border: none; border: none;
border-radius: 3px;
text-decoration: none; text-decoration: none;
color: white; color: white;
cursor: pointer; cursor: pointer;
@ -91,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

@ -1,16 +1,24 @@
{ {
"login": { "login": {
"INVALID_LOGIN": "E-Mail or Phone not found", "INVALID_LOGIN": "Ungültiger Benutzername/E-Mail oder Passwort.",
"INVALID_PASSWORD": "Invalid Password", "INVALID_PASSWORD": "Ungültiges Passwort",
"ACCOUNT_DISABLED": "This account is disabled" "ACCOUNT_DISABLED": "Dieser Account wurde deaktiviert",
"INVALID_TOTP_CODE": "Ungültiger Zwei-Faktor-Authentifierungs-Code.",
"INVALID_TOTP_SECRET": "Ungültiges Zwei-Faktor-Authentifierungs-Secret"
}, },
"register": { "register": {
"REGISTRATION_DISABLED": "New user registration is disabled", "REGISTRATION_DISABLED": "Die Registrierung von neuen Benutzern ist deaktiviert",
"INVITE_ONLY": "You must be invited to register", "INVITE_ONLY": "Du musst eingeladen werden, um dich registrieren zu können",
"EMAIL_INVALID": "Invalid Email", "EMAIL_INVALID": "Ungültige E-Mail-Adresse",
"EMAIL_ALREADY_REGISTERED": "Email is already registered", "EMAIL_ALREADY_REGISTERED": "E-Mail-Adresse ist bereits registriert",
"DATE_OF_BIRTH_UNDERAGE": "You need to be {{years}} years or older", "DATE_OF_BIRTH_UNDERAGE": "Du musst mindestens {{years}} Jahre oder älter sein",
"CONSENT_REQUIRED": "You must agree to the Terms of Service and Privacy Policy.", "PASSWORD_REQUIREMENTS_MIN_LENGTH": "Das Passwort muss mindestens {{min}} Zeichen lang sein.",
"USERNAME_TOO_MANY_USERS": "Too many users have this username, please try another" "CONSENT_REQUIRED": "Du musst den Nutzungsbedingungen und der Datenschutzerklärung zustimmen.",
"USERNAME_TOO_MANY_USERS": "Zu viele Nutzer haben diesen Benutzernamen, bitte probiere einen anderen",
"TOO_MANY_REGISTRATIONS": "Zu viele Registrierungen, bitte versuche es später erneut"
},
"password_reset": {
"EMAIL_DOES_NOT_EXIST": "E-Mail-Adresse existiert nicht.",
"INVALID_TOKEN": "Ungültiger Token."
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"login": { "login": {
"INVALID_LOGIN": "E-Mail or Phone not found", "INVALID_LOGIN": "Invalid login or password.",
"INVALID_PASSWORD": "Invalid Password", "INVALID_PASSWORD": "Invalid Password",
"ACCOUNT_DISABLED": "This account is disabled", "ACCOUNT_DISABLED": "This account is disabled",
"INVALID_TOTP_CODE": "Invalid two-factor code.", "INVALID_TOTP_CODE": "Invalid two-factor code.",
@ -12,9 +12,9 @@
"EMAIL_INVALID": "Invalid Email", "EMAIL_INVALID": "Invalid Email",
"EMAIL_ALREADY_REGISTERED": "Email is already registered", "EMAIL_ALREADY_REGISTERED": "Email is already registered",
"DATE_OF_BIRTH_UNDERAGE": "You need to be {{years}} years or older", "DATE_OF_BIRTH_UNDERAGE": "You need to be {{years}} years or older",
"PASSWORD_REQUIREMENTS_MIN_LENGTH": "The password must be at least {{min}} characters long.",
"CONSENT_REQUIRED": "You must agree to the Terms of Service and Privacy Policy.", "CONSENT_REQUIRED": "You must agree to the Terms of Service and Privacy Policy.",
"USERNAME_TOO_MANY_USERS": "Too many users have this username, please try another", "USERNAME_TOO_MANY_USERS": "Too many users have this username, please try another",
"GUESTS_DISABLED": "Guest users are disabled",
"TOO_MANY_REGISTRATIONS": "Too many registrations, please try again later" "TOO_MANY_REGISTRATIONS": "Too many registrations, please try again later"
}, },
"password_reset": { "password_reset": {

View File

@ -10,7 +10,7 @@
"EMAIL_INVALID": "Invalid Email", "EMAIL_INVALID": "Invalid Email",
"EMAIL_ALREADY_REGISTERED": "Email is already registered", "EMAIL_ALREADY_REGISTERED": "Email is already registered",
"DATE_OF_BIRTH_UNDERAGE": "You need to be {{years}} years or older", "DATE_OF_BIRTH_UNDERAGE": "You need to be {{years}} years or older",
"PASSWORD_REQUIREMENTS_MIN_LENGTH": "Must be at least {{min}} characters long.", "PASSWORD_REQUIREMENTS_MIN_LENGTH": "The password must be at least {{min}} characters long.",
"CONSENT_REQUIRED": "You must agree to the Terms of Service and Privacy Policy.", "CONSENT_REQUIRED": "You must agree to the Terms of Service and Privacy Policy.",
"USERNAME_TOO_MANY_USERS": "Too many users have this username, please try another" "USERNAME_TOO_MANY_USERS": "Too many users have this username, please try another"
} }

Binary file not shown.

View File

@ -1,38 +0,0 @@
const supportedLocales = [
"bg",
"cs",
"da",
"de",
"el",
"en-GB",
"es-ES",
"fi",
"fr",
"hi",
"hr",
"hu",
"it",
"ja",
"ko",
"lt",
"nl",
"no",
"pl",
"pt-BR",
"ro",
"ru",
"sv-SE",
"th",
"tr",
"uk",
"vi",
"zh-CN",
"zh-TW"
];
const settings = JSON.parse(window.localStorage.getItem("UserSettingsStore"));
if (settings && !supportedLocales.includes(settings.locale)) {
// fix client locale wrong and client not loading at all
settings.locale = "en-US";
window.localStorage.setItem("UserSettingsStore", JSON.stringify(settings));
}

View File

@ -1,9 +0,0 @@
// Fixes /oauth2 endpoints not requesting a CSS file
if (location.pathname.startsWith("/oauth2/")) {
const link = document.createElement("link");
link.rel = "stylesheet"
link.type = "text/css"
link.href = "/assets/40532.f7b1e10347ef10e790ac.css"
document.head.appendChild(link)
}

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

View File

@ -1,68 +0,0 @@
/* replace tos acceptance popup */
#app-mount > div:nth-child(7) > div > div > div.tooltipContent-bqVLWK {
visibility: hidden;
}
#app-mount > div:nth-child(7) > div > div > div.tooltipContent-bqVLWK::after {
visibility: visible;
display: block;
content: "You need to agree to this instance's rules to continue";
margin-top: -32px;
}
/* replace login header */
#app-mount > div.app-1q1i1E > div > div > div > div > form > div > div > div.mainLoginContainer-1ddwnR > h3 {
visibility: hidden;
}
h3.title-jXR8lp.marginBottom8-AtZOdT.base-1x0h_U.size24-RIRrxO::after {
margin-top: -32px;
content: "Welcome to Spacebar!";
visibility: visible;
display: block;
}
/* Logo in top left when bg removed */
#app-mount > div.app-1q1i1E > div > a {
/* replace me: original dimensions: 130x36 */
background: url(https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/svg/Spacebar__Logo-Blue.svg);
width: 130px;
height: 23px;
background-size: contain;
}
/* replace TOS text */
#app-mount
> div.app-1q1i1E
> div
> div
> div
> form
> div
> div
> div.flex-1xMQg5.flex-1O1GKY.horizontal-1ae9ci.horizontal-2EEEnY.flex-1O1GKY.directionRow-3v3tfG.justifyStart-2NDFzi.alignCenter-1dQNNs.noWrap-3jynv6.marginTop20-3TxNs6
> label
> div.label-cywgfr.labelClickable-11AuB8.labelForward-1wfipV
> * {
visibility: hidden;
}
#app-mount
> div.app-1q1i1E
> div
> div
> div
> form
> div
> div
> div.flex-1xMQg5.flex-1O1GKY.horizontal-1ae9ci.horizontal-2EEEnY.flex-1O1GKY.directionRow-3v3tfG.justifyStart-2NDFzi.alignCenter-1dQNNs.noWrap-3jynv6.marginTop20-3TxNs6
> label
> div.label-cywgfr.labelClickable-11AuB8.labelForward-1wfipV::after {
visibility: visible;
content: "I have read and agree with the rules set by this instance.";
display: block;
margin-top: -16px;
}
/* shrink login box to same size as register */
.authBoxExpanded-2jqaBe {
width: 480px !important;
}

View File

@ -1,92 +0,0 @@
/* loading spinner */
#app-mount > div.app-1q1i1E > div.container-16j22k.fixClipping-3qAKRb > div.content-1-zrf2 > video {
filter: opacity(1);
background: url("http://www.clipartbest.com/cliparts/7ca/6Rr/7ca6RrLAi.gif");
background-size: contain;
/* width: 64px;
height: 64px; */
padding-bottom: 64px;
background-repeat: no-repeat;
}
/* home button icon */
#app-mount
> div.app-1q1i1E
> div
> div.layers-3iHuyZ.layers-3q14ss
> div
> div
> nav
> ul
> div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih
> div.tutorialContainer-2sGCg9
> div
> div.listItemWrapper-KhRmzM
> div
> svg
> foreignObject
> div
> div {
background-image: url(https://raw.githubusercontent.com/spacebarchat/spacebarchat/master/branding/svg/Spacebar__Icon-Rounded-Subtract.svg);
background-size: contain;
border-radius: 50%;
}
#app-mount
> div.app-1q1i1E
> div
> div.layers-3iHuyZ.layers-3q14ss
> div
> div
> nav
> ul
> div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih
> div.tutorialContainer-2sGCg9
> div
> div.listItemWrapper-KhRmzM
> div
> svg
> foreignObject
> div
> div,
#app-mount
> div.app-1q1i1E
> div
> div.layers-3iHuyZ.layers-3q14ss
> div
> div
> nav
> ul
> div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih
> div.tutorialContainer-2sGCg9
> div
> div.listItemWrapper-KhRmzM
> div
> svg
> foreignObject
> div
> div:hover {
background-color: white;
}
/* Login QR */
#app-mount > div.app-1q1i1E > div > div > div > div > form > div > div > div.transitionGroup-aR7y1d.qrLogin-1AOZMt,
#app-mount > div.app-1q1i1E > div > div > div > div > form > div > div > div.verticalSeparator-3huAjp,
/* Remove login bg */
#app-mount > div.app-1q1i1E > div > svg,
/* Download bar */
#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > div > div.notice-3bPHh-.colorDefault-22HBa0,
/* Connection problem links */
#app-mount > div.app-1q1i1E > div.container-16j22k.fixClipping-3qAKRb > div.problems-3mgf6w.slideIn-sCvzGz > div:nth-child(2),
/* Downloads button */
#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > nav > ul > div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih > div:nth-child(7) > div.listItemWrapper-KhRmzM > div > svg > foreignObject > div,
#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > nav > ul > div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih > div:nth-child(6) > div,
/* help button */
#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > div > div.content-98HsJk > div.chat-3bRxxu > section > div.toolbar-1t6TWx > a,
/* download button start of guild */
#chat-messages-899316648933185083 > div > div > div:nth-child(5),
/* Thread permissions etc popups */
#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > div > div.content-98HsJk > div.sidebar-2K8pFh.hasNotice-1XRy4h > nav > div.container-3O_wAf,
/* home button icon */
#app-mount > div.app-1q1i1E > div > div.layers-3iHuyZ.layers-3q14ss > div > div > nav > ul > div.scroller-1Bvpku.none-2Eo-qx.scrollerBase-289Jih > div.tutorialContainer-2sGCg9 > div > div.listItemWrapper-KhRmzM > div > svg > foreignObject > div > div > svg {
display: none;
}

View File

@ -1 +0,0 @@
/* Your custom CSS goes here, enjoy! */

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>

Binary file not shown.

View File

@ -1,82 +0,0 @@
/*
This file is used to patch client version 134842 ( and probably a lot more ) to send additional info when using webrtc.
If you want to use it, throw it into the `preload-plugins` folder.
TODO: Make it so this file is not required for webrtc.
Do note that webrtc, as of 17/12/2022, is not implemented yet in spacebarchat/server.
*/
(this.webpackChunkdiscord_app = this.webpackChunkdiscord_app || []).push([
[[228974]],
{
632540: (module, exports, req) => {
window.find = (filter, options = {}) => {
const { cacheOnly = false } = options;
for (let i in req.c) {
if (req.c.hasOwnProperty(i)) {
let m = req.c[i].exports;
if (m && m.__esModule && m.default && filter(m.default)) return m.default;
if (m && filter(m)) return m;
}
}
if (cacheOnly) {
console.warn("Cannot find loaded module in cache");
return null;
}
console.warn("Cannot find loaded module in cache. Loading all modules may have unexpected side effects");
for (let i = 0; i < req.m.length; ++i) {
let m = req(i);
if (m && m.__esModule && m.default && filter(m.default)) return m.default;
if (m && filter(m)) return m;
}
console.warn("Cannot find module");
return null;
};
window.findByUniqueProperties = (propNames, options) =>
find((module) => propNames.every((prop) => module[prop] !== undefined), options);
window.findByDisplayName = (displayName, options) => find((module) => module.displayName === displayName, options);
window.req = req;
init();
}
},
(t) => t(632540)
]);
function retry(callback) {
return new Promise((resolve) => {
const interval = setInterval(() => {
const mod = callback();
if (!mod) return;
clearInterval(interval);
resolve(mod);
}, 50);
});
}
async function init() {
const SDP = await retry(() => findByUniqueProperties(["truncateSDP"]));
const StringManipulator = findByUniqueProperties(["uniq"]);
const truncateSDP = SDP.truncateSDP;
SDP.truncateSDP = (e) => {
const result = truncateSDP(e);
const i = result.codecs.find((x) => x.name === "VP8");
const a = new RegExp("^a=ice|a=extmap|opus|VP8|fingerprint|" + i?.rtxPayloadType + " rtx", "i");
return {
sdp: StringManipulator(e)
.split(/\r\n/)
.filter(function (e) {
return a.test(e);
})
.uniq()
.join("\n"),
codecs: result.codecs
};
};
// SDP.generateUnifiedSessionDescription = (e) => {
// console.log(e);
// return new RTCSessionDescription({ sdp: e.baseSDP.replace(/sendonly/g, "recvonly"), type: "answer" });
// };
}

BIN
flake.lock generated Normal file

Binary file not shown.

70
flake.nix Normal file
View File

@ -0,0 +1,70 @@
{
description = "Spacebar server, written in Typescript.";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachSystem flake-utils.lib.allSystems (system:
let
pkgs = import nixpkgs {
inherit system;
};
hashesFile = builtins.fromJSON (builtins.readFile ./hashes.json);
in rec {
packages.default = pkgs.buildNpmPackage {
pname = "spacebar-server-ts";
src = ./.;
name = "spacebar-server-ts";
nativeBuildInputs = with pkgs; [ python3 ];
npmDepsHash = hashesFile.npmDepsHash;
makeCacheWritable = true;
postPatch = ''
substituteInPlace package.json --replace 'npx patch-package' '${pkgs.nodePackages.patch-package}/bin/patch-package'
'';
installPhase = ''
runHook preInstall
set -x
#remove packages not needed for production, or at least try to...
npm prune --omit dev --no-save $npmInstallFlags "''${npmInstallFlagsArray[@]}" $npmFlags "''${npmFlagsArray[@]}"
find node_modules -maxdepth 1 -type d -empty -delete
mkdir -p $out/node_modules/
cp -r node_modules/* $out/node_modules/
cp -r dist/ $out/node_modules/@spacebar
for i in dist/**/start.js
do
makeWrapper ${pkgs.nodejs-slim}/bin/node $out/bin/start-`dirname ''${i/dist\//}` --prefix NODE_PATH : $out/node_modules --add-flags $out/node_modules/@spacebar`dirname ''${i/dist/}`/start.js
done
set +x
substituteInPlace package.json --replace 'dist/' 'node_modules/@spacebar/'
find $out/node_modules/@spacebar/ -type f -name "*.js" | while read srcFile; do
echo Patching imports in ''${srcFile/$out\/node_modules\/@spacebar//}...
substituteInPlace $srcFile --replace 'require("./' 'require(__dirname + "/'
substituteInPlace $srcFile --replace 'require("../' 'require(__dirname + "/../'
substituteInPlace $srcFile --replace ', "assets"' ', "..", "assets"'
#substituteInPlace $srcFile --replace 'require("@spacebar/' 'require("
done
set -x
cp -r assets/ $out/
cp package.json $out/
rm -v $out/assets/openapi.json
#rm -v $out/assets/schemas.json
#debug utils:
#cp $out/node_modules/@spacebar/ $out/build_output -r
set +x
runHook postInstall
'';
};
devShell = pkgs.mkShell {
buildInputs = with pkgs; [
nodejs
nodePackages.typescript
];
};
}
);
}

3
hashes.json Normal file
View File

@ -0,0 +1,3 @@
{
"npmDepsHash": "sha256-fZNDN2/fNy6Nu7tbr0RhQ8j4BP7X1Yhrh/fSTH7hbJc="
}

10
nix-update.sh Executable file
View File

@ -0,0 +1,10 @@
#!/usr/bin/env nix-shell
#!nix-shell -i "bash -x" -p bash prefetch-npm-deps jq git nix-output-monitor
nix flake update
DEPS_HASH=`prefetch-npm-deps package-lock.json`
TMPFILE=$(mktemp)
jq '.npm_deps_hash = "'$DEPS_HASH'"' hashes.json > $TMPFILE
mv -- "$TMPFILE" hashes.json
nom build .# || exit $?
git add hashes.json flake.lock flake.nix

BIN
package-lock.json generated

Binary file not shown.

View File

@ -10,6 +10,7 @@
"start:cdn": "node dist/cdn/start.js", "start:cdn": "node dist/cdn/start.js",
"start:gateway": "node dist/gateway/start.js", "start:gateway": "node dist/gateway/start.js",
"build": "tsc -p .", "build": "tsc -p .",
"watch": "tsc -w -p .",
"test": "node scripts/test.js", "test": "node scripts/test.js",
"lint": "eslint .", "lint": "eslint .",
"setup": "npm run build && npm run generate:schema", "setup": "npm run build && npm run generate:schema",
@ -55,9 +56,9 @@
"@types/probe-image-size": "^7.2.0", "@types/probe-image-size": "^7.2.0",
"@types/sharp": "^0.31.1", "@types/sharp": "^0.31.1",
"@types/ws": "^8.5.5", "@types/ws": "^8.5.5",
"@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^5.62.0", "@typescript-eslint/parser": "^6.21.0",
"eslint": "^8.46.0", "eslint": "^8.56.0",
"express": "^4.18.2", "express": "^4.18.2",
"husky": "^8.0.3", "husky": "^8.0.3",
"prettier": "^2.8.8", "prettier": "^2.8.8",
@ -116,9 +117,11 @@
}, },
"optionalDependencies": { "optionalDependencies": {
"erlpack": "^0.1.4", "erlpack": "^0.1.4",
"mysql": "^2.18.1",
"nodemailer-mailgun-transport": "^2.1.5", "nodemailer-mailgun-transport": "^2.1.5",
"nodemailer-mailjet-transport": "github:n0script22/nodemailer-mailjet-transport", "nodemailer-mailjet-transport": "github:n0script22/nodemailer-mailjet-transport",
"nodemailer-sendgrid-transport": "github:Maria-Golomb/nodemailer-sendgrid-transport", "nodemailer-sendgrid-transport": "github:Maria-Golomb/nodemailer-sendgrid-transport",
"pg": "^8.11.3",
"sqlite3": "^5.1.6" "sqlite3": "^5.1.6"
} }
} }

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";
@ -141,6 +141,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

@ -21,6 +21,7 @@ import {
Application, Application,
ApplicationModifySchema, ApplicationModifySchema,
DiscordApiErrors, DiscordApiErrors,
handleFile,
} from "@spacebar/util"; } from "@spacebar/util";
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
@ -83,6 +84,13 @@ router.patch(
) )
throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
if (body.icon) {
body.icon = await handleFile(
`/app-icons/${app.id}`,
body.icon as string,
);
}
if (app.bot) { if (app.bot) {
app.bot.assign({ bio: body.description }); app.bot.assign({ bio: body.description });
await app.bot.save(); await app.bot.save();

View File

@ -38,7 +38,7 @@ router.get(
"The length of each registration token. Defaults to 255.", "The length of each registration token. Defaults to 255.",
}, },
}, },
right: "OPERATOR", right: "CREATE_REGISTRATION_TOKENS",
responses: { 200: { body: "GenerateRegistrationTokensResponse" } }, responses: { 200: { body: "GenerateRegistrationTokensResponse" } },
}), }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {

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);
}, },
); );
@ -90,6 +96,24 @@ router.delete(
} else if (channel.type === ChannelType.GROUP_DM) { } else if (channel.type === ChannelType.GROUP_DM) {
await Channel.removeRecipientFromChannel(channel, req.user_id); await Channel.removeRecipientFromChannel(channel, req.user_id);
} else { } else {
if (channel.type == ChannelType.GUILD_CATEGORY) {
const channels = await Channel.find({
where: { parent_id: channel_id },
});
for await (const c of channels) {
c.parent_id = null;
await Promise.all([
c.save(),
emitEvent({
event: "CHANNEL_UPDATE",
data: c,
channel_id: c.id,
} as ChannelUpdateEvent),
]);
}
}
await Promise.all([ await Promise.all([
Channel.delete({ id: channel_id }), Channel.delete({ id: channel_id }),
emitEvent({ emitEvent({

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

@ -19,7 +19,6 @@
import { getIpAdress, route } from "@spacebar/api"; import { getIpAdress, route } from "@spacebar/api";
import { import {
Ban, Ban,
BanModeratorSchema,
BanRegistrySchema, BanRegistrySchema,
DiscordApiErrors, DiscordApiErrors,
GuildBanAddEvent, GuildBanAddEvent,
@ -82,7 +81,7 @@ router.get(
); );
router.get( router.get(
"/:user", "/:user_id",
route({ route({
permission: "BAN_MEMBERS", permission: "BAN_MEMBERS",
responses: { responses: {
@ -98,23 +97,21 @@ router.get(
}, },
}), }),
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id } = req.params; const { guild_id, user_id } = req.params;
const user_id = req.params.ban;
let ban = (await Ban.findOneOrFail({ const ban = (await Ban.findOneOrFail({
where: { guild_id: guild_id, user_id: user_id }, where: { guild_id: guild_id, user_id: user_id },
})) as BanRegistrySchema; })) as BanRegistrySchema;
if (ban.user_id === ban.executor_id) throw DiscordApiErrors.UNKNOWN_BAN; if (ban.user_id === ban.executor_id) throw DiscordApiErrors.UNKNOWN_BAN;
// pretend self-bans don't exist to prevent victim chasing // pretend self-bans don't exist to prevent victim chasing
/* Filter secret from registry. */ const banInfo = {
user: await User.getPublicUser(ban.user_id),
reason: ban.reason,
};
ban = ban as BanModeratorSchema; return res.json(banInfo);
delete ban.ip;
return res.json(ban);
}, },
); );
@ -151,6 +148,12 @@ router.put(
if (req.permission?.cache.guild?.owner_id === banned_user_id) if (req.permission?.cache.guild?.owner_id === banned_user_id)
throw new HTTPError("You can't ban the owner", 400); throw new HTTPError("You can't ban the owner", 400);
const existingBan = await Ban.findOne({
where: { guild_id: guild_id, user_id: banned_user_id },
});
// Bans on already banned users are silently ignored
if (existingBan) return res.status(204).send();
const banned_user = await User.getPublicUser(banned_user_id); const banned_user = await User.getPublicUser(banned_user_id);
const ban = Ban.create({ const ban = Ban.create({
@ -174,59 +177,7 @@ router.put(
} as GuildBanAddEvent), } as GuildBanAddEvent),
]); ]);
return res.json(ban); return res.status(204).send();
},
);
router.put(
"/@me",
route({
requestBody: "BanCreateSchema",
responses: {
200: {
body: "Ban",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const banned_user = await User.getPublicUser(req.params.user_id);
if (req.permission?.cache.guild?.owner_id === req.params.user_id)
throw new HTTPError(
"You are the guild owner, hence can't ban yourself",
403,
);
const ban = Ban.create({
user_id: req.params.user_id,
guild_id: guild_id,
ip: getIpAdress(req),
executor_id: req.params.user_id,
reason: req.body.reason, // || otherwise empty
});
await Promise.all([
Member.removeFromGuild(req.user_id, guild_id),
ban.save(),
emitEvent({
event: "GUILD_BAN_ADD",
data: {
guild_id: guild_id,
user: banned_user,
},
guild_id: guild_id,
} as GuildBanAddEvent),
]);
return res.json(ban);
}, },
); );
@ -247,13 +198,10 @@ router.delete(
async (req: Request, res: Response) => { async (req: Request, res: Response) => {
const { guild_id, user_id } = req.params; const { guild_id, user_id } = req.params;
const ban = await Ban.findOneOrFail({ await Ban.findOneOrFail({
where: { guild_id: guild_id, user_id: user_id }, where: { guild_id: guild_id, user_id: user_id },
}); });
if (ban.user_id === ban.executor_id) throw DiscordApiErrors.UNKNOWN_BAN;
// make self-bans irreversible and hide them from view to avoid victim chasing
const banned_user = await User.getPublicUser(user_id); const banned_user = await User.getPublicUser(user_id);
await Promise.all([ await Promise.all([

View File

@ -0,0 +1,130 @@
/*
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 { getIpAdress, route } from "@spacebar/api";
import {
Ban,
DiscordApiErrors,
GuildBanAddEvent,
Member,
User,
emitEvent,
} from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
const router: Router = Router();
router.post(
"/",
route({
requestBody: "BulkBanSchema",
permission: ["BAN_MEMBERS", "MANAGE_GUILD"],
responses: {
200: {
body: "Ban",
},
400: {
body: "APIErrorResponse",
},
403: {
body: "APIErrorResponse",
},
},
}),
async (req: Request, res: Response) => {
const { guild_id } = req.params;
const userIds: Array<string> = req.body.user_ids;
if (!userIds) throw new HTTPError("The user_ids array is missing", 400);
if (userIds.length > 200)
throw new HTTPError(
"The user_ids array must be between 1 and 200 in length",
400,
);
const banned_users = [];
const failed_users = [];
for await (const banned_user_id of userIds) {
if (
req.user_id === banned_user_id &&
banned_user_id === req.permission?.cache.guild?.owner_id
) {
failed_users.push(banned_user_id);
continue;
}
if (req.permission?.cache.guild?.owner_id === banned_user_id) {
failed_users.push(banned_user_id);
continue;
}
const existingBan = await Ban.findOne({
where: { guild_id: guild_id, user_id: banned_user_id },
});
if (existingBan) {
failed_users.push(banned_user_id);
continue;
}
let banned_user;
try {
banned_user = await User.getPublicUser(banned_user_id);
} catch {
failed_users.push(banned_user_id);
continue;
}
const ban = Ban.create({
user_id: banned_user_id,
guild_id: guild_id,
ip: getIpAdress(req),
executor_id: req.user_id,
reason: req.body.reason, // || otherwise empty
});
try {
await Promise.all([
Member.removeFromGuild(banned_user_id, guild_id),
ban.save(),
emitEvent({
event: "GUILD_BAN_ADD",
data: {
guild_id: guild_id,
user: banned_user,
},
guild_id: guild_id,
} as GuildBanAddEvent),
]);
banned_users.push(banned_user_id);
} catch {
failed_users.push(banned_user_id);
continue;
}
}
if (banned_users.length === 0 && failed_users.length > 0)
throw DiscordApiErrors.BULK_BAN_FAILED;
return res.json({
banned_users: banned_users,
failed_users: failed_users,
});
},
);
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

@ -113,9 +113,6 @@ router.patch(
relations: ["roles", "user"], relations: ["roles", "user"],
}); });
const permission = await getPermission(req.user_id, guild_id); const permission = await getPermission(req.user_id, guild_id);
const everyone = await Role.findOneOrFail({
where: { guild_id: guild_id, name: "@everyone", position: 0 },
});
if ("nick" in body) { if ("nick" in body) {
permission.hasThrow("MANAGE_NICKNAMES"); permission.hasThrow("MANAGE_NICKNAMES");
@ -152,15 +149,14 @@ router.patch(
body.roles = body.roles || []; body.roles = body.roles || [];
body.roles.filter((x) => !!x); body.roles.filter((x) => !!x);
if (body.roles.indexOf(everyone.id) === -1) if (body.roles.indexOf(guild_id) === -1) body.roles.push(guild_id);
body.roles.push(everyone.id);
// foreign key constraint will fail if role doesn't exist // foreign key constraint will fail if role doesn't exist
member.roles = body.roles.map((x) => Role.create({ id: x })); member.roles = body.roles.map((x) => Role.create({ id: x }));
} }
await member.save(); await member.save();
member.roles = member.roles.filter((x) => x.id !== everyone.id); member.roles = member.roles.filter((x) => x.id !== guild_id);
// do not use promise.all as we have to first write to db before emitting the event to catch errors // do not use promise.all as we have to first write to db before emitting the event to catch errors
await emitEvent({ await emitEvent({

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,45 +16,49 @@
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, Attachment,
Config, Config,
Sticker, Sticker,
MessageCreateSchema, MessageCreateSchema,
EmbedCache, EmbedCache,
handleFile,
Permissions,
} from "@spacebar/util"; } from "@spacebar/util";
import { HTTPError } from "lambert-server"; import { HTTPError } from "lambert-server";
import { In } from "typeorm"; import { In } from "typeorm";
import { EmbedHandlers } from "@spacebar/api"; import { EmbedHandlers } from "@spacebar/api";
import * as Sentry from "@sentry/node"; import * as Sentry from "@sentry/node";
import fetch from "node-fetch";
const allow_empty = false; const allow_empty = false;
// TODO: check webhook, application, system author, stickers // TODO: check webhook, application, system author, stickers
// TODO: embed gifs/videos/images // TODO: embed gifs/videos/images
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({
@ -69,6 +73,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,
@ -185,7 +190,6 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
otherwise backfilling won't work **/ otherwise backfilling won't work **/
message.type = MessageType.REPLY; message.type = MessageType.REPLY;
} }
}
// TODO: stickers/activity // TODO: stickers/activity
if ( if (
@ -193,7 +197,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);
} }
@ -272,6 +278,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

@ -90,19 +90,23 @@ export function route(opts: RouteOptions) {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
if (opts.permission) { if (opts.permission) {
const required = new Permissions(opts.permission);
req.permission = await getPermission( req.permission = await getPermission(
req.user_id, req.user_id,
req.params.guild_id, req.params.guild_id,
req.params.channel_id, req.params.channel_id,
); );
const requiredPerms = Array.isArray(opts.permission)
? opts.permission
: [opts.permission];
requiredPerms.forEach((perm) => {
// bitfield comparison: check if user lacks certain permission // bitfield comparison: check if user lacks certain permission
if (!req.permission.has(required)) { if (!req.permission!.has(new Permissions(perm))) {
throw DiscordApiErrors.MISSING_PERMISSIONS.withParams( throw DiscordApiErrors.MISSING_PERMISSIONS.withParams(
opts.permission as string, perm as string,
); );
} }
});
} }
if (opts.right) { if (opts.right) {

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([
@ -63,7 +63,15 @@ router.get("/avatars/:id", async (req: Request, res: Response) => {
id = id.split(".")[0]; // remove .file extension id = id.split(".")[0]; // remove .file extension
const hash = defaultAvatarHashMap.get(id); const hash = defaultAvatarHashMap.get(id);
if (!hash) throw new HTTPError("not found", 404); if (!hash) throw new HTTPError("not found", 404);
const path = join(process.cwd(), "assets", "public", `${hash}.png`); const path = join(
__dirname,
"..",
"..",
"..",
"assets",
"public",
`${hash}.png`,
);
const file = await getFile(path); const file = await getFile(path);
if (!file) throw new HTTPError("not found", 404); if (!file) throw new HTTPError("not found", 404);
@ -80,7 +88,15 @@ router.get("/group-avatars/:id", async (req: Request, res: Response) => {
id = id.split(".")[0]; // remove .file extension id = id.split(".")[0]; // remove .file extension
const hash = defaultGroupDMAvatarHashMap.get(id); const hash = defaultGroupDMAvatarHashMap.get(id);
if (!hash) throw new HTTPError("not found", 404); if (!hash) throw new HTTPError("not found", 404);
const path = join(process.cwd(), "assets", "public", `${hash}.png`); const path = join(
__dirname,
"..",
"..",
"..",
"assets",
"public",
`${hash}.png`,
);
const file = await getFile(path); const file = await getFile(path);
if (!file) throw new HTTPError("not found", 404); if (!file) throw new HTTPError("not found", 404);

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

@ -439,6 +439,10 @@ export async function onIdentify(this: WebSocket, data: Payload) {
tutorial: null, tutorial: null,
session_type: "normal", // TODO session_type: "normal", // TODO
auth_session_id_hash: "", // TODO auth_session_id_hash: "", // TODO
notification_settings: {
// ????
flags: 0,
},
}; };
// Send READY // Send READY

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

@ -22,7 +22,7 @@ import path from "path";
import { ConnectionConfig } from "./ConnectionConfig"; import { ConnectionConfig } from "./ConnectionConfig";
import { ConnectionStore } from "./ConnectionStore"; import { ConnectionStore } from "./ConnectionStore";
const root = "dist/connections"; const root = path.join(__dirname, "..", "..", "connections");
const connectionsLoaded = false; const connectionsLoaded = false;
export class ConnectionLoader { export class ConnectionLoader {

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

@ -105,7 +105,7 @@ export class Channel extends BaseClass {
@Column({ nullable: true }) @Column({ nullable: true })
@RelationId((channel: Channel) => channel.parent) @RelationId((channel: Channel) => channel.parent)
parent_id: string; parent_id: string | null;
@JoinColumn({ name: "parent_id" }) @JoinColumn({ name: "parent_id" })
@ManyToOne(() => Channel) @ManyToOne(() => Channel)

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

@ -192,8 +192,8 @@ export class Message extends BaseClass {
party_id: string; party_id: string;
}; };
@Column({ nullable: true }) @Column({ default: 0 })
flags?: number; flags: number;
@Column({ type: "simple-json", nullable: true }) @Column({ type: "simple-json", nullable: true })
message_reference?: { message_reference?: {
@ -252,6 +252,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 ?? "",
}; };
} }
@ -263,6 +264,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[];
@ -341,3 +343,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

@ -63,6 +63,9 @@ export class UserSettings extends BaseClassWithoutId {
@Column({ nullable: true }) @Column({ nullable: true })
explicit_content_filter: number = 0; explicit_content_filter: number = 0;
@Column({ nullable: true })
friend_discovery_flags: number = 0;
@Column({ nullable: true, type: "simple-json" }) @Column({ nullable: true, type: "simple-json" })
friend_source_flags: FriendSourceFlags = { all: true }; friend_source_flags: FriendSourceFlags = { all: true };
@ -116,6 +119,9 @@ export class UserSettings extends BaseClassWithoutId {
@Column({ nullable: true }) @Column({ nullable: true })
timezone_offset: number = 0; // e.g -60 timezone_offset: number = 0; // e.g -60
@Column({ nullable: true })
view_nsfw_guilds: boolean = true;
} }
interface CustomStatus { interface CustomStatus {

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

@ -129,6 +129,9 @@ export interface ReadyEventData {
| "REQUIRE_CAPTCHA" // TODO: allow these to be triggered | "REQUIRE_CAPTCHA" // TODO: allow these to be triggered
| "TOS_UPDATE_ACKNOWLEDGMENT" | "TOS_UPDATE_ACKNOWLEDGMENT"
| "AGREEMENTS"; | "AGREEMENTS";
notification_settings: {
flags: number;
};
} }
export interface ReadyEvent extends Event { export interface ReadyEvent extends Event {

View File

@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MessageFlagsNotNull1713116476900 implements MigrationInterface {
name = "MessageFlagsNotNull1713116476900";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
"ALTER TABLE `messages` CHANGE flags flags_old integer;",
);
await queryRunner.query(
"ALTER TABLE `messages` ADD flags integer NOT NULL DEFAULT 0;",
);
await queryRunner.query(
"UPDATE `messages` SET flags = IFNULL(flags_old, 0);",
);
await queryRunner.query(
"ALTER TABLE `messages` DROP COLUMN flags_old;",
);
}
public async down(): Promise<void> {
// dont care
throw new Error("Migration down is not implemented.");
}
}

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,25 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MessageFlagsNotNull1713116476900 implements MigrationInterface {
name = "MessageFlagsNotNull1713116476900";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
"ALTER TABLE `messages` CHANGE flags flags_old integer;",
);
await queryRunner.query(
"ALTER TABLE `messages` ADD flags integer NOT NULL DEFAULT 0;",
);
await queryRunner.query(
"UPDATE `messages` SET flags = IFNULL(flags_old, 0);",
);
await queryRunner.query(
"ALTER TABLE `messages` DROP COLUMN flags_old;",
);
}
public async down(): Promise<void> {
// dont care
throw new Error("Migration down is not implemented.");
}
}

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 MessageFlagsNotNull1713116476900 implements MigrationInterface {
name = "MessageFlagsNotNull1713116476900";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
"ALTER TABLE messages RENAME COLUMN flags TO flags_old;",
);
await queryRunner.query(
"ALTER TABLE messages ADD COLUMN flags integer NOT NULL DEFAULT 0;",
);
await queryRunner.query(
"UPDATE messages SET flags = COALESCE(flags_old, 0);",
);
await queryRunner.query("ALTER TABLE messages DROP COLUMN flags_old;");
}
public async down(): Promise<void> {
// dont care
throw new Error("Migration down is not implemented.");
}
}

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

@ -0,0 +1,22 @@
/*
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/>.
*/
export interface BulkBanSchema {
user_ids: string[];
delete_message_seconds?: number;
}

Some files were not shown because too many files have changed in this diff Show More