From 563a0ca46d9793e2a1affe77c7283272f9f50ab4 Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Tue, 1 Oct 2024 16:01:44 -0700 Subject: [PATCH] implementing contact module --- app/client/web/src/WebCrypto.ts | 2 +- app/client/web/src/access/Access.tsx | 2 +- app/client/web/src/settings/Settings.tsx | 6 +- app/sdk/src/contact.ts | 114 ++++++++++++++++++++++- app/sdk/src/entities.ts | 4 +- app/sdk/src/items.ts | 42 +++++++-- app/sdk/src/net/getCards.ts | 11 +++ 7 files changed, 165 insertions(+), 16 deletions(-) create mode 100644 app/sdk/src/net/getCards.ts diff --git a/app/client/web/src/WebCrypto.ts b/app/client/web/src/WebCrypto.ts index 04f64623..3ce108cf 100644 --- a/app/client/web/src/WebCrypto.ts +++ b/app/client/web/src/WebCrypto.ts @@ -5,7 +5,7 @@ import { JSEncrypt } from 'jsencrypt' export class WebCrypto implements Crypto { // generate salt for pbk function - public async pbkdfSalt(): { saltHex: string } { + public async pbkdfSalt(): Promise<{ saltHex: string }> { const salt = CryptoJS.lib.WordArray.random(128 / 8); const saltHex = salt.toString(); return { saltHex }; diff --git a/app/client/web/src/access/Access.tsx b/app/client/web/src/access/Access.tsx index 1cc28b28..d4cc0975 100644 --- a/app/client/web/src/access/Access.tsx +++ b/app/client/web/src/access/Access.tsx @@ -44,7 +44,7 @@ export function Access() { await actions.adminLogin() } otpClose() - } catch (err: {message: string}) { + } catch (err: any) { console.log(err.message) if ( err.message === '405' || diff --git a/app/client/web/src/settings/Settings.tsx b/app/client/web/src/settings/Settings.tsx index eaf5cd73..64fb3b26 100644 --- a/app/client/web/src/settings/Settings.tsx +++ b/app/client/web/src/settings/Settings.tsx @@ -1,4 +1,4 @@ -import React, { useSettings } from './useSettings.hook' +import { useSettings } from './useSettings.hook' import { Modal, Textarea, @@ -42,7 +42,7 @@ import { } from '@tabler/icons-react' import { modals } from '@mantine/modals' import { useDisclosure } from '@mantine/hooks' -import { useCallback, useState, useRef } from 'react' +import React, { useCallback, useState, useRef } from 'react' import Cropper from 'react-easy-crop' import { Area } from 'react-easy-crop/types' @@ -179,7 +179,7 @@ export function Settings({ showLogout }: { showLogout: boolean }) { try { await actions.confirmMFA(); mfaClose(); - } catch (err) { + } catch (err: any) { if (err.message === '401') { setAuthMessage(state.strings.mfaError); } else if (err.message === '429') { diff --git a/app/sdk/src/contact.ts b/app/sdk/src/contact.ts index be31d71e..a1420bdc 100644 --- a/app/sdk/src/contact.ts +++ b/app/sdk/src/contact.ts @@ -3,7 +3,12 @@ import type { Contact, Logging } from './api'; import type { Card, Topic, Asset, Tag, Profile, Participant} from './types'; import type { CardEntity } from './entities'; import type { ArticleRevision, ArticleDetail, ChannelRevision, ChannelSummary, ChannelDetail, CardRevision, CardNotification, CardProfile, CardDetail } from './items'; +import { defaultConfigItem } from './items'; import { Store } from './store'; +import { getCards } from './net/getCards'; + +const CLOSE_POLL_MS = 100; +const RETRY_POLL_MS = 2000; export class ContactModule implements Contact { @@ -35,9 +40,9 @@ export class ContactModule implements Contact { this.node = node; this.secure = secure; this.log = log; + this.store = store; this.emitter = new EventEmitter(); - this.cardGuid = new Map(); this.cardEntries = new Map(); this.articleEntries = new Map>; this.channelEntries = new Map>; @@ -122,7 +127,7 @@ export class ContactModule implements Contact { }); // load map of channels - const channels = await this.store.getContactCardChannles(this.guid); + const channels = await this.store.getContactCardChannels(this.guid); channels.forEach(({ cardId, channelId, item }) => { if (!this.channelEntries.has(cardId)) { this.channelEntries.set(cardId, new Map>); @@ -135,7 +140,109 @@ export class ContactModule implements Contact { await this.sync(); } + private getCardEntry(id: string) { item: CardItem, card: Card } { + const entry = this.cardEntries.get(id); + if (entry) { + return entry; + } + const item = JSON.parse(JSON.strifying(defaultCardItem)); + const card = this.setCard(item); + const cardEntry = { item, card }; + this.cardEntries.set(id, cardEntry); + return cardEntry; + } + private async sync(): Promise { + if (!this.syncing) { + this.syncing = true; + while (this.nextRevision && !this.closing) { + if (this.revision !== this.nextRevision) { + const nextRev = this.nextRevision; + try { + const { guid, node, secure, token } = this; + const delta = await getCards(node, secure, token, this.revision); + for (const entity of delta) { + const { id, revision, data } = entity; + if (data) { + const entry = this.getCardEntry(id); + + if (data.detailRevision !== entry.detail.revison) { + // update detail + entry.detail.revision = data.detailRevision; + // store detail + } + + if (data.profileRevision !== entry.profile.revision) { + // update profile + entry.profile.revision = data.profileRevision; + // store profile + } + + if (data.notifiedProfile !== entry.remote.profile) { + entry.remote.profile = data.notifiedProfile; + try { + // sync profile + } + catch (err) { + this.log.warn(err); + entry.offsync = true; + // store offsync + } + // store remote + } + + if (data.notifiedArticle !== entry.remote.article) { + entry.remote.article = data.notifiedArticle; + try { + // sync articles + } + catch (err) { + this.log.warn(err); + entry.offsync = true; + // store offsync + } + this.emitCardArticles(id); + // store remote + } + + if (data.notifiedChannel !== entry.remote.channel) { + entry.remote.channel = data.notifiedChannel; + try { + //sync channels + } + catch (err) { + this.log.warn(err); + entry.offsync = true; + this.emitCardChannels(id); + // store offsync + } + } + } + else { + this.cardEntries.delete(id); + } + } + + this.emitCards(); + await this.store.setContactRevision(guid, nextRev); + this.revision = nextRev; + if (this.nextRevision === nextRev) { + this.nextRevision = null; + } + this.log.info(`card revision: ${nextRev}`); + } + catch (err) { + this.log.warn(err); + await new Promise(r => setTimeout(r, RETRY_POLL_MS)); + } + } + + if (this.revision === this.nextRevsion) { + this.nextRevision = null; + } + } + this.syncing = false; + } } public addCardListener(ev: (cards: Card[]) => void): void { @@ -240,7 +347,8 @@ export class ContactModule implements Contact { } public async setRevision(rev: number): Promise { - console.log('set contact revision:', rev); + this.nextRevision = rev; + await this.sync(); } public async addCard(server: string, guid: string): Promise { diff --git a/app/sdk/src/entities.ts b/app/sdk/src/entities.ts index 9cd5d42a..ef79bb51 100644 --- a/app/sdk/src/entities.ts +++ b/app/sdk/src/entities.ts @@ -9,14 +9,14 @@ export type CardEntity = { notifiedProfile: number, notifiedArticle: number, notifiedChannel: number, - cardDetail: { + cardDetail?: { status: string, statusUpdated: number, token: string, notes: string, groups: [ string ] }, - cardProfile: { + cardProfile?: { guid: string, handle: string, name: string, diff --git a/app/sdk/src/items.ts b/app/sdk/src/items.ts index e26ad6b2..0fd40333 100644 --- a/app/sdk/src/items.ts +++ b/app/sdk/src/items.ts @@ -1,8 +1,3 @@ -export type CardRevision = { - detail: number, - profile: number, -} - export type CardNotification = { profile: number, article: number, @@ -10,12 +5,14 @@ export type CardNotification = { } export type CardDetail = { + revision: number, status: string, statusUpdated: number, token: string, } export type CardProfile = { + revision: number, handle: string, guid: string, name: string, @@ -82,13 +79,46 @@ export type ArticleDetail = { export type CardItem = { offsync: boolean, blocked: boolean, - revision: CardRevision, + revision: number, profile: CardProfile, detail: CardDetail, remote: CardNotification, sync: CardNotification, } +export const defaultCardItem = { + offsync: false, + blocked: false, + cardRevision: 0, + profileRevision: 0, + profile: { + handle: '', + guid: '', + name: '', + description: '', + location: '', + imageSet: false, + node: '', + seal: '', + }, + detailRevision: 0, + detail: { + status: '', + statusUpdateed: 0, + token: 0, + }, + remote: { + profile: 0, + article: 0, + channel: 0, + }, + sync: { + profile: 0, + article: 0, + channel: 0, + }, +}; + export type ArticleItem = { blocked: boolean, detail: ArticleDetail, diff --git a/app/sdk/src/net/getCards.ts b/app/sdk/src/net/getCards.ts new file mode 100644 index 00000000..0b8b974e --- /dev/null +++ b/app/sdk/src/net/getCards.ts @@ -0,0 +1,11 @@ +import { checkResponse, fetchWithTimeout } from './fetchUtil'; +import { CardEntity } from '../entities'; + +export async function getCards(node: string, secure: boolean, token: string, revision: number): Promise { + const param = revision ? `&revision=${revision}` : ''; + const endpoint = `http${secure ? 's' : ''}://${node}/contact/cards?agent=${token}${param}`; + const cards = await fetchWithTimeout(endpoint, { method: 'GET' }); + checkResponse(cards.status); + return await cards.json(); +} +