├── .gitignore ├── karmabot ├── core │ ├── signal.py │ ├── __init__.py │ ├── facets │ │ ├── __init__.py │ │ ├── bot.py │ │ ├── manager.py │ │ ├── base.py │ │ ├── name.py │ │ ├── karma.py │ │ ├── description.py │ │ ├── help.py │ │ └── irc.py │ ├── ircutils.py │ ├── commands │ │ ├── __init__.py │ │ ├── sets.py │ │ └── command.py │ ├── register.py │ ├── storage.py │ ├── utils.py │ ├── subject.py │ └── client.py ├── scripts │ ├── __init__.py │ ├── migrate.py │ ├── runserver.py │ └── reinkarnate.py ├── extensions │ ├── __init__.py │ ├── eightball.py │ ├── reddit.py │ ├── lmgtfy.py │ ├── github.py │ ├── twitter.py │ └── cs_schedule.py └── __init__.py ├── README.markdown ├── AUTHORS ├── LICENSE └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | .Python 4 | karmabot.egg-info/ 5 | *.json 6 | /dev_env/ 7 | /dump.rdb -------------------------------------------------------------------------------- /karmabot/core/signal.py: -------------------------------------------------------------------------------- 1 | from blinker import signal 2 | 3 | post_connection = signal('post_connection') 4 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # karmabot: a simple IRC bot 2 | 3 | **Karmabot** is a simple IRC bot written in Python, with infobot tendencies. It is written using Twisted words, has great ambitions, and wants to be your friend. 4 | -------------------------------------------------------------------------------- /karmabot/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | # 7 | -------------------------------------------------------------------------------- /karmabot/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | # 7 | -------------------------------------------------------------------------------- /karmabot/core/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | # 7 | 8 | VERSION = "0.3" 9 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Karmabot is an open source project from the Karmabot Contributors 2 | 3 | Contributors are: 4 | 5 | - Max Goodman 6 | - Dan Colish 7 | - Clark Boylan 8 | - Eric O'Connell 9 | -------------------------------------------------------------------------------- /karmabot/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | __import__('pkg_resources').declare_namespace(__name__) 7 | -------------------------------------------------------------------------------- /karmabot/core/facets/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | # 7 | 8 | from .base import Facet 9 | 10 | __all__ = ["Facet"] 11 | -------------------------------------------------------------------------------- /karmabot/core/ircutils.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | 7 | 8 | def bold(text): 9 | return u"\u0002{0}\u000F".format(text) 10 | -------------------------------------------------------------------------------- /karmabot/core/commands/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | # 7 | from .sets import CommandSet 8 | 9 | listen = CommandSet("listen") 10 | action = CommandSet("action", regex_format="(^{0}$)") 11 | 12 | __all__ = ['CommandSet', 'listen', 'action'] 13 | -------------------------------------------------------------------------------- /karmabot/core/register.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | 7 | 8 | class FacetRegistry(dict): 9 | def register(self, facet_class): 10 | self[facet_class.name] = facet_class 11 | return facet_class 12 | 13 | def __iter__(self): 14 | return self.itervalues() 15 | 16 | def attach(self, subject, exclude=set()): 17 | for facet_class in self: 18 | if facet_class.name not in exclude: 19 | facet_class(subject) 20 | 21 | facet_registry = FacetRegistry() 22 | -------------------------------------------------------------------------------- /karmabot/core/facets/bot.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | 7 | from karmabot.core import VERSION 8 | from .base import Facet 9 | from karmabot.core import storage 10 | 11 | 12 | #TODO: add save/reload/quit commands, customizable messages and behavior 13 | class KarmaBotFacet(Facet): 14 | name = "karmabot" 15 | display_key = 1 16 | 17 | def does_attach(self, subject): 18 | return subject.name == "karmabot" 19 | 20 | def present(self, context): 21 | return u"[v{0} - {1} subjects]".format(VERSION, len(storage.db)) 22 | -------------------------------------------------------------------------------- /karmabot/core/facets/manager.py: -------------------------------------------------------------------------------- 1 | from ..register import facet_registry 2 | 3 | from .irc import IRCChannelFacet, IRCUserFacet 4 | from .bot import KarmaBotFacet 5 | from .name import NameFacet 6 | from .description import DescriptionFacet 7 | from .karma import KarmaFacet 8 | from .help import HelpFacet 9 | 10 | 11 | class FacetManager(object): 12 | core_facets = (IRCChannelFacet, IRCUserFacet, 13 | KarmaFacet, KarmaBotFacet, 14 | DescriptionFacet, HelpFacet, 15 | NameFacet) 16 | 17 | def load_core(self): 18 | for facet in self.core_facets: 19 | facet_registry.register(facet) 20 | 21 | def load_extensions(self, extensions): 22 | for facet in extensions: 23 | __import__(facet) 24 | -------------------------------------------------------------------------------- /karmabot/core/facets/base.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | 7 | 8 | class Facet(object): 9 | name = None 10 | commands = None 11 | listens = None 12 | display_key = -1 13 | 14 | def __init__(self, subject): 15 | self.subject = subject 16 | if self.does_attach(subject): 17 | subject.add_facet(self) 18 | self.on_attach() 19 | 20 | def __str__(self): 21 | return self.name 22 | 23 | @property 24 | def data(self): 25 | return self.subject.data.setdefault(self.name, {}) 26 | 27 | def does_attach(self, subject): 28 | raise NotImplementedError 29 | 30 | def on_attach(self): 31 | pass 32 | 33 | def present(self, context): 34 | return u"" 35 | -------------------------------------------------------------------------------- /karmabot/core/facets/name.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | 7 | from karmabot.core.commands import CommandSet, action 8 | from .base import Facet 9 | from ..ircutils import bold 10 | 11 | 12 | class NameFacet(Facet): 13 | name = "name" 14 | commands = action.add_child(CommandSet(name)) 15 | display_key = 0 16 | 17 | def does_attach(cls, subject): 18 | return True 19 | 20 | @commands.add(u"{subject}\?*", 21 | help_str=u"show information about {subject}") 22 | def describe(self, context, subject): 23 | # this is a subject object not the list of subjects 24 | context.reply(subject.describe(context)) 25 | 26 | def present(self, context): 27 | return u"%s" % bold(self.subject.name) 28 | -------------------------------------------------------------------------------- /karmabot/core/storage.py: -------------------------------------------------------------------------------- 1 | import cPickle 2 | from redis import Redis 3 | 4 | from .signal import post_connection 5 | from .subject import Subject 6 | 7 | db = None 8 | 9 | 10 | @post_connection.connect 11 | def load_catalog(sender): 12 | global db 13 | db = Catalog() 14 | 15 | 16 | class Catalog(dict): 17 | def __init__(self, host='localhost', port=6379, db=0): 18 | self.redis = Redis(host=host, port=port, db=db) 19 | self.save = self.redis.save 20 | 21 | def __len__(self): 22 | return self.redis.dbsize() 23 | 24 | def get(self, key): 25 | subject = key.strip("() ") 26 | key = subject.lower() 27 | if self.redis.exists(key): 28 | subject = cPickle.loads(self.redis.get(key)) 29 | else: 30 | subject = Subject(key, subject) 31 | self.set(key, subject) 32 | return subject 33 | 34 | def set(self, key, value): 35 | return self.redis.set(key, 36 | cPickle.dumps(value, cPickle.HIGHEST_PROTOCOL)) 37 | -------------------------------------------------------------------------------- /karmabot/core/facets/karma.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | 7 | from .base import Facet 8 | from karmabot.core.commands import CommandSet, listen 9 | 10 | 11 | class KarmaFacet(Facet): 12 | name = "karma" 13 | listens = listen.add_child(CommandSet(name)) 14 | display_key = 2 15 | 16 | def does_attach(self, subject): 17 | return True 18 | 19 | @listens.add(u"{subject}++", help_str=u"add 1 to karma") 20 | def inc(self, subject, context): 21 | self.data.setdefault(subject.name, 0) 22 | self.data[subject.name] += 1 23 | 24 | @listens.add(u"{subject}--", help_str=u"subtract 1 from karma") 25 | def dec(self, subject, context): 26 | self.data.setdefault(subject.name, 0) 27 | self.data[subject.name] -= 1 28 | 29 | def present(self, context): 30 | return u"({karma}): ".format(karma=self.data.get(self.subject.name, 0)) 31 | -------------------------------------------------------------------------------- /karmabot/core/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | import time 7 | 8 | 9 | class Cache: 10 | def __init__(self, func, expire_seconds=None): 11 | self.func = func 12 | self.expire_seconds = expire_seconds 13 | self.last_result = None 14 | self.last_time = None 15 | self.last_args = None 16 | self.last_kwargs = None 17 | 18 | def __call__(self, *args, **kwargs): 19 | call_time = time.time() 20 | if args != self.last_args or kwargs != self.last_kwargs or \ 21 | self.last_time is None or call_time > self.last_time + self.expire_seconds: 22 | self.last_result = self.func(*args, **kwargs) 23 | self.last_time = call_time 24 | self.last_args = args 25 | self.last_kwargs = kwargs 26 | return self.last_result 27 | 28 | def reset(self): 29 | self.last_time = None 30 | 31 | 32 | def created_timestamp(context): 33 | return {"who": context.nick, "when": time.time(), "where": context.where} 34 | -------------------------------------------------------------------------------- /karmabot/extensions/eightball.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | from karmabot.core.facets import Facet 7 | from karmabot.core.commands import CommandSet, thing 8 | import random 9 | 10 | predictions = [ "As I see it, yes", 11 | "It is certain", 12 | "It is decidedly so", 13 | "Most likely", 14 | "Outlook good", 15 | "Signs point to yes", 16 | "Without a doubt", 17 | "Yes", 18 | "Yes - definitely", 19 | "You may rely on it", 20 | "Reply hazy, try again", 21 | "Ask again later", 22 | "Better not tell you now", 23 | "Cannot predict now", 24 | "Concentrate and ask again", 25 | "Don't count on it", 26 | "My reply is no", 27 | "My sources say no", 28 | "Outlook not so good", 29 | "Very doubtful"] 30 | 31 | @thing.facet_classes.register 32 | class EightBallFacet(Facet): 33 | name = "eightball" 34 | commands = thing.add_child(CommandSet(name)) 35 | 36 | @classmethod 37 | def does_attach(cls, thing): 38 | return thing.name == "eightball" 39 | 40 | @commands.add("shake {thing}", help="shake the magic eightball") 41 | def shake(self, thing, context): 42 | context.reply(random.choice(predictions) + ".") 43 | -------------------------------------------------------------------------------- /karmabot/core/subject.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | 7 | from .register import facet_registry 8 | 9 | 10 | class Subject(object): 11 | def __init__(self, key, name): 12 | self.key = key 13 | self.name = name 14 | self.data = {"name": name, 15 | "+facets": [], 16 | "-facets": []} 17 | self.facets = {} 18 | facet_registry.attach(self, set(self.data["-facets"])) 19 | 20 | def add_facet(self, facet): 21 | if str(facet) in self.facets: 22 | return 23 | if isinstance(facet, str): 24 | facet = facet_registry[facet](self) 25 | self.facets[str(facet)] = facet 26 | 27 | def remove_facet(self, facet): 28 | del self.facets[str(facet)] 29 | 30 | def iter_commands(self): 31 | for facet in self.facets.itervalues(): 32 | for command_set in (facet.commands, facet.listens): 33 | if command_set: 34 | for cmd in command_set: 35 | yield cmd 36 | 37 | def describe(self, context): 38 | final_txt = u"" 39 | sorted_facets = sorted(self.facets.itervalues(), 40 | key=lambda x: x.display_key) 41 | for facet in sorted_facets: 42 | final_txt += facet.present(context) 43 | return final_txt 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) <2010>, The Karmabot Team All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this list 7 | of conditions and the following disclaimer. 8 | 9 | Redistributions in binary form must reproduce the above copyright notice, this 10 | list of conditions and the following disclaimer in the documentation and/or 11 | other materials provided with the distribution. 12 | 13 | Neither the name of the Karmabot Team nor the names of its contributors may be 14 | used to endorse or promote products derived from this software without specific 15 | prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 24 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /karmabot/core/facets/description.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | from twisted.python import log 7 | 8 | from .base import Facet 9 | from ..commands import CommandSet, action 10 | from ..utils import created_timestamp 11 | 12 | 13 | class DescriptionFacet(Facet): 14 | name = "description" 15 | commands = action.add_child(CommandSet(name)) 16 | display_key = 3 17 | 18 | @classmethod 19 | def does_attach(cls, subject): 20 | return True 21 | 22 | @commands.add(u"{subject} is {description}", 23 | u"add a description to {subject}") 24 | def description(self, context, subject, description): 25 | self.data.append({"created": created_timestamp(context), 26 | "text": description}) 27 | 28 | @commands.add(u"forget that {subject} is {description}", 29 | u"drop a {description} for {subject}") 30 | def forget(self, context, subject, description): 31 | log.msg(self.descriptions) 32 | for desc in self.descriptions: 33 | if desc["text"] == description: 34 | self.descriptions.remove(desc) 35 | log.msg("removed %s" % desc) 36 | 37 | @property 38 | def data(self): 39 | return self.subject.data.setdefault(self.name, []) 40 | 41 | @property 42 | def descriptions(self): 43 | return self.data 44 | 45 | def present(self, context): 46 | return (u", ".join(desc["text"] for desc in self.descriptions) 47 | or u"") 48 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | """ 7 | Karmabot 8 | -------- 9 | A highly extensible IRC karma+information bot 10 | 11 | Features include, thing storage, karma tracking 12 | 13 | Links 14 | ````` 15 | 16 | * `development version 17 | `_ 18 | """ 19 | 20 | from setuptools import setup, find_packages 21 | 22 | setup( 23 | name="karmabot", 24 | version="dev", 25 | packages=find_packages(), 26 | namespace_packages=['karmabot'], 27 | include_package_data=True, 28 | author="Max Goodman", 29 | author_email="", 30 | description="A highly extensible IRC karma+information bot", 31 | long_description=__doc__, 32 | zip_safe=False, 33 | platforms='any', 34 | license='BSD', 35 | url='http://www.github.com/dcolish/karmabot', 36 | 37 | classifiers=[ 38 | 'Development Status :: 4 - Beta ', 39 | 'Environment :: Console', 40 | 'Framework :: Twisted', 41 | 'Intended Audience :: Developers', 42 | 'License :: OSI Approved :: ', 43 | 'Programming Language :: Python', 44 | 'Topic :: Communications :: Chat :: Internet Relay Chat', 45 | ], 46 | 47 | entry_points={ 48 | 'console_scripts': [ 49 | 'karmabot=karmabot.scripts.runserver:main', 50 | 'migrate=karmabot.scripts.migrate:main', 51 | 'reinkarnate=karmabot.scripts.reinkarnate:main', 52 | ], 53 | }, 54 | 55 | install_requires=[ 56 | 'blinker', 57 | 'pyopenssl', 58 | 'twisted', 59 | 'redis', 60 | 'BeautifulSoup', 61 | ], 62 | ) 63 | -------------------------------------------------------------------------------- /karmabot/core/commands/sets.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | 7 | from itertools import chain 8 | import re 9 | 10 | from .command import Command, CommandParser 11 | 12 | 13 | class CommandSet(object): 14 | 15 | def __init__(self, name, regex_format="{0}", parent=None): 16 | self.name = name 17 | self.regex_format = regex_format 18 | self.parent = parent 19 | self.children = [] 20 | self.commands = [] 21 | 22 | def __iter__(self): 23 | return iter(self.commands) 24 | 25 | def add_child(self, command_set): 26 | command_set.parent = self 27 | self.children.append(command_set) 28 | return command_set 29 | 30 | def add(self, format, help_str, exclusive=False, visible=True): 31 | def decorator(f): 32 | self.commands.append( 33 | Command(self, format, f, help_str, visible, exclusive)) 34 | return f 35 | return decorator 36 | 37 | def compile(self): 38 | def traverse_commands(cmdset): 39 | child_commands = (child.commands for child in cmdset.children) 40 | for command in chain(cmdset.commands, *child_commands): 41 | yield command 42 | 43 | command_infos = [] 44 | for command in traverse_commands(self): 45 | regex = command.to_regex() 46 | formatted_regex = self.regex_format.format(regex) 47 | 48 | command_info = {"re": re.compile(formatted_regex, re.U), 49 | "command": command, 50 | "exclusive": command.exclusive} 51 | command_infos.append(command_info) 52 | 53 | # Sort exclusive commands before non-exclusive ones 54 | command_infos.sort(key=lambda c: c["exclusive"], reverse=True) 55 | 56 | return CommandParser(command_infos) 57 | -------------------------------------------------------------------------------- /karmabot/core/facets/help.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | 7 | from .base import Facet 8 | from karmabot.core.commands import CommandSet, action 9 | from itertools import chain 10 | 11 | 12 | def numbered(strs): 13 | return (u"{0}. {1}".format(num + 1, line) 14 | for num, line in enumerate(strs)) 15 | 16 | 17 | class HelpFacet(Facet): 18 | name = "help" 19 | commands = action.add_child(CommandSet(name)) 20 | short_template = u"\"{0}\"" 21 | full_template = short_template + u": {1}" 22 | 23 | @classmethod 24 | def does_attach(cls, subject): 25 | return True 26 | 27 | def get_topics(self, subject): 28 | topics = dict() 29 | for cmd in chain(action, subject.iter_commands()): 30 | if cmd.visible: 31 | topic = cmd.format.replace("{subject}", subject.name) 32 | help = cmd.help.replace("{subject}", subject.name) 33 | topics[topic] = help 34 | return topics 35 | 36 | def format_help(self, subject, full=False): 37 | line_template = self.full_template if full else self.short_template 38 | help_lines = [line_template.format(topic, help) 39 | for topic, help in self.get_topics(subject).items()] 40 | help_lines.sort() 41 | return help_lines 42 | 43 | @commands.add(u"help {subject}", 44 | help_str=u"view command help for {subject}") 45 | def help(self, context, subject): 46 | context.reply(u"Commands: " + u", ".join(self.format_help(subject))) 47 | 48 | @commands.add(u"help {subject} {topic}", 49 | help_str=u"view help for {topic} on {subject}") 50 | def help_topic(self, context, subject, topic): 51 | topic = topic.strip(u"\"") 52 | topics = self.get_topics(subject) 53 | if topic in topics: 54 | context.reply(self.full_template.format(topic, topics[topic])) 55 | else: 56 | context.reply(u"I know of no such help topic.") 57 | -------------------------------------------------------------------------------- /karmabot/scripts/migrate.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | from __future__ import print_function 7 | 8 | import re 9 | import json 10 | 11 | format_handlers = dict() 12 | 13 | 14 | def copy_keys(d, keys, mappings): 15 | result = dict((key, d[key]) for key in keys) 16 | for key, newkey in mappings.iteritems(): 17 | result[newkey] = d[key] 18 | return result 19 | 20 | 21 | def format_handler(from_format, to_format): 22 | def doit(handler): 23 | format_handlers[(from_format, to_format)] = handler 24 | return doit 25 | 26 | 27 | @format_handler("1", "2") 28 | def migrate_v1(old_data, filename): 29 | new_data = {"things": dict(), "version": "2"} 30 | thing_re = re.compile("^[#\w ]+$") 31 | for old_id, old_thing in old_data["things"].iteritems(): 32 | if thing_re.match(old_id): 33 | new_thing = copy_keys(old_thing, ("name", "created"), 34 | {"desc": "description"}) 35 | new_thing["karma"] = {"": old_thing["karma"]} 36 | new_data["things"][old_id] = new_thing 37 | return new_data 38 | 39 | 40 | def migrate(filename, to_format): 41 | data = json.load(open(filename)) 42 | format = data.get("format", "1") 43 | return format_handlers[(format, to_format)](data, filename) 44 | 45 | 46 | def main(): 47 | from optparse import OptionParser 48 | parser = OptionParser(usage="usage: %prog [options] filenames") 49 | parser.add_option("-t", "--to-format", 50 | action="store", dest="to_format", default="2", 51 | help="format to migrate to") 52 | (options, filenames) = parser.parse_args() 53 | 54 | if not filenames: 55 | parser.error("No files specified.") 56 | 57 | for filename in filenames: 58 | print("Migrating {0}...".format(filename), end="") 59 | migrated_data = migrate(filename, options.to_format) 60 | new_filename = "migrated-{0}".format(filename) 61 | json.dump(migrated_data, open(new_filename, "w"), 62 | sort_keys=True, indent=4) 63 | print(" done.") 64 | 65 | if __name__ == "__main__": 66 | main() 67 | -------------------------------------------------------------------------------- /karmabot/core/facets/irc.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | 7 | from karmabot.core.commands import CommandSet, listen, action 8 | from karmabot.core import storage 9 | from .base import Facet 10 | 11 | 12 | class IRCChannelFacet(Facet): 13 | name = "ircchannel" 14 | commands = action.add_child(CommandSet(name)) 15 | 16 | @classmethod 17 | def does_attach(cls, subject): 18 | return subject.name.startswith("#") 19 | 20 | @commands.add(u"join {subject}", help_str=u"join the channel {subject}") 21 | def join(self, subject, context): 22 | context.bot.join_with_key(subject.name.encode("utf-8")) 23 | 24 | @commands.add(u"leave {subject}", help_str=u"leave the channel {subject}") 25 | def leave(self, subject, context): 26 | channel = subject.name.encode("utf-8") 27 | context.reply("Bye!", where=channel) 28 | context.bot.leave(channel) 29 | 30 | @commands.add(u"set topic of {subject} to {topic}", 31 | u"set the channel topic of {subject}") 32 | def set_topic(self, context, subject, topic): 33 | channel = subject.name.encode("utf-8") 34 | topic = topic.encode("utf-8") 35 | context.bot.topic(channel, topic) 36 | 37 | @property 38 | def topic(self): 39 | return self.data.get("topic", None) 40 | 41 | @topic.setter 42 | def topic(self, value): 43 | self.data["topic"] = value 44 | 45 | def present(self, context): 46 | return u"Topic: {topic}".format(topic=self.topic) 47 | 48 | 49 | class IRCUserFacet(Facet): 50 | #TODO: IRCUser facet, with trusted/admin types and verified hostmasks 51 | name = "ircuser" 52 | 53 | def does_attach(self, subject): 54 | # Attached by the listener 55 | return False 56 | 57 | @property 58 | def is_verified(self): 59 | return self.data.get("verified", False) 60 | 61 | @is_verified.setter 62 | def is_verified(self, value): 63 | self.data["verified"] = value 64 | 65 | @listen.add("u{message}", 66 | u'manage messages coming in') 67 | def message(self, context, **arg): 68 | user_subject = storage.db.get(context.nick) 69 | user_subject.add_facet("ircuser") 70 | -------------------------------------------------------------------------------- /karmabot/extensions/reddit.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | import urllib 7 | 8 | try: 9 | import json 10 | except ImportError: 11 | import simplejson as json 12 | 13 | from karmabot import thing 14 | from karmabot import command 15 | from karmabot.utils import Cache 16 | from karmabot.core.facets import Facet 17 | 18 | 19 | @thing.facet_classes.register 20 | class RedditorFacet(Facet): 21 | name = "redditor" 22 | commands = command.thing.add_child(command.FacetCommandSet(name)) 23 | 24 | def __init__(self, thing_): 25 | super(self, Facet).__init__(self, thing_) 26 | self.get_info = Cache(self._get_info, expire_seconds=10 * 60) 27 | 28 | @classmethod 29 | def does_attach(cls, thing): 30 | return False 31 | 32 | @commands.add(u"forget that {thing} is a redditor", 33 | help=u"unset {thing}'s reddit username", 34 | exclusive=True) 35 | def unset_redditor(self, thing, context): 36 | del self.data 37 | self.thing.remove_facet(self) 38 | 39 | @commands.add(u"{thing} has reddit username {username}", 40 | help=u"set {thing}'s reddit username to {username}") 41 | def set_redditor_username(self, thing, username, context): 42 | self.username = username 43 | 44 | @property 45 | def username(self): 46 | return self.data.get("username", self.thing.name) 47 | 48 | @username.setter 49 | def username(self, value): 50 | if "username" not in self.data or value != self.data["username"]: 51 | self.data["username"] = value 52 | self.get_info.reset() 53 | 54 | def _get_info(self): 55 | about_url = "http://www.reddit.com/user/{0}/about.json" 56 | about = urllib.urlopen(about_url.format(self.username)) 57 | return json.load(about)["data"] 58 | 59 | 60 | @command.thing.add(u"{thing} is a redditor", 61 | help=u"link {thing}'s reddit account to their user", 62 | exclusive=True) 63 | @command.thing_command 64 | def set_redditor(thing, context): 65 | thing.add_facet(RedditorFacet) 66 | 67 | 68 | @thing.presenters.register(set(["redditor"])) 69 | def present(thing, context): 70 | info = thing.facets["redditor"].get_info() 71 | text = u"http://reddit.com/user/{name} ({link_karma}/{comment_karma})".format( 72 | name=info["name"], 73 | link_karma=info["link_karma"], 74 | comment_karma=info["comment_karma"]) 75 | return text 76 | -------------------------------------------------------------------------------- /karmabot/extensions/lmgtfy.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | # dedicated to LC 7 | 8 | from json import JSONDecoder 9 | from urllib import urlencode 10 | from urllib2 import urlopen 11 | 12 | from karmabot.core.client import thing 13 | from karmabot.core.commands.sets import CommandSet 14 | from karmabot.core.register import facet_registry 15 | from karmabot.core.facets import Facet 16 | 17 | import re 18 | import htmlentitydefs 19 | 20 | ## 21 | # Function Placed in public domain by Fredrik Lundh 22 | # http://effbot.org/zone/copyright.htm 23 | # http://effbot.org/zone/re-sub.htm#unescape-html 24 | # Removes HTML or XML character references and entities from a text string. 25 | # 26 | # @param text The HTML (or XML) source text. 27 | # @return The plain text, as a Unicode string, if necessary. 28 | 29 | 30 | def unescape(text): 31 | def fixup(m): 32 | text = m.group(0) 33 | if text[:2] == "&#": 34 | # character reference 35 | try: 36 | if text[:3] == "&#x": 37 | return unichr(int(text[3:-1], 16)) 38 | else: 39 | return unichr(int(text[2:-1])) 40 | except ValueError: 41 | pass 42 | else: 43 | # named entity 44 | try: 45 | text = unichr(htmlentitydefs.name2codepoint[text[1:-1]]) 46 | except KeyError: 47 | pass 48 | # leave as is 49 | return text 50 | return re.sub("&#?\w+;", fixup, text) 51 | 52 | 53 | @facet_registry.register 54 | class LmgtfyFacet(Facet): 55 | name = "lmgtfy" 56 | commands = thing.add_child(CommandSet(name)) 57 | 58 | @classmethod 59 | def does_attach(cls, thing): 60 | return thing.name == "lmgtfy" 61 | 62 | @commands.add(u"lmgtfy {item}", 63 | u"googles for a {item}") 64 | def lmgtfy(self, context, item): 65 | api_url = "http://ajax.googleapis.com/ajax/services/search/web?" 66 | response = urlopen(api_url + urlencode(dict(v="1.0", 67 | q=item))) 68 | response = dict(JSONDecoder().decode(response.read())) 69 | top_result = {} 70 | if response.get('responseStatus') == 200: 71 | results = response.get('responseData').get('results') 72 | top_result = results.pop(0) 73 | context.reply(", ".join([unescape(top_result.get('titleNoFormatting')), 74 | top_result.get('unescapedUrl'), 75 | ])) 76 | -------------------------------------------------------------------------------- /karmabot/extensions/github.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | import urllib 7 | 8 | import json 9 | 10 | from karmabot.core.register import facet_registry 11 | from karmabot.core.facets import Facet 12 | from karmabot.core.commands import thing, CommandSet 13 | from karmabot.core.utils import Cache 14 | 15 | 16 | @facet_registry.register 17 | class GitHubFacet(Facet): 18 | name = "github" 19 | commands = thing.add_child(CommandSet(name)) 20 | 21 | def __init__(self, thing): 22 | super(self, Facet).__init__(self, thing) 23 | self.get_info = Cache(self._get_info, expire_seconds=10 * 60) 24 | 25 | @classmethod 26 | def does_attach(cls, thing): 27 | return False 28 | 29 | @commands.add(u"forget that {thing} is on github", 30 | help=u"unset {thing}'s github username", 31 | exclusive=True) 32 | def unset_github_username(self, thing, context): 33 | del self.data 34 | self.thing.remove_facet(self) 35 | 36 | @commands.add(u"{thing} has github username {username}", 37 | u"set {thing}'s github username to {username}") 38 | def set_github_username(self, thing, username, context): 39 | self.username = username 40 | 41 | @property 42 | def username(self): 43 | return self.data.get("username", self.thing.name) 44 | 45 | @username.setter 46 | def set_username(self, value): 47 | if "username" not in self.data or value != self.data["username"]: 48 | self.data["username"] = value 49 | self.get_info.reset() 50 | 51 | def _get_info(self): 52 | about_url = "http://github.com/{0}.json" 53 | about = urllib.urlopen(about_url.format(self.username)) 54 | return json.load(about) 55 | 56 | @commands.add(u"{thing} commits", 57 | u"show the last 3 commits by {thing}") 58 | def get_github_commits(self, thing, context): 59 | info = self.get_info() 60 | pushes = filter(lambda x: x["type"] == "PushEvent", info) 61 | lines = [] 62 | for push in pushes[:3]: 63 | lines.append(u"\"{last_commit_msg}\" -- {url}".format( 64 | url=push["repository"]["url"], 65 | last_commit_msg=push["payload"]["shas"][0][2])) 66 | context.reply("\n".join(lines)) 67 | 68 | def present(self, context): 69 | github = self.thing.facets["github"] 70 | text = u"http://github.com/{0}".format(github.username) 71 | return text 72 | 73 | 74 | @thing.add(format=u"{thing} is on github", 75 | help_str=u"link {thing}'s github account to their user", 76 | exclusive=True) 77 | def set_githubber(thing, context): 78 | thing.add_facet(GitHubFacet) 79 | -------------------------------------------------------------------------------- /karmabot/scripts/runserver.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | import sys 7 | 8 | from twisted.internet import reactor, ssl 9 | from twisted.python import log 10 | 11 | from karmabot.core.client import KarmaBotFactory 12 | 13 | 14 | def main(): 15 | from optparse import OptionParser 16 | parser = OptionParser(usage="usage: %prog [options] channels") 17 | 18 | # IRC connection options 19 | parser.add_option("-s", "--server", 20 | action="store", dest="server", 21 | default="irc.freenode.net", 22 | help="IRC server to connect to") 23 | parser.add_option("-p", "--port", 24 | action="store", type="int", dest="port", default=None, 25 | help="IRC server to connect to") 26 | parser.add_option("--ssl", 27 | action="store_true", dest="ssl", default=False, 28 | help="use SSL") 29 | parser.add_option("--password", 30 | action="store", dest="password", default=None, 31 | help="server password") 32 | parser.add_option("-n", "--nick", 33 | action="store", dest="nick", default="karmabot", 34 | help="nickname to use") 35 | # Bot options 36 | parser.add_option("-v", "--verbose", 37 | action="store_true", dest="verbose", default=False, 38 | help="enable verbose output") 39 | parser.add_option("-d", "--data", 40 | action="store", dest="filename", default="karma.json", 41 | help="karma data file name") 42 | parser.add_option("-t", "--trust", 43 | action="append", dest="trusted", default=[], 44 | help="trusted hostmasks") 45 | parser.add_option("-f", "--facets", 46 | action="append", dest="facets", default=[], 47 | help="additional facets to load") 48 | 49 | (options, channels) = parser.parse_args() 50 | 51 | if not channels: 52 | parser.error("You must supply some channels to join.") 53 | else: 54 | log.msg("Channels to join: %s" % channels) 55 | 56 | if options.verbose: 57 | log.startLogging(sys.stdout) 58 | 59 | if not options.port: 60 | options.port = 6667 if not options.ssl else 9999 61 | 62 | factory = KarmaBotFactory(options.filename, options.nick, 63 | channels, options.trusted, options.password, 64 | options.facets) 65 | if not options.ssl: 66 | reactor.connectTCP(options.server, options.port, factory) 67 | else: 68 | reactor.connectSSL(options.server, options.port, 69 | factory, ssl.ClientContextFactory()) 70 | reactor.run() 71 | 72 | if __name__ == "__main__": 73 | main() 74 | -------------------------------------------------------------------------------- /karmabot/extensions/twitter.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | import urllib 7 | from xml.sax.saxutils import unescape as unescape_html 8 | 9 | try: 10 | import json 11 | except ImportError: 12 | import simplejson as json 13 | 14 | from karmabot import command, thing 15 | from karmabot.core.facets import Facet 16 | from karmabot.utils import Cache 17 | 18 | @thing.facet_classes.register 19 | class TwitterFacet(Facet): 20 | name = "twitter" 21 | commands = command.thing.add_child(command.FacetCommandSet(name)) 22 | 23 | def __init__(self, thing_): 24 | super(self, Facet).__init__(self, thing_) 25 | self.get_info = Cache(self._get_info, expire_seconds=10*60) 26 | 27 | @classmethod 28 | def does_attach(cls, thing): 29 | return False 30 | 31 | @commands.add(u"forget that {thing} is on twitter", 32 | help=u"unset {thing}'s twitter username", 33 | exclusive=True) 34 | def unset_twitterer(self, thing, context): 35 | del self.data 36 | self.thing.remove_facet(self) 37 | 38 | @commands.add(u"{thing} has twitter username {username}", 39 | help=u"set {thing}'s twitter username to {username}") 40 | def set_twitter_username(self, thing, username, context): 41 | self.username = username 42 | 43 | @property 44 | def username(self): 45 | return self.data.get("username", self.thing.name) 46 | 47 | @username.setter 48 | def username(self, value): 49 | if "username" not in self.data or value != self.data["username"]: 50 | self.data["username"] = value 51 | self.get_info.reset() 52 | 53 | def _get_info(self): 54 | about_url = "http://api.twitter.com/1/statuses/user_timeline/{0}.json" 55 | about = urllib.urlopen(about_url.format(self.username)) 56 | return json.load(about) 57 | 58 | def get_last_tweet(self): 59 | return unescape_html(self.get_info()[0]["text"]) 60 | 61 | @command.thing.add(u"{thing} is on twitter", 62 | help=u"link {thing}'s twitter account to their user", 63 | exclusive=True) 64 | @command.thing_command 65 | def set_twitterer(thing, context): 66 | thing.add_facet(TwitterFacet) 67 | 68 | @thing.presenters.register(set(["twitter"])) 69 | def present(thing, context): 70 | twitter = thing.facets["twitter"] 71 | text = u"@{twitter_name}: \"{tweet}\"".format( 72 | twitter_name = twitter.username, 73 | tweet = twitter.get_last_tweet()) 74 | return text 75 | 76 | @thing.presenters.register(set(["name", "karma", "description", "twitter"])) 77 | def present(thing, context): 78 | twitter = thing.facets["twitter"] 79 | name_display = thing.describe(context, facets=set(["name", "karma"])) 80 | text = u"{name} \"{tweet}\"{descriptions}".format( 81 | name = name_display, 82 | tweet = twitter.get_last_tweet(), 83 | descriptions = ": " + thing.facets["description"].present() 84 | if thing.facets["description"].descriptions else "") 85 | return text 86 | -------------------------------------------------------------------------------- /karmabot/scripts/reinkarnate.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | from __future__ import print_function 7 | 8 | import sys 9 | import os.path 10 | import re 11 | import time 12 | from twisted.python import log 13 | 14 | import karmabot.client as karmabot 15 | from karmabot.facets import ( 16 | bot, 17 | karma, 18 | description, 19 | name, 20 | help, 21 | irc as ircfacet, 22 | ) 23 | 24 | #XXX:dc:whats this for? 25 | sys.path.append("../src") 26 | 27 | # Designed to parse bip logs (http://bip.t1r.net) 28 | LOG_RE = re.compile( 29 | r"(?P[\d-]+\s*[\d:]+)\s*<\s*(?P[^:]+): (?P.*)") 30 | 31 | 32 | def reincarnate(bot, path): 33 | with open(path) as f: 34 | channel = os.path.basename(path).split(".")[0] 35 | for line in f: 36 | m = LOG_RE.match(line) 37 | if m: 38 | when = time.mktime(time.strptime(m.group("when"), 39 | "%d-%m-%Y %H:%M:%S")) 40 | 41 | # Bwahahahaha. 42 | _time = time.time 43 | time.time = lambda: when 44 | try: 45 | bot.privmsg(m.group("user"), channel, m.group("msg")) 46 | except Exception: 47 | log.err() 48 | finally: 49 | time.time = _time 50 | 51 | 52 | class ImaginaryKarmaBotFactory(object): 53 | def __init__(self, nick, filename): 54 | self.nick = nick 55 | self.channels = None 56 | self.filename = filename 57 | self.trusted = None 58 | self.password = None 59 | 60 | 61 | def main(): 62 | from optparse import OptionParser 63 | parser = OptionParser(usage="usage: %prog [options]") 64 | 65 | parser.add_option("-v", "--verbose", 66 | action="store_true", dest="verbose", default=False, 67 | help="enable verbose output") 68 | parser.add_option("-d", "--data", 69 | action="store", dest="filename", default="karma.json", 70 | help="karma data file name") 71 | parser.add_option("-n", "--nick", 72 | action="store", dest="nick", default="karmabot", 73 | help="nickname to use") 74 | parser.add_option("-f", "--facets", 75 | action="append", dest="facets", default=[], 76 | help="additional facets to load") 77 | 78 | (options, paths) = parser.parse_args() 79 | 80 | if options.verbose: 81 | log.startLogging(sys.stdout) 82 | 83 | # FIXME: this needs to be replaced with a real facet manager 84 | for facet_path in options.facets: 85 | execfile(facet_path, globals()) 86 | 87 | kb = karmabot.KarmaBot() 88 | kb.factory = ImaginaryKarmaBotFactory(options.nick, options.filename) 89 | 90 | kb.init() 91 | kb.nickname = options.nick 92 | 93 | # You have no mouth, no legs. 94 | kb.msg = kb.join = kb.leave = lambda *x: None 95 | 96 | # My life is flashing before my eyes... 97 | for path in paths: 98 | reincarnate(kb, path) 99 | 100 | kb.save() 101 | 102 | if __name__ == "__main__": 103 | 104 | main() 105 | -------------------------------------------------------------------------------- /karmabot/core/commands/command.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | import re 7 | from karmabot.core import storage 8 | 9 | 10 | # TODO: regular expressions in this module should be 11 | # replaced with something more robust and more efficient. 12 | 13 | # TODO: stripping listen commands such as --/++ 14 | class Command(object): 15 | 16 | def __init__(self, parent, format, handler, 17 | help=None, visible=True, exclusive=False): 18 | self.parent = parent 19 | self.format = format 20 | self.handler = handler 21 | self.help = help 22 | self.visible = visible 23 | self.exclusive = exclusive 24 | self.state = None 25 | 26 | def to_regex(self): 27 | def sub_parameter(match): 28 | name = match.group(1) 29 | if name == "subject": 30 | parameter_regex = r"(?:\([^()]+\))|[#!\w]+" 31 | else: 32 | # This regex may come back to haunt me. 33 | parameter_regex = r".+" 34 | 35 | return r"(?P<{name}>{regex})".format(name=name, 36 | regex=parameter_regex) 37 | 38 | regex = self.format 39 | regex = regex.replace("+", r"\+") 40 | regex = re.sub(r"{(\w+)}", sub_parameter, regex) 41 | return regex 42 | 43 | 44 | class CommandParser(object): 45 | 46 | def __init__(self, command_infos): 47 | self.command_infos = command_infos 48 | 49 | def __call__(self, text, context, handled=False): 50 | return self.handle_command(text, context, handled) 51 | 52 | def handle_command(self, text, context, handled=False): 53 | for command_info in self.command_infos: 54 | match = command_info["re"].search(text) 55 | 56 | if match: 57 | instance = None 58 | match_group = match.groupdict() 59 | subject = match_group.get('subject', None) 60 | command = command_info['command'] 61 | match_group.update({'context': context}) 62 | 63 | if subject: 64 | match_group.update( 65 | {'subject': storage.db.get(subject)}) 66 | handler_cls = command.handler.__module__.split('.').pop() 67 | instance = match_group['subject'].facets.get(handler_cls) 68 | 69 | substitution = self.dispatch_command(command, 70 | instance, match_group) 71 | handled = True 72 | if substitution: 73 | # Start over with the new string 74 | newtext = ''.join([text[:match.start()], substitution, 75 | text[match.end():]]) 76 | return self.handle_command(newtext, context, True) 77 | 78 | if command_info["exclusive"]: 79 | break 80 | return (handled, text) 81 | 82 | def dispatch_command(self, command, instance, kw): 83 | if instance: 84 | context = kw.get('context') 85 | command.handler(instance, **kw) 86 | if context: 87 | storage.db.set(instance.subject.key, 88 | instance.subject) 89 | return None 90 | else: 91 | return command.handler(command, **kw) 92 | -------------------------------------------------------------------------------- /karmabot/extensions/cs_schedule.py: -------------------------------------------------------------------------------- 1 | """ 2 | An extension/facet for karmabot that will respond to queries for PSU CS course 3 | informmation with information gathered by crawling cs.pdx.edu for said course 4 | info. Depends on Beautiful Soup. 5 | """ 6 | from collections import OrderedDict 7 | import time 8 | from urllib import urlencode 9 | from urllib2 import urlopen 10 | 11 | from BeautifulSoup import BeautifulSoup 12 | 13 | from karmabot.core.client import thing 14 | from karmabot.core.facets import Facet 15 | from karmabot.core.register import facet_registry 16 | from karmabot.core.commands.sets import CommandSet 17 | 18 | #TODO: 19 | # Get rid of course command syntax? maybe? 20 | # Add error handing to webpage fetching. 21 | # Search with different keys. 22 | 23 | 24 | @facet_registry.register 25 | class ScheduleFacet(Facet): 26 | """ 27 | Class which implements the ThingFacet interface and provides the new 28 | karmabot course info reporting functionality. 29 | """ 30 | name = "course" 31 | commands = thing.add_child(CommandSet(name)) 32 | URL = "http://cs.pdx.edu/schedule/termschedule?" 33 | 34 | @commands.add(u"course {CSXXX} {TERM} {YEAR}", 35 | u"Get course information from CS website.") 36 | def course(self, context, CSXXX, TERM, YEAR): 37 | """ 38 | High level command handler for the course command. Manages 39 | course cache and recrawls if timeout is exceeded. 40 | """ 41 | CSXXX = CSXXX.replace('CS', '').strip() 42 | sched_key = TERM + YEAR 43 | cur_time = time.time() 44 | response = "" 45 | _cls = ScheduleFacet 46 | defaults = {"ret_times": {}, "schedules": {}} 47 | 48 | if self.state: 49 | sched_state = self.state.get("cs_sched_state", defaults) 50 | else: 51 | self.state = dict() 52 | sched_state = defaults 53 | 54 | if cur_time - sched_state["ret_times"].get(sched_key, 0) > 3600: 55 | params = urlencode({"term": TERM, "year": YEAR}) 56 | url = _cls.URL + params 57 | result_rows = _cls.apply_schema(_cls.scrape(url)) 58 | sched_state["schedules"][sched_key] = result_rows 59 | sched_state["ret_times"][sched_key] = cur_time 60 | 61 | for course in sched_state["schedules"][sched_key]: 62 | if CSXXX in course["Course"]: 63 | nothanks = [u'Notes', u'Sec', u'', u'-'] 64 | filt = lambda k, v: (True if k not in nothanks 65 | and v not in nothanks else False) 66 | course_str = ' '.join(v for k, v in course.iteritems() 67 | if filt(k, v)) 68 | response = response + course_str + "\n" 69 | 70 | self.state.update({'cs_sched_state': sched_state}) 71 | context.reply(response) 72 | 73 | @classmethod 74 | def does_attach(cls, thing): 75 | """Facet does attach. Return name to signify this.""" 76 | 77 | return thing.name == "course" 78 | 79 | @classmethod 80 | def apply_schema(self, rows): 81 | schema = rows.pop(0) 82 | return [OrderedDict(zip(schema, row)) for row in rows] 83 | 84 | @classmethod 85 | def scrape(self, url): 86 | url_data = urlopen(url) 87 | soup = BeautifulSoup(url_data.read()) 88 | table = soup.findAll('table')[1] 89 | result = [] 90 | 91 | for row in table.findAll('tr'): 92 | tr_row = list() 93 | for td in row.findAll('td'): 94 | td_str = list() 95 | for td_sub in td.recursiveChildGenerator(): 96 | if (td_sub.string and td_sub.string.strip() is not u'' and 97 | td_sub.string.strip() not in td_str): 98 | td_str.append(td_sub.string.strip()) 99 | 100 | if td_str not in tr_row: 101 | tr_row.append(''.join(td_str)) 102 | result.append(tr_row) 103 | 104 | return result 105 | -------------------------------------------------------------------------------- /karmabot/core/client.py: -------------------------------------------------------------------------------- 1 | # Copyright the Karmabot authors and contributors. 2 | # All rights reserved. See AUTHORS. 3 | # 4 | # This file is part of 'karmabot' and is distributed under the BSD license. 5 | # See LICENSE for more details. 6 | import random 7 | 8 | from twisted.words.protocols import irc 9 | from twisted.internet import reactor, task 10 | from twisted.internet.protocol import ReconnectingClientFactory 11 | from twisted.python import log 12 | 13 | from .signal import post_connection 14 | from karmabot.core import storage 15 | from .commands import listen, action 16 | from .facets.manager import FacetManager 17 | 18 | 19 | class Context(object): 20 | 21 | def __init__(self, user, where, bot, private=False): 22 | self.user = user 23 | self.where = where 24 | self.bot = bot 25 | self.replied = False 26 | self.private = private 27 | 28 | @property 29 | def nick(self): 30 | return self.user.split("!", 1)[0] 31 | 32 | def reply(self, msg, where=None, replied=True): 33 | if not where: 34 | where = self.where 35 | self.bot.msg(where, msg, priv=self.private) 36 | self.replied = replied 37 | 38 | 39 | class KarmaBot(irc.IRCClient): 40 | affirmative_prefixes = [u"Affirmative", u"Alright", u"Done", u"K", u"OK", 41 | u"Okay", u"Sure", u"Yes"] 42 | huh_msgs = [u"Huh?", u"What?"] 43 | 44 | def connectionMade(self): 45 | self.nickname = self.factory.nick 46 | self.password = self.factory.password 47 | self.ignores = ['Global', self.nickname] 48 | irc.IRCClient.connectionMade(self) 49 | post_connection.send() 50 | self.init() 51 | 52 | def init(self): 53 | self.facet_manager = FacetManager() 54 | self.facet_manager.load_core() 55 | self.facet_manager.load_extensions(self.factory.extensions) 56 | self.command_parser = action.compile() 57 | self.listen_parser = listen.compile() 58 | self.save_timer = task.LoopingCall(self.save) 59 | self.save_timer.start(60.0 * 5, now=False) 60 | 61 | def connectionLost(self, reason): 62 | log.msg("Disconnected") 63 | storage.db.save() 64 | irc.IRCClient.connectionLost(self, reason) 65 | 66 | def signedOn(self): 67 | log.msg("Connected") 68 | for channel in self.factory.channels: 69 | self.join_with_key(channel) 70 | 71 | def join_with_key(self, channel): 72 | if ":" in channel: 73 | channel, key = channel.split(":") 74 | else: 75 | key = None 76 | log.msg("Joining {0}".format(channel)) 77 | self.join(channel, key) 78 | 79 | def save(self): 80 | log.msg("Saving data") 81 | storage.db.save() 82 | 83 | def topicUpdated(self, user, channel, newTopic): 84 | subject = storage.db.get(channel) 85 | subject.facets["ircchannel"].topic = newTopic 86 | 87 | def error_msg(self, channel): 88 | self.msg(channel, random.choice(self.huh_msgs)) 89 | 90 | def msg(self, channel, message, length=160, priv=False): 91 | """ 92 | Repsonds with unicode only, complies with RFC 1459 93 | http://irchelp.org/irchelp/rfc/rfc.html 94 | """ 95 | if type(message) is unicode: 96 | message = message.encode("utf-8") 97 | if not priv: 98 | message = message[:510] 99 | log.msg('[{channel}] {message}'.format(channel=channel, 100 | message=message)) 101 | for line in message.split("\n"): 102 | irc.IRCClient.msg(self, channel, line, None) 103 | 104 | def privmsg(self, user, channel, msg): 105 | log.msg("[{channel}] {user}: {msg}".format(channel=channel, 106 | user=user, msg=msg)) 107 | msg = msg.decode("utf-8") 108 | context = Context(user, channel, self) 109 | if context.nick in self.ignores: 110 | return 111 | context.private = (channel == self.nickname and 112 | context.nick != self.nickname) 113 | 114 | listen_handled, msg = self.listen_parser.handle_command(msg, context) 115 | 116 | # Addressed (either in channel or by private message) 117 | command = None 118 | 119 | if msg.startswith(self.nickname) or context.private: 120 | if not context.private: 121 | command = msg[len(self.factory.nick):].lstrip(" ,:").rstrip() 122 | else: 123 | channel = context.nick 124 | context.where = context.nick 125 | command = msg.rstrip() 126 | 127 | handled, response = self.command_parser(command, context) 128 | if not handled: 129 | self.error_msg(channel) 130 | elif not context.replied: 131 | self.tell_yes(channel, context.nick) 132 | 133 | def tell_yes(self, who, nick): 134 | self.msg(who, u"{yesmsg}, {nick}.".format( 135 | yesmsg=random.choice(self.affirmative_prefixes), nick=nick)) 136 | 137 | 138 | class KarmaBotFactory(ReconnectingClientFactory): 139 | protocol = KarmaBot 140 | 141 | def __init__(self, filename, nick, channels, trusted, password=None, 142 | extensions=[]): 143 | self.nick = nick 144 | self.channels = channels 145 | self.filename = filename 146 | self.trusted = trusted 147 | self.password = password 148 | self.extensions = extensions 149 | 150 | def buildProtocol(self, addr): 151 | # Reset the ReconnectingClientFactory reconnect delay because we don't 152 | # want the next disconnect to force karmabot to delay forever. 153 | self.resetDelay() 154 | return ReconnectingClientFactory.buildProtocol(self, addr) 155 | 156 | def clientConnectionLost(self, connector, reason): 157 | ReconnectingClientFactory.clientConnectionLost(self, connector, reason) 158 | 159 | def clientConnectionFailed(self, connector, reason): 160 | reactor.stop() 161 | --------------------------------------------------------------------------------