add buffer support to rpc and add profile pictures
This commit is contained in:
parent
622f3c8e63
commit
afb046b3b6
31 changed files with 728 additions and 112 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,4 +1,5 @@
|
|||
node_modules/
|
||||
dist/
|
||||
frontend-new/
|
||||
uploads/avatar/*.webp
|
||||
.env
|
||||
|
|
|
@ -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,26 +113,38 @@
|
|||
}
|
||||
</style>
|
||||
|
||||
<div class="message" class:clumped={ message._clumped } class:has-children={ message._hasChildren } class:pinged={ message._mentions }>
|
||||
{#if !message._clumped}
|
||||
<div class="author-group">
|
||||
<span class="author" class:author-more={message._viaBadge}>{ message._effectiveAuthor }</span>
|
||||
{#if message._viaBadge}
|
||||
<span class="user-badge via-badge">via { message._viaBadge }</span>
|
||||
<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}
|
||||
<span class="message-author-date">{ message._createdAtTimeString }</span>
|
||||
</div>
|
||||
{/if}
|
||||
<span class="message-content" class:pending={ message._isPending }>{ message.content }</span>
|
||||
|
||||
<div class="message-actions">
|
||||
<button class="icon-button material-icons-outlined" on:click="{ reply }" aria-label="Reply to Message">
|
||||
reply
|
||||
</button>
|
||||
{#if message._editable}
|
||||
<button class="icon-button material-icons-outlined" on:click="{ () => overlayStore.push(OverlayType.EditMessage, { message }) }" aria-label="Edit Message">
|
||||
edit
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="message-body">
|
||||
{#if !message._clumped}
|
||||
<div class="author-group">
|
||||
<span class="author" class:author-more={message._viaBadge}>{ message._effectiveAuthor }</span>
|
||||
{#if message._viaBadge}
|
||||
<span class="user-badge via-badge">via { message._viaBadge }</span>
|
||||
{/if}
|
||||
<span class="message-author-date">{ message._createdAtTimeString }</span>
|
||||
</div>
|
||||
{/if}
|
||||
<span class="message-content" class:pending={ message._isPending }>{ message.content }</span>
|
||||
|
||||
<div class="message-actions">
|
||||
<button class="icon-button material-icons-outlined" on:click="{ reply }" aria-label="Reply to Message">
|
||||
reply
|
||||
</button>
|
||||
{#if message._editable}
|
||||
<button class="icon-button material-icons-outlined" on:click="{ () => overlayStore.push(OverlayType.EditMessage, { message }) }" aria-label="Edit Message">
|
||||
edit
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 }>
|
||||
|
|
|
@ -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>
|
38
frontend/src/components/UserView.svelte
Normal file
38
frontend/src/components/UserView.svelte
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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++;
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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`;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
`
|
||||
];
|
||||
|
||||
|
|
|
@ -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}`;
|
||||
|
|
|
@ -18,6 +18,8 @@ export enum GatewayPayloadType {
|
|||
TypingStart = 130,
|
||||
|
||||
PresenceUpdate = 140,
|
||||
|
||||
UserUpdate = 150,
|
||||
}
|
||||
|
||||
export enum GatewayPresenceStatus {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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));
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
1
src/types/gatewaypresence.d.ts
vendored
1
src/types/gatewaypresence.d.ts
vendored
|
@ -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
7
src/types/user.d.ts
vendored
|
@ -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
0
uploads/avatar/.gitkeep
Normal file
269
yarn.lock
269
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue