Refactor to use Twisted IRC instead.
This commit is contained in:
parent
4636ed4a7a
commit
6e9a61b3bf
3 changed files with 304 additions and 2296 deletions
462
ircbot.py
462
ircbot.py
|
|
@ -1,462 +0,0 @@
|
|||
# Copyright (C) 1999--2002 Joel Rosdahl
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Joel Rosdahl <joel@rosdahl.net>
|
||||
#
|
||||
# $Id: ircbot.py,v 1.21 2005/12/23 18:44:43 keltus Exp $
|
||||
|
||||
"""ircbot -- Simple IRC bot library.
|
||||
|
||||
This module contains a single-server IRC bot class that can be used to
|
||||
write simpler bots.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from UserDict import UserDict
|
||||
|
||||
from irclib import SimpleIRCClient
|
||||
from irclib import nm_to_n, irc_lower, all_events
|
||||
from irclib import parse_channel_modes, is_channel
|
||||
from irclib import ServerConnectionError
|
||||
import threading
|
||||
import time
|
||||
|
||||
class OutputManager(threading.Thread):
|
||||
def __init__(self, connection, delay=.5):
|
||||
threading.Thread.__init__(self)
|
||||
self.setDaemon(1)
|
||||
self.connection = connection
|
||||
self.delay = delay
|
||||
self.event = threading.Event()
|
||||
self.queue = []
|
||||
|
||||
def run(self):
|
||||
while 1:
|
||||
self.event.wait()
|
||||
while self.queue:
|
||||
msg,target = self.queue.pop(0)
|
||||
self.connection.privmsg(target, msg)
|
||||
time.sleep(self.delay)
|
||||
self.event.clear()
|
||||
|
||||
def send(self, msg, target):
|
||||
self.queue.append((msg.strip(),target))
|
||||
self.event.set()
|
||||
|
||||
class SingleServerIRCBot(SimpleIRCClient):
|
||||
"""A single-server IRC bot class.
|
||||
|
||||
The bot tries to reconnect if it is disconnected.
|
||||
|
||||
The bot keeps track of the channels it has joined, the other
|
||||
clients that are present in the channels and which of those that
|
||||
have operator or voice modes. The "database" is kept in the
|
||||
self.channels attribute, which is an IRCDict of Channels.
|
||||
"""
|
||||
def __init__(self, server_list, nickname, realname, reconnection_interval=60):
|
||||
"""Constructor for SingleServerIRCBot objects.
|
||||
|
||||
Arguments:
|
||||
|
||||
server_list -- A list of tuples (server, port) that
|
||||
defines which servers the bot should try to
|
||||
connect to.
|
||||
|
||||
nickname -- The bot's nickname.
|
||||
|
||||
realname -- The bot's realname.
|
||||
|
||||
reconnection_interval -- How long the bot should wait
|
||||
before trying to reconnect.
|
||||
|
||||
dcc_connections -- A list of initiated/accepted DCC
|
||||
connections.
|
||||
"""
|
||||
|
||||
SimpleIRCClient.__init__(self)
|
||||
self.channels = IRCDict()
|
||||
self.server_list = server_list
|
||||
if not reconnection_interval or reconnection_interval < 0:
|
||||
reconnection_interval = 2**31
|
||||
self.reconnection_interval = reconnection_interval
|
||||
|
||||
self._nickname = nickname
|
||||
self._realname = realname
|
||||
for i in ["disconnect", "join", "kick", "mode",
|
||||
"namreply", "nick", "part", "quit"]:
|
||||
self.connection.add_global_handler(i,
|
||||
getattr(self, "_on_" + i),
|
||||
-10)
|
||||
def _connected_checker(self):
|
||||
"""[Internal]"""
|
||||
if not self.connection.is_connected():
|
||||
self.connection.execute_delayed(self.reconnection_interval,
|
||||
self._connected_checker)
|
||||
self.jump_server()
|
||||
|
||||
def _connect(self):
|
||||
"""[Internal]"""
|
||||
password = None
|
||||
if len(self.server_list[0]) > 2:
|
||||
password = self.server_list[0][2]
|
||||
try:
|
||||
self.connect(self.server_list[0][0],
|
||||
self.server_list[0][1],
|
||||
self._nickname,
|
||||
password,
|
||||
ircname=self._realname)
|
||||
except ServerConnectionError:
|
||||
pass
|
||||
|
||||
def _on_disconnect(self, c, e):
|
||||
"""[Internal]"""
|
||||
self.channels = IRCDict()
|
||||
self.connection.execute_delayed(self.reconnection_interval,
|
||||
self._connected_checker)
|
||||
|
||||
def _on_join(self, c, e):
|
||||
"""[Internal]"""
|
||||
ch = e.target()
|
||||
nick = nm_to_n(e.source())
|
||||
if nick == c.get_nickname():
|
||||
self.channels[ch] = Channel()
|
||||
self.channels[ch].add_user(nick)
|
||||
|
||||
def _on_kick(self, c, e):
|
||||
"""[Internal]"""
|
||||
nick = e.arguments()[0]
|
||||
channel = e.target()
|
||||
|
||||
if nick == c.get_nickname():
|
||||
del self.channels[channel]
|
||||
else:
|
||||
self.channels[channel].remove_user(nick)
|
||||
|
||||
def _on_mode(self, c, e):
|
||||
"""[Internal]"""
|
||||
modes = parse_channel_modes(" ".join(e.arguments()))
|
||||
t = e.target()
|
||||
if is_channel(t):
|
||||
ch = self.channels[t]
|
||||
for mode in modes:
|
||||
if mode[0] == "+":
|
||||
f = ch.set_mode
|
||||
else:
|
||||
f = ch.clear_mode
|
||||
f(mode[1], mode[2])
|
||||
else:
|
||||
# Mode on self... XXX
|
||||
pass
|
||||
|
||||
def _on_namreply(self, c, e):
|
||||
"""[Internal]"""
|
||||
|
||||
# e.arguments()[0] == "@" for secret channels,
|
||||
# "*" for private channels,
|
||||
# "=" for others (public channels)
|
||||
# e.arguments()[1] == channel
|
||||
# e.arguments()[2] == nick list
|
||||
|
||||
ch = e.arguments()[1]
|
||||
for nick in e.arguments()[2].split():
|
||||
if nick[0] == "@":
|
||||
nick = nick[1:]
|
||||
self.channels[ch].set_mode("o", nick)
|
||||
elif nick[0] == "+":
|
||||
nick = nick[1:]
|
||||
self.channels[ch].set_mode("v", nick)
|
||||
self.channels[ch].add_user(nick)
|
||||
|
||||
def _on_nick(self, c, e):
|
||||
"""[Internal]"""
|
||||
before = nm_to_n(e.source())
|
||||
after = e.target()
|
||||
for ch in self.channels.values():
|
||||
if ch.has_user(before):
|
||||
ch.change_nick(before, after)
|
||||
|
||||
def _on_part(self, c, e):
|
||||
"""[Internal]"""
|
||||
nick = nm_to_n(e.source())
|
||||
channel = e.target()
|
||||
|
||||
if nick == c.get_nickname():
|
||||
del self.channels[channel]
|
||||
else:
|
||||
self.channels[channel].remove_user(nick)
|
||||
|
||||
def _on_quit(self, c, e):
|
||||
"""[Internal]"""
|
||||
nick = nm_to_n(e.source())
|
||||
for ch in self.channels.values():
|
||||
if ch.has_user(nick):
|
||||
ch.remove_user(nick)
|
||||
|
||||
def die(self, msg="Bye, cruel world!"):
|
||||
"""Let the bot die.
|
||||
|
||||
Arguments:
|
||||
|
||||
msg -- Quit message.
|
||||
"""
|
||||
|
||||
self.connection.disconnect(msg)
|
||||
sys.exit(0)
|
||||
|
||||
def disconnect(self, msg="I'll be back!"):
|
||||
"""Disconnect the bot.
|
||||
|
||||
The bot will try to reconnect after a while.
|
||||
|
||||
Arguments:
|
||||
|
||||
msg -- Quit message.
|
||||
"""
|
||||
self.connection.disconnect(msg)
|
||||
|
||||
def get_version(self):
|
||||
"""Returns the bot version.
|
||||
|
||||
Used when answering a CTCP VERSION request.
|
||||
"""
|
||||
return "ircbot.py by Joel Rosdahl <joel@rosdahl.net>"
|
||||
|
||||
def jump_server(self, msg="Changing servers"):
|
||||
"""Connect to a new server, possibly disconnecting from the current.
|
||||
|
||||
The bot will skip to next server in the server_list each time
|
||||
jump_server is called.
|
||||
"""
|
||||
if self.connection.is_connected():
|
||||
self.connection.disconnect(msg)
|
||||
|
||||
self.server_list.append(self.server_list.pop(0))
|
||||
self._connect()
|
||||
|
||||
def on_ctcp(self, c, e):
|
||||
"""Default handler for ctcp events.
|
||||
|
||||
Replies to VERSION and PING requests and relays DCC requests
|
||||
to the on_dccchat method.
|
||||
"""
|
||||
if e.arguments()[0] == "VERSION":
|
||||
c.ctcp_reply(nm_to_n(e.source()),
|
||||
"VERSION " + self.get_version())
|
||||
elif e.arguments()[0] == "PING":
|
||||
if len(e.arguments()) > 1:
|
||||
c.ctcp_reply(nm_to_n(e.source()),
|
||||
"PING " + e.arguments()[1])
|
||||
elif e.arguments()[0] == "DCC" and e.arguments()[1].split(" ", 1)[0] == "CHAT":
|
||||
self.on_dccchat(c, e)
|
||||
|
||||
def on_dccchat(self, c, e):
|
||||
pass
|
||||
|
||||
def start(self):
|
||||
"""Start the bot."""
|
||||
self._connect()
|
||||
SimpleIRCClient.start(self)
|
||||
|
||||
|
||||
class IRCDict:
|
||||
"""A dictionary suitable for storing IRC-related things.
|
||||
|
||||
Dictionary keys a and b are considered equal if and only if
|
||||
irc_lower(a) == irc_lower(b)
|
||||
|
||||
Otherwise, it should behave exactly as a normal dictionary.
|
||||
"""
|
||||
|
||||
def __init__(self, dict=None):
|
||||
self.data = {}
|
||||
self.canon_keys = {} # Canonical keys
|
||||
if dict is not None:
|
||||
self.update(dict)
|
||||
def __repr__(self):
|
||||
return repr(self.data)
|
||||
def __cmp__(self, dict):
|
||||
if isinstance(dict, IRCDict):
|
||||
return cmp(self.data, dict.data)
|
||||
else:
|
||||
return cmp(self.data, dict)
|
||||
def __len__(self):
|
||||
return len(self.data)
|
||||
def __getitem__(self, key):
|
||||
return self.data[self.canon_keys[irc_lower(key)]]
|
||||
def __setitem__(self, key, item):
|
||||
if key in self:
|
||||
del self[key]
|
||||
self.data[key] = item
|
||||
self.canon_keys[irc_lower(key)] = key
|
||||
def __delitem__(self, key):
|
||||
ck = irc_lower(key)
|
||||
del self.data[self.canon_keys[ck]]
|
||||
del self.canon_keys[ck]
|
||||
def __iter__(self):
|
||||
return iter(self.data)
|
||||
def __contains__(self, key):
|
||||
return self.has_key(key)
|
||||
def clear(self):
|
||||
self.data.clear()
|
||||
self.canon_keys.clear()
|
||||
def copy(self):
|
||||
if self.__class__ is UserDict:
|
||||
return UserDict(self.data)
|
||||
import copy
|
||||
return copy.copy(self)
|
||||
def keys(self):
|
||||
return self.data.keys()
|
||||
def items(self):
|
||||
return self.data.items()
|
||||
def values(self):
|
||||
return self.data.values()
|
||||
def has_key(self, key):
|
||||
return irc_lower(key) in self.canon_keys
|
||||
def update(self, dict):
|
||||
for k, v in dict.items():
|
||||
self.data[k] = v
|
||||
def get(self, key, failobj=None):
|
||||
return self.data.get(key, failobj)
|
||||
|
||||
|
||||
class Channel:
|
||||
"""A class for keeping information about an IRC channel.
|
||||
|
||||
This class can be improved a lot.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.userdict = IRCDict()
|
||||
self.operdict = IRCDict()
|
||||
self.voiceddict = IRCDict()
|
||||
self.modes = {}
|
||||
|
||||
def users(self):
|
||||
"""Returns an unsorted list of the channel's users."""
|
||||
return self.userdict.keys()
|
||||
|
||||
def opers(self):
|
||||
"""Returns an unsorted list of the channel's operators."""
|
||||
return self.operdict.keys()
|
||||
|
||||
def voiced(self):
|
||||
"""Returns an unsorted list of the persons that have voice
|
||||
mode set in the channel."""
|
||||
return self.voiceddict.keys()
|
||||
|
||||
def has_user(self, nick):
|
||||
"""Check whether the channel has a user."""
|
||||
return nick in self.userdict
|
||||
|
||||
def is_oper(self, nick):
|
||||
"""Check whether a user has operator status in the channel."""
|
||||
return nick in self.operdict
|
||||
|
||||
def is_voiced(self, nick):
|
||||
"""Check whether a user has voice mode set in the channel."""
|
||||
return nick in self.voiceddict
|
||||
|
||||
def add_user(self, nick):
|
||||
self.userdict[nick] = 1
|
||||
|
||||
def remove_user(self, nick):
|
||||
for d in self.userdict, self.operdict, self.voiceddict:
|
||||
if nick in d:
|
||||
del d[nick]
|
||||
|
||||
def change_nick(self, before, after):
|
||||
self.userdict[after] = 1
|
||||
del self.userdict[before]
|
||||
if before in self.operdict:
|
||||
self.operdict[after] = 1
|
||||
del self.operdict[before]
|
||||
if before in self.voiceddict:
|
||||
self.voiceddict[after] = 1
|
||||
del self.voiceddict[before]
|
||||
|
||||
def set_mode(self, mode, value=None):
|
||||
"""Set mode on the channel.
|
||||
|
||||
Arguments:
|
||||
|
||||
mode -- The mode (a single-character string).
|
||||
|
||||
value -- Value
|
||||
"""
|
||||
if mode == "o":
|
||||
self.operdict[value] = 1
|
||||
elif mode == "v":
|
||||
self.voiceddict[value] = 1
|
||||
else:
|
||||
self.modes[mode] = value
|
||||
|
||||
def clear_mode(self, mode, value=None):
|
||||
"""Clear mode on the channel.
|
||||
|
||||
Arguments:
|
||||
|
||||
mode -- The mode (a single-character string).
|
||||
|
||||
value -- Value
|
||||
"""
|
||||
try:
|
||||
if mode == "o":
|
||||
del self.operdict[value]
|
||||
elif mode == "v":
|
||||
del self.voiceddict[value]
|
||||
else:
|
||||
del self.modes[mode]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def has_mode(self, mode):
|
||||
return mode in self.modes
|
||||
|
||||
def is_moderated(self):
|
||||
return self.has_mode("m")
|
||||
|
||||
def is_secret(self):
|
||||
return self.has_mode("s")
|
||||
|
||||
def is_protected(self):
|
||||
return self.has_mode("p")
|
||||
|
||||
def has_topic_lock(self):
|
||||
return self.has_mode("t")
|
||||
|
||||
def is_invite_only(self):
|
||||
return self.has_mode("i")
|
||||
|
||||
def has_allow_external_messages(self):
|
||||
return self.has_mode("n")
|
||||
|
||||
def has_limit(self):
|
||||
return self.has_mode("l")
|
||||
|
||||
def limit(self):
|
||||
if self.has_limit():
|
||||
return self.modes[l]
|
||||
else:
|
||||
return None
|
||||
|
||||
def has_key(self):
|
||||
return self.has_mode("k")
|
||||
|
||||
def key(self):
|
||||
if self.has_key():
|
||||
return self.modes["k"]
|
||||
else:
|
||||
return None
|
||||
588
lolbot.py
588
lolbot.py
|
|
@ -2,8 +2,7 @@
|
|||
#
|
||||
# LolBot
|
||||
#
|
||||
# Code originally based on example bot and irc-bot class from
|
||||
# Joel Rosdahl <joel@rosdahl.net>, author of included python-irclib.
|
||||
# New version based on Twisted IRC.
|
||||
|
||||
"""
|
||||
Useful bot for folks stuck behind censor walls at work
|
||||
|
|
@ -11,20 +10,22 @@ Logs a channel and collects URLs for later.
|
|||
"""
|
||||
|
||||
try:
|
||||
import sys, string, random, time
|
||||
from ircbot import SingleServerIRCBot, OutputManager
|
||||
from irclib import nm_to_n, nm_to_h, irc_lower
|
||||
import sys
|
||||
import os
|
||||
|
||||
import string
|
||||
import random
|
||||
import time
|
||||
import getopt
|
||||
from twisted.words.protocols import irc
|
||||
from twisted.internet import protocol
|
||||
from twisted.internet import reactor
|
||||
from datetime import datetime
|
||||
from mechanize import Browser
|
||||
|
||||
import getopt
|
||||
from sqlalchemy import MetaData, Table, Column, String, Text, Integer, DateTime, engine_from_config
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
except ImportError:
|
||||
print "Some modules could not be loaded: Lolbot relies on Mechanize and SQLAlchemy.\n"
|
||||
print "Some modules missing: Lolbot relies on Twisted IRC, Mechanize and SQLAlchemy.\n"
|
||||
sys.exit
|
||||
|
||||
# Exclamations - wrong input
|
||||
|
|
@ -48,310 +49,341 @@ ponderings = [
|
|||
SqlBase = declarative_base()
|
||||
|
||||
class Log(SqlBase):
|
||||
"""
|
||||
This class represents an event in the log table and inherits from a SQLAlchemy
|
||||
convenience ORM class.
|
||||
"""
|
||||
"""
|
||||
This class represents an event in the log table and inherits from a SQLAlchemy
|
||||
convenience ORM class.
|
||||
"""
|
||||
|
||||
__tablename__ = "log"
|
||||
__tablename__ = "log"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
timestamp = Column(DateTime)
|
||||
nickname = Column(String(20))
|
||||
text = Column(Text)
|
||||
id = Column(Integer, primary_key=True)
|
||||
timestamp = Column(DateTime)
|
||||
nickname = Column(String(20))
|
||||
text = Column(Text)
|
||||
|
||||
def __init__(self, nickname, text, timestamp=None):
|
||||
if timestamp is None:
|
||||
timestamp = datetime.now()
|
||||
self.timestamp = timestamp
|
||||
self.nickname = nickname
|
||||
self.text = text
|
||||
def __init__(self, nickname, text, timestamp=None):
|
||||
"""
|
||||
Creates an event log for the IRC logger.
|
||||
"""
|
||||
if timestamp is None:
|
||||
timestamp = datetime.now()
|
||||
self.timestamp = timestamp
|
||||
self.nickname = nickname
|
||||
self.text = text
|
||||
|
||||
def __repr__(self):
|
||||
return "(%s) %s: %s" % (self.timestamp.strftime("%Y-%m-%d %H:%M:%S"), self.nickname, self.text)
|
||||
def __repr__(self):
|
||||
return "(%s) %s: %s" % (self.timestamp.strftime("%Y-%m-%d %H:%M:%S"), self.nickname, self.text)
|
||||
|
||||
class Url(SqlBase):
|
||||
"""
|
||||
This class represents a saved URL and inherits from a SQLAlchemy convenience
|
||||
ORM class.
|
||||
"""
|
||||
"""
|
||||
This class represents a saved URL and inherits from a SQLAlchemy convenience
|
||||
ORM class.
|
||||
"""
|
||||
|
||||
__tablename__ = "url"
|
||||
__tablename__ = "url"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
timestamp = Column(DateTime)
|
||||
nickname = Column(String(20))
|
||||
url = Column(String(200), unique=True)
|
||||
title = Column(Text)
|
||||
id = Column(Integer, primary_key=True)
|
||||
timestamp = Column(DateTime)
|
||||
nickname = Column(String(20))
|
||||
url = Column(String(200), unique=True)
|
||||
title = Column(Text)
|
||||
|
||||
def __init__(self, nickname, url, title=None, timestamp=None):
|
||||
if timestamp is None:
|
||||
timestamp = datetime.now()
|
||||
self.timestamp = timestamp
|
||||
self.nickname = nickname
|
||||
self.url = url
|
||||
self.title = title
|
||||
def __init__(self, nickname, url, title=None, timestamp=None):
|
||||
if timestamp is None:
|
||||
timestamp = datetime.now()
|
||||
self.timestamp = timestamp
|
||||
self.nickname = nickname
|
||||
self.url = url
|
||||
self.title = title
|
||||
|
||||
# populate the title from the URL if not given.
|
||||
if title is None:
|
||||
try:
|
||||
br = Browser()
|
||||
br.open(self.url)
|
||||
self.title = br.title()
|
||||
except Exception as ex:
|
||||
self.title = ''
|
||||
# populate the title from the URL if not given.
|
||||
if title is None:
|
||||
try:
|
||||
br = Browser()
|
||||
br.open(self.url)
|
||||
self.title = br.title()
|
||||
except Exception as ex:
|
||||
self.title = ''
|
||||
|
||||
def __repr__(self):
|
||||
if not self.title:
|
||||
return "%s: %s" % (self.nickname, self.url)
|
||||
else:
|
||||
return "%s: %s - %s" % (self.nickname, self.url, self.title)
|
||||
def __repr__(self):
|
||||
if not self.title:
|
||||
return "%s: %s" % (self.nickname, self.url)
|
||||
else:
|
||||
return "%s: %s - %s" % (self.nickname, self.url, self.title)
|
||||
|
||||
class LolBot(SingleServerIRCBot):
|
||||
"""
|
||||
The Lolbot itself.
|
||||
"""
|
||||
class LolBot(irc.IRCClient):
|
||||
"""
|
||||
The Lolbot itself.
|
||||
"""
|
||||
|
||||
def __init__(self, config_path=''):
|
||||
self.get_config(config_path)
|
||||
SingleServerIRCBot.__init__(self, [(self.server, self.port)], self.nickname, self.nickname)
|
||||
def created(self, when):
|
||||
# connect to the database
|
||||
self.dbengine = engine_from_config(self.config, prefix="db.")
|
||||
SqlBase.metadata.bind = self.dbengine
|
||||
SqlBase.metadata.create_all()
|
||||
self.get_session = sessionmaker(bind=self.dbengine)
|
||||
|
||||
# connect to the database
|
||||
self.dbengine = engine_from_config(self.config, prefix="db.")
|
||||
SqlBase.metadata.bind = self.dbengine
|
||||
SqlBase.metadata.create_all()
|
||||
self.get_session = sessionmaker(bind=self.dbengine)
|
||||
self.helptext = "Keeps a list of URLs. Commands: list [n|x-y] - prints the last 10 URLs (or n URLs, or x through y); clear - clears the list; lol - say something funny; <url> - adds the URL to the list; help - this message."
|
||||
|
||||
self.helptext = "Keeps a list of URLs. Commands: list [n|x-y] - prints the last 10 URLs (or n URLs, or x through y); clear - clears the list; lol - say something funny; <url> - adds the URL to the list; help - this message."
|
||||
def now(self):
|
||||
return datetime.today().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
self.queue = OutputManager(self.connection)
|
||||
self.queue.start()
|
||||
self.start()
|
||||
def save_url(self, nickname, url):
|
||||
title = False
|
||||
try:
|
||||
db = self.get_session()
|
||||
if not db.query(Url).filter(Url.url == url).count():
|
||||
theurl = Url(nickname, url)
|
||||
db.add(theurl)
|
||||
db.commit()
|
||||
else:
|
||||
theurl = db.query(Url).filter(Url.url == url).one()
|
||||
print theurl
|
||||
title = theurl.title
|
||||
except Exception, ex:
|
||||
print "Exception caught saving URL: %s" % ex
|
||||
return title
|
||||
|
||||
def get_config(self, config_path):
|
||||
def log_event(self, nick, text):
|
||||
try:
|
||||
entry = Log(nick, text)
|
||||
db = self.get_session()
|
||||
db.add(entry)
|
||||
db.commit()
|
||||
print entry
|
||||
except Exception, ex:
|
||||
print "Exception caught logging event: %s" % ex
|
||||
|
||||
def _get_nickname(self):
|
||||
return self.factory.nickname
|
||||
nickname = property(_get_nickname)
|
||||
|
||||
def _get_channel(self):
|
||||
return self.factory.channel
|
||||
channel = property(_get_channel)
|
||||
|
||||
def _get_config(self):
|
||||
return self.factory.config
|
||||
config = property(_get_config)
|
||||
|
||||
def signedOn(self):
|
||||
self.join(self.channel)
|
||||
print "Signed on as %s." % (self.nickname,)
|
||||
|
||||
def joined(self, channel):
|
||||
print "Joined %s." % (channel,)
|
||||
|
||||
def privmsg(self, user, channel, msg):
|
||||
if channel != self.channel:
|
||||
# Private /msg from a user
|
||||
self.do_command(msg, user)
|
||||
else:
|
||||
# log it
|
||||
self.log_event(user, msg)
|
||||
|
||||
args = string.split(msg, ":", 1)
|
||||
if len(args) > 1 and args[0] == self.nickname:
|
||||
self.do_command(string.strip(args[1]))
|
||||
else:
|
||||
# parse it for links, add URLs to the list
|
||||
words = msg.split(" ")
|
||||
for w in words:
|
||||
if w.startswith('http://') or w.startswith('https://'):
|
||||
title = self.save_url(user, w)
|
||||
if title == False:
|
||||
self.say_public("Sorry, I'm useless at UTF-8.")
|
||||
else:
|
||||
self.say_public("URL added. %s" % title)
|
||||
|
||||
def say_public(self, text):
|
||||
"Print TEXT into public channel, for all to see."
|
||||
self.msg(self.channel, text)
|
||||
self.log_event(self.nickname, text)
|
||||
|
||||
def say_private(self, nick, text):
|
||||
"Send private message of TEXT to NICK."
|
||||
self.msg(nick, text)
|
||||
|
||||
def reply(self, text, to_private=None):
|
||||
"Send TEXT to either public channel or TO_PRIVATE nick (if defined)."
|
||||
if to_private is not None:
|
||||
self.say_private(to_private, text)
|
||||
else:
|
||||
self.say_public(text)
|
||||
|
||||
def ponder(self):
|
||||
"Return a random pondering."
|
||||
return random.choice(ponderings)
|
||||
|
||||
def exclaim(self):
|
||||
"Return a random exclamation string."
|
||||
return random.choice(exclamations)
|
||||
|
||||
def do_command(self, cmd, target=None):
|
||||
"""
|
||||
This is the function called whenever someone sends a public or
|
||||
private message addressed to the bot. (e.g. "bot: blah").
|
||||
"""
|
||||
|
||||
target = target.strip()
|
||||
|
||||
try:
|
||||
if cmd == 'help':
|
||||
self.reply(self.helptext, target)
|
||||
|
||||
elif cmd == 'lol':
|
||||
self.reply(self.ponder(), target)
|
||||
|
||||
elif cmd == 'urls' or cmd == 'list':
|
||||
db = self.get_session()
|
||||
for url in db.query(Url).order_by(Url.timestamp.desc())[:10]:
|
||||
line = "%s %s" % (url.url, url.title)
|
||||
self.reply(line, target)
|
||||
time.sleep(1)
|
||||
|
||||
elif cmd.startswith('urls ') or cmd.startswith('list '):
|
||||
db = self.get_session()
|
||||
(listcmd, n) = cmd.split(" ", 1)
|
||||
n = n.strip()
|
||||
if n == "all":
|
||||
rows = db.query(Url).order_by(Url.timestamp.desc())
|
||||
elif n.find("-") > 0:
|
||||
(x, y) = n.split("-", 1)
|
||||
try:
|
||||
x = abs(int(x))
|
||||
y = abs(int(y))
|
||||
if y < x:
|
||||
x, y = y, x
|
||||
except ValueError, ex:
|
||||
self.reply("Give me a number or a range of numbers, e.g. list 5 or list 11-20", target)
|
||||
raise ex
|
||||
rows = db.query(Url).order_by(Url.timestamp.desc())[x-1:y]
|
||||
else:
|
||||
try:
|
||||
n = abs(int(n))
|
||||
except ValueError, ex:
|
||||
self.reply("Give me a number or a range of numbers, e.g. list 5 or list 11-20", target)
|
||||
raise ex
|
||||
rows = db.query(Url).order_by(Url.timestamp.desc())[:n]
|
||||
|
||||
for url in rows:
|
||||
line = "%s %s" % (url.url, url.title)
|
||||
self.reply(line, target)
|
||||
time.sleep(1)
|
||||
|
||||
elif cmd.startswith('http:') or cmd.startswith('https:'):
|
||||
title = self.save_url(from_private, cmd)
|
||||
if title == False:
|
||||
self.say_public("Sorry, I'm useless at UTF-8.")
|
||||
else:
|
||||
self.reply('URL added. %s' % title, target)
|
||||
|
||||
else:
|
||||
self.reply(self.exclaim(), target)
|
||||
|
||||
except Exception, ex:
|
||||
print "Exception caught processing command: %s" % ex
|
||||
print " command was '%s' from %s" % (cmd, target)
|
||||
self.reply("Sorry, I didn't understand: %s" % cmd, target)
|
||||
self.reply(self.helptext, target)
|
||||
|
||||
class LolBotFactory(protocol.ClientFactory):
|
||||
protocol = LolBot
|
||||
|
||||
def __init__(self, config_path):
|
||||
self.config = get_config(config_path)
|
||||
self.server = self.config['irc.server']
|
||||
self.port = self.config['irc.port']
|
||||
self.nickname = self.config['irc.nickname']
|
||||
self.channel = self.config['irc.channel']
|
||||
|
||||
def clientConnectionLost(self, connector, reason):
|
||||
print "Lost connection (%s), reconnecting." % (reason,)
|
||||
connector.connect()
|
||||
|
||||
def clientConnectionFailed(self, connector, reason):
|
||||
print "Could not connect: %s" % (reason,)
|
||||
|
||||
def get_options():
|
||||
try:
|
||||
(options, args) = getopt.getopt(sys.argv[1:], "hc:", ["help", "config=", ])
|
||||
except getopt.GetoptError, err:
|
||||
print str(err)
|
||||
usage()
|
||||
sys.exit(2)
|
||||
|
||||
config_path = ""
|
||||
for option,value in options:
|
||||
if option in ("-h", "--help"):
|
||||
usage()
|
||||
sys.exit(2)
|
||||
if option in ("-c", "--config"):
|
||||
config_path = value
|
||||
return { 'config_path': config_path }
|
||||
|
||||
def get_config(config_path):
|
||||
"""
|
||||
This method loads configuration options from a lolbot.conf file. The file
|
||||
should look like this:
|
||||
|
||||
irc.server = irc.freenode.net
|
||||
irc.port = 6667
|
||||
irc.channel = #lolbottest
|
||||
irc.nickname = lolbot
|
||||
db.url = sqlite:///lolbot.db
|
||||
irc.server = irc.freenode.net
|
||||
irc.port = 6667
|
||||
irc.channel = #lolbottest
|
||||
irc.nickname = lolbot
|
||||
db.url = sqlite:///lolbot.db
|
||||
|
||||
"""
|
||||
|
||||
if not config_path:
|
||||
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lolbot.conf")
|
||||
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lolbot.conf")
|
||||
if not os.path.exists(config_path):
|
||||
print "Error: configuration file not found. By default lolbot will look for a lolbot.conf file in the same directory as the lolbot script, but you can override this by specifying a path on the command line with the --config option."
|
||||
usage()
|
||||
sys.exit(1)
|
||||
print "Error: configuration file not found. By default lolbot will look for a lolbot.conf file in the same directory as the lolbot script, but you can override this by specifying a path on the command line with the --config option."
|
||||
usage()
|
||||
sys.exit(1)
|
||||
|
||||
# open the configuration file and grab all param=value declarations.
|
||||
config = {}
|
||||
with open(config_path) as f:
|
||||
for line in f:
|
||||
# skip comments
|
||||
if line.strip().startswith("#") or line.strip() == "":
|
||||
continue
|
||||
for line in f:
|
||||
# skip comments
|
||||
if line.strip().startswith("#") or line.strip() == "":
|
||||
continue
|
||||
|
||||
# collect up param = value
|
||||
try:
|
||||
(param, value) = line.strip().split("=", 1)
|
||||
if param.strip() != "":
|
||||
config[param.strip()] = value.strip()
|
||||
except ValueError:
|
||||
continue
|
||||
# collect up param = value
|
||||
try:
|
||||
(param, value) = line.strip().split("=", 1)
|
||||
if param.strip() != "":
|
||||
config[param.strip()] = value.strip()
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
# validate IRC host
|
||||
if "irc.server" not in config.keys():
|
||||
print "Error: the IRC server was not specified. Use --help for more information."
|
||||
sys.exit(1)
|
||||
self.server = config["irc.server"]
|
||||
print "Error: the IRC server was not specified. Use --help for more information."
|
||||
sys.exit(1)
|
||||
|
||||
# validate IRC port
|
||||
if "irc.port" not in config.keys():
|
||||
config["irc.port"] = "6667"
|
||||
config["irc.port"] = "6667"
|
||||
try:
|
||||
self.port = int(config["irc.port"])
|
||||
config["irc.port"] = int(config["irc.port"])
|
||||
except ValueError:
|
||||
print "Error: the IRC port must be an integer. If not specified, lolbot will use the default IRC port value 6667. Use --help for more information."
|
||||
sys.exit(1)
|
||||
print "Error: the IRC port must be an integer. If not specified, lolbot will use the default IRC port value 6667. Use --help for more information."
|
||||
sys.exit(1)
|
||||
|
||||
# validate IRC channel
|
||||
if "irc.channel" not in config.keys() or not config["irc.channel"].startswith("#"):
|
||||
print "Error: the IRC channel is not specified or incorrect. It must begin with a # - e.g. #mychatchannel. Use --help for more information."
|
||||
sys.exit(1)
|
||||
self.channel = config["irc.channel"]
|
||||
print "Error: the IRC channel is not specified or incorrect. It must begin with a # - e.g. #mychatchannel. Use --help for more information."
|
||||
sys.exit(1)
|
||||
|
||||
# validate bot nickname
|
||||
if "irc.nickname" not in config.keys():
|
||||
config["irc.nickname"] = "lolbot"
|
||||
self.nickname = config["irc.nickname"]
|
||||
|
||||
self.config = config
|
||||
|
||||
def now(self):
|
||||
return datetime.today().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
def save_url(self, nickname, url):
|
||||
title = False
|
||||
try:
|
||||
db = self.get_session()
|
||||
if not db.query(Url).filter(Url.url == url).count():
|
||||
theurl = Url(nickname, url)
|
||||
db.add(theurl)
|
||||
db.commit()
|
||||
else:
|
||||
theurl = db.query(Url).filter(Url.url == url).one()
|
||||
print theurl
|
||||
title = theurl.title
|
||||
except Exception, ex:
|
||||
print "Exception caught saving URL: %s" % ex
|
||||
return title
|
||||
|
||||
def log_event(self, nick, text):
|
||||
try:
|
||||
entry = Log(nick, text)
|
||||
db = self.get_session()
|
||||
db.add(entry)
|
||||
db.commit()
|
||||
print entry
|
||||
except Exception, ex:
|
||||
print "Exception caught logging event: %s" % ex
|
||||
|
||||
def on_nicknameinuse(self, connection, event):
|
||||
self.nickname = connection.get_nickname() + "_"
|
||||
connection.nick(self.nickname)
|
||||
|
||||
def on_welcome(self, connection, event):
|
||||
connection.join(self.channel)
|
||||
|
||||
def on_privmsg(self, connection, event):
|
||||
"Deal with a /msg private message."
|
||||
from_nick = nm_to_n(event.source())
|
||||
self.do_command(event, event.arguments()[0], from_nick)
|
||||
|
||||
def on_pubmsg(self, connection, event):
|
||||
"Deal with a public message in a channel."
|
||||
|
||||
# log it
|
||||
from_nick = nm_to_n(event.source())
|
||||
self.log_event(from_nick, event.arguments()[0])
|
||||
|
||||
args = string.split(event.arguments()[0], ":", 1)
|
||||
if len(args) > 1 and irc_lower(args[0]) == irc_lower(self.nickname):
|
||||
self.do_command(event, string.strip(args[1]), from_nick)
|
||||
else:
|
||||
# parse it for links, add URLs to the list
|
||||
words = event.arguments()[0].split(" ")
|
||||
for w in words:
|
||||
if w.startswith('http://') or w.startswith('https://'):
|
||||
title = self.save_url(from_nick, w)
|
||||
if title == False:
|
||||
self.say_public("Sorry, I'm useless at UTF-8.")
|
||||
else:
|
||||
self.say_public("URL added. %s" % title)
|
||||
|
||||
def say_public(self, text):
|
||||
"Print TEXT into public channel, for all to see."
|
||||
self.queue.send(text, self.channel)
|
||||
self.log_event(self.nickname, text)
|
||||
|
||||
def say_private(self, nick, text):
|
||||
"Send private message of TEXT to NICK."
|
||||
self.queue.send(text, nick)
|
||||
|
||||
def reply(self, text, to_private=None):
|
||||
"Send TEXT to either public channel or TO_PRIVATE nick (if defined)."
|
||||
if to_private is not None:
|
||||
self.say_private(to_private, text)
|
||||
else:
|
||||
self.say_public(text)
|
||||
|
||||
def ponder(self):
|
||||
"Return a random pondering."
|
||||
return random.choice(ponderings)
|
||||
|
||||
def exclaim(self):
|
||||
"Return a random exclamation string."
|
||||
return random.choice(exclamations)
|
||||
|
||||
def do_command(self, event, cmd, from_private):
|
||||
"""
|
||||
This is the function called whenever someone sends a public or
|
||||
private message addressed to the bot. (e.g. "bot: blah").
|
||||
"""
|
||||
|
||||
if event.eventtype() == "pubmsg":
|
||||
target = None
|
||||
else:
|
||||
target = from_private.strip()
|
||||
|
||||
try:
|
||||
if cmd == 'help':
|
||||
self.reply(self.helptext, target)
|
||||
|
||||
elif cmd == 'lol':
|
||||
self.reply(self.ponder(), target)
|
||||
|
||||
elif cmd == 'urls' or cmd == 'list':
|
||||
db = self.get_session()
|
||||
for url in db.query(Url).order_by(Url.timestamp.desc())[:10]:
|
||||
line = "%s %s" % (url.url, url.title)
|
||||
self.reply(line, target)
|
||||
time.sleep(1)
|
||||
|
||||
elif cmd.startswith('urls ') or cmd.startswith('list '):
|
||||
db = self.get_session()
|
||||
(listcmd, n) = cmd.split(" ", 1)
|
||||
n = n.strip()
|
||||
if n == "all":
|
||||
rows = db.query(Url).order_by(Url.timestamp.desc())
|
||||
elif n.find("-") > 0:
|
||||
(x, y) = n.split("-", 1)
|
||||
try:
|
||||
x = abs(int(x))
|
||||
y = abs(int(y))
|
||||
if y < x:
|
||||
x, y = y, x
|
||||
except ValueError, ex:
|
||||
self.reply("Give me a number or a range of numbers, e.g. list 5 or list 11-20", target)
|
||||
raise ex
|
||||
rows = db.query(Url).order_by(Url.timestamp.desc())[x-1:y]
|
||||
else:
|
||||
try:
|
||||
n = abs(int(n))
|
||||
except ValueError, ex:
|
||||
self.reply("Give me a number or a range of numbers, e.g. list 5 or list 11-20", target)
|
||||
raise ex
|
||||
rows = db.query(Url).order_by(Url.timestamp.desc())[:n]
|
||||
|
||||
for url in rows:
|
||||
line = "%s %s" % (url.url, url.title)
|
||||
self.reply(line, target)
|
||||
time.sleep(1)
|
||||
|
||||
elif cmd.startswith('http:') or cmd.startswith('https:'):
|
||||
title = self.save_url(from_private, cmd)
|
||||
if title == False:
|
||||
self.say_public("Sorry, I'm useless at UTF-8.")
|
||||
else:
|
||||
self.reply('URL added. %s' % title, target)
|
||||
|
||||
else:
|
||||
self.reply(self.exclaim(), target)
|
||||
|
||||
except Exception, ex:
|
||||
print "Exception caught processing command: %s" % ex
|
||||
print " command was '%s' from %s" % (cmd, target)
|
||||
self.reply("Sorry, I didn't understand: %s" % cmd, target)
|
||||
self.reply(self.helptext, target)
|
||||
config["irc.nickname"] = "lolbot"
|
||||
|
||||
return config
|
||||
|
||||
def usage():
|
||||
print """Run a lolbot.
|
||||
print """Run a lolbot.
|
||||
|
||||
-h, --help
|
||||
This message.
|
||||
|
|
@ -382,23 +414,11 @@ Configuration:
|
|||
"""
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
(options, args) = getopt.getopt(sys.argv[1:], "hc:", ["help", "config=", ])
|
||||
except getopt.GetoptError, err:
|
||||
print str(err)
|
||||
usage()
|
||||
sys.exit(2)
|
||||
|
||||
config_path = ""
|
||||
for option,value in options:
|
||||
if option in ("-h", "--help"):
|
||||
usage()
|
||||
sys.exit(2)
|
||||
if option in ("-c", "--config"):
|
||||
config_path = value
|
||||
|
||||
try:
|
||||
LolBot(config_path).start()
|
||||
except KeyboardInterrupt:
|
||||
print "Shutting down."
|
||||
args = get_options()
|
||||
config = get_config(args['config_path'])
|
||||
try:
|
||||
reactor.connectTCP(config['irc.server'], config['irc.port'], LolBotFactory(args['config_path']))
|
||||
reactor.run()
|
||||
except KeyboardInterrupt:
|
||||
print "Shutting down."
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue