Skip to content
This repository was archived by the owner on Oct 25, 2019. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
http://inamidst.com/phenny/
"""

import logging
import os
import signal
import sys
import threading
import time

logger = logging.getLogger('phenny')


class Watcher(object):
# Cf. http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496735
Expand Down Expand Up @@ -59,7 +62,7 @@ def connect(config):
try:
Watcher()
except Exception as e:
print('Warning:', e, '(in __init__.py)', file=sys.stderr)
logger.warning(str(e) + ' (in __init__.py)')

while True:
try:
Expand All @@ -70,8 +73,7 @@ def connect(config):
if not isinstance(delay, int):
break

msg = "Warning: Disconnected. Reconnecting in {0} seconds..."
print(msg.format(delay), file=sys.stderr)
logger.warning("Disconnected. Reconnecting in {0} seconds...".format(delay))
time.sleep(delay)


Expand Down
294 changes: 164 additions & 130 deletions bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,17 @@
http://inamidst.com/phenny/
"""

import sys, os, re, threading, imp
import sys
import os
import re
import threading
import traceback
import irc
import tools
from importlib.machinery import SourceFileLoader
from tools import GrumbleError, decorate
import logging

logger = logging.getLogger('phenny')

home = os.getcwd()

Expand All @@ -27,6 +35,22 @@ def decode(bytes):
return bytes
return text

def module_control(phenny, module, func):
if not hasattr(module, func):
return True

try:
getattr(module, func)(phenny)
return True
except GrumbleError as e:
desc = str(e)
except Exception as e:
desc = traceback.format_exc()

name = os.path.basename(module.__file__)
logger.error("Error during %s of %s module:\n%s" % (func, name, desc))
return False

class Phenny(irc.Bot):
def __init__(self, config):
args = (config.nick, config.name, config.channels, config.password)
Expand Down Expand Up @@ -57,132 +81,139 @@ def setup(self):
if n.endswith('.py') and not n.startswith('_'):
filenames.append(os.path.join(fn, n))

modules = []
modules = {}
excluded_modules = getattr(self.config, 'exclude', [])

for filename in filenames:
name = os.path.basename(filename)[:-3]
if name in excluded_modules: continue
# if name in sys.modules:
# del sys.modules[name]
try: module = imp.load_source(name, filename)
except Exception as e:
print("Error loading %s: %s (in bot.py)" % (name, e), file=sys.stderr)
else:
if hasattr(module, 'setup'):
module.setup(self)
self.register(vars(module))
modules.append(name)

if modules:
print('Registered modules:', ', '.join(modules), file=sys.stderr)
else: print("Warning: Couldn't find any modules", file=sys.stderr)

try:
module = SourceFileLoader(name, filename).load_module()
except Exception as e:
trace = traceback.format_exc()
logger.error("Error loading %s module:\n%s" % (name, trace))
continue

if module_control(self, module, 'setup'):
self.register(module)
modules[name] = module

self.modules = modules

if modules:
logger.info('Registered modules: ' + ', '.join(sorted(modules.keys())))
else:
logger.warning("Couldn't find any modules")

self.bind_commands()

def register(self, variables):
def register(self, module):
# This is used by reload.py, hence it being methodised
for name, obj in variables.items():
if hasattr(obj, 'commands') or hasattr(obj, 'rule'):
self.variables[name] = obj
self.variables[module.__name__] = {}

def bind_commands(self):
self.commands = {'high': {}, 'medium': {}, 'low': {}}

def bind(self, priority, regexp, func):
print(priority, regexp.pattern.encode('utf-8'), func)
# register documentation
if not hasattr(func, 'name'):
func.name = func.__name__
if func.__doc__:
if hasattr(func, 'example'):
example = func.example
example = example.replace('$nickname', self.nick)
else: example = None
self.doc[func.name] = (func.__doc__, example)
self.commands[priority].setdefault(regexp, []).append(func)
for name, obj in vars(module).items():
if hasattr(obj, 'commands') or hasattr(obj, 'rule'):
self.variables[module.__name__][name] = obj

