Skip to content

Commit

Permalink
Initial support for inbound moderator actions
Browse files Browse the repository at this point in the history
  • Loading branch information
singpolyma committed Dec 18, 2024
1 parent 4da4419 commit 10f8b25
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 16 deletions.
6 changes: 6 additions & 0 deletions snikket/Chat.hx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ abstract class Chat {
}));
case ReactionUpdateStanza(update):
persistence.storeReaction(client.accountId(), update, (m)->{});
case ModerateMessageStanza(action):
client.moderateMessage(action);
default:
// ignore
}
Expand Down Expand Up @@ -1030,6 +1032,10 @@ class Channel extends Chat {
promises.push(new thenshim.Promise((resolve, reject) -> {
persistence.storeReaction(client.accountId(), update, (_) -> resolve(null));
}));
case ModerateMessageStanza(action):
promises.push(new thenshim.Promise((resolve, reject) -> {
client.moderateMessage(action).then((_) -> resolve(null));
}));
default:
// ignore
}
Expand Down
16 changes: 16 additions & 0 deletions snikket/ChatMessage.hx
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,22 @@ class ChatMessage {
return type == MessageChannel || type == MessageChannelPrivate ? serverId : localId;
}

@:allow(snikket)
private function makeModerated(timestamp: String, moderatorId: Null<String>, reason: Null<String>) {
text = null;
attachments = [];
payloads = [];
versions = [];
final cleanedStub = clone();
final payload = new Stanza("retracted", { xmlns: "urn:xmpp:message-retract:1", stamp: timestamp });
if (reason != null) payload.textTag("reason", reason);
payload.tag("moderated", { by: moderatorId, xmlns: "urn:xmpp:message-moderate:1" }).up();
payloads.push(payload);
final head = clone();
head.timestamp = timestamp;
versions = [head, cleanedStub];
}

