Merge pull request #787 from neuroscr/tls-fix

Quality of life fixes
pull/794/head
Ryan Tharp 5 years ago committed by GitHub
commit 7919d22397
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -19,4 +19,4 @@ If applicable, add screenshots or log files to help explain your problem.
* Device: [e.g. PC, Mac] * Device: [e.g. PC, Mac]
* OS: [e.g. Ubuntu 16.04, Windows 10] * OS: [e.g. Ubuntu 16.04, Windows 10]
* Loki messenger Version or Git commit hash: * Session Version or Git commit hash:

@ -2,11 +2,11 @@
[![Build Status](https://travis-ci.org/loki-project/loki-messenger.svg?branch=development)](https://travis-ci.org/loki-project/loki-messenger) [![Build Status](https://travis-ci.org/loki-project/loki-messenger.svg?branch=development)](https://travis-ci.org/loki-project/loki-messenger)
Session allows for truly decentralized, end to end, and private encrypted chats. Session is built to handle both online and fully Asynchronous offline messages. Loki messenger implements the Signal protocol for message encryption. Our Client interface is a fork of [Signal Messenger](https://signal.org/). All communication that passes through Loki messenger is routed through [Lokinet](https://github.com/loki-project/loki-network). Session allows for truly decentralized, end to end, and private encrypted chats. Session is built to handle both online and fully Asynchronous offline messages. Session implements the Signal protocol for message encryption. Our Client interface is a fork of [Signal Messenger](https://signal.org/). All communication that passes through Session is routed through [Lokinet](https://github.com/loki-project/loki-network).
## Summary ## Summary
Loki messenger integrates directly with Loki [Service Nodes](https://lokidocs.com/ServiceNodes/SNOverview/), which are a set of distributed, decentralized and Sybil resistant nodes. Service Nodes act as both federated servers which store messages offline, and a set of nodes which allow for onion routing functionality obfuscating users IP Addresses. For a full understanding of how Loki messenger works, read the [Loki whitepaper](https://loki.network/whitepaper). Session integrates directly with Loki [Service Nodes](https://lokidocs.com/ServiceNodes/SNOverview/), which are a set of distributed, decentralized and Sybil resistant nodes. Service Nodes act as both federated servers which store messages offline, and a set of nodes which allow for onion routing functionality obfuscating users IP Addresses. For a full understanding of how Session works, read the [Loki whitepaper](https://loki.network/whitepaper).
**Online Messages** **Online Messages**

@ -2226,7 +2226,7 @@
}, },
"setAccountPasswordDescription": { "setAccountPasswordDescription": {
"message": "message":
"Require password to unlock Sessions screen. You can still receive message notifications while Screen Lock is enabled. Loki Messengers notification settings allow you to customize information that is displayed", "Require password to unlock Sessions screen. You can still receive message notifications while Screen Lock is enabled. Sessions notification settings allow you to customize information that is displayed",
"description": "Description for set account password setting view" "description": "Description for set account password setting view"
}, },
"changeAccountPasswordTitle": { "changeAccountPasswordTitle": {

@ -1067,6 +1067,7 @@
const sslServerURL = `https://${rawserverURL}`; const sslServerURL = `https://${rawserverURL}`;
const conversationId = `publicChat:${channelId}@${rawserverURL}`; const conversationId = `publicChat:${channelId}@${rawserverURL}`;
// quickly peak to make sure we don't already have it
const conversationExists = window.ConversationController.get( const conversationExists = window.ConversationController.get(
conversationId conversationId
); );
@ -1077,9 +1078,11 @@
}); });
} }
// get server
const serverAPI = await window.lokiPublicChatAPI.findOrCreateServer( const serverAPI = await window.lokiPublicChatAPI.findOrCreateServer(
sslServerURL sslServerURL
); );
// SSL certificate failure or offline
if (!serverAPI) { if (!serverAPI) {
// Url incorrect or server not compatible // Url incorrect or server not compatible
return new Promise((_resolve, reject) => { return new Promise((_resolve, reject) => {
@ -1087,16 +1090,20 @@
}); });
} }
// create conversation
const conversation = await window.ConversationController.getOrCreateAndWait( const conversation = await window.ConversationController.getOrCreateAndWait(
conversationId, conversationId,
'group' 'group'
); );
// convert conversation to a public one
await conversation.setPublicSource(sslServerURL, channelId); await conversation.setPublicSource(sslServerURL, channelId);
// set friend and appropriate SYNC messages for multidevice
await conversation.setFriendRequestStatus( await conversation.setFriendRequestStatus(
window.friends.friendRequestStatusEnum.friends window.friends.friendRequestStatusEnum.friends
); );
// and finally activate it
conversation.getPublicSendData(); // may want "await" if you want to use the API conversation.getPublicSendData(); // may want "await" if you want to use the API
return conversation; return conversation;
@ -1966,7 +1973,10 @@
} }
const isDuplicate = await isMessageDuplicate(message); const isDuplicate = await isMessageDuplicate(message);
if (isDuplicate) { if (isDuplicate) {
// RSS expects duplciates, so squelch log
if (!descriptorId.match(/^rss:/)) {
window.log.warn('Received duplicate message', message.idForLogging()); window.log.warn('Received duplicate message', message.idForLogging());
}
return event.confirm(); return event.confirm();
} }

