add buffer support to rpc and add profile pictures

This commit is contained in:
hippoz 2023-02-25 02:35:12 +02:00
parent 622f3c8e63
commit afb046b3b6
Signed by: hippoz
GPG key ID: 56C4E02A85F2FBED
31 changed files with 728 additions and 112 deletions

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
node_modules/
dist/
frontend-new/
uploads/avatar/*.webp
.env

View file

@ -1,5 +1,6 @@
<script>
import { overlayStore, OverlayType, setMessageInputEvent } from "../stores";
import { avatarUrl } from "../storage";
import { overlayStore, OverlayType, setMessageInputEvent } from "../stores";
export let message;
@ -19,11 +20,26 @@
<style>
.message {
position: relative;
display: flex;
flex-direction: row;
padding: 4px 4px 4px var(--space-xs);
margin-top: 16px;
overflow-x: hidden;
word-wrap: break-word;
padding: 3px 3px 3px var(--space-normplus);
margin-top: 16px;
position: relative;
}
.avatar-container {
margin-top: 5px;
margin-left: var(--space-sm);
margin-right: var(--space-sm);
width: 32px;
}
.avatar {
border-radius: 50%;
width: 32px;
height: 32px;
}
.message:hover, .message.pinged {
@ -97,7 +113,18 @@
}
</style>
<div class="message" class:clumped={ message._clumped } class:has-children={ message._hasChildren } class:pinged={ message._mentions }>
<div class="message" class:clumped={ message._clumped } class:pinged={ message._mentions }>
<div class="avatar-container">
{#if !message._clumped}
{#if message.author_avatar}
<img class="avatar" src={avatarUrl(message.author_avatar, 32)} alt=" ">
{:else}
<span class="material-icons-outlined circled-icon">alternate_email</span>
{/if}
{/if}
</div>
<div class="message-body">
{#if !message._clumped}
<div class="author-group">
<span class="author" class:author-more={message._viaBadge}>{ message._effectiveAuthor }</span>
@ -119,4 +146,5 @@
</button>
{/if}
</div>
</div>
</div>

View file

@ -117,7 +117,7 @@
border-radius: var(--radius-md);
padding: var(--space-sm);
font-size: inherit;
line-height: inherit;
line-height: 24px;
resize: none;
contain: strict;
}
@ -128,6 +128,7 @@
.message-input::placeholder {
color: var(--foreground-color-4);
font-weight: 300;
}
.send-button {
@ -155,6 +156,7 @@
.typing-info-container {
padding-left: var(--space-xxs);
user-select: none;
}
.typing-list {

View file

@ -2,21 +2,30 @@
import { quadInOut } from "svelte/easing";
import { maybeFly, maybeFlyIf } from "../animations";
import { overlayStore, OverlayType, presenceStore, showPresenceSidebar, smallViewport } from "../stores";
import UserView from "./UserView.svelte";
const close = () => {
$showPresenceSidebar = false;
};
</script>
<style>
.online-label {
color: var(--foreground-color-4);
padding-top: var(--space-md);
padding: var(--space-sm);
padding-bottom: 0;
font-weight: 700;
font-size: var(--h6);
}
</style>
<div class="sidebar-container" in:maybeFly="{{ duration: 175, easing: quadInOut, x: 10 }}" out:maybeFlyIf="{{ _condition: !smallViewport.value, duration: 175, easing: quadInOut, x: -10 }}">
<div class="top-bar">
<span class="input-label">User List</span>
</div>
<span class="online-label">ONLINE</span>
<div class="sidebar">
{#each $presenceStore as entry (entry.user.id)}
<button class="sidebar-button">
<div class="material-icons-outlined">alternate_email</div>
<span class="sidebar-button-text">{ entry.user.username }</span>
<UserView size={28} user={entry.user}></UserView>
{#if entry.bridgesTo || entry.privacy || entry.terms}
<span class="user-badge" on:click={ () => overlayStore.push(OverlayType.UserInfo, { presenceEntry: entry }) }>SERVICE</span>
{/if}

View file

@ -2,7 +2,7 @@
import { quadInOut } from "svelte/easing";
import { maybeFly, maybeFlyIf } from "../animations";
import { channels, gatewayStatus, overlayStore, selectedChannel, showSidebar, smallViewport, userInfoStore, unreadStore, OverlayType } from "../stores";
import UserTopBar from "./UserTopBar.svelte";
import UserView from "./UserView.svelte";
const selectChannel = (channel) => {
if ($smallViewport) {
@ -14,7 +14,9 @@
</script>
<div class="sidebar-container" in:maybeFly="{{ duration: 175, easing: quadInOut, x: -10 }}" out:maybeFlyIf="{{ _condition: !smallViewport.value, duration: 175, easing: quadInOut, x: -10 }}">
<UserTopBar />
<div class="top-bar text-small text-bold">
<UserView size={28} user={$userInfoStore}></UserView>
</div>
<div class="sidebar">
{#each $channels as channel (channel.id)}
<button on:click="{ selectChannel(channel) }" class="sidebar-button" class:selected={ channel.id === $selectedChannel.id }>

View file

@ -1,8 +0,0 @@
<script>
import { userInfoStore } from "../stores";
</script>
<div class="top-bar">
<span class="material-icons-outlined">alternate_email</span>
<span class="text-small top-bar-heading accent">{ $userInfoStore ? $userInfoStore.username : "" }</span>
</div>

View file

@ -0,0 +1,38 @@
<script>
import { avatarUrl } from "../storage";
export let user = null;
export let size = 32;
</script>
<style>
div {
display: flex;
align-items: center;
justify-content: center;
}
img {
border-radius: 50%;
min-width: 16px;
min-height: 16px;
max-width: 40px;
max-height: 40px;
display: block;
}
.username {
margin-left: var(--space-xs);
display: block;
}
</style>
<div>
{#if user && user.avatar}
<img src={avatarUrl(user.avatar, size)} alt=" ">
{:else}
<span class="material-icons-outlined circled-icon">alternate_email</span>
{/if}
<span class="username">{ user ? user.username : "" }</span>
</div>

View file

@ -2,10 +2,12 @@
import { overlayStore, userInfoStore, smallViewport, theme, doAnimations, OverlayType } from "../../stores";
import { logOut } from "../../auth";
import { maybeModalFade, maybeModalScale } from "../../animations";
import request from "../../request";
import request, { methods, remoteBlobUpload, remoteCall } from "../../request";
import { apiRoute, getItem } from "../../storage";
import UserView from "../UserView.svelte";
export let close = () => {};
let avatarFileInput;
const doSuperuserPrompt = async () => {
const { ok } = await request("POST", apiRoute("users/self/promote"), true);
@ -45,6 +47,24 @@
});
};
const onAvatarFileChange = async () => {
if (!avatarFileInput || !avatarFileInput.files || !avatarFileInput.files[0]) return;
const file = avatarFileInput.files[0];
const { ok } = await remoteBlobUpload(methods.putUserAvatar, file);
if (ok) {
overlayStore.toast("Your avatar has been updated");
} else {
overlayStore.toast("Failed to upload avatar");
}
};
const openAvatarInput = () => {
if (avatarFileInput) {
avatarFileInput.click();
}
}
const doLogout = () => {
close();
logOut();
@ -103,9 +123,10 @@
<div class="modal-content">
<span class="input-label" on:click={ doDeveloper }>Account</span>
<div class="settings-card full-width">
<span class="material-icons-outlined">alternate_email</span>
<span class="h5 top-bar-heading">{ $userInfoStore ? $userInfoStore.username : "" }</span>
<UserView user={$userInfoStore}></UserView>
<input type="file" style="display: none;" accept="image/png, image/jpeg, image/webp" name="avatar-upload" multiple={false} bind:this={avatarFileInput} on:change={onAvatarFileChange}>
<div class="account-buttons">
<button class="button" on:click="{ openAvatarInput }">Update Avatar</button>
<button class="button button-danger" on:click="{ doLogout }">Log Out</button>
</div>
</div>

View file

@ -32,7 +32,9 @@ export const GatewayPayloadType = {
TypingStart: 130,
PresenceUpdate: 140
PresenceUpdate: 140,
UserUpdate: 150,
}
export const GatewayEventType = {
@ -164,8 +166,17 @@ export default {
return true;
},
send(data) {
return this.ws.send(JSON.stringify(data));
send(jsonPayload, binaryData=null) {
const jsonString = JSON.stringify(jsonPayload);
if (binaryData) {
const dataBlob = new Blob([jsonString, "\n", binaryData], {
type: "application/octet-stream"
});
console.log(dataBlob);
this.ws.send(dataBlob);
} else {
this.ws.send(jsonString);
}
},
dispatch(event, payload) {
const eventHandlers = this.handlers.get(event);
@ -195,14 +206,14 @@ export default {
this.handlers.delete(event);
}
},
sendRPCRequest(calls, isSignal) {
sendRPCRequest(calls, isSignal, binaryData=null) {
return new Promise((resolve, _reject) => {
this.waitingSerials.set(this.serial, resolve);
this.send({
t: isSignal ? GatewayPayloadType.RPCSignal : GatewayPayloadType.RPCRequest,
d: calls,
s: this.serial
});
}, binaryData);
this.serial++;
});
},

View file

@ -10,17 +10,18 @@ export const methods = {
loginUser: method(1, false),
getUserSelf: withCacheable(method(2, true)),
promoteUserSelf: method(3, true),
createChannel: method(4, true),
updateChannelName: method(5, true),
deleteChannel: method(6, true),
getChannel: withCacheable(method(7, true)),
getChannels: withCacheable(method(8, true)),
createChannelMessage: method(9, true),
getChannelMessages: withCacheable(method(10, true)),
putChannelTyping: method(11, true),
deleteMessage: method(12, true),
updateMessageContent: method(13, true),
getMessage: withCacheable(method(14, true))
putUserAvatar: method(4, true),
createChannel: method(5, true),
updateChannelName: method(6, true),
deleteChannel: method(7, true),
getChannel: withCacheable(method(8, true)),
getChannels: withCacheable(method(9, true)),
createChannelMessage: method(10, true),
getChannelMessages: withCacheable(method(11, true)),
putChannelTyping: method(12, true),
deleteMessage: method(13, true),
updateMessageContent: method(14, true),
getMessage: withCacheable(method(15, true))
};
export function compatibleFetch(endpoint, options) {
@ -122,3 +123,17 @@ export async function remoteSignal(method, ...args) {
_isSignal: true
}, ...args);
}
export async function remoteBlobUpload({methodId, requiresAuthentication, _isSignal=false}, blob) {
const calls = [[methodId, [0, blob.size]]];
if (requiresAuthentication && gateway.authenticated) {
const replies = await gateway.sendRPCRequest(calls, _isSignal, blob);
const ok = Array.isArray(replies) && !(replies[0] && replies[0].code);
return {
json: ok ? replies[0] : null,
ok
};
} else {
return { json: null, ok: false };
}
}

View file

@ -1,6 +1,7 @@
const defaults = {
"server:apiBase": `${window.location.origin || ""}/api/v1`,
"server:gatewayBase": `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/gateway`,
"server:avatarsBase": `${window.location.origin || ""}/uploads/avatar`,
"auth:token": "",
"ui:doAnimations": true,
"ui:theme": "dark",
@ -89,3 +90,7 @@ export function init() {
export function apiRoute(fragment) {
return `${getItem("server:apiBase")}/${fragment}`
}
export function avatarUrl(avatarId, size) {
return `${getItem("server:avatarsBase")}/${avatarId}_${size}.webp`;
}

View file

@ -205,6 +205,14 @@ class UserInfoStore extends Store {
this.value = user;
this.updated();
});
gateway.subscribe(GatewayEventType.UserUpdate, (user) => {
console.log(user);
if (this.value && this.value.id === user.id) {
this.value = user;
this.updated();
}
});
}
}
@ -247,7 +255,6 @@ class MessageStore extends Store {
}
if (previous && (message._createdAtDate.getTime() - previous._createdAtDate.getTime()) <= 100 * 1000 && message.author_id === previous.author_id && message._effectiveAuthor === previous._effectiveAuthor && message._viaBadge === previous._viaBadge) {
message._clumped = true;
previous._hasChildren = true;
}
if (!previous || (previous._createdAtDateString !== message._createdAtDateString && !message._aboveDateMarker)) {
message._aboveDateMarker = new Intl.DateTimeFormat(getItem("ui:locale"), { month: "long", day: "numeric", year: "numeric" }).format(message._createdAtDate);
@ -556,6 +563,14 @@ class PresenceStore extends Store {
gateway.subscribe(GatewayEventType.PresenceUpdate, (data) => {
this.ingestPresenceUpdate(data);
});
gateway.subscribe(GatewayEventType.UserUpdate, (data) => {
const entry = this.entryIndexByUserId(data.id);
if (entry === -1) return;
this.value[entry].user.username = data.username;
this.value[entry].user.avatar = data.avatar;
this.updated();
});
}
}

View file

@ -143,8 +143,7 @@ body {
justify-content: left;
width: 100%;
padding: var(--space-sm);
flex-grow: 0;
flex-shrink: 0;
height: 56px;
}
.top-bar-heading {
@ -413,6 +412,7 @@ body {
.h4 {font-size: var(--h4)}
.h5 {font-size: var(--h5)}
.text-small {font-size: var(--h6)}
.text-bold {font-weight: 700}
/* sidebar */
@ -484,6 +484,7 @@ body {
.sidebar-button.selected {
color: var(--foreground-color-1);
background-color: var(--background-color-2);
font-weight: 400;
}
.sidebar-button.selected .icon-button {
@ -495,6 +496,16 @@ body {
color: var(--foreground-special-color-1);
}
.material-icons-outlined.circled-icon {
display: block;
width: 32px;
height: 32px;
border-radius: 50%;
text-align: center;
line-height: 32px;
background-color: var(--background-color-3);
}
/* badges */
.user-badge {

View file

@ -19,15 +19,17 @@
"express-validator": "^6.14.2",
"jsonwebtoken": "^8.5.1",
"pg": "^8.8.0",
"sharp": "^0.31.3",
"ws": "^8.8.1"
},
"devDependencies": {
"@types/cors": "^2.8.12",
"@types/bcrypt": "^5.0.0",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.13",
"@types/jsonwebtoken": "^8.5.9",
"@types/node": "^18.7.13",
"@types/pg": "^8.6.5",
"@types/sharp": "^0.31.1",
"@types/ws": "^8.5.3",
"typescript": "^4.8.2"
}

View file

@ -27,7 +27,10 @@ export default async function databaseInit() {
);
`,
`
ALTER TABLE messages ADD COLUMN nick_username VARCHAR(64) DEFAULT '';
ALTER TABLE messages ADD COLUMN nick_username VARCHAR(64) DEFAULT NULL;
`,
`
ALTER TABLE users ADD COLUMN avatar VARCHAR(48) DEFAULT NULL;
`
];

View file

@ -1,4 +1,4 @@
export const getMessageById = "SELECT messages.id, messages.content, messages.channel_id, messages.created_at, messages.author_id, messages.nick_username, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.id = $1";
export const getMessagesByChannelFirstPage = (limit: number) => `SELECT messages.id, messages.content, messages.created_at, messages.author_id, messages.nick_username, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.channel_id = $1 ORDER BY id DESC LIMIT ${limit}`;
export const getMessagesByChannelPage = (limit: number) => `SELECT messages.id, messages.content, messages.created_at, messages.author_id, messages.nick_username, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.id < $1 AND messages.channel_id = $2 ORDER BY id DESC LIMIT ${limit}`;
export const getMessagesByChannelAfterPage = (limit: number) => `SELECT messages.id, messages.content, messages.created_at, messages.author_id, messages.nick_username, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.id > $1 AND messages.channel_id = $2 ORDER BY id DESC LIMIT ${limit}`;
export const getMessageById = "SELECT messages.id, messages.content, messages.channel_id, messages.created_at, messages.author_id, messages.nick_username, users.avatar AS author_avatar, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.id = $1";
export const getMessagesByChannelFirstPage = (limit: number) => `SELECT messages.id, messages.content, messages.created_at, messages.author_id, messages.nick_username, users.avatar AS author_avatar, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.channel_id = $1 ORDER BY id DESC LIMIT ${limit}`;
export const getMessagesByChannelPage = (limit: number) => `SELECT messages.id, messages.content, messages.created_at, messages.author_id, messages.nick_username, users.avatar AS author_avatar, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.id < $1 AND messages.channel_id = $2 ORDER BY id DESC LIMIT ${limit}`;
export const getMessagesByChannelAfterPage = (limit: number) => `SELECT messages.id, messages.content, messages.created_at, messages.author_id, messages.nick_username, users.avatar AS author_avatar, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.id > $1 AND messages.channel_id = $2 ORDER BY id DESC LIMIT ${limit}`;

View file

@ -18,6 +18,8 @@ export enum GatewayPayloadType {
TypingStart = 130,
PresenceUpdate = 140,
UserUpdate = 150,
}
export enum GatewayPresenceStatus {

View file

@ -8,6 +8,7 @@ import { GatewayPayload } from "../types/gatewaypayload";
import { GatewayPayloadType, GatewayPresenceStatus } from "./gatewaypayloadtype";
import { GatewayPresenceEntry } from "../types/gatewaypresence";
import { processMethodBatch } from "../rpc/rpc";
import { maxGatewayJsonStringByteLength, maxGatewayJsonStringLength, maxGatewayPayloadByteLength } from "../serverconfig";
const GATEWAY_BATCH_INTERVAL = 50000;
const GATEWAY_PING_INTERVAL = 40000;
@ -195,6 +196,7 @@ function getPresenceEntryForConnection(ws: WebSocket, status: GatewayPresenceSta
user: {
id: ws.state.user.id,
username: ws.state.user.username,
avatar: ws.state.user.avatar
},
status
};
@ -296,18 +298,44 @@ export default function(server: Server) {
}
});
ws.on("message", async (rawData, isBinary) => {
if (isBinary) {
return closeWithBadPayload(ws, "Binary messages are not supported");
}
ws.on("message", async (rawData: Buffer, isBinary) => {
ws.state.messagesSinceLastCheck++;
if (ws.state.messagesSinceLastCheck > MAX_CLIENT_MESSAGES_PER_BATCH) {
return closeWithError(ws, gatewayErrors.FLOODING);
}
const stringData = rawData.toString();
if (stringData.length > 4500) {
if (rawData.byteLength >= maxGatewayPayloadByteLength) {
return closeWithError(ws, gatewayErrors.PAYLOAD_TOO_LARGE);
}
let stringData: string;
let binaryStream: Buffer | null = null;
if (isBinary) {
// Binary frames are used in order combine our text data (JSON) with binary data.
// This is especially useful for calling RPC methods that, for example, upload files.
// The format is: [json payload]\n[begin binary stream]
let jsonSlice;
let jsonOffset = -1;
for (let i = 0; i < maxGatewayJsonStringByteLength; i++) {
if (rawData.readUInt8(i) === 0x0A) {
// hit newline
jsonSlice = rawData.subarray(0, i);
jsonOffset = i + 1;
break;
}
}
if (!jsonSlice) {
return closeWithBadPayload(ws, "Did not find newline to delimit JSON from binary stream. JSON payload may be too large, or newline may be missing.");
}
binaryStream = rawData.subarray(jsonOffset, rawData.byteLength);
stringData = jsonSlice.toString();
} else {
stringData = rawData.toString();
}
if (stringData.length > maxGatewayJsonStringLength) {
return closeWithError(ws, gatewayErrors.PAYLOAD_TOO_LARGE);
}
@ -416,7 +444,7 @@ export default function(server: Server) {
}
// RPCSignal is like RPCRequest however it does not send RPC method output unless there is an error
processMethodBatch(ws.state.user, payload.d, (payload.t === GatewayPayloadType.RPCSignal ? true : false)).then((results) => {
processMethodBatch(ws.state.user, payload.d, (payload.t === GatewayPayloadType.RPCSignal ? true : false), binaryStream).then((results) => {
sendPayload(ws, {
t: GatewayPayloadType.RPCResponse,
d: results,

View file

@ -17,6 +17,7 @@ export default async function sendMessage(user: User, channelId: number, optimis
channel_id: channelId,
author_id: authorId,
author_username: user.username,
author_avatar: user.avatar,
created_at: createdAt,
nick_username: nickUsername
};

View file

@ -9,7 +9,7 @@ router.post(
"/",
authenticateRoute(false),
async (req, res) => {
res.json(await processMethodBatch(req.authenticated ? req.user : null, req.body));
res.json(await processMethodBatch(req.authenticated ? req.user : null, req.body, false, null));
}
);
@ -27,7 +27,7 @@ router.get(
} catch(O_o) {
return res.json({ ...errors.BAD_REQUEST, detail: "Bad 'calls': failed to parse as JSON" });
}
res.json(await processMethodBatch(req.authenticated ? req.user : null, callJson));
res.json(await processMethodBatch(req.authenticated ? req.user : null, callJson, false, null));
}
);

View file

@ -361,7 +361,8 @@ router.get(
res.json(await buildSyncPayload({
id: 3,
username: "test",
is_superuser: true
is_superuser: true,
avatar: null
}, cursors, !isInitial, client, channels));
};

View file

@ -1,5 +1,5 @@
import express from "express";
import { channelNameRegex, method, number, string, unsignedNumber, withOptional, withRegexp } from "../rpc";
import { channelNameRegex, method, int, string, uint, withOptional, withRegexp } from "../rpc";
import { query } from "../../database";
import { getMessagesByChannelFirstPage, getMessagesByChannelPage } from "../../database/templates";
import { errors } from "../../errors";
@ -36,7 +36,7 @@ method(
method(
"updateChannelName",
[unsignedNumber(), withRegexp(channelNameRegex, string(1, 32))],
[uint(), withRegexp(channelNameRegex, string(1, 32))],
async (user: User, id: number, name: string) => {
const permissionCheckResult = await query("SELECT owner_id FROM channels WHERE id = $1", [id]);
if (!permissionCheckResult || permissionCheckResult.rowCount < 1) {
@ -68,7 +68,7 @@ method(
method(
"deleteChannel",
[unsignedNumber()],
[uint()],
async (user: User, id: number) => {
const permissionCheckResult = await query("SELECT owner_id FROM channels WHERE id = $1", [id]);
if (!permissionCheckResult || permissionCheckResult.rowCount < 1) {
@ -94,7 +94,7 @@ method(
method(
"getChannel",
[unsignedNumber()],
[uint()],
async (_user: User, id: number) => {
const result = await query("SELECT id, name, owner_id FROM channels WHERE id = $1", [id]);
if (!result || result.rowCount < 1) {
@ -117,7 +117,7 @@ method(
method(
"createChannelMessage",
[unsignedNumber(), string(1, 4000), withOptional(unsignedNumber()), withOptional(string(1, 64))],
[uint(), string(1, 4000), withOptional(uint()), withOptional(string(1, 64))],
async (user: User, id: number, content: string, optimistic_id: number | null, nick_username: string | null) => {
return await sendMessage(user, id, optimistic_id, content, nick_username);
}
@ -125,7 +125,7 @@ method(
method(
"getChannelMessages",
[unsignedNumber(), withOptional(number(5, 100)), withOptional(unsignedNumber())],
[uint(), withOptional(int(5, 100)), withOptional(uint())],
async (_user: User, channelId: number, count: number | null, before: number | null) => {
let limit = count ?? 25;
@ -145,7 +145,7 @@ method(
method(
"putChannelTyping",
[unsignedNumber()],
[uint()],
async (user: User, channelId: number) => {
dispatch(`channel:${channelId}`, {
t: GatewayPayloadType.TypingStart,

View file

@ -1,4 +1,4 @@
import { method, string, unsignedNumber } from "./../rpc";
import { method, string, uint } from "./../rpc";
import { query } from "../../database";
import { getMessageById } from "../../database/templates";
import { errors } from "../../errors";
@ -7,7 +7,7 @@ import { GatewayPayloadType } from "../../gateway/gatewaypayloadtype";
method(
"deleteMessage",
[unsignedNumber()],
[uint()],
async (user: User, id: number) => {
const permissionCheckResult = await query("SELECT author_id, channel_id FROM messages WHERE id = $1", [id]);
if (!permissionCheckResult || permissionCheckResult.rowCount < 1) {
@ -36,7 +36,7 @@ method(
method(
"updateMessageContent",
[unsignedNumber(), string(1, 4000)],
[uint(), string(1, 4000)],
async (user: User, id: number, content: string) => {
const permissionCheckResult = await query(getMessageById, [id]);
if (!permissionCheckResult || permissionCheckResult.rowCount < 1) {
@ -67,7 +67,7 @@ method(
method(
"getMessage",
[unsignedNumber()],
[uint()],
async (user: User, id: number) => {
const result = await query(getMessageById, [id]);
if (!result || result.rowCount < 1) {

View file

@ -2,9 +2,16 @@ import { errors } from "../../errors";
import { query } from "../../database";
import { compare, hash, hashSync } from "bcrypt";
import { getPublicUserObject, loginAttempt } from "../../auth";
import { method, methodButWarningDoesNotAuthenticate, string, usernameRegex, withRegexp } from "./../rpc";
import { bufferSlice, method, methodButWarningDoesNotAuthenticate, string, usernameRegex, withRegexp } from "./../rpc";
import sharp from "sharp";
import path from "path";
import { randomBytes } from "crypto";
import { unlink } from "fs/promises";
import { GatewayPayloadType } from "../../gateway/gatewaypayloadtype";
import { dispatch } from "../../gateway";
const superuserKey = process.env.SUPERUSER_KEY ? hashSync(process.env.SUPERUSER_KEY, 10) : null;
const avatarUploadDirectory = process.env.AVATAR_UPLOADS_DIR ?? "./uploads/avatar";
methodButWarningDoesNotAuthenticate(
"createUser",
@ -23,7 +30,7 @@ methodButWarningDoesNotAuthenticate(
}
const hashedPassword = await hash(password, 10);
const insertedUser = await query("INSERT INTO users(username, password, is_superuser) VALUES ($1, $2, $3) RETURNING id, username, is_superuser", [username, hashedPassword, false]);
const insertedUser = await query("INSERT INTO users(username, password, is_superuser, avatar) VALUES ($1, $2, $3, $4) RETURNING id, username, is_superuser, avatar", [username, hashedPassword, false, null]);
if (!insertedUser || insertedUser.rowCount < 1) {
return errors.GOT_NO_DATABASE_DATA;
}
@ -74,3 +81,103 @@ method(
return errors.BAD_REQUEST_KEY;
}
)
const checkMagic = (buffer: Buffer, magic: number[]) => {
for (let i = 0; i < magic.length; i++) {
try {
if (buffer.readUint8(i) !== magic[i]) {
return false;
}
} catch(O_o) {
return false;
}
}
return true;
};
const profilePictureSizes = [
16, 28, 32, 64, 80, 128, 256
];
method(
"putUserAvatar",
[bufferSlice()],
async (user: User, buffer: Buffer) => {
if (buffer.byteLength >= 3145728) {
// buffer exceeds 3MiB
return { ...errors.BAD_REQUEST, detail: "Uploaded file exceeds 3MiB limit." };
}
// TODO: maybe get rid of this entirely and give buffer directly to `sharp`?
const supportedFormatMagic = [
[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A], // PNG
[0xFF, 0xD8, 0xFF], // JPEG
[0x52, 0x49, 0x46, 0x46] // WebP
];
let isSupported = false;
for (let i = 0; i < supportedFormatMagic.length; i++) {
if (checkMagic(buffer, supportedFormatMagic[i])) {
isSupported = true;
break;
}
}
if (!isSupported) {
return { ...errors.BAD_REQUEST, detail: "Unsupported file format. Supported file formats are: png, jpeg, webp." };
}
const avatarId = randomBytes(8).toString("hex");
const promises = new Array(profilePictureSizes.length);
const filenames = new Array(profilePictureSizes.length);
for (let i = 0; i < profilePictureSizes.length; i++) {
filenames[i] = `${avatarId}_${profilePictureSizes[i]}.webp`;
promises[i] = sharp(buffer, { limitInputPixels: 1000 * 1000 })
.resize(profilePictureSizes[i], profilePictureSizes[i], { fit: "cover" })
.timeout({ seconds: 3 })
.toFile(path.resolve(path.join(avatarUploadDirectory, filenames[i])));
}
try {
await Promise.all(promises);
} catch(O_o) {
console.error("rpc: putUserAvatar: error while processing and saving images", O_o);
console.error("rpc: putUserAvatar: removing all processed images due to error above");
for (let i = 0; i < filenames.length; i++) {
try {
await unlink(path.resolve(path.join(avatarUploadDirectory, filenames[i])));
} catch(o_0) {
console.error("rpc: putUserAvatar: error while removing files (upon error)", o_0);
}
}
return errors.INTERNAL_ERROR;
}
// Now, if the user has an existing avatar, we will remove it
if (user.avatar) {
for (let i = 0; i < profilePictureSizes.length; i++) {
try {
await unlink(path.resolve(path.join(avatarUploadDirectory, `${user.avatar}_${profilePictureSizes[i]}.webp`)));
} catch(o_0) {
console.error("rpc: putUserAvatar: error while removing files (removing old avatar)", o_0);
}
}
}
const updateUserResult = await query("UPDATE users SET avatar = $1 WHERE id = $2", [avatarId, user.id]);
if (!updateUserResult || updateUserResult.rowCount < 1) {
return errors.GOT_NO_DATABASE_DATA;
}
user.avatar = avatarId;
dispatch("*", {
t: GatewayPayloadType.UserUpdate,
d: getPublicUserObject(user),
});
return filenames;
}
);

View file

@ -1,21 +1,28 @@
import { errors } from "../errors";
import { maxBufferByteLength } from "../serverconfig";
export const alphanumericRegex = new RegExp(/^[a-z0-9]+$/i);
export const usernameRegex = new RegExp(/^[a-z0-9_]+$/i);
export const channelNameRegex = new RegExp(/^[a-z0-9_\- ]+$/i);
const defaultStringMaxLength = 3000;
const defaultMaxBufferLength = maxBufferByteLength;
export const unsignedNumber = (): RPCArgument => ({ type: RPCArgumentType.Number, minValue: 0 });
export const number = (minValue?: number, maxValue?: number): RPCArgument => ({ type: RPCArgumentType.Number, minValue, maxValue });
export const uint = (): RPCArgument => ({ type: RPCArgumentType.Integer, minValue: 0 });
export const int = (minValue?: number, maxValue?: number): RPCArgument => ({ type: RPCArgumentType.Integer, minValue, maxValue });
export const string = (minLength = 0, maxLength = defaultStringMaxLength): RPCArgument => ({ type: RPCArgumentType.String, minLength, maxLength });
export const bufferSlice = (minLength = 0, maxLength = defaultMaxBufferLength) => ({ type: RPCArgumentType.Buffer, minLength, maxLength });
export const withRegexp = (regexp: RegExp, arg: RPCArgument): RPCArgument => ({ minLength: 0, maxLength: defaultStringMaxLength, ...arg, regexp });
export const withOptional = (arg: RPCArgument): RPCArgument => ({ ...arg, isOptional: true });
const isInt = (val: any) => typeof val === "number" && Number.isSafeInteger(val);
const isUint = (val: any) => (isInt(val) && val >= 0);
enum RPCArgumentType {
Number,
String
Integer,
String,
Buffer
}
interface RPCArgument {
@ -23,8 +30,8 @@ interface RPCArgument {
isOptional?: boolean
// strings
minLength?: number
maxLength?: number
minLength?: number // also used for buffer
maxLength?: number // also used for buffer
regexp?: RegExp
// numbers
@ -53,7 +60,7 @@ export const methodButWarningDoesNotAuthenticate = (name: string, args: RPCArgum
return method(name, args, func, false);
};
export const userInvokeMethod = async (user: User | null, methodId: number, args: any[]) => {
export const userInvokeMethod = async (user: User | null, methodId: number, args: any[], buffer: Buffer | null) => {
const methodData = methods.get(methodId);
if (!methodData) return {
...errors.BAD_REQUEST,
@ -75,16 +82,16 @@ export const userInvokeMethod = async (user: User | null, methodId: number, args
continue;
}
switch (schema.type) {
case RPCArgumentType.Number: {
if (typeof argument !== "number") {
validationErrors.push({ index: i, msg: `Expected type number, got type ${typeof argument}.` });
case RPCArgumentType.Integer: {
if (!isInt(argument)) {
validationErrors.push({ index: i, msg: `Expected integer.` });
continue;
}
if (schema.minValue !== undefined && argument < schema.minValue) {
validationErrors.push({ index: i, msg: `Provided number is below minimum value of ${schema.minValue}.` });
validationErrors.push({ index: i, msg: `Provided integer is below minimum value of ${schema.minValue}.` });
}
if (schema.maxValue !== undefined && argument > schema.maxValue) {
validationErrors.push({ index: i, msg: `Provided number is above maximum value of ${schema.maxValue}.` });
validationErrors.push({ index: i, msg: `Provided integer is above maximum value of ${schema.maxValue}.` });
}
break;
}
@ -102,6 +109,43 @@ export const userInvokeMethod = async (user: User | null, methodId: number, args
}
break;
}
case RPCArgumentType.Buffer: {
if (!buffer) {
validationErrors.push({ index: i, msg: "RPC method expects buffer, however no buffer was provided." });
continue;
}
// the argument should be an array of this format: [byteOffset: number, byteLength: number]
if (!Array.isArray(argument) || argument.length !== 2 || !isUint(argument[0]) || !isUint(argument[1])) {
validationErrors.push({ index: i, msg: "Expected argument to be an array of format '[byteOffset: uint, byteLength: uint]'." });
continue;
}
// TODO: since slices, can overlap and multiple RPC calls can be done in a single request,
// it makes it possible for someone to send a buffer within the allowed limit, however
// actually tell the server to upload it over and over. We should fix this by adding a
// quota per user.
const [byteOffset, byteLength]: number[] = argument;
const end = byteOffset + byteLength;
if ((schema.minLength !== undefined && byteLength < schema.minLength) || (schema.maxLength !== undefined && byteLength > schema.maxLength)) {
validationErrors.push({ index: i, msg: `Buffer size must be between ${schema.minLength} and ${schema.maxLength} bytes.` });
continue;
}
if (end > buffer.byteLength) {
validationErrors.push({ index: i, msg: "Provided slice exceeds buffer boundaries." });
continue;
}
const slice = buffer.subarray(byteOffset, end);
if (slice.byteLength !== byteLength) {
validationErrors.push({ index: i, msg: "Provided slice is invalid." });
continue;
}
args[i] = slice;
} break;
}
}
@ -121,7 +165,7 @@ export const userInvokeMethod = async (user: User | null, methodId: number, args
}
};
export const processMethodBatch = async (user: User | null, calls: any, ignoreNonErrors = false) => {
export const processMethodBatch = async (user: User | null, calls: any, ignoreNonErrors = false, buffer: Buffer | null) => {
if (!Array.isArray(calls) || !calls.length || calls.length > 5) {
return {
...errors.BAD_REQUEST,
@ -129,6 +173,13 @@ export const processMethodBatch = async (user: User | null, calls: any, ignoreNo
};
}
if (buffer && buffer.byteLength >= maxBufferByteLength) {
return {
...errors.BAD_REQUEST,
detail: `Provided buffer is larger than maximum of ${maxBufferByteLength} bytes.`
};
}
const responses = new Array(calls.length);
const promises = new Array(calls.length);
calls.forEach((call, index) => {
@ -140,7 +191,7 @@ export const processMethodBatch = async (user: User | null, calls: any, ignoreNo
return;
}
const promise = userInvokeMethod(user, call[0], call.slice(1, call.length));
const promise = userInvokeMethod(user, call[0], call.slice(1, call.length), buffer);
promise.then(value => {
if (ignoreNonErrors && !value.code) {
responses[index] = null;

View file

@ -9,6 +9,7 @@ const ENABLE_MATRIX_LAYER = false;
export default function(app: Application) {
app.use(json());
app.use("/api/v1/rpc", rpcRouter);
app.use("/uploads", express.static("uploads"));
app.use("/", express.static("frontend/public"));
if (ENABLE_MATRIX_LAYER) {
app.use("/", matrixRouter);

View file

@ -1,5 +1,10 @@
export default {
superuserRequirement: {
createChannel: false
},
}
};
export const maxBufferByteLength = 8388608; // 8MiB
export const maxGatewayJsonStringLength = 4500;
export const maxGatewayJsonStringByteLength = maxGatewayJsonStringLength * 2; // https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder/encodeInto#buffer_sizing
export const maxGatewayPayloadByteLength = maxBufferByteLength + maxGatewayJsonStringByteLength + 4;

View file

@ -4,6 +4,7 @@ export interface GatewayPresenceEntry {
user: {
username: string,
id: number,
avatar: string | null
},
bridgesTo?: string,
privacy?: string,

7
src/types/user.d.ts vendored
View file

@ -1,6 +1,7 @@
interface User {
password?: string,
username: string,
id: number,
password?: string
username: string
id: number
is_superuser: boolean
avatar: string | null
}

0
uploads/avatar/.gitkeep Normal file
View file

269
yarn.lock
View file

@ -112,6 +112,13 @@
"@types/mime" "^1"
"@types/node" "*"
"@types/sharp@^0.31.1":
version "0.31.1"
resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.31.1.tgz#db768461455dbcf9ff11d69277fd70564483c4df"
integrity sha512-5nWwamN9ZFHXaYEincMSuza8nNfOof8nmO+mcI+Agx1uMUk4/pQnNIcix+9rLPXzKrm1pS34+6WRDbDV0Jn7ag==
dependencies:
"@types/node" "*"
"@types/ws@^8.5.3":
version "8.5.3"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d"
@ -167,6 +174,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
bcrypt@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.0.1.tgz#f1a2c20f208e2ccdceea4433df0c8b2c54ecdf71"
@ -175,6 +187,15 @@ bcrypt@^5.0.1:
"@mapbox/node-pre-gyp" "^1.0.0"
node-addon-api "^3.1.0"
bl@^4.0.3:
version "4.1.0"
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
dependencies:
buffer "^5.5.0"
inherits "^2.0.4"
readable-stream "^3.4.0"
body-parser@1.20.0:
version "1.20.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5"
@ -211,6 +232,14 @@ buffer-writer@2.0.0:
resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04"
integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==
buffer@^5.5.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.1.13"
bytes@3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
@ -224,16 +253,49 @@ call-bind@^1.0.0:
function-bind "^1.1.1"
get-intrinsic "^1.0.2"
chownr@^1.1.1:
version "1.1.4"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
chownr@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
dependencies:
color-name "~1.1.4"
color-name@^1.0.0, color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
color-string@^1.9.0:
version "1.9.1"
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4"
integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==
dependencies:
color-name "^1.0.0"
simple-swizzle "^0.2.2"
color-support@^1.1.2:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
color@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a"
integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==
dependencies:
color-convert "^2.0.1"
color-string "^1.9.0"
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@ -288,6 +350,18 @@ debug@4:
dependencies:
ms "2.1.2"
decompress-response@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==
dependencies:
mimic-response "^3.1.0"
deep-extend@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
delegates@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
@ -303,7 +377,7 @@ destroy@1.2.0:
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
detect-libc@^2.0.0:
detect-libc@^2.0.0, detect-libc@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd"
integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==
@ -335,6 +409,13 @@ encodeurl@~1.0.2:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
end-of-stream@^1.1.0, end-of-stream@^1.4.1:
version "1.4.4"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
dependencies:
once "^1.4.0"
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
@ -345,6 +426,11 @@ etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
expand-template@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
express-validator@^6.14.2:
version "6.14.2"
resolved "https://registry.yarnpkg.com/express-validator/-/express-validator-6.14.2.tgz#6147893f7bec0e14162c3a88b3653121afc4678f"
@ -413,6 +499,11 @@ fresh@0.5.2:
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
fs-constants@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
fs-minipass@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
@ -454,6 +545,11 @@ get-intrinsic@^1.0.2:
has "^1.0.3"
has-symbols "^1.0.3"
github-from-package@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==
glob@^7.1.3:
version "7.2.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
@ -509,6 +605,11 @@ iconv-lite@0.4.24:
dependencies:
safer-buffer ">= 2.1.2 < 3"
ieee754@^1.1.13:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
@ -517,16 +618,26 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
inherits@2, inherits@2.0.4, inherits@^2.0.3:
inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ini@~1.3.0:
version "1.3.8"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
is-arrayish@^0.3.1:
version "0.3.2"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
is-fullwidth-code-point@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
@ -651,6 +762,11 @@ mime@1.6.0:
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
mimic-response@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
minimatch@^3.0.4:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
@ -658,6 +774,11 @@ minimatch@^3.0.4:
dependencies:
brace-expansion "^1.1.7"
minimist@^1.2.0, minimist@^1.2.3:
version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
minipass@^3.0.0:
version "3.1.6"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee"
@ -673,6 +794,11 @@ minizlib@^2.1.1:
minipass "^3.0.0"
yallist "^4.0.0"
mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
mkdirp@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
@ -693,16 +819,33 @@ ms@2.1.3, ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
napi-build-utils@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"
integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==
negotiator@0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
node-abi@^3.3.0:
version "3.33.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.33.0.tgz#8b23a0cec84e1c5f5411836de6a9b84bccf26e7f"
integrity sha512-7GGVawqyHF4pfd0YFybhv/eM9JwTtPqx0mAanQ146O3FlSh3pA24zf9IRQTOsfTSqXTNzPSP5iagAJ94jjuVog==
dependencies:
semver "^7.3.5"
node-addon-api@^3.1.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==
node-addon-api@^5.0.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762"
integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==
node-fetch@^2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
@ -744,7 +887,7 @@ on-finished@2.4.1:
dependencies:
ee-first "1.1.1"
once@^1.3.0:
once@^1.3.0, once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
@ -844,6 +987,24 @@ postgres-interval@^1.1.0:
dependencies:
xtend "^4.0.0"
prebuild-install@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45"
integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==
dependencies:
detect-libc "^2.0.0"
expand-template "^2.0.3"
github-from-package "0.0.0"
minimist "^1.2.3"
mkdirp-classic "^0.5.3"
napi-build-utils "^1.0.1"
node-abi "^3.3.0"
pump "^3.0.0"
rc "^1.2.7"
simple-get "^4.0.0"
tar-fs "^2.0.0"
tunnel-agent "^0.6.0"
proxy-addr@~2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
@ -852,6 +1013,14 @@ proxy-addr@~2.0.7:
forwarded "0.2.0"
ipaddr.js "1.9.1"
pump@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
dependencies:
end-of-stream "^1.1.0"
once "^1.3.1"
qs@6.10.3:
version "6.10.3"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e"
@ -874,6 +1043,25 @@ raw-body@2.5.1:
iconv-lite "0.4.24"
unpipe "1.0.0"
rc@^1.2.7:
version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
dependencies:
deep-extend "^0.6.0"
ini "~1.3.0"
minimist "^1.2.0"
strip-json-comments "~2.0.1"
readable-stream@^3.1.1, readable-stream@^3.4.0:
version "3.6.1"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.1.tgz#f9f9b5f536920253b3d26e7660e7da4ccff9bb62"
integrity sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ==
dependencies:
inherits "^2.0.3"
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
readable-stream@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
@ -917,6 +1105,13 @@ semver@^7.3.5:
dependencies:
lru-cache "^6.0.0"
semver@^7.3.8:
version "7.3.8"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
dependencies:
lru-cache "^6.0.0"
send@0.18.0:
version "0.18.0"
resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"
@ -956,6 +1151,20 @@ setprototypeof@1.2.0:
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
sharp@^0.31.3:
version "0.31.3"
resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.31.3.tgz#60227edc5c2be90e7378a210466c99aefcf32688"
integrity sha512-XcR4+FCLBFKw1bdB+GEhnUNXNXvnt0tDo4WsBsraKymuo/IAuPuCBVAL2wIkUw2r/dwFW5Q5+g66Kwl2dgDFVg==
dependencies:
color "^4.2.3"
detect-libc "^2.0.1"
node-addon-api "^5.0.0"
prebuild-install "^7.1.1"
semver "^7.3.8"
simple-get "^4.0.1"
tar-fs "^2.1.1"
tunnel-agent "^0.6.0"
side-channel@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
@ -970,6 +1179,27 @@ signal-exit@^3.0.0:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
simple-concat@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==
simple-get@^4.0.0, simple-get@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543"
integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==
dependencies:
decompress-response "^6.0.0"
once "^1.3.1"
simple-concat "^1.0.0"
simple-swizzle@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==
dependencies:
is-arrayish "^0.3.1"
split2@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/split2/-/split2-4.1.0.tgz#101907a24370f85bb782f08adaabe4e281ecf809"
@ -1003,6 +1233,32 @@ strip-ansi@^6.0.1:
dependencies:
ansi-regex "^5.0.1"
strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
tar-fs@^2.0.0, tar-fs@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
dependencies:
chownr "^1.1.1"
mkdirp-classic "^0.5.2"
pump "^3.0.0"
tar-stream "^2.1.4"
tar-stream@^2.1.4:
version "2.2.0"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
dependencies:
bl "^4.0.3"
end-of-stream "^1.4.1"
fs-constants "^1.0.0"
inherits "^2.0.3"
readable-stream "^3.1.1"
tar@^6.1.11:
version "6.1.11"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621"
@ -1025,6 +1281,13 @@ tr46@~0.0.3:
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
tunnel-agent@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==
dependencies:
safe-buffer "^5.0.1"
type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"