private function set_localId(localId:Null<String>) {
if(this.localId != null) {
throw new Exception("Message already has a localId set");
Expand Down
23 changes: 23 additions & 0 deletions snikket/Client.hx
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ class Client extends EventEmitter {
fetchMediaByHash([hash], [from]);
}
persistence.storeReaction(accountId(), update, (stored) -> if (stored != null) notifyMessageHandlers(stored, ReactionEvent));
case ModerateMessageStanza(action):
moderateMessage(action).then((stored) -> if (stored != null) notifyMessageHandlers(stored, CorrectionEvent));
default:
// ignore
}
Expand Down Expand Up @@ -900,6 +902,23 @@ class Client extends EventEmitter {
return chats.find((chat) -> chat.chatId == chatId);
}

@:allow(snikket)
private function moderateMessage(action: ModerationAction): Promise<Null<ChatMessage>> {
return new thenshim.Promise((resolve, reject) ->
persistence.getMessage(accountId(), action.chatId, action.moderateServerId, null, (moderateMessage) -> {
if (moderateMessage == null) return resolve(null);
for(attachment in moderateMessage.attachments) {
for(hash in attachment.hashes) {
persistence.removeMedia(hash.algorithm, hash.hash);
}
}
moderateMessage.makeModerated(action.timestamp, action.moderatorId, action.reason);
persistence.updateMessage(accountId(), moderateMessage);
resolve(moderateMessage);
})
);
}

@:allow(snikket)
private function getDirectChat(chatId:String, triggerIfNew:Bool = true):DirectChat {
for (chat in chats) {
Expand Down Expand Up @@ -1417,6 +1436,10 @@ class Client extends EventEmitter {
promises.push(new thenshim.Promise((resolve, reject) -> {
persistence.storeReaction(accountId(), update, (_) -> resolve(null));
}));
case ModerateMessageStanza(action):
promises.push(new thenshim.Promise((resolve, reject) -> {
moderateMessage(action).then((_) -> resolve(null));
}));
default:
// ignore
}
Expand Down
33 changes: 25 additions & 8 deletions snikket/Message.hx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ enum abstract MessageType(Int) {
enum MessageStanza {
ErrorMessageStanza(stanza: Stanza);
ChatMessageStanza(message: ChatMessage);
ModerateMessageStanza(action: ModerationAction);
ReactionUpdateStanza(update: ReactionUpdate);
UnknownMessageStanza(stanza: Stanza);
}
Expand Down Expand Up @@ -198,7 +199,30 @@ class Message {
Reflect.setField(msg, "localId", jmi.attr.get("id"));
}

if (msg.text == null && msg.attachments.length < 1) return new Message(msg.chatId(), msg.senderId(), msg.threadId, UnknownMessageStanza(stanza));
final retract = stanza.getChild("replace", "urn:xmpp:message-retract:1");
final fasten = stanza.getChild("apply-to", "urn:xmpp:fasten:0");
final moderated = retract?.getChild("moderated", "urn:xmpp:message-retract:1") ?? fasten?.getChild("moderated", "urn:xmpp:message-moderate:0");
final moderateServerId = retract?.attr?.get("id") ?? fasten?.attr?.get("id");
if (moderated != null && moderateServerId != null && isGroupchat && msg.from != null && msg.from.isBare() && msg.from.asString() == msg.chatId()) {
final reason = retract?.getChildText("reason") ?? moderated?.getChildText("reason");
final by = moderated.attr.get("by");
// TODO: occupant id as well / instead of by?
return new Message(
msg.chatId(),
msg.senderId(),
msg.threadId,
ModerateMessageStanza(new ModerationAction(msg.chatId(), moderateServerId, timestamp, by, reason))
);
}

final replace = stanza.getChild("replace", "urn:xmpp:message-correct:0");
final replaceId = replace?.attr?.get("id");
if (replaceId != null) {
msg.versions = [msg.clone()];
Reflect.setField(msg, "localId", replaceId);
}

if (msg.text == null && msg.attachments.length < 1 && msg.versions.length < 1) return new Message(msg.chatId(), msg.senderId(), msg.threadId, UnknownMessageStanza(stanza));

for (fallback in stanza.allTags("fallback", "urn:xmpp:fallback:0")) {
msg.payloads.push(fallback);
Expand Down Expand Up @@ -271,13 +295,6 @@ class Message {
}
}

final replace = stanza.getChild("replace", "urn:xmpp:message-correct:0");
final replaceId = replace?.attr?.get("id");
if (replaceId != null) {
msg.versions = [msg.clone()];
Reflect.setField(msg, "localId", replaceId);
}

return new Message(msg.chatId(), msg.senderId(), msg.threadId, ChatMessageStanza(msg));
}
}
17 changes: 17 additions & 0 deletions snikket/ModerationAction.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package snikket;

class ModerationAction {
public final chatId: String;
public final moderateServerId: String;
public final timestamp: String;
public final moderatorId: Null<String>;
public final reason: Null<String>;

public function new(chatId: String, moderateServerId: String, timestamp: String, moderatorId: Null<String>, reason: Null<String>) {
this.chatId = chatId;
this.moderateServerId = moderateServerId;
this.timestamp = timestamp;
this.moderatorId = moderatorId;
this.reason = reason;
}
}
3 changes: 3 additions & 0 deletions snikket/Persistence.hx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ interface Persistence {
public function getChatsUnreadDetails(accountId: String, chats: Array<Chat>, callback: (details:Array<{ chatId: String, message: ChatMessage, unreadCount: Int }>)->Void):Void;
public function storeReaction(accountId: String, update: ReactionUpdate, callback: (Null<ChatMessage>)->Void):Void;
public function storeMessage(accountId: String, message: ChatMessage, callback: (ChatMessage)->Void):Void;
public function updateMessage(accountId: String, message: ChatMessage):Void;
public function updateMessageStatus(accountId: String, localId: String, status:MessageStatus, callback: (ChatMessage)->Void):Void;
public function getMessage(accountId: String, chatId: String, serverId: Null<String>, localId: Null<String>, callback: (Null<ChatMessage>)->Void):Void;
public function getMessagesBefore(accountId: String, chatId: String, beforeId: Null<String>, beforeTime: Null<String>, callback: (messages:Array<ChatMessage>)->Void):Void;
public function getMessagesAfter(accountId: String, chatId: String, afterId: Null<String>, afterTime: Null<String>, callback: (messages:Array<ChatMessage>)->Void):Void;
public function getMessagesAround(accountId: String, chatId: String, aroundId: Null<String>, aroundTime: Null<String>, callback: (messages:Array<ChatMessage>)->Void):Void;
public function hasMedia(hashAlgorithm:String, hash:BytesData, callback: (has:Bool)->Void):Void;
public function storeMedia(mime:String, bytes:BytesData, callback: ()->Void):Void;
public function removeMedia(hashAlgorithm:String, hash:BytesData):Void;
public function storeCaps(caps:Caps):Void;
public function getCaps(ver:String, callback: (Null<Caps>)->Void):Void;
public function storeLogin(login:String, clientId:String, displayName:String, token:Null<String>):Void;
Expand Down
13 changes: 13 additions & 0 deletions snikket/persistence/Dummy.hx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ class Dummy implements Persistence {
callback(message);
}

@HaxeCBridge.noemit
public function updateMessage(accountId: String, message: ChatMessage) {
}

@HaxeCBridge.noemit
public function getMessage(accountId: String, chatId: String, serverId: Null<String>, localId: Null<String>, callback: (Null<ChatMessage>)->Void) {
callback(null);
}

@HaxeCBridge.noemit
public function getMessagesBefore(accountId: String, chatId: String, beforeId: Null<String>, beforeTime: Null<String>, callback: (Array<ChatMessage>)->Void) {
callback([]);
Expand Down Expand Up @@ -86,6 +95,10 @@ class Dummy implements Persistence {
callback();
}

@HaxeCBridge.noemit
public function removeMedia(hashAlgorithm:String, hash:BytesData) {
}

@HaxeCBridge.noemit
public function storeCaps(caps:Caps) { }

Expand Down
48 changes: 41 additions & 7 deletions snikket/persistence/Sqlite.hx
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,35 @@ class Sqlite implements Persistence {
callback(message);
}

@HaxeCBridge.noemit
public function updateMessage(accountId: String, message: ChatMessage) {
storeMessage(accountId, message, (_)->{});
}


public function getMessage(accountId: String, chatId: String, serverId: Null<String>, localId: Null<String>, callback: (Null<ChatMessage>)->Void) {
final q = new StringBuf();
q.add("SELECT stanza FROM messages WHERE account_id=");
db.addValue(q, accountId);
q.add(" AND chat_id=");
db.addValue(q, chatId);
if (serverId != null) {
q.add(" AND mam_id=");
db.addValue(q, serverId);
} else if (localId != null) {
q.add(" AND stanza_id=");
db.addValue(q, localId);
}
q.add("LIMIT 1");
final result = db.request(q.toString());
final messages = [];
for (row in result) {
callback(ChatMessage.fromStanza(Stanza.parse(row.stanza), JID.parse(accountId))); // TODO
return;
}
callback(null);
}

private function getMessages(accountId: String, chatId: String, time: String, op: String) {
final q = new StringBuf();
q.add("SELECT stanza FROM messages WHERE account_id=");
Expand Down Expand Up @@ -294,13 +323,13 @@ class Sqlite implements Persistence {
}

@HaxeCBridge.noemit
public function getMediaUri(hashAlgorithm:String, hash:BytesData, callback: (Null<String>)->Void) {
public function getMediaPath(hashAlgorithm:String, hash:BytesData) {
if (hashAlgorithm == "sha-256") {
final path = blobpath + "/f" + Bytes.ofData(hash).toHex();
if (FileSystem.exists(path)) {
callback("file://" + FileSystem.absolutePath(path));
return FileSystem.absolutePath(path);
} else {
callback(null);
return null;
}
} else if (hashAlgorithm == "sha-1") {
final q = new StringBuf();
Expand All @@ -309,18 +338,23 @@ class Sqlite implements Persistence {
q.add(" LIMIT 1");
final result = db.request(q.toString());
for (row in result) {
getMediaUri("sha-256", row.sha256, callback);
return;
return getMediaPath("sha-256", row.sha256);
}
callback(null);
return null;
} else {
throw "Unknown hash algorithm: " + hashAlgorithm;
}
}

@HaxeCBridge.noemit
public function hasMedia(hashAlgorithm:String, hash:BytesData, callback: (Bool)->Void) {
getMediaUri(hashAlgorithm, hash, (uri) -> callback(uri != null));
callback(getMediaPath(hashAlgorithm, hash) != null);
}

@HaxeCBridge.noemit
public function removeMedia(hashAlgorithm:String, hash:BytesData) {
final path = getMediaPath(hashAlgorithm, hash);
if (path != null) FileSystem.deleteFile(path);
}

@HaxeCBridge.noemit
Expand Down
24 changes: 23 additions & 1 deletion snikket/persistence/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,12 @@ const browser = (dbname, tokenize, stemmer) => {
});
},

updateMessage: function(account, message) {
const tx = db.transaction(["messages"], "readwrite");
const store = tx.objectStore("messages");
store.put(serializeMessage(account, message));
},

updateMessageStatus: function(account, localId, status, callback) {
const tx = db.transaction(["messages"], "readwrite");
const store = tx.objectStore("messages");
Expand Down Expand Up @@ -569,11 +575,27 @@ const browser = (dbname, tokenize, stemmer) => {

hasMedia: function(hashAlgorithm, hash, callback) {
(async () => {
const response = await this.getMediaResponse(hashAlgorithm, hash);
const response = await this.getMediaResponse(mkNiUrl(hashAlgorithm, hash));
return !!response;
})().then(callback);
},

removeMedia: function(hashAlgorithm, hash) {
(async () => {
var niUrl;
if (hashAlgorithm === "sha-256") {
niUrl = mkNiUrl(hashAlgorithm, hash);
} else {
const tx = db.transaction(["keyvaluepairs"], "readonly");
const store = tx.objectStore("keyvaluepairs");
niUrl = await promisifyRequest(store.get(mkNiUrl(hashAlgorithm, hash)));
if (!niUrl) return;
}

return await cache.delete(niUrl);
})();
},

storeMedia: function(mime, buffer, callback) {
(async function() {
const sha256 = await crypto.subtle.digest("SHA-256", buffer);
Expand Down

0 comments on commit 10f8b25

Please sign in to comment.