all death to the tab character

This commit is contained in:
Thorsten
2015-11-30 19:17:40 +01:00
parent d94d62335f
commit a7b53d855a
7 changed files with 1328 additions and 1329 deletions

266
common.py
View File

@@ -26,195 +26,195 @@ USER_AGENT = '''Mozilla/5.0 (X11; Linux x86_64; rv:31.0) Gecko/20100101 Firefox/
basedir = '.'
if 2 == len(sys.argv):
basedir = sys.argv[1]
basedir = sys.argv[1]
def conf_save(obj):
with open(conf('persistent_storage'), 'wb') as fd:
return pickle.dump(obj, fd)
with open(conf('persistent_storage'), 'wb') as fd:
return pickle.dump(obj, fd)
def conf_load():
path = conf('persistent_storage')
if os.path.isfile(path):
with open(path, 'rb') as fd:
fd.seek(0)
return pickle.load(fd)
else:
return {}
path = conf('persistent_storage')
if os.path.isfile(path):
with open(path, 'rb') as fd:
fd.seek(0)
return pickle.load(fd)
else:
return {}
def conf_set(key, value):
blob = conf_load()
blob[key] = value
conf_save(blob)
blob = conf_load()
blob[key] = value
conf_save(blob)
def conf_get(key, default=None):
blob = conf_load()
return blob.get(key, default)
blob = conf_load()
return blob.get(key, default)
Bucket = namedtuple("BucketConfig", ["history", "period", "max_hist_len"])
buckets = {
# everything else
RATE_GLOBAL: Bucket(history=[], period=60, max_hist_len=10),
# bot writes with no visible stimuli
RATE_NO_SILENCE: Bucket(history=[], period=10, max_hist_len=5),
# interactive stuff like ping
RATE_INTERACTIVE: Bucket(history=[], period=30, max_hist_len=5),
# chitty-chat, master volume control
RATE_CHAT: Bucket(history=[], period=10, max_hist_len=5),
# reacting on URLs
RATE_URL: Bucket(history=[], period=10, max_hist_len=5),
# triggering events
RATE_EVENT: Bucket(history=[], period=60, max_hist_len=10),
# bot blames people, produces cake and entertains
RATE_FUN: Bucket(history=[], period=180, max_hist_len=5),
# everything else
RATE_GLOBAL: Bucket(history=[], period=60, max_hist_len=10),
# bot writes with no visible stimuli
RATE_NO_SILENCE: Bucket(history=[], period=10, max_hist_len=5),
# interactive stuff like ping
RATE_INTERACTIVE: Bucket(history=[], period=30, max_hist_len=5),
# chitty-chat, master volume control
RATE_CHAT: Bucket(history=[], period=10, max_hist_len=5),
# reacting on URLs
RATE_URL: Bucket(history=[], period=10, max_hist_len=5),
# triggering events
RATE_EVENT: Bucket(history=[], period=60, max_hist_len=10),
# bot blames people, produces cake and entertains
RATE_FUN: Bucket(history=[], period=180, max_hist_len=5),
}
rate_limit_classes = buckets.keys()
def rate_limit(rate_class=RATE_GLOBAL):
"""
Remember N timestamps,
if N[0] newer than now()-T then do not output, do not append.
else pop(0); append()
"""
Remember N timestamps,
if N[0] newer than now()-T then do not output, do not append.
else pop(0); append()
:param rate_class: the type of message to verify
:return: False if blocked, True if allowed
"""
if rate_class not in rate_limit_classes:
return all(rate_limit(c) for c in rate_limit_classes if c & rate_class)
:param rate_class: the type of message to verify
:return: False if blocked, True if allowed
"""
if rate_class not in rate_limit_classes:
return all(rate_limit(c) for c in rate_limit_classes if c & rate_class)
now = time.time()
bucket = buckets[rate_class]
logging.getLogger(__name__).debug("[ratelimit][bucket=%x][time=%s]%s" % (rate_class, now, bucket.history))
now = time.time()
bucket = buckets[rate_class]
logging.getLogger(__name__).debug("[ratelimit][bucket=%x][time=%s]%s" % (rate_class, now, bucket.history))
if len(bucket.history) >= bucket.max_hist_len and bucket.history[0] > (now - bucket.period):
# print("blocked")
return False
else:
if bucket.history and len(bucket.history) > bucket.max_hist_len:
bucket.history.pop(0)
bucket.history.append(now)
return True
if len(bucket.history) >= bucket.max_hist_len and bucket.history[0] > (now - bucket.period):
# print("blocked")
return False
else:
if bucket.history and len(bucket.history) > bucket.max_hist_len:
bucket.history.pop(0)
bucket.history.append(now)
return True
def rate_limited(max_per_second):
"""
very simple flow control context manager
:param max_per_second: how many events per second may be executed - more are delayed
:return:
"""
min_interval = 1.0 / float(max_per_second)
"""
very simple flow control context manager
:param max_per_second: how many events per second may be executed - more are delayed
:return:
"""
min_interval = 1.0 / float(max_per_second)
def decorate(func):
lasttimecalled = [0.0]
def decorate(func):
lasttimecalled = [0.0]
def ratelimitedfunction(*args, **kargs):
elapsed = time.clock() - lasttimecalled[0]
lefttowait = min_interval - elapsed
if lefttowait > 0:
time.sleep(lefttowait)
ret = func(*args, **kargs)
lasttimecalled[0] = time.clock()
return ret
def ratelimitedfunction(*args, **kargs):
elapsed = time.clock() - lasttimecalled[0]
lefttowait = min_interval - elapsed
if lefttowait > 0:
time.sleep(lefttowait)
ret = func(*args, **kargs)
lasttimecalled[0] = time.clock()
return ret
return ratelimitedfunction
return ratelimitedfunction
return decorate
return decorate
def get_version_git():
import subprocess
import subprocess
cmd = ['git', 'log', '--oneline', '--abbrev-commit']
cmd = ['git', 'log', '--oneline', '--abbrev-commit']
try:
p = subprocess.Popen(cmd, bufsize=1, stdout=subprocess.PIPE)
first_line = p.stdout.readline()
line_count = len(p.stdout.readlines()) + 1
try:
p = subprocess.Popen(cmd, bufsize=1, stdout=subprocess.PIPE)
first_line = p.stdout.readline()
line_count = len(p.stdout.readlines()) + 1
if 0 == p.wait():
# skip this 1st, 2nd, 3rd stuff and use always [0-9]th
return "version (Git, %dth rev) '%s'" % (
line_count, str(first_line.strip(), encoding='utf8')
)
else:
return "(unknown version)"
except:
return "cannot determine version"
if 0 == p.wait():
# skip this 1st, 2nd, 3rd stuff and use always [0-9]th
return "version (Git, %dth rev) '%s'" % (
line_count, str(first_line.strip(), encoding='utf8')
)
else:
return "(unknown version)"
except:
return "cannot determine version"
VERSION = get_version_git()
def fetch_page(url):
log = logging.getLogger(__name__)
log.info('fetching page ' + url)
try:
request = urllib.request.Request(url)
request.add_header('User-Agent', USER_AGENT)
response = urllib.request.urlopen(request)
html_text = response.read(BUFSIZ) # ignore more than BUFSIZ
response.close()
return 0, html_text, response.headers
except Exception as e:
log.warn('failed: %s' % e)
return 1, str(e), 'dummy'
log = logging.getLogger(__name__)
log.info('fetching page ' + url)
try:
request = urllib.request.Request(url)
request.add_header('User-Agent', USER_AGENT)
response = urllib.request.urlopen(request)
html_text = response.read(BUFSIZ) # ignore more than BUFSIZ
response.close()
return 0, html_text, response.headers
except Exception as e:
log.warn('failed: %s' % e)
return 1, str(e), 'dummy'
def extract_title(url):
log = logging.getLogger(__name__)
global parser
log = logging.getLogger(__name__)
global parser
if 'repo/urlbot-native.git' in url:
log.info('repo URL found: ' + url)
return 3, 'wee, that looks like my home repo!'
if 'repo/urlbot-native.git' in url:
log.info('repo URL found: ' + url)
return 3, 'wee, that looks like my home repo!'
log.info('extracting title from ' + url)
log.info('extracting title from ' + url)
(code, html_text, headers) = fetch_page(url)
(code, html_text, headers) = fetch_page(url)
if 1 == code:
return 3, 'failed: %s for %s' % (html_text, url)
if 1 == code:
return 3, 'failed: %s for %s' % (html_text, url)
if not html_text:
return -1, 'error'
if not html_text:
return -1, 'error'
charset = ''
if 'content-type' in headers:
log.debug('content-type: ' + headers['content-type'])
charset = ''
if 'content-type' in headers:
log.debug('content-type: ' + headers['content-type'])
if 'text/' != headers['content-type'][:len('text/')]:
return 1, headers['content-type']
if 'text/' != headers['content-type'][:len('text/')]:
return 1, headers['content-type']
charset = re.sub(
r'.*charset=(?P<charset>\S+).*',
r'\g<charset>', headers['content-type'], re.IGNORECASE
)
charset = re.sub(
r'.*charset=(?P<charset>\S+).*',
r'\g<charset>', headers['content-type'], re.IGNORECASE
)
if '' != charset:
try:
html_text = html_text.decode(charset)
except LookupError:
log.warn("invalid charset in '%s': '%s'" % (headers['content-type'], charset))
if '' != charset:
try:
html_text = html_text.decode(charset)
except LookupError:
log.warn("invalid charset in '%s': '%s'" % (headers['content-type'], charset))
if str != type(html_text):
html_text = str(html_text)
if str != type(html_text):
html_text = str(html_text)
result = re.match(r'.*?<title.*?>(.*?)</title>.*?', html_text, re.S | re.M | re.IGNORECASE)
if result:
match = result.groups()[0]
result = re.match(r'.*?<title.*?>(.*?)</title>.*?', html_text, re.S | re.M | re.IGNORECASE)
if result:
match = result.groups()[0]
parser = html.parser.HTMLParser()
try:
expanded_html = parser.unescape(match)
except UnicodeDecodeError as e: # idk why this can happen, but it does
log.warn('parser.unescape() expoded here: ' + str(e))
expanded_html = match
return 0, expanded_html
else:
return 2, 'no title'
parser = html.parser.HTMLParser()
try:
expanded_html = parser.unescape(match)
except UnicodeDecodeError as e: # idk why this can happen, but it does
log.warn('parser.unescape() expoded here: ' + str(e))
expanded_html = match
return 0, expanded_html
else:
return 2, 'no title'

View File

@@ -6,18 +6,18 @@ import sys
from common import VERSION, EVENTLOOP_DELAY, conf_load
try:
from local_config import conf, set_conf
from local_config import conf, set_conf
except ImportError:
sys.stderr.write('''
sys.stderr.write('''
%s: E: local_config.py isn't tracked because of included secrets and
%s site specific configurations. Rename local_config.py.skel and
%s adjust to you needs.
'''[1:] % (
sys.argv[0],
' ' * len(sys.argv[0]),
' ' * len(sys.argv[0])
))
sys.exit(1)
sys.argv[0],
' ' * len(sys.argv[0]),
' ' * len(sys.argv[0])
))
sys.exit(1)
from sleekxmpp import ClientXMPP
@@ -25,98 +25,98 @@ got_hangup = False
class IdleBot(ClientXMPP):
def __init__(self, jid, password, rooms, nick):
ClientXMPP.__init__(self, jid, password)
def __init__(self, jid, password, rooms, nick):
ClientXMPP.__init__(self, jid, password)
self.rooms = rooms
self.nick = nick
self.rooms = rooms
self.nick = nick
self.add_event_handler('session_start', self.session_start)
self.add_event_handler('groupchat_message', self.muc_message)
self.priority = 0
self.status = None
self.show = None
self.add_event_handler('session_start', self.session_start)
self.add_event_handler('groupchat_message', self.muc_message)
self.priority = 0
self.status = None
self.show = None
self.logger = logging.getLogger(__name__)
self.logger = logging.getLogger(__name__)
def session_start(self, _):
self.get_roster()
self.send_presence(ppriority=self.priority, pstatus=self.status, pshow=self.show)
def session_start(self, _):
self.get_roster()
self.send_presence(ppriority=self.priority, pstatus=self.status, pshow=self.show)
for room in self.rooms:
self.logger.info('%s: joining' % room)
ret = self.plugin['xep_0045'].joinMUC(
room,
self.nick,
wait=True
)
self.logger.info('%s: joined with code %s' % (room, ret))
for room in self.rooms:
self.logger.info('%s: joining' % room)
ret = self.plugin['xep_0045'].joinMUC(
room,
self.nick,
wait=True
)
self.logger.info('%s: joined with code %s' % (room, ret))
def muc_message(self, msg_obj):
"""
Handle muc messages, return if irrelevant content or die by hangup.
:param msg_obj:
:return:
"""
# don't talk to yourself
if msg_obj['mucnick'] == self.nick or 'groupchat' != msg_obj['type']:
return False
elif msg_obj['body'].startswith(conf('bot_user')) and 'hangup' in msg_obj['body']:
self.logger.warn("got 'hangup' from '%s': '%s'" % (
msg_obj['mucnick'], msg_obj['body']
))
global got_hangup
got_hangup = True
return False
elif msg_obj['mucnick'] in conf_load().get("other_bots", ()):
# not talking to the other bot.
return False
else:
return True
def muc_message(self, msg_obj):
"""
Handle muc messages, return if irrelevant content or die by hangup.
:param msg_obj:
:return:
"""
# don't talk to yourself
if msg_obj['mucnick'] == self.nick or 'groupchat' != msg_obj['type']:
return False
elif msg_obj['body'].startswith(conf('bot_user')) and 'hangup' in msg_obj['body']:
self.logger.warn("got 'hangup' from '%s': '%s'" % (
msg_obj['mucnick'], msg_obj['body']
))
global got_hangup
got_hangup = True
return False
elif msg_obj['mucnick'] in conf_load().get("other_bots", ()):
# not talking to the other bot.
return False
else:
return True
def start(botclass, active=False):
logging.basicConfig(
level=conf('loglevel', logging.INFO),
format=sys.argv[0] + ' %(asctime)s %(levelname).1s %(funcName)-15s %(message)s'
)
logger = logging.getLogger(__name__)
logger.info(VERSION)
logging.basicConfig(
level=conf('loglevel', logging.INFO),
format=sys.argv[0] + ' %(asctime)s %(levelname).1s %(funcName)-15s %(message)s'
)
logger = logging.getLogger(__name__)
logger.info(VERSION)
jid = conf('jid')
if '/' not in jid:
jid = '%s/%s' % (jid, botclass.__name__)
bot = botclass(
jid=jid,
password=conf('password'),
rooms=conf('rooms'),
nick=conf('bot_user')
)
import plugins
jid = conf('jid')
if '/' not in jid:
jid = '%s/%s' % (jid, botclass.__name__)
bot = botclass(
jid=jid,
password=conf('password'),
rooms=conf('rooms'),
nick=conf('bot_user')
)
import plugins
if active:
plugins.register_all()
if plugins.plugin_enabled_get(plugins.command_dsa_watcher):
# first result is lost.
plugins.command_dsa_watcher(['dsa-watcher', 'crawl'])
if active:
plugins.register_all()
if plugins.plugin_enabled_get(plugins.command_dsa_watcher):
# first result is lost.
plugins.command_dsa_watcher(['dsa-watcher', 'crawl'])
bot.connect()
bot.register_plugin('xep_0045')
bot.process()
global got_hangup
bot.connect()
bot.register_plugin('xep_0045')
bot.process()
global got_hangup
while 1:
try:
# print("hangup: %s" % got_hangup)
if got_hangup or not plugins.event_trigger():
bot.disconnect()
sys.exit(1)
while 1:
try:
# print("hangup: %s" % got_hangup)
if got_hangup or not plugins.event_trigger():
bot.disconnect()
sys.exit(1)
time.sleep(EVENTLOOP_DELAY)
except KeyboardInterrupt:
print('')
exit(130)
time.sleep(EVENTLOOP_DELAY)
except KeyboardInterrupt:
print('')
exit(130)
if '__main__' == __name__:
start(IdleBot)
start(IdleBot)

