modernize module system: 5.0.0

This commit is contained in:
dave 2017-11-27 18:58:20 -08:00
parent 5b68793f4f
commit d55e111767
42 changed files with 968 additions and 871 deletions

View File

@ -6,8 +6,8 @@ Quick start
----------- -----------
* Install: `python3 setup.py install` * Install: `python3 setup.py install`
* Configure: `cd examples ; vim config.json data/config/Services.json` * Configure: `vim examples/config.json examples/data/config/Services.json`
* Run: `pyircbot -c config.json` * Run: `pyircbot -c examples/config.json`
Running in docker Running in docker
----------------- -----------------

131
bin/pubsubbot Executable file
View File

@ -0,0 +1,131 @@
#!/usr/bin/env python3
import sys
import os
import logging
from contextlib import closing
from argparse import ArgumentParser
from json import loads, load
from msgbus.client import MsgbusSubClient
import pyircbot
import traceback
from pyircbot.pyircbot import PrimitiveBot
from pyircbot.irccore import IRCEvent, UserPrefix
from json import dumps
# IRCEvent = namedtuple("IRCEvent", "command args prefix trailing")
# UserPrefix = namedtuple("UserPrefix", "nick username hostname")
# ServerPrefix = namedtuple("ServerPrefix", "hostname")
class PyIRCBotSub(PrimitiveBot):
def __init__(self, name, host, port, config):
super().__init__(config)
self.name = name
self.host = host
self.port = port
self.client = None # PubSub socket
def run(self):
# Connect to msgbus and loop through messages
with closing(MsgbusSubClient(self.host, self.port)) as self.client:
self.client.sub("pyircbot_privmsg")#TODO More of these
while True:
try:
channel, body = self.client.recv()
self.process_line(channel, body)
except Exception as e:
traceback.print_exc()
def process_line(self, channel, body):
name, rest = body.split(" ", 1)
if name != self.name:
return
# pyircbot_privmsg default
# [["#jesusandhacking"], "xMopxShell", "test", {"prefix": ["xMopxShell", "~xMopxShel", "192.95.23.134"]}]
args, sender, trailing, extras = loads(rest)
nick, username, hostname = extras["prefix"]
msg = IRCEvent("PRIVMSG",
args,
UserPrefix(nick,
username,
hostname),
trailing)
for module_name, module in self.moduleInstances.items():
for hook in module.irchooks:
validation = hook.validator(msg, self)
if validation:
hook.method(msg, validation)
# client.pub("pyircbot_send", "default privmsg {}".format(dumps([channel, "{}: pong".format(sender)])))
" Filesystem Methods "
def getConfigPath(self, moduleName):
"""Return the absolute path for a module's config file
:param moduleName: the module who's config file we want
:type moduleName: str"""
basepath = "%s/config/%s" % (self.botconfig["bot"]["datadir"], moduleName)
if os.path.exists("%s.json" % basepath):
return "%s.json" % basepath
return None
def getDataPath(self, moduleName):
"""Return the absolute path for a module's data dir
:param moduleName: the module who's data dir we want
:type moduleName: str"""
module_dir = os.path.join(self.botconfig["bot"]["datadir"], "data", moduleName)
if not os.path.exists(module_dir):
os.mkdir(module_dir)
return module_dir
" IRC methods "
def act_PRIVMSG(self, towho, message):
"""Use the `/msg` command
:param towho: the target #channel or user's name
:type towho: str
:param message: the message to send
:type message: str"""
# self.sendRaw("PRIVMSG %s :%s" % (towho, message))
self.client.pub("pyircbot_send", "{} {} {}".format(self.name, "privmsg", dumps([towho, message])))
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO,
format="%(asctime)-15s %(levelname)-8s %(filename)s:%(lineno)d %(message)s")
log = logging.getLogger('main')
# parse command line args
parser = ArgumentParser(description="Run pyircbot plugins behind a pubsub client")
parser.add_argument("-c", "--config", help="Pyircbot config file")
parser.add_argument("-s", "--server", default="localhost", help="Msgbus server address")
parser.add_argument("-p", "--port", default=7100, type=int, help="Msgbus server port")
parser.add_argument("-n", "--name", default="default", help="bot name")
parser.add_argument("--debug", action="store_true", help="")
args = parser.parse_args()
if args.debug:
logging.getLogger().setLevel(logging.DEBUG)
log.debug(args)
# Load config
with open(args.config) as f:
config = load(f)
bot = PyIRCBotSub(args.name, args.server, int(args.port), config)
# Load modules in config
sys.path.append(os.path.dirname(pyircbot.__file__) + "/modules/")
for modulename in config["modules"]:
print("Load: ", modulename)
bot.loadmodule(modulename)
bot.run()

View File

@ -3,6 +3,7 @@ import sys
import logging import logging
import signal import signal
from argparse import ArgumentParser from argparse import ArgumentParser
from pyircbot.common import load
from pyircbot import PyIRCBot from pyircbot import PyIRCBot
from json import loads from json import loads
@ -30,7 +31,7 @@ if __name__ == "__main__":
log.critical("No bot config file specified (-c). Exiting.") log.critical("No bot config file specified (-c). Exiting.")
sys.exit(1) sys.exit(1)
botconfig = loads(sys.stdin.read()) if args.config == "-" else PyIRCBot.load(args.config) botconfig = loads(sys.stdin.read()) if args.config == "-" else load(args.config)
log.debug(botconfig) log.debug(botconfig)

View File

@ -13,6 +13,7 @@ Contents:
modules/_modules.rst modules/_modules.rst
rpc/_rpc.rst rpc/_rpc.rst
module_guide/_module_guide.rst module_guide/_module_guide.rst
module_guide/_pubsub_mode.rst
changelog.rst changelog.rst
More Information More Information

View File

@ -74,6 +74,8 @@ Prefix may also be a ``ServerPrefix`` object, if the hook is for an IRC method
that interacts with the server directly, such as PING. It would have the that interacts with the server directly, such as PING. It would have the
properties ``event.prefix.hostname`` and ``event.prefix.str``. properties ``event.prefix.hostname`` and ``event.prefix.str``.
There are more hook-like decorators. See @regex and @command.
Since the module described above echos messages, let's do that: Since the module described above echos messages, let's do that:
.. code-block:: python .. code-block:: python
@ -134,30 +136,6 @@ In usage:
4:40:17 PM <Beefpile> test 4:40:17 PM <Beefpile> test
4:40:17 PM <derpbot420> test 4:40:17 PM <derpbot420> test
New Style Module Hooks
----------------------
Instead of receiving the values of the IRC event a module is responding to in
3 separate arguments, hooks can receive them as one object. The hook system
will automatically determine which argument style to use.
The reason for this change is to eliminate some unnecessary code in modules.
Any module that looks at a user's nick or hostname may find itself doing
something like this in every hook:
.. code-block:: python
def saynick(self, args, prefix, trailing):
prefixObj = self.bot.decodePrefix(prefix)
self.bot.act_PRIVMSG(args[0], "Hello, %s. You are connecting from %s" % (prefixObj.nick, prefixObj.hostname))
With the new style, one line can be eliminated, as the passed ``IRCEvent``
event has the prefix already parsed:
.. code-block:: python
def saynick(self, event):
self.bot.act_PRIVMSG(event.args[0], "Hello, %s. You are connecting from %s" % (event.prefix.nick, event.prefix.hostname))
Advanced Usage Advanced Usage
============== ==============

View File

