diff --git a/kaikout/assets/commands.toml b/kaikout/assets/commands.toml index a81bb54..a038711 100644 --- a/kaikout/assets/commands.toml +++ b/kaikout/assets/commands.toml @@ -159,6 +159,14 @@ timer Timer value (in seconds) for countdown before committing an action. """ +[rtbl] +allow = """ +ignore [+|-] +Jabber IDs to ignore +comma-separated keywords +'+' appends to, '-' removes from. +""" + [statistics] score = """ scores diff --git a/kaikout/assets/rtbl.toml b/kaikout/assets/rtbl.toml index 1e683be..6827dc8 100644 --- a/kaikout/assets/rtbl.toml +++ b/kaikout/assets/rtbl.toml @@ -1,6 +1,7 @@ # This file lists default RTBL sources per database. # See file /usr/share/kaikout/rtbl.toml -[[node]] +[[sources]] +description = "A general block list focussing on spam and service abuse." jabber_id = "xmppbl.org" -node_name = "muc_bans_sha256" +node_id = "muc_bans_sha256" diff --git a/kaikout/assets/settings.toml b/kaikout/assets/settings.toml index 2785cc9..a6b6cdf 100644 --- a/kaikout/assets/settings.toml +++ b/kaikout/assets/settings.toml @@ -13,6 +13,7 @@ frequency_messages = 1 # The maximum allowed frequency (in seconds) of sent me frequency_presence = 180 # The maximum allowed frequency (in seconds) of changed status messages. inactivity_span = 30 # The maximum allowed time (in days) of inactivity. inactivity_warn = 300 # The time (in minutes) of inactivity to send a warning upon before action. Value can not be higher than of inactivity_span. +rtbl_ignore = [] # A list of RTBL lists to exclude. score_messages = 3 # The maximum allowed number of message faults to act upon. score_presence = 10 # The maximum allowed number of presence faults to act upon. timer = 180 # Timer value (in seconds) for countdown before committing an action. diff --git a/kaikout/utilities.py b/kaikout/utilities.py index 92b86b9..f51f1ff 100644 --- a/kaikout/utilities.py +++ b/kaikout/utilities.py @@ -199,6 +199,67 @@ class Log: with open(filename, 'w') as f: f.write(content) +class BlockList: + + + def get_filename(): + """ + Get pathname of filename. + If filename does not exist, create it. + + Parameters + ---------- + None. + + Returns + ------- + filename : str + Pathname. + """ + data_dir = Config.get_default_data_directory() + if not os.path.isdir(data_dir): os.mkdir(data_dir) + filename = os.path.join(data_dir, r"blocklist.toml") + if not os.path.exists(filename): + data = {'entries' : {}} + content = tomli_w.dumps(data) + with open(filename, 'w') as f: f.write(content) + return filename + + + def load_blocklist(self): + filename = BlockList.get_filename() + with open(filename, 'rb') as f: + self.blocklist = tomllib.load(f) + + + def add_entry_to_blocklist(self, jabber_id, node_id, item_id): + """ + Update blocklist file. + + Parameters + ---------- + jabber_id : str + Jabber ID. + node_id : str + Node name. + item_id : str + Item ID. + + Returns + ------- + None. + """ + if jabber_id not in self.blocklist['entries']: + self.blocklist['entries'][jabber_id] = {} + if node_id not in self.blocklist['entries'][jabber_id]: + self.blocklist['entries'][jabber_id][node_id] = [] + self.blocklist['entries'][jabber_id][node_id].append(item_id) + data = self.blocklist + content = tomli_w.dumps(data) + filename = BlockList.get_filename() + with open(filename, 'w') as f: f.write(content) + + class Url: diff --git a/kaikout/xmpp/chat.py b/kaikout/xmpp/chat.py index e23b1eb..0381595 100644 --- a/kaikout/xmpp/chat.py +++ b/kaikout/xmpp/chat.py @@ -260,7 +260,7 @@ class XmppChat: else: response = str(self.settings[room]['action']) case _ if command_lowercase.startswith('allow +'): - value = command[7:] + value = command[7:].strip() if value: response = XmppCommands.set_filter( self, room, db_file, value, 'allow', True) @@ -268,7 +268,7 @@ class XmppChat: response = ('No action has been taken. ' 'Missing keywords.') case _ if command_lowercase.startswith('allow -'): - value = command[7:] + value = command[7:].strip() if value: response = XmppCommands.set_filter( self, room, db_file, value, 'allow', False) @@ -386,6 +386,22 @@ class XmppChat: await XmppCommands.muc_leave(self, room) else: response = 'This command is valid in groupchat only.' + case _ if command_lowercase.startswith('ignore +'): + value = command[8:].strip() + if value: + response = XmppCommands.set_filter( + self, room, db_file, value, 'rtbl_ignore', True) + else: + response = ('No action has been taken. ' + 'Missing Jabber IDs.') + case _ if command_lowercase.startswith('ignore -'): + value = command[8:].strip() + if value: + response = XmppCommands.set_filter( + self, room, db_file, value, 'rtbl_ignore', False) + else: + response = ('No action has been taken. ' + 'Missing Jabber IDs.') case 'inactivity off': XmppCommands.update_setting_value( self, room, db_file, 'check_inactivity', 0) @@ -537,4 +553,4 @@ class XmppChat: if room in self.settings and self.settings[room]['finished']: response_finished = ('Finished. Total time: {}s' .format(command_time_total)) - XmppMessage.send_reply(self, message, response_finished) \ No newline at end of file + XmppMessage.send_reply(self, message, response_finished) diff --git a/kaikout/xmpp/client.py b/kaikout/xmpp/client.py index 7ef1a0e..bf42a84 100644 --- a/kaikout/xmpp/client.py +++ b/kaikout/xmpp/client.py @@ -3,11 +3,10 @@ import asyncio from datetime import datetime -import slixmpp from kaikout.about import Documentation from kaikout.database import Toml from kaikout.log import Logger -from kaikout.utilities import Config, Log +from kaikout.utilities import Config, Log, BlockList from kaikout.xmpp.bookmark import XmppBookmark from kaikout.xmpp.chat import XmppChat from kaikout.xmpp.commands import XmppCommands @@ -17,6 +16,7 @@ from kaikout.xmpp.moderation import XmppModeration from kaikout.xmpp.muc import XmppMuc from kaikout.xmpp.pubsub import XmppPubsub from kaikout.xmpp.status import XmppStatus +import slixmpp import time # time_now = datetime.now() @@ -53,10 +53,13 @@ class XmppClient(slixmpp.ClientXMPP): self.reconnect_timeout = Config.get_values('accounts.toml', 'xmpp')['settings']['reconnect_timeout'] # A handler for operators. self.operators = Config.get_values('accounts.toml', 'xmpp')['operators'] - # A handler for settings. - self.settings = {} + # A handler for blocklist. + #self.blocklist = {} + BlockList.load_blocklist(self) # A handler for sessions. self.sessions = {} + # A handler for settings. + self.settings = {} # A handler for tasks. self.tasks = {} # Register plugins. @@ -267,14 +270,15 @@ class XmppClient(slixmpp.ClientXMPP): jid = presence['muc']['jid'] from hashlib import sha256 jid_to_sha256 = sha256(jid.bare.encode('utf-8')).hexdigest() - rtbl_jid_full = 'xmppbl.org' - rtbl_node_id = 'muc_bans_sha256' - rtbl_list = await XmppPubsub.get_items(self, rtbl_jid_full, rtbl_node_id) - for item in rtbl_list['pubsub']['items']: - if jid_to_sha256 == item['id']: - reason = 'Jabber ID has been marked by RTBL.' - await XmppCommands.devoice(self, room, alias, reason) - break + for jid in self.blocklist['entries']: + if jid not in self.settings[room]['rtbl_ignore']: + for node in self.blocklist['entries'][jid]: + for item_id in self.blocklist['entries'][jid][node]: + if jid_to_sha256 == item_id: + reason = 'Jabber ID has been marked by RTBL: Publisher: {}; Node: {}.'.format( + jid, node) + await XmppCommands.devoice(self, room, alias, reason) + break # message_body = 'Greetings {} and welcome to groupchat {}'.format(alias, room) # XmppMessage.send(self, jid.bare, message_body, 'chat') # Send MUC-PM in case there is no indication for reception of 1:1 @@ -477,14 +481,32 @@ class XmppClient(slixmpp.ClientXMPP): """ # self.command_list() # await self.get_roster() - subscriptions_of_node = await self['xep_0060'].get_node_subscriptions("xmppbl.org", "muc_bans_sha256") - print() - print('=== subscriptions_of_node ===') - print() - print(subscriptions_of_node) - print() - print('=== subscriptions_of_node ===') - print() + rtbl_sources = Config.get_values('rtbl.toml')['sources'] + for source in rtbl_sources: + jabber_id = source['jabber_id'] + node_id = source['node_id'] + subscribe = await XmppPubsub.subscribe(self, jabber_id, node_id) + if subscribe['pubsub']['subscription']['subscription'] == 'subscribed': + rtbl_list = await XmppPubsub.get_items(self, jabber_id, node_id) + rtbl_items = rtbl_list['pubsub']['items'] + for item in rtbl_items: + exist = False + item_id = item['id'] + for jid in self.blocklist['entries']: + for node in jid: + for item in node: + if item_id == item: + exist = True + break + if not exist: + # TODO Extract items item_payload.find(namespace + 'title') + # NOTE (Pdb) + # for i in item['payload'].iter(): i.attrib + # {'reason': 'urn:xmpp:reporting:abuse'} + BlockList.add_entry_to_blocklist(self, jabber_id, node_id, item_id) +# subscribe['from'] = xmppbl.org +# subscribe['pubsub']['subscription']['node'] = 'muc_bans_sha256' + subscriptions = await XmppPubsub.get_node_subscriptions(self, jabber_id, node_id) await self['xep_0115'].update_caps() bookmarks = await XmppBookmark.get_bookmarks(self) print(bookmarks) diff --git a/kaikout/xmpp/pubsub.py b/kaikout/xmpp/pubsub.py index 3396f52..1db550a 100644 --- a/kaikout/xmpp/pubsub.py +++ b/kaikout/xmpp/pubsub.py @@ -9,6 +9,7 @@ Functions create_node and create_entry are derived from project atomtopubsub. import hashlib from kaikout.log import Logger +from slixmpp.exceptions import IqTimeout, IqError logger = Logger(__name__) @@ -66,3 +67,16 @@ class XmppPubsub: async def get_items(self, jid, node): items = await self.plugin['xep_0060'].get_items(jid, node) return items + + + async def subscribe(self, jid, node): + result = await self['xep_0060'].subscribe(jid, node) + return result + + + async def get_node_subscriptions(self, jid, node): + try: + subscriptions = await self['xep_0060'].get_node_subscriptions(jid, node) + return subscriptions + except IqError as e: + print(e)