View File

@@ -3,63 +3,63 @@
import time
if '__main__' == __name__:
print('''this is a config file, which is not meant to be executed''')
exit(-1)
print('''this is a config file, which is not meant to be executed''')
exit(-1)
config = {
'jid': 'FIXME',
'password': 'FIXME',
'rooms': ['FIXME'],
'jid': 'FIXME',
'password': 'FIXME',
'rooms': ['FIXME'],
'src-url': 'http://aero2k.de/t/repos/urlbot-native.git',
'src-url': 'http://aero2k.de/t/repos/urlbot-native.git',
'bot_user': 'native-urlbot',
'bot_owner': 'FIXME',
'bot_user': 'native-urlbot',
'bot_owner': 'FIXME',
'hist_max_count': 5,
'hist_max_time': 10 * 60,
'hist_max_count': 5,
'hist_max_time': 10 * 60,
'uptime': -time.time(),
'request_counter': 0,
'uptime': -time.time(),
'request_counter': 0,
'persistent_storage': 'urlbot.persistent',
'persistent_locked': False,
'persistent_storage': 'urlbot.persistent',
'persistent_locked': False,
'url_blacklist': [
r'^.*heise\.de/.*-[0-9]+\.html$',
r'^.*wikipedia\.org/wiki/.*$',
r'^.*blog\.fefe\.de/\?ts=[0-9a-f]+$',
r'^.*ibash\.de/zitat.*$',
r'^.*golem\.de/news/.*$'
r'^.*paste\.debian\.net/((hidden|plainh?)/)?[0-9a-f]+/?$',
r'^.*example\.(org|net|com).*$',
r'^.*sprunge\.us/.*$',
r'^.*ftp\...\.debian\.org.*$'
],
'url_blacklist': [
r'^.*heise\.de/.*-[0-9]+\.html$',
r'^.*wikipedia\.org/wiki/.*$',
r'^.*blog\.fefe\.de/\?ts=[0-9a-f]+$',
r'^.*ibash\.de/zitat.*$',
r'^.*golem\.de/news/.*$'
r'^.*paste\.debian\.net/((hidden|plainh?)/)?[0-9a-f]+/?$',
r'^.*example\.(org|net|com).*$',
r'^.*sprunge\.us/.*$',
r'^.*ftp\...\.debian\.org.*$'
],
# the "dice" feature will use more efficient random data (0) for given users
'enhanced-random-user': ('FIXME', 'FIXME'),
# the "dice" feature will use more efficient random data (0) for given users
'enhanced-random-user': ('FIXME', 'FIXME'),
# the "moin" feature will be "disabled" for given users
'moin-modified-user': (),
'moin-disabled-user': (),
# the "moin" feature will be "disabled" for given users
'moin-modified-user': (),
'moin-disabled-user': (),
'tea_steep_time': (3*60 + 40),
'tea_steep_time': (3 * 60 + 40),
'image_preview': True,
'dsa_watcher_interval': 15 * 60
'image_preview': True,
'dsa_watcher_interval': 15 * 60
}
def conf(val):
import logging
logger = logging.getLogger(__name__)
if val in list(config.keys()):
return config[val]
logger.warn('conf(): unknown key ' + str(val))
return None
import logging
logger = logging.getLogger(__name__)
if val in list(config.keys()):
return config[val]
logger.warn('conf(): unknown key ' + str(val))
return None
def set_conf(key, val):
config[key] = val
return None
config[key] = val
return None

