Merge pull request #638 from neuroscr/multidevice-publicchat

Public Chat - home server refactor / read public chat avatars from home server
pull/654/head
Ryan Tharp 6 years ago committed by GitHub
commit 6cde9aa263
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -248,8 +248,8 @@
// singleton to interface the File server // singleton to interface the File server
// If already exists we registered as a secondary device // If already exists we registered as a secondary device
if (!window.lokiFileServerAPI) { if (!window.lokiFileServerAPI) {
window.lokiFileServerAPI = new window.LokiFileServerAPI(ourKey); window.lokiFileServerAPIFactory = new window.LokiFileServerAPI(ourKey);
await window.lokiFileServerAPI.establishConnection( window.lokiFileServerAPI = await window.lokiFileServerAPIFactory.establishHomeConnection(
window.getDefaultFileServer() window.getDefaultFileServer()
); );
} }
@ -904,6 +904,12 @@
displayName: newName, displayName: newName,
avatar: newAvatarPath, avatar: newAvatarPath,
}); });
// inform all your registered public servers
// could put load on all the servers
// if they just keep changing their names without sending messages
// so we could disable this here
// or least it enable for the quickest response
window.lokiPublicChatAPI.setProfileName(newName);
}, },
}); });
} }
@ -1134,8 +1140,8 @@
if (Whisper.Registration.ongoingSecondaryDeviceRegistration()) { if (Whisper.Registration.ongoingSecondaryDeviceRegistration()) {
const ourKey = textsecure.storage.user.getNumber(); const ourKey = textsecure.storage.user.getNumber();
window.lokiMessageAPI = new window.LokiMessageAPI(ourKey); window.lokiMessageAPI = new window.LokiMessageAPI(ourKey);
window.lokiFileServerAPI = new window.LokiFileServerAPI(ourKey); window.lokiFileServerAPIFactory = new window.LokiFileServerAPI(ourKey);
await window.lokiFileServerAPI.establishConnection( window.lokiFileServerAPI = await window.lokiFileServerAPIFactory.establishHomeConnection(
window.getDefaultFileServer() window.getDefaultFileServer()
); );
window.localLokiServer = null; window.localLokiServer = null;

@ -1,4 +1,4 @@
/* global _, Whisper, Backbone, storage, textsecure, libsignal, lokiPublicChatAPI */ /* global _, Whisper, Backbone, storage, textsecure, libsignal, log */
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
@ -160,8 +160,12 @@
return; return;
} }
if (conversation.isPublic()) { if (conversation.isPublic()) {
const server = conversation.getPublicSource(); const channelAPI = await conversation.getPublicSendData();
lokiPublicChatAPI.unregisterChannel(server.server, server.channelId); if (channelAPI === null) {
log.warn(`Could not get API for public conversation ${id}`);
} else {
channelAPI.serverAPI.partChannel(channelAPI.channelId);
}
} }
await conversation.destroyMessages(); await conversation.destroyMessages();
const deviceIds = await textsecure.storage.protocol.getDeviceIds(id); const deviceIds = await textsecure.storage.protocol.getDeviceIds(id);

@ -2333,7 +2333,7 @@
); );
return null; return null;
} }
const channelAPI = serverAPI.findOrCreateChannel( const channelAPI = await serverAPI.findOrCreateChannel(
this.get('channelId'), this.get('channelId'),
this.id this.id
); );