@ -14,12 +14,19 @@
LokiFileServerAPI.secureRpcPubKey LokiFileServerAPI.secureRpcPubKey
); );
let nextWaitSeconds = 1;
const checkForUpgrades = async () => { const checkForUpgrades = async () => {
const response = await window.tokenlessFileServerAdnAPI.serverRequest( const result = await window.tokenlessFileServerAdnAPI.serverRequest(
'loki/v1/version/client/desktop' 'loki/v1/version/client/desktop'
); );
if (response && response.response) { if (
const latestVer = semver.clean(response.response.data[0][0]); result &&
result.response &&
result.response.data &&
result.response.data.length &&
result.response.data[0].length
) {
const latestVer = semver.clean(result.response.data[0][0]);
if (semver.valid(latestVer)) { if (semver.valid(latestVer)) {
const ourVersion = window.getVersion(); const ourVersion = window.getVersion();
if (latestVer === ourVersion) { if (latestVer === ourVersion) {
@ -36,10 +43,11 @@
} }
} else { } else {
// give it a minute // give it a minute
log.warn('Could not check to see if newer version is available'); log.warn('Could not check to see if newer version is available', result);
nextWaitSeconds = 60;
setTimeout(async () => { setTimeout(async () => {
await checkForUpgrades(); await checkForUpgrades();
}, 60 * 1000); // wait a minute }, nextWaitSeconds * 1000); // wait a minute
} }
// no message logged means serverRequest never returned... // no message logged means serverRequest never returned...
}; };
@ -56,22 +64,33 @@
if (expiredVersion !== null) { if (expiredVersion !== null) {
return res(expiredVersion); return res(expiredVersion);
} }
log.info('Delaying sending checks for 1s, no version yet'); log.info(
setTimeout(waitForVersion, 1000); 'Delaying sending checks for',
nextWaitSeconds,
's, no version yet'
);
setTimeout(waitForVersion, nextWaitSeconds * 1000);
return true; return true;
} }
waitForVersion(); waitForVersion();
return true; return true;
}; };
// just get current status
window.extension.expiredStatus = () => expiredVersion;
// actually wait until we know for sure
window.extension.expiredPromise = () => new Promise(resolveWhenReady); window.extension.expiredPromise = () => new Promise(resolveWhenReady);
window.extension.expired = cb => { window.extension.expired = cb => {
if (expiredVersion === null) { if (expiredVersion === null) {
// just give it another second // just give it another second
log.info('Delaying expire banner determination for 1s'); log.info(
'Delaying expire banner determination for',
nextWaitSeconds,
's'
);
setTimeout(() => { setTimeout(() => {
window.extension.expired(cb); window.extension.expired(cb);
}, 1000); }, nextWaitSeconds * 1000);
return; return;
} }
// yes we know // yes we know

@ -561,6 +561,7 @@
type: this.isPrivate() ? 'direct' : 'group', type: this.isPrivate() ? 'direct' : 'group',
isMe: this.isMe(), isMe: this.isMe(),
isPublic: this.isPublic(), isPublic: this.isPublic(),
isRss: this.isRss(),
isClosable: this.isClosable(), isClosable: this.isClosable(),
isTyping: typingKeys.length > 0, isTyping: typingKeys.length > 0,
lastUpdated: this.get('timestamp'), lastUpdated: this.get('timestamp'),
@ -2798,11 +2799,19 @@
this.messageCollection.reset([]); this.messageCollection.reset([]);
// let's try to keep the RSS conversation open just empty...
if (this.isRss()) {
this.set({
lastMessage: null,
});
} else {
// this will remove the conversation from conversation lists...
this.set({ this.set({
lastMessage: null, lastMessage: null,
timestamp: null, timestamp: null,
active_at: null, active_at: null,
}); });
}
// Reset our friend status if we're not friends // Reset our friend status if we're not friends
if (!this.isFriend()) { if (!this.isFriend()) {

@ -185,6 +185,15 @@ class LokiAppDotNetServerAPI {
} }
this.token = token; this.token = token;
// if no token to verify, just bail now
if (!token) {
//
if (!forceRefresh) {
token = await this.getOrRefreshServerToken(true);
}
return token;
}
// verify token info // verify token info
const tokenRes = await this.serverRequest('token'); const tokenRes = await this.serverRequest('token');
// if no problems and we have data // if no problems and we have data
@ -270,7 +279,31 @@ class LokiAppDotNetServerAPI {
res = await this.proxyFetch(url); res = await this.proxyFetch(url);
} catch (e) { } catch (e) {
log.error('requestToken request failed', e); // should we retry here?
// no, this is the low level function
// not really an error, from a client's pov, network servers can fail...
if (e.code === 'ECONNREFUSED') {
// down
log.warn(
'requestToken request can not connect',
this.baseServerUrl,
e.message
);
} else if (e.code === 'ECONNRESET') {
// got disconnected
log.warn(
'requestToken request lost connection',
this.baseServerUrl,
e.message
);
} else {
log.error(
'requestToken request failed',
this.baseServerUrl,
e.code,
e.message
);
}
return null; return null;
} }
if (!res.ok) { if (!res.ok) {
@ -302,14 +335,17 @@ class LokiAppDotNetServerAPI {
); );
return res.ok; return res.ok;
} catch (e) { } catch (e) {
log.error('submitToken proxyFetch failure', e.code, e.message);
return false; return false;
} }
} }
async proxyFetch(urlObj, fetchOptions) { async proxyFetch(urlObj, fetchOptions = { method: 'GET' }) {
if ( if (
window.lokiFeatureFlags.useSnodeProxy && window.lokiFeatureFlags.useSnodeProxy &&
(this.baseServerUrl === 'https://file-dev.lokinet.org' || (this.baseServerUrl === 'https://file-dev.lokinet.org' ||
this.baseServerUrl === 'https://file.lokinet.org' ||
this.baseServerUrl === 'https://file-dev.getsession.org' ||
this.baseServerUrl === 'https://file.getsession.org') this.baseServerUrl === 'https://file.getsession.org')
) { ) {
const finalOptions = { ...fetchOptions }; const finalOptions = { ...fetchOptions };
@ -408,7 +444,21 @@ class LokiAppDotNetServerAPI {
const result = await nodeFetch(url, firstHopOptions); const result = await nodeFetch(url, firstHopOptions);
const txtResponse = await result.text(); const txtResponse = await result.text();
let response = JSON.parse(txtResponse); if (txtResponse === 'Service node is not ready: not in any swarm; \n') {
// mark snode bad
log.warn('Marking random snode bad', randSnode);
lokiSnodeAPI.markRandomNodeUnreachable(randSnode);
// retry (hopefully with new snode)
// FIXME: max number of retries...
return this._sendToProxy(endpoint, fetchOptions);
}
let response = {};
try {
response = JSON.parse(txtResponse);
} catch (e) {
log.warn(`_sendToProxy Could not parse outer JSON [${txtResponse}]`);
}
if (response.meta && response.meta.code === 200) { if (response.meta && response.meta.code === 200) {
// convert base64 in response to binary // convert base64 in response to binary
@ -423,9 +473,17 @@ class LokiAppDotNetServerAPI {
const textDecoder = new TextDecoder(); const textDecoder = new TextDecoder();
const json = textDecoder.decode(decrypted); const json = textDecoder.decode(decrypted);
// replace response // replace response
try {
response = JSON.parse(json); response = JSON.parse(json);
} catch (e) {
log.warn(`_sendToProxy Could not parse inner JSON [${json}]`);
}
} else { } else {
log.warn('file server secure_rpc gave an non-200 response'); log.warn(
'file server secure_rpc gave an non-200 response: ',
response,
` txtResponse[${txtResponse}]`
);
} }
return { result, txtResponse, response }; return { result, txtResponse, response };
} }
@ -469,7 +527,7 @@ class LokiAppDotNetServerAPI {
fetchOptions.agent = snodeHttpsAgent; fetchOptions.agent = snodeHttpsAgent;
} }
} catch (e) { } catch (e) {
log.info('serverRequest set up error:', JSON.stringify(e)); log.info('serverRequest set up error:', e.code, e.message);
return { return {
err: e, err: e,
}; };
@ -483,6 +541,8 @@ class LokiAppDotNetServerAPI {
if ( if (
window.lokiFeatureFlags.useSnodeProxy && window.lokiFeatureFlags.useSnodeProxy &&
(this.baseServerUrl === 'https://file-dev.lokinet.org' || (this.baseServerUrl === 'https://file-dev.lokinet.org' ||
this.baseServerUrl === 'https://file.lokinet.org' ||
this.baseServerUrl === 'https://file-dev.getsession.org' ||
this.baseServerUrl === 'https://file.getsession.org') this.baseServerUrl === 'https://file.getsession.org')
) { ) {
mode = '_sendToProxy'; mode = '_sendToProxy';
@ -902,7 +962,11 @@ class LokiPublicChannelAPI {
try { try {
await this.pollOnceForModerators(); await this.pollOnceForModerators();
} catch (e) { } catch (e) {
log.warn(`Error while polling for public chat moderators: ${e}`); log.warn(
'Error while polling for public chat moderators:',
e.code,
e.message
);
} }
if (this.running) { if (this.running) {
this.timers.moderator = setTimeout(() => { this.timers.moderator = setTimeout(() => {
@ -1052,7 +1116,11 @@ class LokiPublicChannelAPI {
try { try {
await this.pollForChannelOnce(); await this.pollForChannelOnce();
} catch (e) { } catch (e) {
log.warn(`Error while polling for public chat room details: ${e}`); log.warn(
'Error while polling for public chat room details',
e.code,
e.message
);
} }
if (this.running) { if (this.running) {
this.timers.channel = setTimeout(() => { this.timers.channel = setTimeout(() => {
@ -1103,7 +1171,11 @@ class LokiPublicChannelAPI {
try { try {
await this.pollOnceForDeletions(); await this.pollOnceForDeletions();
} catch (e) { } catch (e) {
log.warn(`Error while polling for public chat deletions: ${e}`); log.warn(
'Error while polling for public chat deletions:',
e.code,
e.message
);
} }
if (this.running) { if (this.running) {
this.timers.delete = setTimeout(() => { this.timers.delete = setTimeout(() => {
@ -1278,7 +1350,11 @@ class LokiPublicChannelAPI {
try { try {
await this.pollOnceForMessages(); await this.pollOnceForMessages();
} catch (e) { } catch (e) {
log.warn(`Error while polling for public chat messages: ${e}`); log.warn(
'Error while polling for public chat messages:',
e.code,
e.message
);
} }
if (this.running) { if (this.running) {
this.timers.message = setTimeout(() => { this.timers.message = setTimeout(() => {

@ -288,7 +288,7 @@ class LokiMessageAPI {
// Execute callback even with empty array to signal online status // Execute callback even with empty array to signal online status
callback(messages); callback(messages);
} catch (e) { } catch (e) {
log.warn('Loki retrieve messages:', e); log.warn('Loki retrieve messages:', e.code, e.message);
if (e instanceof textsecure.WrongSwarmError) { if (e instanceof textsecure.WrongSwarmError) {
const { newSwarm } = e; const { newSwarm } = e;
await lokiSnodeAPI.updateSwarmNodes(this.ourKey, newSwarm); await lokiSnodeAPI.updateSwarmNodes(this.ourKey, newSwarm);
@ -352,7 +352,6 @@ class LokiMessageAPI {
const lastHash = await window.Signal.Data.getLastHashBySnode( const lastHash = await window.Signal.Data.getLastHashBySnode(
nodes[i].address nodes[i].address
); );
this.ourSwarmNodes[nodes[i].address] = { this.ourSwarmNodes[nodes[i].address] = {
...nodes[i], ...nodes[i],
lastHash, lastHash,

@ -1,4 +1,4 @@
/* global log, window */ /* global log, window, process */
const EventEmitter = require('events'); const EventEmitter = require('events');
const nodeFetch = require('node-fetch'); const nodeFetch = require('node-fetch');
const LokiAppDotNetAPI = require('./loki_app_dot_net_api'); const LokiAppDotNetAPI = require('./loki_app_dot_net_api');
@ -17,6 +17,26 @@ class LokiPublicChatFactoryAPI extends EventEmitter {
await Promise.all(this.servers.map(server => server.close())); await Promise.all(this.servers.map(server => server.close()));
} }
static async validServer(serverUrl) {
// test to make sure it's online (and maybe has a valid SSL cert)
try {
// allow .loki (may only need an agent but not sure
// until we have a .loki to test with)
process.env.NODE_TLS_REJECT_UNAUTHORIZED = serverUrl.match(/\.loki\//)
? 0
: 1;
await nodeFetch(serverUrl);
process.env.NODE_TLS_REJECT_UNAUTHORIZED = 1;
// const txt = await res.text();
} catch (e) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = 1;
log.warn(`failing to created ${serverUrl}`, e.code, e.message);
// bail out if not valid enough
return false;
}
return true;
}
// server getter/factory // server getter/factory
async findOrCreateServer(serverUrl) { async findOrCreateServer(serverUrl) {
let thisServer = this.servers.find( let thisServer = this.servers.find(
@ -24,7 +44,14 @@ class LokiPublicChatFactoryAPI extends EventEmitter {
); );
if (!thisServer) { if (!thisServer) {
log.info(`LokiAppDotNetAPI creating ${serverUrl}`); log.info(`LokiAppDotNetAPI creating ${serverUrl}`);
if (!await this.constructor.validServer(serverUrl)) {
return null;
}
// after verification then we can start up all the pollers
thisServer = new LokiAppDotNetAPI(this.ourKey, serverUrl); thisServer = new LokiAppDotNetAPI(this.ourKey, serverUrl);
const gotToken = await thisServer.getOrRefreshServerToken(); const gotToken = await thisServer.getOrRefreshServerToken();
if (!gotToken) { if (!gotToken) {
log.warn(`Invalid server ${serverUrl}`); log.warn(`Invalid server ${serverUrl}`);

@ -13,16 +13,18 @@ const LOKI_EPHEMKEY_HEADER = 'X-Loki-EphemKey';
const endpointBase = '/storage_rpc/v1'; const endpointBase = '/storage_rpc/v1';
const decryptResponse = async (response, address) => { const decryptResponse = async (response, address) => {
let plaintext = false;
try { try {
const ciphertext = await response.text(); const ciphertext = await response.text();
const plaintext = await libloki.crypto.snodeCipher.decrypt( plaintext = await libloki.crypto.snodeCipher.decrypt(address, ciphertext);
address,
ciphertext
);
const result = plaintext === '' ? {} : JSON.parse(plaintext); const result = plaintext === '' ? {} : JSON.parse(plaintext);
return result; return result;
} catch (e) { } catch (e) {
log.warn(`Could not decrypt response from ${address}`, e); log.warn(
`Could not decrypt response [${plaintext}] from [${address}],`,
e.code,
e.message
);
} }
return {}; return {};
}; };
@ -33,12 +35,6 @@ const sendToProxy = async (options = {}, targetNode) => {
const url = `https://${randSnode.ip}:${randSnode.port}/proxy`; const url = `https://${randSnode.ip}:${randSnode.port}/proxy`;
log.info(
`Proxy snode request to ${targetNode.pubkey_ed25519} via ${
randSnode.pubkey_ed25519
}`
);
const snPubkeyHex = StringView.hexToArrayBuffer(targetNode.pubkey_x25519); const snPubkeyHex = StringView.hexToArrayBuffer(targetNode.pubkey_x25519);
const myKeys = window.libloki.crypto.snodeCipher._ephemeralKeyPair; const myKeys = window.libloki.crypto.snodeCipher._ephemeralKeyPair;
@ -86,11 +82,21 @@ const sendToProxy = async (options = {}, targetNode) => {
const textDecoder = new TextDecoder(); const textDecoder = new TextDecoder();
const plaintext = textDecoder.decode(plaintextBuffer); const plaintext = textDecoder.decode(plaintextBuffer);
try {
const jsonRes = JSON.parse(plaintext); const jsonRes = JSON.parse(plaintext);
// emulate nodeFetch response...
jsonRes.json = () => JSON.parse(jsonRes.body); jsonRes.json = () => JSON.parse(jsonRes.body);
return jsonRes; return jsonRes;
} catch (e) {
log.error(
'lokiRpc sendToProxy error',
e.code,
e.message,
'json',
plaintext
);
}
return false;
}; };
// A small wrapper around node-fetch which deserializes response // A small wrapper around node-fetch which deserializes response

@ -9,8 +9,8 @@ class LokiSnodeAPI {
if (!is.string(serverUrl)) { if (!is.string(serverUrl)) {
throw new Error('WebAPI.initialize: Invalid server url'); throw new Error('WebAPI.initialize: Invalid server url');
} }
this.serverUrl = serverUrl; this.serverUrl = serverUrl; // random.snode
this.localUrl = localUrl; this.localUrl = localUrl; // localhost.loki
this.randomSnodePool = []; this.randomSnodePool = [];
this.swarmsPendingReplenish = {}; this.swarmsPendingReplenish = {};
} }
@ -63,7 +63,7 @@ class LokiSnodeAPI {
pubkey_ed25519: snode.pubkey_ed25519, pubkey_ed25519: snode.pubkey_ed25519,
})); }));
} catch (e) { } catch (e) {
log.warn('initialiseRandomPool error', JSON.stringify(e)); log.warn('initialiseRandomPool error', e.code, e.message);
if (seedNodes.length === 0) { if (seedNodes.length === 0) {
throw new window.textsecure.SeedNodeError( throw new window.textsecure.SeedNodeError(
'Failed to contact seed node' 'Failed to contact seed node'
@ -73,6 +73,7 @@ class LokiSnodeAPI {
} }
} }
// nodeUrl is like 9hrje1bymy7hu6nmtjme9idyu3rm8gr3mkstakjyuw1997t7w4ny.snode
async unreachableNode(pubKey, nodeUrl) { async unreachableNode(pubKey, nodeUrl) {
const conversation = ConversationController.get(pubKey); const conversation = ConversationController.get(pubKey);
const swarmNodes = [...conversation.get('swarmNodes')]; const swarmNodes = [...conversation.get('swarmNodes')];
@ -82,6 +83,13 @@ class LokiSnodeAPI {
await conversation.updateSwarmNodes(filteredNodes); await conversation.updateSwarmNodes(filteredNodes);
} }
markRandomNodeUnreachable(snode) {
this.randomSnodePool = _.without(
this.randomSnodePool,
_.find(this.randomSnodePool, { ip: snode.ip, port: snode.port })
);
}
async updateLastHash(snode, hash, expiresAt) { async updateLastHash(snode, hash, expiresAt) {
await window.Signal.Data.updateLastHash({ snode, hash, expiresAt }); await window.Signal.Data.updateLastHash({ snode, hash, expiresAt });
} }
@ -151,12 +159,8 @@ class LokiSnodeAPI {
const snodes = result.snodes.filter(tSnode => tSnode.ip !== '0.0.0.0'); const snodes = result.snodes.filter(tSnode => tSnode.ip !== '0.0.0.0');
return snodes; return snodes;
} catch (e) { } catch (e) {
log.error('getSwarmNodes', JSON.stringify(e)); log.error('getSwarmNodes error', e.code, e.message);
// this.markRandomNodeUnreachable(snode);
this.randomSnodePool = _.without(
this.randomSnodePool,
_.find(this.randomSnodePool, { ip: snode.ip })
);
return this.getSwarmNodes(pubKey); return this.getSwarmNodes(pubKey);
} }
} }

@ -1,4 +1,4 @@
/* global Whisper, i18n, ConversationController, friends */ /* global Whisper, i18n, log */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function() { (function() {
@ -24,31 +24,13 @@
'click .cancel': 'close', 'click .cancel': 'close',
}, },
async attemptConnection(serverUrl, channelId) { async attemptConnection(serverUrl, channelId) {
const rawServerUrl = serverUrl let conversation = null;
.replace(/^https?:\/\//i, '') try {
.replace(/[/\\]+$/i, ''); conversation = await window.attemptConnection(serverUrl, channelId);
const sslServerUrl = `https://${rawServerUrl}`; } catch (e) {
const conversationId = `publicChat:${channelId}@${rawServerUrl}`; log.error('can not connect', e.message, e.code);
return this.resolveWith({ errorCode: e.message });
const conversationExists = ConversationController.get(conversationId);
if (conversationExists) {
// We are already a member of this public chat
return this.resolveWith({ errorCode: i18n('publicChatExists') });
} }
// create conversation
const conversation = await ConversationController.getOrCreateAndWait(
conversationId,
'group'
);
// convert conversation to a public one
await conversation.setPublicSource(sslServerUrl, channelId);
// set friend and appropriate SYNC messages for multidevice
await conversation.setFriendRequestStatus(
friends.friendRequestStatusEnum.friends
);
// and finally activate it
conversation.getPublicSendData(); // may want "await" if you want to use the API
return this.resolveWith({ conversation }); return this.resolveWith({ conversation });
}, },
resolveWith(result) { resolveWith(result) {

@ -1860,8 +1860,8 @@
message = window.Signal.Emoji.replaceColons(message).trim(); message = window.Signal.Emoji.replaceColons(message).trim();
const toastOptions = { type: 'info' }; const toastOptions = { type: 'info' };
const expiredVersion = await extension.expiredPromise(); // let it pass if we're still trying to read it or it's false...
if (expiredVersion) { if (extension.expiredStatus() === true) {
toastOptions.title = i18n('expiredWarning'); toastOptions.title = i18n('expiredWarning');
toastOptions.id = 'expiredWarning'; toastOptions.id = 'expiredWarning';
} }

@ -23,6 +23,7 @@ export type PropsData = {
avatarPath?: string; avatarPath?: string;
isMe: boolean; isMe: boolean;
isPublic?: boolean; isPublic?: boolean;
isRss?: boolean;
isClosable?: boolean; isClosable?: boolean;
lastUpdated: number; lastUpdated: number;
@ -176,6 +177,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
isBlocked, isBlocked,
isMe, isMe,
isClosable, isClosable,
isRss,
isPublic, isPublic,
hasNickname, hasNickname,
onDeleteContact, onDeleteContact,
@ -192,18 +194,18 @@ export class ConversationListItem extends React.PureComponent<Props> {
return ( return (
<ContextMenu id={triggerId}> <ContextMenu id={triggerId}>
{!isPublic && !isMe ? ( {!isPublic && !isRss && !isMe ? (
<MenuItem onClick={blockHandler}>{blockTitle}</MenuItem> <MenuItem onClick={blockHandler}>{blockTitle}</MenuItem>
) : null} ) : null}
{!isPublic && !isMe ? ( {!isPublic && !isRss && !isMe ? (
<MenuItem onClick={onChangeNickname}> <MenuItem onClick={onChangeNickname}>
{i18n('changeNickname')} {i18n('changeNickname')}
</MenuItem> </MenuItem>
) : null} ) : null}
{!isPublic && !isMe && hasNickname ? ( {!isPublic && !isRss && !isMe && hasNickname ? (
<MenuItem onClick={onClearNickname}>{i18n('clearNickname')}</MenuItem> <MenuItem onClick={onClearNickname}>{i18n('clearNickname')}</MenuItem>
) : null} ) : null}
{!isPublic ? ( {!isPublic && !isRss ? (
<MenuItem onClick={onCopyPublicKey}>{i18n('copyPublicKey')}</MenuItem> <MenuItem onClick={onCopyPublicKey}>{i18n('copyPublicKey')}</MenuItem>
) : null} ) : null}
<MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem> <MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem>

@ -319,7 +319,9 @@ export class ConversationHeader extends React.Component<Props> {
return ( return (
<ContextMenu id={triggerId}> <ContextMenu id={triggerId}>
{this.renderPublicMenuItems()} {this.renderPublicMenuItems()}
{!isRss ? (
<MenuItem onClick={onCopyPublicKey}>{copyIdLabel}</MenuItem> <MenuItem onClick={onCopyPublicKey}>{copyIdLabel}</MenuItem>
) : null}
<MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem> <MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem>
{isPrivateGroup || amMod ? ( {isPrivateGroup || amMod ? (
<MenuItem onClick={onUpdateGroup}>{i18n('updateGroup')}</MenuItem> <MenuItem onClick={onUpdateGroup}>{i18n('updateGroup')}</MenuItem>
@ -477,7 +479,8 @@ export class ConversationHeader extends React.Component<Props> {
<MenuItem onClick={onResetSession}>{i18n('resetSession')}</MenuItem> <MenuItem onClick={onResetSession}>{i18n('resetSession')}</MenuItem>
); );
const blockHandlerMenuItem = !isMe && const blockHandlerMenuItem = !isMe &&
!isGroup && <MenuItem onClick={blockHandler}>{blockTitle}</MenuItem>; !isGroup &&
!isRss && <MenuItem onClick={blockHandler}>{blockTitle}</MenuItem>;
const changeNicknameMenuItem = !isMe && const changeNicknameMenuItem = !isMe &&
!isGroup && ( !isGroup && (
<MenuItem onClick={onChangeNickname}>{i18n('changeNickname')}</MenuItem> <MenuItem onClick={onChangeNickname}>{i18n('changeNickname')}</MenuItem>

Loading…
Cancel
Save