1602
plugins.py

File diff suppressed because it is too large Load Diff

View File

@@ -470,31 +470,31 @@ DNS server drank too much and had a hiccup
'''.split('\n')[1:-1]
moin_strings_hi = [
'Hi',
'Guten Morgen', 'Morgen',
'Moin',
'Tag', 'Tach',
'NAbend', 'Abend',
'Hallo', 'Hello'
'Hi',
'Guten Morgen', 'Morgen',
'Moin',
'Tag', 'Tach',
'NAbend', 'Abend',
'Hallo', 'Hello'
]
moin_strings_bye = [
'Nacht', 'gN8', 'N8',
'bye',
'Nacht', 'gN8', 'N8',
'bye',
]
cakes = [
"No cake for you!",
("The Enrichment Center is required to remind you "
"that you will be baked, and then there will be cake."),
"The cake is a lie!",
("This is your fault. I'm going to kill you. "
"And all the cake is gone. You don't even care, do you?"),
"Quit now and cake will be served immediately.",
("Enrichment Center regulations require both hands to be "
"empty before any cake..."),
("Uh oh. Somebody cut the cake. I told them to wait for "
"you, but they did it anyway. There is still some left, "
"though, if you hurry back."),
"I'm going to kill you, and all the cake is gone.",
"Who's gonna make the cake when I'm gone? You?"
"No cake for you!",
("The Enrichment Center is required to remind you "
"that you will be baked, and then there will be cake."),
"The cake is a lie!",
("This is your fault. I'm going to kill you. "
"And all the cake is gone. You don't even care, do you?"),
"Quit now and cake will be served immediately.",
("Enrichment Center regulations require both hands to be "
"empty before any cake..."),
("Uh oh. Somebody cut the cake. I told them to wait for "
"you, but they did it anyway. There is still some left, "
"though, if you hurry back."),
"I'm going to kill you, and all the cake is gone.",
"Who's gonna make the cake when I'm gone? You?"
]

View File

@@ -3,19 +3,18 @@ To be executed with nose
"""
import unittest
import time
from common import buckets, rate_limit, RATE_GLOBAL
class TestEventlooper(unittest.TestCase):
def test_broken_url(self):
"""
Test that broken socket calls are not breaking
"""
from common import fetch_page
broken_url = 'http://foo'
result = fetch_page(url=broken_url)
self.assertEqual(result, (None, None))
def test_broken_url(self):
"""
Test that broken socket calls are not breaking
"""
from common import fetch_page
broken_url = 'http://foo'
result = fetch_page(url=broken_url)
self.assertEqual(result, (None, None))
from collections import namedtuple
@@ -24,34 +23,33 @@ Bucket = namedtuple("BucketConfig", ["history", "period", "max_hist_len"])
class TestRateLimiting(unittest.TestCase):
def setUp(self):
# just for assertions
self.called = {
RATE_GLOBAL: [],
}
def setUp(self):
# just for assertions
self.called = {
RATE_GLOBAL: [],
}
def say(self, msg, rate_class=RATE_GLOBAL):
if rate_limit(rate_class):
self.called[rate_class].append(msg)
# print(msg)
time.sleep(0.1)
def say(self, msg, rate_class=RATE_GLOBAL):
if rate_limit(rate_class):
self.called[rate_class].append(msg)
# print(msg)
time.sleep(0.1)
def test_simple_burst(self):
messages = ["x_%d" % i for i in range(1, 9)]
for m in messages:
self.say(msg=m)
self.assertEqual(messages[:buckets[RATE_GLOBAL].max_hist_len], self.called[RATE_GLOBAL])
def test_simple_burst(self):
messages = ["x_%d" % i for i in range(1, 9)]
for m in messages:
self.say(msg=m)
self.assertEqual(messages[:buckets[RATE_GLOBAL].max_hist_len], self.called[RATE_GLOBAL])
def test_msg_two_bursts(self):
# custom bucket, just for testing
buckets[0x42] = Bucket(history=[], period=1, max_hist_len=5)
self.called[0x42] = []
def test_msg_two_bursts(self):
# custom bucket, just for testing
buckets[0x42] = Bucket(history=[], period=1, max_hist_len=5)
self.called[0x42] = []
bucket = buckets[0x42]
messages = ["x_%d" % i for i in range(0, 15)]
for i, m in enumerate(messages):
if i % bucket.max_hist_len == 0:
time.sleep(bucket.period)
self.say(msg=m, rate_class=0x42)
self.assertEqual(messages, self.called[0x42])
bucket = buckets[0x42]
messages = ["x_%d" % i for i in range(0, 15)]
for i, m in enumerate(messages):
if i % bucket.max_hist_len == 0:
time.sleep(bucket.period)
self.say(msg=m, rate_class=0x42)
self.assertEqual(messages, self.called[0x42])

423
urlbot.py
View File

@@ -6,276 +6,277 @@ import re
import sys
from common import (
conf_load, conf_save,
extract_title,
rate_limit_classes,
RATE_GLOBAL,
RATE_CHAT,
RATE_NO_SILENCE,
RATE_EVENT,
# rate_limited,
rate_limit,
RATE_URL, conf_set)
conf_load, conf_save,
extract_title,
rate_limit_classes,
RATE_GLOBAL,
RATE_CHAT,
RATE_NO_SILENCE,
RATE_EVENT,
# rate_limited,
rate_limit,
RATE_URL, conf_set)
from idlebot import IdleBot, start
from plugins import (
plugins as plugin_storage,
ptypes_COMMAND,
plugin_enabled_get,
ptypes_PARSE,
register_event,
else_command
plugins as plugin_storage,
ptypes_COMMAND,
plugin_enabled_get,
ptypes_PARSE,
register_event,
else_command
)
try:
from local_config import conf, set_conf
from local_config import conf, set_conf
except ImportError:
sys.stderr.write('''
sys.stderr.write('''
%s: E: local_config.py isn't tracked because of included secrets and
%s site specific configurations. Rename local_config.py.skel and
%s adjust to your needs.
'''[1:] % (
sys.argv[0],
' ' * len(sys.argv[0]),
' ' * len(sys.argv[0])
))
sys.exit(1)
sys.argv[0],
' ' * len(sys.argv[0]),
' ' * len(sys.argv[0])
))
sys.exit(1)
class UrlBot(IdleBot):
def __init__(self, jid, password, rooms, nick):
super(UrlBot, self).__init__(jid, password, rooms, nick)
def __init__(self, jid, password, rooms, nick):
super(UrlBot, self).__init__(jid, password, rooms, nick)
self.hist_ts = {p: [] for p in rate_limit_classes}
self.hist_flag = {p: True for p in rate_limit_classes}
self.hist_ts = {p: [] for p in rate_limit_classes}
self.hist_flag = {p: True for p in rate_limit_classes}
self.add_event_handler('message', self.message)
self.priority = 100
self.add_event_handler('message', self.message)
self.priority = 100
for r in self.rooms:
self.add_event_handler('muc::%s::got_online' % r, self.muc_online)
for r in self.rooms:
self.add_event_handler('muc::%s::got_online' % r, self.muc_online)
def muc_message(self, msg_obj):
return super(UrlBot, self).muc_message(msg_obj) and self.handle_msg(msg_obj)
def muc_message(self, msg_obj):
return super(UrlBot, self).muc_message(msg_obj) and self.handle_msg(msg_obj)
def message(self, msg_obj):
if 'groupchat' == msg_obj['type']:
return
else:
self.logger.info("Got the following PM: %s" % str(msg_obj))
def message(self, msg_obj):
if 'groupchat' == msg_obj['type']:
return
else:
self.logger.info("Got the following PM: %s" % str(msg_obj))
def muc_online(self, msg_obj):
"""
Hook for muc event "user joins"
"""
# don't react to yourself
if msg_obj['muc']['nick'] == self.nick:
return
def muc_online(self, msg_obj):
"""
Hook for muc event "user joins"
"""
# don't react to yourself
if msg_obj['muc']['nick'] == self.nick:
return
# TODO: move this to a undirected plugin, maybe new plugin type
arg_user = msg_obj['muc']['nick']
arg_user_key = arg_user.lower()
blob_userrecords = conf_load().get('user_records', {})
# TODO: move this to a undirected plugin, maybe new plugin type
arg_user = msg_obj['muc']['nick']
arg_user_key = arg_user.lower()
blob_userrecords = conf_load().get('user_records', {})
if arg_user_key in blob_userrecords:
records = blob_userrecords[arg_user_key]
if arg_user_key in blob_userrecords:
records = blob_userrecords[arg_user_key]
if not records:
return
if not records:
return
self.send_message(
mto=msg_obj['from'].bare,
mbody='%s, there %s %d message%s for you:\n%s' % (
arg_user,
'is' if 1 == len(records) else 'are',
len(records),
'' if 1 == len(records) else 's',
'\n'.join(records)
),
mtype='groupchat'
)
self.logger.info('sent %d offline records to room %s' % (
len(records), msg_obj['from'].bare
))
self.send_message(
mto=msg_obj['from'].bare,
mbody='%s, there %s %d message%s for you:\n%s' % (
arg_user,
'is' if 1 == len(records) else 'are',
len(records),
'' if 1 == len(records) else 's',
'\n'.join(records)
),
mtype='groupchat'
)
self.logger.info('sent %d offline records to room %s' % (
len(records), msg_obj['from'].bare
))
if conf('persistent_locked'):
self.logger.warn("couldn't get exclusive lock")
return False
if conf('persistent_locked'):
self.logger.warn("couldn't get exclusive lock")
return False
set_conf('persistent_locked', True)
blob = conf_load()
set_conf('persistent_locked', True)
blob = conf_load()
if 'user_records' not in blob:
blob['user_records'] = {}
if 'user_records' not in blob:
blob['user_records'] = {}
if arg_user_key in blob['user_records']:
blob['user_records'].pop(arg_user_key)
if arg_user_key in blob['user_records']:
blob['user_records'].pop(arg_user_key)
conf_save(blob)
set_conf('persistent_locked', False)
conf_save(blob)
set_conf('persistent_locked', False)
# @rate_limited(10)
def send_reply(self, message, msg_obj=None):
"""
Send a reply to a message
"""
if self.show:
self.logger.warn("I'm muted! (status: %s)" % self.show)
return
# @rate_limited(10)
def send_reply(self, message, msg_obj=None):
"""
Send a reply to a message
"""
if self.show:
self.logger.warn("I'm muted! (status: %s)" % self.show)
return
set_conf('request_counter', conf('request_counter') + 1)
set_conf('request_counter', conf('request_counter') + 1)
if str is not type(message):
message = '\n'.join(message)
if str is not type(message):
message = '\n'.join(message)
# check other bots, add nospoiler with urls
def _prevent_panic(message, room):
if 'http' in message:
other_bots = conf_load().get("other_bots", ())
users = self.plugin['xep_0045'].getRoster(room)
if set(users).intersection(set(other_bots)):
message = '(nospoiler) %s' % message
return message
# check other bots, add nospoiler with urls
def _prevent_panic(message, room):
if 'http' in message:
other_bots = conf_load().get("other_bots", ())
users = self.plugin['xep_0045'].getRoster(room)
if set(users).intersection(set(other_bots)):
message = '(nospoiler) %s' % message
return message
if conf('debug_mode', False):
print(message)
else:
if msg_obj:
message = _prevent_panic(message, msg_obj['from'].bare)
self.send_message(
mto=msg_obj['from'].bare,
mbody=message,
mtype='groupchat'
)
else: # unset msg_obj == broadcast
for room in self.rooms:
message = _prevent_panic(message, room)
self.send_message(
mto=room,
mbody=message,
mtype='groupchat'
)
if conf('debug_mode', False):
print(message)
else:
if msg_obj:
message = _prevent_panic(message, msg_obj['from'].bare)
self.send_message(
mto=msg_obj['from'].bare,
mbody=message,
mtype='groupchat'
)
else: # unset msg_obj == broadcast
for room in self.rooms:
message = _prevent_panic(message, room)
self.send_message(
mto=room,
mbody=message,
mtype='groupchat'
)
def handle_msg(self, msg_obj):
"""
called for incoming messages
:param msg_obj:
:returns nothing
"""
content = msg_obj['body']
def handle_msg(self, msg_obj):
"""
called for incoming messages
:param msg_obj:
:returns nothing
"""
content = msg_obj['body']
if 'has set the subject to:' in content:
return
if 'has set the subject to:' in content:
return
if sys.argv[0] in content:
self.logger.info('silenced, this is my own log')
return
if sys.argv[0] in content:
self.logger.info('silenced, this is my own log')
return
if 'nospoiler' in content:
self.logger.info('no spoiler for: ' + content)
return
if 'nospoiler' in content:
self.logger.info('no spoiler for: ' + content)
return
self.data_parse_commands(msg_obj)
self.data_parse_other(msg_obj)
self.data_parse_commands(msg_obj)
self.data_parse_other(msg_obj)
def data_parse_commands(self, msg_obj):
"""
react to a message with the bots nick
:param msg_obj: dictionary with incoming message parameters
def data_parse_commands(self, msg_obj):
"""
react to a message with the bots nick
:param msg_obj: dictionary with incoming message parameters
:returns: nothing
"""
global got_hangup
:returns: nothing
"""
global got_hangup
data = msg_obj['body']
words = data.split()
data = msg_obj['body']
words = data.split()
if 2 > len(words): # need at least two words
return None
if 2 > len(words): # need at least two words
return None
# don't reply if beginning of the text matches bot_user
if not data.startswith(conf('bot_user')):
return None
# don't reply if beginning of the text matches bot_user
if not data.startswith(conf('bot_user')):
return None
if 'hangup' in data:
self.logger.warn('received hangup: ' + data)
got_hangup = True
sys.exit(1)
if 'hangup' in data:
self.logger.warn('received hangup: ' + data)
got_hangup = True
sys.exit(1)
reply_user = msg_obj['mucnick']
reply_user = msg_obj['mucnick']
# TODO: check how several commands/plugins in a single message behave (also with rate limiting)
reacted = False
for plugin in plugin_storage[ptypes_COMMAND]:
# TODO: check how several commands/plugins in a single message behave (also with rate limiting)
reacted = False
for plugin in plugin_storage[ptypes_COMMAND]:
if not plugin_enabled_get(plugin):
continue
if not plugin_enabled_get(plugin):
continue
ret = plugin(
data=data,
cmd_list=[pl.plugin_name for pl in plugin_storage[ptypes_COMMAND]],
parser_list=[pl.plugin_name for pl in plugin_storage[ptypes_PARSE]],
reply_user=reply_user,
msg_obj=msg_obj,
argv=words[1:]
)
ret = plugin(
data=data,
cmd_list=[pl.plugin_name for pl in plugin_storage[ptypes_COMMAND]],
parser_list=[pl.plugin_name for pl in plugin_storage[ptypes_PARSE]],
reply_user=reply_user,
msg_obj=msg_obj,
argv=words[1:]
)
if ret:
self._run_action(ret, plugin, msg_obj)
reacted = True
if ret:
self._run_action(ret, plugin, msg_obj)
reacted = True
if not reacted and rate_limit(RATE_GLOBAL):
ret = else_command({'reply_user': reply_user})
if ret:
if 'msg' in ret:
self.send_reply(ret['msg'], msg_obj)
if not reacted and rate_limit(RATE_GLOBAL):
ret = else_command({'reply_user': reply_user})
if ret:
if 'msg' in ret:
self.send_reply(ret['msg'], msg_obj)
def data_parse_other(self, msg_obj):
"""
react to any message
def data_parse_other(self, msg_obj):
"""
react to any message
:param msg_obj: incoming message parameters
:return:
"""
data = msg_obj['body']
reply_user = msg_obj['mucnick']
:param msg_obj: incoming message parameters
:return:
"""
data = msg_obj['body']
reply_user = msg_obj['mucnick']
for plugin in plugin_storage[ptypes_PARSE]:
if not plugin_enabled_get(plugin):
continue
for plugin in plugin_storage[ptypes_PARSE]:
if not plugin_enabled_get(plugin):
continue
ret = plugin(reply_user=reply_user, data=data)
ret = plugin(reply_user=reply_user, data=data)
if ret:
self._run_action(ret, plugin, msg_obj)
if ret:
self._run_action(ret, plugin, msg_obj)
def _run_action(self, action, plugin, msg_obj):
"""
Execute the plugin's execution plan
:param action: dict with event and/or msg
:param plugin: plugin obj
:param msg_obj: xmpp message obj
"""
if 'event' in action:
event = action["event"]
if 'msg' in event:
register_event(event["time"], self.send_reply, [event['msg']])
elif 'command' in event:
command = event["command"]
if rate_limit(RATE_EVENT):
register_event(event["time"], command[0], command[1])
def _run_action(self, action, plugin, msg_obj):
"""
Execute the plugin's execution plan
:param action: dict with event and/or msg
:param plugin: plugin obj
:param msg_obj: xmpp message obj
"""
if 'event' in action:
event = action["event"]
if 'msg' in event:
register_event(event["time"], self.send_reply, [event['msg']])
elif 'command' in event:
command = event["command"]
if rate_limit(RATE_EVENT):
register_event(event["time"], command[0], command[1])
if 'msg' in action and rate_limit(RATE_CHAT | plugin.ratelimit_class):
self.send_reply(action['msg'], msg_obj)
if 'msg' in action and rate_limit(RATE_CHAT | plugin.ratelimit_class):
self.send_reply(action['msg'], msg_obj)
if 'presence' in action:
presence = action['presence']
conf_set('presence', presence)
if 'presence' in action:
presence = action['presence']
conf_set('presence', presence)
self.status = presence.get('msg')
self.show = presence.get('status')
self.status = presence.get('msg')
self.show = presence.get('status')
self.send_presence(pstatus=self.status, pshow=self.show)
# self.reconnect(wait=True)
self.send_presence(pstatus=self.status, pshow=self.show)
# self.reconnect(wait=True)
if '__main__' == __name__:
start(UrlBot, True)
start(UrlBot, True)