Merge pull request #455 from neuroscr/public-delete

Public delete
pull/387/head
Beaudan Campbell-Brown 6 years ago committed by GitHub
commit f4e76f0576
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

@ -790,7 +790,7 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) {
await instance.run(
`ALTER TABLE messages
ADD COLUMN serverId STRING;`
ADD COLUMN serverId INTEGER;`
);
await instance.run(
@ -2060,11 +2060,14 @@ async function removeMessage(id) {
);
}
async function getMessageByServerId(serverId) {
async function getMessageByServerId(serverId, conversationId) {
const row = await db.get(
'SELECT * FROM messages WHERE serverId = $serverId;',
`SELECT * FROM messages WHERE
serverId = $serverId AND
conversationId = $conversationId;`,
{
$serverId: serverId,
$conversationId: conversationId,
}
);

@ -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 => {
@ -487,6 +488,28 @@
}
});
Whisper.events.on(
'deleteLocalPublicMessage',
async ({ messageServerId, conversationId }) => {
const message = await window.Signal.Data.getMessageByServerId(
messageServerId,
conversationId,
{
Message: Whisper.Message,
}
);
if (message) {
const conversation = ConversationController.get(conversationId);
if (conversation) {
conversation.removeMessage(message.id);
}
await window.Signal.Data.removeMessage(message.id, {
Message: Whisper.Message,
});
}
}
);
Whisper.events.on('setupAsNewDevice', () => {
const { appView } = window.owsDesktopApp;
if (appView) {
@ -1418,6 +1441,7 @@
let messageData = {
source: data.source,
sourceDevice: data.sourceDevice,
serverId: data.serverId,
sent_at: data.timestamp,
received_at: data.receivedAt || Date.now(),
conversationId: data.source,

@ -1376,7 +1376,7 @@
const options = this.getSendOptions();
options.messageType = message.get('type');
options.isPublic = this.isPublic();
if (this.isPublic()) {
if (options.isPublic) {
options.publicSendData = await this.getPublicSendData();
}
@ -2073,17 +2073,28 @@
const serverAPI = lokiPublicChatAPI.findOrCreateServer(
this.get('server')
);
// Can be null if fails
const token = await serverAPI.getOrRefreshServerToken();
const channelAPI = serverAPI.findOrCreateChannel(
this.get('channelId'),
this.id
);
const publicEndpoint = channelAPI.getEndpoint();
return {
publicEndpoint,
token,
};
return channelAPI;
},
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
@ -2288,6 +2299,31 @@
});
},
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) {
const message = this.messageCollection.models.find(
msg => msg.id === messageId
);
if (message) {
message.trigger('unload');
this.messageCollection.remove(messageId);
}
},
deleteMessages() {
Whisper.events.trigger('showConfirmationDialog', {
message: i18n('deleteConversationConfirmation'),

@ -357,9 +357,6 @@
onDestroy() {
this.cleanup();
},
deleteMessage() {
this.trigger('delete', this);
},
async cleanup() {
MessageController.unregister(this.id);
this.unload();
@ -675,6 +672,10 @@
isP2p: !!this.get('isP2p'),
isPublic: !!this.get('isPublic'),
isRss: !!this.get('isRss'),
isDeletable:
!this.get('isPublic') ||
this.getConversation().getModStatus() ||
this.getSource() === this.OUR_NUMBER,
onCopyText: () => this.copyText(),
onReply: () => this.trigger('reply', this),
@ -1243,6 +1244,9 @@
Message: Whisper.Message,
});
},
getServerId() {
return this.get('serverId');
},
async setServerId(serverId) {
if (_.isEqual(this.get('serverId'), serverId)) return;

@ -908,8 +908,8 @@ async function _removeMessages(ids) {
await channels.removeMessage(ids);
}
async function getMessageByServerId(id, { Message }) {
const message = await channels.getMessageByServerId(id);
async function getMessageByServerId(serverId, conversationId, { Message }) {
const message = await channels.getMessageByServerId(serverId, conversationId);
if (!message) {
return null;
}

@ -4,7 +4,6 @@
const _ = require('lodash');
const { rpc } = require('./loki_rpc');
const nodeFetch = require('node-fetch');
const DEFAULT_CONNECTIONS = 3;
const MAX_ACCEPTABLE_FAILURES = 1;
@ -89,57 +88,23 @@ class LokiMessageAPI {
};
if (isPublic) {
const { token, publicEndpoint } = publicSendData;
if (!token) {
throw new window.textsecure.PublicChatError(
`Failed to retrieve valid token for ${publicEndpoint}`
);
}
const { profile } = data;
let displayName = 'Anonymous';
if (profile && profile.displayName) {
({ displayName } = profile);
}
const payload = {
text: data.body,
annotations: [
{
type: 'network.loki.messenger.publicChat',
value: {
timestamp: messageTimeStamp,
from: displayName,
source: this.ourKey,
},
},
],
};
let result;
try {
result = await nodeFetch(publicEndpoint, {
method: 'post',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(payload),
});
} catch (e) {
throw new window.textsecure.PublicChatError(
`Failed to send public chat message: ${e}`
);
}
const body = await result.json();
if (!result.ok) {
if (result.status === 401) {
// TODO: Handle token timeout
}
const error = body.meta.error_message;
const res = await publicSendData.sendMessage(
data.body,
messageTimeStamp,
displayName,
this.ourKey
);
if (res === false) {
throw new window.textsecure.PublicChatError(
`Failed to send public chat message: ${error}`
'Failed to send public chat message'
);
}
messageEventData.serverId = body.data.id;
messageEventData.serverId = res;
window.Whisper.events.trigger('publicMessageSent', messageEventData);
return;
}

@ -1,43 +1,52 @@
/* 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);
// server getter/factory
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);
// channel getter/factory
findOrCreateChannel(serverUrl, channelId, conversationId) {
const server = this.findOrCreateServer(serverUrl);
return server.findOrCreateChannel(channelId, conversationId);
}
unregisterChannel(hostport, channelId) {
// deallocate resources server uses
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);
@ -51,17 +60,27 @@ class LokiPublicServerAPI {
this.channels = [];
this.tokenPromise = null;
this.baseServerUrl = url;
const ref = this;
(async function justToEnableAsyncToGetToken() {
ref.token = await ref.getOrRefreshServerToken();
log.info(`set token ${ref.token}`);
})();
}
// channel getter/factory
findOrCreateChannel(channelId, conversationId) {
let thisChannel = this.channels.find(
channel => channel.channelId === channelId
);
if (!thisChannel) {
log.info(`LokiPublicChatAPI creating channel ${conversationId}`);
thisChannel = new LokiPublicChannelAPI(this, channelId, conversationId);
this.channels.push(thisChannel);
}
return thisChannel;
}
// deallocate resources channel uses
unregisterChannel(channelId) {
let thisChannel;
let i = 0;
@ -78,10 +97,17 @@ class LokiPublicServerAPI {
thisChannel.stopPolling = true;
}
async getOrRefreshServerToken() {
let token = await Signal.Data.getPublicServerTokenByServerUrl(
this.baseServerUrl
);
// get active token for this server
async getOrRefreshServerToken(forceRefresh = false) {
let token;
if (!forceRefresh) {
if (this.token) {
return this.token;
}
token = await Signal.Data.getPublicServerTokenByServerUrl(
this.baseServerUrl
);
}
if (!token) {
token = await this.refreshServerToken();
if (token) {
@ -91,30 +117,40 @@ class LokiPublicServerAPI {
});
}
}
this.token = token;
return token;
}
// get active token from server (but only allow one request at a time)
async refreshServerToken() {
// if currently not in progress
if (this.tokenPromise === null) {
// set lock
this.tokenPromise = new Promise(async res => {
// request the oken
const token = await this.requestToken();
if (!token) {
res(null);
return;
}
// activate the token
const registered = await this.submitToken(token);
if (!registered) {
res(null);
return;
}
// resolve promise to release lock
res(token);
});
}
// wait until we have it set
const token = await this.tokenPromise;
// clear lock
this.tokenPromise = null;
return token;
}
// request an token from the server
async requestToken() {
const url = new URL(`${this.baseServerUrl}/loki/v1/get_challenge`);
const params = {
@ -136,6 +172,7 @@ class LokiPublicServerAPI {
return token;
}
// activate token
async submitToken(token) {
const options = {
method: 'POST',
@ -162,97 +199,233 @@ class LokiPublicServerAPI {
class LokiPublicChannelAPI {
constructor(serverAPI, channelId, conversationId) {
// properties
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;
this.stopPolling = false;
this.modStatus = false;
this.deleteLastId = 1;
// end properties
log.info(`registered LokiPublicChannel ${channelId}`);
// start polling
this.pollForMessages();
this.pollForDeletions();
}
// make a request to the server
async serverRequest(endpoint, options = {}) {
const { params = {}, method, objBody, forceFreshToken = false } = options;
const url = new URL(`${this.serverAPI.baseServerUrl}/${endpoint}`);
if (params) {
url.search = new URLSearchParams(params);
}
let result;
let { token } = this.serverAPI;
if (!token) {
token = await this.serverAPI.getOrRefreshServerToken();
if (!token) {
log.error('NO TOKEN');
return {
err: 'noToken',
};
}
}
try {
const fetchOptions = {};
const headers = {
Authorization: `Bearer ${this.serverAPI.token}`,
};
if (method) {
fetchOptions.method = method;
}
if (objBody) {
headers['Content-Type'] = 'application/json';
fetchOptions.body = JSON.stringify(objBody);
}
fetchOptions.headers = new Headers(headers);
result = await nodeFetch(url, fetchOptions || undefined);
} catch (e) {
log.info(`e ${e}`);
return {
err: e,
};
}
let response = null;
try {
response = await result.json();
} catch (e) {
log.info(`serverRequest json arpse ${e}`);
return {
err: e,
statusCode: result.status,
};
}
// if it's a response style with a meta
if (result.status !== 200) {
if (!forceFreshToken && response.meta.code === 401) {
// copy options because lint complains if we modify this directly
const updatedOptions = options;
// force it this time
updatedOptions.forceFreshToken = true;
// retry with updated options
return this.serverRequest(endpoint, updatedOptions);
}
return {
err: 'statusCode',
statusCode: result.status,
response,
};
}
return {
statusCode: result.status,
response,
};
}
// get moderator status
async refreshModStatus() {
const res = this.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);
}
// delete a message on the server
async deleteMessage(serverId) {
const res = await this.serverRequest(
this.modStatus
? `loki/v1/moderation/message/${serverId}`
: `${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;
}
// used for sending messages
getEndpoint() {
const endpoint = `${this.serverAPI.baseServerUrl}/channels/${
this.channelId
const endpoint = `${this.serverAPI.baseServerUrl}/${
this.baseChannelUrl
}/messages`;
return endpoint;
}
async pollForChannel(source, endpoint) {
// update room details
async pollForChannel() {
// groupName will be loaded from server
const url = new URL(this.baseChannelUrl);
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}`,
}),
};
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,
};
}
// get moderation actions
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;
}
// grab the last 200 deletions
const params = {
count: 200,
};
const response = await res.json();
if (response.meta.code !== 200) {
success = false;
// start loop
let more = true;
while (more) {
// set params to from where we last checked
params.since_id = this.deleteLastId;
// grab the next 200 deletions from where we last checked
// eslint-disable-next-line no-await-in-loop
const res = await this.serverRequest(
`loki/v1/channel/${this.channelId}/deletes`,
{ params }
);
// Process results
res.response.data.reverse().forEach(deleteEntry => {
// Escalate it up to the subsystem that can check to see if this has
// been processed
Whisper.events.trigger('deleteLocalPublicMessage', {
messageServerId: deleteEntry.message_id,
conversationId: this.conversationId,
});
});
// if we had a problem break the loop
if (res.response.data.length < 200) {
break;
}
// update where we last checked
this.deleteLastId = res.response.meta.max_id;
({ more } = res.response);
}
return success;
// set up next poll
setTimeout(() => {
this.pollForDeletions();
}, DELETION_POLL_EVERY);
}
// get channel messages
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);
const res = await this.serverRequest(`${this.baseChannelUrl}/messages`, {
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;
}
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 +437,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,
@ -309,6 +492,33 @@ class LokiPublicChannelAPI {
this.pollForMessages();
}, GROUPCHAT_POLL_EVERY);
}
// create a message in the channel
async sendMessage(text, messageTimeStamp, displayName, pubKey) {
const payload = {
text,
annotations: [
{
type: 'network.loki.messenger.publicChat',
value: {
timestamp: messageTimeStamp,
// will deprecated
from: displayName,
// will deprecated
source: pubKey,
},
},
],
};
const res = await this.serverRequest(`${this.baseChannelUrl}/messages`, {
method: 'POST',
objBody: payload,
});
if (!res.err && res.response) {
return res.response.data.id;
}
return false;
}
}
module.exports = LokiPublicChatAPI;

@ -1291,6 +1291,29 @@
},
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({
message: i18n('deleteWarning'),
okText: i18n('delete'),

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

Loading…
Cancel
Save