Compare commits

...
Sign in to create a new pull request.

9 commits

Author SHA1 Message Date
Jonatan Pålsson
529bcbb003 Add commands support 2017-06-12 21:47:14 +02:00
Jonatan Pålsson
bf661d9577 Fix nick treates as channel issue
Since nickname changes apply server-wide, they were incorrectly treated
as their own channels (this was a bug) - fixed this, and nickname
changes are now logged in all channels the bot has joined.
2017-06-10 12:23:07 +02:00
Viktor Sjölind
96e6da2752 Refactor to run in both Python 2.7 and Python 3.6
Refactor to run in both Python 2.7 and Python 3.6. This is important as
python 2.7 is end of life.

I tried to edit as few things as possible in this commit to minimize
conflicts which means the code could be prettier.

Signed-off-by: Viktor Sjölind <viktor@sjolind.se>
2017-06-10 11:59:11 +02:00
Jonatan Pålsson
2125b2d739 Make flask thread a daemon
This means the flask thread will also exit when the main thread receives
a KeyboardInterrupt. This closes #12.
2017-06-10 11:24:21 +02:00
Jonatan Pålsson
924fad4320 Use ',' rather than 'and' in .select() queries
This closes #11
2017-06-10 11:22:54 +02:00
Jonatan Pålsson
6292af08e6 Add environment variables for config 2017-06-10 10:57:41 +02:00
Richard Pannek
0a6a0c4e6f Add db and seen_prs to gitignore 2017-06-09 16:06:55 +02:00
Richard Pannek
233519ac3a Merge pull request #9 from rpannek/readme
Change project name in README
2017-06-09 12:58:15 +02:00
Richard Pannek
63a2a7af31 Change project name in README 2017-06-09 12:56:48 +02:00
8 changed files with 178 additions and 99 deletions

3
.gitignore vendored
View file

@ -1 +1,4 @@
*.pyc
irc.db
seen_prs

15
Db.py
View file

@ -23,11 +23,16 @@ class LogMessage(BaseModel):
message_type = CharField()
message = CharField()
class Quote(BaseModel):
datetime = DateTimeField()
author = CharField()
message = CharField()
def create_tables():
database.connect()
try:
database.create_tables([LogMessage, Day, Channel])
database.create_tables([Quote, LogMessage, Day, Channel])
except OperationalError:
pass # Database already exists
@ -55,6 +60,12 @@ def add_log_message(channel, nickname, message_type, message = None):
message_type = message_type,
message = message)
def add_quote(author, message):
Quote.create(
author = author,
message = message,
datetime = datetime.datetime.now().strftime("%H:%m:%S"))
def show_all_messages():
for message in LogMessage.select():
print "<%s> %s" % (message.nickname, message.message)
print("<%s> %s" % (message.nickname, message.message))

View file

@ -1,21 +1,32 @@
LogBot 0.4.2
============
# smooth-operator
Written by Chris Oliver <chris@excid3.com>
smooth-operator is a IRC bot which logs everything in a channel and offers more convinient things like notifications about new commits on GitHub, etc. For a roadmap please check the issues on GitHub.
Many thanks to Filip Slagter for his contributions.
Originally this was written by Chris Oliver <chris@excid3.com> with contributions from Filip Slagter. Now it has diverged quite a lot.
Requirements
------------
LogBot shows logs using flask, and stores logs using peewee. Install these dependencies using ``pip``:
## Requirements
smooth-operator shows logs using flask, and stores logs using peewee. Install these dependencies using ``pip``:
pip install flask peewee
Usage
-----
LogBot requires Python 2. It is NOT compatible with Python 3.
Configuration is done inside logbot.py.
## Usage
python logbot.py
smooth-operator requires Python 2. It is NOT compatible with Python 3. Configuration is either done inside logbot.py, or using environment variables. The following environment variables are respected:
You can view logs on http://localhost:5000
- ``IRC_SERVER``: IRC server
- ``IRC_PORT``: IRC server port
- ``IRC_SERVER_PASS``: Password for IRC server, if any
- ``IRC_CHANNELS``: IRC channels to join, separated by ``,``
- ``IRC_NICK``: Nickname
- ``IRC_NICK_PASS``: Password to use when authenticating to nickserv, if any
The bot can be launched using:
python2 logbot.py
You can view the logs on http://localhost:5000
## License
This project is licensed under the GPLv2.

44
commands.py Normal file
View file

