Merge branch 'deletion-final' of https://github.com/BeaudanBrown/loki-messenger into public-delete

pull/455/head
Ryan Tharp 6 years ago
commit 95cca859e9

@ -948,6 +948,10 @@
"delete": { "delete": {
"message": "Delete" "message": "Delete"
}, },
"deletePublicWarning": {
"message":
"Are you sure? Clicking 'delete' will permanently remove this message for everyone in this channel."
},
"deleteWarning": { "deleteWarning": {
"message": "message":
"Are you sure? Clicking 'delete' will permanently remove this message from this device only." "Are you sure? Clicking 'delete' will permanently remove this message from this device only."

@ -224,11 +224,12 @@
); );
publicConversations.forEach(conversation => { publicConversations.forEach(conversation => {
const settings = conversation.getPublicSource(); const settings = conversation.getPublicSource();
window.lokiPublicChatAPI.registerChannel( const channel = window.lokiPublicChatAPI.findOrCreateChannel(
settings.server, settings.server,
settings.channelId, settings.channelId,
conversation.id conversation.id
); );
channel.refreshModStatus();
}); });
window.lokiP2pAPI = new window.LokiP2pAPI(ourKey); window.lokiP2pAPI = new window.LokiP2pAPI(ourKey);
window.lokiP2pAPI.on('pingContact', pubKey => { window.lokiP2pAPI.on('pingContact', pubKey => {

@ -2085,6 +2085,23 @@
token, token,
}; };
}, },
getModStatus() {
if (!this.isPublic()) {
return false;
}
return this.get('modStatus');
},
async setModStatus(newStatus) {
if (!this.isPublic()) {
return;
}
if (this.get('modStatus') !== newStatus) {
this.set({ modStatus: newStatus });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
}
},
// SIGNAL PROFILES // SIGNAL PROFILES
@ -2288,6 +2305,21 @@
}); });
}, },
async deletePublicMessage(message) {
const serverAPI = lokiPublicChatAPI.findOrCreateServer(
this.get('server')
);
const channelAPI = serverAPI.findOrCreateChannel(
this.get('channelId'),
this.id
);
const success = await channelAPI.deleteMessage(message.getServerId());
if (success) {
this.removeMessage(message.id);
}
return success;
},
removeMessage(messageId) { removeMessage(messageId) {
const message = this.messageCollection.models.find( const message = this.messageCollection.models.find(
msg => msg.id === messageId msg => msg.id === messageId

@ -672,6 +672,8 @@
isP2p: !!this.get('isP2p'), isP2p: !!this.get('isP2p'),
isPublic: !!this.get('isPublic'), isPublic: !!this.get('isPublic'),
isRss: !!this.get('isRss'), isRss: !!this.get('isRss'),
isDeletable:
!this.get('isPublic') || this.getConversation().getModStatus(),
onCopyText: () => this.copyText(), onCopyText: () => this.copyText(),
onReply: () => this.trigger('reply', this), onReply: () => this.trigger('reply', this),
@ -1240,6 +1242,9 @@
Message: Whisper.Message, Message: Whisper.Message,
}); });
}, },
getServerId() {
return this.get('serverId');
},
async setServerId(serverId) { async setServerId(serverId) {
if (_.isEqual(this.get('serverId'), serverId)) return; if (_.isEqual(this.get('serverId'), serverId)) return;

@ -1,4 +1,4 @@
/* global log, textsecure, libloki, Signal, Whisper, Headers */ /* global log, textsecure, libloki, Signal, Whisper, Headers, ConversationController */
const EventEmitter = require('events'); const EventEmitter = require('events');
const nodeFetch = require('node-fetch'); const nodeFetch = require('node-fetch');
const { URL, URLSearchParams } = require('url'); const { URL, URLSearchParams } = require('url');
@ -12,35 +12,35 @@ class LokiPublicChatAPI extends EventEmitter {
constructor(ourKey) { constructor(ourKey) {
super(); super();
this.ourKey = ourKey; this.ourKey = ourKey;
this.lastGot = {};
this.servers = []; this.servers = [];
} }
findOrCreateServer(hostport) { findOrCreateServer(serverUrl) {
let thisServer = this.servers.find(server => server.server === hostport); let thisServer = this.servers.find(
server => server.baseServerUrl === serverUrl
);
if (!thisServer) { if (!thisServer) {
log.info(`LokiPublicChatAPI creating ${hostport}`); log.info(`LokiPublicChatAPI creating ${serverUrl}`);
thisServer = new LokiPublicServerAPI(this, hostport); thisServer = new LokiPublicServerAPI(this, serverUrl);
this.servers.push(thisServer); this.servers.push(thisServer);
} }
return thisServer; return thisServer;
} }
// rename to findOrCreateChannel? findOrCreateChannel(serverUrl, channelId, conversationId) {
registerChannel(hostport, channelId, conversationId) { const server = this.findOrCreateServer(serverUrl);
const server = this.findOrCreateServer(hostport);
return server.findOrCreateChannel(channelId, conversationId); return server.findOrCreateChannel(channelId, conversationId);
} }
unregisterChannel(hostport, channelId) { unregisterChannel(serverUrl, channelId) {
let thisServer; let thisServer;
let i = 0; let i = 0;
for (; i < this.servers.length; i += 1) { for (; i < this.servers.length; i += 1) {
if (this.servers[i].server === hostport) { if (this.servers[i].server === serverUrl) {
thisServer = this.servers[i]; thisServer = this.servers[i];
break; break;
} }
} }
if (!thisServer) { if (!thisServer) {
log.warn(`Tried to unregister from nonexistent server ${hostport}`); log.warn(`Tried to unregister from nonexistent server ${serverUrl}`);
return; return;
} }
thisServer.unregisterChannel(channelId); thisServer.unregisterChannel(channelId);
@ -88,6 +88,9 @@ class LokiPublicServerAPI {
} }
async getOrRefreshServerToken() { async getOrRefreshServerToken() {
if (this.token) {
return this.token;
}
let token = await Signal.Data.getPublicServerTokenByServerUrl( let token = await Signal.Data.getPublicServerTokenByServerUrl(
this.baseServerUrl this.baseServerUrl
); );
@ -100,6 +103,7 @@ class LokiPublicServerAPI {
}); });
} }
} }
this.token = token;
return token; return token;
} }
@ -171,6 +175,7 @@ class LokiPublicServerAPI {
class LokiPublicChannelAPI { class LokiPublicChannelAPI {
constructor(serverAPI, channelId, conversationId) { constructor(serverAPI, channelId, conversationId) {
// properties
this.serverAPI = serverAPI; this.serverAPI = serverAPI;
this.channelId = channelId; this.channelId = channelId;
this.baseChannelUrl = `channels/${this.channelId}`; this.baseChannelUrl = `channels/${this.channelId}`;
@ -178,24 +183,21 @@ class LokiPublicChannelAPI {
this.conversationId = conversationId; this.conversationId = conversationId;
this.lastGot = 0; this.lastGot = 0;
this.stopPolling = false; this.stopPolling = false;
this.modStatus = false;
this.deleteLastId = 1;
// end properties
log.info(`registered LokiPublicChannel ${channelId}`); log.info(`registered LokiPublicChannel ${channelId}`);
// start polling // start polling
this.pollForMessages(); this.pollForMessages();
this.deleteLastId = 1;
this.pollForDeletions(); this.pollForDeletions();
} }
getEndpoint() { async serverRequest(endpoint, options = {}) {
const endpoint = `${this.serverAPI.baseServerUrl}/${
this.baseChannelUrl
}/messages`;
return endpoint;
}
// we'll pass token for now
async serverRequest(endpoint, params, method) {
const url = new URL(`${this.serverAPI.baseServerUrl}/${endpoint}`); const url = new URL(`${this.serverAPI.baseServerUrl}/${endpoint}`);
url.search = new URLSearchParams(params); if (options.params) {
url.search = new URLSearchParams(params);
}
let res; let res;
let { token } = this.serverAPI; let { token } = this.serverAPI;
if (!token) { if (!token) {
@ -208,108 +210,155 @@ class LokiPublicChannelAPI {
} }
} }
try { try {
// eslint-disable-next-line no-await-in-loop const fetchOptions = {
const options = {
headers: new Headers({ headers: new Headers({
Authorization: `Bearer ${this.serverAPI.token}`, Authorization: `Bearer ${this.serverAPI.token}`,
}), }),
}; };
if (method) { if (options.method) {
options.method = method; fetchOptions.method = options.method;
} }
res = await nodeFetch(url, options || undefined); res = await nodeFetch(url, fetchOptions || undefined);
} catch (e) { } catch (e) {
log.info(`e ${e}`); log.info(`e ${e}`);
return { return {
err: e, err: e,
}; };
} }
// eslint-disable-next-line no-await-in-loop
const response = await res.json(); const response = await res.json();
if (response.meta.code !== 200) {
// if it's a response style with a meta
if (res.status !== 200) {
return { return {
err: 'statusCode', err: 'statusCode',
response, response,
}; };
} }
return { return {
statusCode: res.status,
response, response,
}; };
} }
async refreshModStatus() {
const res = serverRequest('/loki/v1/user_info');
// if no problems and we have data
if (!res.err && res.response && res.response.data) {
this.modStatus = res.response.data.moderator_status;
}
const conversation = ConversationController.get(this.conversationId);
await conversation.setModStatus(this.modStatus);
}
async deleteMessage(serverId) {
const res = await this.serverRequest(
this.modStatus?`loki/v1/moderation/message/${messageServerId}`:`${this.baseChannelUrl}/messages/${serverId}`,
{ method: 'DELETE' }
);
if (!res.err && res.response) {
log.info(`deleted ${serverId} on ${this.baseChannelUrl}`);
return true;
}
log.warn(`failed to delete ${serverId} on ${this.baseChannelUrl}`);
return false;
}
getEndpoint() {
const endpoint = `${this.serverAPI.baseServerUrl}/${
this.baseChannelUrl
}/messages`;
return endpoint;
}
// update room details
async pollForChannel(source, endpoint) { async pollForChannel(source, endpoint) {
// groupName will be loaded from server // groupName will be loaded from server
const url = new URL(this.baseChannelUrl); const url = new URL(this.baseChannelUrl);
let res; let res;
let success = true; const token = await this.serverAPI.getOrRefreshServerToken();
if (!token) {
log.error('NO TOKEN');
return {
err: 'noToken',
};
}
try { 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) { } catch (e) {
success = false; log.info(`e ${e}`);
return {
err: e,
};
} }
// eslint-disable-next-line no-await-in-loop
const response = await res.json(); const response = await res.json();
if (response.meta.code !== 200) { if (response.meta.code !== 200) {
success = false; return {
err: 'statusCode',
response,
};
} }
// update this.groupId return {
return endpoint || success; response,
};
} }
// get moderation actions
async pollForDeletions() { async pollForDeletions() {
// read all messages from 0 to current // grab the last 200 deletions
// delete local copies if server state has changed to delete
// run every minute
const pollAgain = () => {
setTimeout(() => {
this.pollForDeletions();
}, DELETION_POLL_EVERY);
};
const params = { const params = {
count: 200, count: 200,
}; };
// full scan // start loop
let more = true; let more = true;
// eslint-disable-next-line no-await-in-loop
while (more) { while (more) {
// set params to from where we last checked
params.since_id = this.deleteLastId; params.since_id = this.deleteLastId;
// grab the next 200 deletions from where we last checked
const res = await this.serverRequest( const res = await this.serverRequest(
`loki/v1/channel/${this.channelId}/deletes`, `loki/v1/channel/${this.channelId}/deletes`,
params { params }
); );
// eslint-disable-next-line no-loop-func // Process rresult
res.response.data.reverse().forEach(deleteEntry => { res.response.data.reverse().forEach(deleteEntry => {
// Escalate it up to the subsystem that can check to see if this has been processsed
Whisper.events.trigger('deleteLocalPublicMessage', { Whisper.events.trigger('deleteLocalPublicMessage', {
messageServerId: deleteEntry.message_id, messageServerId: deleteEntry.message_id,
conversationId: this.conversationId, conversationId: this.conversationId,
}); });
}); });
// if we had a problem break the loop
if (res.response.data.length < 200) { if (res.response.data.length < 200) {
break; break;
} }
// update where we last checked
this.deleteLastId = res.response.meta.max_id; this.deleteLastId = res.response.meta.max_id;
({ more } = res.response); ({ more } = res.response);
} }
pollAgain();
}
async deleteMessage(serverId) { // set up next poll
const params = {}; setTimeout(() => {
const res = await this.serverRequest( this.pollForDeletions();
`${this.baseChannelUrl}/messages/${serverId}`, }, DELETION_POLL_EVERY);
params,
'DELETE'
);
if (!res.err && res.response) {
log.info(`deleted ${serverId} on ${this.baseChannelUrl}`);
return true;
}
log.warn(`failed to delete ${serverId} on ${this.baseChannelUrl}`);
return false;
} }
// get channel messages
async pollForMessages() { async pollForMessages() {
const params = { const params = {
include_annotations: 1, include_annotations: 1,
@ -321,7 +370,7 @@ class LokiPublicChannelAPI {
} }
const res = await this.serverRequest( const res = await this.serverRequest(
`${this.baseChannelUrl}/messages`, `${this.baseChannelUrl}/messages`,
params { params }
); );
if (!res.err && res.response) { if (!res.err && res.response) {

@ -1291,6 +1291,29 @@
}, },
deleteMessage(message) { deleteMessage(message) {
if (this.model.isPublic()) {
const dialog = new Whisper.ConfirmationDialogView({
message: i18n('deletePublicWarning'),
okText: i18n('delete'),
resolve: async () => {
const success = await this.model.deletePublicMessage(message);
if (!success) {
// Message failed to delete from server, show error?
return;
}
await window.Signal.Data.removeMessage(message.id, {
Message: Whisper.Message,
});
message.trigger('unload');
this.resetPanel();
this.updateHeader();
},
});
this.$el.prepend(dialog.el);
dialog.focusCancel();
return;
}
const dialog = new Whisper.ConfirmationDialogView({ const dialog = new Whisper.ConfirmationDialogView({
message: i18n('deleteWarning'), message: i18n('deleteWarning'),
okText: i18n('delete'), okText: i18n('delete'),

@ -48,6 +48,7 @@ interface LinkPreviewType {
export interface Props { export interface Props {
disableMenu?: boolean; disableMenu?: boolean;
isDeletable: boolean;
text?: string; text?: string;
textPending?: boolean; textPending?: boolean;
id?: string; id?: string;
@ -819,6 +820,7 @@ export class Message extends React.PureComponent<Props, State> {
onCopyText, onCopyText,
direction, direction,
status, status,
isDeletable,
onDelete, onDelete,
onDownload, onDownload,
onReply, onReply,
@ -876,14 +878,16 @@ export class Message extends React.PureComponent<Props, State> {
{i18n('retrySend')} {i18n('retrySend')}
</MenuItem> </MenuItem>
) : null} ) : null}
<MenuItem {isDeletable ? (
attributes={{ <MenuItem
className: 'module-message__context__delete-message', attributes={{
}} className: 'module-message__context__delete-message',
onClick={onDelete} }}
> onClick={onDelete}
{i18n('deleteMessage')} >
</MenuItem> {i18n('deleteMessage')}
</MenuItem>
) : null}
</ContextMenu> </ContextMenu>
); );
} }

Loading…
Cancel
Save