From 8d77d6fd79d74cd8e02b27e9c9425c1015037f7e Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Fri, 30 Aug 2019 15:07:45 +1000 Subject: [PATCH] All the API updates, enable mod status retrieval and allow for message deletion --- js/background.js | 3 +- js/modules/loki_public_chat_api.js | 222 +++++++++++++++++++++-------- 2 files changed, 163 insertions(+), 62 deletions(-) diff --git a/js/background.js b/js/background.js index 705b936ea..799d8f8f3 100644 --- a/js/background.js +++ b/js/background.js @@ -224,11 +224,12 @@ ); publicConversations.forEach(conversation => { const settings = conversation.getPublicSource(); - window.lokiPublicChatAPI.registerChannel( + const channel = window.lokiPublicChatAPI.findOrCreateChannel( settings.server, settings.channelId, conversation.id ); + channel.refreshModStatus(); }); window.lokiP2pAPI = new window.LokiP2pAPI(ourKey); window.lokiP2pAPI.on('pingContact', pubKey => { diff --git a/js/modules/loki_public_chat_api.js b/js/modules/loki_public_chat_api.js index 89d0fea7c..32bec06d1 100644 --- a/js/modules/loki_public_chat_api.js +++ b/js/modules/loki_public_chat_api.js @@ -1,43 +1,46 @@ -/* global log, textsecure, libloki, Signal */ +/* global log, textsecure, libloki, Signal, Whisper, Headers, ConversationController */ const EventEmitter = require('events'); const nodeFetch = require('node-fetch'); const { URL, URLSearchParams } = require('url'); -const GROUPCHAT_POLL_EVERY = 1000; // 1 second +// Can't be less than 1200 if we have unauth'd requests +const GROUPCHAT_POLL_EVERY = 1500; // 1.5s +const DELETION_POLL_EVERY = 5000; // 1 second // singleton to relay events to libtextsecure/message_receiver class LokiPublicChatAPI extends EventEmitter { constructor(ourKey) { super(); this.ourKey = ourKey; - this.lastGot = {}; this.servers = []; } - findOrCreateServer(hostport) { - log.info(`LokiPublicChatAPI looking for ${hostport}`); - let thisServer = this.servers.find(server => server.server === hostport); + findOrCreateServer(serverUrl) { + let thisServer = this.servers.find( + server => server.baseServerUrl === serverUrl + ); if (!thisServer) { - thisServer = new LokiPublicServerAPI(this, hostport); + log.info(`LokiPublicChatAPI creating ${serverUrl}`); + thisServer = new LokiPublicServerAPI(this, serverUrl); this.servers.push(thisServer); } return thisServer; } - registerChannel(hostport, channelId, conversationId) { - const server = this.findOrCreateServer(hostport); - server.findOrCreateChannel(channelId, conversationId); + findOrCreateChannel(serverUrl, channelId, conversationId) { + const server = this.findOrCreateServer(serverUrl); + return server.findOrCreateChannel(channelId, conversationId); } - unregisterChannel(hostport, channelId) { + unregisterChannel(serverUrl, channelId) { let thisServer; let i = 0; for (; i < this.servers.length; i += 1) { - if (this.servers[i].server === hostport) { + if (this.servers[i].server === serverUrl) { thisServer = this.servers[i]; break; } } if (!thisServer) { - log.warn(`Tried to unregister from nonexistent server ${hostport}`); + log.warn(`Tried to unregister from nonexistent server ${serverUrl}`); return; } thisServer.unregisterChannel(channelId); @@ -57,6 +60,7 @@ class LokiPublicServerAPI { channel => channel.channelId === channelId ); if (!thisChannel) { + log.info(`LokiPublicChatAPI creating channel ${conversationId}`); thisChannel = new LokiPublicChannelAPI(this, channelId, conversationId); this.channels.push(thisChannel); } @@ -79,6 +83,9 @@ class LokiPublicServerAPI { } async getOrRefreshServerToken() { + if (this.token) { + return this.token; + } let token = await Signal.Data.getPublicServerTokenByServerUrl( this.baseServerUrl ); @@ -91,6 +98,7 @@ class LokiPublicServerAPI { }); } } + this.token = token; return token; } @@ -164,9 +172,7 @@ class LokiPublicChannelAPI { constructor(serverAPI, channelId, conversationId) { this.serverAPI = serverAPI; this.channelId = channelId; - this.baseChannelUrl = `${serverAPI.baseServerUrl}/channels/${ - this.channelId - }`; + this.baseChannelUrl = `channels/${this.channelId}`; this.groupName = 'unknown'; this.conversationId = conversationId; this.lastGot = 0; @@ -174,85 +180,169 @@ class LokiPublicChannelAPI { log.info(`registered LokiPublicChannel ${channelId}`); // start polling this.pollForMessages(); + this.deleteLastId = 1; + this.pollForDeletions(); + } + + async refreshModStatus() { + const url = new URL(`${this.serverAPI.baseServerUrl}/loki/v1/user_info`); + const token = await this.serverAPI.getOrRefreshServerToken(); + let modStatus = false; + try { + const result = await nodeFetch(url, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + }); + if (result.ok) { + const response = await result.json(); + if (response.data.moderator_status) { + modStatus = response.data.moderator_status; + } + } + } catch (e) { + modStatus = false; + } + const conversation = ConversationController.get(this.conversationId); + await conversation.setModStatus(modStatus); + } + + async deleteMessage(messageServerId) { + // TODO: Allow deletion of your own messages without moderator status + const url = new URL( + `${ + this.serverAPI.baseServerUrl + }/loki/v1/moderation/message/${messageServerId}` + ); + const token = await this.serverAPI.getOrRefreshServerToken(); + try { + const result = await nodeFetch(url, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + }); + // 200 for successful delete + // 404 for trying to delete a message that doesn't exist + // 410 for successful moderator delete + const validResults = [404, 410]; + if (result.ok || validResults.includes(result.status)) { + return true; + } + } catch (e) { + log.warn( + `Failed to delete message from public server with ID ${messageServerId}` + ); + } + return false; } getEndpoint() { - const endpoint = `${this.serverAPI.baseServerUrl}/channels/${ - this.channelId + const endpoint = `${this.serverAPI.baseServerUrl}/${ + this.baseChannelUrl }/messages`; return endpoint; } - async pollForChannel(source, endpoint) { - // groupName will be loaded from server - const url = new URL(this.baseChannelUrl); + async serverRequest(endpoint, params, method) { + const url = new URL(`${this.serverAPI.baseServerUrl}/${endpoint}`); + url.search = new URLSearchParams(params); let res; - let success = true; + const token = await this.serverAPI.getOrRefreshServerToken(); + if (!token) { + log.error('NO TOKEN'); + return { + err: 'noToken', + }; + } try { - res = await nodeFetch(url); + // eslint-disable-next-line no-await-in-loop + const options = { + headers: new Headers({ + Authorization: `Bearer ${token}`, + }), + }; + if (method) { + options.method = method; + } + res = await nodeFetch(url, options || undefined); } catch (e) { - success = false; + log.info(`e ${e}`); + return { + err: e, + }; } - + // eslint-disable-next-line no-await-in-loop const response = await res.json(); if (response.meta.code !== 200) { - success = false; + return { + err: 'statusCode', + response, + }; } - // update this.groupId - return endpoint || success; + return { + response, + }; } async pollForDeletions() { // read all messages from 0 to current // delete local copies if server state has changed to delete // run every minute - const url = new URL(this.baseChannelUrl); - let res; - let success = true; - try { - res = await nodeFetch(url); - } catch (e) { - success = false; - } + const pollAgain = () => { + setTimeout(() => { + this.pollForDeletions(); + }, DELETION_POLL_EVERY); + }; - const response = await res.json(); - if (response.meta.code !== 200) { - success = false; + const params = { + count: 200, + }; + + // full scan + let more = true; + while (more) { + params.since_id = this.deleteLastId; + const res = await this.serverRequest( + `loki/v1/channel/${this.channelId}/deletes`, + params + ); + + // eslint-disable-next-line no-loop-func + res.response.data.reverse().forEach(deleteEntry => { + Whisper.events.trigger('deleteLocalPublicMessage', { + messageServerId: deleteEntry.message_id, + conversationId: this.conversationId, + }); + }); + if (res.response.data.length < 200) { + break; + } + this.deleteLastId = res.response.meta.max_id; + ({ more } = res.response); } - return success; + pollAgain(); } async pollForMessages() { - const url = new URL(`${this.baseChannelUrl}/messages`); const params = { include_annotations: 1, count: -20, + include_deleted: false, }; if (this.lastGot) { params.since_id = this.lastGot; } - url.search = new URLSearchParams(params); - - let res; - let success = true; - try { - res = await nodeFetch(url); - } catch (e) { - success = false; - } - - const response = await res.json(); - if (this.stopPolling) { - // Stop after latest await possible - return; - } - if (response.meta.code !== 200) { - success = false; - } + const res = await this.serverRequest( + `${this.baseChannelUrl}/messages`, + params + ); - if (success) { + if (!res.err && res.response) { let receivedAt = new Date().getTime(); - response.data.reverse().forEach(adnMessage => { + res.response.data.reverse().forEach(adnMessage => { let timestamp = new Date(adnMessage.created_at).getTime(); let from = adnMessage.user.username; let source; @@ -264,6 +354,16 @@ class LokiPublicChannelAPI { ({ from, timestamp, source } = noteValue); } + if ( + !from || + !timestamp || + !source || + !adnMessage.id || + !adnMessage.text + ) { + return; // Invalid message + } + const messageData = { serverId: adnMessage.id, friendRequest: false,