@ -0,0 +1,44 @@
from Db import *
from irclib import nm_to_n
import random
class Commands:
def __init__(self):
self.commands = {
"quote": self.cmd_quote,
"remember_quote": self.cmd_remember_quote
}
def process(self, c, e):
msg = e.arguments()[0]
if msg.startswith("!"):
cmd = msg.split("!")[1].split(" ")[0]
if cmd in self.commands:
msg = " ".join(msg.split(" ")[1:])
self.commands[cmd](c, msg, e.target(), nm_to_n(e.source()))
else:
print "Unknown command", cmd
def cmd_quote(self, c, msg, target, source):
replies = [
"%s once said \"%s\"",
"I head from %s that \"%s\"",
"A wise man (haha, just kidding, it was actually %s) once said \"%s\""
]
reply = lambda msg: c.privmsg(target, msg)
random_query = Quote.select().order_by(fn.Random())
try:
one_quote = random_query.get()
reply(random.choice(replies) % (one_quote.author, one_quote.message))
except: # No quotes
reply("I don't know ay quotes :(")
def cmd_remember_quote(self, c, msg, target, source):
reply = lambda msg: c.privmsg(target, msg)
if (len(msg) > 1):
add_quote(msg.split(" ")[0], ' '.join(msg.split(" ")[1:]))
reply("I'll try to remember that!")
else:
reply("I didn't get that :(")

View file

