added logging

This commit is contained in:
iou1name 2018-05-27 21:45:17 -04:00
parent 601bf4de02
commit ea4e7a6653
3 changed files with 251 additions and 16 deletions

7
.gitignore vendored
View File

@ -2,9 +2,10 @@ __pycache__/
*/__pycache__/ */__pycache__/
logs/ logs/
*.cfg *.cfg
*.db
*.pid
*.dat *.dat
*.txt *.db
*.log
*.pid
*.swp *.swp
*.txt
tourettes.py tourettes.py

211
bot.py
View File

@ -8,6 +8,7 @@ import time
import functools import functools
import threading import threading
import traceback import traceback
from datetime import datetime
from twisted.internet import protocol from twisted.internet import protocol
from twisted.words.protocols import irc from twisted.words.protocols import irc
@ -32,12 +33,29 @@ class Fulvia(irc.IRCClient):
self.username = config.core.username self.username = config.core.username
"""The bot's username ident used for logging into the server.""" """The bot's username ident used for logging into the server."""
self.host = ""
"""The bot's host, virtual or otherwise. To be filled in later."""
self.prefix = config.core.prefix self.prefix = config.core.prefix
"""The command prefix the bot watches for.""" """The command prefix the bot watches for."""
self.static = os.path.join(config.homedir, "static") self.static = os.path.join(config.homedir, "static")
os.makedirs(self.static, exist_ok=True)
"""The path to the bot's static file directory.""" """The path to the bot's static file directory."""
self.log_path = os.path.join(config.homedir, "logs")
os.makedirs(self.static, exist_ok=True)
"""The path to the bot's log files."""
self._log_dump = tools.FulviaMemoryDefault(list)
"""
Internal thread-safe dictionary containing log dumps yet to be
written.
"""
self._last_log_dump = 0
"""The last time logs were dumped."""
self.channels = tools.FulviaMemory() self.channels = tools.FulviaMemory()
""" """
A dictionary of all channels the bot is currently in and the users A dictionary of all channels the bot is currently in and the users
@ -195,6 +213,30 @@ class Fulvia(irc.IRCClient):
traceback.print_exc() traceback.print_exc()
def log(self, channel, text):
"""
Logs text to file. Logging structure is
{self.log_path}/{self.server}/{channel}/{date}.log
Only permits log dumping once per second.
"""
# TODO: use time module instead of datetime
t = datetime.fromtimestamp(time.time())
timestamp = t.strftime(self.config.core.default_time_format)
self._log_dump[channel].append(timestamp + " " + text)
if time.time() - self._last_log_dump > 1:
for ch in self._log_dump.keys():
fname = t.strftime("%Y-%m-%d") + ".log"
path = os.path.join(self.log_path, self.hostname, ch)
os.makedirs(path, exist_ok=True)
with open(os.path.join(path, fname), "a+") as file:
file.write("\n".join(self._log_dump[ch]) + "\n")
del(self._log_dump)
self._log_dump = tools.FulviaMemoryDefault(list)
## Actions involving the bot directly. ## Actions involving the bot directly.
## These will get called automatically. ## These will get called automatically.
@ -203,6 +245,15 @@ class Fulvia(irc.IRCClient):
Called when the bot receives a PRIVMSG, which can come from channels Called when the bot receives a PRIVMSG, which can come from channels
or users alike. or users alike.
""" """
nick = user.partition("!")[0].partition("@")[0]
if channel.startswith("#"):
opSym = tools.getOpSym(self.channels[channel].privileges[nick])
else:
opSym = ""
channel = nick
line = "<" + opSym + nick + ">" + " " + message
self.log(channel, line)
func_names = [] func_names = []
if message.startswith(self.prefix) and message != self.prefix: if message.startswith(self.prefix) and message != self.prefix:
command, _, _ = message.partition(" ") command, _, _ = message.partition(" ")
@ -246,19 +297,44 @@ class Fulvia(irc.IRCClient):
def joined(self, channel): def joined(self, channel):
"""Called when the bot joins a new channel.""" """Called when the bot joins a new channel."""
line = "-!- " + self.nickname + " " + "[" + self.username + "@"
line += self.host + "] has joined " + channel
self.log(channel, line)
print(f"Joined {channel}") print(f"Joined {channel}")
if channel not in self.channels: if channel not in self.channels:
self.channels[channel] = tools.Channel(channel) self.channels[channel] = tools.Channel(channel)
def left(self, channel): def left(self, channel, reason=""):
"""Called when the leaves a channel.""" """Called when the leaves a channel."""
line = "-!- " + self.nickname + " " + "[" + self.username + "@"
line += self.host + "] has left " + channel + " [" + reason + "]"
self.log(channel, line)
print(f"Parted {channel}") print(f"Parted {channel}")
self.channels.pop(channel) self.channels.pop(channel)
def noticed(self, user, channel, message):
"""Called when the bot receives a NOTICE from a user or channel."""
# TODO: something?
pass
def modeChanged(self, user, channel, add_mode, modes, args): def modeChanged(self, user, channel, add_mode, modes, args):
"""Called when users or channel's modes are changed.""" """Called when users or channel's modes are changed."""
line = "-!- mode/" + channel + " ["
if add_mode:
line += "+"
else:
line += "-"
line += modes
if any(args):
line += " " + " ".join(args)
line += "] by " + user.partition("!")[0].partition("@")[0]
self.log(channel, line)
if not channel.startswith("#"): # server level if not channel.startswith("#"): # server level
return return
@ -284,12 +360,22 @@ class Fulvia(irc.IRCClient):
def signedOn(self): def signedOn(self):
"""Called when the bot successfully connects to the server.""" """Called when the bot successfully connects to the server."""
print(f"Signed on as {self.nickname}") print(f"Signed on as {self.nickname}")
self.whois(self.nickname)
line = "*** Signed onto " + self.hostname + " as "
line += self.nickname + "!" + self.username + "@" + self.host
self.log(self.hostname, line)
for channel in self.config.core.channels.split(","): for channel in self.config.core.channels.split(","):
self.join(channel) self.join(channel)
def kickedFrom(self, channel, kicker, message): def kickedFrom(self, channel, kicker, message):
"""Called when the bot is kicked from a channel.""" """Called when the bot is kicked from a channel."""
line = "-!- " + self.nickname + " was kicked from " + channel
line += " by " + kicker + " [" + message + "]"
self.log(channel, line)
self.channels.pop(channel) self.channels.pop(channel)
@ -298,43 +384,69 @@ class Fulvia(irc.IRCClient):
def userJoined(self, user, channel): def userJoined(self, user, channel):
"""Called when the bot sees another user join a channel.""" """Called when the bot sees another user join a channel."""
if user not in self.users: nick, _, host = user.partition("!")
self.users[user] = tools.User(user)
self.channels[channel].add_user(self.users[user]) line = "-!- " + nick + " " + "[" + host + "] has joined " + channel
self.log(channel, line)
if nick not in self.users:
self.users[nick] = tools.User(nick)
self.channels[channel].add_user(self.users[nick])
def userLeft(self, user, channel): def userLeft(self, user, channel, reason=""):
"""Called when the bot sees another user join a channel.""" """Called when the bot sees another user leave a channel."""
self.channels[channel].remove_user(user) nick, _, host = user.partition("!")
line = "-!- " + nick + " " + "[" + host + "] has left "
line += channel + " [" + reason + "]"
self.log(channel, line)
self.channels[channel].remove_user(nick)
def userQuit(self, user, quitMessage): def userQuit(self, user, quitMessage):
"""Called when the bot sees another user disconnect from the network.""" """Called when the bot sees another user disconnect from the network."""
channels = list(self.users[user].channels.keys()) nick, _, host = user.partition("!")
line = "-!- " + nick + " [" + host + "] has quit [" + quitMessage + "]"
channels = list(self.users[nick].channels.keys())
for channel in channels: for channel in channels:
# self.users[user].channels[channel].remove_user self.log(channel, line)
# channel.remove_user(user)
self.channels[channel].remove_user(user) self.channels[channel].remove_user(nick)
self.users.pop(user) self.users.pop(nick)
def userKicked(self, kickee, channel, kicker, message): def userKicked(self, kickee, channel, kicker, message):
""" """
Called when the bot sees another user getting kicked from the channel. Called when the bot sees another user getting kicked from the channel.
""" """
line = "-!- " + kickee + " was kicked from " + channel
line += " by " + kicker + " [" + message + "]"
self.log(channel, line)
self.channels[channel].remove_user(kickee) self.channels[channel].remove_user(kickee)
def topicUpdated(self, user, channel, newTopic): def topicUpdated(self, user, channel, newTopic):
"""Called when the bot sees a user update the channel's topic.""" """Called when the bot sees a user update the channel's topic."""
line = "-!- " + user + " changed the topic of " + channel + " to: "
line += newTopic
self.log(channel, line)
self.channels[channel].topic = newTopic self.channels[channel].topic = newTopic
def userRenamed(self, oldname, newname): def userRenamed(self, oldname, newname):
"""Called wehn the bot sees a user change their nickname.""" """Called wehn the bot sees a user change their nickname."""
line = "-!- " + oldname + " is now known as " + newname
user = self.users.pop(oldname) user = self.users.pop(oldname)
self.users[newname] = user self.users[newname] = user
for key, channel in user.channels.items(): for key, channel in user.channels.items():
self.log(key, line)
channel.rename_user(oldname, newname) channel.rename_user(oldname, newname)
user.nick = newname user.nick = newname
@ -354,8 +466,36 @@ class Fulvia(irc.IRCClient):
self.channels[channel].privileges[nick] = op_level self.channels[channel].privileges[nick] = op_level
def whoisUser(self, nick, ident, host, realname):
"""
Called when the bot receives a WHOIS about a particular user.
"""
if nick == self.nickname:
self.username = ident
self.host = host
self.realname = realname
## User commands, from client->server ## User commands, from client->server
def msg(self, user, message, length=None):
"""
Sends a message 'message' to 'user' (can be a user or a channel).
If 'length' is specified, the message will be split into lengths
of that size. If 'length' is not specified, the bot will determine
an appropriate length automatically.
"""
if user.startswith("#"):
priv = self.channels[user].privileges[self.nickname]
opSym = tools.getOpSym(priv)
else:
opSym = ""
line = "<" + opSym + self.nickname + ">" + " " + message
self.log(user, line)
irc.IRCClient.msg(self, user, message, length=None)
def reply(self, text, dest, reply_to, notice=False): def reply(self, text, dest, reply_to, notice=False):
""" """
Sends a message to 'dest' prefixed with 'reply_to' and a colon, Sends a message to 'dest' prefixed with 'reply_to' and a colon,
@ -390,6 +530,53 @@ class Fulvia(irc.IRCClient):
self.namesReceived(channel, channel_type, nicklist) self.namesReceived(channel, channel_type, nicklist)
def irc_JOIN(self, prefix, params):
"""
Called when a user joins a channel.
"""
nick = prefix.split('!')[0]
channel = params[-1]
if nick == self.nickname:
self.joined(channel)
else:
self.userJoined(prefix, channel)
def irc_PART(self, prefix, params):
"""
Called when a user leaves a channel.
"""
nick = prefix.split('!')[0]
channel = params[0]
if len(params) > 1:
reason = params[1]
else:
reason = ""
if nick == self.nickname:
self.left(channel, reason)
else:
self.userLeft(prefix, channel, reason)
def irc_QUIT(self, prefix, params):
"""
Called when a user has quit.
"""
nick = prefix.split('!')[0]
self.userQuit(prefix, params[0])
def irc_RPL_WHOISUSER(self, prefix, params):
"""
Called when we receive the server's response from our "WHOIS [user]"
query.
"""
_, nick, ident, host, _, realname = params
self.whoisUser(nick, ident, host, realname)
class FulviaWrapper(): class FulviaWrapper():
""" """
A wrapper class for Fulvia to provide default destinations for msg A wrapper class for Fulvia to provide default destinations for msg

