Compare commits

..

13 Commits

Author SHA1 Message Date
braph
98e9efe682 re-inserted mutex code 2016-09-06 18:25:05 +02:00
braph
abb6494acf xchoose: added further error handling 2016-09-06 13:23:46 +02:00
braph
8ab4e8cdd4 added xchoose command 2016-09-05 23:53:30 +02:00
Thorsten
d92a177aa8 mutex for events, re-enable dsa-watcher 2016-09-05 23:39:34 +02:00
Thorsten
bddc3034d5 put a show on the pancake on its head 2016-09-05 20:30:15 +02:00
Thorsten
3b506d737c put a pancake on its head 2016-09-05 20:07:47 +02:00
Thorsten S
f00555de90 put a pancake on its head 2016-08-21 15:53:27 +02:00
Thorsten S
c3f5371fe3 put a pancake on its head 2016-08-21 15:39:43 +02:00
Thorsten
e0385a7db3 second iteration of morse code plugin, provided by braph 2016-08-13 02:32:08 +02:00
Thorsten
cde081dad2 who let the bots out? 2016-08-13 02:31:45 +02:00
Thorsten
492ca7a7c8 spellcheck 2016-07-18 20:16:40 +02:00
Thorsten
877de9b5c3 fix punctuation 2016-07-08 21:19:07 +02:00
Thorsten
f8373a61c4 decisive bot 2016-07-08 21:17:08 +02:00
6 changed files with 359 additions and 14 deletions

View File

@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import logging
import time import time
import sched import sched
import threading import threading
@@ -8,7 +9,7 @@ EVENTLOOP_DELAY = 0.100 # seconds
event_list = sched.scheduler(time.time, time.sleep) 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 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) action = callback(*func_args)
if action: if action:
action_runner(action=action, plugin=plugin, msg_obj=msg_obj) 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) event_list.enterabs(t, 0, callback, args)

View File

@@ -20,6 +20,7 @@ class IdleBot(ClientXMPP):
self.add_event_handler('session_start', self.session_start) self.add_event_handler('session_start', self.session_start)
self.add_event_handler('groupchat_message', self.muc_message) self.add_event_handler('groupchat_message', self.muc_message)
self.add_event_handler('disconnected', self.disconnected) self.add_event_handler('disconnected', self.disconnected)
self.add_event_handler('presence_error', self.disconnected)
self.priority = 0 self.priority = 0
self.status = None self.status = None
self.show = None self.show = None
@@ -29,7 +30,7 @@ class IdleBot(ClientXMPP):
self.add_event_handler('muc::%s::got_offline' % room, self.muc_offline) self.add_event_handler('muc::%s::got_offline' % room, self.muc_offline)
def disconnected(self, _): def disconnected(self, _):
exit(0) self.disconnect(wait=True)
def session_start(self, _): def session_start(self, _):
self.get_roster() self.get_roster()
@@ -81,8 +82,7 @@ class IdleBot(ClientXMPP):
""" """
disconnect and exit disconnect and exit
""" """
self.disconnect() self.disconnect(wait=True)
sys.exit(1)
def start(botclass, active=False): def start(botclass, active=False):

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import logging import logging
import re
import events import events
import json import json
import random import random
@@ -279,15 +280,158 @@ 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) @pluginfunction('choose', 'chooses randomly between arguments', ptypes.COMMAND, ratelimit_class=RATE_INTERACTIVE)
def command_choose(argv, **args): def command_choose(argv, **args):
alternatives = argv alternatives = argv
binary = ['Yes', 'No', 'Maybe'] 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 # single or no choice
if len(alternatives) < 2: if len(alternatives) < 2:
return { return {
'msg': '{}: {}.'.format(args['reply_user'], random.choice(binary)) 'msg': '{}: {}'.format(args['reply_user'], random.choice(weighted_choice(binary)))
} }
elif 'choose' not in alternatives: elif 'choose' not in alternatives:
choice = random.choice(alternatives) choice = random.choice(alternatives)
@@ -302,14 +446,14 @@ def command_choose(argv, **args):
for item in options: for item in options:
if item == 'choose': if item == 'choose':
if len(current_choices) < 2: if len(current_choices) < 2:
responses.append(random.choice(binary)) responses.append(random.choice(weighted_choice(binary)))
else: else:
responses.append(random.choice(current_choices)) responses.append(random.choice(current_choices))
current_choices = [] current_choices = []
else: else:
current_choices.append(item) current_choices.append(item)
if len(current_choices) < 2: if len(current_choices) < 2:
responses.append(random.choice(binary)) responses.append(random.choice(weighted_choice(binary)))
else: else:
responses.append(random.choice(current_choices)) responses.append(random.choice(current_choices))
return responses return responses
@@ -350,7 +494,8 @@ def command_teatimer(argv, **args):
), ),
'event': { 'event': {
'time': ready, 'time': ready,
'msg': (args['reply_user'] + ': Your tea is ready!') 'msg': (args['reply_user'] + ': Your tea is ready!'),
'mutex': 'teatimer_{}'.format(args['reply_user'])
} }
} }
@@ -616,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)) msg = 'next crawl set to %s' % time.strftime('%Y-%m-%d %H:%M', time.localtime(crawl_at))
out.append(msg) out.append(msg)
return { return {
# 'msg': out,
'event': { 'event': {
'time': crawl_at, 'time': crawl_at,
'command': (command_dsa_watcher, ([],)) 'command': (command_dsa_watcher, ([],)),
'mutex': 'dsa'
} }
} }

