From d875a8f8fb164e2d10a2ea585d4d2f248d44dd8d Mon Sep 17 00:00:00 2001 From: Michael Peters Date: Mon, 13 Dec 2021 01:32:29 -0600 Subject: [PATCH] fixed subscriptions re-requesting because of re-created fetch functions --- src/client/webapp/data-types.ts | 19 ++++-- .../displays/display-guild-invites.tsx | 5 +- .../overlays/overlay-guild-settings.tsx | 5 ++ .../elements/require/guild-subscriptions.ts | 61 ++++++++++--------- src/client/webapp/guild-combined.ts | 34 +++++++++++ src/client/webapp/guild-socket.ts | 21 +++++++ src/client/webapp/guild-types.ts | 4 ++ src/client/webapp/guilds-manager.ts | 6 ++ src/server/db.ts | 7 ++- src/server/server-controller.ts | 21 +++++-- 10 files changed, 141 insertions(+), 42 deletions(-) diff --git a/src/client/webapp/data-types.ts b/src/client/webapp/data-types.ts index d007a0d..28e9239 100644 --- a/src/client/webapp/data-types.ts +++ b/src/client/webapp/data-types.ts @@ -323,20 +323,21 @@ export class Resource implements WithEquals { this.hash.toString('hex') === other.hash.toString('hex') ); } + + toString() { + return 'r#' + this.id; + } } export class Token implements WithEquals { - public id: string; - constructor( + public readonly id: string, public readonly token: string, public member: Member | { id: string } | null, public readonly created: Date, public readonly expires: Date | null, public readonly source?: unknown - ) { - this.id = token; // for comparison purposes - } + ) {} fill(members: Map) { if (this.member) { @@ -349,6 +350,7 @@ export class Token implements WithEquals { static fromDBData(dataToken: any): Token { return new Token( + dataToken.id, dataToken.token, dataToken.member_id ? { id: dataToken.member_id } : null, new Date(dataToken.created), @@ -359,11 +361,12 @@ export class Token implements WithEquals { equals(other: Token) { return ( + this.id === other.id && this.token === other.token && this.member?.id === other.member?.id && this.created === other.created && this.expires === other.expires - ) + ); } static sortFurthestExpiresFirst(a: Token, b: Token) { @@ -378,6 +381,10 @@ export class Token implements WithEquals { return 0; } } + + toString() { + return `t#${this.id}/${this.token}`; + } } export class NotInitializedError extends Error { diff --git a/src/client/webapp/elements/displays/display-guild-invites.tsx b/src/client/webapp/elements/displays/display-guild-invites.tsx index ae6a17e..4c6c8fe 100644 --- a/src/client/webapp/elements/displays/display-guild-invites.tsx +++ b/src/client/webapp/elements/displays/display-guild-invites.tsx @@ -72,7 +72,10 @@ const GuildInvitesDisplay: FC = (props: GuildInvitesDi
- +
); diff --git a/src/client/webapp/elements/overlays/overlay-guild-settings.tsx b/src/client/webapp/elements/overlays/overlay-guild-settings.tsx index ffb584f..14b7776 100644 --- a/src/client/webapp/elements/overlays/overlay-guild-settings.tsx +++ b/src/client/webapp/elements/overlays/overlay-guild-settings.tsx @@ -1,3 +1,8 @@ +import * as electronRemote from '@electron/remote'; +const electronConsole = electronRemote.getGlobal('console') as Console; +import Logger from '../../../../logger/logger'; +const LOG = Logger.create(__filename, electronConsole); + import React, { FC, useEffect, useMemo, useState } from "react"; import CombinedGuild from "../../guild-combined"; import ChoicesControl from "../components/control-choices"; diff --git a/src/client/webapp/elements/require/guild-subscriptions.ts b/src/client/webapp/elements/require/guild-subscriptions.ts index 2a552d3..5bd821a 100644 --- a/src/client/webapp/elements/require/guild-subscriptions.ts +++ b/src/client/webapp/elements/require/guild-subscriptions.ts @@ -70,23 +70,24 @@ export default class GuildSubscriptions { private static useGuildSubscriptionEffect( isMountedRef: React.MutableRefObject, subscriptionParams: EffectParams, - fetchFunc: (() => Promise) | (() => Promise), - fetchDeps: DependencyList + fetchFunc: (() => Promise) | (() => Promise) ) { const { guild, onFetch, onFetchError, bindEventsFunc, unbindEventsFunc } = subscriptionParams; - const fetchManagerFunc = useCallback(async () => { - if (!isMountedRef.current) return; - try { - const value = await fetchFunc(); + const fetchManagerFunc = useMemo(() => { + return async () => { if (!isMountedRef.current) return; - onFetch(value); - } catch (e: unknown) { - LOG.error('error fetching for subscription', e); - if (!isMountedRef.current) return; - onFetchError(e); + try { + const value = await fetchFunc(); + if (!isMountedRef.current) return; + onFetch(value); + } catch (e: unknown) { + LOG.error('error fetching for subscription', e); + if (!isMountedRef.current) return; + onFetchError(e); + } } - }, [ ...fetchDeps, fetchFunc ]); + }, [ fetchFunc ]); useEffect(() => { isMountedRef.current = true; @@ -111,8 +112,7 @@ export default class GuildSubscriptions { private static useSingleGuildSubscription( guild: CombinedGuild, eventMappingParams: SingleEventMappingParams, - fetchFunc: (() => Promise) | (() => Promise), - fetchDeps: DependencyList + fetchFunc: (() => Promise) | (() => Promise) ): [value: T | null, fetchError: unknown | null, events: EventEmitter] { const { updatedEventName, updatedEventArgsMap, conflictEventName, conflictEventArgsMap } = eventMappingParams; @@ -173,7 +173,7 @@ export default class GuildSubscriptions { onFetchError, bindEventsFunc, unbindEventsFunc - }, fetchFunc, fetchDeps); + }, fetchFunc); return [ value, fetchError, events ]; } @@ -187,8 +187,7 @@ export default class GuildSubscriptions { >( guild: CombinedGuild, eventMappingParams: MultipleEventMappingParams, - fetchFunc: (() => Promise) | (() => Promise), - fetchDeps: DependencyList + fetchFunc: (() => Promise) | (() => Promise) ): [value: T[] | null, fetchError: unknown | null, events: EventEmitter>] { const { newEventName, newEventArgsMap, @@ -291,35 +290,43 @@ export default class GuildSubscriptions { onFetchError, bindEventsFunc, unbindEventsFunc - }, fetchFunc, fetchDeps); + }, fetchFunc); return [ value, fetchError, events ]; } static useGuildMetadataSubscription(guild: CombinedGuild) { + const fetchMetadataFunc = useCallback(async () => { + //LOG.silly('fetching metadata for subscription'); + return await guild.fetchMetadata(); + }, [ guild ]); return GuildSubscriptions.useSingleGuildSubscription(guild, { updatedEventName: 'update-metadata', updatedEventArgsMap: (guildMeta: GuildMetadata) => guildMeta, conflictEventName: 'conflict-metadata', conflictEventArgsMap: (changesType: AutoVerifierChangesType, oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => newGuildMeta - }, async () => { - return await guild.fetchMetadata() - }, [ guild ]); + }, fetchMetadataFunc); } static useResourceSubscription(guild: CombinedGuild, resourceId: string | null) { + const fetchResourceFunc = useCallback(async () => { + //LOG.silly('fetching resource for subscription (resourceId: ' + resourceId + ')'); + if (resourceId === null) return null; + return await guild.fetchResource(resourceId); + }, [ guild, resourceId ]); return GuildSubscriptions.useSingleGuildSubscription(guild, { updatedEventName: 'update-resource', updatedEventArgsMap: (resource: Resource) => resource, conflictEventName: 'conflict-resource', conflictEventArgsMap: (query: IDQuery, changesType: AutoVerifierChangesType, oldResource: Resource, newResource: Resource) => newResource - }, async () => { - if (resourceId === null) return null; - return await guild.fetchResource(resourceId); - }, [ guild, resourceId ]); + }, fetchResourceFunc); } static useTokensSubscription(guild: CombinedGuild) { + const fetchTokensFunc = useCallback(async () => { + //LOG.silly('fetching tokens for subscription'); + return await guild.fetchTokens(); + }, [ guild ]); return GuildSubscriptions.useMultipleGuildSubscription(guild, { newEventName: 'new-tokens', newEventArgsMap: (tokens: Token[]) => tokens, @@ -330,8 +337,6 @@ export default class GuildSubscriptions { conflictEventName: 'conflict-tokens', conflictEventArgsMap: (changesType: AutoVerifierChangesType, changes: Changes) => changes, sortFunc: Token.sortFurthestExpiresFirst - }, async () => { - return await guild.fetchTokens(); - }, [ guild ]); + }, fetchTokensFunc); } } diff --git a/src/client/webapp/guild-combined.ts b/src/client/webapp/guild-combined.ts index 84fac75..348bba3 100644 --- a/src/client/webapp/guild-combined.ts +++ b/src/client/webapp/guild-combined.ts @@ -123,6 +123,40 @@ export default class CombinedGuild extends EventEmitter { + LOG.info(`g#${this.id} updated ${updatedResource}`); + this.ramGuild.handleResourceChanged(updatedResource); + this.personalDBGuild.handleResourceChanged(updatedResource); + this.emit('update-resource', updatedResource); + }); + + // Tokens + this.socketGuild.on('new-tokens', async (newTokens: Token[]) => { + for (const token of newTokens) { + LOG.info(`g#${this.id} new token ${token}`); + } + this.ramGuild.handleTokensAdded(newTokens); + this.personalDBGuild.handleTokensAdded(newTokens); + this.emit('new-tokens', newTokens); + }); + this.socketGuild.on('update-tokens', async (updatedTokens: Token[]) => { + for (const token of updatedTokens) { + LOG.info(`g#${this.id} updated ${token}`); + } + this.ramGuild.handleTokensChanged(updatedTokens); + this.personalDBGuild.handleTokensChanged(updatedTokens); + this.emit('update-tokens', updatedTokens); + }); + this.socketGuild.on('remove-tokens', async (removedTokens: Token[]) => { + for (const token of removedTokens) { + LOG.info(`g#${this.id} removed ${token}`); + } + this.ramGuild.handleTokensDeleted(removedTokens); + this.personalDBGuild.handleTokensDeleted(removedTokens); + this.emit('remove-tokens', removedTokens); + }); + const diskSocket = new PairVerifierFetchable(this.personalDBGuild, this.socketGuild); const ramDiskSocket = new PairVerifierFetchable(this.ramGuild, diskSocket); diff --git a/src/client/webapp/guild-socket.ts b/src/client/webapp/guild-socket.ts index 4ffc2f7..d8cc0ba 100644 --- a/src/client/webapp/guild-socket.ts +++ b/src/client/webapp/guild-socket.ts @@ -62,6 +62,27 @@ export default class SocketGuild extends EventEmitter implements As const message = Message.fromDBData(dataMessage); this.emit('new-messages', [ message ]); }); + + // TODO: The server does not emit update-resource + this.socket.on('update-resource', async (updatedDataResource: any) => { + const updatedResource = Resource.fromDBData(updatedDataResource); + this.emit('update-resource', updatedResource); + }); + + // TODO: The server does not emit new-token + this.socket.on('new-token', async (newDataToken: any) => { + const newToken = Token.fromDBData(newDataToken); + this.emit('new-tokens', [ newToken ]); + }); + // TODO: The server does not emit update-token + this.socket.on('update-token', async (updatedDataToken: any) => { + const updatedToken = Token.fromDBData(updatedDataToken); + this.emit('update-tokens', [ updatedToken ]); + }); + this.socket.on('revoke-token', async (revokedDataToken: any) => { + const revokedToken = Token.fromDBData(revokedDataToken); + this.emit('remove-tokens', [ revokedToken ]); + }); } public disconnect() { diff --git a/src/client/webapp/guild-types.ts b/src/client/webapp/guild-types.ts index 09cbab2..d3442bb 100644 --- a/src/client/webapp/guild-types.ts +++ b/src/client/webapp/guild-types.ts @@ -162,6 +162,10 @@ export const GuildEventNames = [ 'new-messages', 'update-messages', 'remove-messages', + 'update-resource', + 'new-tokens', + 'update-tokens', + 'remove-tokens', 'conflict-metadata', 'conflict-channels', 'conflict-members', diff --git a/src/client/webapp/guilds-manager.ts b/src/client/webapp/guilds-manager.ts index 53b9ed7..a0ddb7a 100644 --- a/src/client/webapp/guilds-manager.ts +++ b/src/client/webapp/guilds-manager.ts @@ -40,6 +40,12 @@ export default class GuildsManager extends EventEmitter<{ 'update-messages': (guild: CombinedGuild, updatedMessages: Message[]) => void; 'remove-messages': (guild: CombinedGuild, removedMessages: Message[]) => void; + 'update-resource': (guild: CombinedGuild, updatedResource: Resource) => void; + + 'new-tokens': (guild: CombinedGuild, newTokens: Token[]) => void; + 'update-tokens': (guild: CombinedGuild, updatedTokens: Token[]) => void; + 'remove-tokens': (guild: CombinedGuild, removedTokens: Token[]) => void; + 'conflict-metadata': (guild: CombinedGuild, changesType: AutoVerifierChangesType, oldGuildMeta: GuildMetadata, newGuildMeta: GuildMetadata) => void; 'conflict-channels': (guild: CombinedGuild, changesType: AutoVerifierChangesType, changes: Changes) => void; 'conflict-members': (guild: CombinedGuild, changesType: AutoVerifierChangesType, changes: Changes) => void; diff --git a/src/server/db.ts b/src/server/db.ts index b16e4ec..25c9484 100644 --- a/src/server/db.ts +++ b/src/server/db.ts @@ -596,10 +596,11 @@ export default class DB { return result; } - static async revokeToken(token: string): Promise { - const result = await db.query('DELETE FROM "tokens" WHERE "token"=$1', [ token ]); - if (result.rowCount != 1) { + static async revokeToken(guildId: string, token: string): Promise { + 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'); } + return result.rows[0]; } } diff --git a/src/server/server-controller.ts b/src/server/server-controller.ts index 0c87bd3..5396ebd 100644 --- a/src/server/server-controller.ts +++ b/src/server/server-controller.ts @@ -62,6 +62,10 @@ class SignatureError extends Error { } } +function guildPrivilegeName(guildId: string, privilege: string) { + return guildId + '&' + privilege; +} + function bindEvent( client: socketio.Socket, identity: IIdentity | null, @@ -283,9 +287,10 @@ function bindChallengeVerificationEvents(io: socketio.Server, client: socketio.S throw new EventError('invalid signature'); } + let member: any; try { await DB.setMemberStatus(identity.guildId, identity.memberId, 'online'); - const member = await DB.getMember(identity.guildId, identity.memberId); + member = await DB.getMember(identity.guildId, identity.memberId); io.to(identity.guildId).emit('update-member', member); } catch (e: unknown) { LOG.warn('unable to set status for m#' + identity.memberId, e); @@ -293,9 +298,13 @@ function bindChallengeVerificationEvents(io: socketio.Server, client: socketio.S } identity.verified = true; - client.join(identity.guildId); // join the socket.io guild room + const rooms = [ identity.guildId ].concat( + member.privileges.split(',').map((privilege: string) => guildPrivilegeName(identity.guildId as string, privilege)) + ); + LOG.debug(`c#${client.id} joining ${rooms.join(', ')}`); + client.join(rooms); + LOG.info(`c#${client.id}: verified as g#${identity.guildId} u#${identity.memberId}`); - respond(null, identity.memberId); } ); @@ -459,8 +468,12 @@ function bindAdminEvents(io: socketio.Server, client: socketio.Socket, identity: throw new EventError('token already taken'); } LOG.debug(`u#${identity.memberId}: revoking t#${token}`); - await DB.revokeToken(token); + const revokedToken = await DB.revokeToken(identity.guildId, token); respond(null); + + const targetRoom = guildPrivilegeName(identity.guildId as string, 'modify_members'); + LOG.debug('emitting revoke token to members in ' + targetRoom); + io.in(targetRoom).emit('revoke-token', revokedToken); } ); }