overlay fixes, tokens fix
This commit is contained in:
parent
e0d0b2a9df
commit
609947b350
2519
package-lock.json
generated
2519
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -93,7 +93,7 @@ $borderRadius: 8px;
|
||||
.text-input {
|
||||
flex: 1;
|
||||
padding: 12px 12px 12px 0;
|
||||
max-height: 300px;
|
||||
max-height: 180px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
overflow-wrap: anywhere;
|
||||
|
@ -15,7 +15,12 @@ import Button from '../components/button';
|
||||
import TokenRow from '../components/token-row';
|
||||
import CombinedGuild from '../../guild-combined';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { guildMetaState, guildResourceSoftImgSrcState, guildTokensState, useRecoilValueSoftImgSrc } from '../require/atoms';
|
||||
import {
|
||||
guildMetaState,
|
||||
guildResourceSoftImgSrcState,
|
||||
guildTokensState,
|
||||
useRecoilValueSoftImgSrc,
|
||||
} from '../require/atoms';
|
||||
import { isFailed, isLoaded } from '../require/loadables';
|
||||
|
||||
export interface GuildInvitesDisplayProps {
|
||||
@ -32,7 +37,12 @@ const GuildInvitesDisplay: FC<GuildInvitesDisplayProps> = (props: GuildInvitesDi
|
||||
const [expiresFromNow, setExpiresFromNow] = useState<Duration | null>(moment.duration(1, 'day'));
|
||||
const [expiresFromNowText, setExpiresFromNowText] = useState<string>('1 day');
|
||||
|
||||
const iconSrc = useRecoilValueSoftImgSrc(guildResourceSoftImgSrcState({ guildId: guild.id, resourceId: isLoaded(guildMeta) ? guildMeta.value.iconResourceId : null }));
|
||||
const iconSrc = useRecoilValueSoftImgSrc(
|
||||
guildResourceSoftImgSrcState({
|
||||
guildId: guild.id,
|
||||
resourceId: isLoaded(guildMeta) ? guildMeta.value.iconResourceId : null,
|
||||
}),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (expiresFromNowText === 'never') {
|
||||
@ -46,7 +56,9 @@ const GuildInvitesDisplay: FC<GuildInvitesDisplayProps> = (props: GuildInvitesDi
|
||||
const [createTokenFunc, tokenButtonText, tokenButtonShaking, _, createTokenFailMessage] = useAsyncSubmitButton(
|
||||
async () => {
|
||||
try {
|
||||
const createdToken = await guild.requestDoCreateToken(expiresFromNowText === 'never' ? null : expiresFromNowText);
|
||||
const createdToken = await guild.requestDoCreateToken(
|
||||
expiresFromNowText === 'never' ? null : expiresFromNowText,
|
||||
);
|
||||
return { result: createdToken, errorMessage: null };
|
||||
} catch (e: unknown) {
|
||||
LOG.error('error creating token', e);
|
||||
@ -54,7 +66,7 @@ const GuildInvitesDisplay: FC<GuildInvitesDisplayProps> = (props: GuildInvitesDi
|
||||
}
|
||||
},
|
||||
[guild, expiresFromNowText],
|
||||
{ start: 'Create Token', done: 'Create Another Token' },
|
||||
{ start: 'Create Token', done: 'Create Token' },
|
||||
);
|
||||
|
||||
const errorMessage = useMemo(() => {
|
||||
@ -65,7 +77,9 @@ const GuildInvitesDisplay: FC<GuildInvitesDisplayProps> = (props: GuildInvitesDi
|
||||
const tokenElements = useMemo(() => {
|
||||
if (isFailed(tokens)) return <div className="tokens-failed">Unable to load tokens</div>; // TODO: Try Again
|
||||
if (!isLoaded(tokens)) return null; // TODO: Pending indicator
|
||||
return tokens.value.map((token: Token) => <TokenRow key={guild.id + token.token} url={url} token={token} guild={guild} />);
|
||||
return tokens.value.map((token: Token) => (
|
||||
<TokenRow key={guild.id + token.token} url={url} token={token} guild={guild} />
|
||||
));
|
||||
}, [url, guild, tokens]);
|
||||
|
||||
return (
|
||||
@ -92,7 +106,12 @@ const GuildInvitesDisplay: FC<GuildInvitesDisplayProps> = (props: GuildInvitesDi
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<InvitePreview name={isLoaded(guildMeta) ? guildMeta.value.name : 'Your Guild'} iconSrc={iconSrc} url={url} expiresFromNow={expiresFromNow} />
|
||||
<InvitePreview
|
||||
name={isLoaded(guildMeta) ? guildMeta.value.name : 'Your Guild'}
|
||||
iconSrc={iconSrc}
|
||||
url={url}
|
||||
expiresFromNow={expiresFromNow}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="divider" />
|
||||
|
@ -22,13 +22,20 @@ const GuildOverviewDisplay: FC<GuildOverviewDisplayProps> = (props: GuildOvervie
|
||||
|
||||
const guildMeta = useRecoilValue(guildMetaState(guild.id));
|
||||
|
||||
const iconResource = useRecoilValue(guildResourceState({ guildId: guild.id, resourceId: isLoaded(guildMeta) ? guildMeta.value.iconResourceId : null }));
|
||||
const iconResource = useRecoilValue(
|
||||
guildResourceState({
|
||||
guildId: guild.id,
|
||||
resourceId: isLoaded(guildMeta) ? guildMeta.value.iconResourceId : null,
|
||||
}),
|
||||
);
|
||||
|
||||
const [savedName, setSavedName] = useState<string | null>(null);
|
||||
const [savedIconBuff, setSavedIconBuff] = useState<Buffer | null>(null);
|
||||
const [savedName, setSavedName] = useState<string | null>(isLoaded(guildMeta) ? guildMeta.value.name : null);
|
||||
const [savedIconBuff, setSavedIconBuff] = useState<Buffer | null>(
|
||||
isLoaded(iconResource) ? iconResource.value.data : null,
|
||||
);
|
||||
|
||||
const [name, setName] = useState<string | null>(null);
|
||||
const [iconBuff, setIconBuff] = useState<Buffer | null>(null);
|
||||
const [name, setName] = useState<string | null>(isLoaded(guildMeta) ? guildMeta.value.name : null);
|
||||
const [iconBuff, setIconBuff] = useState<Buffer | null>(isLoaded(iconResource) ? iconResource.value.data : null);
|
||||
|
||||
const [saving, setSaving] = useState<boolean>(false);
|
||||
const [saveFailed, setSaveFailed] = useState<boolean>(false);
|
||||
@ -50,9 +57,14 @@ const GuildOverviewDisplay: FC<GuildOverviewDisplayProps> = (props: GuildOvervie
|
||||
if (iconBuff === savedIconBuff) setIconBuff(iconResource.value.data);
|
||||
setSavedIconBuff(iconResource.value.data);
|
||||
}
|
||||
}, [iconBuff, iconResource, savedIconBuff]);
|
||||
// this effect is only for when the iconResource changes
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [iconResource]);
|
||||
|
||||
const changes = useMemo(() => name !== savedName || iconBuff?.toString('hex') !== savedIconBuff?.toString('hex'), [name, savedName, iconBuff, savedIconBuff]);
|
||||
const changes = useMemo(
|
||||
() => name !== savedName || iconBuff?.toString('hex') !== savedIconBuff?.toString('hex'),
|
||||
[name, savedName, iconBuff, savedIconBuff],
|
||||
);
|
||||
|
||||
const errorMessage = useMemo(() => {
|
||||
if (isFailed(iconResource)) return 'Unable to load icon';
|
||||
@ -95,7 +107,6 @@ const GuildOverviewDisplay: FC<GuildOverviewDisplayProps> = (props: GuildOvervie
|
||||
if (iconBuff && iconBuff?.toString('hex') !== savedIconBuff?.toString('hex')) {
|
||||
// save icon
|
||||
try {
|
||||
LOG.debug('saving icon');
|
||||
await guild.requestSetGuildIcon(iconBuff);
|
||||
setSavedIconBuff(iconBuff);
|
||||
} catch (e: unknown) {
|
||||
@ -110,11 +121,25 @@ const GuildOverviewDisplay: FC<GuildOverviewDisplayProps> = (props: GuildOvervie
|
||||
}, [name, iconBuff, errorMessage, saving, savedName, savedIconBuff, guild]);
|
||||
|
||||
return (
|
||||
<Display changes={changes} resetChanges={resetChanges} saveChanges={saveChanges} saving={saving} saveFailed={saveFailed} errorMessage={errorMessage} infoMessage={infoMessage}>
|
||||
<Display
|
||||
changes={changes}
|
||||
resetChanges={resetChanges}
|
||||
saveChanges={saveChanges}
|
||||
saving={saving}
|
||||
saveFailed={saveFailed}
|
||||
errorMessage={errorMessage}
|
||||
infoMessage={infoMessage}
|
||||
>
|
||||
<div className="overview">
|
||||
<div className="metadata">
|
||||
<div className="icon">
|
||||
<ImageEditInput maxSize={Globals.MAX_GUILD_ICON_SIZE} value={iconBuff} setValue={setIconBuff} setValid={setIconInputValid} setMessage={setIconInputMessage} />
|
||||
<ImageEditInput
|
||||
maxSize={Globals.MAX_GUILD_ICON_SIZE}
|
||||
value={iconBuff}
|
||||
setValue={setIconBuff}
|
||||
setValid={setIconInputValid}
|
||||
setMessage={setIconInputMessage}
|
||||
/>
|
||||
</div>
|
||||
<div className="name">
|
||||
<TextInput
|
||||
|
@ -4,13 +4,45 @@ import Logger from '../../../../logger/logger';
|
||||
const LOG = Logger.create(__filename, electronConsole);
|
||||
|
||||
import { ReactNode, useEffect } from 'react';
|
||||
import { atom, atomFamily, GetRecoilValue, RecoilState, RecoilValue, RecoilValueReadOnly, selector, selectorFamily, useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { Changes, Channel, GuildMetadata, Member, Message, Resource, ShouldNeverHappenError, Token } from '../../data-types';
|
||||
import {
|
||||
atom,
|
||||
atomFamily,
|
||||
GetRecoilValue,
|
||||
RecoilState,
|
||||
RecoilValue,
|
||||
RecoilValueReadOnly,
|
||||
selector,
|
||||
selectorFamily,
|
||||
useRecoilState,
|
||||
useSetRecoilState,
|
||||
} from 'recoil';
|
||||
import {
|
||||
Changes,
|
||||
Channel,
|
||||
GuildMetadata,
|
||||
Member,
|
||||
Message,
|
||||
Resource,
|
||||
ShouldNeverHappenError,
|
||||
Token,
|
||||
} from '../../data-types';
|
||||
import CombinedGuild from '../../guild-combined';
|
||||
import GuildsManager from '../../guilds-manager';
|
||||
import ElementsUtil from './elements-util';
|
||||
import Globals from '../../globals';
|
||||
import { createFailedValue, createLoadedValue, DEF_PENDED_VALUE, DEF_UNLOADED_SCROLLING_VALUE, DEF_UNLOADED_VALUE, isFailed, isLoaded, isPended, isUnload, LoadableValue, LoadableValueScrolling } from './loadables';
|
||||
import {
|
||||
createFailedValue,
|
||||
createLoadedValue,
|
||||
DEF_PENDED_VALUE,
|
||||
DEF_UNLOADED_SCROLLING_VALUE,
|
||||
DEF_UNLOADED_VALUE,
|
||||
isFailed,
|
||||
isLoaded,
|
||||
isPended,
|
||||
isUnload,
|
||||
LoadableValue,
|
||||
LoadableValueScrolling,
|
||||
} from './loadables';
|
||||
import {
|
||||
applyChangedElements,
|
||||
applyIfLoaded,
|
||||
@ -52,18 +84,22 @@ export const guildMetaState = atomFamily<LoadableValue<GuildMetadata>, number>({
|
||||
key: 'guildMetaState',
|
||||
default: DEF_UNLOADED_VALUE,
|
||||
effects_UNSTABLE: (guildId: number) => [
|
||||
guildDataSubscriptionLoadableSingleEffect(guildId, async (guild: CombinedGuild) => await guild.fetchMetadata(), {
|
||||
updatedEvent: {
|
||||
name: 'update-metadata',
|
||||
argsMap: (newMeta: GuildMetadata) => newMeta,
|
||||
applyFunc: applyIfLoaded,
|
||||
guildDataSubscriptionLoadableSingleEffect(
|
||||
guildId,
|
||||
async (guild: CombinedGuild) => await guild.fetchMetadata(),
|
||||
{
|
||||
updatedEvent: {
|
||||
name: 'update-metadata',
|
||||
argsMap: (newMeta: GuildMetadata) => newMeta,
|
||||
applyFunc: applyIfLoaded,
|
||||
},
|
||||
conflictEvent: {
|
||||
name: 'conflict-metadata',
|
||||
argsMap: (_changesType, _oldMeta, newMeta: GuildMetadata) => newMeta,
|
||||
applyFunc: applyIfLoaded,
|
||||
},
|
||||
},
|
||||
conflictEvent: {
|
||||
name: 'conflict-metadata',
|
||||
argsMap: (_changesType, _oldMeta, newMeta: GuildMetadata) => newMeta,
|
||||
applyFunc: applyIfLoaded,
|
||||
},
|
||||
}),
|
||||
),
|
||||
],
|
||||
dangerouslyAllowMutability: true,
|
||||
});
|
||||
@ -118,28 +154,32 @@ const guildMembersState = atomFamily<LoadableValue<Member[]>, number>({
|
||||
key: 'guildMembersState',
|
||||
default: DEF_UNLOADED_VALUE,
|
||||
effects_UNSTABLE: (guildId: number) => [
|
||||
guildDataSubscriptionLoadableMultipleEffect(guildId, async (guild: CombinedGuild) => await guild.fetchMembers(), {
|
||||
newEvent: {
|
||||
name: 'new-members',
|
||||
argsMap: (newMembers: Member[]) => newMembers,
|
||||
applyFunc: applyListFuncIfLoaded(applyNewElements, Member.sortForList),
|
||||
guildDataSubscriptionLoadableMultipleEffect(
|
||||
guildId,
|
||||
async (guild: CombinedGuild) => await guild.fetchMembers(),
|
||||
{
|
||||
newEvent: {
|
||||
name: 'new-members',
|
||||
argsMap: (newMembers: Member[]) => newMembers,
|
||||
applyFunc: applyListFuncIfLoaded(applyNewElements, Member.sortForList),
|
||||
},
|
||||
updatedEvent: {
|
||||
name: 'update-members',
|
||||
argsMap: (newMembers: Member[]) => newMembers,
|
||||
applyFunc: applyListFuncIfLoaded(applyUpdatedElements, Member.sortForList),
|
||||
},
|
||||
removedEvent: {
|
||||
name: 'remove-members',
|
||||
argsMap: (newMembers: Member[]) => newMembers,
|
||||
applyFunc: applyListFuncIfLoaded(applyRemovedElements, Member.sortForList),
|
||||
},
|
||||
conflictEvent: {
|
||||
name: 'conflict-members',
|
||||
argsMap: (_changeType, changes: Changes<Member>) => changes,
|
||||
applyFunc: applyListFuncIfLoaded(applyChangedElements, Member.sortForList),
|
||||
},
|
||||
},
|
||||
updatedEvent: {
|
||||
name: 'update-members',
|
||||
argsMap: (newMembers: Member[]) => newMembers,
|
||||
applyFunc: applyListFuncIfLoaded(applyUpdatedElements, Member.sortForList),
|
||||
},
|
||||
removedEvent: {
|
||||
name: 'remove-members',
|
||||
argsMap: (newMembers: Member[]) => newMembers,
|
||||
applyFunc: applyListFuncIfLoaded(applyRemovedElements, Member.sortForList),
|
||||
},
|
||||
conflictEvent: {
|
||||
name: 'conflict-members',
|
||||
argsMap: (_changeType, changes: Changes<Member>) => changes,
|
||||
applyFunc: applyListFuncIfLoaded(applyChangedElements, Member.sortForList),
|
||||
},
|
||||
}),
|
||||
),
|
||||
],
|
||||
dangerouslyAllowMutability: true,
|
||||
});
|
||||
@ -174,28 +214,32 @@ const guildChannelsState = atomFamily<LoadableValue<Channel[]>, number>({
|
||||
key: 'guildChannelsState',
|
||||
default: DEF_UNLOADED_VALUE,
|
||||
effects_UNSTABLE: (guildId: number) => [
|
||||
guildDataSubscriptionLoadableMultipleEffect(guildId, async (guild: CombinedGuild) => await guild.fetchChannels(), {
|
||||
newEvent: {
|
||||
name: 'new-channels',
|
||||
argsMap: (newChannels: Channel[]) => newChannels,
|
||||
applyFunc: applyListFuncIfLoaded(applyNewElements, Channel.sortByIndex),
|
||||
guildDataSubscriptionLoadableMultipleEffect(
|
||||
guildId,
|
||||
async (guild: CombinedGuild) => await guild.fetchChannels(),
|
||||
{
|
||||
newEvent: {
|
||||
name: 'new-channels',
|
||||
argsMap: (newChannels: Channel[]) => newChannels,
|
||||
applyFunc: applyListFuncIfLoaded(applyNewElements, Channel.sortByIndex),
|
||||
},
|
||||
updatedEvent: {
|
||||
name: 'update-channels',
|
||||
argsMap: (updatedChannels: Channel[]) => updatedChannels,
|
||||
applyFunc: applyListFuncIfLoaded(applyUpdatedElements, Channel.sortByIndex),
|
||||
},
|
||||
removedEvent: {
|
||||
name: 'remove-channels',
|
||||
argsMap: (removedChannels: Channel[]) => removedChannels,
|
||||
applyFunc: applyListFuncIfLoaded(applyRemovedElements, Channel.sortByIndex),
|
||||
},
|
||||
conflictEvent: {
|
||||
name: 'conflict-channels',
|
||||
argsMap: (_changeType, changes: Changes<Channel>) => changes,
|
||||
applyFunc: applyListFuncIfLoaded(applyChangedElements, Channel.sortByIndex),
|
||||
},
|
||||
},
|
||||
updatedEvent: {
|
||||
name: 'update-channels',
|
||||
argsMap: (updatedChannels: Channel[]) => updatedChannels,
|
||||
applyFunc: applyListFuncIfLoaded(applyUpdatedElements, Channel.sortByIndex),
|
||||
},
|
||||
removedEvent: {
|
||||
name: 'remove-channels',
|
||||
argsMap: (removedChannels: Channel[]) => removedChannels,
|
||||
applyFunc: applyListFuncIfLoaded(applyRemovedElements, Channel.sortByIndex),
|
||||
},
|
||||
conflictEvent: {
|
||||
name: 'conflict-channels',
|
||||
argsMap: (_changeType, changes: Changes<Channel>) => changes,
|
||||
applyFunc: applyListFuncIfLoaded(applyChangedElements, Channel.sortByIndex),
|
||||
},
|
||||
}),
|
||||
),
|
||||
],
|
||||
dangerouslyAllowMutability: true,
|
||||
});
|
||||
@ -226,16 +270,22 @@ const guildActiveChannelState = selectorFamily<LoadableValue<Channel>, number>({
|
||||
dangerouslyAllowMutability: true,
|
||||
});
|
||||
|
||||
const guildChannelMessagesState = atomFamily<LoadableValueScrolling<Message[], Message>, { guildId: number; channelId: string }>({
|
||||
const guildChannelMessagesState = atomFamily<
|
||||
LoadableValueScrolling<Message[], Message>,
|
||||
{ guildId: number; channelId: string }
|
||||
>({
|
||||
key: 'guildChannelMessagesState',
|
||||
default: DEF_UNLOADED_SCROLLING_VALUE,
|
||||
effects_UNSTABLE: ({ guildId, channelId }) => [
|
||||
multipleScrollingGuildSubscriptionEffect(
|
||||
guildId,
|
||||
{
|
||||
fetchBottomFunc: async (guild: CombinedGuild, count: number) => await guild.fetchMessagesRecent(channelId, count),
|
||||
fetchAboveFunc: async (guild: CombinedGuild, reference: Message, count: number) => await guild.fetchMessagesBefore(channelId, reference._order, count),
|
||||
fetchBelowFunc: async (guild: CombinedGuild, reference: Message, count: number) => await guild.fetchMessagesAfter(channelId, reference._order, count),
|
||||
fetchBottomFunc: async (guild: CombinedGuild, count: number) =>
|
||||
await guild.fetchMessagesRecent(channelId, count),
|
||||
fetchAboveFunc: async (guild: CombinedGuild, reference: Message, count: number) =>
|
||||
await guild.fetchMessagesBefore(channelId, reference._order, count),
|
||||
fetchBelowFunc: async (guild: CombinedGuild, reference: Message, count: number) =>
|
||||
await guild.fetchMessagesAfter(channelId, reference._order, count),
|
||||
},
|
||||
Globals.MESSAGES_PER_REQUEST,
|
||||
Globals.MAX_CURRENT_MESSAGES,
|
||||
@ -243,18 +293,33 @@ const guildChannelMessagesState = atomFamily<LoadableValueScrolling<Message[], M
|
||||
{
|
||||
newEvent: {
|
||||
name: 'new-messages',
|
||||
argsMap: (newMessages: Message[]) => newMessages.filter(message => message.channel.id === channelId),
|
||||
applyFunc: applyListScrollingFuncIfLoaded(applyNewElements, Message.sortOrder, Globals.MAX_CURRENT_MESSAGES),
|
||||
argsMap: (newMessages: Message[]) =>
|
||||
newMessages.filter(message => message.channel.id === channelId),
|
||||
applyFunc: applyListScrollingFuncIfLoaded(
|
||||
applyNewElements,
|
||||
Message.sortOrder,
|
||||
Globals.MAX_CURRENT_MESSAGES,
|
||||
),
|
||||
},
|
||||
updatedEvent: {
|
||||
name: 'update-messages',
|
||||
argsMap: (updatedMessages: Message[]) => updatedMessages.filter(message => message.channel.id === channelId),
|
||||
applyFunc: applyListScrollingFuncIfLoaded(applyUpdatedElements, Message.sortOrder, Globals.MAX_CURRENT_MESSAGES),
|
||||
argsMap: (updatedMessages: Message[]) =>
|
||||
updatedMessages.filter(message => message.channel.id === channelId),
|
||||
applyFunc: applyListScrollingFuncIfLoaded(
|
||||
applyUpdatedElements,
|
||||
Message.sortOrder,
|
||||
Globals.MAX_CURRENT_MESSAGES,
|
||||
),
|
||||
},
|
||||
removedEvent: {
|
||||
name: 'remove-messages',
|
||||
argsMap: (removedMessages: Message[]) => removedMessages.filter(message => message.channel.id === channelId),
|
||||
applyFunc: applyListScrollingFuncIfLoaded(applyRemovedElements, Message.sortOrder, Globals.MAX_CURRENT_MESSAGES),
|
||||
argsMap: (removedMessages: Message[]) =>
|
||||
removedMessages.filter(message => message.channel.id === channelId),
|
||||
applyFunc: applyListScrollingFuncIfLoaded(
|
||||
applyRemovedElements,
|
||||
Message.sortOrder,
|
||||
Globals.MAX_CURRENT_MESSAGES,
|
||||
),
|
||||
},
|
||||
conflictEvent: {
|
||||
name: 'conflict-messages',
|
||||
@ -263,7 +328,11 @@ const guildChannelMessagesState = atomFamily<LoadableValueScrolling<Message[], M
|
||||
updated: changes.updated.filter(change => change.newDataPoint.channel.id === channelId),
|
||||
deleted: changes.deleted.filter(message => message.channel.id === channelId),
|
||||
}),
|
||||
applyFunc: applyListScrollingFuncIfLoaded(applyChangedElements, Message.sortOrder, Globals.MAX_CURRENT_MESSAGES),
|
||||
applyFunc: applyListScrollingFuncIfLoaded(
|
||||
applyChangedElements,
|
||||
Message.sortOrder,
|
||||
Globals.MAX_CURRENT_MESSAGES,
|
||||
),
|
||||
},
|
||||
},
|
||||
),
|
||||
@ -275,28 +344,32 @@ export const guildTokensState = atomFamily<LoadableValue<Token[]>, number>({
|
||||
key: 'guildTokensState',
|
||||
default: DEF_UNLOADED_VALUE,
|
||||
effects_UNSTABLE: (guildId: number) => [
|
||||
guildDataSubscriptionLoadableMultipleEffect(guildId, async (guild: CombinedGuild) => await guild.fetchTokens(), {
|
||||
newEvent: {
|
||||
name: 'new-tokens',
|
||||
argsMap: (newTokens: Token[]) => newTokens,
|
||||
applyFunc: applyListFuncIfLoaded(applyNewElements, Token.sortRecentCreatedFirst),
|
||||
guildDataSubscriptionLoadableMultipleEffect(
|
||||
guildId,
|
||||
async (guild: CombinedGuild) => await guild.fetchTokens(),
|
||||
{
|
||||
newEvent: {
|
||||
name: 'new-tokens',
|
||||
argsMap: (newTokens: Token[]) => newTokens,
|
||||
applyFunc: applyListFuncIfLoaded(applyNewElements, Token.sortFurthestExpiresFirst),
|
||||
},
|
||||
updatedEvent: {
|
||||
name: 'update-tokens',
|
||||
argsMap: (updatedTokens: Token[]) => updatedTokens,
|
||||
applyFunc: applyListFuncIfLoaded(applyUpdatedElements, Token.sortFurthestExpiresFirst),
|
||||
},
|
||||
removedEvent: {
|
||||
name: 'remove-tokens',
|
||||
argsMap: (removedTokens: Token[]) => removedTokens,
|
||||
applyFunc: applyListFuncIfLoaded(applyRemovedElements, Token.sortFurthestExpiresFirst),
|
||||
},
|
||||
conflictEvent: {
|
||||
name: 'conflict-tokens',
|
||||
argsMap: (_changeType, changes: Changes<Token>) => changes,
|
||||
applyFunc: applyListFuncIfLoaded(applyChangedElements, Token.sortFurthestExpiresFirst),
|
||||
},
|
||||
},
|
||||
updatedEvent: {
|
||||
name: 'update-tokens',
|
||||
argsMap: (updatedTokens: Token[]) => updatedTokens,
|
||||
applyFunc: applyListFuncIfLoaded(applyUpdatedElements, Token.sortRecentCreatedFirst),
|
||||
},
|
||||
removedEvent: {
|
||||
name: 'remove-tokens',
|
||||
argsMap: (removedTokens: Token[]) => removedTokens,
|
||||
applyFunc: applyListFuncIfLoaded(applyRemovedElements, Token.sortRecentCreatedFirst),
|
||||
},
|
||||
conflictEvent: {
|
||||
name: 'conflict-tokens',
|
||||
argsMap: (_changeType, changes: Changes<Token>) => changes,
|
||||
applyFunc: applyListFuncIfLoaded(applyChangedElements, Token.sortRecentCreatedFirst),
|
||||
},
|
||||
}),
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
@ -327,7 +400,11 @@ function createCurrentGuildStateGetter<T>(subSelectorFamily: (guildId: number) =
|
||||
return value;
|
||||
};
|
||||
}
|
||||
function createCurrentGuildHardValueWithParamStateGetter<T, P>(subSelectorFamily: (param: P) => RecoilValueReadOnly<T> | RecoilState<T>, guildIdToParam: (guildId: number) => P, unloadedValue: T) {
|
||||
function createCurrentGuildHardValueWithParamStateGetter<T, P>(
|
||||
subSelectorFamily: (param: P) => RecoilValueReadOnly<T> | RecoilState<T>,
|
||||
guildIdToParam: (guildId: number) => P,
|
||||
unloadedValue: T,
|
||||
) {
|
||||
return ({ get }: { get: GetRecoilValue }) => {
|
||||
// use the unloaded value if the current guild hasn't been selected yet or doesn't exist
|
||||
const currGuildId = get(currGuildIdState);
|
||||
@ -338,7 +415,9 @@ function createCurrentGuildHardValueWithParamStateGetter<T, P>(subSelectorFamily
|
||||
return value;
|
||||
};
|
||||
}
|
||||
function createCurrentGuildLoadableStateGetter<T>(subSelectorFamily: (guildId: number) => RecoilValueReadOnly<LoadableValue<T>> | RecoilState<LoadableValue<T>>) {
|
||||
function createCurrentGuildLoadableStateGetter<T>(
|
||||
subSelectorFamily: (guildId: number) => RecoilValueReadOnly<LoadableValue<T>> | RecoilState<LoadableValue<T>>,
|
||||
) {
|
||||
return ({ get }: { get: GetRecoilValue }) => {
|
||||
// use the unloaded value if the current guild hasn't been selected yet or doesn't exist
|
||||
const currGuildId = get(currGuildIdState);
|
||||
@ -348,7 +427,9 @@ function createCurrentGuildLoadableStateGetter<T>(subSelectorFamily: (guildId: n
|
||||
return value;
|
||||
};
|
||||
}
|
||||
function createCurrentGuildActiveChannelLoadableScrollingStateGetter<T>(subSelectorFamily: (param: { guildId: number; channelId: string }) => RecoilState<LoadableValueScrolling<T[], T>>) {
|
||||
function createCurrentGuildActiveChannelLoadableScrollingStateGetter<T>(
|
||||
subSelectorFamily: (param: { guildId: number; channelId: string }) => RecoilState<LoadableValueScrolling<T[], T>>,
|
||||
) {
|
||||
return ({ get }: { get: GetRecoilValue }) => {
|
||||
// use the unloaded value if the current guild / current guild active channel hasn't been selected yet or doesn't exist
|
||||
const guildId = get(currGuildIdState);
|
||||
@ -360,7 +441,10 @@ function createCurrentGuildActiveChannelLoadableScrollingStateGetter<T>(subSelec
|
||||
return value;
|
||||
};
|
||||
}
|
||||
function createCurrentGuildLoadableWithParamStateGetter<T, P>(subSelectorFamily: (param: P) => RecoilValueReadOnly<LoadableValue<T>> | RecoilState<LoadableValue<T>>, guildIdToParam: (guildId: number) => P) {
|
||||
function createCurrentGuildLoadableWithParamStateGetter<T, P>(
|
||||
subSelectorFamily: (param: P) => RecoilValueReadOnly<LoadableValue<T>> | RecoilState<LoadableValue<T>>,
|
||||
guildIdToParam: (guildId: number) => P,
|
||||
) {
|
||||
return ({ get }: { get: GetRecoilValue }) => {
|
||||
// use the unloaded value if the current guild hasn't been selected yet or doesn't exist
|
||||
const currGuildId = get(currGuildIdState);
|
||||
@ -388,13 +472,22 @@ export const currGuildMetaState = selector<LoadableValue<GuildMetadata>>({
|
||||
});
|
||||
export const currGuildResourceState = selectorFamily<LoadableValue<Resource>, string | null>({
|
||||
key: 'currGuildResourceState',
|
||||
get: (resourceId: string | null) => createCurrentGuildLoadableWithParamStateGetter(guildResourceState, (guildId: number) => ({ guildId, resourceId })),
|
||||
get: (resourceId: string | null) =>
|
||||
createCurrentGuildLoadableWithParamStateGetter(guildResourceState, (guildId: number) => ({
|
||||
guildId,
|
||||
resourceId,
|
||||
})),
|
||||
|
||||
dangerouslyAllowMutability: true,
|
||||
});
|
||||
export const currGuildResourceSoftImgSrcState = selectorFamily<string, string | null>({
|
||||
key: 'currGuildResourceSoftImgSrcState',
|
||||
get: (resourceId: string | null) => createCurrentGuildHardValueWithParamStateGetter(guildResourceSoftImgSrcState, (guildId: number) => ({ guildId, resourceId }), './img/loading.svg'),
|
||||
get: (resourceId: string | null) =>
|
||||
createCurrentGuildHardValueWithParamStateGetter(
|
||||
guildResourceSoftImgSrcState,
|
||||
(guildId: number) => ({ guildId, resourceId }),
|
||||
'./img/loading.svg',
|
||||
),
|
||||
|
||||
dangerouslyAllowMutability: true,
|
||||
});
|
||||
|
@ -54,7 +54,9 @@ export default class DB {
|
||||
}
|
||||
|
||||
static async getAllGuilds(): Promise<any[]> {
|
||||
const result = await db.query('SELECT * FROM "guilds" LEFT JOIN "guilds_meta" ON "guilds"."id"="guilds_meta"."id"');
|
||||
const result = await db.query(
|
||||
'SELECT * FROM "guilds" LEFT JOIN "guilds_meta" ON "guilds"."id"="guilds_meta"."id"',
|
||||
);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
@ -63,9 +65,15 @@ export default class DB {
|
||||
const der = publicKey.export({ type: 'spki', format: 'der' });
|
||||
|
||||
// eslint-disable-next-line newline-per-chained-call
|
||||
LOG.silly(`searching for public key (der hash: ${LOG.inspect(crypto.createHash('sha256').update(der).digest().toString('hex'))})`);
|
||||
LOG.silly(
|
||||
`searching for public key (der hash: ${LOG.inspect(
|
||||
crypto.createHash('sha256').update(der).digest().toString('hex'),
|
||||
)})`,
|
||||
);
|
||||
|
||||
const result = await db.query('SELECT "id" AS member_id, "guild_id" FROM "members" WHERE "public_key"=$1', [der]);
|
||||
const result = await db.query('SELECT "id" AS member_id, "guild_id" FROM "members" WHERE "public_key"=$1', [
|
||||
der,
|
||||
]);
|
||||
|
||||
if (result.rows.length !== 1) {
|
||||
throw new Error('unable to find member with specified public key');
|
||||
@ -116,7 +124,10 @@ export default class DB {
|
||||
}
|
||||
|
||||
static async getMember(guildId: string, memberId: string): Promise<any> {
|
||||
const result = await db.query('SELECT * FROM "members_with_roles" WHERE "guild_id"=$1 AND "id"=$2', [guildId, memberId]);
|
||||
const result = await db.query('SELECT * FROM "members_with_roles" WHERE "guild_id"=$1 AND "id"=$2', [
|
||||
guildId,
|
||||
memberId,
|
||||
]);
|
||||
if (result.rows.length !== 1) {
|
||||
throw new Error('unable to get member');
|
||||
}
|
||||
@ -124,14 +135,19 @@ export default class DB {
|
||||
}
|
||||
|
||||
static async getChannels(guildId: string): Promise<any[]> {
|
||||
const result = await db.query('SELECT "id", "index", "name", "flavor_text" FROM "channels" WHERE "guild_id"=$1 ORDER BY "index"', [guildId]);
|
||||
const result = await db.query(
|
||||
'SELECT "id", "index", "name", "flavor_text" FROM "channels" WHERE "guild_id"=$1 ORDER BY "index"',
|
||||
[guildId],
|
||||
);
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
static async createChannel(guildId: string, name: string, flavorText: string): Promise<any> {
|
||||
let channel = null;
|
||||
await DB.queueTransaction(async () => {
|
||||
const indexResult = await db.query('SELECT MAX("index") AS max_index FROM "channels" WHERE "guild_id"=$1', [guildId]);
|
||||
const indexResult = await db.query('SELECT MAX("index") AS max_index FROM "channels" WHERE "guild_id"=$1', [
|
||||
guildId,
|
||||
]);
|
||||
if (indexResult.rows.length !== 1) {
|
||||
throw new Error('invalid index result');
|
||||
}
|
||||
@ -189,7 +205,12 @@ export default class DB {
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
static async getMessagesBefore(guildId: string, channelId: string, messageOrderId: string, number: number): Promise<any[]> {
|
||||
static async getMessagesBefore(
|
||||
guildId: string,
|
||||
channelId: string,
|
||||
messageOrderId: string,
|
||||
number: number,
|
||||
): Promise<any[]> {
|
||||
const result = await db.query(
|
||||
`
|
||||
SELECT * FROM (
|
||||
@ -215,7 +236,12 @@ export default class DB {
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
static async getMessagesAfter(guildId: string, channelId: string, messageOrderId: string, number: number): Promise<any[]> {
|
||||
static async getMessagesAfter(
|
||||
guildId: string,
|
||||
channelId: string,
|
||||
messageOrderId: string,
|
||||
number: number,
|
||||
): Promise<any[]> {
|
||||
const result = await db.query(
|
||||
`
|
||||
SELECT
|
||||
@ -313,7 +339,18 @@ export default class DB {
|
||||
, "resource_preview_id"
|
||||
, "order"
|
||||
`,
|
||||
[id, guildId, channelId, memberId, text, resourceId, resourceName, resourceWidth, resourceHeight, resourcePreviewId],
|
||||
[
|
||||
id,
|
||||
guildId,
|
||||
channelId,
|
||||
memberId,
|
||||
text,
|
||||
resourceId,
|
||||
resourceName,
|
||||
resourceWidth,
|
||||
resourceHeight,
|
||||
resourcePreviewId,
|
||||
],
|
||||
);
|
||||
|
||||
if (result.rows.length !== 1) {
|
||||
@ -425,7 +462,7 @@ export default class DB {
|
||||
static async assignRoleToMember(guildId: string, roleId: string, memberId: string): Promise<void> {
|
||||
const existsResult = await db.query(
|
||||
`
|
||||
SELECT COUNT(*) AS c FROM "member_roles" WHERE
|
||||
SELECT COUNT(*)::integer AS c FROM "member_roles" WHERE
|
||||
"role_id" = $1
|
||||
AND "guild_id" = $2
|
||||
AND "member_id" = $3
|
||||
@ -458,7 +495,7 @@ export default class DB {
|
||||
static async revokeRoleFromMember(guildId: string, roleId: string, memberId: string): Promise<void> {
|
||||
const existsResult = await db.query(
|
||||
`
|
||||
SELECT COUNT(*) AS c FROM "member_roles" WHERE
|
||||
SELECT COUNT(*)::integer AS c FROM "member_roles" WHERE
|
||||
"role_id" = $1
|
||||
AND "guild_id" = $2
|
||||
AND "member_id" = $3
|
||||
@ -490,7 +527,7 @@ export default class DB {
|
||||
static async assignRolePrivilege(guildId: string, roleId: string, privilege: string): Promise<void> {
|
||||
const existsResult = await db.query(
|
||||
`
|
||||
SELECT COUNT(*) AS c FROM "role_privileges" WHERE
|
||||
SELECT COUNT(*)::integer AS c FROM "role_privileges" WHERE
|
||||
"role_id" = $1
|
||||
AND "guild_id" = $2
|
||||
AND "privilege" = $3
|
||||
@ -523,7 +560,7 @@ export default class DB {
|
||||
static async revokeRolePrivilege(roleId: string, guildId: string, privilege: string): Promise<void> {
|
||||
const existsResult = await db.query(
|
||||
`
|
||||
SELECT COUNT(*) AS c FROM "role_privileges" WHERE
|
||||
SELECT COUNT(*)::integer AS c FROM "role_privileges" WHERE
|
||||
"role_id" = $1
|
||||
AND "guild_id" = $2
|
||||
AND "privilege" = $3
|
||||
@ -555,7 +592,7 @@ export default class DB {
|
||||
static async hasPrivilege(guildId: string, memberId: string, privilege: string): Promise<boolean> {
|
||||
const result = await db.query(
|
||||
`
|
||||
SELECT COUNT(*) AS c FROM
|
||||
SELECT COUNT(*)::integer AS c FROM
|
||||
member_roles
|
||||
, role_privileges
|
||||
WHERE
|
||||
@ -612,28 +649,42 @@ export default class DB {
|
||||
}
|
||||
|
||||
static async isTokenReal(token: string): Promise<boolean> {
|
||||
const result = await db.query(`SELECT COUNT(*) AS c FROM "tokens" WHERE "token"=$1`, [token]);
|
||||
const result = await db.query(`SELECT COUNT(*)::integer AS c FROM "tokens" WHERE "token"=$1`, [token]);
|
||||
return result.rows[0].c === 1;
|
||||
}
|
||||
|
||||
static async isTokenForGuild(token: string, guildId: string): Promise<boolean> {
|
||||
const result = await db.query(`SELECT COUNT(*) AS c FROM "tokens" WHERE "token"=$1 AND "guild_id"=$2`, [token, guildId]);
|
||||
const result = await db.query(
|
||||
`SELECT COUNT(*)::integer AS c FROM "tokens" WHERE "token"=$1 AND "guild_id"=$2`,
|
||||
[token, guildId],
|
||||
);
|
||||
return result.rows[0].c === 1;
|
||||
}
|
||||
|
||||
static async isTokenTaken(token: string): Promise<boolean> {
|
||||
const result = await db.query(`SELECT COUNT(*) AS c FROM "tokens" WHERE "token"=$1 AND "member_id" IS NOT NULL`, [token]);
|
||||
const result = await db.query(
|
||||
`SELECT COUNT(*)::integer AS c FROM "tokens" WHERE "token"=$1 AND "member_id" IS NOT NULL`,
|
||||
[token],
|
||||
);
|
||||
return result.rows[0].c === 1;
|
||||
}
|
||||
|
||||
static async isTokenActive(token: string): Promise<boolean> {
|
||||
const result = await db.query(`SELECT COUNT(*) AS c FROM "tokens" WHERE "token"=$1 AND "member_id" IS NULL AND "expires">NOW()`, [token]);
|
||||
const result = await db.query(
|
||||
`SELECT COUNT(*)::integer AS c FROM "tokens" WHERE "token"=$1 AND "member_id" IS NULL AND "expires">NOW()`,
|
||||
[token],
|
||||
);
|
||||
return result.rows[0].c === 1;
|
||||
}
|
||||
|
||||
// registers a user for with a token
|
||||
// NOTE: Tokens are unique across guilds so they can be used to get the guildId
|
||||
static async registerWithToken(token: string, derBuff: Buffer, displayName: string, avatarBuff: Buffer): Promise<{ guildId: string; memberId: string }> {
|
||||
static async registerWithToken(
|
||||
token: string,
|
||||
derBuff: Buffer,
|
||||
displayName: string,
|
||||
avatarBuff: Buffer,
|
||||
): Promise<{ guildId: string; memberId: string }> {
|
||||
// insert avatar as resource
|
||||
let result: { memberId: string; guildId: string } | null = null;
|
||||
await DB.queueTransaction(async () => {
|
||||
@ -679,7 +730,10 @@ export default class DB {
|
||||
}
|
||||
|
||||
static async revokeToken(guildId: string, token: string): Promise<any> {
|
||||
const result = await db.query('DELETE FROM "tokens" WHERE "guild_id"=$1 AND "token"=$2 RETURNING *', [guildId, token]);
|
||||
const result = await db.query('DELETE FROM "tokens" WHERE "guild_id"=$1 AND "token"=$2 RETURNING *', [
|
||||
guildId,
|
||||
token,
|
||||
]);
|
||||
if (result.rows.length !== 1) {
|
||||
throw new Error('unable to remove token');
|
||||
}
|
||||
|
@ -139,10 +139,17 @@ function bindEvent(
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof SignatureError) {
|
||||
LOG.warn(`c#${client.id}: ${name} request does not match expected signature`, { signature, args, msg: e.message });
|
||||
LOG.warn(`c#${client.id}: ${name} request does not match expected signature`, {
|
||||
signature,
|
||||
args,
|
||||
msg: e.message,
|
||||
});
|
||||
// do not respond to requests with invalid signatures
|
||||
} else if (e instanceof EventError) {
|
||||
LOG.warn(`c#${client.id}: ${e.message}${e.extended_message ? ' / ' + e.extended_message : ''}`, e.cause);
|
||||
LOG.warn(
|
||||
`c#${client.id}: ${e.message}${e.extended_message ? ' / ' + e.extended_message : ''}`,
|
||||
e.cause,
|
||||
);
|
||||
respond(e.message);
|
||||
} else {
|
||||
LOG.error('caught unhandled error', e);
|
||||
@ -163,40 +170,47 @@ function bindRegistrationEvents(io: socketio.Server, client: socketio.Socket): v
|
||||
* @param respond.errStr Error string if an error, else null
|
||||
* @param respond.member The member created by the registration
|
||||
*/
|
||||
bindEvent(client, null, null, 'register-with-token', ['string', 'buffer', 'string', 'buffer', 'function'], async (token, publicKeyBuff, displayName, avatarBuff, respond) => {
|
||||
if (displayName.length > MAX_DISPLAY_NAME_LENGTH || displayName.length === 0) {
|
||||
throw new EventError('invalid display name');
|
||||
}
|
||||
if (avatarBuff.length > MAX_AVATAR_SIZE) {
|
||||
throw new EventError('invalid avatar');
|
||||
}
|
||||
bindEvent(
|
||||
client,
|
||||
null,
|
||||
null,
|
||||
'register-with-token',
|
||||
['string', 'buffer', 'string', 'buffer', 'function'],
|
||||
async (token, publicKeyBuff, displayName, avatarBuff, respond) => {
|
||||
if (displayName.length > MAX_DISPLAY_NAME_LENGTH || displayName.length === 0) {
|
||||
throw new EventError('invalid display name');
|
||||
}
|
||||
if (avatarBuff.length > MAX_AVATAR_SIZE) {
|
||||
throw new EventError('invalid avatar');
|
||||
}
|
||||
|
||||
const typeResult = await FileType.fromBuffer(avatarBuff);
|
||||
if (!typeResult || ['image/png', 'image/jpeg', 'image/jpg'].indexOf(typeResult.mime) === -1) {
|
||||
throw new EventError('invalid avatar mime type');
|
||||
}
|
||||
const typeResult = await FileType.fromBuffer(avatarBuff);
|
||||
if (!typeResult || ['image/png', 'image/jpeg', 'image/jpg'].indexOf(typeResult.mime) === -1) {
|
||||
throw new EventError('invalid avatar mime type');
|
||||
}
|
||||
|
||||
if (!(await DB.isTokenReal(token))) {
|
||||
throw new EventError('not a real token');
|
||||
}
|
||||
if (await DB.isTokenTaken(token)) {
|
||||
throw new EventError('token already used');
|
||||
}
|
||||
if (!(await DB.isTokenActive(token))) {
|
||||
throw new EventError('token expired');
|
||||
}
|
||||
if (!(await DB.isTokenReal(token))) {
|
||||
throw new EventError('not a real token');
|
||||
}
|
||||
if (await DB.isTokenTaken(token)) {
|
||||
throw new EventError('token already used');
|
||||
}
|
||||
if (!(await DB.isTokenActive(token))) {
|
||||
throw new EventError('token expired');
|
||||
}
|
||||
|
||||
const { guildId, memberId } = await DB.registerWithToken(token, publicKeyBuff, displayName, avatarBuff);
|
||||
const { guildId, memberId } = await DB.registerWithToken(token, publicKeyBuff, displayName, avatarBuff);
|
||||
|
||||
const member = await DB.getMember(guildId, memberId);
|
||||
const meta = await DB.getGuild(guildId);
|
||||
const member = await DB.getMember(guildId, memberId);
|
||||
const meta = await DB.getGuild(guildId);
|
||||
|
||||
LOG.info(`c#${client.id}: registered with t#${token} as u#${member.id} / ${member.display_name}`);
|
||||
LOG.info(`c#${client.id}: registered with t#${token} as u#${member.id} / ${member.display_name}`);
|
||||
|
||||
respond(null, member, meta);
|
||||
respond(null, member, meta);
|
||||
|
||||
io.to(guildId).emit('new-member', member);
|
||||
});
|
||||
io.to(guildId).emit('new-member', member);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function bindChallengeVerificationEvents(io: socketio.Server, client: socketio.Socket, identity: IIdentity): void {
|
||||
@ -294,7 +308,13 @@ function bindChallengeVerificationEvents(io: socketio.Server, client: socketio.S
|
||||
}
|
||||
|
||||
identity.verified = true;
|
||||
const rooms = [identity.guildId].concat(member && member.privileges ? member.privileges.split(',').map((privilege: string) => guildPrivilegeRoomName(identity.guildId as string, privilege)) : []);
|
||||
const rooms = [identity.guildId].concat(
|
||||
member && member.privileges
|
||||
? member.privileges
|
||||
.split(',')
|
||||
.map((privilege: string) => guildPrivilegeRoomName(identity.guildId as string, privilege))
|
||||
: [],
|
||||
);
|
||||
LOG.debug(`c#${client.id} joining ${rooms.join(', ')}`);
|
||||
client.join(rooms);
|
||||
|
||||
@ -311,18 +331,25 @@ function bindAdminEvents(io: socketio.Server, client: socketio.Socket, identity:
|
||||
* @param respond.errStr Error string if an error, else null
|
||||
* @param respond.guildMeta The update guild metadata information
|
||||
*/
|
||||
bindEvent(client, identity, { verified: true, privileges: ['modify_profile'] }, 'set-name', ['string', 'function'], async (name, respond) => {
|
||||
if (name.length === 0 || name.length > MAX_GUILD_NAME_LENGTH) throw new EventError('invalid guild name');
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
bindEvent(
|
||||
client,
|
||||
identity,
|
||||
{ verified: true, privileges: ['modify_profile'] },
|
||||
'set-name',
|
||||
['string', 'function'],
|
||||
async (name, respond) => {
|
||||
if (name.length === 0 || name.length > MAX_GUILD_NAME_LENGTH) throw new EventError('invalid guild name');
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
|
||||
LOG.debug(`g#${identity.guildId} u#${identity.memberId} set-name to ${name}`);
|
||||
LOG.debug(`g#${identity.guildId} u#${identity.memberId} set-name to ${name}`);
|
||||
|
||||
const newMeta = await DB.setName(identity.guildId, name);
|
||||
const newMeta = await DB.setName(identity.guildId, name);
|
||||
|
||||
respond(null, newMeta);
|
||||
respond(null, newMeta);
|
||||
|
||||
io.to(identity.guildId).emit('update-metadata', newMeta);
|
||||
});
|
||||
io.to(identity.guildId).emit('update-metadata', newMeta);
|
||||
},
|
||||
);
|
||||
|
||||
/*
|
||||
* sets the icon of the guild
|
||||
@ -331,24 +358,31 @@ function bindAdminEvents(io: socketio.Server, client: socketio.Socket, identity:
|
||||
* @param respond.errStr Error string if an error, else null
|
||||
* @param respond.guildMeta The update guild metadata information
|
||||
*/
|
||||
bindEvent(client, identity, { verified: true, privileges: ['modify_profile'] }, 'set-icon', ['buffer', 'function'], async (iconBuff, respond) => {
|
||||
if (iconBuff.length === 0 || iconBuff.length > MAX_ICON_SIZE) throw new EventError('invalid guild icon');
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
bindEvent(
|
||||
client,
|
||||
identity,
|
||||
{ verified: true, privileges: ['modify_profile'] },
|
||||
'set-icon',
|
||||
['buffer', 'function'],
|
||||
async (iconBuff, respond) => {
|
||||
if (iconBuff.length === 0 || iconBuff.length > MAX_ICON_SIZE) throw new EventError('invalid guild icon');
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
|
||||
LOG.debug(`g#${identity.guildId} u#${identity.memberId} set-icon`);
|
||||
LOG.debug(`g#${identity.guildId} u#${identity.memberId} set-icon`);
|
||||
|
||||
const typeResult = await FileType.fromBuffer(iconBuff);
|
||||
if (!typeResult || !['image/png', 'image/jpeg', 'image/jpg'].includes(typeResult.mime)) {
|
||||
throw new EventError('detected invalid mime type');
|
||||
}
|
||||
const typeResult = await FileType.fromBuffer(iconBuff);
|
||||
if (!typeResult || !['image/png', 'image/jpeg', 'image/jpg'].includes(typeResult.mime)) {
|
||||
throw new EventError('detected invalid mime type');
|
||||
}
|
||||
|
||||
const iconResourceId = await DB.insertResource(identity.guildId, iconBuff);
|
||||
const newMeta = await DB.setIcon(identity.guildId, iconResourceId);
|
||||
const iconResourceId = await DB.insertResource(identity.guildId, iconBuff);
|
||||
const newMeta = await DB.setIcon(identity.guildId, iconResourceId);
|
||||
|
||||
respond(null, newMeta);
|
||||
respond(null, newMeta);
|
||||
|
||||
io.to(identity.guildId).emit('update-metadata', newMeta);
|
||||
});
|
||||
io.to(identity.guildId).emit('update-metadata', newMeta);
|
||||
},
|
||||
);
|
||||
|
||||
/*
|
||||
* creates a text channel
|
||||
@ -358,24 +392,34 @@ function bindAdminEvents(io: socketio.Server, client: socketio.Socket, identity:
|
||||
* @param respond.errStr Error string if an error, else null
|
||||
* @param respond.channel The created channel
|
||||
*/
|
||||
bindEvent(client, identity, { verified: true, privileges: ['modify_channels'] }, 'create-text-channel', ['string', 'string?', 'function'], async (name, flavorText, respond) => {
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
bindEvent(
|
||||
client,
|
||||
identity,
|
||||
{ verified: true, privileges: ['modify_channels'] },
|
||||
'create-text-channel',
|
||||
['string', 'string?', 'function'],
|
||||
async (name, flavorText, respond) => {
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
|
||||
flavorText = flavorText || null;
|
||||
flavorText = flavorText || null;
|
||||
|
||||
if (name.length === 0 || name.length > MAX_CHANNEL_NAME_LENGTH || !/^[A-Za-z0-9-]+$/.exec(name)) {
|
||||
throw new EventError('invalid channel name');
|
||||
}
|
||||
if (flavorText !== null && (flavorText.length === 0 || flavorText.length > MAX_CHANNEL_FLAVOR_TEXT_LENGTH)) {
|
||||
throw new EventError('invalid flavor text');
|
||||
}
|
||||
if (name.length === 0 || name.length > MAX_CHANNEL_NAME_LENGTH || !/^[A-Za-z0-9-]+$/.exec(name)) {
|
||||
throw new EventError('invalid channel name');
|
||||
}
|
||||
if (
|
||||
flavorText !== null &&
|
||||
(flavorText.length === 0 || flavorText.length > MAX_CHANNEL_FLAVOR_TEXT_LENGTH)
|
||||
) {
|
||||
throw new EventError('invalid flavor text');
|
||||
}
|
||||
|
||||
const newChannel = await DB.createChannel(identity.guildId, name, flavorText);
|
||||
const newChannel = await DB.createChannel(identity.guildId, name, flavorText);
|
||||
|
||||
respond(null, newChannel);
|
||||
respond(null, newChannel);
|
||||
|
||||
io.to(identity.guildId).emit('new-channel', newChannel);
|
||||
});
|
||||
io.to(identity.guildId).emit('new-channel', newChannel);
|
||||
},
|
||||
);
|
||||
|
||||
/*
|
||||
* updates a channel
|
||||
@ -386,24 +430,34 @@ function bindAdminEvents(io: socketio.Server, client: socketio.Socket, identity:
|
||||
* @param respond.errStr Error string if an error, else null
|
||||
* @param respond.channel Updated channel
|
||||
*/
|
||||
bindEvent(client, identity, { verified: true, privileges: ['modify_channels'] }, 'update-channel', ['string', 'string', 'string?', 'function'], async (channelId, name, flavorText, respond) => {
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
bindEvent(
|
||||
client,
|
||||
identity,
|
||||
{ verified: true, privileges: ['modify_channels'] },
|
||||
'update-channel',
|
||||
['string', 'string', 'string?', 'function'],
|
||||
async (channelId, name, flavorText, respond) => {
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
|
||||
flavorText = flavorText || null;
|
||||
flavorText = flavorText || null;
|
||||
|
||||
if (name.length === 0 || name.length > MAX_CHANNEL_NAME_LENGTH || !/^[A-Za-z0-9-]+$/.exec(name)) {
|
||||
throw new EventError('invalid name');
|
||||
}
|
||||
if (flavorText !== null && (flavorText.length === 0 || flavorText.length > MAX_CHANNEL_FLAVOR_TEXT_LENGTH)) {
|
||||
throw new EventError('invalid flavor text');
|
||||
}
|
||||
if (name.length === 0 || name.length > MAX_CHANNEL_NAME_LENGTH || !/^[A-Za-z0-9-]+$/.exec(name)) {
|
||||
throw new EventError('invalid name');
|
||||
}
|
||||
if (
|
||||
flavorText !== null &&
|
||||
(flavorText.length === 0 || flavorText.length > MAX_CHANNEL_FLAVOR_TEXT_LENGTH)
|
||||
) {
|
||||
throw new EventError('invalid flavor text');
|
||||
}
|
||||
|
||||
const updatedChannel = await DB.updateChannel(identity.guildId, channelId, name, flavorText);
|
||||
const updatedChannel = await DB.updateChannel(identity.guildId, channelId, name, flavorText);
|
||||
|
||||
respond(null, updatedChannel);
|
||||
respond(null, updatedChannel);
|
||||
|
||||
io.to(identity.guildId).emit('update-channel', updatedChannel);
|
||||
});
|
||||
io.to(identity.guildId).emit('update-channel', updatedChannel);
|
||||
},
|
||||
);
|
||||
|
||||
/*
|
||||
* gets the list of tokens that have been or are available to use
|
||||
@ -411,12 +465,19 @@ function bindAdminEvents(io: socketio.Server, client: socketio.Socket, identity:
|
||||
* @param respond.errStr Error string if an error, else null
|
||||
* @param respond.tokens The list of outstanding tokens in form of [ { id, token, member_id, created, expires } ]
|
||||
*/
|
||||
bindEvent(client, identity, { verified: true, privileges: ['modify_members'] }, 'fetch-tokens', ['function'], async respond => {
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
LOG.debug(`u#${identity.memberId}: fetching tokens`);
|
||||
const tokens = await DB.getTokens(identity.guildId);
|
||||
respond(null, tokens);
|
||||
});
|
||||
bindEvent(
|
||||
client,
|
||||
identity,
|
||||
{ verified: true, privileges: ['modify_members'] },
|
||||
'fetch-tokens',
|
||||
['function'],
|
||||
async respond => {
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
LOG.debug(`u#${identity.memberId}: fetching tokens`);
|
||||
const tokens = await DB.getTokens(identity.guildId);
|
||||
respond(null, tokens);
|
||||
},
|
||||
);
|
||||
|
||||
/*
|
||||
* creates a token to allow a new member to register
|
||||
@ -424,16 +485,23 @@ function bindAdminEvents(io: socketio.Server, client: socketio.Socket, identity:
|
||||
* @param respond.errStr Error string if an error, else null
|
||||
* @param respond.token The token that was created in form [ { id, token, member_id, created, expires } ]
|
||||
*/
|
||||
bindEvent(client, identity, { verified: true, privileges: ['modify_members'] }, 'create-token', ['string?', 'function'], async (expiresAfter, respond) => {
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
bindEvent(
|
||||
client,
|
||||
identity,
|
||||
{ verified: true, privileges: ['modify_members'] },
|
||||
'create-token',
|
||||
['string?', 'function'],
|
||||
async (expiresAfter, respond) => {
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
|
||||
LOG.debug(`u#${identity.memberId}: creating token with expiresAfter=${expiresAfter}`);
|
||||
const token = await DB.createToken(identity.guildId, expiresAfter);
|
||||
respond(null, token);
|
||||
LOG.debug(`u#${identity.memberId}: creating token with expiresAfter=${expiresAfter}`);
|
||||
const token = await DB.createToken(identity.guildId, expiresAfter);
|
||||
respond(null, token);
|
||||
|
||||
const targetRoom = guildPrivilegeRoomName(identity.guildId as string, 'modify_members');
|
||||
io.in(targetRoom).emit('create-token', token);
|
||||
});
|
||||
const targetRoom = guildPrivilegeRoomName(identity.guildId as string, 'modify_members');
|
||||
io.in(targetRoom).emit('create-token', token);
|
||||
},
|
||||
);
|
||||
|
||||
/*
|
||||
* revokes a token so that it can no longer be used
|
||||
@ -441,24 +509,31 @@ function bindAdminEvents(io: socketio.Server, client: socketio.Socket, identity:
|
||||
* @param respond function(errStr) as a socket.io response function
|
||||
* @param respond.errStr Error string if an error, else null
|
||||
*/
|
||||
bindEvent(client, identity, { verified: true, privileges: ['modify_members'] }, 'revoke-token', ['string', 'function'], async (token, respond) => {
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
if (!(await DB.isTokenReal(token))) {
|
||||
throw new EventError('fake token');
|
||||
}
|
||||
if (!(await DB.isTokenForGuild(token, identity.guildId))) {
|
||||
throw new EventError('fake token', undefined, 'tried to revoke token for a different guild');
|
||||
}
|
||||
if (await DB.isTokenTaken(token)) {
|
||||
throw new EventError('token already taken');
|
||||
}
|
||||
LOG.debug(`u#${identity.memberId}: revoking t#${token}`);
|
||||
const revokedToken = await DB.revokeToken(identity.guildId, token);
|
||||
respond(null);
|
||||
bindEvent(
|
||||
client,
|
||||
identity,
|
||||
{ verified: true, privileges: ['modify_members'] },
|
||||
'revoke-token',
|
||||
['string', 'function'],
|
||||
async (token, respond) => {
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
if (!(await DB.isTokenReal(token))) {
|
||||
throw new EventError('fake token', undefined, 'tokenId: ' + token);
|
||||
}
|
||||
if (!(await DB.isTokenForGuild(token, identity.guildId))) {
|
||||
throw new EventError('fake token', undefined, 'tried to revoke token for a different guild');
|
||||
}
|
||||
if (await DB.isTokenTaken(token)) {
|
||||
throw new EventError('token already taken');
|
||||
}
|
||||
LOG.debug(`u#${identity.memberId}: revoking t#${token}`);
|
||||
const revokedToken = await DB.revokeToken(identity.guildId, token);
|
||||
respond(null);
|
||||
|
||||
const targetRoom = guildPrivilegeRoomName(identity.guildId as string, 'modify_members');
|
||||
io.in(targetRoom).emit('revoke-token', revokedToken);
|
||||
});
|
||||
const targetRoom = guildPrivilegeRoomName(identity.guildId as string, 'modify_members');
|
||||
io.in(targetRoom).emit('revoke-token', revokedToken);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function bindActionEvents(io: socketio.Server, client: socketio.Socket, identity: IIdentity) {
|
||||
@ -470,19 +545,30 @@ function bindActionEvents(io: socketio.Server, client: socketio.Socket, identity
|
||||
* @param respond.errStr Error string if an error, else null
|
||||
* @param respond.message Sent message data object parsed from PostgreSQL
|
||||
*/
|
||||
bindEvent(client, identity, { verified: true }, 'send-message', ['string', 'string', 'function'], async (channelId, text, respond) => {
|
||||
if (text.length > MAX_TEXT_MESSAGE_LENGTH) throw new EventError('message is too long');
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
if (!identity.memberId) throw new EventError('identity no memberId');
|
||||
bindEvent(
|
||||
client,
|
||||
identity,
|
||||
{ verified: true },
|
||||
'send-message',
|
||||
['string', 'string', 'function'],
|
||||
async (channelId, text, respond) => {
|
||||
if (text.length > MAX_TEXT_MESSAGE_LENGTH) throw new EventError('message is too long');
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
if (!identity.memberId) throw new EventError('identity no memberId');
|
||||
|
||||
const message = await DB.insertMessage(identity.guildId, channelId, identity.memberId, text);
|
||||
const message = await DB.insertMessage(identity.guildId, channelId, identity.memberId, text);
|
||||
|
||||
LOG.info(`m#${message.id} ch#${message.channel_id} s@${formatDate(message.sent_dtg)} u#${message.member_id}: ${message.text}`);
|
||||
LOG.info(
|
||||
`m#${message.id} ch#${message.channel_id} s@${formatDate(message.sent_dtg)} u#${message.member_id}: ${
|
||||
message.text
|
||||
}`,
|
||||
);
|
||||
|
||||
respond(null, message);
|
||||
respond(null, message);
|
||||
|
||||
io.to(identity.guildId).emit('new-message', message);
|
||||
});
|
||||
io.to(identity.guildId).emit('new-message', message);
|
||||
},
|
||||
);
|
||||
|
||||
/*
|
||||
* send a message with a resource to a channel
|
||||
@ -494,79 +580,103 @@ function bindActionEvents(io: socketio.Server, client: socketio.Socket, identity
|
||||
* @param respond.errStr Error string if an error, else null
|
||||
* @param respond.message Sent message data object parsed from PostgreSQL
|
||||
*/
|
||||
bindEvent(client, identity, { verified: true }, 'send-message-with-resource', ['string', 'string?', 'buffer', 'string', 'function'], async (channelId, text, resource, resourceName, respond) => {
|
||||
if (text && text.length > MAX_TEXT_MESSAGE_LENGTH) throw new EventError('message is too long');
|
||||
if (resource.length > MAX_RESOURCE_SIZE) throw new EventError('resource is too large');
|
||||
bindEvent(
|
||||
client,
|
||||
identity,
|
||||
{ verified: true },
|
||||
'send-message-with-resource',
|
||||
['string', 'string?', 'buffer', 'string', 'function'],
|
||||
async (channelId, text, resource, resourceName, respond) => {
|
||||
if (text && text.length > MAX_TEXT_MESSAGE_LENGTH) throw new EventError('message is too long');
|
||||
if (resource.length > MAX_RESOURCE_SIZE) throw new EventError('resource is too large');
|
||||
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
if (!identity.memberId) throw new EventError('identity no memberId');
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
if (!identity.memberId) throw new EventError('identity no memberId');
|
||||
|
||||
resourceName = resourceName.trim();
|
||||
resourceName = resourceName.replace(/[^A-Za-z0-9 .]/g, '_'); // only alphanumerics for file names
|
||||
resourceName = resourceName.trim();
|
||||
resourceName = resourceName.replace(/[^A-Za-z0-9 .]/g, '_'); // only alphanumerics for file names
|
||||
|
||||
LOG.info(`u#${identity.memberId}: resource message with resource of size: ${resource.length} bytes`);
|
||||
LOG.info(`u#${identity.memberId}: resource message with resource of size: ${resource.length} bytes`);
|
||||
|
||||
// try to get the dimensions of the resource if it is an image so that we can scale it down for the preview
|
||||
const fileType = (await FileType.fromBuffer(resource)) ?? { mime: null, ext: null };
|
||||
// try to get the dimensions of the resource if it is an image so that we can scale it down for the preview
|
||||
const fileType = (await FileType.fromBuffer(resource)) ?? { mime: null, ext: null };
|
||||
|
||||
const dimensions: { width: number | null; height: number | null } = { width: null, height: null };
|
||||
const dimensions: { width: number | null; height: number | null } = { width: null, height: null };
|
||||
|
||||
switch (fileType.mime) {
|
||||
case 'image/png':
|
||||
case 'image/jpeg':
|
||||
case 'image/gif': {
|
||||
const size = sizeOf(resource);
|
||||
dimensions.width = size.width ?? null;
|
||||
dimensions.height = size.height ?? null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// pre-scale the image (optimized for the electron discord client's max-dimensions of 400x300)
|
||||
let resourcePreview: Buffer | null = null;
|
||||
if (dimensions.width !== null && dimensions.height !== null) {
|
||||
let previewWidth = dimensions.width;
|
||||
let previewHeight = dimensions.height;
|
||||
if (previewWidth > 400) {
|
||||
const scale = 400 / previewWidth;
|
||||
previewWidth *= scale;
|
||||
previewHeight *= scale;
|
||||
}
|
||||
if (previewHeight > 300) {
|
||||
const scale = 300 / previewHeight;
|
||||
previewWidth *= scale;
|
||||
previewHeight *= scale;
|
||||
}
|
||||
// jpeg for image compression and trash visuals B)
|
||||
resourcePreview = await sharp(resource).resize(Math.floor(previewWidth), Math.floor(previewHeight)).jpeg().toBuffer();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let message: any | null = null;
|
||||
try {
|
||||
await DB.queueTransaction(async () => {
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
if (!identity.memberId) throw new EventError('identity no guildId');
|
||||
|
||||
let resourcePreviewId: string | null = null;
|
||||
if (resourcePreview) {
|
||||
resourcePreviewId = await DB.insertResource(identity.guildId, resourcePreview);
|
||||
switch (fileType.mime) {
|
||||
case 'image/png':
|
||||
case 'image/jpeg':
|
||||
case 'image/gif': {
|
||||
const size = sizeOf(resource);
|
||||
dimensions.width = size.width ?? null;
|
||||
dimensions.height = size.height ?? null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const resourceId = await DB.insertResource(identity.guildId, resource);
|
||||
// pre-scale the image (optimized for the electron discord client's max-dimensions of 400x300)
|
||||
let resourcePreview: Buffer | null = null;
|
||||
if (dimensions.width !== null && dimensions.height !== null) {
|
||||
let previewWidth = dimensions.width;
|
||||
let previewHeight = dimensions.height;
|
||||
if (previewWidth > 400) {
|
||||
const scale = 400 / previewWidth;
|
||||
previewWidth *= scale;
|
||||
previewHeight *= scale;
|
||||
}
|
||||
if (previewHeight > 300) {
|
||||
const scale = 300 / previewHeight;
|
||||
previewWidth *= scale;
|
||||
previewHeight *= scale;
|
||||
}
|
||||
// jpeg for image compression and trash visuals B)
|
||||
resourcePreview = await sharp(resource)
|
||||
.resize(Math.floor(previewWidth), Math.floor(previewHeight))
|
||||
.jpeg()
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
message = await DB.insertMessageWithResource(identity.guildId, channelId, identity.memberId, text, resourceId, resourceName, dimensions.width, dimensions.height, resourcePreviewId);
|
||||
});
|
||||
} catch (e) {
|
||||
throw new EventError('unable to insert message with resource', e as Error);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let message: any | null = null;
|
||||
try {
|
||||
await DB.queueTransaction(async () => {
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
if (!identity.memberId) throw new EventError('identity no guildId');
|
||||
|
||||
LOG.info(`m#${message.id} ch#${message.channel_id} s@${formatDate(message.sent_dtg)} u#${message.member_id}: ${message.text ? message.text + ' / ' : ''}${message.resource_name}`);
|
||||
let resourcePreviewId: string | null = null;
|
||||
if (resourcePreview) {
|
||||
resourcePreviewId = await DB.insertResource(identity.guildId, resourcePreview);
|
||||
}
|
||||
|
||||
respond(null, message);
|
||||
const resourceId = await DB.insertResource(identity.guildId, resource);
|
||||
|
||||
io.to(identity.guildId).emit('new-message', message);
|
||||
});
|
||||
message = await DB.insertMessageWithResource(
|
||||
identity.guildId,
|
||||
channelId,
|
||||
identity.memberId,
|
||||
text,
|
||||
resourceId,
|
||||
resourceName,
|
||||
dimensions.width,
|
||||
dimensions.height,
|
||||
resourcePreviewId,
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
throw new EventError('unable to insert message with resource', e as Error);
|
||||
}
|
||||
|
||||
LOG.info(
|
||||
`m#${message.id} ch#${message.channel_id} s@${formatDate(message.sent_dtg)} u#${message.member_id}: ${
|
||||
message.text ? message.text + ' / ' : ''
|
||||
}${message.resource_name}`,
|
||||
);
|
||||
|
||||
respond(null, message);
|
||||
|
||||
io.to(identity.guildId).emit('new-message', message);
|
||||
},
|
||||
);
|
||||
|
||||
/*
|
||||
* set the status for a verified member
|
||||
@ -600,20 +710,28 @@ function bindActionEvents(io: socketio.Server, client: socketio.Socket, identity
|
||||
* @param respond.errStr Error string if an error, else null
|
||||
* @param respond.updatedMember The updated member object (note: an update-member event will be emitted)
|
||||
*/
|
||||
bindEvent(client, identity, { verified: true }, 'set-display-name', ['string', 'function'], async (displayName, respond) => {
|
||||
if (displayName.length > MAX_DISPLAY_NAME_LENGTH || displayName.length === 0) throw new EventError('invalid display name');
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
if (!identity.memberId) throw new EventError('identity no memberId');
|
||||
bindEvent(
|
||||
client,
|
||||
identity,
|
||||
{ verified: true },
|
||||
'set-display-name',
|
||||
['string', 'function'],
|
||||
async (displayName, respond) => {
|
||||
if (displayName.length > MAX_DISPLAY_NAME_LENGTH || displayName.length === 0)
|
||||
throw new EventError('invalid display name');
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
if (!identity.memberId) throw new EventError('identity no memberId');
|
||||
|
||||
LOG.info(`u#${identity.memberId}: setting display name to ${displayName}`);
|
||||
LOG.info(`u#${identity.memberId}: setting display name to ${displayName}`);
|
||||
|
||||
await DB.setMemberDisplayName(identity.guildId, identity.memberId, displayName);
|
||||
const updated = await DB.getMember(identity.guildId, identity.memberId);
|
||||
await DB.setMemberDisplayName(identity.guildId, identity.memberId, displayName);
|
||||
const updated = await DB.getMember(identity.guildId, identity.memberId);
|
||||
|
||||
respond(null, updated);
|
||||
respond(null, updated);
|
||||
|
||||
io.to(identity.guildId).emit('update-member', updated);
|
||||
});
|
||||
io.to(identity.guildId).emit('update-member', updated);
|
||||
},
|
||||
);
|
||||
|
||||
/*
|
||||
* set the status for a verified member
|
||||
@ -622,31 +740,38 @@ function bindActionEvents(io: socketio.Server, client: socketio.Socket, identity
|
||||
* @param respond.errStr Error string if an error, else null
|
||||
* @param respond.updatedMember The updated member object (note: an update-member event will be emitted)
|
||||
*/
|
||||
bindEvent(client, identity, { verified: true }, 'set-avatar', ['buffer', 'function'], async (avatarBuff, respond) => {
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
if (!identity.memberId) throw new EventError('identity no memberId');
|
||||
bindEvent(
|
||||
client,
|
||||
identity,
|
||||
{ verified: true },
|
||||
'set-avatar',
|
||||
['buffer', 'function'],
|
||||
async (avatarBuff, respond) => {
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
if (!identity.memberId) throw new EventError('identity no memberId');
|
||||
|
||||
if (avatarBuff.length > MAX_AVATAR_SIZE) {
|
||||
LOG.warn(`c#${client.id}: avatar too large`);
|
||||
respond('buffer too large');
|
||||
return;
|
||||
}
|
||||
if (avatarBuff.length > MAX_AVATAR_SIZE) {
|
||||
LOG.warn(`c#${client.id}: avatar too large`);
|
||||
respond('buffer too large');
|
||||
return;
|
||||
}
|
||||
|
||||
const typeResult = (await FileType.fromBuffer(avatarBuff)) ?? { mime: null, ext: null };
|
||||
if ((['image/png', 'image/jpeg', 'image/jpg'] as (string | null)[]).indexOf(typeResult.mime) === -1) {
|
||||
throw new EventError('invalid avatar buffer');
|
||||
}
|
||||
const typeResult = (await FileType.fromBuffer(avatarBuff)) ?? { mime: null, ext: null };
|
||||
if ((['image/png', 'image/jpeg', 'image/jpg'] as (string | null)[]).indexOf(typeResult.mime) === -1) {
|
||||
throw new EventError('invalid avatar buffer');
|
||||
}
|
||||
|
||||
LOG.info(`u#${identity.memberId}: uploaded new avatar`);
|
||||
LOG.info(`u#${identity.memberId}: uploaded new avatar`);
|
||||
|
||||
const resourceId = await DB.insertResource(identity.guildId, avatarBuff);
|
||||
await DB.setMemberAvatarResourceId(identity.guildId, identity.memberId, resourceId);
|
||||
const updated = await DB.getMember(identity.guildId, identity.memberId);
|
||||
const resourceId = await DB.insertResource(identity.guildId, avatarBuff);
|
||||
await DB.setMemberAvatarResourceId(identity.guildId, identity.memberId, resourceId);
|
||||
const updated = await DB.getMember(identity.guildId, identity.memberId);
|
||||
|
||||
respond(null, updated);
|
||||
respond(null, updated);
|
||||
|
||||
io.to(identity.guildId).emit('update-member', updated);
|
||||
});
|
||||
io.to(identity.guildId).emit('update-member', updated);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function bindFetchEvents(client: socketio.Socket, identity: IIdentity): void {
|
||||
@ -688,13 +813,20 @@ function bindFetchEvents(client: socketio.Socket, identity: IIdentity): void {
|
||||
* @param respond.errStr Error string if an error, else null
|
||||
* @param respond.messages List of message data objects in the channel from PostgreSQL
|
||||
*/
|
||||
bindEvent(client, identity, { verified: true }, 'fetch-messages-recent', ['string', 'number', 'function'], async (channelId, number, respond) => {
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
if (!identity.memberId) throw new EventError('identity no memberId');
|
||||
LOG.info(`u#${identity.memberId}: fetching recent messages for ch#${channelId} number: ${number}`);
|
||||
const messages = await DB.getMessagesRecent(identity.guildId, channelId, number);
|
||||
respond(null, messages);
|
||||
});
|
||||
bindEvent(
|
||||
client,
|
||||
identity,
|
||||
{ verified: true },
|
||||
'fetch-messages-recent',
|
||||
['string', 'number', 'function'],
|
||||
async (channelId, number, respond) => {
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
if (!identity.memberId) throw new EventError('identity no memberId');
|
||||
LOG.info(`u#${identity.memberId}: fetching recent messages for ch#${channelId} number: ${number}`);
|
||||
const messages = await DB.getMessagesRecent(identity.guildId, channelId, number);
|
||||
respond(null, messages);
|
||||
},
|
||||
);
|
||||
|
||||
/*
|
||||
* client.on('fetch-messages-before', (channelId, messageId, number, respond(errStr, messages)))
|
||||
@ -706,13 +838,22 @@ function bindFetchEvents(client: socketio.Socket, identity: IIdentity): void {
|
||||
* @param respond.errStr Error string if an error, else null
|
||||
* @param respond.messages List of message data objects in the channel from PostgreSQL
|
||||
*/
|
||||
bindEvent(client, identity, { verified: true }, 'fetch-messages-before', ['string', 'string', 'number', 'function'], async (channelId, messageOrderId, number, respond) => {
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
if (!identity.memberId) throw new EventError('identity no memberId');
|
||||
LOG.info(`u#${identity.memberId}: fetching messages before ch#${channelId} mo#${messageOrderId} number: ${number}`);
|
||||
const messages = await DB.getMessagesBefore(identity.guildId, channelId, messageOrderId, number);
|
||||
respond(null, messages);
|
||||
});
|
||||
bindEvent(
|
||||
client,
|
||||
identity,
|
||||
{ verified: true },
|
||||
'fetch-messages-before',
|
||||
['string', 'string', 'number', 'function'],
|
||||
async (channelId, messageOrderId, number, respond) => {
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
if (!identity.memberId) throw new EventError('identity no memberId');
|
||||
LOG.info(
|
||||
`u#${identity.memberId}: fetching messages before ch#${channelId} mo#${messageOrderId} number: ${number}`,
|
||||
);
|
||||
const messages = await DB.getMessagesBefore(identity.guildId, channelId, messageOrderId, number);
|
||||
respond(null, messages);
|
||||
},
|
||||
);
|
||||
|
||||
/*
|
||||
* fetch messages coming after the specified message
|
||||
@ -723,13 +864,22 @@ function bindFetchEvents(client: socketio.Socket, identity: IIdentity): void {
|
||||
* @param respond.errStr Error string if an error, else null
|
||||
* @param respond.messages List of message data objects in the channel from PostgreSQL
|
||||
*/
|
||||
bindEvent(client, identity, { verified: true }, 'fetch-messages-after', ['string', 'string', 'number', 'function'], async (channelId, messageOrderId, number, respond) => {
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
if (!identity.memberId) throw new EventError('identity no memberId');
|
||||
LOG.info(`u#${identity.memberId}: fetching messages after ch#${channelId} mo#${messageOrderId} number: ${number}`);
|
||||
const messages = await DB.getMessagesAfter(identity.guildId, channelId, messageOrderId, number);
|
||||
respond(null, messages);
|
||||
});
|
||||
bindEvent(
|
||||
client,
|
||||
identity,
|
||||
{ verified: true },
|
||||
'fetch-messages-after',
|
||||
['string', 'string', 'number', 'function'],
|
||||
async (channelId, messageOrderId, number, respond) => {
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
if (!identity.memberId) throw new EventError('identity no memberId');
|
||||
LOG.info(
|
||||
`u#${identity.memberId}: fetching messages after ch#${channelId} mo#${messageOrderId} number: ${number}`,
|
||||
);
|
||||
const messages = await DB.getMessagesAfter(identity.guildId, channelId, messageOrderId, number);
|
||||
respond(null, messages);
|
||||
},
|
||||
);
|
||||
|
||||
/*
|
||||
* @param resourceId The id of the resource to fetch
|
||||
@ -737,13 +887,20 @@ function bindFetchEvents(client: socketio.Socket, identity: IIdentity): void {
|
||||
* @param respond.errStr Error string if an error, else null
|
||||
* @param respond.resource Resource object { id, guild_id, hash, data }
|
||||
*/
|
||||
bindEvent(client, identity, { verified: true }, 'fetch-resource', ['string', 'function'], async (resourceId, respond) => {
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
if (!identity.memberId) throw new EventError('identity no memberId');
|
||||
LOG.info(`u#${identity.memberId}: fetching r#${resourceId}`);
|
||||
const resource = await DB.getResource(identity.guildId, resourceId);
|
||||
respond(null, resource);
|
||||
});
|
||||
bindEvent(
|
||||
client,
|
||||
identity,
|
||||
{ verified: true },
|
||||
'fetch-resource',
|
||||
['string', 'function'],
|
||||
async (resourceId, respond) => {
|
||||
if (!identity.guildId) throw new EventError('identity no guildId');
|
||||
if (!identity.memberId) throw new EventError('identity no memberId');
|
||||
LOG.info(`u#${identity.memberId}: fetching r#${resourceId}`);
|
||||
const resource = await DB.getResource(identity.guildId, resourceId);
|
||||
respond(null, resource);
|
||||
},
|
||||
);
|
||||
|
||||
/*
|
||||
* fetch members in the guild
|
||||
|
Loading…
Reference in New Issue
Block a user