def bind(self, module, name, func, regexp):
# register documentation
if not hasattr(func, 'name'):
func.name = func.__name__

if func.__doc__:
if hasattr(func, 'example'):
example = func.example
example = example.replace('$nickname', self.nick)
else: example = None

self.doc[func.name] = (func.__doc__, example)

self.commands[func.priority].setdefault(regexp, []).append(func)

def bind_command(self, module, name, func):
logger.debug("Binding module '{:}' command '{:}'".format(module, name))

if not hasattr(func, 'priority'):
func.priority = 'medium'

if not hasattr(func, 'thread'):
func.thread = True

if not hasattr(func, 'event'):
func.event = 'PRIVMSG'
else:
func.event = func.event.upper()

def sub(pattern, self=self):
# These replacements have significant order
pattern = pattern.replace('$nickname', re.escape(self.nick))
return pattern.replace('$nick', r'%s[,:] +' % re.escape(self.nick))

for name, func in self.variables.items():
# print name, func
if not hasattr(func, 'priority'):
func.priority = 'medium'

if not hasattr(func, 'thread'):
func.thread = True

if not hasattr(func, 'event'):
func.event = 'PRIVMSG'
else: func.event = func.event.upper()

if hasattr(func, 'rule'):
if isinstance(func.rule, str):
pattern = sub(func.rule)
regexp = re.compile(pattern)
bind(self, func.priority, regexp, func)

if isinstance(func.rule, tuple):
# 1) e.g. ('$nick', '(.*)')
if len(func.rule) == 2 and isinstance(func.rule[0], str):
prefix, pattern = func.rule
prefix = sub(prefix)
regexp = re.compile(prefix + pattern)
bind(self, func.priority, regexp, func)

# 2) e.g. (['p', 'q'], '(.*)')
elif len(func.rule) == 2 and isinstance(func.rule[0], list):
prefix = self.config.prefix
commands, pattern = func.rule
for command in commands:
command = r'(%s)\b(?: +(?:%s))?' % (command, pattern)
regexp = re.compile(prefix + command)
bind(self, func.priority, regexp, func)

# 3) e.g. ('$nick', ['p', 'q'], '(.*)')
elif len(func.rule) == 3:
prefix, commands, pattern = func.rule
prefix = sub(prefix)
for command in commands:
command = r'(%s) +' % command
regexp = re.compile(prefix + command + pattern)
bind(self, func.priority, regexp, func)

if hasattr(func, 'commands'):
for command in func.commands:
template = r'^%s(%s)(?: +(.*))?$'
pattern = template % (self.config.prefix, command)
regexp = re.compile(pattern)
bind(self, func.priority, regexp, func)
if hasattr(func, 'rule'):
if isinstance(func.rule, str):
pattern = sub(func.rule)
regexp = re.compile(pattern)
self.bind(module, name, func, regexp)

if isinstance(func.rule, tuple):
# 1) e.g. ('$nick', '(.*)')
if len(func.rule) == 2 and isinstance(func.rule[0], str):
prefix, pattern = func.rule
prefix = sub(prefix)
regexp = re.compile(prefix + pattern)
self.bind(module, name, func, regexp)

# 2) e.g. (['p', 'q'], '(.*)')
elif len(func.rule) == 2 and isinstance(func.rule[0], list):
prefix = self.config.prefix
commands, pattern = func.rule
for command in commands:
command = r'(%s)\b(?: +(?:%s))?' % (command, pattern)
regexp = re.compile(prefix + command)
self.bind(module, name, func, regexp)

# 3) e.g. ('$nick', ['p', 'q'], '(.*)')
elif len(func.rule) == 3:
prefix, commands, pattern = func.rule
prefix = sub(prefix)
for command in commands:
command = r'(%s) +' % command
regexp = re.compile(prefix + command + pattern)
self.bind(module, name, func, regexp)