@ -0,0 +1,31 @@
***********
PubSub Mode
***********
The PubSubClient module provides a ZeroMQ-based PubSub remote control capability to pyircbot. Well, what if you could
also run pyircbot modules as a client of the pubsub bus? Look no further!
Obviously, the :py:class:`pyircbot.modules.PubSubClient.PubSubClient` module should be enabled. Take note of the `name`
parameter (which may be left at the default "default").
The so-called footless (as opposed to headless, as the bot's head is the IRC client connection) client needs only a
small set of bootstrap modules:
- PingResponder (:py:class:`pyircbot.modules.PingResponder.PingResponder`)
- Services (:py:class:`pyircbot.modules.Services.Services`)
- PubSubClient (:py:class:`pyircbot.modules.PubSubClient.PubSubClient`)
Launch the bot and let it connect to the msgbus server.
Next, create a config identical to that of a normal pyircbot, but with any modules desired enabled. Also, read the
`--help` text for the `pubsubbot` program. Launch the `pubsubbot` process:
.. code-block:: shell
pubsubbot -c config-sub.json --name <name>
After connecting to the message bus, `pubsubbot` will be hosting any configured modules. It can be exited and restarted
any time without affecting the IRC client connection!
For a module to support running in this mode, it must use only methods described in the Module Developers Guide. Using
APIs outside of this - while not discouraged - will definitely break compatibility.

View File

@ -1,6 +1,6 @@
{ {
"bot":{ "bot":{
"datadir":"./data/", "datadir":"./examples/data/",
"rpcbind":"0.0.0.0", "rpcbind":"0.0.0.0",
"rpcport":1876, "rpcport":1876,
"usermodules": [ "./data/modules/" ] "usermodules": [ "./data/modules/" ]

View File

@ -1,8 +1,8 @@
{ {
"limit": 25, "limit": 25,
"recv_msg": "Oh, thanks, I'll keep %(adjective)s%(item)s safe", "recv_msg": "Oh, thanks, I'll keep %(adjective)s%(item)s safe",
"inv_msg": "\u0000\u0001ACTION is carrying %(itemlist)s\u0000\u0001", "inv_msg": "\u0001ACTION is carrying %(itemlist)s\u0001",
"swap_msg": "\u0000\u0001ACTION takes %(adjective)s%(recv_item)s but drops %(drop_item)s\u0000\u0010", "swap_msg": "\u0001ACTION takes %(adjective)s%(recv_item)s but drops %(drop_item)s\u0010",
"dupe_msg": "No thanks, I've already got %(item)s", "dupe_msg": "No thanks, I've already got %(item)s",
"adjectives": [ "adjectives": [
"some", "some",
@ -10,4 +10,4 @@
"an", "an",
"these" "these"
] ]
} }

View File

@ -1,2 +0,0 @@
#!/bin/sh
pyircbot --config config.json

View File

@ -1,5 +1,10 @@
from time import time from time import time
from math import floor from math import floor
from json import load as json_load
from collections import namedtuple
ParsedCommand = namedtuple("ParsedCommand", "command args args_str message")
class burstbucket(object): class burstbucket(object):
@ -39,3 +44,63 @@ class burstbucket(object):
self.bucket -= 1 self.bucket -= 1
return 0 return 0
return self.bucket_period - since_fill return self.bucket_period - since_fill
def messageHasCommand(command, message, requireArgs=False):
"""Check if a message has a command with or without args in it
:param command: the command string to look for, like !ban. If a list is passed, the first match is returned.
:type command: str or list
:param message: the message string to look in, like "!ban Lil_Mac"
:type message: str
:param requireArgs: if true, only validate if the command use has any amount of trailing text
:type requireArgs: bool"""
if not type(command) == list:
command = [command]
for item in command:
cmd = messageHasCommandSingle(item, message, requireArgs)
if cmd:
return cmd
return False
def messageHasCommandSingle(command, message, requireArgs=False):
# Check if the message at least starts with the command
messageBeginning = message[0:len(command)]
if messageBeginning != command:
return False
# Make sure it's not a subset of a longer command (ie .meme being set off by .memes)
subsetCheck = message[len(command):len(command) + 1]
if subsetCheck != " " and subsetCheck != "":
return False
# We've got the command! Do we need args?
argsStart = len(command)
args = ""
if argsStart > 0:
args = message[argsStart + 1:].strip()
if requireArgs and args == '':
return False
# Verified! Return the set.
return ParsedCommand(command,
args.split(" ") if args else [],
args,
message)
def load(filepath):
"""Return an object from the passed filepath
:param filepath: path to a json file. filename must end with .json
:type filepath: str
:Returns: | dict
"""
if filepath.endswith(".json"):
with open(filepath, 'r') as f:
return json_load(f)
else:
raise Exception("Unknown config format")

View File

@ -172,6 +172,7 @@ class IRCCore(object):
def initHooks(self): def initHooks(self):
"""Defines hooks that modules can listen for events of""" """Defines hooks that modules can listen for events of"""
self.hooks = [ self.hooks = [
'_ALL',
'_CONNECT', '_CONNECT',
'_DISCONNECT', '_DISCONNECT',
'_RECV', '_RECV',
@ -222,7 +223,7 @@ class IRCCore(object):
:type prefix: str :type prefix: str
:param trailing: data payload of the command :param trailing: data payload of the command
:type trailing: str""" :type trailing: str"""
for hook in self.hookcalls[command]: for hook in self.hookcalls["_ALL"] + self.hookcalls[command]:
try: try:
if len(getargspec(hook).args) == 2: if len(getargspec(hook).args) == 2:
hook(IRCCore.packetAsObject(command, args, prefix, trailing)) hook(IRCCore.packetAsObject(command, args, prefix, trailing))

View File

@ -9,7 +9,8 @@
import re import re
import os import os
import logging import logging
from .pyircbot import PyIRCBot from .common import load as pload
from .common import messageHasCommand
class ModuleBase: class ModuleBase:
@ -28,9 +29,6 @@ class ModuleBase:
self.bot = bot self.bot = bot
"""Reference to the master PyIRCBot object""" """Reference to the master PyIRCBot object"""
self.hooks = []
"""Low-level protocol hooks this module has"""
self.irchooks = [] self.irchooks = []
"""IRC Hooks this module has""" """IRC Hooks this module has"""
@ -73,7 +71,7 @@ class ModuleBase:
if not self.config: if not self.config:
configPath = self.getConfigPath() configPath = self.getConfigPath()
if configPath is not None: if configPath is not None:
self.config = PyIRCBot.load(configPath) self.config = pload(configPath)
def onenable(self): def onenable(self):
"""Called when the module is enabled""" """Called when the module is enabled"""
@ -228,7 +226,7 @@ class command(hook):
""" """
if not super().validate(msg, bot): if not super().validate(msg, bot):
return False return False
if not self.allow_private and msg.args[0] == "#": if msg.args[0][0] != "#" and not self.allow_private:
return False return False
for keyword in self.keywords: for keyword in self.keywords:
single = self._validate_one(msg, keyword) single = self._validate_one(msg, keyword)
@ -238,7 +236,7 @@ class command(hook):
def _validate_one(self, msg, keyword): def _validate_one(self, msg, keyword):
with_prefix = "{}{}".format(self.prefix, keyword) with_prefix = "{}{}".format(self.prefix, keyword)
return PyIRCBot.messageHasCommand(with_prefix, msg.trailing, requireArgs=self.require_args) return messageHasCommand(with_prefix, msg.trailing, requireArgs=self.require_args)
class regex(hook): class regex(hook):

View File

@ -13,7 +13,6 @@ from pyircbot.modulebase import ModuleBase
class AttributeStorage(ModuleBase): class AttributeStorage(ModuleBase):
def __init__(self, bot, moduleName): def __init__(self, bot, moduleName):
ModuleBase.__init__(self, bot, moduleName) ModuleBase.__init__(self, bot, moduleName)
self.hooks = []
self.services = ["attributes"] self.services = ["attributes"]
self.db = None self.db = None
serviceProviders = self.bot.getmodulesbyservice("mysql") serviceProviders = self.bot.getmodulesbyservice("mysql")

View File

@ -13,7 +13,6 @@ from pyircbot.modulebase import ModuleBase
class AttributeStorageLite(ModuleBase): class AttributeStorageLite(ModuleBase):
def __init__(self, bot, moduleName): def __init__(self, bot, moduleName):
ModuleBase.__init__(self, bot, moduleName) ModuleBase.__init__(self, bot, moduleName)
self.hooks = []
self.services = ["attributes"] self.services = ["attributes"]
self.db = None self.db = None
serviceProviders = self.bot.getmodulesbyservice("sqlite") serviceProviders = self.bot.getmodulesbyservice("sqlite")

View File

@ -6,7 +6,9 @@
""" """
from pyircbot.modulebase import ModuleBase, ModuleHook from pyircbot.modulebase import ModuleBase, command
from pyircbot.modules.ModInfo import info
from decimal import Decimal
from requests import get from requests import get
from time import time from time import time
@ -18,25 +20,23 @@ class BitcoinPrice(ModuleBase):
self.cache = None self.cache = None
self.cacheAge = 0 self.cacheAge = 0
self.hooks = [ @info("btc retrieve the current price of bitcoin", cmds=["btc"])
ModuleHook(["PRIVMSG"], self.btc) @command("btc", "bitcoin")
] def btc(self, msg, cmd):
replyTo = msg.prefix.nick if "#" not in msg.args[0] else msg.args[0]
def btc(self, args, prefix, trailing): data = self.getApi()
prefix = self.bot.decodePrefix(prefix) self.bot.act_PRIVMSG(replyTo, "%s: %s" % (
replyTo = prefix.nick if "#" not in args[0] else args[0] msg.prefix.nick,
"\x02\x0307Bitcoin:\x03\x02 \x0307${price:.2f}\x0f - "
cmd = self.bot.messageHasCommand([".btc", ".bitcoin"], trailing) "24h change: \x0307${change:.2f}\x0f - "
if cmd: "24h volume: \x0307${volume:.0f}M\x0f".format(price=Decimal(data["price_usd"]),
data = self.getApi() change=Decimal(data["percent_change_24h"]),
self.bot.act_PRIVMSG(replyTo, "%s: %s" % ( volume=Decimal(data["24h_volume_usd"]) / 10**6)
prefix.nick, ))
"\x02\x0307Bitcoin:\x03\x02 \x0307{buy:.0f}\x0f$ - High: \x0307{high:.0f}\x0f$ - "
"Low: \x0307{low:.0f}\x0f$ - Volume: \x0307{vol_cur:.0f}\x03฿".format(**data['ticker'])
))
def getApi(self): def getApi(self):
if self.cache is None or time() - self.cacheAge > self.config["cache"]: if self.cache is None or time() - self.cacheAge > self.config["cache"]:
self.cache = get("https://btc-e.com/api/2/btc_usd/ticker").json() self.cache = get("https://api.coinmarketcap.com/v1/ticker/bitcoin/").json()[0]
self.cacheAge = time() self.cacheAge = time()
return self.cache return self.cache

View File

@ -1,5 +1,5 @@
from pyircbot.modulebase import ModuleBase, ModuleHook, MissingDependancyException, regex, command from pyircbot.modulebase import ModuleBase, MissingDependancyException, regex, command
from pyircbot.modules.ModInfo import info from pyircbot.modules.ModInfo import info
import datetime import datetime
import time import time
@ -93,12 +93,13 @@ class Calc(ModuleBase):
if word and changeit: if word and changeit:
# Add a new calc or delete # Add a new calc or delete
if self.config["allowDelete"] and not value: if not value:
result = self.deleteCalc(channel, word) if self.config["allowDelete"]:
if result: result = self.deleteCalc(channel, word)
self.bot.act_PRIVMSG(channel, "Calc deleted, %s." % sender) if result:
else: self.bot.act_PRIVMSG(channel, "Calc deleted, %s." % sender)
self.bot.act_PRIVMSG(channel, "Sorry %s, I don't know what '%s' is." % (sender, word)) else:
self.bot.act_PRIVMSG(channel, "Sorry %s, I don't know what '%s' is." % (sender, word))
else: else:
if self.config["delaySubmit"] > 0 and self.timeSince(channel, "add") < self.config["delaySubmit"]: if self.config["delaySubmit"] > 0 and self.timeSince(channel, "add") < self.config["delaySubmit"]:
self.bot.act_PRIVMSG(channel, self.remainingToStr(self.config["delaySubmit"], self.bot.act_PRIVMSG(channel, self.remainingToStr(self.config["delaySubmit"],

View File

@ -15,7 +15,6 @@ from threading import Thread
class CryptoWalletRPC(ModuleBase): class CryptoWalletRPC(ModuleBase):
def __init__(self, bot, moduleName): def __init__(self, bot, moduleName):
ModuleBase.__init__(self, bot, moduleName) ModuleBase.__init__(self, bot, moduleName)
self.hooks = []
self.services = ["bitcoinrpc"] self.services = ["bitcoinrpc"]
self.rpcservices = {} self.rpcservices = {}
self.loadrpcservices() self.loadrpcservices()

View File

@ -14,7 +14,6 @@ from bitcoinrpc.authproxy import AuthServiceProxy
class DogeRPC(ModuleBase): class DogeRPC(ModuleBase):
def __init__(self, bot, moduleName): def __init__(self, bot, moduleName):
ModuleBase.__init__(self, bot, moduleName) ModuleBase.__init__(self, bot, moduleName)
self.hooks = []
self.services = ["dogerpc"] self.services = ["dogerpc"]
self.rpc = DogeController(self) self.rpc = DogeController(self)

View File

@ -7,7 +7,8 @@
""" """
from pyircbot.modulebase import ModuleBase, ModuleHook from pyircbot.modulebase import ModuleBase, command
from pyircbot.modules.ModInfo import info
import time import time
import json import json
import random import random
@ -18,7 +19,6 @@ import os
class DuckHunt(ModuleBase): class DuckHunt(ModuleBase):
def __init__(self, bot, moduleName): def __init__(self, bot, moduleName):
ModuleBase.__init__(self, bot, moduleName) ModuleBase.__init__(self, bot, moduleName)
self.hooks = [ModuleHook("PRIVMSG", self.hunt)]
self.jsonPath = self.getFilePath("scores.json") self.jsonPath = self.getFilePath("scores.json")
@ -29,109 +29,103 @@ class DuckHunt(ModuleBase):
self.startHunt() self.startHunt()
def hunt(self, args, prefix, trailing): @info("huntscore show your duckhunt score", cmds=["huntscore"])
prefixObj = self.bot.decodePrefix(prefix) @command("huntscore", allow_private=True)
fromWho = prefixObj.nick def hunt(self, msg, cmd):
scores = self.loadScores()
fromWho = msg.prefix.nick
if fromWho not in scores:
self.bot.act_PRIVMSG(fromWho, "You have no points :(")
else:
scores = scores[fromWho]
kills = 0
runts = 0
prime = 0
weight = 0.0
shots = 0
misses = 0
for kill in scores:
if kill["prime"]:
prime += 1
if kill["runt"]:
runts += 1
kills += 1
weight += kill["weight"]
shots += 1
shots += kill["misses"]
misses += kill["misses"]
cmd = self.bot.messageHasCommand("!huntscore", trailing, False) self.bot.act_PRIVMSG(fromWho, "You've shot %s %s for a total weight of %s lbs." %
if cmd: (kills, self.config["animalSpeciesPlural"], weight))
scores = self.loadScores() self.bot.act_PRIVMSG(fromWho, "%s prime catches, %s runts, %s bullets used and %s misses." %
if fromWho not in scores: (prime, runts, shots, misses))
self.bot.act_PRIVMSG(fromWho, "You have no points :(") # self.bot.act_PRIVMSG(fromWho, "More info & highscores: http://duckhunt.xmopx.net/")
@info("shoot shoot active targets", cmds=["shoot"])
@command("shoot")
def cmd_shoot(self, msg, args):
if self.isDuckOut:
fromWho = msg.prefix.nick
if fromWho not in self.misses:
self.misses[fromWho] = 0
shotIn = round(time.time() - self.outStart, 2)
if random.randint(0, 100) <= self.config["missChance"]:
self.bot.act_PRIVMSG(self.config["activeChannel"], "%s fires after %s seconds and misses!" %
(fromWho, shotIn))
self.misses[fromWho] += 1
return
self.isDuckOut = False
bagged = {
"species": self.config["animalSpecies"],
"gender": "M" if random.randint(0, 1) == 1 else "F",
"time": shotIn,
"prime": False,
"runt": False,
"weight": 0.0,
"date": time.time(),
"misses": self.misses[fromWho]
}
message = "%s %s " % (fromWho, "bags")
if random.randint(0, 100) <= self.config["primeChance"]:
bagged["prime"] = True
bagged["weight"] = self.getRandWeight(self.config["weightMax"], self.config["weightFat"])
message += "a prime catch, a "
elif random.randint(0, 100) <= self.config["runtChance"]:
bagged["runt"] = True
bagged["weight"] = self.getRandWeight(self.config["weightRunt"], self.config["weightMin"])
message += "a runt of a catch, a "
else: else:
scores = scores[fromWho] bagged["weight"] = self.getRandWeight(self.config["weightMin"], self.config["weightMax"])
kills = 0 message += "a "
runts = 0
prime = 0
weight = 0.0
shots = 0
misses = 0
for kill in scores:
if kill["prime"]:
prime += 1
if kill["runt"]:
runts += 1
kills += 1
weight += kill["weight"]
shots += 1
shots += kill["misses"]
misses += kill["misses"]
self.bot.act_PRIVMSG(fromWho, "You've shot %s %s for a total weight of %s lbs." % message += "%s lb " % (bagged["weight"])
(kills, self.config["animalSpeciesPlural"], weight)) if bagged["gender"] == "M":
self.bot.act_PRIVMSG(fromWho, "%s prime catches, %s runts, %s bullets used and %s misses." % message += self.config["animalNameMale"] + " "
(prime, runts, shots, misses)) else:
# self.bot.act_PRIVMSG(fromWho, "More info & highscores: http://duckhunt.xmopx.net/") message += self.config["animalNameFemale"] + " "
self.log.info("DuckHunt: %s used !huntscore" % fromWho)
return
# Channel only message += "in %s seconds!" % shotIn
if not args[0][0] == "#": self.bot.act_PRIVMSG(self.config["activeChannel"], message)
return
cmd = self.bot.messageHasCommand("!shoot", trailing, False) self.addKillFor(fromWho, bagged)
if cmd:
if self.isDuckOut:
if fromWho not in self.misses: self.misses = {}
self.misses[fromWho] = 0
shotIn = round(time.time() - self.outStart, 2) self.startHunt()
if random.randint(0, 100) <= self.config["missChance"]:
self.bot.act_PRIVMSG(self.config["activeChannel"], "%s fires after %s seconds and misses!" %
(fromWho, shotIn))
self.misses[fromWho] += 1
return
self.isDuckOut = False
bagged = {
"species": self.config["animalSpecies"],
"gender": "M" if random.randint(0, 1) == 1 else "F",
"time": shotIn,
"prime": False,
"runt": False,
"weight": 0.0,
"date": time.time(),
"misses": self.misses[fromWho]
}
message = "%s %s " % (fromWho, "bags")
if random.randint(0, 100) <= self.config["primeChance"]:
bagged["prime"] = True
bagged["weight"] = self.getRandWeight(self.config["weightMax"], self.config["weightFat"])
message += "a prime catch, a "
elif random.randint(0, 100) <= self.config["runtChance"]:
bagged["runt"] = True
bagged["weight"] = self.getRandWeight(self.config["weightRunt"], self.config["weightMin"])
message += "a runt of a catch, a "
else:
bagged["weight"] = self.getRandWeight(self.config["weightMin"], self.config["weightMax"])
message += "a "
message += "%s lb " % (bagged["weight"])
if bagged["gender"] == "M":
message += self.config["animalNameMale"] + " "
else:
message += self.config["animalNameFemale"] + " "
message += "in %s seconds!" % shotIn
self.bot.act_PRIVMSG(self.config["activeChannel"], message)
self.addKillFor(fromWho, bagged)
self.misses = {}
self.startHunt()
def startHunt(self): def startHunt(self):
" Creates a timer that waits a certain amount of time then sends out a bird \\_o< quack" " Creates a timer that waits a certain amount of time then sends out a bird \\_o< quack"
delay = self.config["delayMin"] + random.randint(0, self.config["delayMax"] - self.config["delayMin"]) delay = self.config["delayMin"] + random.randint(0, self.config["delayMax"] - self.config["delayMin"])
self.timer = Timer(delay, self.duckOut) self.timer = Timer(delay, self.duckOut)
self.timer.start() self.timer.start()
print("DuckHunt: Sending out animal in %s seconds" % delay) self.log.info(" Sending out animal in %s seconds" % delay)
def duckOut(self): def duckOut(self):
self.isDuckOut = True self.isDuckOut = True
@ -143,12 +137,6 @@ class DuckHunt(ModuleBase):
weight = float(weight) * random.random() weight = float(weight) * random.random()
return round(weight + minW, 2) return round(weight + minW, 2)
def getScoreFor(self, playername):
return 1
def getScoresFor(self, playername):
return 1
def addKillFor(self, playername, kill): def addKillFor(self, playername, kill):
scores = self.loadScores() scores = self.loadScores()
if scores is None: if scores is None:

View File

@ -7,14 +7,14 @@
""" """
from pyircbot.modulebase import ModuleBase, ModuleHook from pyircbot.modulebase import ModuleBase, command
from pyircbot.modules.ModInfo import info
from datetime import datetime from datetime import datetime
class Inventory(ModuleBase): class Inventory(ModuleBase):
def __init__(self, bot, moduleName): def __init__(self, bot, moduleName):
ModuleBase.__init__(self, bot, moduleName) ModuleBase.__init__(self, bot, moduleName)
self.hooks = []
self.db = None self.db = None
serviceProviders = self.bot.getmodulesbyservice("sqlite") serviceProviders = self.bot.getmodulesbyservice("sqlite")
if not serviceProviders: if not serviceProviders:
@ -33,42 +33,38 @@ class Inventory(ModuleBase):
) ;""") ) ;""")
c.close() c.close()
self.hooks = [ModuleHook("PRIVMSG", self.checkInv)] @info("have <item> give the bot an item", cmds=["have"])
@command("have")
def checkInv(self, args, prefix, trailing): def checkInv(self, msg, cmd):
if not args[0][0] == "#": if len(cmd.args) < 1:
return return
prefixObj = self.bot.decodePrefix(prefix) adjective = None
cmd = self.bot.messageHasCommand([".have"], trailing, True) newItem = cmd.args_str
if cmd: if cmd.args[0] in self.config["adjectives"]:
if len(cmd.args) < 1: newItem = cmd.args_str[len(cmd.args[0]):].strip()
return adjective = cmd.args[0]
adjective = None
if cmd.args[0] in self.config["adjectives"]:
cmd.args_str = cmd.args_str[len(cmd.args[0]):].strip()
adjective = cmd.args[0]
newItem = cmd.args_str
if self.has_item(newItem): if self.has_item(newItem):
self.bot.act_PRIVMSG(args[0], self.config["dupe_msg"] % {"item": newItem}) self.bot.act_PRIVMSG(msg.args[0], self.config["dupe_msg"] % {"item": newItem})
return return
dropped = self.add_item(prefixObj.nick, newItem) dropped = self.add_item(msg.prefix.nick, newItem)
if len(dropped) > 0: if len(dropped) > 0:
self.bot.act_PRIVMSG(args[0], self.config["swap_msg"] % self.bot.act_PRIVMSG(msg.args[0], self.config["swap_msg"] %
{"adjective": (adjective + " ") if adjective else "", {"adjective": (adjective + " ") if adjective else "",
"recv_item": newItem, "drop_item": ", ".join(dropped)}) "recv_item": newItem, "drop_item": ", ".join(dropped)})
else: else:
self.bot.act_PRIVMSG(args[0], self.config["recv_msg"] % self.bot.act_PRIVMSG(msg.args[0], self.config["recv_msg"] %
{"item": newItem, "adjective": "these " if newItem[-1:] == "s" else "this "}) {"item": newItem, "adjective": "these " if newItem[-1:] == "s" else "this "})
cmd = self.bot.messageHasCommand([".inventory", ".inv"], trailing) @info("inventory show the bot's inventory", cmds=["inventory", "inv"])
if cmd: @command("inventory", "inv")
inv = self.getinventory() def cmd_inv(self, msg, cmd):
if len(inv) == 0: inv = self.getinventory()
self.bot.act_PRIVMSG(args[0], self.config["inv_msg"] % {"itemlist": "nothing!"}) if len(inv) == 0:
else: self.bot.act_PRIVMSG(msg.args[0], self.config["inv_msg"] % {"itemlist": "nothing!"})
self.bot.act_PRIVMSG(args[0], self.config["inv_msg"] % {"itemlist": ", ".join(inv)}) else:
self.bot.act_PRIVMSG(msg.args[0], self.config["inv_msg"] % {"itemlist": ", ".join(inv)})
def has_item(self, itemName): def has_item(self, itemName):
c = self.db.query("SELECT COUNT(*) as `num` FROM `inventory` WHERE `item`=? COLLATE NOCASE", (itemName,)) c = self.db.query("SELECT COUNT(*) as `num` FROM `inventory` WHERE `item`=? COLLATE NOCASE", (itemName,))

View File

@ -7,24 +7,20 @@
""" """
import urllib.parse import urllib.parse
from pyircbot.modulebase import ModuleBase, ModuleHook from pyircbot.modulebase import ModuleBase, command
from pyircbot.modules.ModInfo import info
BASE_URL = "http://lmgtfy.com/?q=" BASE_URL = "http://lmgtfy.com/?q="
class LMGTFY(ModuleBase): class LMGTFY(ModuleBase):
def __init__(self, bot, moduleName):
ModuleBase.__init__(self, bot, moduleName)
self.hooks.append(ModuleHook("PRIVMSG", self.handleMessage))
self.bot = bot
def handleMessage(self, args, prefix, trailing): @info("lmgtfy <term> display a condescending internet query", cmds=["lmgtfy"])
channel = args[0] @command("lmgtfy", require_args=True)
prefix = self.bot.decodePrefix(prefix) def handleMessage(self, msg, cmd):
if self.bot.messageHasCommand(".lmgtfy", trailing): message = msg.trailing.split(" ")[1:]
message = trailing.split(" ")[1:] link = self.createLink(message)
link = self.createLink(message) self.bot.act_PRIVMSG(msg.args[0], "%s: %s" % (msg.prefix.nick, link))
self.bot.act_PRIVMSG(channel, "%s: %s" % (prefix.nick, link))
def createLink(self, message): def createLink(self, message):
finalUrl = BASE_URL finalUrl = BASE_URL

View File

@ -7,7 +7,7 @@
""" """
from pyircbot.modulebase import ModuleBase, ModuleHook from pyircbot.modulebase import ModuleBase, hook
from requests import get from requests import get
import re import re
import time import time
@ -22,16 +22,14 @@ class LinkTitler(ModuleBase):
def __init__(self, bot, moduleName): def __init__(self, bot, moduleName):
ModuleBase.__init__(self, bot, moduleName) ModuleBase.__init__(self, bot, moduleName)
self.REQUEST_SIZE_LIMIT = 10 * 1024 self.REQUEST_SIZE_LIMIT = 10 * 1024
self.hooks = [ModuleHook("PRIVMSG", self.searches)]
def searches(self, args, prefix, trailing): @hook("PRIVMSG")
t = Thread(target=self.doLinkTitle, args=(args, prefix, trailing)) def searches(self, msg, cmd):
t = Thread(target=self.doLinkTitle, args=(msg.args, msg.prefix.nick, msg.trailing))
t.daemon = True t.daemon = True
t.start() t.start()
def doLinkTitle(self, args, prefix, trailing): def doLinkTitle(self, args, sender, trailing):
sender = self.bot.decodePrefix(prefix)
# Youtube # Youtube
matches = re.compile(r'(?:youtube.*?(?:v=|/v/)|youtu\.be/|yooouuutuuube.*?id=)([-_a-z0-9]+)', re.I) \ matches = re.compile(r'(?:youtube.*?(?:v=|/v/)|youtu\.be/|yooouuutuuube.*?id=)([-_a-z0-9]+)', re.I) \
.findall(trailing) .findall(trailing)
@ -67,7 +65,7 @@ class LinkTitler(ModuleBase):
"domain": submission.domain, "domain": submission.domain,
"nsfw": "[NSFW]" if submission.over_18 else "", "nsfw": "[NSFW]" if submission.over_18 else "",
"points": submission.ups, "points": submission.ups,
"percent": "%s%%" % int(submission.upvote_ratio *100), "percent": "%s%%" % int(submission.upvote_ratio * 100),
"comments": submission.num_comments, "comments": submission.num_comments,
"author": submission.author.name, "author": submission.author.name,
"date": datetime.datetime.fromtimestamp(submission.created).strftime("%Y.%m.%d") "date": datetime.datetime.fromtimestamp(submission.created).strftime("%Y.%m.%d")
@ -98,11 +96,11 @@ class LinkTitler(ModuleBase):
# Fetch HTML title # Fetch HTML title
title = self.url_htmltitle(match[0]) title = self.url_htmltitle(match[0])
if title: if title:
self.bot.act_PRIVMSG(args[0], "%s: \x02%s\x02" % (sender.nick, title)) self.bot.act_PRIVMSG(args[0], "%s: \x02%s\x02" % (sender, title))
else: else:
# Unknown types, just print type and size # Unknown types, just print type and size
self.bot.act_PRIVMSG(args[0], "%s: \x02%s\x02, %s" % self.bot.act_PRIVMSG(args[0], "%s: \x02%s\x02, %s" %
(sender.nick, headers["Content-Type"], (sender, headers["Content-Type"],
self.nicesize(int(headers["Content-Length"])) if self.nicesize(int(headers["Content-Length"])) if
"Content-Length" in headers else "unknown size")) "Content-Length" in headers else "unknown size"))

View File

@ -34,8 +34,16 @@ class info(object):
self.commands = cmds or [] self.commands = cmds or []
def __call__(self, func): def __call__(self, func):
setattr(func, "irchelp", self.docstring) if hasattr(func, "irchelp"):
setattr(func, "irchelpc", self.commands) func.irchelp.append(self.docstring)
else:
setattr(func, "irchelp", [self.docstring])
if hasattr(func, "irchelpc"):
func.irchelpc.append(self.commands)
else:
setattr(func, "irchelpc", [self.commands])
return func return func
@ -77,5 +85,6 @@ class ModInfo(ModuleBase):
for attr_name in dir(module): for attr_name in dir(module):
attr = getattr(module, attr_name) attr = getattr(module, attr_name)
if callable(attr) and hasattr(attr, "irchelp"): if callable(attr) and hasattr(attr, "irchelp"):
yield (modname, module, getattr(attr, "irchelp"), getattr(attr, "irchelpc"), ) for cmdhelp, cmdaliases in zip(getattr(attr, "irchelp"), getattr(attr, "irchelpc")):
yield (modname, module, cmdhelp, cmdaliases, )
raise StopIteration() raise StopIteration()

View File

@ -19,7 +19,6 @@ except:
class MySQL(ModuleBase): class MySQL(ModuleBase):
def __init__(self, bot, moduleName): def __init__(self, bot, moduleName):
ModuleBase.__init__(self, bot, moduleName) ModuleBase.__init__(self, bot, moduleName)
self.hooks = []
self.services = ["mysql"] self.services = ["mysql"]
self.connection = self.getConnection() self.connection = self.getConnection()

View File

@ -6,7 +6,8 @@
""" """
from pyircbot.modulebase import ModuleBase, ModuleHook from pyircbot.modulebase import ModuleBase, command
from pyircbot.modules.ModInfo import info
from time import time from time import time
from requests import get from requests import get
from lxml import etree from lxml import etree
@ -19,112 +20,107 @@ class NFLLive(ModuleBase):
self.cache = None self.cache = None
self.cacheAge = 0 self.cacheAge = 0
self.hooks = [ModuleHook(["PRIVMSG"], self.nflitup)] @info("nfl show nfl schedule & score", cmds=["nfl"])
@command("nfl")
def nflitup(self, message, cmd):
games = self.getNflGamesCached()
msg = []
def nflitup(self, args, prefix, trailing): liveGames = []
prefix = self.bot.decodePrefix(prefix) gamesLaterToday = []
replyTo = prefix.nick if "#" not in args[0] else args[0] gamesToday = []
gamesUpcoming = []
gamesEarlierWeek = []
cmd = self.bot.messageHasCommand(".nfl", trailing) # sort games
if cmd: for game in games["games"]:
games = self.getNflGamesCached() if game["time"] is not None:
msg = [] liveGames.append(game)
elif game["quarter"] == "P" and game["startdate"].day == datetime.now().day:
liveGames = [] gamesLaterToday.append(game)
gamesLaterToday = [] elif game["startdate"].day == datetime.now().day:
gamesToday = [] gamesToday.append(game)
gamesUpcoming = [] elif game["startdate"].day > datetime.now().day:
gamesEarlierWeek = [] gamesUpcoming.append(game)
# sort games
for game in games["games"]:
if game["time"] is not None:
liveGames.append(game)
elif game["quarter"] == "P" and game["startdate"].day == datetime.now().day:
gamesLaterToday.append(game)
elif game["startdate"].day == datetime.now().day:
gamesToday.append(game)
elif game["startdate"].day > datetime.now().day:
gamesUpcoming.append(game)
else:
gamesEarlierWeek.append(game)
# create list of formatted games
liveGamesStr = []
for game in liveGames:
liveGamesStr.append(self.formatGameLive(game))
liveGamesStr = ", ".join(liveGamesStr)
gamesLaterTodayStr = []
for game in gamesLaterToday:
gamesLaterTodayStr.append(self.formatGameFuture(game))
gamesLaterTodayStr = ", ".join(gamesLaterTodayStr)
gamesTodayStr = []
for game in gamesToday:
gamesTodayStr.append(self.formatGamePast(game))
gamesTodayStr = ", ".join(gamesTodayStr)
gamesUpcomingStr = []
for game in gamesUpcoming:
gamesUpcomingStr.append(self.formatGameFuture(game))
gamesUpcomingStr = ", ".join(gamesUpcomingStr)
gamesEarlierWeekStr = []
for game in gamesEarlierWeek:
gamesEarlierWeekStr.append(self.formatGamePast(game))
gamesEarlierWeekStr = ", ".join(gamesEarlierWeekStr)
msgPieces = []
msgPieces.append("\x02NFL week %s\x02:" % (games["season"]["week"]))
# Depending on args build the respon pieces
if len(cmd.args) > 0 and cmd.args[0] == "today":
if not liveGamesStr == "":
msgPieces.append("\x02Playing now:\x02 %s" % liveGamesStr)
if not gamesLaterTodayStr == "":
msgPieces.append("\x02Later today:\x02 %s" % gamesLaterTodayStr)
if not gamesTodayStr == "":
msgPieces.append("\x02Earlier today:\x02 %s" % gamesTodayStr)
elif len(cmd.args) > 0 and cmd.args[0] == "live":
if not liveGamesStr == "":
msgPieces.append("\x02Playing now:\x02 %s" % liveGamesStr)
elif len(cmd.args) > 0 and cmd.args[0] == "scores":
if not liveGamesStr == "":
msgPieces.append("\x02Playing now:\x02 %s" % liveGamesStr)
if not gamesTodayStr == "":
msgPieces.append("\x02Earlier today:\x02 %s" % gamesTodayStr)
if not gamesEarlierWeekStr == "":
msgPieces.append("\x02Earlier this week: \x02 %s" % gamesEarlierWeekStr)
else: else:
if not liveGamesStr == "": gamesEarlierWeek.append(game)
msgPieces.append("\x02Playing now:\x02 %s" % liveGamesStr)
if not gamesLaterTodayStr == "":
msgPieces.append("\x02Later today:\x02 %s" % gamesLaterTodayStr)
if not gamesTodayStr == "":
msgPieces.append("\x02Earlier today:\x02 %s" % gamesTodayStr)
if not gamesEarlierWeekStr == "":
msgPieces.append("\x02Earlier this week: \x02 %s" % gamesEarlierWeekStr)
if not gamesUpcomingStr == "":
msgPieces.append("\x02Upcoming:\x02 %s" % gamesUpcomingStr)
# Collaspe the list into a repsonse string. Fix grammar # create list of formatted games
msg = ", ".join(msgPieces).replace(":, ", ": ") liveGamesStr = []
for game in liveGames:
liveGamesStr.append(self.formatGameLive(game))
liveGamesStr = ", ".join(liveGamesStr)
# Nothing means there were probably no games gamesLaterTodayStr = []
if len(msgPieces) == 1: for game in gamesLaterToday:
msg = "No games!" gamesLaterTodayStr.append(self.formatGameFuture(game))
gamesLaterTodayStr = ", ".join(gamesLaterTodayStr)
if len(msg) > 0: gamesTodayStr = []
# The message can be long so chunk it into pieces splitting at commas for game in gamesToday:
while len(msg) > 0: gamesTodayStr.append(self.formatGamePast(game))
piece = msg[0:330] gamesTodayStr = ", ".join(gamesTodayStr)
msg = msg[330:]
while not piece[-1:] == "," and len(msg) > 0: gamesUpcomingStr = []
piece += msg[0:1] for game in gamesUpcoming:
msg = msg[1:] gamesUpcomingStr.append(self.formatGameFuture(game))
self.bot.act_PRIVMSG(replyTo, "%s: %s" % (prefix.nick, piece.strip())) gamesUpcomingStr = ", ".join(gamesUpcomingStr)
gamesEarlierWeekStr = []
for game in gamesEarlierWeek:
gamesEarlierWeekStr.append(self.formatGamePast(game))
gamesEarlierWeekStr = ", ".join(gamesEarlierWeekStr)
msgPieces = []
msgPieces.append("\x02NFL week %s\x02:" % (games["season"]["week"]))
# Depending on args build the respon pieces
if len(cmd.args) > 0 and cmd.args[0] == "today":
if not liveGamesStr == "":
msgPieces.append("\x02Playing now:\x02 %s" % liveGamesStr)
if not gamesLaterTodayStr == "":
msgPieces.append("\x02Later today:\x02 %s" % gamesLaterTodayStr)
if not gamesTodayStr == "":
msgPieces.append("\x02Earlier today:\x02 %s" % gamesTodayStr)
elif len(cmd.args) > 0 and cmd.args[0] == "live":
if not liveGamesStr == "":
msgPieces.append("\x02Playing now:\x02 %s" % liveGamesStr)
elif len(cmd.args) > 0 and cmd.args[0] == "scores":
if not liveGamesStr == "":
msgPieces.append("\x02Playing now:\x02 %s" % liveGamesStr)
if not gamesTodayStr == "":
msgPieces.append("\x02Earlier today:\x02 %s" % gamesTodayStr)
if not gamesEarlierWeekStr == "":
msgPieces.append("\x02Earlier this week: \x02 %s" % gamesEarlierWeekStr)
else:
if not liveGamesStr == "":
msgPieces.append("\x02Playing now:\x02 %s" % liveGamesStr)
if not gamesLaterTodayStr == "":
msgPieces.append("\x02Later today:\x02 %s" % gamesLaterTodayStr)
if not gamesTodayStr == "":
msgPieces.append("\x02Earlier today:\x02 %s" % gamesTodayStr)
if not gamesEarlierWeekStr == "":
msgPieces.append("\x02Earlier this week: \x02 %s" % gamesEarlierWeekStr)
if not gamesUpcomingStr == "":
msgPieces.append("\x02Upcoming:\x02 %s" % gamesUpcomingStr)
# Collaspe the list into a repsonse string. Fix grammar
msg = ", ".join(msgPieces).replace(":, ", ": ")
# Nothing means there were probably no games
if len(msgPieces) == 1:
msg = "No games!"
if len(msg) > 0:
# The message can be long so chunk it into pieces splitting at commas
while len(msg) > 0:
piece = msg[0:330]
msg = msg[330:]
while not piece[-1:] == "," and len(msg) > 0:
piece += msg[0:1]
msg = msg[1:]
self.bot.act_PRIVMSG(message.args[0], "%s: %s" % (message.prefix.nick, piece.strip()))
def formatGameLive(self, game): def formatGameLive(self, game):
c_vis = 3 if int(game["visitor_score"]) > int(game["home_score"]) else 4 c_vis = 3 if int(game["visitor_score"]) > int(game["home_score"]) else 4

View File

@ -7,14 +7,14 @@
""" """
from pyircbot.modulebase import ModuleBase, ModuleHook from pyircbot.modulebase import ModuleBase, hook
import hashlib from pyircbot.common import messageHasCommand
from pyircbot.modules.ModInfo import info
class NickUser(ModuleBase): class NickUser(ModuleBase):
def __init__(self, bot, moduleName): def __init__(self, bot, moduleName):
ModuleBase.__init__(self, bot, moduleName) ModuleBase.__init__(self, bot, moduleName)
self.hooks = [ModuleHook("PRIVMSG", self.gotmsg)]
self.services = ["login"] self.services = ["login"]
def check(self, nick, hostname): def check(self, nick, hostname):
@ -28,17 +28,19 @@ class NickUser(ModuleBase):
pass pass
# TODO: log out all users # TODO: log out all users
def gotmsg(self, args, prefix, trailing): @hook("PRIVMSG")
channel = args[0] def gotmsg(self, msg, cmd):
if channel[0] == "#": if msg.args[0][0] == "#":
# Ignore channel messages # Ignore channel messages
pass return
else: else:
self.handlePm(args, prefix, trailing) self.handlePm(msg.prefix, msg.trailing)
def handlePm(self, args, prefix, trailing): @info("setpass [<oldpass>] <password> set or change password", cmds=["setpass"])
prefix = self.bot.decodePrefix(prefix) @info("login <password> authenticate with the bot", cmds=["login"])
cmd = self.bot.messageHasCommand(".setpass", trailing) @info("logout log out of the bot", cmds=["logout"])
def handlePm(self, prefix, trailing):
cmd = messageHasCommand(".setpass", trailing)
if cmd: if cmd:
if len(cmd.args) == 0: if len(cmd.args) == 0:
self.bot.act_PRIVMSG(prefix.nick, ".setpass: usage: \".setpass newpass\" or " self.bot.act_PRIVMSG(prefix.nick, ".setpass: usage: \".setpass newpass\" or "
@ -61,7 +63,7 @@ class NickUser(ModuleBase):
self.bot.act_PRIVMSG(prefix.nick, self.bot.act_PRIVMSG(prefix.nick,
".setpass: You must provide the old password when setting a new one.") ".setpass: You must provide the old password when setting a new one.")
cmd = self.bot.messageHasCommand(".login", trailing) cmd = messageHasCommand(".login", trailing)
if cmd: if cmd:
attr = self.bot.getBestModuleForService("attributes") attr = self.bot.getBestModuleForService("attributes")
userpw = attr.getKey(prefix.nick, "password") userpw = attr.getKey(prefix.nick, "password")
@ -78,7 +80,7 @@ class NickUser(ModuleBase):
self.bot.act_PRIVMSG(prefix.nick, ".login: incorrect password.") self.bot.act_PRIVMSG(prefix.nick, ".login: incorrect password.")
else: else:
self.bot.act_PRIVMSG(prefix.nick, ".login: usage: \".login password\"") self.bot.act_PRIVMSG(prefix.nick, ".login: usage: \".login password\"")
cmd = self.bot.messageHasCommand(".logout", trailing) cmd = messageHasCommand(".logout", trailing)
if cmd: if cmd:
attr = self.bot.getBestModuleForService("attributes") attr = self.bot.getBestModuleForService("attributes")
loggedin = attr.getKey(prefix.nick, "loggedinfrom") loggedin = attr.getKey(prefix.nick, "loggedinfrom")
@ -87,8 +89,3 @@ class NickUser(ModuleBase):
else: else:
attr.setKey(prefix.nick, "loggedinfrom", None) attr.setKey(prefix.nick, "loggedinfrom", None)
self.bot.act_PRIVMSG(prefix.nick, ".logout: You have been logged out.") self.bot.act_PRIVMSG(prefix.nick, ".logout: You have been logged out.")
def md5(self, data):
m = hashlib.md5()
m.update(data.encode("ascii"))
return m.hexdigest()

View File

@ -9,26 +9,23 @@
from time import time, sleep from time import time, sleep
from threading import Thread from threading import Thread
from pyircbot.modulebase import ModuleBase, ModuleHook from pyircbot.modulebase import ModuleBase, hook
class PingResponder(ModuleBase): class PingResponder(ModuleBase):
def __init__(self, bot, moduleName): def __init__(self, bot, moduleName):
ModuleBase.__init__(self, bot, moduleName) ModuleBase.__init__(self, bot, moduleName)
self.timer = PingRespondTimer(self) self.timer = PingRespondTimer(self)
self.hooks = [
ModuleHook("PING", self.pingrespond),
ModuleHook("_RECV", self.resettimer),
ModuleHook("_SEND", self.resettimer)
]
def pingrespond(self, args, prefix, trailing): @hook("PING")
def pingrespond(self, msg, cmd):
"""Respond to the PING command""" """Respond to the PING command"""
# got a ping? send it right back # got a ping? send it right back
self.bot.act_PONG(trailing) self.bot.act_PONG(msg.trailing)
self.log.info("%s Responded to a ping: %s" % (self.bot.get_nick(), trailing)) self.log.info("%s Responded to a ping: %s" % (self.bot.get_nick(), msg.trailing))
def resettimer(self, msg): @hook("_RECV", "_SEND")
def resettimer(self, msg, cmd):
"""Resets the connection failure timer""" """Resets the connection failure timer"""
self.timer.reset() self.timer.reset()
@ -37,7 +34,9 @@ class PingResponder(ModuleBase):
class PingRespondTimer(Thread): class PingRespondTimer(Thread):
"Tracks last ping from server, and reconnects if over a threshold" """
Tracks last ping from server, and reconnects if over a threshold
"""
def __init__(self, master): def __init__(self, master):
Thread.__init__(self) Thread.__init__(self)
self.daemon = True self.daemon = True
@ -47,11 +46,15 @@ class PingRespondTimer(Thread):
self.start() self.start()
def reset(self): def reset(self):
"Reset the internal ping timeout counter" """
Reset the internal ping timeout counter
"""
self.lastping = time() self.lastping = time()
def disable(self): def disable(self):
"Allow the thread to die" """
Allow the thread to die
"""
self.alive = False self.alive = False
def run(self): def run(self):

View File

@ -62,28 +62,28 @@ class PubSubClient(ModuleBase):
self.bus.pub(self.config.get("publish").format(subchannel), "{} {}".format("default", message)) self.bus.pub(self.config.get("publish").format(subchannel), "{} {}".format("default", message))
@hook("PRIVMSG") @hook("PRIVMSG")
def bus_privmsg(self, msg): def bus_privmsg(self, msg, cmd):
""" """
Relay a privmsg to the event bus Relay a privmsg to the event bus
""" """
self.publish("privmsg", dumps([msg.args, msg.prefix[0], msg.trailing, {"prefix": msg.prefix}])) self.publish("privmsg", dumps([msg.args, msg.prefix[0], msg.trailing, {"prefix": msg.prefix}]))
@hook("JOIN") @hook("JOIN")
def bus_join(self, msg): def bus_join(self, msg, cmd):
""" """
Relay a join message to the event bus Relay a join message to the event bus
""" """
self.publish("join", dumps([msg.prefix[0], msg.trailing, {"prefix": msg.prefix}])) self.publish("join", dumps([msg.prefix[0], msg.trailing, {"prefix": msg.prefix}]))
@hook("PART") @hook("PART")
def bus_part(self, msg): def bus_part(self, msg, cmd):
""" """
Relay a part message to the event bus Relay a part message to the event bus
""" """
self.publish("part", dumps([msg.args, msg.prefix[0], msg.trailing, {"prefix": msg.prefix}])) self.publish("part", dumps([msg.args, msg.prefix[0], msg.trailing, {"prefix": msg.prefix}]))
@hook("PRIVMSG") @hook("PRIVMSG")
def bus_command(self, msg): def bus_command(self, msg, cmd):
""" """
Parse commands and publish as separate channels on the bus. Commands like `.seen nick` will be published Parse commands and publish as separate channels on the bus. Commands like `.seen nick` will be published
to channel `command_seen`. to channel `command_seen`.

View File

@ -7,14 +7,14 @@
""" """
from pyircbot.modulebase import ModuleBase, ModuleHook from pyircbot.modulebase import ModuleBase, hook, command
from datetime import datetime from datetime import datetime
from pyircbot.modules.ModInfo import info
class RandQuote(ModuleBase): class RandQuote(ModuleBase):
def __init__(self, bot, moduleName): def __init__(self, bot, moduleName):
ModuleBase.__init__(self, bot, moduleName) ModuleBase.__init__(self, bot, moduleName)
self.hooks = []
self.db = None self.db = None
serviceProviders = self.bot.getmodulesbyservice("sqlite") serviceProviders = self.bot.getmodulesbyservice("sqlite")
if not serviceProviders: if not serviceProviders:
@ -33,26 +33,19 @@ class RandQuote(ModuleBase):
) ;""") ) ;""")
c.close() c.close()
self.hooks = [ModuleHook("PRIVMSG", self.logquote), @info("randquote print a random quote", cmds=["randquote", "randomquote", "rq"])
ModuleHook("PRIVMSG", self.fetchquotes)] @command("randquote", "randomquote", "rq")
def fetchquotes(self, msg, cmd):
c = self.db.query("SELECT * FROM `chat` ORDER BY RANDOM() LIMIT 1;")
row = c.fetchone()
c.close()
if row:
self.bot.act_PRIVMSG(msg.args[0], "<%s> %s" % (row["sender"], row["message"],))
def fetchquotes(self, args, prefix, trailing): @hook("PRIVMSG")
if not args[0][0] == "#": def logquote(self, msg, cmd):
return
cmd = self.bot.messageHasCommand([".randomquote", ".randquote", ".rq"], trailing)
if cmd:
c = self.db.query("SELECT * FROM `chat` ORDER BY RANDOM() LIMIT 1;")
row = c.fetchone()
c.close()
if row:
self.bot.act_PRIVMSG(args[0], "<%s> %s" % (row["sender"], row["message"],))
def logquote(self, args, prefix, trailing):
if not args[0][0] == "#":
return
prefixObj = self.bot.decodePrefix(prefix)
self.db.query("INSERT INTO `chat` (`date`, `sender`, `message`) VALUES (?, ?, ?)", self.db.query("INSERT INTO `chat` (`date`, `sender`, `message`) VALUES (?, ?, ?)",
(int(datetime.now().timestamp()), prefixObj.nick, trailing)).close() (int(datetime.now().timestamp()), msg.prefix.nick, msg.trailing)).close()
# Trim quotes # Trim quotes
c = self.db.query("SELECT * FROM `chat` ORDER BY `date` DESC LIMIT %s, 1000000;" % self.config["limit"]) c = self.db.query("SELECT * FROM `chat` ORDER BY `date` DESC LIMIT %s, 1000000;" % self.config["limit"])
while True: while True:

View File

@ -6,12 +6,13 @@
""" """
from pyircbot.modulebase import ModuleBase, ModuleHook from pyircbot.modulebase import ModuleBase, command
from datetime import datetime, timedelta from datetime import datetime, timedelta
from threading import Thread from threading import Thread
from time import sleep from time import sleep
import re import re
import pytz import pytz
from pyircbot.modules.ModInfo import info
class Remind(ModuleBase): class Remind(ModuleBase):
@ -37,9 +38,6 @@ class Remind(ModuleBase):
) ;""") ) ;""")
c.close() c.close()
self.hooks = [ModuleHook("PRIVMSG", self.remindin),
ModuleHook("PRIVMSG", self.remindat)]
self.disabled = False self.disabled = False
# Start monitor thread # Start monitor thread
@ -108,86 +106,81 @@ class Remind(ModuleBase):
def ondisable(self): def ondisable(self):
self.disabled = True self.disabled = True
def remindat(self, args, prefix, trailing): @info("remind <time> have the bot remind you", cmds=["remind", "at"])
prefixObj = self.bot.decodePrefix(prefix) @command("remind", "at", allow_private=True)
def remindat(self, msg, cmd):
regex = re.compile(r'(\d+):(\d+)(?::(\d+))?([^\s\d]+)? (.*)')
match = regex.match(cmd.args_str)
replyTo = msg.args[0]
try:
hour, minute, second, tz, message = match.groups()
message = message.strip()
replyTo = prefixObj.nick if "#" not in args[0] else args[0] assert not message == ""
# Lots of code borrowed from https://github.com/embolalia/willie/blob/master/willie/modules/remind.py hour = int(hour)
cmd = self.bot.messageHasCommand([".at", ".remind"], trailing, True) minute = int(minute)
if cmd: if second is not None:
regex = re.compile(r'(\d+):(\d+)(?::(\d+))?([^\s\d]+)? (.*)') second = int(second)
match = regex.match(cmd.args_str)
except:
self.bot.act_PRIVMSG(replyTo, "%s: .at - Remind at a time. Example: .at 20:45EST Do your homework!" %
msg.prefix.nick)
return
now = datetime.now()
remindAt = datetime.now()
# if there was timezone, make sure the time we're reminding them at is relative to their timezone
if tz is not None:
try: try:
hour, minute, second, tz, message = match.groups() theirzone = pytz.timezone(Remind.translateZoneStr(tz))
message = message.strip()
assert not message == ""
hour = int(hour)
minute = int(minute)
if second is not None:
second = int(second)
except: except:
self.bot.act_PRIVMSG(replyTo, "%s: .at - Remind at a time. Example: .at 20:45EST Do your homework!" % self.bot.act_PRIVMSG(replyTo, "%s: I don't recognize the timezone '%s'." % (msg.prefix.nick, tz))
prefixObj.nick)
return return
remindAt = theirzone.localize(remindAt, is_dst=Remind.is_dst(theirzone))
now = datetime.now() # Set the hour and minute we'll remind them at today.
remindAt = datetime.now() # If the ends up being in the past, we'll add a day alter
remindAt = remindAt.replace(hour=hour).replace(minute=minute).replace(microsecond=0)
# if there was timezone, make sure the time we're reminding them at is relative to their timezone # Set seconds
if tz is not None: if second is None:
try: remindAt = remindAt.replace(second=0)
theirzone = pytz.timezone(Remind.translateZoneStr(tz)) else:
except: remindAt = remindAt.replace(second=second)
self.bot.act_PRIVMSG(replyTo, "%s: I don't recognize the timezone '%s'." % (prefixObj.nick, tz))
return
remindAt = theirzone.localize(remindAt, is_dst=Remind.is_dst(theirzone))
# Set the hour and minute we'll remind them at today. # if there was timezone, convert remindAt to our zone
# If the ends up being in the past, we'll add a day alter if tz is not None:
remindAt = remindAt.replace(hour=hour).replace(minute=minute).replace(microsecond=0) try:
theirzone = pytz.timezone(Remind.translateZoneStr(tz))
except:
self.bot.act_PRIVMSG(replyTo, "%s: I don't recognize the timezone '%s'." % (msg.prefix.nick, tz))
return
remindAt = remindAt.astimezone(pytz.timezone(self.config["mytimezone"])).replace(tzinfo=None)
# Set seconds # Advance it a day if the time would have been earlier today
if second is None: while remindAt < now:
remindAt = remindAt.replace(second=0) remindAt += timedelta(days=1)
else:
remindAt = remindAt.replace(second=second)
# if there was timezone, convert remindAt to our zone timediff = remindAt - datetime.now()
if tz is not None: # self.bot.act_PRIVMSG(replyTo, "Time: %s" % str(remindAt))
try: # self.bot.act_PRIVMSG(replyTo, "Diff: %s" % (timediff))
theirzone = pytz.timezone(Remind.translateZoneStr(tz))
except:
self.bot.act_PRIVMSG(replyTo, "%s: I don't recognize the timezone '%s'." % (prefixObj.nick, tz))
return
remindAt = remindAt.astimezone(pytz.timezone(self.config["mytimezone"])).replace(tzinfo=None)
# Advance it a day if the time would have been earlier today # Save the reminder
while remindAt < now: c = self.db.query("INSERT INTO `reminders` (`sender`, `senderch`, `when`, `message`) VALUES (?, ?, ?, ?)", (
remindAt += timedelta(days=1) msg.prefix.nick,
msg.args[0] if "#" in msg.args[0] else "",
remindAt,
message
))
c.close()
timediff = remindAt - datetime.now() diffHours = int(timediff.seconds / 60 / 60)
# self.bot.act_PRIVMSG(replyTo, "Time: %s" % str(remindAt)) diffMins = int((timediff.seconds - diffHours * 60 * 60) / 60)
# self.bot.act_PRIVMSG(replyTo, "Diff: %s" % (timediff))
# Save the reminder self.bot.act_PRIVMSG(replyTo, "%s: Ok, will do. Approx %sh%sm to go." %
c = self.db.query("INSERT INTO `reminders` (`sender`, `senderch`, `when`, `message`) VALUES (?, ?, ?, ?)", ( (msg.prefix.nick, diffHours, diffMins))
prefixObj.nick,
args[0] if "#" in args[0] else "",
remindAt,
message
))
c.close()
diffHours = int(timediff.seconds / 60 / 60)
diffMins = int((timediff.seconds - diffHours * 60 * 60) / 60)
self.bot.act_PRIVMSG(replyTo, "%s: Ok, will do. Approx %sh%sm to go." %
(prefixObj.nick, diffHours, diffMins))
@staticmethod @staticmethod
def is_dst(tz): def is_dst(tz):
@ -205,46 +198,45 @@ class Remind(ModuleBase):
else: else:
return zonestr return zonestr
def remindin(self, args, prefix, trailing): @info("after <duration> have the bot remind after", cmds=["after", "in"])
prefixObj = self.bot.decodePrefix(prefix) @command("after", "in", allow_private=True)
replyTo = prefixObj.nick if "#" not in args[0] else args[0] def remindin(self, msg, cmd):
replyTo = msg.args[0]
cmd = self.bot.messageHasCommand([".in", ".after"], trailing, True) if not cmd.args:
if cmd: self.bot.act_PRIVMSG(replyTo, "%s: .in - Remind after x amount of time. Example: .in 1week5d2h1m Go "
if not cmd.args: "fuck yourself" % msg.prefix.nick)
self.bot.act_PRIVMSG(replyTo, "%s: .in - Remind after x amount of time. Example: .in 1week5d2h1m Go " return
"fuck yourself" % prefixObj.nick)
timepieces = re.compile(r'([0-9]+)([a-zA-Z]+)').findall(cmd.args[0])
if not timepieces:
self.bot.act_PRIVMSG(replyTo, "%s: .in - Remind after x amount of time. Example: .in 1week5d2h1m Go "
"fuck yourself" % msg.prefix.nick)
return
delaySeconds = 0
for match in timepieces:
# ('30', 'm')
if not match[1] in Remind.scaling:
self.bot.act_PRIVMSG(replyTo, "%s: Sorry, I don't understand the time unit '%s'" %
(msg.prefix.nick, match[1]))
return return
timepieces = re.compile(r'([0-9]+)([a-zA-Z]+)').findall(cmd.args[0]) delaySeconds += (Remind.scaling[match[1]] * int(match[0]))
if not timepieces:
self.bot.act_PRIVMSG(replyTo, "%s: .in - Remind after x amount of time. Example: .in 1week5d2h1m Go "
"fuck yourself" % prefixObj.nick)
return
delaySeconds = 0 remindAt = datetime.now() + timedelta(seconds=delaySeconds)
for match in timepieces:
# ('30', 'm')
if not match[1] in Remind.scaling:
self.bot.act_PRIVMSG(replyTo, "%s: Sorry, I don't understand the time unit '%s'" %
(prefixObj.nick, match[1]))
return
delaySeconds += (Remind.scaling[match[1]] * int(match[0])) self.db.query("INSERT INTO `reminders` (`sender`, `senderch`, `when`, `message`) VALUES (?, ?, ?, ?)", (
msg.prefix.nick,
msg.args[0] if "#" in msg.args[0] else "",
remindAt,
cmd.args_str[len(cmd.args[0]):].strip()
)).close()
remindAt = datetime.now() + timedelta(seconds=delaySeconds) hours = int(delaySeconds / 60 / 60)
minutes = int((delaySeconds - (hours * 60 * 60)) / 60)
self.db.query("INSERT INTO `reminders` (`sender`, `senderch`, `when`, `message`) VALUES (?, ?, ?, ?)", ( self.bot.act_PRIVMSG(replyTo, "%s: Ok, talk to you in approx %sh%sm" % (msg.prefix.nick, hours, minutes))
prefixObj.nick,
args[0] if "#" in args[0] else "",
remindAt,
cmd.args_str[len(cmd.args[0]):].strip()
)).close()
hours = int(delaySeconds / 60 / 60)
minutes = int((delaySeconds - (hours * 60 * 60)) / 60)
self.bot.act_PRIVMSG(replyTo, "%s: Ok, talk to you in approx %sh%sm" % (prefixObj.nick, hours, minutes))
scaling = { scaling = {
"years": 365.25 * 24 * 3600, "years": 365.25 * 24 * 3600,

View File

@ -156,7 +156,9 @@ class SMS(ModuleBase):
try: try:
self.twilio.api.account.messages.create(to=self.config["contacts"][contact], self.twilio.api.account.messages.create(to=self.config["contacts"][contact],
from_=self.config["number"], from_=self.config["number"],
body=msg.trailing[7 + len(contact):].strip()) body="{} <{}>: {}".format(msg.args[0],
msg.prefix.nick,
msg.trailing[7 + len(contact):].strip()))
except Exception as e: except Exception as e:
self.bot.act_PRIVMSG(msg.args[0], "Could not send message: {}".format(repr(e))) self.bot.act_PRIVMSG(msg.args[0], "Could not send message: {}".format(repr(e)))
else: else:

View File

@ -14,7 +14,6 @@ import sqlite3
class SQLite(ModuleBase): class SQLite(ModuleBase):
def __init__(self, bot, moduleName): def __init__(self, bot, moduleName):
ModuleBase.__init__(self, bot, moduleName) ModuleBase.__init__(self, bot, moduleName)
self.hooks = []
self.services = ["sqlite"] self.services = ["sqlite"]
def opendb(self, dbname): def opendb(self, dbname):

View File

@ -7,15 +7,15 @@
""" """
from pyircbot.modulebase import ModuleBase, ModuleHook from pyircbot.modulebase import ModuleBase, hook
class ServerPassword(ModuleBase): class ServerPassword(ModuleBase):
def __init__(self, bot, moduleName): def __init__(self, bot, moduleName):
ModuleBase.__init__(self, bot, moduleName) ModuleBase.__init__(self, bot, moduleName)
self.hooks = [ModuleHook("_CONNECT", self.doConnect)]
def doConnect(self, args, prefix, trailing): @hook("_CONNECT")
def doConnect(self, msg, cmd):
"""Hook for when the IRC conneciton is opened""" """Hook for when the IRC conneciton is opened"""
if "password" in self.config and self.config["password"]: if "password" in self.config and self.config["password"]:
self.log.info("Sending server password") self.log.info("Sending server password")

View File

@ -7,27 +7,25 @@
""" """
from pyircbot.modulebase import ModuleBase, ModuleHook from pyircbot.modulebase import ModuleBase, hook
from time import sleep from time import sleep
class Services(ModuleBase): class Services(ModuleBase):
def __init__(self, bot, moduleName): def __init__(self, bot, moduleName):
ModuleBase.__init__(self, bot, moduleName) ModuleBase.__init__(self, bot, moduleName)
self.hooks = [ModuleHook("_CONNECT", self.doConnect),
ModuleHook("433", self.nickTaken),
ModuleHook("001", self.initservices),
ModuleHook("INVITE", self.invited), ]
self.current_nick = 0 self.current_nick = 0
self.do_ghost = False self.do_ghost = False
def doConnect(self, args, prefix, trailing): @hook("_CONNECT")
def doConnect(self, msg, cmd):
"""Hook for when the IRC conneciton is opened""" """Hook for when the IRC conneciton is opened"""
self.bot.act_NICK(self.config["user"]["nick"][0]) self.bot.act_NICK(self.config["user"]["nick"][0])
self.bot.act_USER(self.config["user"]["username"], self.config["user"]["hostname"], self.bot.act_USER(self.config["user"]["username"], self.config["user"]["hostname"],
self.config["user"]["realname"]) self.config["user"]["realname"])
def nickTaken(self, args, prefix, trailing): @hook("433")
def nickTaken(self, msg, cmd):
"""Hook that responds to 433, meaning our nick is taken""" """Hook that responds to 433, meaning our nick is taken"""
if self.config["ident"]["ghost"]: if self.config["ident"]["ghost"]:
self.do_ghost = True self.do_ghost = True
@ -37,7 +35,8 @@ class Services(ModuleBase):
return return
self.bot.act_NICK(self.config["user"]["nick"][self.current_nick]) self.bot.act_NICK(self.config["user"]["nick"][self.current_nick])
def initservices(self, args, prefix, trailing): @hook("001")
def initservices(self, msg, cmd):
"""Hook that sets our initial nickname""" """Hook that sets our initial nickname"""
if self.do_ghost: if self.do_ghost:
self.bot.act_PRIVMSG(self.config["ident"]["ghost_to"], self.config["ident"]["ghost_cmd"] % self.bot.act_PRIVMSG(self.config["ident"]["ghost_to"], self.config["ident"]["ghost_cmd"] %
@ -46,11 +45,12 @@ class Services(ModuleBase):
self.bot.act_NICK(self.config["user"]["nick"][0]) self.bot.act_NICK(self.config["user"]["nick"][0])
self.do_initservices() self.do_initservices()
def invited(self, args, prefix, trailing): @hook("INVITE")
def invited(self, msg, cmd):
"""Hook responding to INVITE channel invitations""" """Hook responding to INVITE channel invitations"""
if trailing.lower() in self.config["privatechannels"]["list"]: if msg.trailing.lower() in self.config["privatechannels"]["list"]:
self.log.info("Invited to %s, joining" % trailing) self.log.info("Invited to %s, joining" % msg.trailing)
self.bot.act_JOIN(trailing) self.bot.act_JOIN(msg.trailing)
def do_initservices(self): def do_initservices(self):
"""Identify with nickserv and join startup channels""" """Identify with nickserv and join startup channels"""

View File

@ -6,9 +6,10 @@
""" """
from pyircbot.modulebase import ModuleBase, ModuleHook from pyircbot.modulebase import ModuleBase, command, hook
import datetime import datetime
from time import mktime from time import mktime
from pyircbot.modules.ModInfo import info
class Tell(ModuleBase): class Tell(ModuleBase):
@ -38,27 +39,21 @@ class Tell(ModuleBase):
self.db.query("DELETE FROM `tells` WHERE `when`<?", self.db.query("DELETE FROM `tells` WHERE `when`<?",
(int(mktime(datetime.datetime.now().timetuple())) - self.config["maxage"],)).close() (int(mktime(datetime.datetime.now().timetuple())) - self.config["maxage"],)).close()
self.hooks = [ @hook("PRIVMSG", "JOIN")
ModuleHook(["JOIN", "PRIVMSG"], self.showtell), def showtell(self, msg, cmd):
ModuleHook(["PRIVMSG"], self.tellcmds)
]
def showtell(self, args, prefix, trailing):
prefix = self.bot.decodePrefix(prefix)
# Look for tells for this person # Look for tells for this person
c = self.db.query("SELECT * FROM `tells` WHERE `recip`=?", (prefix.nick,)) c = self.db.query("SELECT * FROM `tells` WHERE `recip`=?", (msg.prefix.nick,))
tells = c.fetchall() tells = c.fetchall()
c.close() c.close()
for tell in tells: for tell in tells:
agostr = Tell.timesince(datetime.datetime.fromtimestamp(tell["when"])) agostr = Tell.timesince(datetime.datetime.fromtimestamp(tell["when"]))
recip = None recip = None
if tell["channel"] == "": if tell["channel"] == "":
recip = prefix.nick recip = msg.prefix.nick
else: else:
recip = tell["channel"] recip = tell["channel"]
self.bot.act_PRIVMSG(recip, "%s: %s said %s ago: %s" % ( self.bot.act_PRIVMSG(recip, "%s: %s said %s ago: %s" % (
prefix.nick, msg.prefix.nick,
tell["sender"], tell["sender"],
agostr, agostr,
tell["message"] tell["message"]
@ -66,34 +61,31 @@ class Tell(ModuleBase):
# Delete # Delete
self.db.query("DELETE FROM `tells` WHERE `id`=?", (tell["id"],)) self.db.query("DELETE FROM `tells` WHERE `id`=?", (tell["id"],))
def tellcmds(self, args, prefix, trailing): @info(".tell <person> <message> relay a message when the target is online", cmds=["tell"])
prefixObj = self.bot.decodePrefix(prefix) @command("tell", allow_private=True)
replyTo = prefixObj.nick if "#" not in args[0] else args[0] def tellcmds(self, msg, cmd):
if len(cmd.args) < 2:
self.bot.act_PRIVMSG(msg.args[0], "%s: .tell <person> <message> - Tell someone something the next time "
"they're seen. Example: .tell antiroach Do your homework!" % msg.prefix.nick)
return
cmd = self.bot.messageHasCommand(".tell", trailing) recip = cmd.args[0]
if cmd: message = ' '.join(cmd.args[1:]).strip()
if len(cmd.args) < 2:
self.bot.act_PRIVMSG(replyTo, "%s: .tell <person> <message> - Tell someone something the next time "
"they're seen. Example: .tell antiroach Do your homework!" % prefixObj.nick)
return
recip = cmd.args[0] if not message:
message = ' '.join(cmd.args[1:]).strip() self.bot.act_PRIVMSG(msg.args[0], "%s: .tell <person> <message> - Tell someone something the next time "
if not message:
self.bot.act_PRIVMSG(replyTo, "%s: .tell <person> <message> - Tell someone something the next time "
"they're seen. Example: .tell antiroach Do your homework!" % "they're seen. Example: .tell antiroach Do your homework!" %
prefixObj.nick) msg.prefix.nick)
return return
self.db.query("INSERT INTO `tells` (`sender`, `channel`, `when`, `recip`, `message`) VALUES " self.db.query("INSERT INTO `tells` (`sender`, `channel`, `when`, `recip`, `message`) VALUES "
"(?, ?, ?, ?, ?);", (prefixObj.nick, "(?, ?, ?, ?, ?);", (msg.prefix.nick,
args[0] if "#" in args[0] else "", msg.args[0] if "#" in msg.args[0] else "",
int(mktime(datetime.datetime.now().timetuple())), int(mktime(datetime.datetime.now().timetuple())),
recip, recip,
message)).close() message)).close()
self.bot.act_PRIVMSG(replyTo, "%s: I'll pass that along." % prefixObj.nick) self.bot.act_PRIVMSG(msg.args[0], "%s: I'll pass that along." % msg.prefix.nick)
# Copyright (c) Django Software Foundation and individual contributors. # Copyright (c) Django Software Foundation and individual contributors.
# All rights reserved. # All rights reserved.

View File

@ -7,7 +7,7 @@
from threading import Thread from threading import Thread
from time import sleep, time from time import sleep, time
from pyircbot.modulebase import ModuleBase, ModuleHook from pyircbot.modulebase import ModuleBase, hook
from random import randrange, choice from random import randrange, choice
@ -15,15 +15,15 @@ class Triggered(ModuleBase):
def __init__(self, bot, moduleName): def __init__(self, bot, moduleName):
ModuleBase.__init__(self, bot, moduleName) ModuleBase.__init__(self, bot, moduleName)
self.quietuntil = time() self.quietuntil = time()
self.hooks.append(ModuleHook("PRIVMSG", self.check))
def check(self, args): @hook("PRIVMSG")
def check(self, msg, cmd):
if time() < self.quietuntil: if time() < self.quietuntil:
return return
if not args.args[0].lower() in self.config["channels"]: if not msg.args[0].lower() in self.config["channels"]:
return return
message = args.trailing.lower() message = msg.trailing.lower()
triggered = False triggered = False
for word in self.config["words"]: for word in self.config["words"]:
if word.lower() in message: if word.lower() in message:
@ -33,7 +33,7 @@ class Triggered(ModuleBase):
if not triggered: if not triggered:
return return
msg = Thread(target=self.scream, args=(args.args[0],)) msg = Thread(target=self.scream, args=(msg.args[0],))
msg.daemon = True msg.daemon = True
msg.start() msg.start()

View File

@ -6,7 +6,7 @@
""" """
from pyircbot.modulebase import ModuleBase, ModuleHook from pyircbot.modulebase import ModuleBase, hook
from random import randint from random import randint
import time import time
import re import re
@ -19,9 +19,6 @@ class UnoPlay(ModuleBase):
def __init__(self, bot, moduleName): def __init__(self, bot, moduleName):
ModuleBase.__init__(self, bot, moduleName) ModuleBase.__init__(self, bot, moduleName)
self.servicesModule = self.bot.getmodulebyname("Services") self.servicesModule = self.bot.getmodulebyname("Services")
self.hooks = [ModuleHook("PRIVMSG", self.unoplay),
ModuleHook("PRIVMSG", self.trigger),
ModuleHook("NOTICE", self.decklisten)]
self.current_card = None self.current_card = None
self.shouldgo = False self.shouldgo = False
self.has_drawn = False self.has_drawn = False
@ -32,30 +29,32 @@ class UnoPlay(ModuleBase):
assert self.config["strategy"] in self.strategies assert self.config["strategy"] in self.strategies
self.cards = [] self.cards = []
def trigger(self, args, prefix, trailing): @hook("PRIVMSG")
if trailing.startswith("["): # anti-znc buffer playback def trigger(self, msg, cmd):
if msg.trailing.startswith("["): # anti-znc buffer playback
return return
if self.config["enable_trigger"] and "!jo" in trailing: if self.config["enable_trigger"] and "!jo" in msg.trailing:
self.bot.act_PRIVMSG(self.config["unochannel"], "jo") self.bot.act_PRIVMSG(self.config["unochannel"], "jo")
elif trailing == "jo": elif msg.trailing == "jo":
# Reset streak counter & join if another human joins # Reset streak counter & join if another human joins
self.games_played = 0 self.games_played = 0
self.join_game() self.join_game()
def decklisten(self, args, prefix, trailing): @hook("NOTICE")
def decklisten(self, msg, cmd):
""" """
Listen for messages sent via NOTICe to the bot, this is usually a list of cards in our hand. Parse them. Listen for messages sent via NOTICe to the bot, this is usually a list of cards in our hand. Parse them.
""" """
if trailing.startswith("["): # anti-znc buffer playback if msg.trailing.startswith("["): # anti-znc buffer playback
return return
if not prefix or self.config["unobot"] not in prefix: if not msg.prefix or self.config["unobot"] not in msg.prefix:
return return
if "You don't have that card" in trailing: if "You don't have that card" in msg.trailing:
self.log.error("played invalid card!") self.log.error("played invalid card!")
return return
trailing = self.stripcolors(trailing) trailing = self.stripcolors(msg.trailing)
cards = [] cards = []
@ -73,13 +72,14 @@ class UnoPlay(ModuleBase):
self.shouldgo = False self.shouldgo = False
self.taketurn() self.taketurn()
def unoplay(self, args, prefix, trailing): @hook("PRIVMSG")
if trailing.startswith("["): # anti-znc buffer playback def unoplay(self, msg, cmd):
if msg.trailing.startswith("["): # anti-znc buffer playback
return return
trailing = self.stripcolors(trailing) trailing = self.stripcolors(msg.trailing)
if self.config["unobot"] not in prefix: if self.config["unobot"] not in msg.prefix.nick:
return return
# Parse card from beginning message # Parse card from beginning message

View File

@ -7,30 +7,24 @@
""" """
from pyircbot.modulebase import ModuleBase, ModuleHook from pyircbot.modulebase import ModuleBase, command
from requests import get from requests import get
from pyircbot.modules.ModInfo import info
class Urban(ModuleBase): class Urban(ModuleBase):
def __init__(self, bot, moduleName):
ModuleBase.__init__(self, bot, moduleName)
self.hooks = [ModuleHook("PRIVMSG", self.urban)]
def urban(self, args, prefix, trailing): @info("urban <term> lookup an urban dictionary definition", cmds=["urban", "u"])
cmd = self.bot.messageHasCommand(".urban", trailing) @command("urban", "u")
if not cmd: def urban(self, msg, cmd):
cmd = self.bot.messageHasCommand(".u", trailing) print(cmd)
if cmd and args[0][0:1] == "#": definitions = get("http://www.urbandictionary.com/iphone/search/define",
if cmd.args_str.strip() == "": params={"term": cmd.args_str}).json()["list"]
self.bot.act_PRIVMSG(args[0], ".u/.urban <phrase> -- looks up <phrase> on urbandictionary.com") if len(definitions) == 0:
return self.bot.act_PRIVMSG(msg.args[0], "Urban definition: no results!")
definitions = get("http://www.urbandictionary.com/iphone/search/define", else:
params={"term": cmd.args_str}).json()["list"] defstr = definitions[0]['definition'].replace('\n', ' ').replace('\r', '')
if len(definitions) == 0: if len(defstr) > 360:
self.bot.act_PRIVMSG(args[0], "Urban definition: no results!") defstr = defstr[0:360] + "..."
else: self.bot.act_PRIVMSG(msg.args[0], "Urban definition: %s - http://urbanup.com/%s" %
defstr = definitions[0]['definition'].replace('\n', ' ').replace('\r', '') (defstr, definitions[0]['defid']))
if len(defstr) > 360:
defstr = defstr[0:360] + "..."
self.bot.act_PRIVMSG(args[0], "Urban definition: %s - http://urbanup.com/%s" %
(defstr, definitions[0]['defid']))

View File

@ -7,8 +7,9 @@
""" """
from pyircbot.modulebase import ModuleBase, ModuleHook from pyircbot.modulebase import ModuleBase, command
from requests import get from requests import get
from pyircbot.modules.ModInfo import info
class Weather(ModuleBase): class Weather(ModuleBase):
@ -31,80 +32,71 @@ class Weather(ModuleBase):
self.log.error("Weather: An 'attributes' service is required") self.log.error("Weather: An 'attributes' service is required")
return return
self.hooks = [ModuleHook("PRIVMSG", self.weather)] @info("weather [location] display the forecast", cmds=["weather", "w"])
@command("weather", "w")
def weather(self, args, prefix, trailing): def cmd_weather(self, msg, cmd):
prefixObj = self.bot.decodePrefix(prefix) hasUnit = self.attr.get(msg.prefix.nick, "weather-unit")
fromWho = prefixObj.nick
replyTo = args[0] if "#" in args[0] else fromWho
hasUnit = self.attr.get(fromWho, "weather-unit")
if hasUnit: if hasUnit:
hasUnit = hasUnit.upper() hasUnit = hasUnit.upper()
cmd = self.bot.messageHasCommand([".w", ".weather"], trailing) if len(cmd.args_str) > 0:
if cmd: self.send_weather(msg.args[0], msg.prefix.nick, cmd.args_str, hasUnit)
if len(cmd.args_str) > 0: return
self.send_weather(replyTo, fromWho, cmd.args_str, hasUnit)
return
weatherZip = self.attr.get(fromWho, "weather-zip") weatherZip = self.attr.get(msg.prefix.nick, "weather-zip")
if weatherZip is None: if weatherZip is None:
self.bot.act_PRIVMSG(replyTo, "%s: you must set a location with .setloc" % (fromWho,)) self.bot.act_PRIVMSG(msg.args[0], "%s: you must set a location with .setloc" % (msg.prefix.nick,))
return return
self.bot.act_PRIVMSG(replyTo, "%s: %s" % (fromWho, self.getWeather(weatherZip, hasUnit))) self.bot.act_PRIVMSG(msg.args[0], "%s: %s" % (msg.prefix.nick, self.getWeather(weatherZip, hasUnit)))
cmd = self.bot.messageHasCommand(".setloc", trailing) @info("setloc <location> set your home location for weather lookups", cmds=["setloc"])
if cmd and not args[0] == "#": @command("setloc", allow_private=True)
def cmd_setloc(self, msg, cmd):
# if not cmd.args:
# self.bot.act_PRIVMSG(fromWho, ".setloc: set your location for weather lookup. Example: "
# ".setloc Rochester, NY")
# return
reply_to = msg.args[0] if msg.args[0].startswith("#") else msg.prefix.nick
weatherLoc = cmd.args_str
try:
result = self.getWeather(weatherLoc) # NOQA
except LocationNotSpecificException as lnse:
self.bot.act_PRIVMSG(reply_to, "'%s': location not specific enough. Did you mean: %s" %
(weatherLoc, self.alternates_to_str(lnse.alternates)))
return
except LocationException as le:
self.bot.act_PRIVMSG(reply_to, "'%s': location not found: %s" % (weatherLoc, le))
return
if not cmd.args: if not self.login.check(msg.prefix.nick, msg.prefix.hostname):
self.bot.act_PRIVMSG(fromWho, ".setloc: set your location for weather lookup. Example: " self.bot.act_PRIVMSG(reply_to, ".setloc: you need to be logged in to do that (try .login)")
".setloc Rochester, NY") return
return
weatherLoc = cmd.args_str self.attr.set(msg.prefix.nick, "weather-zip", weatherLoc)
self.bot.act_PRIVMSG(reply_to, "Saved your location as %s"
% self.attr.get(msg.prefix.nick, "weather-zip"))
if self.attr.get(msg.prefix.nick, "weather-zip") is None:
self.bot.act_PRIVMSG(reply_to, "Tip: choose C or F with .wunit <C/F>")
try: @info("wunit <c|f> set preferred weather unit", cmds=["wunit"])
result = self.getWeather(weatherLoc) # NOQA @command("wunit", allow_private=True)
except LocationNotSpecificException as lnse: def cmd_wunit(self, msg, cmd):
self.bot.act_PRIVMSG(fromWho, "'%s': location not specific enough. Did you mean: %s" % if cmd.args[0].lower() not in ['c', 'f']:
(weatherLoc, self.alternates_to_str(lnse.alternates))) return
return unit = cmd.args[0].lower()
except LocationException as le: reply_to = msg.args[0] if msg.args[0].startswith("#") else msg.prefix.nick
self.bot.act_PRIVMSG(fromWho, "'%s': location not found: %s" % (weatherLoc, le))
return
if not self.login.check(prefixObj.nick, prefixObj.hostname): # if unit is None:
self.bot.act_PRIVMSG(fromWho, ".setloc: you need to be logged in to do that (try .login)") # self.bot.act_PRIVMSG(fromWho, ".wunit: set your preferred temperature unit to C or F")
return # return
self.attr.set(fromWho, "weather-zip", weatherLoc) if not self.login.check(msg.prefix.nick, msg.prefix.hostname):
self.bot.act_PRIVMSG(fromWho, "Saved your location as %s" % self.attr.get(fromWho, "weather-zip")) self.bot.act_PRIVMSG(reply_to, ".wunit: you need to be logged in to do that (try .login)")
if self.attr.get(fromWho, "weather-zip") is None: return
self.bot.act_PRIVMSG(fromWho, "Tip: choose C or F with .wunit <C/F>")
cmd = self.bot.messageHasCommand(".wunit", trailing) self.attr.set(msg.prefix.nick, "weather-unit", unit.lower())
if cmd and not args[0] == "#": self.bot.act_PRIVMSG(reply_to, "Saved your preferred unit as %s" % unit)
unit = None
try:
assert cmd.args[0].lower() in ['c', 'f']
unit = cmd.args[0]
except:
pass
if unit is None:
self.bot.act_PRIVMSG(fromWho, ".wunit: set your preferred temperature unit to C or F")
return
if not self.login.check(prefixObj.nick, prefixObj.hostname):
self.bot.act_PRIVMSG(fromWho, ".wunit: you need to be logged in to do that (try .login)")
return
self.attr.set(fromWho, "weather-unit", unit.lower())
self.bot.act_PRIVMSG(fromWho, "Saved your preferred unit as %s" %
self.attr.get(fromWho, "weather-unit").upper())
def send_weather(self, target, hilight, location, units=None): def send_weather(self, target, hilight, location, units=None):
try: try:

View File

@ -7,7 +7,9 @@
""" """
from pyircbot.modulebase import ModuleBase, ModuleHook from pyircbot.modulebase import ModuleBase, command
from pyircbot.modules.ModInfo import info
from random import shuffle
from requests import get from requests import get
import time import time
import re import re
@ -16,7 +18,6 @@ import re
class Youtube(ModuleBase): class Youtube(ModuleBase):
def __init__(self, bot, moduleName): def __init__(self, bot, moduleName):
ModuleBase.__init__(self, bot, moduleName) ModuleBase.__init__(self, bot, moduleName)
self.hooks = [ModuleHook("PRIVMSG", self.youtube)]
def getISOdurationseconds(self, stamp): def getISOdurationseconds(self, stamp):
ISO_8601_period_rx = re.compile( ISO_8601_period_rx = re.compile(
@ -33,25 +34,24 @@ class Youtube(ModuleBase):
) # http://stackoverflow.com/a/16742742 ) # http://stackoverflow.com/a/16742742
return ISO_8601_period_rx.match(stamp).groupdict() return ISO_8601_period_rx.match(stamp).groupdict()
def youtube(self, args, prefix, trailing): @info("yt search for youtube videos", cmds=["yt", "youtube"])
@command("yt", "youtube")
def youtube(self, msg, cmd):
j = get("https://www.googleapis.com/youtube/v3/search",
params={"key": self.config["api_key"],
"part": "snippet",
"type": "video",
"maxResults": "25",
"safeSearch": self.config.get("safe_search", "none"),
"q": cmd.args_str}).json()
cmd = self.bot.messageHasCommand(".youtube", trailing) if 'error' in j or len(j["items"]) == 0:
if not cmd: self.bot.act_PRIVMSG(msg.args[0], "No results found.")
cmd = self.bot.messageHasCommand(".yt", trailing) else:
if cmd and args[0][0:1] == "#": shuffle(j['items'])
# TODO search youtube vid_id = j["items"][0]['id']['videoId']
if cmd.args_str.strip() == "": self.bot.act_PRIVMSG(msg.args[0], "http://youtu.be/{} :: {}".format(vid_id,
self.bot.act_PRIVMSG(args[0], '.youtube <query> -- returns the first YouTube search result for <query>') self.get_video_description(vid_id)))
return
j = get("http://gdata.youtube.com/feeds/api/videos?v=2&alt=jsonc&max-results=1",
params={"q": trailing}).json()
if 'error' in j or j['data']['totalItems'] == 0:
self.bot.act_PRIVMSG(args[0], "YouTube: No results found.")
else:
vid_id = j['data']['items'][0]['id']
vidinfo = self.get_video_description(vid_id)
if vidinfo:
self.bot.act_PRIVMSG(args[0], "http://youtu.be/%s :: %s" % (vid_id, vidinfo))
def get_video_description(self, vid_id): def get_video_description(self, vid_id):
apidata = get('https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails,statistics&id=%s' apidata = get('https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails,statistics&id=%s'

View File

@ -10,128 +10,21 @@ import logging
import sys import sys
from pyircbot.rpc import BotRPC from pyircbot.rpc import BotRPC
from pyircbot.irccore import IRCCore from pyircbot.irccore import IRCCore
from collections import namedtuple
from socket import AF_INET, AF_INET6 from socket import AF_INET, AF_INET6
from json import load
import os.path import os.path
import asyncio import asyncio
import traceback import traceback
ParsedCommand = namedtuple("ParsedCommand", "command args args_str message") class ModuleLoader(object):
def __init__(self):
class PyIRCBot(object):
""":param botconfig: The configuration of this instance of the bot. Passed by main.py.
:type botconfig: dict
"""
version = "4.1.0"
def __init__(self, botconfig):
self.log = logging.getLogger('PyIRCBot')
"""Reference to logger object"""
self.loop = asyncio.get_event_loop()
"""saved copy of the instance config"""
self.botconfig = botconfig
"""storage of imported modules""" """storage of imported modules"""
self.modules = {} self.modules = {}
"""instances of modules""" """instances of modules"""
self.moduleInstances = {} self.moduleInstances = {}
"""Reference to BotRPC thread""" self.log = logging.getLogger('ModuleLoader')
self.rpc = BotRPC(self)
ratelimit = self.botconfig["connection"].get("rate_limit", None) or dict(rate_max=5.0, rate_int=1.1)
"""IRC protocol handler"""
self.irc = IRCCore(servers=self.botconfig["connection"]["servers"],
loop=self.loop,
rate_limit=True if ratelimit else False,
rate_max=ratelimit["rate_max"],
rate_int=ratelimit["rate_int"])
if self.botconfig.get("connection").get("force_ipv6", False):
self.irc.connection_family = AF_INET6
elif self.botconfig.get("connection").get("force_ipv4", False):
self.irc.connection_family = AF_INET
self.irc.bind_addr = self.botconfig.get("connection").get("bind", None)
# legacy support
self.act_PONG = self.irc.act_PONG
self.act_USER = self.irc.act_USER
self.act_NICK = self.irc.act_NICK
self.act_JOIN = self.irc.act_JOIN
self.act_PRIVMSG = self.irc.act_PRIVMSG
self.act_MODE = self.irc.act_MODE
self.act_ACTION = self.irc.act_ACTION
self.act_KICK = self.irc.act_KICK
self.act_QUIT = self.irc.act_QUIT
self.act_PASS = self.irc.act_PASS
self.get_nick = self.irc.get_nick
self.decodePrefix = IRCCore.decodePrefix
# Load modules
self.initModules()
# Internal usage hook
self.irc.addHook("PRIVMSG", self._irchook_internal)
def run(self):
self.client = asyncio.ensure_future(self.irc.loop(self.loop), loop=self.loop)
try:
self.loop.set_debug(True)
self.loop.run_until_complete(self.client)
finally:
logging.debug("Escaped main loop")
def disconnect(self, message):
"""Send quit message and disconnect from IRC.
:param message: Quit message
:type message: str
:param reconnect: True causes a reconnection attempt to be made after the disconnect
:type reconnect: bool
"""
self.log.info("disconnect")
self.kill(message=message)
def kill(self, message="Help! Another thread is killing me :(", forever=True):
"""Close the connection violently
:param sys_exit: True causes sys.exit(0) to be called
:type sys_exit: bool
:param message: Quit message
:type message: str
"""
if forever:
self.closeAllModules()
asyncio.run_coroutine_threadsafe(self.irc.kill(message=message, forever=forever), self.loop)
def initModules(self):
"""load modules specified in instance config"""
" append module location to path "
sys.path.append(os.path.dirname(__file__) + "/modules/")
" append usermodule dir to beginning of path"
for path in self.botconfig["bot"]["usermodules"]:
sys.path.insert(0, path + "/")
for modulename in self.botconfig["modules"]:
self.loadmodule(modulename)
def _irchook_internal(self, msg):
"""
IRC hook handler. Calling point for IRCHook based module hooks. This method is called when a message is
received. It tests all hooks in all modules against the message can calls the hooked function on hits.
"""
for module_name, module in self.moduleInstances.items():
for hook in module.irchooks:
validation = hook.validator(msg, self)
if validation:
hook.method(msg, validation)
def importmodule(self, name): def importmodule(self, name):
"""Import a module """Import a module
@ -189,7 +82,7 @@ class PyIRCBot(object):
" init the module " " init the module "
self.moduleInstances[name] = getattr(self.modules[name], name)(self, name) self.moduleInstances[name] = getattr(self.modules[name], name)(self, name)
" load hooks " " load hooks "
self.loadModuleHooks(self.moduleInstances[name]) # self.loadModuleHooks(self.moduleInstances[name])
self.moduleInstances[name].onenable() self.moduleInstances[name].onenable()
def unloadmodule(self, name): def unloadmodule(self, name):
@ -201,7 +94,7 @@ class PyIRCBot(object):
" notify the module of disabling " " notify the module of disabling "
self.moduleInstances[name].ondisable() self.moduleInstances[name].ondisable()
" unload all hooks " " unload all hooks "
self.unloadModuleHooks(self.moduleInstances[name]) # self.unloadModuleHooks(self.moduleInstances[name])
" remove & delete the instance " " remove & delete the instance "
self.moduleInstances.pop(name) self.moduleInstances.pop(name)
self.log.info("Module %s unloaded" % name) self.log.info("Module %s unloaded" % name)
@ -246,32 +139,6 @@ class PyIRCBot(object):
self.loadmodule(name) self.loadmodule(name)
return (True, None) return (True, None)
def loadModuleHooks(self, module):
"""**Internal.** Enable (connect) hooks of a module
:param module: module object to hook in
:type module: object"""
" activate a module's hooks "
for hook in module.hooks:
if type(hook.hook) == list:
for hookcmd in hook.hook:
self.irc.addHook(hookcmd, hook.method)
else:
self.irc.addHook(hook.hook, hook.method)
def unloadModuleHooks(self, module):
"""**Internal.** Disable (disconnect) hooks of a module
:param module: module object to unhook
:type module: object"""
" remove a modules hooks "
for hook in module.hooks:
if type(hook.hook) == list:
for hookcmd in hook.hook:
self.irc.removeHook(hookcmd, hook.method)
else:
self.irc.removeHook(hook.hook, hook.method)
def getmodulebyname(self, name): def getmodulebyname(self, name):
"""Get a module object by name """Get a module object by name
@ -305,8 +172,16 @@ class PyIRCBot(object):
return m[0] return m[0]
return None return None
class PrimitiveBot(ModuleLoader):
def __init__(self, botconfig):
super().__init__()
self.botconfig = botconfig
def closeAllModules(self): def closeAllModules(self):
""" Deport all modules (for shutdown). Modules are unloaded in the opposite order listed in the config. """ """
Deport all modules (for shutdown). Modules are unloaded in the opposite order listed in the config.
"""
loaded = list(self.moduleInstances.keys()) loaded = list(self.moduleInstances.keys())
loadOrder = self.botconfig["modules"] loadOrder = self.botconfig["modules"]
loadOrder.reverse() loadOrder.reverse()
@ -318,16 +193,6 @@ class PyIRCBot(object):
self.deportmodule(key) self.deportmodule(key)
" Filesystem Methods " " Filesystem Methods "
def getDataPath(self, moduleName):
"""Return the absolute path for a module's data dir
:param moduleName: the module who's data dir we want
:type moduleName: str"""
module_dir = os.path.join(self.botconfig["bot"]["datadir"], "data", moduleName)
if not os.path.exists(module_dir):
os.mkdir(module_dir)
return module_dir
def getConfigPath(self, moduleName): def getConfigPath(self, moduleName):
"""Return the absolute path for a module's config file """Return the absolute path for a module's config file
@ -340,63 +205,140 @@ class PyIRCBot(object):
return "%s.json" % basepath return "%s.json" % basepath
return None return None
" Utility methods " def getDataPath(self, moduleName):
@staticmethod """Return the absolute path for a module's data dir
def messageHasCommand(command, message, requireArgs=False):
"""Check if a message has a command with or without args in it
:param command: the command string to look for, like !ban. If a list is passed, the first match is returned. :param moduleName: the module who's data dir we want
:type command: str or list :type moduleName: str"""
:param message: the message string to look in, like "!ban Lil_Mac" module_dir = os.path.join(self.botconfig["bot"]["datadir"], "data", moduleName)
if not os.path.exists(module_dir):
os.mkdir(module_dir)
return module_dir
class PyIRCBot(PrimitiveBot):
""":param botconfig: The configuration of this instance of the bot. Passed by main.py.
:type botconfig: dict
"""
version = "5.0.0"
def __init__(self, botconfig):
super().__init__(botconfig)
self.log = logging.getLogger('PyIRCBot')
"""Reference to logger object"""
self.loop = asyncio.get_event_loop()
"""Reference to BotRPC thread"""
self.rpc = BotRPC(self)
ratelimit = self.botconfig["connection"].get("rate_limit", None) or dict(rate_max=5.0, rate_int=1.1)
"""IRC protocol handler"""
self.irc = IRCCore(servers=self.botconfig["connection"]["servers"],
loop=self.loop,
rate_limit=True if ratelimit else False,
rate_max=ratelimit["rate_max"],
rate_int=ratelimit["rate_int"])
if self.botconfig.get("connection").get("force_ipv6", False):
self.irc.connection_family = AF_INET6
elif self.botconfig.get("connection").get("force_ipv4", False):
self.irc.connection_family = AF_INET
self.irc.bind_addr = self.botconfig.get("connection").get("bind", None)
self.act_PONG = self.irc.act_PONG
self.act_USER = self.irc.act_USER
self.act_NICK = self.irc.act_NICK
self.act_JOIN = self.irc.act_JOIN
self.act_PRIVMSG = self.irc.act_PRIVMSG
self.act_MODE = self.irc.act_MODE
self.act_ACTION = self.irc.act_ACTION
self.act_KICK = self.irc.act_KICK
self.act_QUIT = self.irc.act_QUIT
self.act_PASS = self.irc.act_PASS
self.get_nick = self.irc.get_nick
self.decodePrefix = IRCCore.decodePrefix
# Load modules
self.initModules()
# Internal usage hook
self.irc.addHook("_ALL", self._irchook_internal)
def initModules(self):
"""load modules specified in instance config"""
" append module location to path "
sys.path.append(os.path.dirname(__file__) + "/modules/")
" append usermodule dir to beginning of path"
for path in self.botconfig["bot"]["usermodules"]:
sys.path.insert(0, path + "/")
for modulename in self.botconfig["modules"]:
self.loadmodule(modulename)
def run(self):
self.client = asyncio.ensure_future(self.irc.loop(self.loop), loop=self.loop)
try:
self.loop.set_debug(True)
self.loop.run_until_complete(self.client)
finally:
logging.debug("Escaped main loop")
def disconnect(self, message):
"""Send quit message and disconnect from IRC.
:param message: Quit message
:type message: str :type message: str
:param requireArgs: if true, only validate if the command use has any amount of trailing text :param reconnect: True causes a reconnection attempt to be made after the disconnect
:type requireArgs: bool""" :type reconnect: bool
if not type(command) == list:
command = [command]
for item in command:
cmd = PyIRCBot.messageHasCommandSingle(item, message, requireArgs)
if cmd:
return cmd
return False
@staticmethod
def messageHasCommandSingle(command, message, requireArgs=False):
# Check if the message at least starts with the command
messageBeginning = message[0:len(command)]
if messageBeginning != command:
return False
# Make sure it's not a subset of a longer command (ie .meme being set off by .memes)
subsetCheck = message[len(command):len(command) + 1]
if subsetCheck != " " and subsetCheck != "":
return False
# We've got the command! Do we need args?
argsStart = len(command)
args = ""
if argsStart > 0:
args = message[argsStart + 1:].strip()
if requireArgs and args == '':
return False
# Verified! Return the set.
return ParsedCommand(command,
args.split(" ") if args else [],
args,
message)
@staticmethod
def load(filepath):
"""Return an object from the passed filepath
:param filepath: path to a json file. filename must end with .json
:type filepath: str
:Returns: | dict
""" """
self.log.info("disconnect")
self.kill(message=message)
if filepath.endswith(".json"): def kill(self, message="Help! Another thread is killing me :(", forever=True):
with open(filepath, 'r') as f: """Close the connection violently
return load(f)
else: :param sys_exit: True causes sys.exit(0) to be called
raise Exception("Unknown config format") :type sys_exit: bool
:param message: Quit message
:type message: str
"""
if forever:
self.closeAllModules()
asyncio.run_coroutine_threadsafe(self.irc.kill(message=message, forever=forever), self.loop)
def _irchook_internal(self, msg):
"""
IRC hook handler. Calling point for IRCHook based module hooks. This method is called when any message is
received. It tests all hooks in all modules against the message can calls the hooked function on hits.
"""
for module_name, module in self.moduleInstances.items():
for hook in module.irchooks:
validation = hook.validator(msg, self)
if validation:
hook.method(msg, validation)
" Filesystem Methods "
def getConfigPath(self, moduleName):
"""Return the absolute path for a module's config file
:param moduleName: the module who's config file we want
:type moduleName: str"""
basepath = "%s/config/%s" % (self.botconfig["bot"]["datadir"], moduleName)
if os.path.exists("%s.json" % basepath):
return "%s.json" % basepath
return None
def getDataPath(self, moduleName):
"""Return the absolute path for a module's data dir
:param moduleName: the module who's data dir we want
:type moduleName: str"""
module_dir = os.path.join(self.botconfig["bot"]["datadir"], "data", moduleName)
if not os.path.exists(module_dir):
os.mkdir(module_dir)
return module_dir

8
run-example.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/bash -x
CONFPATH=${1:-examples/config.json}
export PYTHONUNBUFFERED=1
export PYTHONPATH=.
./bin/pyircbot -c $CONFPATH --debug