@ -25,7 +25,14 @@ write simpler bots.
"""
import sys
# UserDict is moved to collections in Python3
# In order to support Python 2.7 this has to be imported
# in the following way
try:
from UserDict import UserDict
except ImportError:
from collections import UserDict
from irclib import SimpleIRCClient
from irclib import nm_to_n, irc_lower, all_events
@ -160,7 +167,7 @@ class SingleServerIRCBot(SimpleIRCClient):
"""[Internal]"""
before = nm_to_n(e.source())
after = e.target()
for ch in self.channels.values():
for ch in list(self.channels.values()):
if ch.has_user(before):
ch.change_nick(before, after)
@ -177,7 +184,7 @@ class SingleServerIRCBot(SimpleIRCClient):
def _on_quit(self, c, e):
"""[Internal]"""
nick = nm_to_n(e.source())
for ch in self.channels.values():
for ch in list(self.channels.values()):
if ch.has_user(nick):
ch.remove_user(nick)
@ -283,8 +290,6 @@ class IRCDict:
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()
@ -294,15 +299,15 @@ class IRCDict:
import copy
return copy.copy(self)
def keys(self):
return self.data.keys()
return list(self.data.keys())
def items(self):
return self.data.items()
return list(self.data.items())
def values(self):
return self.data.values()
return list(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():
for k, v in list(dict.items()):
self.data[k] = v
def get(self, key, failobj=None):
return self.data.get(key, failobj)
@ -322,16 +327,16 @@ class Channel:
def users(self):
"""Returns an unsorted list of the channel's users."""
return self.userdict.keys()
return list(self.userdict.keys())
def opers(self):
"""Returns an unsorted list of the channel's operators."""
return self.operdict.keys()
return list(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()
return list(self.voiceddict.keys())
def has_user(self, nick):
"""Check whether the channel has a user."""

View file

@ -207,8 +207,8 @@ class IRC:
incoming data, if there are any. If that seems boring, look
at the process_forever method.
"""
sockets = map(lambda x: x._get_socket(), self.connections)
sockets = filter(lambda x: x != None, sockets)
sockets = [x._get_socket() for x in self.connections]
sockets = [x for x in sockets if x != None]
if sockets:
(i, o, e) = select.select(sockets, [], [], timeout)
self.process_data(i)
@ -342,7 +342,7 @@ class Connection:
self.irclibobj = irclibobj
def _get_socket():
raise IRCError, "Not overridden"
raise IRCError("Not overridden")
##############################
### Convenience wrappers.
@ -433,10 +433,10 @@ class ServerConnection(Connection):
self.socket.connect((self.server, self.port))
if ssl:
self.ssl = socket.ssl(self.socket)
except socket.error, x:
except socket.error as x:
self.socket.close()
self.socket = None
raise ServerConnectionError, "Couldn't connect to socket: %s" % x
raise ServerConnectionError("Couldn't connect to socket: %s" % x)
self.connected = 1
if self.irclibobj.fn_to_add_socket:
self.irclibobj.fn_to_add_socket(self.socket)
@ -491,7 +491,7 @@ class ServerConnection(Connection):
new_data = self.ssl.read(2**14)
else:
new_data = self.socket.recv(2**14)
except socket.error, x:
except socket.error as x:
# The server hung up.
self.disconnect("Connection reset by peer")
return
@ -500,14 +500,14 @@ class ServerConnection(Connection):
self.disconnect("Connection reset by peer")
return
lines = _linesep_regexp.split(self.previous_buffer + new_data)
lines = _linesep_regexp.split(self.previous_buffer + new_data.decode())
# Save the last, unfinished line.
self.previous_buffer = lines.pop()
for line in lines:
if DEBUG:
print "FROM SERVER:", line
print("FROM SERVER:", line)
if not line:
continue
@ -561,7 +561,7 @@ class ServerConnection(Connection):
command = "privnotice"
for m in messages:
if type(m) is types.TupleType:
if type(m) is tuple:
if command in ["privmsg", "pubmsg"]:
command = "ctcp"
else:
@ -569,15 +569,15 @@ class ServerConnection(Connection):
m = list(m)
if DEBUG:
print "command: %s, source: %s, target: %s, arguments: %s" % (
command, prefix, target, m)
print("command: %s, source: %s, target: %s, arguments: %s" % (
command, prefix, target, m))
self._handle_event(Event(command, prefix, target, m))
if command == "ctcp" and m[0] == "ACTION":
self._handle_event(Event("action", prefix, target, m[1:]))
else:
if DEBUG:
print "command: %s, source: %s, target: %s, arguments: %s" % (
command, prefix, target, [m])
print("command: %s, source: %s, target: %s, arguments: %s" % (
command, prefix, target, [m]))
self._handle_event(Event(command, prefix, target, [m]))
else:
target = None
@ -595,8 +595,8 @@ class ServerConnection(Connection):
command = "umode"
if DEBUG:
print "command: %s, source: %s, target: %s, arguments: %s" % (
command, prefix, target, arguments)
print("command: %s, source: %s, target: %s, arguments: %s" % (
command, prefix, target, arguments))
self._handle_event(Event(command, prefix, target, arguments))
def _handle_event(self, event):
@ -660,7 +660,7 @@ class ServerConnection(Connection):
try:
self.socket.close()
except socket.error, x:
except socket.error as x:
pass
self.socket = None
self._handle_event(Event("disconnect", self.server, "", [message]))
@ -743,7 +743,7 @@ class ServerConnection(Connection):
def part(self, channels, message=""):
"""Send a PART command."""
if type(channels) == types.StringType:
if type(channels) == bytes:
self.send_raw("PART " + channels + (message and (" " + message)))
else:
self.send_raw("PART " + ",".join(channels) + (message and (" " + message)))
@ -782,15 +782,16 @@ class ServerConnection(Connection):
The string will be padded with appropriate CR LF.
"""
if self.socket is None:
raise ServerNotConnectedError, "Not connected."
raise ServerNotConnectedError("Not connected.")
try:
string += "\r\n"
if self.ssl:
self.ssl.write(string + "\r\n")
self.ssl.write(string.encode())
else:
self.socket.send(string + "\r\n")
self.socket.send(string.encode())
if DEBUG:
print "TO SERVER:", string
except socket.error, x:
print("TO SERVER:", string)
except socket.error as x:
# Ouch!
self.disconnect("Connection reset by peer.")
@ -888,8 +889,8 @@ class DCCConnection(Connection):
self.passive = 0
try:
self.socket.connect((self.peeraddress, self.peerport))
except socket.error, x:
raise DCCConnectionError, "Couldn't connect to socket: %s" % x
except socket.error as x:
raise DCCConnectionError("Couldn't connect to socket: %s" % x)
self.connected = 1
if self.irclibobj.fn_to_add_socket:
self.irclibobj.fn_to_add_socket(self.socket)
@ -913,8 +914,8 @@ class DCCConnection(Connection):
self.socket.bind((socket.gethostbyname(socket.gethostname()), 0))
self.localaddress, self.localport = self.socket.getsockname()
self.socket.listen(10)
except socket.error, x:
raise DCCConnectionError, "Couldn't bind socket: %s" % x
except socket.error as x:
raise DCCConnectionError("Couldn't bind socket: %s" % x)
return self
def disconnect(self, message=""):
@ -930,7 +931,7 @@ class DCCConnection(Connection):
self.connected = 0
try:
self.socket.close()
except socket.error, x:
except socket.error as x:
pass
self.socket = None
self.irclibobj._handle_event(
@ -947,8 +948,8 @@ class DCCConnection(Connection):
self.socket = conn
self.connected = 1
if DEBUG:
print "DCC connection from %s:%d" % (
self.peeraddress, self.peerport)
print("DCC connection from %s:%d" % (
self.peeraddress, self.peerport))
self.irclibobj._handle_event(
self,
Event("dcc_connect", self.peeraddress, None, None))
@ -956,7 +957,7 @@ class DCCConnection(Connection):
try:
new_data = self.socket.recv(2**14)
except socket.error, x:
except socket.error as x:
# The server hung up.
self.disconnect("Connection reset by peer")
return
@ -985,11 +986,11 @@ class DCCConnection(Connection):
target = None
for chunk in chunks:
if DEBUG:
print "FROM PEER:", chunk
print("FROM PEER:", chunk)
arguments = [chunk]
if DEBUG:
print "command: %s, source: %s, target: %s, arguments: %s" % (
command, prefix, target, arguments)
print("command: %s, source: %s, target: %s, arguments: %s" % (
command, prefix, target, arguments))
self.irclibobj._handle_event(
self,
Event(command, prefix, target, arguments))
@ -1009,8 +1010,8 @@ class DCCConnection(Connection):
if self.dcctype == "chat":
self.socket.send("\n")
if DEBUG:
print "TO PEER: %s\n" % string
except socket.error, x:
print("TO PEER: %s\n" % string)
except socket.error as x:
# Ouch!
self.disconnect("Connection reset by peer.")
@ -1181,10 +1182,6 @@ def mask_matches(nick, mask):
r = re.compile(mask, re.IGNORECASE)
return r.match(nick)
_special = "-[]\\`^{}"
nick_characters = string.ascii_letters + string.digits + _special
_ircstring_translation = string.maketrans(string.ascii_uppercase + "[]\\^",
string.ascii_lowercase + "{}|~")
def irc_lower(s):
"""Returns a lowercased string.
@ -1192,7 +1189,12 @@ def irc_lower(s):
The definition of lowercased comes from the IRC specification (RFC
1459).
"""
return s.translate(_ircstring_translation)
s = s.lower()
s = s.replace("[", "{")
s = s.replace("]", "}")
s = s.replace("\\", "|")
s = s.replace("^", "~")
return s
def _ctcp_dequote(message):
"""[Internal] Dequote a message according to CTCP specifications.
@ -1259,16 +1261,16 @@ def ip_numstr_to_quad(num):
"""Convert an IP number as an integer given in ASCII
representation (e.g. '3232235521') to an IP address string
(e.g. '192.168.0.1')."""
n = long(num)
p = map(str, map(int, [n >> 24 & 0xFF, n >> 16 & 0xFF,
n >> 8 & 0xFF, n & 0xFF]))
n = int(num)
p = list(map(str, list(map(int, [n >> 24 & 0xFF, n >> 16 & 0xFF,
n >> 8 & 0xFF, n & 0xFF]))))
return ".".join(p)
def ip_quad_to_numstr(quad):
"""Convert an IP address string (e.g. '192.168.0.1') to an IP
number as an integer given in ASCII representation
(e.g. '3232235521')."""
p = map(long, quad.split("."))
p = list(map(int, quad.split(".")))
s = str((p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3])
if s[-1] == "L":
s = s[:-1]
@ -1557,4 +1559,4 @@ protocol_events = [
"pong",
]
all_events = generated_events + protocol_events + numeric_events.values()
all_events = generated_events + protocol_events + list(numeric_events.values())

View file

@ -59,6 +59,7 @@ from pullrequest import PullRequest
from Db import *
from flask import *
import threading
from commands import Commands
pat1 = re.compile(r"(^|[\n ])(([\w]+?://[\w\#$%&~.\-;:=,?@\[\]+]*)(/[\w\#$%&~/.\-;:=,?@\[\]+]*)?)", re.IGNORECASE | re.DOTALL)
@ -79,12 +80,12 @@ def urlify2(value):
DEBUG = False
# IRC Server Configuration
SERVER = "irc.freenode.net"
PORT = 6667
SERVER_PASS = None
CHANNELS=["#pelux"]
NICK = "pelux"
NICK_PASS = ""
SERVER = os.getenv("IRC_SERVER", "irc.freenode.net")
PORT = os.getenv("IRC_PORT", 6667)
SERVER_PASS = os.getenv("IRC_SERVER_PASS", None)
CHANNELS = os.getenv("IRC_CHANNELS", "#pelux").split(",")
NICK = os.getenv("IRC_NICK", "pelux")
NICK_PASS = os.getenv("IRC_NICK_PASS", "")
# The local folder to save logs
LOG_FOLDER = "/var/www/html/"
@ -115,13 +116,13 @@ def search(channel = None, nickname = None):
try:
channel = Channel.get(Channel.name == channel)
messages = LogMessage.select() \
.where(LogMessage.channel == channel and \
.where(LogMessage.channel == channel, \
LogMessage.message.contains(query))
except:
pass # No such channel
elif nickname:
messages = LogMessage.select() \
.where(LogMessage.nickname == nickname and \
.where(LogMessage.nickname == nickname, \
LogMessage.message.contains(query))
else:
messages = LogMessage.select() \
@ -135,13 +136,13 @@ def search(channel = None, nickname = None):
@flaskapp.route("/channels/<channel>/")
@flaskapp.route("/channels/<channel>/<day>/")
def channel(channel, day = None, query = None):
def channel(channel, day = None):
channel = Channel.get(Channel.name == channel)
if day:
d = Day.get(Day.date == day)
messages = LogMessage.select() \
.where(LogMessage.day == d and \
.where(LogMessage.day == d, \
LogMessage.channel == channel)
return render_template("messages.html",
@ -164,18 +165,17 @@ def channels():
def append_line(filename, line):
data = open(filename, "rb").readlines()[:-2]
data += [line, "\n", "\n</body>", "\n</html>"]
data += [line.encode(), "\n".encode(), "\n</body>".encode(), "\n</html>".encode()]
write_lines(filename, data)
def write_lines(filename, lines):
f = open(filename, "wb")
with open(filename, "wb") as f:
f.writelines(lines)
f.close()
def write_string(filename, string):
f = open(filename, "wb")
f.write(string)
f.close()
with open(filename, "wb") as f:
f.write(string.encode())
color_pattern = re.compile(r'(\[\d{1,2}m)')
"Pattern that matches ANSI color codes and the text that follows"
@ -204,16 +204,17 @@ class Logbot(SingleServerIRCBot):
self.chans = [x.lower() for x in channels]
self.set_ftp()
self.nick_pass = nick_pass
self.commands = Commands()
print "Logbot %s" % __version__
print "Connecting to %s:%i..." % (server, port)
print "Press Ctrl-C to quit"
print("Logbot %s" % __version__)
print("Connecting to %s:%i..." % (server, port))
print("Press Ctrl-C to quit")
def quit(self):
self.connection.disconnect("Quitting...")
def color(self, user):
return "#%s" % md5(user).hexdigest()[:6]
return "#%s" % md5(user.encode()).hexdigest()[:6]
def set_ftp(self, ftp=None):
self.ftp = ftp
@ -223,6 +224,7 @@ class Logbot(SingleServerIRCBot):
if event_name == "nick":
message = params["new"]
target = params["chan"]
elif event_name == "kick":
message = "%s kicked %s from %s. Reason: %s" % (nm_to_n(params["kicker"]),
params["user"], params["channel"], params["reason"])
@ -257,7 +259,7 @@ class Logbot(SingleServerIRCBot):
def on_all_raw_messages(self, c, e):
"""Display all IRC connections in terminal"""
if DEBUG: print e.arguments()[0]
if DEBUG: print(e.arguments()[0])
def on_welcome(self, c, e):
"""Join channels after successful connection"""
@ -320,6 +322,7 @@ class Logbot(SingleServerIRCBot):
def on_pubmsg(self, c, e):
# if e.arguments()[0].startswith(NICK):
# c.privmsg(e.target(), self.format["help"])
self.commands.process(c, e)
self.write_event("pubmsg", e)
def on_pubnotice(self, c, e):
@ -327,7 +330,7 @@ class Logbot(SingleServerIRCBot):
def on_privmsg(self, c, e):
# c.privmsg(nm_to_n(e.source()), self.format["help"])
pas
pass
def on_quit(self, c, e):
nick = nm_to_n(e.source())
@ -340,7 +343,7 @@ class Logbot(SingleServerIRCBot):
self.write_event("topic", e)
def connect_ftp():
print "Using FTP %s..." % (FTP_SERVER)
print("Using FTP %s..." % (FTP_SERVER))
f = ftplib.FTP(FTP_SERVER, FTP_USER, FTP_PASS)
f.cwd(FTP_FOLDER)
return f
@ -350,6 +353,7 @@ def main():
create_tables()
t = threading.Thread(target=flaskapp.run, kwargs={"host": "0.0.0.0"})
t.daemon = True
t.start()
# Create the logs directory
@ -368,7 +372,6 @@ def main():
except KeyboardInterrupt:
if FTP_SERVER: bot.ftp.quit()
bot.quit()
t.join()
if __name__ == "__main__":

View file

@ -69,7 +69,7 @@ class PullRequest:
for repo in self.repos:
r = requests.get(repo["uri"])
if r.status_code != 200:
print "Error fetching %s", repo["name"]
print("Error fetching %s", repo["name"])
break
prs = r.json()
@ -83,4 +83,4 @@ class PullRequest:
if __name__ == "__main__":
p = PullRequest()
for line in p.check_all():
print line["message"]
print(line["message"])