diff --git a/common.py b/common.py index 74a2755..3323606 100644 --- a/common.py +++ b/common.py @@ -5,6 +5,7 @@ import logging import re import requests from urllib.error import URLError +import sleekxmpp BUFSIZ = 8192 USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64; rv:31.0) ' \ @@ -96,11 +97,24 @@ def giphy(subject, api_key): def get_nick_from_object(message_obj): - """ - not quite correct yet, also the private property access isn't nice. - """ - nick = message_obj['mucnick'] or message_obj['from']._jid[2] - return nick + + if isinstance(message_obj, sleekxmpp.Message): + msg_type = message_obj.getType() + + if msg_type == "groupchat": + return message_obj.getMucnick() + elif msg_type == "chat": + jid = message_obj.getFrom() + return jid.resource + else: + raise Exception("Message, but not groupchat/chat") + + elif isinstance(message_obj, sleekxmpp.Presence): + jid = message_obj.getFrom() + return jid.resource + + else: + raise Exception("Message type is: " + str(type(message_obj))) def else_command(args): diff --git a/deploy/deploy.yml b/deploy/deploy.yml index 712ed3c..4aa719b 100644 --- a/deploy/deploy.yml +++ b/deploy/deploy.yml @@ -20,6 +20,7 @@ vars: - botrepo: http://aero2k.de/t/repos/urlbot-native.git - pypi_mirror: http://pypi.fcio.net/simple/ + - systemd: true tasks: - include_vars: credentials.yml tags: [render_config] @@ -27,6 +28,7 @@ shell: virtualenv -p python3 --system-site-packages ~/botenv creates=~/botenv/bin/activate - name: virtualenv for supervisord shell: virtualenv -p python2 ~/svenv creates=~/svenv/bin/activate + when: not systemd - name: clone repository git: repo="{{botrepo}}" dest=~/urlbot force=yes update=yes register: source_code @@ -34,6 +36,7 @@ pip: requirements="~/urlbot/requirements.txt" virtualenv=~/botenv extra_args="-i {{pypi_mirror}}" - name: install supervisor pip: name=supervisor virtualenv=~/svenv extra_args="-i {{pypi_mirror}}" + when: not systemd - name: set configuration lineinfile: dest=~/urlbot/local_config.ini create=yes line="{{item.key}} = {{item.value}}" regexp="^{{item.key}}.=" @@ -60,29 +63,94 @@ - name: create supervisor config copy: src=supervisord.conf dest=~/supervisord.conf register: supervisord + when: not systemd + + - name: create directory for systemd unit file + shell: mkdir -p ~/.config/systemd/user/ creates=~/.config/systemd/user/ + when: systemd + + - name: create unitfile + copy: src=urlbug@.service dest=~/.config/systemd/user/urlbug@.service + when: systemd + register: unitfile + + # crapshit does not work + - name: reload unitfiles + become: true + shell: systemctl daemon-reload + when: unitfile.changed + ignore_errors: true + + - name: enable services + shell: "systemctl --user enable urlbug@{{item}}.service" + with_items: + - idlebot + - urlbot + when: systemd - name: verify supervisor running shell: nc -z 127.0.0.1 9004; echo $? executable=/bin/bash register: supervisor_running changed_when: false + when: not systemd - name: start supervisord shell: source ~/svenv/bin/activate && supervisord executable=/bin/bash register: start_supervisor - when: supervisord.changed or supervisor_running.stdout == "1" + when: + - not systemd + - supervisord.changed or supervisor_running.stdout == "1" #changed_when: "'already listening' not in start_supervisor.stdout" - name: activate supervisord changes - when: supervisord.changed shell: source ~/svenv/bin/activate && supervisorctl reload executable=/bin/bash + when: + - not systemd + - supervisord.changed - name: idlebot started supervisorctl: name=idlebot state=restarted supervisorctl_path=~/svenv/bin/supervisorctl - when: (source_code.changed or urlbot_config.changed) and not supervisord.changed + when: + - not systemd + - (source_code.changed or urlbot_config.changed) and not supervisord.changed - - pause: seconds=30 - when: (source_code.changed or urlbot_config.changed) and not supervisord.changed + # following tasks are workaround for missing ansible systemd-user support + - name: get systemd unit status + shell: systemctl --user status urlbug.slice + register: systemd_unit_status + + - debug: var=systemd_unit_status + - debug: msg="{{'{{item}}.service' not in systemd_unit_status.stdout}}" + with_items: + - idlebot + - urlbot + + - name: bots started + shell: "systemctl --user start urlbug@{{item}}.service && sleep 20" + with_items: + - idlebot + - urlbot + when: systemd and '{{item}}.service' not in systemd_unit_status.stdout + register: started_bots + + - debug: var=started_bots + + - name: bots restarted + shell: "systemctl --user restart urlbug@{{item}}.service && sleep 10" + with_items: + - idlebot + - urlbot + when: + - systemd + - source_code.changed or urlbot_config.changed + + - pause: seconds=20 + when: + - not systemd + - (source_code.changed or urlbot_config.changed) and not supervisord.changed - name: urlbot started supervisorctl: name=bot state=restarted supervisorctl_path=~/svenv/bin/supervisorctl - when: (source_code.changed or urlbot_config.changed) and not supervisord.changed + when: + - not systemd + - (source_code.changed or urlbot_config.changed) and not supervisord.changed diff --git a/deploy/hosts b/deploy/hosts index d7902ca..23a01b2 100644 --- a/deploy/hosts +++ b/deploy/hosts @@ -1,2 +1,2 @@ [bots] -aero2k.de ansible_host=2a01:4f8:d16:130c::2 +aero2k.de ansible_host=2a01:4f8:d16:130c::2 ansible_become_method=su diff --git a/deploy/requirements-deploy.yml b/deploy/requirements-deploy.yml new file mode 100644 index 0000000..7547399 --- /dev/null +++ b/deploy/requirements-deploy.yml @@ -0,0 +1,2 @@ +ansible +markupsafe diff --git a/deploy/urlbug@.service b/deploy/urlbug@.service new file mode 100644 index 0000000..7c8f923 --- /dev/null +++ b/deploy/urlbug@.service @@ -0,0 +1,12 @@ +[Unit] +Description=jabber bot entertaining and supporting activity on jabber MUCs + +[Service] +ExecStart=/home/jabberbot/botenv/bin/python3 /home/jabberbot/urlbot/%i.py +WorkingDirectory=/home/jabberbot/urlbot/ +StandardOutput=journal+console +StandardError=journal+console +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/events.py b/events.py index 13f122f..a00cf63 100644 --- a/events.py +++ b/events.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import logging import time import sched import threading @@ -8,7 +9,7 @@ EVENTLOOP_DELAY = 0.100 # seconds event_list = sched.scheduler(time.time, time.sleep) -def register_active_event(t, callback, args, action_runner, plugin, msg_obj): +def register_active_event(t, callback, args, action_runner, plugin, msg_obj, mutex=None): """ Execute a callback at a given time and react on the output @@ -24,10 +25,14 @@ def register_active_event(t, callback, args, action_runner, plugin, msg_obj): action = callback(*func_args) if action: action_runner(action=action, plugin=plugin, msg_obj=msg_obj) - event_list.enterabs(t, 0, func, args) + register_event(t, func, args, mutex=mutex) -def register_event(t, callback, args): +def register_event(t, callback, args, **kwargs): + for pending_event in event_list.queue: + if kwargs.get('mutex') and pending_event.kwargs.get('mutex', None) == kwargs.get('mutex'): + logging.debug("Dropped event: %s", kwargs.get('mutex')) + return event_list.enterabs(t, 0, callback, args) diff --git a/idlebot.py b/idlebot.py index b3c7535..840ae56 100755 --- a/idlebot.py +++ b/idlebot.py @@ -20,6 +20,8 @@ class IdleBot(ClientXMPP): self.add_event_handler('session_start', self.session_start) self.add_event_handler('groupchat_message', self.muc_message) self.add_event_handler('disconnected', self.disconnected) + self.add_event_handler('presence_error', self.disconnected) + self.add_event_handler('session_end', self.disconnected) self.priority = 0 self.status = None self.show = None @@ -29,7 +31,8 @@ class IdleBot(ClientXMPP): self.add_event_handler('muc::%s::got_offline' % room, self.muc_offline) def disconnected(self, _): - exit(0) + self.logger.warn("Disconnected! dbg: {}".format(str(_))) + self.disconnect(wait=True) def session_start(self, _): self.get_roster() @@ -81,8 +84,7 @@ class IdleBot(ClientXMPP): """ disconnect and exit """ - self.disconnect() - sys.exit(1) + self.disconnect(wait=True) def start(botclass, active=False): @@ -105,6 +107,8 @@ def start(botclass, active=False): bot.connect() bot.register_plugin('xep_0045') + bot.register_plugin('xep_0199', {'keepalive': True}) + bot.register_plugin('xep_0308') bot.process() config.runtimeconf_set('start_time', -time.time()) diff --git a/plugins/cake.py b/plugins/cake.py index 9d9e295..4a4df59 100644 --- a/plugins/cake.py +++ b/plugins/cake.py @@ -6,28 +6,48 @@ from plugin_system import pluginfunction, ptypes from rate_limit import RATE_FUN, RATE_GLOBAL +def give_item(user, item_name, search_word=None): + if not search_word: + search_word = item_name + return {'msg': '{} for {}: {}'.format(item_name, user, giphy(search_word, 'dc6zaTOxFJmzC'))} + + +def cake_excuse(user): + return { + 'msg': '{}: {}'.format(user, random.choice(cakes)) + } + + @pluginfunction('cake', 'displays a cake ASCII art', ptypes.COMMAND, ratelimit_class=RATE_FUN | RATE_GLOBAL) def command_cake(argv, **args): if {'please', 'bitte'}.intersection(set(argv)): - return { - 'msg': 'cake for {}: {}'.format(args['reply_user'], giphy('cake', 'dc6zaTOxFJmzC')) - } - - return { - 'msg': args['reply_user'] + ': %s' % random.choice(cakes) - } + return give_item(args['reply_user'], 'cake') + else: + return cake_excuse(args['reply_user']) @pluginfunction('keks', 'keks!', ptypes.COMMAND, ratelimit_class=RATE_FUN | RATE_GLOBAL) def command_cookie(argv, **args): if {'please', 'bitte'}.intersection(set(argv)): - return { - 'msg': 'keks für {}: {}'.format(args['reply_user'], giphy('cookie', 'dc6zaTOxFJmzC')) - } + return give_item(args['reply_user'], 'keks', 'cookie') + else: + return cake_excuse(args['reply_user']) - return { - 'msg': args['reply_user'] + ': %s' % random.choice(cakes) - } + +@pluginfunction('schnitzel', 'schnitzel!', ptypes.COMMAND, ratelimit_class=RATE_FUN | RATE_GLOBAL) +def command_schnitzel(argv, **args): + if {'please', 'bitte'}.intersection(set(argv)): + return give_item(args['reply_user'], 'schnitzel') + else: + return cake_excuse(args['reply_user']) + + +@pluginfunction('kaffee', 'kaffee!', ptypes.COMMAND, ratelimit_class=RATE_FUN | RATE_GLOBAL) +def command_coffee(argv, **args): + if {'please', 'bitte'}.intersection(set(argv)): + return give_item(args['reply_user'], 'kaffee', 'coffee') + else: + return cake_excuse(args['reply_user']) cakes = [ @@ -46,4 +66,3 @@ cakes = [ "I'm going to kill you, and all the cake is gone.", "Who's gonna make the cake when I'm gone? You?" ] - diff --git a/plugins/commands.py b/plugins/commands.py index 2193bfe..4a4e9b1 100644 --- a/plugins/commands.py +++ b/plugins/commands.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import logging +import re import events import json import random @@ -15,6 +16,7 @@ import config from common import VERSION from rate_limit import RATE_FUN, RATE_GLOBAL, RATE_INTERACTIVE, RATE_NO_SILENCE, RATE_NO_LIMIT from plugin_system import pluginfunction, ptypes, plugin_storage, plugin_enabled_get, plugin_enabled_set + log = logging.getLogger(__name__) @@ -92,7 +94,6 @@ def command_plugin_activation(argv, **args): @pluginfunction('list', 'list plugin and parser status', ptypes.COMMAND) def command_list(argv, **args): - log.info('list plugin called') if 'enabled' in argv and 'disabled' in argv: @@ -279,18 +280,187 @@ def command_dice(argv, **args): } +@pluginfunction('xchoose', 'chooses randomly between nested option groups', ptypes.COMMAND, ratelimit_class=RATE_INTERACTIVE) +def command_xchoose(argv, **args): + + class ChooseTree(): + def __init__(self, item=None): + self.item = item + self.tree = None + self.closed = False + + # opening our root node + if self.item is None: + self.open() + + def open(self): + if self.tree is None: + self.tree = [] + elif self.closed: + raise Exception("cannot re-open group for item '%s'" % (self.item)) + + def close(self): + if self.tree is None: + raise Exception("close on unopened bracket") + elif len(self.tree) == 0: + raise Exception("item '%s' has a group without sub options" % (self.item)) + else: + self.closed = True + + def last(self): + return self.tree[-1] + + def choose(self): + if self.item: + yield self.item + + if self.tree: + sel = random.choice(self.tree) + for sub in sel.choose(): + yield sub + + def add(self, item): + self.tree.append( ChooseTree(item) ) + + # because of error handling we're nesting this function here + def xchoose(line): + item = '' + quote = None + choose_tree = ChooseTree() + choose_stack = [ choose_tree ] + bracket_stack = [] + + for pos, c in enumerate(line, 1): + try: + if quote: + if c == quote: + quote = None + else: + item += c + + elif c == ' ': + if item: + choose_stack[-1].add(item) + item = '' + + elif c in ('(', '[', '{', '<'): + if item: + choose_stack[-1].add(item) + item = '' + + try: + last = choose_stack[-1].last() + last.open() + choose_stack.append(last) + bracket_stack.append(c) + except IndexError: + raise Exception("cannot open group without preceding option") + + elif c in (')', ']', '}', '>'): + if not bracket_stack: + raise Exception("missing leading bracket for '%s'" % (c)) + + opening_bracket = bracket_stack.pop(-1) + wanted_closing_bracket = { '(':')', '[':']', '{':'}', '<':'>' }[opening_bracket] + if c != wanted_closing_bracket: + raise Exception("bracket mismatch, wanted bracket '%s' but got '%s'" % ( + wanted_closing_bracket, c)) + + if item: + choose_stack[-1].add(item) + item = '' + + choose_stack[-1].close() + choose_stack.pop(-1) + + elif c in ('"', "'"): + quote = c + + else: + item += c + + except Exception as e: + raise Exception("%s (at pos %d)" % (e, pos)) + + if bracket_stack: + raise Exception("missing closing bracket for '%s'" % (bracket_stack[-1])) + + if quote: + raise Exception("missing closing quote (%s)" % (quote)) + + if item: + choose_stack[-1].add(item) + + return ' '.join(choose_tree.choose()) + + + # start of command_xchoose + line = re.sub('.*xchoose *', '', args['data']) + if not line: + return { + 'msg': '%s: %s' % (args['reply_user'], 'missing options') + } + try: + return { + 'msg': '%s: %s' % (args['reply_user'], xchoose(line)) + } + except Exception as e: + return { + 'msg': '%s: %s' % (args['reply_user'], str(e)) + } + + @pluginfunction('choose', 'chooses randomly between arguments', ptypes.COMMAND, ratelimit_class=RATE_INTERACTIVE) def command_choose(argv, **args): alternatives = argv + binary = ( + (('Yes.', 'Yeah!', 'Ok!', 'Aye!', 'Great!'), 4), + (('No.', 'Naah..', 'Meh.', 'Nay.', 'You stupid?'), 4), + (('Maybe.', 'Dunno.', 'I don\'t care.'), 2) + ) + + def weighted_choice(choices): + total = sum(w for c, w in choices) + r = random.uniform(0, total) + upto = 0 + for c, w in choices: + if upto + w >= r: + return c + upto += w + + # single or no choice if len(alternatives) < 2: return { - 'msg': '{}: {}.'.format(args['reply_user'], random.choice(['Yes', 'No'])) + 'msg': '{}: {}'.format(args['reply_user'], random.choice(weighted_choice(binary))) + } + elif 'choose' not in alternatives: + choice = random.choice(alternatives) + return { + 'msg': '%s: I prefer %s!' % (args['reply_user'], choice) } - choice = random.choice(alternatives) - log.info('sent random choice') + def choose_between(options): + responses = [] + current_choices = [] + + for item in options: + if item == 'choose': + if len(current_choices) < 2: + responses.append(random.choice(weighted_choice(binary))) + else: + responses.append(random.choice(current_choices)) + current_choices = [] + else: + current_choices.append(item) + if len(current_choices) < 2: + responses.append(random.choice(weighted_choice(binary))) + else: + responses.append(random.choice(current_choices)) + return responses + + log.info('sent multiple random choices') return { - 'msg': '%s: I prefer %s!' % (args['reply_user'], choice) + 'msg': '%s: My choices are: %s!' % (args['reply_user'], ', '.join(choose_between(alternatives))) } @@ -324,7 +494,8 @@ def command_teatimer(argv, **args): ), 'event': { 'time': ready, - 'msg': (args['reply_user'] + ': Your tea is ready!') + 'msg': (args['reply_user'] + ': Your tea is ready!'), + 'mutex': 'teatimer_{}'.format(args['reply_user']) } } @@ -564,8 +735,8 @@ def command_dsa_watcher(argv=None, **_): for dsa_about in reversed(dsa_about_list): dsa_id = get_id_from_about_string(dsa_about) title = xmldoc.xpath( - '//purl:item[@rdf:about="{}"]/purl:title/text()'.format(dsa_about), - namespaces=nsmap + '//purl:item[@rdf:about="{}"]/purl:title/text()'.format(dsa_about), + namespaces=nsmap )[0] if after and dsa_id <= after: continue @@ -590,9 +761,11 @@ def command_dsa_watcher(argv=None, **_): msg = 'next crawl set to %s' % time.strftime('%Y-%m-%d %H:%M', time.localtime(crawl_at)) out.append(msg) return { + # 'msg': out, 'event': { 'time': crawl_at, - 'command': (command_dsa_watcher, ([],)) + 'command': (command_dsa_watcher, ([],)), + 'mutex': 'dsa' } } @@ -621,8 +794,9 @@ def remove_from_botlist(argv, **args): return False -@pluginfunction("add-to-botlist", "add a user to the botlist", ptypes.COMMAND) +@pluginfunction("add-to-botlist", "add a user to the botlist", ptypes.COMMAND, enabled=False) def add_to_botlist(argv, **args): + return {'msg': 'feature disabled until channel separation'} if not argv: return {'msg': "wrong number of arguments!"} suspect = argv[0] @@ -707,35 +881,6 @@ def reload_runtimeconfig(argv, **args): return {'msg': 'done'} -@pluginfunction('snitch', "tell on a spammy user", ptypes.COMMAND) -def ignore_user(argv, **args): - if not argv: - return {'msg': 'syntax: "{}: snitch username"'.format(config.conf_get("bot_nickname"))} - - then = time.time() + 15 * 60 - spammer = argv[0] - - if spammer == config.conf_get("bot_owner"): - return { - 'msg': 'My owner does not spam, he is just very informative.' - } - - if spammer not in config.runtime_config_store['spammers']: - config.runtime_config_store['spammers'].append(spammer) - - def unblock_user(user): - if user not in config.runtime_config_store['spammers']: - config.runtime_config_store['spammers'].append(user) - - return { - 'msg': 'user reported and ignored till {}'.format(time.strftime('%H:%M', time.localtime(then))), - 'event': { - 'time': then, - 'command': (unblock_user, ([spammer],)) - } - } - - @pluginfunction('search', 'search the web (using duckduckgo)', ptypes.COMMAND) def search_the_web(argv, **args): url = 'http://api.duckduckgo.com/' @@ -776,9 +921,10 @@ def raise_an_error(argv, **args): @pluginfunction('repeat', 'repeat the last message', ptypes.COMMAND) def repeat_message(argv, **args): - return { - 'msg': args['stack'][-1]['body'] - } + if args['stack']: + return { + 'msg': args['stack'][-1]['body'] + } @pluginfunction('isdown', 'check if a website is reachable', ptypes.COMMAND) diff --git a/plugins/comment_joins.py b/plugins/comment_joins.py index 9efda82..8e47e6d 100644 --- a/plugins/comment_joins.py +++ b/plugins/comment_joins.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- import logging - import time import random - import config from plugin_system import pluginfunction, ptypes + log = logging.getLogger(__name__) comment_joins_strings = [ @@ -17,7 +16,7 @@ comment_joins_strings = [ @config.config_locked def comment_joins(**args): # max elapsed time between the latest and the N latest join - timespan = 120 + timespan = 120 max_joins = 6 current_timestamp = int(time.time()) @@ -26,12 +25,12 @@ def comment_joins(**args): arg_user_key = arg_user.lower() if arg_user_key not in config.runtime_config_store['user_joins']: - config.runtime_config_store['user_joins'][arg_user_key] = [ current_timestamp ] + config.runtime_config_store['user_joins'][arg_user_key] = [current_timestamp] config.runtimeconf_persist() return None - user_joins = [] - + user_joins = [] + for ts in config.runtime_config_store['user_joins'][arg_user_key]: if current_timestamp - int(ts) <= timespan: user_joins.append(ts) @@ -42,7 +41,7 @@ def comment_joins(**args): config.runtime_config_store['user_joins'].pop(arg_user_key) config.runtimeconf_persist() log.info("send comment on join") - return { 'msg': random.choice(comment_joins_strings) % arg_user } + return {'msg': random.choice(comment_joins_strings) % arg_user} else: user_joins.append(current_timestamp) config.runtime_config_store['user_joins'][arg_user_key] = user_joins diff --git a/plugins/morse.py b/plugins/morse.py new file mode 100644 index 0000000..1874de1 --- /dev/null +++ b/plugins/morse.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- +import logging +import re + +from plugin_system import pluginfunction, ptypes +from rate_limit import RATE_FUN, RATE_GLOBAL + +log = logging.getLogger(__name__) + +# copy from https://de.wikipedia.org/wiki/Morsezeichen +raw_wiki_copy = """ +A· − +B− · · · +C− · − · +D− · · +E· +F· · − · +G− − · +H· · · · +I· · +J· − − − +K− · − +L· − · · +M− − +N− · +O− − − +P· − − · +Q− − · − +R· − · +S· · · +T− +U· · − +V· · · − +W· − − +X− · · − +Y− · − − +Z− − · · +1· − − − − +2· · − − − +3· · · − − +4· · · · − +5· · · · · +6− · · · · +7− − · · · +8− − − · · +9− − − − · +0− − − − − +""" + + +# machen dictionary aus wikipaste +def wiki_paste_to_morse_dict(wikicopy): + wikicopy = wikicopy.replace(' ', '') + morse_dict = {l[0]: l[1:] for l in wikicopy.splitlines() if l} + return morse_dict + + +ascii_morse = wiki_paste_to_morse_dict(raw_wiki_copy) +morse_ascii = {v: k for k, v in ascii_morse.items()} + + +# return a dictionary of possible morse-chars as key +# and their count as value +def possible_morse_chars(string): + """ + returns dit,dah or None + """ + stats = {} + + for c in re.sub("[\w\d ]", '', string): + try: + stats[c] += 1 + except KeyError: + stats[c] = 1 + + return stats + + +# return morse-encoded string +def morse_encode(string, dot='·', dash='−', sep=' ', ignore_unknown=False): + morse_codes = [] + + for char in string.upper(): + try: + morse_codes.append(ascii_morse[char].replace('·', dot).replace('−', dash)) + except KeyError: + if not ignore_unknown: + morse_codes.append(char) + + return sep.join(morse_codes) + + +# return morse-decoded string with number of errors as tuple +# -> (decoded string, num errors) +def morse_decode(string, dot=None, dash=None): + """ + decode a "morse string" to ascii text + uses \s{2,} as word separator + """ + # dot and dash given, just decode + if dot and dash: + errors = 0 + + words = [] + # drawback: does not allow single characters. + for match in re.finditer('([{dit}{dah}]+((?:\\s)[{dit}{dah}]+)+|\w+)'.format(dit=dot, dah=dash), string): + word = match.group() + log.debug("morse word: ", word) + if any([dot in word, dash in word]): + w = [] + for morse_character in word.split(): + try: + character = morse_ascii[morse_character.replace(dot, '·').replace(dash, '−')] + print("Converted \t{} \tto {}".format(morse_character, character)) + except KeyError: + character = morse_character + errors += 1 + w.append(character) + words.append(''.join(w)) + # words.append(''.join([morse_ascii[x.replace(dot, '·').replace(dash, '−')] for x in word.split()])) + else: + words.append(word) + return ' '.join(words), errors + + # dot/dash given, search for dash/dot + else: + if not dash: + dash_stats = {x: string.count(x) for x in '-−_'} + dash = max(dash_stats, key=dash_stats.get) + if not dot: + dot_stats = {x: string.count(x) for x in '.·*'} + dot = max(dot_stats, key=dot_stats.get) + + return morse_decode(string, dot=dot, dash=dash) + + +@pluginfunction('morse-encode', 'encode string to morse', ptypes.COMMAND, ratelimit_class=RATE_FUN | RATE_GLOBAL) +def command_morse_encode(argv, **args): + if not argv: + return { + 'msg': args['reply_user'] + "usage: morse-encode " + } + + if len(argv) == 1 and argv[0] == 'that': + message_stack = args['stack'] + if not message_stack[-1]: + return + message = message_stack[-1]['body'] + else: + message = ' '.join(argv) + + return { + 'msg': args['reply_user'] + ': %s' % morse_encode(message) + } + + +@pluginfunction('morse-decode', 'decode morse encoded string', ptypes.COMMAND, ratelimit_class=RATE_FUN | RATE_GLOBAL) +def command_morse_decode(argv, **args): + if not argv: + return { + 'msg': args['reply_user'] + "usage: morse-decode " + } + + if len(argv) == 1 and argv[0] == 'that': + message_stack = args['stack'] + if not message_stack[-1]: + return + message = message_stack[-1]['body'] + else: + message = ' '.join(argv) + + decoded, errors = morse_decode(message, dot='·', dash='-') + + return { + 'msg': args['reply_user'] + ': %s (%d errors)' % (decoded, errors) + } diff --git a/plugins/parsers.py b/plugins/parsers.py index 93712ee..54e56b5 100644 --- a/plugins/parsers.py +++ b/plugins/parsers.py @@ -39,6 +39,14 @@ def parse_mental_ill(**args): } +@pluginfunction('woof', '*puts sunglasses on*', ptypes.PARSE, ratelimit_class=RATE_NO_SILENCE | RATE_GLOBAL) +def command_woof(**args): + if 'who let the bots out' in args['data']: + return { + 'msg': 'beeep! beep! beep! beep! beep!' + } + + @pluginfunction('debbug', 'parse Debian bug numbers', ptypes.PARSE, ratelimit_class=RATE_NO_SILENCE | RATE_GLOBAL) def parse_debbug(**args): bugs = re.findall(r'#(\d{4,})', args['data']) @@ -50,14 +58,11 @@ def parse_debbug(**args): log.info('detected Debian bug #%s' % b) url = 'https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=%s' % b - status, title = extract_title(url) - if 0 == status: + title = extract_title(url) + + if title: out.append('Debian Bug: %s: %s' % (title, url)) - elif 3 == status: - out.append('error for #%s: %s' % (b, title)) - else: - log.info('unknown status %d' % status) return { 'msg': out @@ -122,32 +127,6 @@ def parse_slash_me(**args): } -@pluginfunction("recognize_bots", "got ya", ptypes.PARSE) -def recognize_bots(**args): - unique_standard_phrases = ( - 'independent bot and have nothing to do with other artificial intelligence systems', - 'new Debian Security Announce', - 'I\'m a bot (highlight me', - ) - - def _add_to_list(username, message): - if username not in config.runtime_config_store['other_bots']: - config.runtime_config_store['other_bots'].append(username) - config.runtimeconf_persist() - log.info("Adding {} to the list of bots (now {})".format(username, config.runtime_config_store['other_bots'])) - return { - 'event': { - 'time': time.time() + 3, - 'msg': message - } - } - - if any([phrase in args['data'] for phrase in unique_standard_phrases]): - return _add_to_list(args['reply_user'], 'Making notes...') - elif 'I\'ll be back' in args['data']: - return _add_to_list(args['reply_user'], 'Hey there, buddy!') - - @pluginfunction('resolve-url-title', 'extract titles from urls', ptypes.PARSE, ratelimit_class=RATE_URL) def resolve_url_title(**args): user = args['reply_user'] @@ -163,7 +142,7 @@ def resolve_url_title(**args): url_blacklist = config.runtime_config_store['url_blacklist'].values() out = [] - for url in result: + for url in result[:10]: if any([re.match(b, url) for b in url_blacklist]): log.info('url blacklist match for ' + url) break @@ -184,3 +163,10 @@ def resolve_url_title(**args): 'msg': out } + +@pluginfunction('doctor', 'parse doctor', ptypes.PARSE, ratelimit_class=RATE_FUN | RATE_GLOBAL) +def parse_doctor(**args): + if 'doctor' in args['data'].lower() or 'doktor' in args['data'].lower(): + return { + 'msg': 'ELIMINIEREN! ELIMINIEREN!' + } diff --git a/plugins/translate.py b/plugins/translate.py index 701faea..dcdbda4 100644 --- a/plugins/translate.py +++ b/plugins/translate.py @@ -22,6 +22,8 @@ def translate(argv, **args): if not api_key: return message_stack = args['stack'] + if not message_stack[-1]: + return last_message = message_stack[-1]['body'] data = { 'q': last_message, diff --git a/urlbot.py b/urlbot.py index a1f3a43..a89cd6a 100755 --- a/urlbot.py +++ b/urlbot.py @@ -7,10 +7,14 @@ import re import shlex import sys import time +from collections import deque + from lxml import etree import requests +from sleekxmpp.plugins import PluginNotFound +import plugins # force initialization from plugin_system import plugin_storage, ptypes, plugin_enabled_get from rate_limit import rate_limit_classes, RATE_GLOBAL, RATE_CHAT, RATE_EVENT, rate_limit @@ -34,7 +38,7 @@ class UrlBot(IdleBot): self.hist_ts = {p: [] for p in rate_limit_classes} self.hist_flag = {p: True for p in rate_limit_classes} - self.message_stack = [] + self.message_stack = {str(room): deque(maxlen=5) for room in self.rooms} self.add_event_handler('message', self.message) self.priority = 100 @@ -42,6 +46,9 @@ class UrlBot(IdleBot): for room in self.rooms: self.add_event_handler('muc::%s::got_online' % room, self.muc_online) + dsa_plugin = list(filter(lambda x: x.plugin_name == 'dsa-watcher', plugin_storage[ptypes.COMMAND]))[0] + self._run_action(dsa_plugin(), dsa_plugin, None) + def muc_message(self, msg_obj): """ Handle muc messages, return if irrelevant content or die by hangup. @@ -81,7 +88,7 @@ class UrlBot(IdleBot): request_counter = int(config.runtimeconf_get('request_counter')) config.runtimeconf_set('request_counter', request_counter + 1) - if str is not type(message): + if not isinstance(message, str): message = '\n'.join(message) def cached(function, ttl=60): @@ -107,7 +114,10 @@ class UrlBot(IdleBot): other_bots = config.runtimeconf_get("other_bots") if not other_bots: return False - users = self.plugin['xep_0045'].getRoster(room) + try: + users = self.plugin['xep_0045'].getRoster(room) + except PluginNotFound: + users = [] return set(users).intersection(set(other_bots)) def _prevent_panic(message, room): @@ -196,9 +206,8 @@ class UrlBot(IdleBot): except Exception as e: self.logger.exception(e) finally: - if len(self.message_stack) > 4: - self.message_stack.pop(0) - self.message_stack.append(msg_obj) + if msg_obj['from'].bare in self.rooms: + self.message_stack[msg_obj['from'].bare].append(msg_obj) def handle_muc_online(self, msg_obj): """ @@ -268,7 +277,7 @@ class UrlBot(IdleBot): reply_user=reply_user, msg_obj=msg_obj, argv=words[2:] if len(words) > 1 else [], - stack=self.message_stack + stack=self.message_stack.get(msg_obj['from'].bare, []) ) if ret: @@ -291,7 +300,7 @@ class UrlBot(IdleBot): if not plugin_enabled_get(plugin): continue - ret = plugin(reply_user=reply_user, data=data) + ret = plugin(reply_user=reply_user, data=data, sender=msg_obj['from']) if ret: self._run_action(ret, plugin, msg_obj) @@ -348,7 +357,8 @@ class UrlBot(IdleBot): args=command[1], action_runner=self._run_action, plugin=plugin, - msg_obj=msg_obj + msg_obj=msg_obj, + mutex=event.get('mutex') ) if 'msg' in action and rate_limit(RATE_CHAT | plugin.ratelimit_class):