if hasattr(func, 'commands'):
for command in func.commands:
template = r'^%s(%s)(?: +(.*))?$'
pattern = template % (self.config.prefix, command)
regexp = re.compile(pattern)
self.bind(module, name, func, regexp)

def bind_commands(self):
self.commands = {'high': {}, 'medium': {}, 'low': {}}

for module, functions in self.variables.items():
for name, func in functions.items():
self.bind_command(module, name, func)

def wrapped(self, origin, text, match):
class PhennyWrapper(object):
def __init__(self, phenny):
self.bot = phenny

def __getattr__(self, attr):
sender = origin.sender or text
if attr == 'reply':
return (lambda msg:
self.bot.msg(sender, origin.nick + ': ' + msg))
elif attr == 'say':
return lambda msg: self.bot.msg(sender, msg)
elif attr == 'do':
return lambda msg: self.bot.action(sender, msg)
return getattr(self.bot, attr)

return PhennyWrapper(self)

def input(self, origin, text, bytes, match, event, args):
sender = origin.sender or text
delegate = {
'reply': lambda msg: self.msg(sender, origin.nick + ': ' + msg),
'say': lambda msg: self.msg(sender, msg),
'do': lambda msg: self.action(sender, msg),
}

return decorate(self, delegate)

def input(self, origin, text, match, event, args):
class CommandInput(str):
def __new__(cls, text, origin, bytes, match, event, args):
def __new__(cls, text, origin, match, event, args):
s = str.__new__(cls, text)
s.sender = decode(origin.sender)
s.nick = decode(origin.nick)
s.event = event
s.bytes = bytes
s.bytes = text
s.match = match
s.group = match.group
s.groups = match.groups
Expand All @@ -191,13 +222,14 @@ def __new__(cls, text, origin, bytes, match, event, args):
s.owner = s.nick == self.config.owner
return s

return CommandInput(text, origin, bytes, match, event, args)
return CommandInput(text, origin, match, event, args)

def call(self, func, origin, phenny, input):
try: func(phenny, input)
except tools.GrumbleError as e:
def call(self, func, origin, phenny, input):
try:
func(phenny, input)
except GrumbleError as e:
self.msg(origin.sender, str(e))
except Exception as e:
except Exception as e:
self.error(origin)

def limit(self, origin, func):
Expand All @@ -208,36 +240,38 @@ def limit(self, origin, func):
return True
return False

def dispatch(self, origin, args):
bytes, event, args = args[0], args[1], args[2:]
text = decode(bytes)
event = decode(event)
def dispatch(self, origin, args, text):
event = args[0]

if origin.nick in self.config.ignore:
return

for priority in ('high', 'medium', 'low'):
for priority in ('high', 'medium', 'low'):
items = list(self.commands[priority].items())
for regexp, funcs in items:
for func in funcs:

for regexp, funcs in items:
for func in funcs:
if event != func.event and func.event != '*': continue

match = regexp.match(text)
if match:
if self.limit(origin, func): continue
if not match: continue

if self.limit(origin, func): continue

phenny = self.wrapped(origin, text, match)
input = self.input(origin, text, bytes, match, event, args)
phenny = self.wrapped(origin, text, match)
input = self.input(origin, text, match, event, args)

if func.thread:
targs = (func, origin, phenny, input)
t = threading.Thread(target=self.call, args=targs)
t.start()
else: self.call(func, origin, phenny, input)
if func.thread:
targs = (func, origin, phenny, input)
t = threading.Thread(target=self.call, args=targs)
t.start()
else:
self.call(func, origin, phenny, input)

for source in [decode(origin.sender), decode(origin.nick)]:
try: self.stats[(func.name, source)] += 1
except KeyError:
for source in [decode(origin.sender), decode(origin.nick)]:
try:
self.stats[(func.name, source)] += 1
except KeyError:
self.stats[(func.name, source)] = 1

if __name__ == '__main__':
Expand Down
Loading