@ -1,5 +1,5 @@
/* global log, textsecure, libloki, Signal, Whisper, Headers, ConversationController, /* global log, textsecure, libloki, Signal, Whisper, Headers, ConversationController,
clearTimeout, MessageController, libsignal, StringView, window, _, lokiFileServerAPI, clearTimeout, MessageController, libsignal, StringView, window, _,
dcodeIO, Buffer */ dcodeIO, Buffer */
const EventEmitter = require('events'); const EventEmitter = require('events');
const nodeFetch = require('node-fetch'); const nodeFetch = require('node-fetch');
@ -13,7 +13,8 @@ const PUBLICCHAT_DELETION_POLL_EVERY = 5 * 1000; // 5s
const PUBLICCHAT_MOD_POLL_EVERY = 30 * 1000; // 30s const PUBLICCHAT_MOD_POLL_EVERY = 30 * 1000; // 30s
const PUBLICCHAT_MIN_TIME_BETWEEN_DUPLICATE_MESSAGES = 10 * 1000; // 10s const PUBLICCHAT_MIN_TIME_BETWEEN_DUPLICATE_MESSAGES = 10 * 1000; // 10s
const ATTACHMENT_TYPE = 'net.app.core.oembed'; const HOMESERVER_USER_ANNOTATION_TYPE = 'network.loki.messenger.homeserver';
const MESSAGE_ATTACHMENT_TYPE = 'net.app.core.oembed';
const LOKI_ATTACHMENT_TYPE = 'attachment'; const LOKI_ATTACHMENT_TYPE = 'attachment';
const LOKI_PREVIEW_TYPE = 'preview'; const LOKI_PREVIEW_TYPE = 'preview';
@ -26,7 +27,6 @@ class LokiAppDotNetAPI extends EventEmitter {
this.myPrivateKey = false; this.myPrivateKey = false;
this.allMembers = []; this.allMembers = [];
// Multidevice states // Multidevice states
this.slavePrimaryMap = {};
this.primaryUserProfileName = {}; this.primaryUserProfileName = {};
} }
@ -91,7 +91,21 @@ class LokiAppDotNetAPI extends EventEmitter {
this.servers.splice(i, 1); this.servers.splice(i, 1);
} }
getListOfMembers() { // shouldn't this be scoped per conversation?
async getListOfMembers() {
// enable in the next release
/*
let members = [];
await Promise.all(this.servers.map(async server => {
await Promise.all(server.channels.map(async channel => {
const newMembers = await channel.getSubscribers();
members = [...members, ...newMembers];
}));
}));
const results = members.map(member => {
return { authorPhoneNumber: member.username };
});
*/
return this.allMembers; return this.allMembers;
} }
@ -100,6 +114,25 @@ class LokiAppDotNetAPI extends EventEmitter {
setListOfMembers(members) { setListOfMembers(members) {
this.allMembers = members; this.allMembers = members;
} }
async setProfileName(profileName) {
await Promise.all(
this.servers.map(async server => {
await server.setProfileName(profileName);
})
);
}
async setHomeServer(homeServer) {
await Promise.all(
this.servers.map(async server => {
// this may fail
// but we can't create a sql table to remember to retry forever
// I think we just silently fail for now
await server.setHomeServer(homeServer);
})
);
}
} }
class LokiAppDotNetServerAPI { class LokiAppDotNetServerAPI {
@ -118,18 +151,30 @@ class LokiAppDotNetServerAPI {
} }
// channel getter/factory // channel getter/factory
findOrCreateChannel(channelId, conversationId) { async findOrCreateChannel(channelId, conversationId) {
let thisChannel = this.channels.find( let thisChannel = this.channels.find(
channel => channel.channelId === channelId channel => channel.channelId === channelId
); );
if (!thisChannel) { if (!thisChannel) {
log.info(`LokiAppDotNetAPI creating channel ${conversationId}`); log.info(`LokiAppDotNetAPI registering channel ${conversationId}`);
// make sure we're subscribed
// eventually we'll need to move to account registration/add server
await this.serverRequest(`channels/${channelId}/subscribe`, {
method: 'POST',
});
thisChannel = new LokiPublicChannelAPI(this, channelId, conversationId); thisChannel = new LokiPublicChannelAPI(this, channelId, conversationId);
this.channels.push(thisChannel); this.channels.push(thisChannel);
} }
return thisChannel; return thisChannel;
} }
async partChannel(channelId) {
await this.serverRequest(`channels/${channelId}/subscribe`, {
method: 'DELETE',
});
this.unregisterChannel(channelId);
}
// deallocate resources channel uses // deallocate resources channel uses
unregisterChannel(channelId) { unregisterChannel(channelId) {
let thisChannel; let thisChannel;
@ -147,6 +192,68 @@ class LokiAppDotNetServerAPI {
this.channels.splice(i, 1); this.channels.splice(i, 1);
} }
async setProfileName(profileName) {
// when we add an annotation, may need this
/*
const privKey = await this.serverAPI.chatAPI.getPrivateKey();
// we might need an annotation that sets the homeserver for media
// better to include this with each attachment...
const objToSign = {
name: profileName,
version: 1,
annotations: [],
};
const sig = await libsignal.Curve.async.calculateSignature(
privKey,
JSON.stringify(objToSign)
);
*/
const res = await this.serverRequest('users/me', {
method: 'PATCH',
objBody: {
name: profileName,
},
});
// no big deal if it fails...
if (res.err || !res.response || !res.response.data) {
if (res.err) {
log.error(`Error ${res.err}`);
}
return [];
}
// expecting a user object
return res.response.data.annotations || [];
// if no profileName should we update the local from the server?
// no because there will be multiple public chat servers
}
async setHomeServer(homeServer) {
const res = await this.serverRequest('users/me', {
method: 'PATCH',
objBody: {
annotations: [
{
type: HOMESERVER_USER_ANNOTATION_TYPE,
value: homeServer,
},
],
},
});
if (res.err || !res.response || !res.response.data) {
if (res.err) {
log.error(`Error ${res.err}`);
}
return [];
}
// expecting a user object
return res.response.data.annotations || [];
}
// get active token for this server // get active token for this server
async getOrRefreshServerToken(forceRefresh = false) { async getOrRefreshServerToken(forceRefresh = false) {
let token; let token;
@ -178,42 +285,14 @@ class LokiAppDotNetServerAPI {
tokenRes.response.data && tokenRes.response.data &&
tokenRes.response.data.user tokenRes.response.data.user
) { ) {
// get our profile name and write it to the network // get our profile name
const ourNumber = textsecure.storage.user.getNumber(); const ourNumber = textsecure.storage.user.getNumber();
const profileConvo = ConversationController.get(ourNumber); const profileConvo = ConversationController.get(ourNumber);
const profileName = profileConvo.getProfileName(); const profileName = profileConvo.getProfileName();
// if doesn't match, write it to the network
// update profile name as needed
if (tokenRes.response.data.user.name !== profileName) { if (tokenRes.response.data.user.name !== profileName) {
if (profileName) { // update our profile name if it got out of sync
// will need this when we add an annotation this.setProfileName(profileName);
/*
const privKey = await this.serverAPI.chatAPI.getPrivateKey();
// we might need an annotation that sets the homeserver for media
// better to include this with each attachment...
const objToSign = {
name: profileName,
version: 1,
annotations: [],
};
const sig = await libsignal.Curve.async.calculateSignature(
privKey,
JSON.stringify(objToSign)
);
*/
await this.serverRequest('users/me', {
method: 'PATCH',
objBody: {
name: profileName,
},
});
// no big deal if it fails...
// } else {
// should we update the local from the server?
// guessing no because there will be multiple servers
}
// update our avatar if needed
} }
} }
@ -393,6 +472,71 @@ class LokiAppDotNetServerAPI {
return res.response.data.annotations || []; return res.response.data.annotations || [];
} }
async getSubscribers(channelId, wantObjects) {
if (!channelId) {
log.warn('No channelId provided to getSubscribers!');
return [];
}
let res = {};
if (!Array.isArray(channelId) && wantObjects) {
res = await this.serverRequest(`channels/${channelId}/subscribers`, {
method: 'GET',
params: {
include_user_annotations: 1,
},
});
} else {
// not deployed on all backends yet
res.err = 'array subscribers endpoint not yet implemented';
/*
var list = channelId;
if (!Array.isArray(list)) {
list = [channelId];
}
const idres = await this.serverRequest(`channels/subscribers/ids`, {
method: 'GET',
params: {
ids: list.join(','),
include_user_annotations: 1,
},
});
if (wantObjects) {
if (idres.err || !idres.response || !idres.response.data) {
if (idres.err) {
log.error(`Error ${idres.err}`);
}
return [];
}
const userList = [];
await Promise.all(idres.response.data.map(async channelId => {
const channelUserObjs = await this.getUsers(idres.response.data[channelId]);
userList.push(...channelUserObjs);
}));
res = {
response: {
meta: {
code: 200,
},
data: userList
}
}
} else {
res = idres;
}
*/
}
if (res.err || !res.response || !res.response.data) {
if (res.err) {
log.error(`Error ${res.err}`);
}
return [];
}
return res.response.data || [];
}
async getUsers(pubKeys) { async getUsers(pubKeys) {
if (!pubKeys) { if (!pubKeys) {
log.warn('No pubKeys provided to getUsers!'); log.warn('No pubKeys provided to getUsers!');
@ -565,6 +709,10 @@ class LokiPublicChannelAPI {
return this.serverAPI.serverRequest(endpoint, options); return this.serverAPI.serverRequest(endpoint, options);
} }
getSubscribers() {
return this.serverAPI.getSubscribers(this.channelId, true);
}
// get moderation actions // get moderation actions
async pollForModerators() { async pollForModerators() {
try { try {
@ -874,6 +1022,7 @@ class LokiPublicChannelAPI {
async pollOnceForMessages() { async pollOnceForMessages() {
const params = { const params = {
include_annotations: 1, include_annotations: 1,
include_user_annotations: 1, // to get the home server
include_deleted: false, include_deleted: false,
}; };
if (!this.conversation) { if (!this.conversation) {
@ -885,6 +1034,7 @@ class LokiPublicChannelAPI {
params.since_id = this.lastGot; params.since_id = this.lastGot;
// Just grab the most recent 100 messages if you don't have a valid lastGot // Just grab the most recent 100 messages if you don't have a valid lastGot
params.count = this.lastGot === 0 ? -100 : 20; params.count = this.lastGot === 0 ? -100 : 20;
// log.info(`Getting ${params.count} from ${this.lastGot} on ${this.baseChannelUrl}`);
const res = await this.serverRequest(`${this.baseChannelUrl}/messages`, { const res = await this.serverRequest(`${this.baseChannelUrl}/messages`, {
params, params,
}); });
@ -894,11 +1044,16 @@ class LokiPublicChannelAPI {
} }
let receivedAt = new Date().getTime(); let receivedAt = new Date().getTime();
const pubKeys = []; const homeServerPubKeys = {};
let pendingMessages = []; let pendingMessages = [];
// get our profile name
const ourNumber = textsecure.storage.user.getNumber();
let lastProfileName = false;
// the signature forces this to be async // the signature forces this to be async
pendingMessages = await Promise.all( pendingMessages = await Promise.all(
// process these in chronological order
res.response.data.reverse().map(async adnMessage => { res.response.data.reverse().map(async adnMessage => {
// still update our last received if deleted, not signed or not valid // still update our last received if deleted, not signed or not valid
this.lastGot = !this.lastGot this.lastGot = !this.lastGot
@ -920,13 +1075,7 @@ class LokiPublicChannelAPI {
return false; return false;
} }
const { const { timestamp, quote, attachments, preview } = messengerData;
timestamp,
quote,
attachments,
preview,
avatar,
} = messengerData;
if (!timestamp) { if (!timestamp) {
return false; // Invalid message return false; // Invalid message
} }
@ -961,11 +1110,35 @@ class LokiPublicChannelAPI {
].splice(-5); ].splice(-5);
const from = adnMessage.user.name || 'Anonymous'; // profileName const from = adnMessage.user.name || 'Anonymous'; // profileName
const avatarObj = avatar || null;
// if us
if (adnMessage.user.username === ourNumber) {
// update the last name we saw from ourself
lastProfileName = from;
}
// track sources for multidevice support // track sources for multidevice support
if (pubKeys.indexOf(`@${adnMessage.user.username}`) === -1) { // sort it by home server
pubKeys.push(`@${adnMessage.user.username}`); let homeServer = window.getDefaultFileServer();
if (adnMessage.user && adnMessage.user.annotations.length) {
const homeNotes = adnMessage.user.annotations.filter(
note => note.type === HOMESERVER_USER_ANNOTATION_TYPE
);
// FIXME: this annotation should probably be signed and verified...
homeServer = homeNotes.reduce(
(curVal, note) => (note.value ? note.value : curVal),
homeServer
);
}
if (homeServerPubKeys[homeServer] === undefined) {
homeServerPubKeys[homeServer] = [];
}
if (
homeServerPubKeys[homeServer].indexOf(
`@${adnMessage.user.username}`
) === -1
) {
homeServerPubKeys[homeServer].push(`@${adnMessage.user.username}`);
} }
// generate signal message object // generate signal message object
@ -999,7 +1172,6 @@ class LokiPublicChannelAPI {
preview, preview,
profile: { profile: {
displayName: from, displayName: from,
avatar: avatarObj,
}, },
}, },
}; };
@ -1010,27 +1182,59 @@ class LokiPublicChannelAPI {
return messageData; return messageData;
}) })
); );
this.conversation.setLastRetrievedMessage(this.lastGot);
// do we really need this? // do we really need this?
if (!pendingMessages.length) { if (!pendingMessages.length) {
this.conversation.setLastRetrievedMessage(this.lastGot);
return; return;
} }
// slave to primary map for this group of messages
let slavePrimaryMap = {};
// pubKey to avatar
let avatarMap = {};
// reduce list of servers into verified maps and keys
const verifiedPrimaryPKs = await Object.keys(homeServerPubKeys).reduce(
async (curVal, serverUrl) => {
// get an API to this server
const serverAPI = await window.lokiFileServerAPIFactory.establishConnection(
serverUrl
);
// get list of verified primary PKs // get list of verified primary PKs
const verifiedPrimaryPKs = await lokiFileServerAPI.verifyPrimaryPubKeys( const result = await serverAPI.verifyPrimaryPubKeys(
pubKeys homeServerPubKeys[serverUrl]
);
// merged these device mappings into our slavePrimaryMap
// should not be any collisions, since each pubKey can only have one home server
slavePrimaryMap = { ...slavePrimaryMap, ...result.slaveMap };
// merge this servers avatarMap into our map
// again shouldn't be any collisions
avatarMap = { ...avatarMap, ...serverAPI.avatarMap };
// copy verified pub keys into result
return curVal.concat(result.verifiedPrimaryPKs);
},
[]
); );
// access slavePrimaryMap set by verifyPrimaryPubKeys
const { slavePrimaryMap } = this.serverAPI.chatAPI;
// sort pending messages by if slave device or not // sort pending messages by if slave device or not
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
const slaveMessages = pendingMessages const slaveMessages = pendingMessages
.filter(messageData => !!messageData) .filter(messageData => !!messageData) // filter out false messages
.reduce((retval, messageData) => { .reduce((retval, messageData) => {
// if a known slave, queue // if a known slave
if (slavePrimaryMap[messageData.source]) { if (slavePrimaryMap[messageData.source]) {
// pop primary device avatars in
if (avatarMap[slavePrimaryMap[messageData.source]]) {
// modify messageData for user's avatar
messageData.message.profile.avatar =
avatarMap[slavePrimaryMap[messageData.source]];
}
// delay sending the message // delay sending the message
if (retval[messageData.source] === undefined) { if (retval[messageData.source] === undefined) {
retval[messageData.source] = [messageData]; retval[messageData.source] = [messageData];
@ -1038,7 +1242,15 @@ class LokiPublicChannelAPI {
retval[messageData.source].push(messageData); retval[messageData.source].push(messageData);
} }
} else { } else {
// no user or isPrimary means not multidevice, send event now // not from a paired/slave/unregistered device
// pop current device avatars in
if (avatarMap[messageData.source]) {
// modify messageData for user's avatar
messageData.message.profile.avatar = avatarMap[messageData.source];
}
// send event now
this.serverAPI.chatAPI.emit('publicMessage', { this.serverAPI.chatAPI.emit('publicMessage', {
message: messageData, message: messageData,
}); });
@ -1066,7 +1278,6 @@ class LokiPublicChannelAPI {
/* eslint-enable no-param-reassign */ /* eslint-enable no-param-reassign */
// process remaining messages // process remaining messages
const ourNumber = textsecure.storage.user.getNumber();
Object.keys(slaveMessages).forEach(slaveKey => { Object.keys(slaveMessages).forEach(slaveKey => {
// prevent our own device sent messages from coming back in // prevent our own device sent messages from coming back in
if (slaveKey === ourNumber) { if (slaveKey === ourNumber) {
@ -1092,6 +1303,21 @@ class LokiPublicChannelAPI {
}); });
}); });
}); });
// if we received one of our own messages
if (lastProfileName !== false) {
// get current profileName
const profileConvo = ConversationController.get(ourNumber);
const profileName = profileConvo.getProfileName();
// check to see if it out of sync
if (profileName !== lastProfileName) {
// out of sync, update this server
this.serverAPI.setProfileName(profileName);
}
}
// finally update our position
this.conversation.setLastRetrievedMessage(this.lastGot);
} }
static getPreviewFromAnnotation(annotation) { static getPreviewFromAnnotation(annotation) {
@ -1119,7 +1345,7 @@ class LokiPublicChannelAPI {
static getAnnotationFromPreview(preview) { static getAnnotationFromPreview(preview) {
const annotation = { const annotation = {
type: ATTACHMENT_TYPE, type: MESSAGE_ATTACHMENT_TYPE,
value: { value: {
// Mandatory ADN fields // Mandatory ADN fields
version: '1.0', version: '1.0',
@ -1157,7 +1383,7 @@ class LokiPublicChannelAPI {
type = 'other'; type = 'other';
} }
const annotation = { const annotation = {
type: ATTACHMENT_TYPE, type: MESSAGE_ATTACHMENT_TYPE,
value: { value: {
// Mandatory ADN fields // Mandatory ADN fields
version: '1.0', version: '1.0',
@ -1191,6 +1417,7 @@ class LokiPublicChannelAPI {
type: 'network.loki.messenger.publicChat', type: 'network.loki.messenger.publicChat',
value: { value: {
timestamp: messageTimeStamp, timestamp: messageTimeStamp,
// can remove after this release
avatar: avatarAnnotation, avatar: avatarAnnotation,
}, },
}, },

@ -1,64 +1,50 @@
/* global window, log, libloki */ /* global log, libloki */
/* global storage: false */ /* global storage: false */
/* global Signal: false */ /* global Signal: false */
/* global log: false */ /* global log: false */
const LokiAppDotNetAPI = require('./loki_app_dot_net_api'); const LokiAppDotNetAPI = require('./loki_app_dot_net_api');
const DEVICE_MAPPING_ANNOTATION_KEY = 'network.loki.messenger.devicemapping'; const DEVICE_MAPPING_USER_ANNOTATION_TYPE =
'network.loki.messenger.devicemapping';
// can have multiple of these objects instances as each user can have a // can have multiple of these instances as each user can have a
// different home server // different home server
class LokiFileServerAPI { class LokiFileServerInstance {
constructor(ourKey) { constructor(ourKey) {
this.ourKey = ourKey; this.ourKey = ourKey;
// why don't we extend this?
this._adnApi = new LokiAppDotNetAPI(ourKey); this._adnApi = new LokiAppDotNetAPI(ourKey);
this.avatarMap = {};
} }
async establishConnection(serverUrl) { async establishConnection(serverUrl) {
// FIXME: we don't always need a token...
this._server = await this._adnApi.findOrCreateServer(serverUrl); this._server = await this._adnApi.findOrCreateServer(serverUrl);
// TODO: Handle this failure gracefully // TODO: Handle this failure gracefully
if (!this._server) { if (!this._server) {
log.error('Failed to establish connection to file server'); log.error('Failed to establish connection to file server');
} }
} }
async getUserDeviceMapping(pubKey) { async getUserDeviceMapping(pubKey) {
const annotations = await this._server.getUserAnnotations(pubKey); const annotations = await this._server.getUserAnnotations(pubKey);
const deviceMapping = annotations.find( const deviceMapping = annotations.find(
annotation => annotation.type === DEVICE_MAPPING_ANNOTATION_KEY annotation => annotation.type === DEVICE_MAPPING_USER_ANNOTATION_TYPE
); );
return deviceMapping ? deviceMapping.value : null; return deviceMapping ? deviceMapping.value : null;
} }
async updateOurDeviceMapping() {
const isPrimary = !storage.get('isSecondaryDevice');
let authorisations;
if (isPrimary) {
authorisations = await Signal.Data.getGrantAuthorisationsForPrimaryPubKey(
this.ourKey
);
} else {
authorisations = [
await Signal.Data.getGrantAuthorisationForSecondaryPubKey(this.ourKey),
];
}
return this._setOurDeviceMapping(authorisations, isPrimary);
}
async getDeviceMappingForUsers(pubKeys) {
const users = await this._server.getUsers(pubKeys);
return users;
}
async verifyUserObjectDeviceMap(pubKeys, isRequest, iterator) { async verifyUserObjectDeviceMap(pubKeys, isRequest, iterator) {
const users = await this.getDeviceMappingForUsers(pubKeys); const users = await this._server.getUsers(pubKeys);
// go through each user and find deviceMap annotations // go through each user and find deviceMap annotations
const notFoundUsers = []; const notFoundUsers = [];
await Promise.all( await Promise.all(
users.map(async user => { users.map(async user => {
let found = false; let found = false;
// if this user has an avatar set, copy it into the map
this.avatarMap[user.username] = user.avatar_image
? user.avatar_image.url
: false;
if (!user.annotations || !user.annotations.length) { if (!user.annotations || !user.annotations.length) {
log.info( log.info(
`verifyUserObjectDeviceMap no annotation for ${user.username}` `verifyUserObjectDeviceMap no annotation for ${user.username}`
@ -66,7 +52,7 @@ class LokiFileServerAPI {
return; return;
} }
const mappingNote = user.annotations.find( const mappingNote = user.annotations.find(
note => note.type === DEVICE_MAPPING_ANNOTATION_KEY note => note.type === DEVICE_MAPPING_USER_ANNOTATION_TYPE
); );
const { authorisations } = mappingNote.value; const { authorisations } = mappingNote.value;
if (!Array.isArray(authorisations)) { if (!Array.isArray(authorisations)) {
@ -105,6 +91,10 @@ class LokiFileServerAPI {
// checkSig disabled for now // checkSig disabled for now
// const checkSigs = {}; // cache for authorisation // const checkSigs = {}; // cache for authorisation
const primaryPubKeys = []; const primaryPubKeys = [];
const result = {
verifiedPrimaryPKs: [],
slaveMap: {},
};
// go through multiDeviceResults and get primary Pubkey // go through multiDeviceResults and get primary Pubkey
await this.verifyUserObjectDeviceMap(pubKeys, true, (slaveKey, auth) => { await this.verifyUserObjectDeviceMap(pubKeys, true, (slaveKey, auth) => {
@ -144,7 +134,8 @@ class LokiFileServerAPI {
// no valid primary pubkeys to check // no valid primary pubkeys to check
if (!primaryPubKeys.length) { if (!primaryPubKeys.length) {
// log.warn(`no valid primary pubkeys to check ${pubKeys}`); // log.warn(`no valid primary pubkeys to check ${pubKeys}`);
return []; // do we want to update slavePrimaryMap?
return result;
} }
const verifiedPrimaryPKs = []; const verifiedPrimaryPKs = [];
@ -193,30 +184,43 @@ class LokiFileServerAPI {
}); });
}); });
// FIXME: move to a return value since we're only scoped to pubkeys given log.info(`Updated device mappings ${JSON.stringify(newSlavePrimaryMap)}`);
// make new map final
window.lokiPublicChatAPI.slavePrimaryMap = newSlavePrimaryMap;
log.info(
`Updated device mappings ${JSON.stringify(
window.lokiPublicChatAPI.slavePrimaryMap
)}`
);
return verifiedPrimaryPKs; result.verifiedPrimaryPKs = verifiedPrimaryPKs;
result.slaveMap = newSlavePrimaryMap;
return result;
} }
}
// extends LokiFileServerInstance with functions we'd only perform on our own home server
// so we don't accidentally send info to the wrong file server
class LokiHomeServerInstance extends LokiFileServerInstance {
_setOurDeviceMapping(authorisations, isPrimary) { _setOurDeviceMapping(authorisations, isPrimary) {
const content = { const content = {
isPrimary: isPrimary ? '1' : '0', isPrimary: isPrimary ? '1' : '0',
authorisations, authorisations,
}; };
return this._server.setSelfAnnotation( return this._server.setSelfAnnotation(
DEVICE_MAPPING_ANNOTATION_KEY, DEVICE_MAPPING_USER_ANNOTATION_TYPE,
content content
); );
} }
async updateOurDeviceMapping() {
const isPrimary = !storage.get('isSecondaryDevice');
let authorisations;
if (isPrimary) {
authorisations = await Signal.Data.getGrantAuthorisationsForPrimaryPubKey(
this.ourKey
);
} else {
authorisations = [
await Signal.Data.getGrantAuthorisationForSecondaryPubKey(this.ourKey),
];
}
return this._setOurDeviceMapping(authorisations, isPrimary);
}
uploadAvatar(data) { uploadAvatar(data) {
return this._server.uploadAvatar(data); return this._server.uploadAvatar(data);
} }
@ -230,4 +234,38 @@ class LokiFileServerAPI {
} }
} }
module.exports = LokiFileServerAPI; // this will be our instance factory
class LokiFileServerFactoryAPI {
constructor(ourKey) {
this.ourKey = ourKey;
this.servers = [];
}
async establishHomeConnection(serverUrl) {
let thisServer = this.servers.find(
server => server._server.baseServerUrl === serverUrl
);
if (!thisServer) {
thisServer = new LokiHomeServerInstance(this.ourKey);
log.info(`Registering HomeServer ${serverUrl}`);
await thisServer.establishConnection(serverUrl);
this.servers.push(thisServer);
}
return thisServer;
}
async establishConnection(serverUrl) {
let thisServer = this.servers.find(
server => server._server.baseServerUrl === serverUrl
);
if (!thisServer) {
thisServer = new LokiFileServerInstance(this.ourKey);
log.info(`Registering FileServer ${serverUrl}`);
await thisServer.establishConnection(serverUrl);
this.servers.push(thisServer);
}
return thisServer;
}
}
module.exports = LokiFileServerFactoryAPI;

@ -48,7 +48,7 @@
conversationId, conversationId,
'group' 'group'
); );
serverAPI.findOrCreateChannel(channelId, conversationId); await serverAPI.findOrCreateChannel(channelId, conversationId);
await conversation.setPublicSource(sslServerUrl, channelId); await conversation.setPublicSource(sslServerUrl, channelId);
await conversation.setFriendRequestStatus( await conversation.setFriendRequestStatus(
friends.friendRequestStatusEnum.friends friends.friendRequestStatusEnum.friends

@ -2568,7 +2568,7 @@
this.view.scrollToBottomIfNeeded(); this.view.scrollToBottomIfNeeded();
}, },
maybeShowMembers(event) { async maybeShowMembers(event) {
const filterMembers = (caseSensitiveQuery, member) => { const filterMembers = (caseSensitiveQuery, member) => {
const { authorPhoneNumber, authorProfileName } = member; const { authorPhoneNumber, authorProfileName } = member;
@ -2613,8 +2613,11 @@
let allMembers; let allMembers;
if (this.model.isPublic()) { if (this.model.isPublic()) {
const members = window.lokiPublicChatAPI // const api = await this.model.getPublicSendData();
.getListOfMembers() // not quite in the right format tho yet...
// let members = await api.getSubscribers();
let members = await window.lokiPublicChatAPI.getListOfMembers();
members = members
.filter(d => !!d) .filter(d => !!d)
.filter(d => d.authorProfileName !== 'Anonymous'); .filter(d => d.authorProfileName !== 'Anonymous');
allMembers = _.uniq(members, true, d => d.authorPhoneNumber); allMembers = _.uniq(members, true, d => d.authorPhoneNumber);

@ -79,7 +79,7 @@ class Mention extends React.Component<MentionProps, MentionState> {
} }
} }
private findMember(pubkey: String) { private async findMember(pubkey: String) {
let groupMembers; let groupMembers;
const groupConvos = window.getConversations().models.filter((d: any) => { const groupConvos = window.getConversations().models.filter((d: any) => {
@ -97,10 +97,8 @@ class Mention extends React.Component<MentionProps, MentionState> {
} }
if (thisConvo.isPublic()) { if (thisConvo.isPublic()) {
// TODO: make this work for other public chats as well groupMembers = await window.lokiPublicChatAPI.getListOfMembers();
groupMembers = window.lokiPublicChatAPI groupMembers = groupMembers.filter((m: any) => !!m);
.getListOfMembers()
.filter((m: any) => !!m);
} else { } else {
const privateConvos = window const privateConvos = window
.getConversations() .getConversations()

Loading…
Cancel
Save