View File

@ -4,6 +4,7 @@ Some helper functions and other tools.
""" """
import re import re
import threading import threading
from collections import defaultdict
op_level = { op_level = {
"voice": 1, "voice": 1,
@ -55,6 +56,36 @@ class FulviaMemory(dict):
return result return result
class FulviaMemoryDefault(defaultdict):
"""
A simple thread-safe dict implementation.
In order to prevent exceptions when iterating over the values and changing
them at the same time from different threads, we use a blocking lock on
``__setitem__`` and ``__contains__``.
"""
def __init__(self, *args):
defaultdict.__init__(self, *args)
self.lock = threading.Lock()
def __setitem__(self, key, value):
self.lock.acquire()
result = defaultdict.__setitem__(self, key, value)
self.lock.release()
return result
def __contains__(self, key):
"""
Check if a key is in the dict. Locks it for writes when doing so.
"""
self.lock.acquire()
result = defaultdict.__contains__(self, key)
self.lock.release()
return result
class User(object): class User(object):
"""A representation of a user Fulvia is aware of.""" """A representation of a user Fulvia is aware of."""
def __init__(self, nick): def __init__(self, nick):
@ -168,3 +199,19 @@ def configureHostMask(mask):
if m is not None: if m is not None:
return '%s!%s@*' % (m.group(1), m.group(2)) return '%s!%s@*' % (m.group(1), m.group(2))
return '' return ''
def getOpSym(level):
"""Returns the appropriate op_level symbol given a level value."""
if level >= op_level["owner"]:
return "~"
elif level >= op_level["admin"]:
return "&"
elif level >= op_level["op"]:
return "@"
elif level >= op_level["halfop"]:
return "%"
elif level >= op_level["voice"]:
return "+"
else:
return " "