implementing focus module

This commit is contained in:
balzack 2024-12-02 13:58:39 -08:00
parent 0e24bf1bb4
commit 40bb9c2a12
13 changed files with 223 additions and 206 deletions

View File

@ -151,7 +151,7 @@ const crypto = new TestCrypto();
test('received contact updates', async () => {
let testCards: Card[] = [];
const update = (cards: Card[]) => { testCards = cards }
const contact = new ContactModule(log, store, crypto, 'test_guid', 'test_token', 'test_url', false, [], []);
const contact = new ContactModule(log, store, crypto, null, 'test_guid', 'test_token', 'test_url', false, [], []);
contact.addCardListener(update);
contact.setRevision(1)
await waitFor(() => testCards.length === 1);
@ -162,7 +162,7 @@ test('received contact updates', async () => {
test('adds new contact', async () => {
let testCards: Card[] = [];
const update = (cards: Card[]) => { testCards = cards }
const contact = new ContactModule(log, store, crypto, 'test_guid', 'test_token', 'test_url', false, [], []);
const contact = new ContactModule(log, store, crypto, null, 'test_guid', 'test_token', 'test_url', false, [], []);
contact.addCardListener(update);
contact.setRevision(3)
await waitFor(() => testCards.length === 1);
@ -175,7 +175,7 @@ test('adds new contact', async () => {
test('removes contact', async () => {
let testCards: Card[] = [];
const update = (cards: Card[]) => { testCards = cards }
const contact = new ContactModule(log, store, crypto, 'test_guid', 'test_token', 'test_url', false, [], []);
const contact = new ContactModule(log, store, crypto, null, 'test_guid', 'test_token', 'test_url', false, [], []);
contact.addCardListener(update);
contact.setRevision(8)
await waitFor(() => testCards.length === 1);
@ -188,7 +188,7 @@ test('removes contact', async () => {
test('connects and disconnects with known contact', async () => {
let testCards: Card[] = [];
const update = (cards: Card[]) => { testCards = cards }
const contact = new ContactModule(log, store, crypto, 'test_guid', 'test_token', 'test_url', false, [], []);
const contact = new ContactModule(log, store, crypto, null, 'test_guid', 'test_token', 'test_url', false, [], []);
contact.addCardListener(update);
contact.setRevision(11)
await waitFor(() => testCards.length === 1);

View File

@ -129,10 +129,10 @@ jest.mock('../src/net/fetchUtil', () => {
url == 'http://test_url/content/channels?agent=test_token&types=%5B%5D') {
return Promise.resolve({ status: 200, json: () => [getChannel('test_subject_0', 'test_message_0', 1)] });
} else if (url == 'https://URL_A/content/channels?contact=G000A.T000A&viewRevision=1&channelRevision=1&types=%5B%5D' ||
url == 'http://test_url/content/channels?agent=test_token&revision=1&types=%5B%5D') {
url == 'http://test_url/content/channels?agent=test_token&channelRevision=1&types=%5B%5D') {
return Promise.resolve({ status: 200, json: () => [getChannel('test_subject_1', 'test_message_1', 2)] });
} else if (url == 'https://URL_A/content/channels?contact=G000A.T000A&viewRevision=1&channelRevision=2&types=%5B%5D' ||
url == 'http://test_url/content/channels?agent=test_token&revision=2&types=%5B%5D') {
url == 'http://test_url/content/channels?agent=test_token&channelRevision=2&types=%5B%5D') {
return Promise.resolve({ status: 200, json: () => [getChannel('test_subject_2', 'test_message_2', 3)] });
} else if (url == 'https://URL_A/content/channels/CHAN1/detail?contact=G000A.T000A' ||
url == 'http://test_url/content/channels/CHAN1/detail?agent=test_token') {
@ -159,8 +159,8 @@ const store = new TestStore();
test('received contact updates', async () => {
const cardChannels = new Map<string | null, Channel[]>();
const stream = new StreamModule(log, store, null, 'test_guid', 'test_token', 'test_url', false, []);
const contact = new ContactModule(log, store, null, 'test_guid', 'test_token', 'test_url', false, [], []);
const stream = new StreamModule(log, store, null, null, 'test_guid', 'test_token', 'test_url', false, []);
const contact = new ContactModule(log, store, null, null, 'test_guid', 'test_token', 'test_url', false, [], []);
const content = new ContentModule(log, null, contact, stream);
const channelUpdate = ({ channels, cardId }: { channels: Channel[]; cardId: string | null }) => {
@ -170,6 +170,7 @@ test('received contact updates', async () => {
await contact.setRevision(1);
await waitFor(() => cardChannels.get('C000A')?.length === 1);
await waitFor(() => cardChannels.get('C000A')?.[0].data.subject === 'test_subject_0');
await waitFor(() => cardChannels.get('C000A')?.[0].lastTopic.data.message === 'test_message_0');
@ -186,8 +187,8 @@ test('received contact updates', async () => {
test('received stream updates', async () => {
const streamChannels = new Map<string | null, Channel[]>();
const stream = new StreamModule(log, store, null, 'test_guid', 'test_token', 'test_url', false, []);
const contact = new ContactModule(log, store, null, 'test_guid', 'test_token', 'test_url', false, [], []);
const stream = new StreamModule(log, store, null, null, 'test_guid', 'test_token', 'test_url', false, []);
const contact = new ContactModule(log, store, null, null, 'test_guid', 'test_token', 'test_url', false, [], []);
const content = new ContentModule(log, null, contact, stream);
const channelUpdate = ({ channels, cardId }: { channels: Channel[]; cardId: string | null }) => {

View File

@ -72,19 +72,8 @@ export interface Contact {
getBlockedCards(): Promise<Card[]>;
getRegistry(handle: string | null, server: string | null): Promise<Profile[]>;
leaveChannel(cardId: string, channelId: string): Promise<void>;
flagChannel(cardId: string, channelId: string): Promise<void>;
setBlockedChannel(cardId: string, channelId: string, blocked: boolean): Promise<void>;
getBlockedChannels(): Promise<Channel[]>;
setUnreadChannel(cardId: string, channelId: string, unread: boolean): Promise<void>;
getChannelNotifications(cardId: string, channelId: string): Promise<boolean>;
setChannelNotifications(cardId: string, channelId: string, enabled: boolean): Promise<void>;
addCardListener(ev: (cards: Card[]) => void): void;
removeCardListener(ev: (cards: Card[]) => void): void;
addChannelListener(ev: (arg: { channels: Channel[]; cardId: string | null }) => void): void;
removeChannelListener(ev: (arg: { channels: Channel[]; cardId: string | null }) => void): void;
}
export interface Content {

View File

@ -274,17 +274,19 @@ export type Login = {
pushSupported: boolean;
};
export type BasicAsset = {
encrypted?: { type: string, thumb: string, label: string, extension: string, parts: { blockIv: string, partId: string }[] },
image?: { thumb: string, full: string },
audio?: { label: string, full: string },
video?: { thumb: string, lq: string, hd: string },
binary?: { label: string, extension: string, data: string }
}
export type BasicEntity = {
text: string,
textColor: string,
textSize: string,
assets: {
encrypted?: { type: string, thumb: string, label: string, extension: string, parts: { blockIv: string, partId: string }[] },
image?: { thumb: string, full: string },
audio?: { label: string, full: string },
video?: { thumb: string, lq: string, hd: string },
binary?: { extension: string, data: string }
}
assets: BasicAsset[],
}
export type SealedBasicEntity = {

View File

@ -1,13 +1,13 @@
import { EventEmitter } from 'eventemitter3';
import type { Focus } from './api';
import type { TopicItem} from './items';
import type { TopicItem, AssetItem, TopicDetail } from './items';
import type { Topic, Asset, AssetSource, Participant } from './types';
import { TransformType, HostingMode } from './types';
import { TransformType, HostingMode, AssetType } from './types';
import type { Logging } from './logging';
import { Store } from './store';
import { Crypto } from './crypto';
import { Media } from './media';
import { HostingMode } from './types';
import { BasicEntity, BasicAsset, SealedBasicEntity, TopicDetailEntity } from './entities';
import { defaultTopicItem } from './items';
import { getChannelTopics } from './net/getChannelTopics';
import { getChannelTopicDetail } from './net/getChannelTopicDetail';
@ -38,22 +38,23 @@ export class FocusModule implements Focus {
private connection: { node: string; secure: boolean; token: string } | null;
private syncing: boolean;
private closing: boolean;
private nextRevision: number;
private cacheView: {revision: number | null, marker: number | null};
private storeView: {topicId: string, position: number} | null;
private nextRevision: number | null;
private storeView: {revision: number | null, marker: number | null};
private cacheView: {topicId: string, position: number} | null;
private localComplete: boolean;
private remoteComplete: boolean;
private sealEnabled: boolean;
private channelKey: string | null;
private loadMore: boolean;
private closeMedia: (()=>Promsie<void>)[];
private closeMedia: (()=>Promise<void>)[];
private unsealAll: boolean;
private markers: Set<string>;
// view of topics
private topicEntries: Map<string, { item: TopicItem; topic: Topic }>;
constructor(log: Logging, store: Store, crypto: Crypto | null, media: Media | null, cardId: string | null, channelId: string, guid: string, connection: { node: string; secure: boolean; token: string } | null, channelKey: string, sealEnabled: boolean, revision: number) {
constructor(log: Logging, store: Store, crypto: Crypto | null, media: Media | null, cardId: string | null, channelId: string, guid: string, connection: { node: string; secure: boolean; token: string } | null, channelKey: string | null, sealEnabled: boolean, revision: number) {
this.cardId = cardId;
this.channelId = channelId;
this.log = log;
@ -75,6 +76,7 @@ export class FocusModule implements Focus {
this.closeMedia = [];
this.nextRevision = null;
this.loadMore = false;
this.unsealAll = false;
this.localComplete = false;
this.remoteComplete = false;
this.init(revision);
@ -83,7 +85,7 @@ export class FocusModule implements Focus {
private async init(revision: number) {
const { guid } = this;
this.nextRevision = revision;
this.storeView = this.getChannelTopicRevision();
this.storeView = await this.getChannelTopicRevision();
// load markers
const values = await this.store.getMarkers(guid);
@ -101,7 +103,6 @@ export class FocusModule implements Focus {
private async sync(): Promise<void> {
if (!this.syncing) {
this.syncing = true;
const { guid, node, secure, token, channelTypes } = this;
while ((this.loadMore || this.unsealAll || this.nextRevision) && !this.closing && this.connection) {
if (this.loadMore) {
try {
@ -138,14 +139,14 @@ export class FocusModule implements Focus {
entry.item.position = detail.created;
await this.unsealTopicDetail(entry.item);
entry.topic = this.setTopic(id, entry.item);
await this.setLocalChannelTopicDetail(id, detail, entry.item.unsealedDetail, detail.created);
await this.setLocalChannelTopicDetail(id, entry.item.detail, entry.item.unsealedDetail, detail.created);
}
} else {
log.error('ignoring unexpected delete entry on initial load');
this.log.error('ignoring unexpected delete entry on initial load');
}
}
if (delta.topics.length === 0) {
this.remoteCompleted = true;
this.remoteComplete = true;
}
const rev = this.storeView.revision ? this.storeView.revision : delta.revision;
const mark = delta.topics.length ? delta.marker : null;
@ -170,24 +171,25 @@ export class FocusModule implements Focus {
const { id, revision, data } = entity;
if (data) {
const { detailRevision, topicDetail } = data;
if (!this.cacheView || this.cacheView.position > detail.created || (this.cacheView.position === detail.created && this.cacheView.topicId >= topicId)) {
const detail = topicDetail ? topicDetail : await this.getRemoteChannelTopicDetail(id);
if (!this.cacheView || this.cacheView.position > detail.created || (this.cacheView.position === detail.created && this.cacheView.topicId >= id)) {
const entry = await this.getTopicEntry(id);
if (detailRevision > entry.item.detail.revision) {
const detail = topicDetail ? topicDetail : await this.getRemoteChannelTopicDetail(id);
entry.item.detail = this.getTopicDetail(detail, detailRevision);
entry.item.unsealedDetail = null;
entry.item.position = detail.created;
await this.unsealTopicDetail(entry.item);
entry.topic = this.setTopic(id, entry.item);
await this.setLocalChannelTopicDetail(id, detail, entry.item.unsealedDetail, detail.created);
await this.setLocalChannelTopicDetail(id, entry.item.detail, entry.item.unsealedDetail, detail.created);
}
} else {
const item = { detail, position: detail.created, unsealedDetail: null };
await this.store.addLocalChannelTopic(id, item);
const itemDetail= this.getTopicDetail(detail, detailRevision);
const item = { detail: itemDetail, position: detail.created, unsealedDetail: null };
await this.addLocalChannelTopic(id, item);
}
} else {
this.topicEntries.delete(id);
await this.store.removeLocalChannelTopic(id);
await this.removeLocalChannelTopic(id);
}
}
this.storeView = { revision: nextRev, marker: delta.marker };
@ -213,7 +215,7 @@ export class FocusModule implements Focus {
try {
const { item } = entry;
if (await this.unsealTopicDetail(item)) {
await this.setLocalChannelTopicUnsealedDetail(guid, topicId, item.unsealedDetail);
await this.setLocalChannelTopicUnsealedDetail(topicId, item.unsealedDetail);
entry.topic = this.setTopic(topicId, item);
}
} catch (err) {
@ -234,7 +236,7 @@ export class FocusModule implements Focus {
if (!connection) {
throw new Error('disconnected from channel');
}
const { node, secure, token } = this.connection;
const { node, secure, token } = connection;
const params = `${cardId ? 'contact' : 'agent'}=${token}`
const url = `http${secure ? 's' : ''}://${node}/content/channels/${channelId}/topics/${topicId}/assets/${blockId}?${params}`
@ -261,7 +263,7 @@ export class FocusModule implements Focus {
if (!connection) {
throw new Error('disconnected from channel');
}
const { node, secure, token } = this.connection;
const { node, secure, token } = connection;
const params = `${cardId ? 'contact' : 'agent'}=${token}`
const url = `http${secure ? 's' : ''}://${node}/content/channels/${channelId}/topics/${topicId}/blocks?${params}`
@ -269,7 +271,7 @@ export class FocusModule implements Focus {
const xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.setRequestHeader('Content-Type', 'text/plain');
xhr.progress = progress;
xhr.onprogress = (ev: ProgressEvent<EventTarget>)=>{ progress(0) };
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
@ -293,7 +295,7 @@ export class FocusModule implements Focus {
if (!connection) {
throw new Error('disconnected from channel');
}
const { node, secure, token } = this.connection;
const { node, secure, token } = connection;
const params = `${cardId ? 'contact' : 'agent'}=${token}&body=multipart`
const url = `http${secure ? 's' : ''}://${node}/content/channels/${channelId}/topics/${topicId}/blocks?${params}`
const formData = new FormData();
@ -302,7 +304,7 @@ export class FocusModule implements Focus {
return new Promise(function (resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.progress = progress;
xhr.onprogress = (ev: ProgressEvent<EventTarget>)=>{ progress(0) };
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
@ -326,7 +328,7 @@ export class FocusModule implements Focus {
if (!connection) {
throw new Error('disconnected from channel');
}
const { node, secure, token } = this.connection;
const { node, secure, token } = connection;
const params = `${cardId ? 'contact' : 'agent'}=${token}&transforms=${encodeURIComponent(JSON.stringify(transforms))}`
const url = `http${secure ? 's' : ''}://${node}/content/channels/${channelId}/topics/${topicId}/assets?${params}`
const formData = new FormData();
@ -335,7 +337,7 @@ export class FocusModule implements Focus {
return new Promise(function (resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.progress = progress;
xhr.onprogress = (ev: ProgressEvent<EventTarget>)=>{ progress(0) };
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
@ -365,9 +367,12 @@ export class FocusModule implements Focus {
if (files.length == 0) {
const data = subject([]);
if (sealed) {
if (!crypto || !channelKey) {
throw new Error('duplicate throw for build warning');
}
const subjectString = JSON.stringify(data);
const { ivHex } = await crypto.aesIv();
const { encryptedDataB64 } = await crypto.aesEncrypt(decryptedString, ivHex, channelKey);
const { encryptedDataB64 } = await crypto.aesEncrypt(subjectString, ivHex, channelKey);
const dataEncrypted = { messageEncrypted: encryptedDataB64, messageIv: ivHex };
return await this.addRemoteChannelTopic(type, dataEncrypted, true);
} else {
@ -389,14 +394,21 @@ export class FocusModule implements Focus {
appAsset.push({appId: transform.appId, assetId: assetItem.assetId});
assetItems.push(assetItem);
} else if (transform.type === TransformType.Copy) {
const mediaFile = await this.media.read(asset.source);
const { media } = this;
if (!media) {
throw new Error('media file processing support not enabled');
}
if (!crypto || !channelKey) {
throw new Error('duplicate throw for build warning');
}
const mediaFile = await media.read(asset.source);
const split = [] as { partId: string, blockIv: string }[];
for (let i = 0; i * ENCRYPT_BLOCK_SIZE < mediaFile.size; i++) {
const length = mediaFile.size - (i * ENCRYPT_BLOCK_SIZE) > ENCRYPT_BLOCK_SIZE ? ENCRYPT_BLOCK_SIZE : mediaFile.size - (i * ENCRYPT_BLOCK_SIZE);
const base64Data = await mediaFile.getData(i * ENCRYPT_BLOCK_SIZE, length);
const { ivHex } = await crypto.aesIv();
const { encryptedDataB64 } = await crypto.aesEncrypt(base64Data, ivHex, channelKey);
const partId = await this.uploadBlock(encryptedDataB64, topicId, (percent: number) => { console.log(`percent: ${percent}`) });
const partId = await this.uploadBlock(encryptedDataB64, topicId, progress);
split.push({ partId, blockIv: ivHex });
}
const assetItem = {
@ -405,7 +417,7 @@ export class FocusModule implements Focus {
hosting: HostingMode.Split,
split,
}
appAsset.push({appId: transform.appId, assetId: assetItems.assetId});
appAsset.push({appId: transform.appId, assetId: assetItem.assetId});
assetItems.push(assetItem);
} else {
throw new Error('transform not supported')
@ -417,55 +429,53 @@ export class FocusModule implements Focus {
const transforms = [];
const transformMap = new Map<string, string>();
for (let transform of asset.transforms) {
if (transform.type === TransformType.Thumb && asset.mimeType === 'image') {
if (transform.type === TransformType.Thumb && asset.type === AssetType.Image) {
transforms.push('ithumb;photo');
transformMap.set('ithumb;photo', transform.appId);
} else if (transform.type === TransformType.HighQualirty && asset.mimeType === 'image') {
} else if (transform.type === TransformType.HighQuality && asset.type === AssetType.Image) {
transforms.push('ilg;photo');
transformMap.set('ilg;photo', transform.appId);
} else if (transform.type === TransformType.Copy && asset.mimeType === 'image') {
} else if (transform.type === TransformType.Copy && asset.type === AssetType.Image) {
transforms.push('icopy;photo');
transformMap.set('icopy;photo', transform.appId);
} else if (transform.type === TransformType.Thumb && asset.mimeType === 'video') {
} else if (transform.type === TransformType.Thumb && asset.type === AssetType.Video) {
transforms.push('vthumb;video');
transformMap.set('vthumb;video', transform.appId);
} else if (transform.type === TransformType.Copy && asset.mimeType === 'video') {
} else if (transform.type === TransformType.Copy && asset.type === AssetType.Video) {
transforms.push('vcopy;video');
transformMap.set('vcopy;video', transform.appId);
} else if (transform.type === TransformType.HighQuality && asset.mimeType === 'video') {
} else if (transform.type === TransformType.HighQuality && asset.type === AssetType.Video) {
transforms.push('vhd;video');
transformMap.set('vhd;video', transform.appId);
} else if (transform.type === TransformType.LowQuality && asset.mimeType === 'video') {
} else if (transform.type === TransformType.LowQuality && asset.type === AssetType.Video) {
transforms.push('vlq;video');
transformMap.set('vlq;video', transform.appId);
} else if (transform.type === TransformType.Copy && asset.mimeType === 'audio') {
} else if (transform.type === TransformType.Copy && asset.type === AssetType.Audio) {
transforms.push('acopy;audio');
transformMap.set('acopy;audio', transform.appId);
} else if (transform.type === TransformType.Copy && asset.mimeType === 'binary') {
const assetId = await this.mirrorFile(asset.source, topicId, (percent: number)=>{console.log(`progress: ${percent}`)});
} else if (transform.type === TransformType.Copy && asset.type === AssetType.Binary) {
const assetId = await this.mirrorFile(asset.source, topicId, progress);
const assetItem = {
assetId: `${assetItems.length}`,
encrytped: false,
hosting: HostingMode.Basic,
basic: assetId,
}
appAsset.push({appId: transform.appId, assetId: assetitem.assetId});
appAsset.push({appId: transform.appId, assetId: assetItem.assetId});
assetItems.push(assetItem);
} else {
throw new Error('transform not supported');
}
}
if (transforms.length > 0) {
const transformAssets = await this.transformFile(asset.source, topicId, transforms, (percent: number)=>{console.log(`progress: ${percent}`)});
const transformAssets = await this.transformFile(asset.source, topicId, transforms, progress);
for (let transformAsset of transformAssets) {
const assetItem = {
assetId: `${assetItems.length}`,
encrypted: false,
hosting: HostingMode.Basic,
basic: transformAsset.assetId,
}
if (transformMap.has(transformAsset.transform)) {
const appId = transformMap.get(transformAsset.transform)
const appId = transformMap.get(transformAsset.transform) || '' //or to make build happy
appAsset.push({appId, assetId: transformAsset.assetId });
assetItems.push(assetItem);
}
@ -482,17 +492,17 @@ export class FocusModule implements Focus {
throw new Error('invalid assetId in subject');
}
if (item.hosting === HostingMode.Inline) {
return item.inline;
}
if (item.hosting === HostingMode.Split) {
return item.inline;
} else if (item.hosting === HostingMode.Split) {
return item.split;
}
if (item.hosting === HostingMode.Basic) {
} else if (item.hosting === HostingMode.Basic) {
return item.basic;
} else {
throw new Error('unknown hosting mode');
}
}
const filtered = !assets ? [] : assets.filter(asset => {
const filtered = !assets ? [] : assets.filter((asset: any)=>{
if (sealed && asset.encrypted) {
return true;
} else if (!sealed && !asset.encrypted) {
@ -501,8 +511,8 @@ export class FocusModule implements Focus {
return false;
}
});
const mapped = filtered.map(asset => {
if (sealed) {
const mapped = filtered.map((asset: any) => {
if (asset.encrypted) {
const { type, thumb, parts } = asset.encrypted;
return { encrypted: { type, thumb: getAsset(thumb), parts: getAsset(parts) } };
} else if (asset.image) {
@ -512,7 +522,7 @@ export class FocusModule implements Focus {
const { thumb, lq, hd } = asset.video;
return { video: { thumb: getAsset(thumb), lq: getAsset(lq), hd: getAsset(hd) } };
} else if (asset.audio) {
const { label, fulle } = asset.audio;
const { label, full } = asset.audio;
return { audio: { label, full: getAsset(full) } };
} else if (asset.binary) {
const { label, extension, data } = asset.binary;
@ -521,6 +531,9 @@ export class FocusModule implements Focus {
});
const updated = { text, textColor, textSize, assets: mapped };
if (sealed) {
if (!crypto || !channelKey) {
throw new Error('duplicate throw to make build happy')
}
const subjectString = JSON.stringify({ message: updated });
const { ivHex } = await crypto.aesIv();
const { encryptedDataB64 } = await crypto.aesEncrypt(subjectString, ivHex, channelKey);
@ -536,48 +549,54 @@ export class FocusModule implements Focus {
public async setTopicSubject(topicId: string, type: string, subject: (assets: {assetId: string, appId: string}[])=>any, files: AssetSource[], progress: (percent: number)=>boolean) {
const entry = this.topicEntries.get(topicId);
if (!entry) {
throw new Error('topic not found');
}
const { item } = entry;
const { sealed } = item.detail;
const { sealEnabled, channelKey, crypto } = this;
if (sealed && (!sealEnabled || !channelKey || !crypto)) {
throw new Error('encryption not set');
}
const item = this.topicEntries(topicId);
if (!item) {
throw new Error('topic not found');
}
const { assets: assetItems } = getTopicData(item);
const { assets: assetItems } = this.getTopicData(item);
const appAsset = [] as {assetId: string, appId: string}[];
if (sealed) {
for (const asset of assets) {
for (const asset of files) {
for (const transform of asset.transforms) {
if (transform.type === TransformType.Thumb && transform.thumb) {
const assetItem = {
assetId: `${assetItems.length}`,
encrytped: true,
hosting: HostingMode.Inline,
inline: await transform.thumb(),
}
appAsset.push({appId: transform.appId, assetId: assetItem.assetId});
assetItems.push(assetItem);
} else if (transform.type === TransformType.Copy) {
const mediaFile = await this.file.read(asset.source);
const { media } = this;
if (!media) {
throw new Error('media file processing support not enabled');
}
if (!crypto || !channelKey) {
throw new Error('duplicate throw for build warning');
}
const mediaFile = await media.read(asset.source);
const split = [] as { partId: string, blockIv: string }[];
for (let i = 0; i * ENCRYPT_BLOCK_SIZE < mediaFile.size; i++) {
const length = mediaFile.size - (i * ENCRYPT_BLOCK_SIZE) > ENCRYPT_BLOCK_SIZE ? ENCRYPT_BLOCK_SIZE : mediaFile.size - (i * ENCRYPT_BLOCK_SIZE);
const base64Data = await mediaFile.getData(i * ENCRYPT_BLOCK_SIZE, length);
const { ivHex } = await crypto.aesIv();
const { encryptedDataB64 } = await crypto.aesEncrypt(base64Data, ivHex, channelKey);
const partId = await this.uploadBlock(encryptedDataB64, topicId, (percent: number) => { console.log(`percent: ${percent}`) });
const partId = await this.uploadBlock(encryptedDataB64, topicId, progress);
split.push({ partId, blockIv: ivHex });
}
const assetItem = {
assetId: `${assetItems.length}`,
encrypted: true,
hosting: HostingMode.Split,
split,
}
appAsset.push({appId: transform.appId, assetId: assetItems.assetId});
appAsset.push({appId: transform.appId, assetId: assetItem.assetId});
assetItems.push(assetItem);
} else {
throw new Error('transform not supported')
@ -585,54 +604,54 @@ export class FocusModule implements Focus {
}
}
} else {
for (const asset of assets) {
for (const asset of files) {
const transforms = [];
const transformMap = new Map<string, string>();
for (let transform of asset.transforms) {
if (transform.type === TransformType.Thumb && asset.mimeType === 'image') {
if (transform.type === TransformType.Thumb && asset.type === AssetType.Image) {
transforms.push('ithumb;photo');
transformMap.set('ithumb;photo', transform.appId);
} else if (transform.type === TransformType.Copy && asset.mimeType === 'image') {
} else if (transform.type === TransformType.Copy && asset.type === AssetType.Image) {
transforms.push('icopy;photo');
transformMap.set('icopy;photo', transform.appId);
} else if (transform.type === TransformType.Thumb && asset.mimeType === 'video') {
} else if (transform.type === TransformType.Thumb && asset.type === AssetType.Video) {
transforms.push('vthumb;video');
transformMap.set('vthumb;video', transform.appId);
} else if (transform.type === TransformType.Copy && asset.mimeType === 'video') {
} else if (transform.type === TransformType.Copy && asset.type === AssetType.Video) {
transforms.push('vcopy;video');
transformMap.set('vcopy;video', transform.appId);
} else if (transform.type === TransformType.LowQuality && asset.mimeType === 'video') {
} else if (transform.type === TransformType.LowQuality && asset.type === AssetType.Video) {
transforms.push('vlq;video');
transformMap.set('vlq;video', transform.appId);
} else if (transform.type === TransformType.Copy && asset.mimeType === 'audio') {
} else if (transform.type === TransformType.Copy && asset.type === AssetType.Audio) {
transforms.push('acopy;audio');
transformMap.set('acopy;audio', transform.appId);
} else if (transform.type === TransformType.Copy && asset.mimeType === 'binary') {
const assetId = await this.mirrorFile(asset.source, topicId, (percent: number)=>{console.log(`progress: ${percent}`)});
} else if (transform.type === TransformType.Copy && asset.type === AssetType.Binary) {
const assetId = await this.mirrorFile(asset.source, topicId, progress);
const assetItem = {
assetId: `${assetItems.length}`,
encrytped: false,
hosting: HostingMode.Basic,
basic: assetId,
}
appAsset.push({appId: transform.appId, assetId: assetitem.assetId});
appAsset.push({appId: transform.appId, assetId: assetItem.assetId});
assetItems.push(assetItem);
} else {
throw new Error('transform not supported');
}
}
if (transforms.length > 0) {
const transformAssets = await this.transformFile(asset.source, topicId, transforms, (percent: number)=>{console.log(`progress: ${percent}`)});
for (transformAsset of transformAssets) {
const transformAssets = await this.transformFile(asset.source, topicId, transforms, progress);
for (let transformAsset of transformAssets) {
const assetItem = {
assetId: `${assetItem.size}`,
encrypted: false,
assetId: `${assetItems.length}`,
hosting: HostingMode.Basic,
basic: transformAsset.assetId,
}
const appId = transformMap.get(assetItem.assetId)
appAsset.push({appId, assetId: assetItem.assetId });
assetItems.push(assetItem);
if (transformMap.get(assetItem.assetId)) {
const appId = transformMap.get(assetItem.assetId) || '' //or to make build happy
appAsset.push({appId, assetId: assetItem.assetId });
assetItems.push(assetItem);
}
}
}
}
@ -641,18 +660,21 @@ export class FocusModule implements Focus {
const getAsset = (assetId: string) => {
const index = parseInt(assetId);
const item = assetItems[index];
if (!item) {
throw new Error('invalid assetId in subject');
}
if (item.hosting === HostingMode.Inline) {
return item.inline;
}
if (item.hosting === HostingMode.Split) {
} if (item.hosting === HostingMode.Split) {
return item.split;
}
if (item.hosting === HostingMode.Basic) {
} if (item.hosting === HostingMode.Basic) {
return item.basic;
} else {
throw new Error('unknown hosting mode');
}
}
const filtered = !assets ? [] : assets.filter(asset => {
const filtered = !assets ? [] : assets.filter((asset: any) => {
if (sealed && asset.encrypted) {
return true;
} else if (!sealed && !asset.encrypted) {
@ -661,7 +683,7 @@ export class FocusModule implements Focus {
return false;
}
});
const mapped = filtered.map(asset => {
const mapped = filtered.map((asset: any) => {
if (sealed) {
const { type, thumb, parts } = asset.encrypted;
return { encrypted: { type, thumb: getAsset(thumb), parts: getAsset(parts) } };
@ -672,7 +694,7 @@ export class FocusModule implements Focus {
const { thumb, lq, hd } = asset.video;
return { video: { thumb: getAsset(thumb), lq: getAsset(lq), hd: getAsset(hd) } };
} else if (asset.audio) {
const { label, fulle } = asset.audio;
const { label, full } = asset.audio;
return { audio: { label, full: getAsset(full) } };
} else if (asset.binary) {
const { label, extension, data } = asset.binary;
@ -682,6 +704,9 @@ export class FocusModule implements Focus {
const updated = { text, textColor, textSize, assets: mapped };
if (sealed) {
if (!crypto || !channelKey) {
throw new Error('duplicate throw to make build happy')
}
const subjectString = JSON.stringify({ message: updated });
const { ivHex } = await crypto.aesIv();
const { encryptedDataB64 } = await crypto.aesEncrypt(subjectString, ivHex, channelKey);
@ -693,39 +718,34 @@ export class FocusModule implements Focus {
}
}
public async removeTopic(topicId: string) {}
public async setUnreadChannel() {}
public async clearUnreadChannel() {}
public async removeTopic(topicId: string) {
await this.removeRemoteChannelTopic(topicId);
}
public async getTopicAssetUrl(topicId: string, assetId: string, progress: (percent: number) => boolean): Promise<string> {
const entry = this.topicEntries.get(topicId);
if (!entry) {
throw new Error('topic entry not found');
}
const { assets } = this.getTopicData(entry);
const asset = assets.find(item => item.assetId === assetid);
const { assets } = this.getTopicData(entry.item);
const asset = assets.find(item => item.assetId === assetId);
if (!asset) {
throw new Error('asset entry not found');
}
if (asset.hosting === HostingMode.Inline) {
if (asset.hosting === HostingMode.Inline && asset.inline) {
return `data://${asset.inline}`;
} else if (asset.hosting === HostingMode.Basic) {
return this.getRemoteChannelTopicAsseturl(topicId, assset.basic);
} else if (asset.hosting === HostingMode.Split) {
const write = this.media.write();
this.closeMedia.push(write.close);
} else if (asset.hosting === HostingMode.Basic && asset.basic) {
return this.getRemoteChannelTopicAssetUrl(topicId, asset.basic);
} else if (asset.hosting === HostingMode.Split && asset.split) {
const { sealEnabled, channelKey, crypto, media } = this;
if (!sealEnabled || !channelKey || !crypto || !media) {
throw new Error('media decryption not set');
}
if (!asset.split || !asset.split.length) {
throw new Error('invalid split media');
throw new Error('media file decryption not set');
}
const write = await media.write();
this.closeMedia.push(write.close);
for (let i = 0; i < asset.split.length; i++) {
const block = await this.downloadBlock(topiccId, asset.split[i].blockId);
const { data } = await this.crypto.aesDecrypt(block, asset.split[i].partIv, channelKey);
const block = await this.downloadBlock(topicId, asset.split[i].partId);
const { data } = await crypto.aesDecrypt(block, asset.split[i].blockIv, channelKey);
await write.setData(data);
}
return await write.getUrl();
@ -734,6 +754,15 @@ export class FocusModule implements Focus {
}
}
public async setUnreadChannel() {}
public async clearUnreadChannel() {}
public async flagTopic(topicId: string) {}
public async setBlockTopic(topicId: string) {}
@ -741,6 +770,11 @@ export class FocusModule implements Focus {
public async clearBlockTopic(topicId: string) {}
private async unsealTopicDetail(item: TopicItem): Promise<boolean> {
if (item.detail.sealed && !item.unsealedDetail && this.sealEnabled && this.channelKey && this.crypto) {
try {
@ -810,7 +844,7 @@ export class FocusModule implements Focus {
await this.sync();
}
public async setChannelKey(cardId: string | null, channelId: string, channelKey: string) {
public async setChannelKey(cardId: string | null, channelId: string, channelKey: string | null) {
if (cardId === this.cardId && channelId === this.channelId) {
this.channelKey = channelKey;
this.unsealAll = true;
@ -830,7 +864,7 @@ export class FocusModule implements Focus {
private emitStatus() {
const status = this.connection ? 'connected' : 'disconnected'
ev(status);
this.emitter.emit('status', status);
}
private getTopicData(item: TopicItem): { data: any, assets: AssetItem[] } {
@ -843,15 +877,12 @@ export class FocusModule implements Focus {
const { text, textColor, textSize, assets } = topicDetail;
let index: number = 0;
const assetItems = new Set<AssetItem>();
const dataAssets = !assets ? [] : assets.map(({ encrypted, image, audio, video, binary }) => {
const dataAssets = !assets ? [] : assets.map(({ encrypted, image, audio, video, binary }: BasicAsset) => {
if (encrypted) {
const { type, thumb, label, extension, parts } = encrypted;
if (thumb) {
const asset = {
assetId: `${revision}.${index}`,
mimeType: 'image/png',
extension: 'png',
encrypted: false,
hosting: HostingMode.Inline,
inline: thumb,
}
@ -860,9 +891,6 @@ export class FocusModule implements Focus {
}
const asset = {
assetId: `${revision}.${index}`,
mimeType: type,
extension: extension,
encrypted: true,
hosting: HostingMode.Split,
split: parts,
}
@ -870,18 +898,15 @@ export class FocusModule implements Focus {
index += 1;
if (thumb) {
return { type, thumb: `${revision}.${index-2}`, data: `${revision}.${index-1}` }
return { encrypted: { type, thumb: `${revision}.${index-2}`, data: `${revision}.${index-1}`, label, extension } }
} else {
return { type, data: `${revision}.${index-1}` }
return { encrypted: { type, data: `${revision}.${index-1}`, label, extension } }
}
} else {
const { thumb, label, full, lq, hd, extension, data } = binary || image || audio || video;
const { thumb, label, full, lq, hd, extension, data } = (binary || image || audio || video) as any;
if (thumb) {
const asset = {
assetId: `${revision}.${index}`,
mimeType: 'image/png',
extension: 'png',
encrypted: false,
hosting: HostingMode.Basic,
basic: thumb,
}
@ -890,20 +915,20 @@ export class FocusModule implements Focus {
}
const asset = {
assetId: `${revision}.${index}`,
mimeType: image ? 'image' : audio ? 'audio' : video ? 'video' : 'binary',
extension: extension,
encrypted: false,
hosting: HostingMode.Basic,
basic: full || hd || lq,
}
assetItems.add(asset);
index += 1;
const type = image ? 'image' : audio ? 'audio' : video ? 'video' : 'binary';
const assetEntry = {} as any;
if (thumb) {
return { type: asset.mimeType, thumb: `${revision}.${index-2}`, data: `${revision}.${index-1}` }
assetEntry[type] = { thumb: `${revision}.${index-2}`, data: `${revision}.${index-1}`, label, extension }
} else {
return { type: asset.mimeType, data: `${revision}.${index-1}` }
assetEntry[type] = { data: `${revision}.${index-1}`, label, extension }
}
return assetEntry;
}
})
return { data: { text, textColor, textSize, assets: dataAssets }, assets: Array.from(assetItems.values()) };
@ -923,13 +948,13 @@ export class FocusModule implements Focus {
status: item.detail.status,
transform: item.detail.transform,
assets: assets.map(asset => {
const { assetId, mimeType, hosting, extension } = asset;
return { assetId, mimeType, hosting, extension };
const { assetId, hosting } = asset;
return { assetId, hosting };
}),
}
}
private getTopicDetail(entity: TopicDetailEntity, revision: number) {
private getTopicDetail(entity: TopicDetailEntity, revision: number): TopicDetail {
const { guid, dataType, data, created, updated, status, transform } = entity;
return {
revision,
@ -967,8 +992,8 @@ export class FocusModule implements Focus {
}
}
private async setChannelTopicRevision(sync: { revision: number, marker: number}) {
const { guid, cardId, channelId, revision } = this;
private async setChannelTopicRevision(sync: { revision: number | null, marker: number | null}) {
const { guid, cardId, channelId } = this;
if (cardId) {
await this.store.setContactCardChannelTopicRevision(guid, cardId, channelId, sync);
}
@ -977,7 +1002,7 @@ export class FocusModule implements Focus {
}
}
private async getLocalChannelTopics(offset: {topicId: string, position: number}) {
private async getLocalChannelTopics(offset: {topicId: string, position: number} | null) {
const { guid, cardId, channelId } = this;
if (cardId) {
return await this.store.getContactCardChannelTopics(guid, cardId, channelId, BATCH_COUNT, offset);
@ -1009,7 +1034,7 @@ export class FocusModule implements Focus {
if (cardId) {
await this.store.setContactCardChannelTopicDetail(guid, cardId, channelId, topicId, detail, unsealedDetail, position);
} else {
await this.store.setContentChannelTopicDetail(guid, channelId, topicId, unsealedDetail);
await this.store.setContentChannelTopicDetail(guid, channelId, topicId, detail, unsealedDetail, position);
}
}
@ -1018,7 +1043,7 @@ export class FocusModule implements Focus {
if (cardId) {
await this.store.setContactCardChannelTopicUnsealedDetail(guid, cardId, channelId, topicId, unsealedDetail);
} else {
await this.store.setContentChannelTopicDetail(guid, channelId, topicId, unsealedDetail);
await this.store.setContentChannelTopicUnsealedDetail(guid, channelId, topicId, unsealedDetail);
}
}
@ -1027,7 +1052,7 @@ export class FocusModule implements Focus {
if (!connection) {
throw new Error('disconnected channel');
}
const { node, secure, token } = this.connection;
const { node, secure, token } = connection;
return `http${secure ? 's' : ''}//${node}/content/channels/${channelId}/topics/${topicId}/assets/${assetId}?${cardId ? 'contact' : 'agent'}=${token}`
}
@ -1036,7 +1061,7 @@ export class FocusModule implements Focus {
if (!connection) {
throw new Error('disconnected channel');
}
const { node, secure, token } = this.connection
const { node, secure, token } = connection
if (cardId) {
return await getContactChannelTopics(node, secure, token, channelId, revision, (begin || !revision) ? BATCH_COUNT : null, begin, end);
} else {
@ -1049,7 +1074,7 @@ export class FocusModule implements Focus {
if (!connection) {
throw new Error('disconnected channel');
}
const { node, secure, token } = this.connection
const { node, secure, token } = connection
if (cardId) {
return await getContactChannelTopicDetail(node, secure, token, channelId, topicId);
} else {
@ -1062,7 +1087,7 @@ export class FocusModule implements Focus {
if (!connection) {
throw new Error('disconnected channel');
}
const { node, secure, token } = this.connection;
const { node, secure, token } = connection;
if (cardId) {
return await addContactChannelTopic(node, secure, token, channelId, dataType, data, confirm);
} else {
@ -1075,7 +1100,7 @@ export class FocusModule implements Focus {
if (!connection) {
throw new Error('disconnected from channel');
}
const { node, secure, token } = this.connection;
const { node, secure, token } = connection;
if (cardId) {
return await setContactChannelTopicSubject(node, secure, token, channelId, topicId, dataType, data);
} else {
@ -1083,14 +1108,14 @@ export class FocusModule implements Focus {
}
}
private async removeRemoveChannelTopic(topicId: string) {
private async removeRemoteChannelTopic(topicId: string) {
const { cardId, channelId, connection } = this;
if (!connection) {
throw new Error('disconnected from channel');
}
const { node, secure, token } = this.connection;
const { node, secure, token } = connection;
if (cardId) {
return await removeContactChannelTopicSubject(node, secure, token, channelId, topicId);
return await removeContactChannelTopic(node, secure, token, channelId, topicId);
} else {
return await removeChannelTopic(node, secure, token, channelId, topicId);
}

View File

@ -157,7 +157,6 @@ export type TopicDetail = {
sealed: boolean;
dataType: string;
data: any;
assets: AssetItem[];
created: number;
updated: number;
status: string;
@ -172,11 +171,8 @@ export type TopicItem = {
export type AssetItem = {
assetId: string;
mimeType: string;
extension: string;
encrypted: boolean;
hosting: HostingMode;
split?: { blockId: string, blockIv: string }[];
split?: { partId: string, blockIv: string }[];
basic?: string;
inline?: string;
};

View File

@ -1,14 +1,14 @@
import { checkResponse, fetchWithTimeout } from './fetchUtil';
import { TopicEntity } from '../entities';
export async function getChannelTopics(node: string, secure: boolean, token: string, channelId: stirng, revision: number | null, count: number | null, begin: number | null, end: number | null): Promise<{marker: number, revision: number, topics: TopicEntity[]}> {
export async function getChannelTopics(node: string, secure: boolean, token: string, channelId: string, revision: number | null, count: number | null, begin: number | null, end: number | null): Promise<{marker: number, revision: number, topics: TopicEntity[]}> {
const params = (revision ? `&revision=${revision}` : '') + (count ? `&count=${count}` : '') + (begin ? `&begin=${begin}` : '') + (end ? `&end=${end}` : '');
const endpoint = `http${secure ? 's' : ''}://${node}/content/channels/${channelId}/topics?agent=${token}${params}`;
const topics = await fetchWithTimeout(endpoint, { method: 'GET' });
checkResponse(topics.status);
return {
marker: parseInt(topics.headers.get('topic-marker')),
revision: parseInt(topics.headers.get('topic-revision')),
marker: parseInt(topics.headers.get('topic-marker') || '0'),
revision: parseInt(topics.headers.get('topic-revision') || '0'),
topics: await topics.json(),
}
}

View File

@ -7,8 +7,8 @@ export async function getContactChannelTopics(node: string, secure: boolean, gui
const topics = await fetchWithTimeout(endpoint, { method: 'GET' });
checkResponse(topics.status);
return {
marker: parseInt(topics.headers.get('topic-marker')),
revision: parseInt(topics.headers.get('topic-revision')),
marker: parseInt(topics.headers.get('topic-marker') || '0'),
revision: parseInt(topics.headers.get('topic-revision') || '0'),
topics: await topics.json(),
}
}

View File

@ -1,7 +1,7 @@
import { checkResponse, fetchWithTimeout } from './fetchUtil';
export async function removeChannelTopic(node: string, secure: boolean, token: string, channelId: string, topicId: string): Promise<void> {
const endpoint = `http${secure ? 's' : ''}://${node}/content/channels/${channelId}/${topics}/${topicId}?agent=${token}`;
const endpoint = `http${secure ? 's' : ''}://${node}/content/channels/${channelId}/topics/${topicId}?agent=${token}`;
const { status } = await fetchWithTimeout(endpoint, { method: 'DELETE' });
checkResponse(status);
}

View File

@ -1,6 +1,6 @@
import { checkResponse, fetchWithTimeout } from './fetchUtil';
export async function removeContactChannelTopic(node: string, secure: boolean, guidToken: string, channelId: string) {
export async function removeContactChannelTopic(node: string, secure: boolean, guidToken: string, channelId: string, topicId: string) {
const endpoint = `http${secure ? 's' : ''}://${node}/content/channels/${channelId}/topics/${topicId}?contact=${guidToken}`;
const response = await fetchWithTimeout(endpoint, { method: 'DELETE' });
checkResponse(response.status);

View File

@ -75,7 +75,7 @@ export interface Store {
setContentChannelUnsealedSummary(guid: string, channelId: string, data: string | null): Promise<void>;
getContentChannelTopicRevision(guid: string, channelId: string): Promise<{ revision: number | null, marker: number | null }>;
setContentChannelTopicRevision(guid: string, channelId: string, sync: { revision: number, marker: number }): Promise<void>;
setContentChannelTopicRevision(guid: string, channelId: string, sync: { revision: number | null, marker: number | null }): Promise<void>;
getContentChannelTopics(guid: string, channelId: string, count: number, position: { topicId: string, position: number } | null): Promise<{ topicId: string, item: TopicItem }[]>;
addContentChannelTopic(guid: string, channelId: string, topicId: string, item: TopicItem): Promise<void>;
removeContentChannelTopic(guid: string, channelId: string, topicId: string): Promise<void>;
@ -83,7 +83,7 @@ export interface Store {
setContentChannelTopicUnsealedDetail(guid: string, channelId: string, topicId: string, unsealedDetail: any): Promise<void>;
getContactCardChannelTopicRevision(guid: string, cardId: string, channelId: string): Promise<{ revision: number | null, marker: number | null }>;
setContactCardChannelTopicRevision(guid: string, cardId: string, channelId: string, sync: { revision: number, marker: number }): Promise<void>;
setContactCardChannelTopicRevision(guid: string, cardId: string, channelId: string, sync: { revision: number | null, marker: number | null }): Promise<void>;
getContactCardChannelTopics(guid: string, cardId: string, channelId: string, count: number, position: { topicId: string, position: number } | null): Promise<{ topicId: string, item: TopicItem }[]>;
addContactCardChannelTopic(guid: string, cardId: string, channelId: string, topicId: string, item: TopicItem): Promise<void>;
removeContactCardChannelTopic(guid: string, cardId: string, channelId: string, topicId: string): Promise<void>;
@ -499,7 +499,7 @@ export class OfflineStore implements Store {
public async getContentChannelTopicRevision(guid: string, channelId: string): Promise<{ revision: number | null, marker: number | null }> {
return await this.getTableValue(guid, 'channel', 'sync', [{field: 'channel_id', value: channelId}], { revision: null, marker: null });
}
public async setContentChannelTopicRevision(guid: string, channelId: string, sync: { revision: number, marker: number }): Promise<void> {
public async setContentChannelTopicRevision(guid: string, channelId: string, sync: { revision: number | null, marker: number | null }): Promise<void> {
await this.setTableValue(guid, 'channel', [{field: 'sync', value: JSON.stringify(sync)}], [{field: 'channel_id', value: channelId}]);
}
public async getContentChannelTopics(guid: string, channelId: string, count: number, offset: { topicId: string, position: number } | null): Promise<{ topicId: string, item: TopicItem }[]> {
@ -536,7 +536,7 @@ export class OfflineStore implements Store {
public async getContactCardChannelTopicRevision(guid: string, cardId: string, channelId: string): Promise<{ revision: number | null, marker: number | null }> {
return await this.getTableValue(guid, 'card_channel', 'sync', [{field: 'card_id', value: cardId},{field: 'channel_id', value: channelId}], { revision: null, marker: null });
}
public async setContactCardChannelTopicRevision(guid: string, cardId: string, channelId: string, sync: { revision: number, marker: number }): Promise<void> {
public async setContactCardChannelTopicRevision(guid: string, cardId: string, channelId: string, sync: { revision: number | null, marker: number | null }): Promise<void> {
return await this.setTableValue(guid, 'card_channel', [{field: 'sync', value: JSON.stringify(sync)}], [{field: 'card_id', value: cardId}, {field: 'channel_id', value: channelId}]);
}
public async getContactCardChannelTopics(guid: string, cardId: string, channelId: string, count: number, offset: { topicId: string, position: number } | null): Promise<{ topicId: string, item: TopicItem }[]> {
@ -752,7 +752,7 @@ export class OnlineStore implements Store {
public async getContentChannelTopicRevision(guid: string, channelId: string): Promise<{ revision: number | null, marker: number | null }> {
return { revision: null, marker: null };
}
public async setContentChannelTopicRevision(guid: string, channelId: string, sync: { revision: number, marker: number }): Promise<void> {
public async setContentChannelTopicRevision(guid: string, channelId: string, sync: { revision: number | null, marker: number | null }): Promise<void> {
}
public async getContentChannelTopics(guid: string, channelId: string, count: number, position: { topicId: string, position: number } | null): Promise<{ topicId: string, item: TopicItem }[]> {
return [];
@ -765,7 +765,7 @@ export class OnlineStore implements Store {
public async getContactCardChannelTopicRevision(guid: string, cardId: string, channelId: string): Promise<{ revision: number | null, marker: number | null }> {
return { revision: null, marker: null };
}
public async setContactCardChannelTopicRevision(guid: string, cardId: string, channelId: string, sync: { revision: number, marker: number }): Promise<void> {
public async setContactCardChannelTopicRevision(guid: string, cardId: string, channelId: string, sync: { revision: number | null, marker: number | null }): Promise<void> {
}
public async getContactCardChannelTopics(guid: string, cardId: string, channelId: string, count: number, position: { topicId: string, position: number } | null): Promise<{ topicId: string, item: TopicItem }[]> {
return [];
@ -912,7 +912,7 @@ export class NoStore implements Store {
public async getContentChannelTopicRevision(guid: string, channelId: string): Promise<{ revision: number | null, marker: number | null }> {
return { revision: null, marker: null };
}
public async setContentChannelTopicRevision(guid: string, channelId: string, sync: { revision: number, marker: number }): Promise<void> {
public async setContentChannelTopicRevision(guid: string, channelId: string, sync: { revision: number | null, marker: number | null }): Promise<void> {
}
public async getContentChannelTopics(guid: string, channelId: string, count: number, position: { topicId: string, position: number } | null): Promise<{ topicId: string, item: TopicItem }[]> {
return [];
@ -925,7 +925,7 @@ export class NoStore implements Store {
public async getContactCardChannelTopicRevision(guid: string, cardId: string, channelId: string): Promise<{ revision: number | null, marker: number | null }> {
return { revision: null, marker: null };
}
public async setContactCardChannelTopicRevision(guid: string, cardId: string, channelId: string, sync: { revision: number, marker: number }): Promise<void> {
public async setContactCardChannelTopicRevision(guid: string, cardId: string, channelId: string, sync: { revision: number | null, marker: number | null }): Promise<void> {
}
public async getContactCardChannelTopics(guid: string, cardId: string, channelId: string, count: number, position: { topicId: string, position: number } | null): Promise<{ topicId: string, item: TopicItem }[]> {
return [];

View File

@ -382,7 +382,7 @@ export class StreamModule {
this.focus = new FocusModule(this.log, this.store, this.crypto, this.media, null, channelId, this.guid, { node, secure, token }, channelKey, sealEnabled, revision);
}
else {
this.focus = new FocusModule(this.log, this.store, this.crypto, this.media, cardId, channelId, this.guid, null, null, false, 0);
this.focus = new FocusModule(this.log, this.store, this.crypto, this.media, null, channelId, this.guid, null, null, false, 0);
}
return this.focus;
@ -518,9 +518,9 @@ export class StreamModule {
item.channelKey = await this.getChannelKey(seals);
if (this.focus) {
try {
await this.focus.setChannelKey(null, channelId, this.channelKey);
await this.focus.setChannelKey(null, channelId, item.channelKey);
} catch (err) {
log.warn(err);
this.log.warn(err);
}
}
}
@ -544,9 +544,9 @@ export class StreamModule {
item.channelKey = await this.getChannelKey(seals);
if (this.focus) {
try {
await this.focus.setChannelKey(null, channelId, this.channelKey);
await this.focus.setChannelKey(null, channelId, item.channelKey);
} catch (err) {
log.warn(err);
this.log.warn(err);
}
}
}

View File

@ -89,6 +89,7 @@ export type Topic = {
topicId: string;
guid: string;
sealed: boolean;
blocked: boolean;
dataType: string;
data: any;
created: number;
@ -123,18 +124,21 @@ export enum TransformType {
LowQuality = 'low', // transcode to low quality
}
export enum AssetType {
Image = 'image',
Video = 'video',
Audio = 'audio',
Binary = 'binary',
}
export type AssetSource = {
name: string;
type: string;
extension: string;
type: AssetType;
source: any;
transforms: {type: TransformType, appId: string, position?: number, thumb?: ()=>Promise<string>}[],
}
export type Asset = {
assetIndex: string;
mimeType: string;
extension: string;
assetId: string;
hosting: HostingMode;
};