mirror of
https://github.com/balzack/databag.git
synced 2025-04-26 11:35:19 +00:00
1236 lines
48 KiB
TypeScript
1236 lines
48 KiB
TypeScript
import { EventEmitter } from 'eventemitter3';
|
|
import type { Contact, Focus } from './api';
|
|
import { Logging } from './logging';
|
|
import { FocusModule } from './focus';
|
|
import type { Card, Channel, Article, Topic, Asset, Tag, FocusDetail, Profile, Participant } from './types';
|
|
import { type CardEntity, avatar } from './entities';
|
|
import type { ArticleDetail, ChannelSummary, ChannelDetail, CardProfile, CardDetail, CardItem, ChannelItem, ArticleItem } from './items';
|
|
import { defaultCardItem, defaultChannelItem } from './items';
|
|
import { Store } from './store';
|
|
import { Crypto } from './crypto';
|
|
import { Staging } from './staging';
|
|
import { getCards } from './net/getCards';
|
|
import { getCardProfile } from './net/getCardProfile';
|
|
import { getCardDetail } from './net/getCardDetail';
|
|
import { setCardProfile } from './net/setCardProfile';
|
|
import { getContactProfile } from './net/getContactProfile';
|
|
import { getContactChannels } from './net/getContactChannels';
|
|
import { getContactChannelDetail } from './net/getContactChannelDetail';
|
|
import { getContactChannelSummary } from './net/getContactChannelSummary';
|
|
import { getCardImageUrl } from './net/getCardImageUrl';
|
|
import { addCard } from './net/addCard';
|
|
import { removeCard } from './net/removeCard';
|
|
import { getContactListing } from './net/getContactListing';
|
|
import { setCardConfirmed } from './net/setCardConfirmed';
|
|
import { setCardConnecting } from './net/setCardConnecting';
|
|
import { setCardConnected } from './net/setCardConnected';
|
|
import { removeContactChannel } from './net/removeContactChannel';
|
|
import { getContactChannelNotifications } from './net/getContactChannelNotifications';
|
|
import { setContactChannelNotifications } from './net/setContactChannelNotifications';
|
|
import { getRegistryImageUrl } from './net/getRegistryImageUrl';
|
|
import { getRegistryListing } from './net/getRegistryListing';
|
|
import { addFlag } from './net/addFlag';
|
|
import { getCardOpenMessage } from './net/getCardOpenMessage';
|
|
import { setCardOpenMessage } from './net/setCardOpenMessage';
|
|
import { getCardCloseMessage } from './net/getCardCloseMessage';
|
|
import { setCardCloseMessage } from './net/setCardCloseMessage';
|
|
import { getLegacyData } from './legacy';
|
|
|
|
const CLOSE_POLL_MS = 100;
|
|
const RETRY_POLL_MS = 2000;
|
|
|
|
export class ContactModule implements Contact {
|
|
private log: Logging;
|
|
private guid: string;
|
|
private token: string;
|
|
private node: string;
|
|
private secure: boolean;
|
|
private emitter: EventEmitter;
|
|
private articleTypes: string[];
|
|
private channelTypes: string[];
|
|
private seal: { privateKey: string; publicKey: string } | null;
|
|
private unsealAll: boolean;
|
|
private focus: FocusModule | null;
|
|
|
|
private crypto: Crypto | null;
|
|
private staging: Staging | null;
|
|
private store: Store;
|
|
private revision: number;
|
|
private nextRevision: number | null;
|
|
private syncing: boolean;
|
|
private closing: boolean;
|
|
private hasSynced: boolean;
|
|
|
|
// set of markers
|
|
private blockedCard: Set<string>;
|
|
private blockedCardChannel: Set<string>;
|
|
private read: Map<string, number>;
|
|
|
|
// set of cards to resync
|
|
private resync: Set<string>;
|
|
|
|
// view of cards
|
|
private cardEntries: Map<string, { item: CardItem; card: Card }>;
|
|
|
|
// view of articles
|
|
private articleEntries: Map<string, Map<string, { item: ArticleItem; article: Article }>>;
|
|
|
|
// view of channels
|
|
private channelEntries: Map<string, Map<string, { item: ChannelItem; channel: Channel }>>;
|
|
|
|
constructor(log: Logging, store: Store, crypto: Crypto | null, staging: Staging | null, guid: string, token: string, node: string, secure: boolean, articleTypes: string[], channelTypes: string[]) {
|
|
this.guid = guid;
|
|
this.token = token;
|
|
this.node = node;
|
|
this.secure = secure;
|
|
this.log = log;
|
|
this.store = store;
|
|
this.crypto = crypto;
|
|
this.staging = staging;
|
|
this.emitter = new EventEmitter();
|
|
this.articleTypes = articleTypes;
|
|
this.channelTypes = channelTypes;
|
|
this.unsealAll = false;
|
|
this.focus = null;
|
|
this.seal = null;
|
|
|
|
this.cardEntries = new Map<string, { item: CardItem; card: Card }>();
|
|
this.articleEntries = new Map<string, Map<string, { item: ArticleItem; article: Article }>>();
|
|
this.channelEntries = new Map<string, Map<string, { item: ChannelItem; channel: Channel }>>();
|
|
this.resync = new Set<string>();
|
|
this.blockedCard = new Set<string>();
|
|
this.blockedCardChannel = new Set<string>();
|
|
this.read = new Map<string, number>();
|
|
|
|
this.revision = 0;
|
|
this.syncing = true;
|
|
this.closing = false;
|
|
this.nextRevision = null;
|
|
this.hasSynced = false;
|
|
this.init();
|
|
}
|
|
|
|
private async init() {
|
|
const { guid } = this;
|
|
this.revision = await this.store.getContactRevision(guid);
|
|
|
|
const blockedCardMarkers = await this.store.getMarkers(guid, 'blocked_card');
|
|
blockedCardMarkers.forEach((marker) => {
|
|
this.blockedCard.add(marker.id);
|
|
});
|
|
const blockedCardChannelMarkers = await this.store.getMarkers(guid, 'blocked_card_channel');
|
|
blockedCardChannelMarkers.forEach((marker) => {
|
|
this.blockedCardChannel.add(marker.id);
|
|
});
|
|
const readMarkers = await this.store.getMarkers(guid, 'read_card_channel');
|
|
readMarkers.forEach((marker) => {
|
|
this.read.set(marker.id, parseInt(marker.value));
|
|
});
|
|
const hasSyncedMarkers = await this.store.getMarkers(guid, 'first_sync_complete');
|
|
this.hasSynced = hasSyncedMarkers.filter((marker) => (marker.id === 'contact')).length !== 0;
|
|
|
|
// load map of articles
|
|
const articles = await this.store.getContactCardArticles(guid);
|
|
articles.forEach(({ cardId, articleId, item }) => {
|
|
const articles = this.articleEntries.get(cardId);
|
|
const article = this.setArticle(cardId, articleId, item);
|
|
if (!articles) {
|
|
const entries = new Map<string, { item: ArticleItem; article: Article }>();
|
|
this.articleEntries.set(cardId, entries);
|
|
entries.set(articleId, { item, article });
|
|
} else {
|
|
articles.set(articleId, { item, article });
|
|
}
|
|
});
|
|
|
|
// load map of channels
|
|
const channels = await this.store.getContactCardChannels(guid);
|
|
channels.forEach(({ cardId, channelId, item }) => {
|
|
const channels = this.channelEntries.get(cardId);
|
|
const channel = this.setChannel(cardId, channelId, item);
|
|
if (!channels) {
|
|
const entries = new Map<string, { item: ChannelItem; channel: Channel }>();
|
|
this.channelEntries.set(cardId, entries);
|
|
entries.set(channelId, { item, channel });
|
|
} else {
|
|
channels.set(channelId, { item, channel });
|
|
}
|
|
});
|
|
|
|
// load map of cards
|
|
const cards = await this.store.getContacts(guid);
|
|
cards.forEach(({ cardId, item }) => {
|
|
const card = this.setCard(cardId, item);
|
|
this.cardEntries.set(cardId, { item, card });
|
|
this.emitArticles(cardId);
|
|
this.emitChannels(cardId);
|
|
});
|
|
this.emitCards();
|
|
|
|
this.unsealAll = true;
|
|
this.syncing = false;
|
|
await this.sync();
|
|
}
|
|
|
|
public async setRevision(rev: number): Promise<void> {
|
|
this.nextRevision = rev;
|
|
await this.sync();
|
|
}
|
|
|
|
public async close() {
|
|
this.closing = true;
|
|
if (this.focus) {
|
|
await this.focus.close();
|
|
this.focus = null;
|
|
}
|
|
while (this.syncing) {
|
|
await new Promise((r) => setTimeout(r, CLOSE_POLL_MS));
|
|
}
|
|
}
|
|
|
|
private isCardBlocked(cardId: string): boolean {
|
|
return this.blockedCard.has(cardId);
|
|
}
|
|
|
|
private async setCardBlocked(cardId: string) {
|
|
const entry = this.cardEntries.get(cardId);
|
|
if (!entry) {
|
|
throw new Error('card not found');
|
|
}
|
|
this.blockedCard.add(cardId);
|
|
entry.card = this.setCard(cardId, entry.item);
|
|
this.emitCards();
|
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
await this.store.setMarker(this.guid, 'blocked_card', cardId, JSON.stringify({cardId, timestamp}));
|
|
}
|
|
|
|
private async clearCardBlocked(cardId: string) {
|
|
const entry = this.cardEntries.get(cardId);
|
|
if (!entry) {
|
|
throw new Error('card not found');
|
|
}
|
|
this.blockedCard.delete(cardId);
|
|
entry.card = this.setCard(cardId, entry.item);
|
|
this.emitCards();
|
|
await this.store.clearMarker(this.guid, 'blocked_card', cardId);
|
|
}
|
|
|
|
private isChannelBlocked(cardId: string, channelId: string): boolean {
|
|
const id = `${cardId}:${channelId}`;
|
|
return this.blockedCardChannel.has(id);
|
|
}
|
|
|
|
private async setChannelBlocked(cardId: string, channelId: string) {
|
|
const channelsEntry = this.channelEntries.get(cardId);
|
|
if (!channelsEntry) {
|
|
throw new Error('card not found');
|
|
}
|
|
const channelEntry = channelsEntry.get(channelId);
|
|
if (!channelEntry) {
|
|
throw new Error('channel not found');
|
|
}
|
|
const id = `${cardId}:${channelId}`;
|
|
this.blockedCardChannel.add(id);
|
|
channelEntry.channel = this.setChannel(cardId, channelId, channelEntry.item);
|
|
this.emitChannels(cardId);
|
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
await this.store.setMarker(this.guid, 'blocked_card_channel', id, JSON.stringify({ cardId, channelId, timestamp }));
|
|
}
|
|
|
|
private async clearChannelBlocked(cardId: string, channelId: string) {
|
|
const channelsEntry = this.channelEntries.get(cardId);
|
|
if (!channelsEntry) {
|
|
throw new Error('card not found');
|
|
}
|
|
const channelEntry = channelsEntry.get(channelId);
|
|
if (!channelEntry) {
|
|
throw new Error('channel not found');
|
|
}
|
|
const id = `${cardId}:${channelId}`;
|
|
this.blockedCardChannel.delete(id);
|
|
channelEntry.channel = this.setChannel(cardId, channelId, channelEntry.item);
|
|
this.emitChannels(cardId);
|
|
await this.store.clearMarker(this.guid, 'blocked_card_channel', id);
|
|
}
|
|
|
|
private isArticleBlocked(cardId: string, articleId: string): boolean {
|
|
return false;
|
|
}
|
|
|
|
private async setArticleBlocked(cardId: string, articleId: string) {
|
|
}
|
|
|
|
private async clearArticleBlocked(cardId: string, articleId: string) {
|
|
}
|
|
|
|
private isChannelUnread(cardId: string, channelId: string, revision: number): boolean {
|
|
const id = `${cardId}:${channelId}`;
|
|
if (this.read.has(id)) {
|
|
const read = this.read.get(id);
|
|
if (read && read >= revision) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private async markChannelUnread(cardId: string, channelId: string, revision: number) {
|
|
const id = `${cardId}:${channelId}`;
|
|
if (!this.read.has(id)) {
|
|
const read = this.read.get(id);
|
|
if (read && read < revision) {
|
|
this.read.delete(id);
|
|
await this.store.clearMarker(this.guid, 'read_card_channel', id);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async markChannelRead(cardId: string, channelId: string, revision: number) {
|
|
const id = `${cardId}:${channelId}`;
|
|
const read = this.read.get(id);
|
|
if (!read || read < revision) {
|
|
this.read.set(id, revision);
|
|
await this.store.setMarker(this.guid, 'read_card_channel', id, revision.toString());
|
|
}
|
|
}
|
|
|
|
public async resyncCard(cardId: string): Promise<void> {
|
|
this.resync.add(cardId);
|
|
await this.sync();
|
|
}
|
|
|
|
private async sync(): Promise<void> {
|
|
if (!this.syncing) {
|
|
this.syncing = true;
|
|
const { guid, node, secure, token } = this;
|
|
while ((this.unsealAll || this.nextRevision || this.resync.size) && !this.closing) {
|
|
if (this.resync.size) {
|
|
const entries = Array.from(this.cardEntries, ([key, value]) => ({ key, value }));
|
|
for (const entry of entries) {
|
|
const { key, value } = entry;
|
|
if (this.resync.has(key)) {
|
|
if (value.item.offsyncProfile) {
|
|
try {
|
|
const { profile, detail, profileRevision, offsyncProfile } = value.item;
|
|
await this.syncProfile(key, profile.node, profile.guid, detail.token, profileRevision);
|
|
value.item.profileRevision = offsyncProfile;
|
|
await this.store.setContactCardProfileRevision(guid, key, profileRevision);
|
|
value.item.offsyncProfile = null;
|
|
await this.store.clearContactCardOffsyncProfile(guid, key);
|
|
entry.value.card = this.setCard(key, entry.value.item);
|
|
this.emitCards();
|
|
} catch (err) {
|
|
this.log.warn(err);
|
|
}
|
|
}
|
|
if (value.item.offsyncArticle) {
|
|
try {
|
|
const { profile, detail, articleRevision, offsyncArticle } = value.item;
|
|
await this.syncArticles(key, profile.node, profile.guid, detail.token, articleRevision);
|
|
value.item.articleRevision = offsyncArticle;
|
|
await this.store.setContactCardArticleRevision(guid, key, articleRevision);
|
|
value.item.offsyncArticle = null;
|
|
await this.store.clearContactCardOffsyncArticle(guid, key);
|
|
entry.value.card = this.setCard(key, entry.value.item);
|
|
this.emitCards();
|
|
} catch (err) {
|
|
this.log.warn(err);
|
|
}
|
|
}
|
|
if (value.item.offsyncChannel) {
|
|
try {
|
|
const { profile, detail, channelRevision, offsyncChannel } = value.item;
|
|
await this.syncChannels(key, { guid: profile.guid, node: profile.node, token: detail.token }, channelRevision);
|
|
value.item.channelRevision = offsyncChannel;
|
|
await this.store.setContactCardChannelRevision(guid, key, value.item.channelRevision);
|
|
value.item.offsyncChannel = null;
|
|
await this.store.clearContactCardOffsyncChannel(guid, key);
|
|
entry.value.card = this.setCard(key, entry.value.item);
|
|
this.emitCards();
|
|
} catch (err) {
|
|
this.log.warn(err);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
this.resync.clear();
|
|
}
|
|
|
|
if (this.nextRevision && this.revision !== this.nextRevision) {
|
|
const nextRev = this.nextRevision;
|
|
try {
|
|
const delta = await getCards(node, secure, token, this.revision);
|
|
for (const entity of delta) {
|
|
const { id, revision, data } = entity;
|
|
if (data) {
|
|
const entry = await this.getCardEntry(id);
|
|
|
|
if (data.detailRevision !== entry.item.detail.revison) {
|
|
const detail = data.cardDetail ? data.cardDetail : await getCardDetail(node, secure, token, id);
|
|
const { status, statusUpdated, token: cardToken } = detail;
|
|
entry.item.detail = {
|
|
revision: data.detailRevision,
|
|
status,
|
|
statusUpdated,
|
|
token: cardToken,
|
|
};
|
|
entry.card = this.setCard(id, entry.item);
|
|
await this.store.setContactCardDetail(guid, id, entry.item.detail);
|
|
}
|
|
|
|
if (data.profileRevision !== entry.item.profile.revision) {
|
|
const profile = data.cardProfile ? data.cardProfile : await getCardProfile(node, secure, token, id);
|
|
entry.item.profile = {
|
|
revision: data.profileRevision,
|
|
handle: profile.handle,
|
|
guid: profile.guid,
|
|
name: profile.name,
|
|
description: profile.description,
|
|
location: profile.location,
|
|
imageSet: profile.imageSet,
|
|
node: profile.node,
|
|
seal: profile.seal,
|
|
};
|
|
entry.card = this.setCard(id, entry.item);
|
|
await this.store.setContactCardProfile(guid, id, entry.item.profile);
|
|
}
|
|
|
|
const { profileRevision, articleRevision, channelRevision, offsyncProfile, offsyncChannel, offsyncArticle } = entry.item;
|
|
|
|
if (data.notifiedProfile > entry.item.profile.revision && data.notifiedProfile !== profileRevision) {
|
|
if (offsyncProfile) {
|
|
entry.item.offsyncProfile = data.notifiedProfile;
|
|
await this.store.setContactCardOffsyncProfile(guid, id, data.notifiedProfile);
|
|
entry.card = this.setCard(id, entry.item);
|
|
} else {
|
|
try {
|
|
await this.syncProfile(id, entry.item.profile.node, entry.item.profile.guid, entry.item.detail.token, entry.item.profileRevision);
|
|
entry.item.profileRevision = data.notifiedProfile;
|
|
await this.store.setContactCardProfileRevision(guid, id, data.notifiedProfile);
|
|
} catch (err) {
|
|
this.log.warn(err);
|
|
entry.item.offsyncProfile = data.notifiedProfile;
|
|
await this.store.setContactCardOffsyncProfile(guid, id, data.notifiedProfile);
|
|
entry.card = this.setCard(id, entry.item);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (data.notifiedArticle !== articleRevision) {
|
|
if (offsyncArticle) {
|
|
entry.item.offsyncArticle = data.notifiedArticle;
|
|
await this.store.setContactCardOffsyncArticle(guid, id, data.notifiedArticle);
|
|
entry.card = this.setCard(id, entry.item);
|
|
} else {
|
|
try {
|
|
await this.syncArticles(id, entry.item.profile.node, entry.item.profile.guid, entry.item.detail.token, entry.item.articleRevision);
|
|
entry.item.articleRevision = data.notifiedArticle;
|
|
await this.store.setContactCardArticleRevision(guid, id, data.notifiedArticle);
|
|
this.emitArticles(id);
|
|
} catch (err) {
|
|
this.log.warn(err);
|
|
entry.item.offsyncArticle = data.notifiedArticle;
|
|
await this.store.setContactCardOffsyncArticle(guid, id, data.notifiedArticle);
|
|
entry.card = this.setCard(id, entry.item);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (data.notifiedChannel !== channelRevision) {
|
|
if (offsyncChannel) {
|
|
entry.item.offsyncChannel = data.notifiedChannel;
|
|
await this.store.setContactCardOffsyncChannel(guid, id, data.notifiedChannel);
|
|
entry.card = this.setCard(id, entry.item);
|
|
} else {
|
|
try {
|
|
const { profile, detail } = entry.item;
|
|
await this.syncChannels(id, { guid: profile.guid, node: profile.node, token: detail.token }, entry.item.channelRevision);
|
|
entry.item.channelRevision = data.notifiedChannel;
|
|
await this.store.setContactCardChannelRevision(guid, id, data.notifiedChannel);
|
|
this.emitChannels(id);
|
|
} catch (err) {
|
|
this.log.warn(err);
|
|
entry.item.offsyncChannel = data.notifiedChannel;
|
|
await this.store.setContactCardOffsyncChannel(guid, id, data.notifiedChannel);
|
|
entry.card = this.setCard(id, entry.item);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
this.cardEntries.delete(id);
|
|
await this.store.removeContactCard(guid, id);
|
|
this.channelEntries.delete(id);
|
|
this.emitChannels(id);
|
|
this.articleEntries.delete(id);
|
|
this.emitArticles(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.nextRevision) {
|
|
this.nextRevision = null;
|
|
}
|
|
|
|
if (this.unsealAll) {
|
|
for (const [cardId, channels] of this.channelEntries.entries()) {
|
|
for (const [channelId, entry] of channels.entries()) {
|
|
try {
|
|
const { item } = entry;
|
|
if (await this.unsealChannelDetail(cardId, channelId, item)) {
|
|
await this.store.setContactCardChannelUnsealedDetail(guid, cardId, channelId, item.unsealedDetail);
|
|
}
|
|
if (await this.unsealChannelSummary(cardId, channelId, item)) {
|
|
await this.store.setContactCardChannelUnsealedSummary(guid, cardId, channelId, item.unsealedSummary);
|
|
}
|
|
entry.channel = this.setChannel(cardId, channelId, item);
|
|
} catch (err) {
|
|
this.log.warn(err);
|
|
}
|
|
}
|
|
this.emitChannels(cardId);
|
|
}
|
|
this.unsealAll = false;
|
|
}
|
|
}
|
|
|
|
if (this.revision && !this.hasSynced) {
|
|
this.hasSynced = true;
|
|
await this.store.setMarker(this.guid, 'first_sync_complete', 'contact', '');
|
|
}
|
|
this.syncing = false;
|
|
}
|
|
}
|
|
|
|
private async syncProfile(cardId: string, cardNode: string, cardGuid: string, cardToken: string, revision: number): Promise<void> {
|
|
const { node, secure, token } = this;
|
|
const server = cardNode ? cardNode : node;
|
|
const insecure = /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|:\d+$|$)){4}$/.test(server);
|
|
const message = await getContactProfile(server, !insecure, cardGuid, cardToken);
|
|
await setCardProfile(node, secure, token, cardId, message);
|
|
}
|
|
|
|
private async syncArticles(cardId: string, cardNode: string, cardGuid: string, cardToken: string, revision: number): Promise<void> {
|
|
const { node, secure, token } = this;
|
|
const server = cardNode ? cardNode : node;
|
|
const insecure = /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|:\d+$|$)){4}$/.test(server);
|
|
}
|
|
|
|
private async syncChannels(cardId: string, card: { guid: string; node: string; token: string }, revision: number): Promise<void> {
|
|
const { guid, node, secure, token, channelTypes } = this;
|
|
const server = card.node ? card.node : node;
|
|
const insecure = /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|:\d+$|$)){4}$/.test(server);
|
|
const delta = await getContactChannels(server, !insecure, card.guid, card.token, revision, channelTypes);
|
|
|
|
for (const entity of delta) {
|
|
const { id, revision, data } = entity;
|
|
if (data) {
|
|
const { detailRevision, topicRevision, channelSummary, channelDetail } = data;
|
|
const entries = this.getChannelEntries(cardId);
|
|
const entry = await this.getChannelEntry(entries, cardId, id);
|
|
|
|
if (detailRevision !== entry.item.detail.revision) {
|
|
const detail = channelDetail ? channelDetail : await getContactChannelDetail(server, !insecure, card.guid, card.token, id);
|
|
entry.item.detail = {
|
|
revision: detailRevision,
|
|
sealed: detail.dataType === 'sealed',
|
|
dataType: detail.dataType,
|
|
data: detail.data,
|
|
created: detail.created,
|
|
updated: detail.updated,
|
|
enableImage: detail.enableImage,
|
|
enableAudio: detail.enableAudio,
|
|
enableVideo: detail.enableVideo,
|
|
enableBinary: detail.enableBinary,
|
|
contacts: detail.contacts,
|
|
members: detail.members,
|
|
};
|
|
entry.item.unsealedDetail = null;
|
|
await this.unsealChannelDetail(cardId, id, entry.item);
|
|
if (this.focus) {
|
|
const { dataType, data, enableImage, enableAudio, enableVideo, enableBinary, members, created } = detail;
|
|
const sealed = dataType === 'sealed';
|
|
const channelData = sealed ? entry.item.unsealedDetail : data;
|
|
const focusDetail = {
|
|
sealed,
|
|
locked: sealed && (!this.seal || !entry.item.channelKey),
|
|
dataType,
|
|
data: this.parse(channelData),
|
|
enableImage,
|
|
enableAudio,
|
|
enableVideo,
|
|
enableBinary,
|
|
created,
|
|
members: members.map(guid => ({ guid })),
|
|
}
|
|
this.focus.setDetail(cardId, id, focusDetail);
|
|
}
|
|
entry.channel = this.setChannel(cardId, id, entry.item);
|
|
await this.store.setContactCardChannelDetail(guid, cardId, id, entry.item.detail, entry.item.unsealedDetail);
|
|
}
|
|
|
|
if (topicRevision !== entry.item.summary.revision) {
|
|
const summary = channelSummary ? channelSummary : await getContactChannelSummary(server, !insecure, card.guid, card.token, id);
|
|
entry.item.summary = {
|
|
revision: topicRevision,
|
|
sealed: summary.lastTopic.dataType === 'sealedtopic',
|
|
guid: summary.lastTopic.guid,
|
|
dataType: summary.lastTopic.dataType,
|
|
data: summary.lastTopic.data,
|
|
created: summary.lastTopic.created,
|
|
updated: summary.lastTopic.updated,
|
|
status: summary.lastTopic.status,
|
|
transform: summary.lastTopic.transform,
|
|
};
|
|
entry.item.unsealedSummary = null;
|
|
await this.unsealChannelSummary(cardId, id, entry.item);
|
|
if (this.hasSynced) {
|
|
await this.markChannelUnread(cardId, id, topicRevision);
|
|
} else {
|
|
await this.markChannelRead(cardId, id, topicRevision);
|
|
}
|
|
entry.channel = this.setChannel(cardId, id, entry.item);
|
|
await this.store.setContactCardChannelSummary(guid, cardId, id, entry.item.summary, entry.item.unsealedSummary);
|
|
if (this.focus) {
|
|
await this.focus.setRevision(cardId, id, topicRevision);
|
|
}
|
|
}
|
|
} else {
|
|
const channels = this.getChannelEntries(cardId);
|
|
channels.delete(id);
|
|
if (this.focus) {
|
|
this.focus.disconnect(cardId, id);
|
|
}
|
|
await this.store.removeContactCardChannel(guid, cardId, id);
|
|
}
|
|
}
|
|
}
|
|
|
|
public addCardListener(ev: (cards: Card[]) => void): void {
|
|
this.emitter.on('card', ev);
|
|
const cards = Array.from(this.cardEntries, ([cardId, entry]) => entry.card);
|
|
ev(cards);
|
|
}
|
|
|
|
public removeCardListener(ev: (cards: Card[]) => void): void {
|
|
this.emitter.off('card', ev);
|
|
}
|
|
|
|
private emitCards() {
|
|
const cards = Array.from(this.cardEntries, ([cardId, entry]) => entry.card);
|
|
this.emitter.emit('card', cards);
|
|
}
|
|
|
|
public addArticleListener(id: string | null, ev: (arg: { cardId: string; articles: Article[] }) => void): void {
|
|
if (id) {
|
|
const cardId = id as string;
|
|
this.emitter.on(`article::${cardId}`, ev);
|
|
const entries = this.articleEntries.get(cardId);
|
|
const articles = entries ? Array.from(entries, ([articleId, entry]) => entry.article) : [];
|
|
ev({ cardId, articles });
|
|
} else {
|
|
this.emitter.on('article', ev);
|
|
this.articleEntries.forEach((entries, cardId) => {
|
|
const articles = Array.from(entries, ([articleId, entry]) => entry.article);
|
|
ev({ cardId, articles });
|
|
});
|
|
}
|
|
}
|
|
|
|
public removeArticleListener(id: string | null, ev: (arg: { cardId: string; articles: Article[] }) => void): void {
|
|
if (id) {
|
|
const cardId = id as string;
|
|
this.emitter.off(`article::${cardId}`, ev);
|
|
} else {
|
|
this.emitter.off('article', ev);
|
|
}
|
|
}
|
|
|
|
private emitArticles(cardId: string) {
|
|
const entries = this.articleEntries.get(cardId);
|
|
const articles = entries ? Array.from(entries, ([articleId, entry]) => entry.article) : [];
|
|
this.emitter.emit('article', { cardId, articles });
|
|
this.emitter.emit(`article::${cardId}`, { cardId, articles });
|
|
}
|
|
|
|
public addChannelListener(ev: (arg: { cardId: string; channels: Channel[] }) => void): void {
|
|
this.emitter.on('channel', ev);
|
|
this.channelEntries.forEach((entries, cardId) => {
|
|
const channels = Array.from(entries, ([channelId, entry]) => entry.channel);
|
|
ev({ cardId, channels });
|
|
});
|
|
}
|
|
|
|
public removeChannelListener(ev: (arg: { cardId: string; channels: Channel[] }) => void): void {
|
|
this.emitter.off('channel', ev);
|
|
}
|
|
|
|
private emitChannels(cardId: string) {
|
|
const entries = this.channelEntries.get(cardId);
|
|
const channels = entries ? Array.from(entries, ([channelId, entry]) => entry.channel) : [];
|
|
this.emitter.emit('channel', { cardId, channels });
|
|
}
|
|
|
|
public async setFocus(cardId: string, channelId: string): Promise<Focus> {
|
|
if (this.focus) {
|
|
this.focus.close();
|
|
}
|
|
|
|
const markRead = async () => {
|
|
try {
|
|
await this.setUnreadChannel(cardId, channelId, false);
|
|
} catch (err) {
|
|
this.log.error(err);
|
|
}
|
|
}
|
|
|
|
const flagTopic = async (topicId: string) => {
|
|
const entry = this.cardEntries.get(cardId);
|
|
if (entry) {
|
|
const server = entry.item.profile.node ? entry.item.profile.node : this.node;
|
|
const insecure = /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|:\d+$|$)){4}$/.test(server);
|
|
await addFlag(server, !insecure, entry.item.profile.guid, { channelId, topicId });
|
|
}
|
|
}
|
|
|
|
const cardEntry = this.cardEntries.get(cardId);
|
|
const channelsEntry = this.channelEntries.get(cardId);
|
|
const channelEntry = channelsEntry?.get(channelId);
|
|
if (cardEntry && channelEntry) {
|
|
// allocate focus
|
|
const node = cardEntry.item.profile.node;
|
|
const guid = cardEntry.item.profile.guid;
|
|
const token = cardEntry.item.detail.token;
|
|
const revision = channelEntry.item.summary.revision;
|
|
const channelKey = await this.setChannelKey(channelEntry.item);
|
|
const sealEnabled = Boolean(this.seal);
|
|
const insecure = /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|:\d+$|$)){4}$/.test(node);
|
|
this.focus = new FocusModule(this.log, this.store, this.crypto, this.staging, cardId, channelId, this.guid, { node, secure: !insecure, token: `${guid}.${token}` }, channelKey, sealEnabled, revision, markRead, flagTopic);
|
|
|
|
// set current detail
|
|
const { dataType, data, enableImage, enableAudio, enableVideo, enableBinary, members, created } = channelEntry.item.detail;
|
|
const sealed = dataType === 'sealed';
|
|
const channelData = sealed ? channelEntry.item.unsealedDetail : data;
|
|
const focusDetail = {
|
|
sealed,
|
|
locked: sealed && (!this.seal || !channelEntry.item.channelKey),
|
|
dataType,
|
|
data: this.parse(channelData),
|
|
enableImage,
|
|
enableAudio,
|
|
enableVideo,
|
|
enableBinary,
|
|
created,
|
|
members: members.map(guid => ({ guid })),
|
|
}
|
|
this.focus.setDetail(cardId, channelId, focusDetail);
|
|
} else {
|
|
this.focus = new FocusModule(this.log, this.store, this.crypto, this.staging, cardId, channelId, this.guid, null, null, false, 0, markRead, flagTopic);
|
|
}
|
|
return this.focus;
|
|
}
|
|
|
|
public clearFocus() {
|
|
if (this.focus) {
|
|
this.focus.close();
|
|
this.focus = null;
|
|
}
|
|
}
|
|
|
|
public async addCard(server: string | null, guid: string): Promise<string> {
|
|
const { node, secure, token } = this;
|
|
const insecure = server ? /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|:\d+$|$)){4}$/.test(server) : false;
|
|
const message = server ? await getContactListing(server, !insecure, guid) : await getContactListing(node, secure, guid);
|
|
const added = await addCard(node, secure, token, message);
|
|
return added.id;
|
|
}
|
|
|
|
public async removeCard(cardId: string): Promise<void> {
|
|
const { node, secure, token } = this;
|
|
await removeCard(node, secure, token, cardId);
|
|
}
|
|
|
|
public async confirmCard(cardId: string): Promise<void> {
|
|
const { node, secure, token } = this;
|
|
await setCardConfirmed(node, secure, token, cardId);
|
|
}
|
|
|
|
public async connectCard(cardId: string): Promise<void> {
|
|
const { node, secure, token } = this;
|
|
await setCardConnecting(node, secure, token, cardId);
|
|
try {
|
|
const message = await getCardOpenMessage(node, secure, token, cardId);
|
|
const entry = this.cardEntries.get(cardId);
|
|
if (entry) {
|
|
const server = entry.item.profile.node ? entry.item.profile.node : node;
|
|
const insecure = /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|:\d+$|$)){4}$/.test(server);
|
|
const contact = await setCardOpenMessage(server, !insecure, message);
|
|
if (contact.status === 'connected') {
|
|
const { token: contactToken, articleRevision, channelRevision, profileRevision } = contact;
|
|
await setCardConnected(node, secure, token, cardId, contactToken, articleRevision, channelRevision, profileRevision);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
this.log.error('failed to deliver open message');
|
|
}
|
|
}
|
|
|
|
public async disconnectCard(cardId: string): Promise<void> {
|
|
const { node, secure, token } = this;
|
|
await setCardConfirmed(node, secure, token, cardId);
|
|
try {
|
|
const message = await getCardCloseMessage(node, secure, token, cardId);
|
|
const entry = this.cardEntries.get(cardId);
|
|
if (entry) {
|
|
const server = entry.item.profile.node ? entry.item.profile.node : node;
|
|
const insecure = /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|:\d+$|$)){4}$/.test(server);
|
|
await setCardCloseMessage(server, !insecure, message);
|
|
}
|
|
} catch (err) {
|
|
this.log.warn('failed to deliver close message');
|
|
}
|
|
}
|
|
|
|
public async denyCard(cardId: string): Promise<void> {
|
|
const { node, secure, token } = this;
|
|
const entry = this.cardEntries.get(cardId);
|
|
if (entry) {
|
|
try {
|
|
const message = await getCardCloseMessage(node, secure, token, cardId);
|
|
const server = entry.item.profile.node ? entry.item.profile.node : node;
|
|
const insecure = /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|:\d+$|$)){4}$/.test(server);
|
|
await setCardCloseMessage(server, !insecure, message);
|
|
} catch (err) {
|
|
this.log.warn('failed to deliver close message');
|
|
}
|
|
if (entry.item.detail.status === 'pending') {
|
|
await removeCard(node, secure, token, cardId);
|
|
} else {
|
|
await setCardConfirmed(node, secure, token, cardId);
|
|
}
|
|
}
|
|
}
|
|
|
|
public async ignoreCard(cardId: string): Promise<void> {
|
|
const { node, secure, token } = this;
|
|
const entry = this.cardEntries.get(cardId);
|
|
if (entry) {
|
|
if (entry.item.detail.status === 'pending') {
|
|
await removeCard(node, secure, token, cardId);
|
|
} else {
|
|
await setCardConfirmed(node, secure, token, cardId);
|
|
}
|
|
}
|
|
}
|
|
|
|
public async removeArticle(cardId: string, articleId: string): Promise<void> {}
|
|
|
|
public async leaveChannel(cardId: string, channelId: string): Promise<void> {
|
|
const entry = this.cardEntries.get(cardId);
|
|
if (entry) {
|
|
const server = entry.item.profile.node ? entry.item.profile.node : this.node;
|
|
const insecure = /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|:\d+$|$)){4}$/.test(server);
|
|
await removeContactChannel(server, !insecure, entry.item.profile.guid, entry.item.detail.token, channelId);
|
|
}
|
|
}
|
|
|
|
public async setBlockedCard(cardId: string, blocked: boolean): Promise<void> {
|
|
const entry = this.cardEntries.get(cardId);
|
|
if (entry) {
|
|
if (blocked) {
|
|
await this.setCardBlocked(cardId);
|
|
} else {
|
|
await this.clearCardBlocked(cardId);
|
|
}
|
|
entry.card = this.setCard(cardId, entry.item);
|
|
this.emitCards();
|
|
}
|
|
}
|
|
|
|
public async setBlockedChannel(cardId: string, channelId: string, blocked: boolean): Promise<void> {
|
|
const entries = this.channelEntries.get(cardId);
|
|
if (entries) {
|
|
const entry = entries.get(channelId);
|
|
if (entry) {
|
|
if (blocked) {
|
|
await this.setChannelBlocked(cardId, channelId);
|
|
} else {
|
|
await this.clearChannelBlocked(cardId, channelId);
|
|
}
|
|
entry.channel = this.setChannel(cardId, channelId, entry.item);
|
|
this.emitChannels(cardId);
|
|
}
|
|
}
|
|
}
|
|
|
|
public async clearBlockedChannelTopic(cardId: string, channelId: string, topicId: string) {
|
|
const { guid } = this;
|
|
const id = `${cardId}:${channelId}:${topicId}`
|
|
await this.store.clearMarker(guid, 'blocked_topic', id);
|
|
if (this.focus) {
|
|
await this.focus.clearBlockedChannelTopic(cardId, channelId, topicId);
|
|
}
|
|
}
|
|
|
|
public async setBlockedArticle(cardId: string, articleId: string, blocked: boolean): Promise<void> {
|
|
const entries = this.articleEntries.get(cardId);
|
|
if (entries) {
|
|
const entry = entries.get(articleId);
|
|
if (entry) {
|
|
if (blocked) {
|
|
await this.setArticleBlocked(cardId, articleId);
|
|
} else {
|
|
await this.clearArticleBlocked(cardId, articleId);
|
|
}
|
|
entry.article = this.setArticle(cardId, articleId, entry.item);
|
|
this.emitArticles(cardId);
|
|
}
|
|
}
|
|
}
|
|
|
|
public async flagCard(cardId: string): Promise<void> {
|
|
const entry = this.cardEntries.get(cardId);
|
|
if (entry) {
|
|
const server = entry.item.profile.node ? entry.item.profile.node : this.node;
|
|
const insecure = /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|:\d+$|$)){4}$/.test(server);
|
|
await addFlag(server, !insecure, entry.item.profile.guid, {});
|
|
}
|
|
}
|
|
|
|
public async flagArticle(cardId: string, articleId: string): Promise<void> {
|
|
const entry = this.cardEntries.get(cardId);
|
|
if (entry) {
|
|
const server = entry.item.profile.node ? entry.item.profile.node : this.node;
|
|
const insecure = /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|:\d+$|$)){4}$/.test(server);
|
|
await addFlag(server, !insecure, entry.item.profile.guid, { articleId });
|
|
}
|
|
}
|
|
|
|
public async flagChannel(cardId: string, channelId: string): Promise<void> {
|
|
const entry = this.cardEntries.get(cardId);
|
|
if (entry) {
|
|
const server = entry.item.profile.node ? entry.item.profile.node : this.node;
|
|
const insecure = /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|:\d+$|$)){4}$/.test(server);
|
|
await addFlag(server, !insecure, entry.item.profile.guid, { channelId });
|
|
}
|
|
}
|
|
|
|
public async setUnreadChannel(cardId: string, channelId: string, unread: boolean): Promise<void> {
|
|
const channelsEntry = this.channelEntries.get(cardId);
|
|
if (!channelsEntry) {
|
|
throw new Error('card not found');
|
|
}
|
|
const channelEntry = channelsEntry.get(channelId);
|
|
if (!channelEntry) {
|
|
throw new Error('channel not found');
|
|
}
|
|
const id = `${cardId}:${channelId}`;
|
|
if (unread) {
|
|
this.read.delete(id);
|
|
channelEntry.channel = this.setChannel(cardId, channelId, channelEntry.item);
|
|
this.emitChannels(cardId);
|
|
await this.store.clearMarker(this.guid, 'read_card_channel', id);
|
|
} else {
|
|
const revision = channelEntry.item.summary.revision;
|
|
this.read.set(id, revision);
|
|
channelEntry.channel = this.setChannel(cardId, channelId, channelEntry.item);
|
|
this.emitChannels(cardId);
|
|
await this.store.setMarker(this.guid, 'read_card_channel', id, revision.toString());
|
|
}
|
|
}
|
|
|
|
public async getChannelNotifications(cardId: string, channelId: string): Promise<boolean> {
|
|
const entry = this.cardEntries.get(cardId);
|
|
if (entry) {
|
|
const server = entry.item.profile.node ? entry.item.profile.node : this.node;
|
|
const insecure = /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|:\d+$|$)){4}$/.test(server);
|
|
return await getContactChannelNotifications(server, !insecure, entry.item.profile.guid, entry.item.detail.token, channelId);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public async setChannelNotifications(cardId: string, channelId: string, enabled: boolean): Promise<void> {
|
|
const entry = this.cardEntries.get(cardId);
|
|
if (entry) {
|
|
const server = entry.item.profile.node ? entry.item.profile.node : this.node;
|
|
const insecure = /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|:\d+$|$)){4}$/.test(server);
|
|
await setContactChannelNotifications(server, !insecure, entry.item.profile.guid, entry.item.detail.token, channelId, enabled);
|
|
}
|
|
}
|
|
|
|
public async getRegistry(handle: string | null, server: string | null): Promise<Profile[]> {
|
|
const { node, secure } = this;
|
|
const insecure = server ? /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|:\d+$|$)){4}$/.test(server) : false;
|
|
const listing = server ? await getRegistryListing(handle, server, !insecure) : await getRegistryListing(handle, node, secure);
|
|
return listing.map((entity) => {
|
|
return {
|
|
guid: entity.guid,
|
|
handle: entity.handle,
|
|
name: entity.name,
|
|
description: entity.description,
|
|
location: entity.location,
|
|
node: entity.node,
|
|
version: entity.version,
|
|
sealSet: Boolean(entity.seal),
|
|
imageUrl: entity.imageSet ? (server ? getRegistryImageUrl(server, true, entity.guid) : getRegistryImageUrl(node, secure, entity.guid)) : avatar,
|
|
imageSet: entity.imageSet,
|
|
};
|
|
});
|
|
}
|
|
|
|
private setCard(cardId: string, item: CardItem): Card {
|
|
const { node, secure, token } = this;
|
|
const { profile, detail } = item;
|
|
return {
|
|
cardId,
|
|
offsync: Boolean(item.offsyncProfile || item.offsyncChannel || item.offsyncArticle),
|
|
blocked: this.isCardBlocked(cardId),
|
|
sealable: Boolean(item.profile.seal),
|
|
status: detail.status,
|
|
statusUpdated: detail.statusUpdated,
|
|
guid: profile.guid,
|
|
handle: profile.handle,
|
|
name: profile.name,
|
|
description: profile.description,
|
|
location: profile.location,
|
|
imageUrl: profile.imageSet ? getCardImageUrl(node, secure, token, cardId, item.profile.revision) : avatar,
|
|
imageSet: profile.imageSet,
|
|
node: profile.node,
|
|
version: profile.version,
|
|
};
|
|
}
|
|
|
|
private setArticle(cardId: string, articleId: string, item: ArticleItem): Article {
|
|
const { detail } = item;
|
|
const articleData = detail.sealed ? item.unsealedDetail : detail.data;
|
|
return {
|
|
cardId,
|
|
articleId,
|
|
sealed: detail.sealed,
|
|
blocked: this.isArticleBlocked(cardId, articleId),
|
|
dataType: detail.dataType,
|
|
data: articleData,
|
|
created: detail.created,
|
|
updated: detail.updated,
|
|
};
|
|
}
|
|
|
|
private setChannel(cardId: string, channelId: string, item: ChannelItem): Channel {
|
|
const { summary, detail } = item;
|
|
const channelData = detail.sealed ? item.unsealedDetail : detail.data || '{}';
|
|
const topicData = summary.sealed ? item.unsealedSummary : summary.data || '{}';
|
|
const parsed = this.parse(topicData);
|
|
|
|
return {
|
|
channelId,
|
|
cardId,
|
|
lastTopic: {
|
|
guid: summary.guid,
|
|
sealed: summary.sealed,
|
|
dataType: summary.dataType,
|
|
data: getLegacyData(parsed).data,
|
|
created: summary.created,
|
|
updated: summary.updated,
|
|
status: summary.status,
|
|
transform: summary.transform,
|
|
},
|
|
blocked: this.isChannelBlocked(cardId, channelId),
|
|
unread: this.isChannelUnread(cardId, channelId, summary.revision),
|
|
sealed: detail.sealed,
|
|
locked: detail.sealed && (!this.seal || !item.channelKey),
|
|
dataType: detail.dataType,
|
|
data: this.parse(channelData),
|
|
created: detail.created,
|
|
updated: detail.updated,
|
|
enableImage: detail.enableImage,
|
|
enableAudio: detail.enableAudio,
|
|
enableVideo: detail.enableVideo,
|
|
enableBinary: detail.enableBinary,
|
|
members: detail.members.map((guid) => ({ guid })),
|
|
};
|
|
}
|
|
|
|
public async setSeal(seal: { privateKey: string; publicKey: string } | null) {
|
|
this.seal = seal;
|
|
if (this.focus) {
|
|
await this.focus.setSealEnabled(Boolean(this.seal));
|
|
}
|
|
this.unsealAll = true;
|
|
await this.sync();
|
|
}
|
|
|
|
public async getSeal(cardId: string, keyData: string): Promise<{ publicKey: string; sealedKey: string }> {
|
|
if (!this.crypto) {
|
|
throw new Error('crypto not set');
|
|
}
|
|
const card = this.cardEntries.get(cardId);
|
|
if (!card) {
|
|
throw new Error('specified card not found');
|
|
}
|
|
const publicKey = card.item.profile.seal;
|
|
if (!publicKey) {
|
|
throw new Error('seal key not set for card');
|
|
}
|
|
const sealed = await this.crypto.rsaEncrypt(keyData, publicKey);
|
|
const sealedKey = sealed.encryptedDataB64;
|
|
return { publicKey, sealedKey };
|
|
}
|
|
|
|
private async getChannelKey(seals: [{ publicKey: string; sealedKey: string }]): Promise<string | null> {
|
|
const seal = seals.find(({ publicKey }) => this.seal && publicKey === this.seal.publicKey);
|
|
if (seal && this.crypto && this.seal) {
|
|
const key = await this.crypto.rsaDecrypt(seal.sealedKey, this.seal.privateKey);
|
|
return key.data;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private async setChannelKey(item: ChannelItem) {
|
|
if (!item.channelKey && item.detail.dataType === 'sealed' && this.seal && this.crypto) {
|
|
try {
|
|
const { seals } = JSON.parse(item.detail.data);
|
|
item.channelKey = await this.getChannelKey(seals);
|
|
} catch (err) {
|
|
console.log(err);
|
|
}
|
|
}
|
|
return item.channelKey;
|
|
}
|
|
|
|
private async unsealChannelDetail(cardId: string, channelId: string, item: ChannelItem): Promise<boolean> {
|
|
if (item.unsealedDetail == null && item.detail.dataType === 'sealed' && this.seal && this.crypto) {
|
|
try {
|
|
const { subjectEncrypted, subjectIv, seals } = JSON.parse(item.detail.data);
|
|
if (!item.channelKey) {
|
|
item.channelKey = await this.getChannelKey(seals);
|
|
if (this.focus) {
|
|
try {
|
|
await this.focus.setChannelKey(cardId, channelId, item.channelKey);
|
|
} catch (err) {
|
|
this.log.warn(err);
|
|
}
|
|
}
|
|
}
|
|
if (item.channelKey) {
|
|
const { data } = await this.crypto.aesDecrypt(subjectEncrypted, subjectIv, item.channelKey);
|
|
item.unsealedDetail = data;
|
|
if (this.focus) {
|
|
const { dataType, enableImage, enableAudio, enableVideo, enableBinary, members, created } = item.detail;
|
|
const focusDetail = {
|
|
sealed: true,
|
|
locked: false,
|
|
dataType,
|
|
data: this.parse(data),
|
|
enableImage,
|
|
enableAudio,
|
|
enableVideo,
|
|
enableBinary,
|
|
created,
|
|
members: members.map(guid => ({ guid })),
|
|
}
|
|
this.focus.setDetail(cardId, channelId, focusDetail);
|
|
}
|
|
return true;
|
|
}
|
|
} catch (err) {
|
|
this.log.warn(err);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private async unsealChannelSummary(cardId: string, channelId: string, item: ChannelItem): Promise<boolean> {
|
|
if (item.unsealedSummary == null && item.summary.dataType === 'sealedtopic' && this.seal && this.crypto) {
|
|
try {
|
|
if (!item.channelKey) {
|
|
const { seals } = JSON.parse(item.detail.data);
|
|
item.channelKey = await this.getChannelKey(seals);
|
|
if (this.focus) {
|
|
try {
|
|
await this.focus.setChannelKey(cardId, channelId, item.channelKey);
|
|
} catch (err) {
|
|
this.log.warn(err);
|
|
}
|
|
}
|
|
}
|
|
if (item.channelKey) {
|
|
const { messageEncrypted, messageIv } = JSON.parse(item.summary.data);
|
|
if (!messageEncrypted || !messageIv) {
|
|
this.log.warn('invalid sealed summary');
|
|
} else {
|
|
const { data } = await this.crypto.aesDecrypt(messageEncrypted, messageIv, item.channelKey);
|
|
item.unsealedSummary = data;
|
|
return true;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
this.log.warn(err);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private async getCardEntry(cardId: string) {
|
|
const { guid } = this;
|
|
const entry = this.cardEntries.get(cardId);
|
|
if (entry) {
|
|
return entry;
|
|
}
|
|
const item = JSON.parse(JSON.stringify(defaultCardItem));
|
|
const card = this.setCard(cardId, item);
|
|
const cardEntry = { item, card };
|
|
this.cardEntries.set(cardId, cardEntry);
|
|
await this.store.addContactCard(guid, cardId, item);
|
|
return cardEntry;
|
|
}
|
|
|
|
private getChannelEntries(cardId: string) {
|
|
const entries = this.channelEntries.get(cardId);
|
|
if (entries) {
|
|
return entries;
|
|
}
|
|
const channels = new Map<string, { item: ChannelItem; channel: Channel }>();
|
|
this.channelEntries.set(cardId, channels);
|
|
return channels;
|
|
}
|
|
|
|
private async getChannelEntry(channels: Map<string, { item: ChannelItem; channel: Channel }>, cardId: string, channelId: string) {
|
|
const { guid } = this;
|
|
const entry = channels.get(channelId);
|
|
if (entry) {
|
|
return entry;
|
|
}
|
|
const item = JSON.parse(JSON.stringify(defaultChannelItem));
|
|
const channel = this.setChannel(cardId, channelId, item);
|
|
const channelEntry = { item, channel };
|
|
channels.set(channelId, channelEntry);
|
|
await this.store.addContactCardChannel(guid, cardId, channelId, item);
|
|
return channelEntry;
|
|
}
|
|
|
|
private parse(data: string | null): any {
|
|
if (data) {
|
|
try {
|
|
if (data == null) {
|
|
return null;
|
|
}
|
|
return JSON.parse(data);
|
|
} catch (err) {
|
|
this.log.error('invalid contact data');
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
}
|