176
plugins/morse.py Normal file
View File

@@ -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 = """
B · · ·
C · ·
D · ·
F· · ·
G ·
H· · · ·
I· ·
K ·
· ·
M
N ·
O
·
Q ·
·
S· · ·
T
U· ·
V· · ·
X · ·
Y ·
Z · ·
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 <string>"
}
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 <string>"
}
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)
}

View File

@@ -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) @pluginfunction('debbug', 'parse Debian bug numbers', ptypes.PARSE, ratelimit_class=RATE_NO_SILENCE | RATE_GLOBAL)
def parse_debbug(**args): def parse_debbug(**args):
bugs = re.findall(r'#(\d{4,})', args['data']) bugs = re.findall(r'#(\d{4,})', args['data'])

View File

@@ -12,7 +12,9 @@ from collections import deque
from lxml import etree from lxml import etree
import requests import requests
from sleekxmpp.plugins import PluginNotFound
import plugins # force initialization
from plugin_system import plugin_storage, ptypes, plugin_enabled_get 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 from rate_limit import rate_limit_classes, RATE_GLOBAL, RATE_CHAT, RATE_EVENT, rate_limit
@@ -44,6 +46,9 @@ class UrlBot(IdleBot):
for room in self.rooms: for room in self.rooms:
self.add_event_handler('muc::%s::got_online' % room, self.muc_online) 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): def muc_message(self, msg_obj):
""" """
Handle muc messages, return if irrelevant content or die by hangup. Handle muc messages, return if irrelevant content or die by hangup.
@@ -109,7 +114,10 @@ class UrlBot(IdleBot):
other_bots = config.runtimeconf_get("other_bots") other_bots = config.runtimeconf_get("other_bots")
if not other_bots: if not other_bots:
return False return False
try:
users = self.plugin['xep_0045'].getRoster(room) users = self.plugin['xep_0045'].getRoster(room)
except PluginNotFound:
users = []
return set(users).intersection(set(other_bots)) return set(users).intersection(set(other_bots))
def _prevent_panic(message, room): def _prevent_panic(message, room):
@@ -349,7 +357,8 @@ class UrlBot(IdleBot):
args=command[1], args=command[1],
action_runner=self._run_action, action_runner=self._run_action,
plugin=plugin, 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): if 'msg' in action and rate_limit(RATE_CHAT | plugin.ratelimit_class):