2018-03-16 03:13:43 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
"""
|
|
|
|
The core bot class for Fulvia.
|
|
|
|
"""
|
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
import time
|
|
|
|
import threading
|
|
|
|
import traceback
|
2018-05-27 21:45:17 -04:00
|
|
|
from datetime import datetime
|
2018-03-16 03:13:43 -04:00
|
|
|
|
|
|
|
from twisted.internet import protocol
|
|
|
|
from twisted.words.protocols import irc
|
|
|
|
|
|
|
|
import db
|
|
|
|
import tools
|
2019-10-08 12:39:13 -04:00
|
|
|
import config
|
2018-03-16 03:13:43 -04:00
|
|
|
import loader
|
|
|
|
from trigger import Trigger
|
|
|
|
|
|
|
|
class Fulvia(irc.IRCClient):
|
2019-10-08 12:39:13 -04:00
|
|
|
def __init__(self):
|
|
|
|
self.nickname = config.nickname
|
|
|
|
self.nick = config.nickname
|
2018-03-16 03:13:43 -04:00
|
|
|
"""The bot's current nickname."""
|
|
|
|
|
2019-10-08 12:39:13 -04:00
|
|
|
self.realname = config.realname
|
2018-03-16 03:13:43 -04:00
|
|
|
"""The bot's 'real name', used in whois."""
|
|
|
|
|
2019-10-08 12:39:13 -04:00
|
|
|
self.username = config.username
|
2018-03-16 03:13:43 -04:00
|
|
|
"""The bot's username ident used for logging into the server."""
|
|
|
|
|
2018-05-27 21:45:17 -04:00
|
|
|
self.host = ""
|
|
|
|
"""The bot's host, virtual or otherwise. To be filled in later."""
|
|
|
|
|
2019-10-08 12:39:13 -04:00
|
|
|
self.static = "static"
|
2018-05-27 21:45:17 -04:00
|
|
|
os.makedirs(self.static, exist_ok=True)
|
2018-03-16 03:13:43 -04:00
|
|
|
"""The path to the bot's static file directory."""
|
|
|
|
|
2019-10-08 12:39:13 -04:00
|
|
|
self.log_path = "logs"
|
2019-10-09 18:35:38 -04:00
|
|
|
os.makedirs(self.log_path, exist_ok=True)
|
2018-05-27 21:45:17 -04:00
|
|
|
"""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."""
|
|
|
|
|
2018-03-16 03:13:43 -04:00
|
|
|
self.channels = tools.FulviaMemory()
|
|
|
|
"""
|
|
|
|
A dictionary of all channels the bot is currently in and the users
|
|
|
|
in them.
|
|
|
|
"""
|
|
|
|
|
|
|
|
self.memory = tools.FulviaMemory()
|
|
|
|
"""
|
|
|
|
A thread-safe general purpose dictionary to be used by whatever
|
|
|
|
modules need it.
|
|
|
|
"""
|
|
|
|
|
2019-10-08 12:39:13 -04:00
|
|
|
self.db = db.FulviaDB()
|
2018-03-16 03:13:43 -04:00
|
|
|
"""
|
|
|
|
A class with some basic interactions for the bot's sqlite3 databse.
|
|
|
|
"""
|
|
|
|
|
|
|
|
self._hooks = []
|
|
|
|
"""
|
|
|
|
A list containing all function names to be hooked with every message
|
|
|
|
received.
|
|
|
|
"""
|
|
|
|
|
2018-05-25 11:53:15 -04:00
|
|
|
self.commands = {}
|
2018-03-16 03:13:43 -04:00
|
|
|
"""
|
2018-03-28 23:03:06 -04:00
|
|
|
A dictionary containing all commands to look for and the name
|
2018-03-16 03:13:43 -04:00
|
|
|
of the function they call.
|
|
|
|
"""
|
|
|
|
|
|
|
|
self._times = {}
|
|
|
|
"""
|
|
|
|
A dictionary full of times when certain functions were called for
|
|
|
|
use with the @rate decorator. Keys are function names.
|
|
|
|
"""
|
|
|
|
|
|
|
|
self.url_callbacks = {}
|
|
|
|
"""
|
|
|
|
A dictionary of URL callbacks to allow other modules interact with
|
|
|
|
the URL module.
|
|
|
|
"""
|
|
|
|
|
2019-10-07 16:37:39 -04:00
|
|
|
self._user_joined = []
|
|
|
|
"""These get called when a user joins a channel."""
|
|
|
|
|
2019-10-08 12:39:13 -04:00
|
|
|
self._disabled_modules = config.disabled_modules
|
2018-06-11 12:50:09 -04:00
|
|
|
"""These modules will NOT be loaded when load_modules() is called."""
|
|
|
|
|
2018-03-16 03:13:43 -04:00
|
|
|
self.load_modules()
|
|
|
|
|
|
|
|
|
|
|
|
def load_modules(self):
|
|
|
|
"""
|
|
|
|
Find and load all of our modules.
|
|
|
|
"""
|
|
|
|
print(f"Loading modules...")
|
2018-05-25 11:53:15 -04:00
|
|
|
self._hooks = []
|
|
|
|
self.commands = {}
|
|
|
|
self._times = {}
|
|
|
|
self.url_callbacks = {}
|
2018-03-16 03:13:43 -04:00
|
|
|
# ensure they're empty
|
|
|
|
|
2019-10-08 12:39:13 -04:00
|
|
|
modules = loader.find_modules()
|
2018-03-16 03:13:43 -04:00
|
|
|
loaded = 0
|
|
|
|
failed = 0
|
|
|
|
for name, path in modules.items():
|
|
|
|
try:
|
2018-06-11 12:50:09 -04:00
|
|
|
if name in self._disabled_modules:
|
|
|
|
continue
|
2018-03-16 03:13:43 -04:00
|
|
|
loader.load_module(self, path)
|
|
|
|
loaded += 1
|
|
|
|
except Exception as e:
|
|
|
|
print(f"{name} failed to load.")
|
|
|
|
print(e)
|
|
|
|
traceback.print_exc()
|
|
|
|
failed += 1
|
|
|
|
continue
|
|
|
|
print(f"Registered {loaded} modules,")
|
|
|
|
print(f"{failed} modules failed to load")
|
|
|
|
|
|
|
|
|
2018-05-27 14:16:50 -04:00
|
|
|
def register_callable(self, func):
|
2018-03-16 03:13:43 -04:00
|
|
|
"""
|
|
|
|
Registers all callable functions loaded from modules into one
|
|
|
|
convenient table.
|
|
|
|
"""
|
2018-05-27 14:16:50 -04:00
|
|
|
if hasattr(func, 'commands'):
|
|
|
|
for cmd in func.commands:
|
2019-10-08 07:52:37 -04:00
|
|
|
self.commands[cmd] = func
|
2018-03-16 03:13:43 -04:00
|
|
|
|
2018-05-27 14:16:50 -04:00
|
|
|
if func.hook:
|
2019-10-08 07:52:37 -04:00
|
|
|
self._hooks.append(func)
|
2018-03-16 03:13:43 -04:00
|
|
|
|
2018-05-27 14:16:50 -04:00
|
|
|
if func.rate or func.channel_rate or func.global_rate:
|
|
|
|
self._times[func.__name__] = {}
|
2018-03-16 03:13:43 -04:00
|
|
|
|
2018-05-27 14:16:50 -04:00
|
|
|
if hasattr(func, 'url_callback'):
|
|
|
|
for url in func.url_callback:
|
|
|
|
self.url_callbacks[url] = func
|
2018-03-16 03:13:43 -04:00
|
|
|
|
2019-10-07 16:37:39 -04:00
|
|
|
if func.user_joined:
|
|
|
|
self._user_joined.append(func)
|
|
|
|
|
2018-03-16 03:13:43 -04:00
|
|
|
|
|
|
|
def unregister_callable(self, func):
|
|
|
|
"""
|
|
|
|
Removes the provided function object from the internal list of
|
|
|
|
module functions.
|
|
|
|
"""
|
|
|
|
if not callable(func) or not loader.is_triggerable(func):
|
|
|
|
return
|
|
|
|
|
|
|
|
if hasattr(func, 'commands'):
|
|
|
|
for command in func.commands:
|
2018-05-25 11:53:15 -04:00
|
|
|
self.commands.pop(command)
|
2018-03-16 03:13:43 -04:00
|
|
|
|
|
|
|
if func.hook:
|
2020-01-07 18:58:19 -05:00
|
|
|
self._hooks.remove(func)
|
2018-03-16 03:13:43 -04:00
|
|
|
|
|
|
|
if func.rate or func.channel_rate or func.global_rate:
|
2020-01-07 18:58:19 -05:00
|
|
|
self._times.pop(func)
|
2018-03-16 03:13:43 -04:00
|
|
|
|
|
|
|
if hasattr(func, 'url_callback'):
|
|
|
|
for url in func.url_callback:
|
|
|
|
self.url_callbacks.pop(url)
|
|
|
|
|
2019-10-07 16:37:39 -04:00
|
|
|
if func.user_joined:
|
|
|
|
self._user_joined.remove(func)
|
|
|
|
|
2018-03-16 03:13:43 -04:00
|
|
|
|
|
|
|
def stillConnected(self):
|
|
|
|
"""Returns true if the bot is still connected to the server."""
|
|
|
|
if self._heartbeat:
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def call(self, func, bot, trigger):
|
|
|
|
"""
|
|
|
|
A wrapper for calling module functions so that we can have nicer
|
|
|
|
error handling.
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
func(bot, trigger)
|
|
|
|
except Exception as e:
|
|
|
|
msg = type(e).__name__ + ": " + str(e)
|
|
|
|
self.msg(trigger.channel, msg)
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
|
|
|
2018-05-27 21:45:17 -04:00
|
|
|
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())
|
2019-10-08 12:39:13 -04:00
|
|
|
timestamp = t.strftime(config.default_time_format)
|
2018-05-27 21:45:17 -04:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
2018-03-16 03:13:43 -04:00
|
|
|
## Actions involving the bot directly.
|
|
|
|
## These will get called automatically.
|
|
|
|
|
|
|
|
def privmsg(self, user, channel, message):
|
|
|
|
"""
|
|
|
|
Called when the bot receives a PRIVMSG, which can come from channels
|
|
|
|
or users alike.
|
|
|
|
"""
|
2018-05-27 21:45:17 -04:00
|
|
|
nick = user.partition("!")[0].partition("@")[0]
|
|
|
|
if channel.startswith("#"):
|
2019-10-08 11:45:21 -04:00
|
|
|
opSym = self.channels[channel].users[nick].op_level
|
2018-05-27 21:45:17 -04:00
|
|
|
else:
|
2019-10-08 11:45:21 -04:00
|
|
|
opSym = ''
|
2018-05-27 21:45:17 -04:00
|
|
|
channel = nick
|
2019-10-08 11:45:21 -04:00
|
|
|
line = f"<{opSym}{nick}> {message}"
|
2018-05-27 21:45:17 -04:00
|
|
|
self.log(channel, line)
|
|
|
|
|
2019-10-08 07:52:37 -04:00
|
|
|
funcs = []
|
2019-10-08 12:39:13 -04:00
|
|
|
if message.startswith(config.prefix) and message != config.prefix:
|
2019-10-08 07:52:37 -04:00
|
|
|
command = message.partition(" ")[0]
|
2019-10-08 12:39:13 -04:00
|
|
|
command = command.replace(config.prefix, "", 1)
|
2018-05-29 08:08:42 -04:00
|
|
|
cmd = self.commands.get(command)
|
|
|
|
if not cmd:
|
2018-03-16 03:13:43 -04:00
|
|
|
return
|
2019-10-08 07:52:37 -04:00
|
|
|
funcs.append(cmd)
|
2018-03-16 03:13:43 -04:00
|
|
|
|
2019-10-08 07:52:37 -04:00
|
|
|
funcs += self._hooks
|
2018-03-16 03:13:43 -04:00
|
|
|
|
2019-10-08 07:52:37 -04:00
|
|
|
for func in funcs:
|
2020-01-07 18:58:19 -05:00
|
|
|
trigger = Trigger(user, channel, message)
|
2018-03-16 03:13:43 -04:00
|
|
|
bot = FulviaWrapper(self, trigger)
|
|
|
|
|
|
|
|
if func.rate:
|
2020-01-07 18:58:19 -05:00
|
|
|
t = self._times[func.__name__].get(trigger.nick, 0)
|
2018-03-16 03:13:43 -04:00
|
|
|
if time.time() - t < func.rate and not trigger.admin:
|
|
|
|
return
|
2020-01-07 18:58:19 -05:00
|
|
|
self._times[func.__name__][trigger.nick] = time.time()
|
2018-03-16 03:13:43 -04:00
|
|
|
|
|
|
|
if func.channel_rate:
|
2020-01-07 18:58:19 -05:00
|
|
|
t = self._times[func.__name__].get(trigger.channel, 0)
|
2018-03-16 03:13:43 -04:00
|
|
|
if time.time() - t < func.channel_rate and not trigger.admin:
|
|
|
|
return
|
2020-01-07 18:58:19 -05:00
|
|
|
self._times[func.__name__][trigger.channel] = time.time()
|
2018-03-16 03:13:43 -04:00
|
|
|
|
|
|
|
if func.global_rate:
|
2020-01-07 18:58:19 -05:00
|
|
|
t = self._times[func.__name__].get("global", 0)
|
2018-03-16 03:13:43 -04:00
|
|
|
if time.time() - t < func.channel_rate and not trigger.admin:
|
|
|
|
return
|
2020-01-07 18:58:19 -05:00
|
|
|
self._times[func.__name__]["global"] = time.time()
|
2018-03-16 03:13:43 -04:00
|
|
|
|
|
|
|
if func.thread == True:
|
2018-05-27 14:16:50 -04:00
|
|
|
t = threading.Thread(target=self.call,args=(func, bot, trigger))
|
2018-03-16 03:13:43 -04:00
|
|
|
t.start()
|
|
|
|
else:
|
|
|
|
self.call(func, bot, trigger)
|
|
|
|
|
|
|
|
|
|
|
|
def joined(self, channel):
|
|
|
|
"""Called when the bot joins a new channel."""
|
2019-10-08 11:45:21 -04:00
|
|
|
line = f"-!- {self.nickname} [{self.username}@{self.host}] "
|
|
|
|
line += f"has joined {channel}"
|
2018-05-27 21:45:17 -04:00
|
|
|
self.log(channel, line)
|
|
|
|
|
2018-03-16 03:13:43 -04:00
|
|
|
print(f"Joined {channel}")
|
2019-10-08 11:45:21 -04:00
|
|
|
self.channels[channel] = tools.Channel(channel)
|
|
|
|
user = tools.User(self.nickname)
|
|
|
|
self.channels[channel].users[self.nickname] = user
|
2018-03-16 03:13:43 -04:00
|
|
|
|
|
|
|
|
2018-05-27 21:45:17 -04:00
|
|
|
def left(self, channel, reason=""):
|
2019-03-17 13:31:36 -04:00
|
|
|
"""Called when the bot leaves a channel."""
|
2019-10-08 11:45:21 -04:00
|
|
|
line = f"-!- {self.nickname} [{self.username}@{self.host}] "
|
|
|
|
line += f"has left {channel} [{reason}]"
|
2018-05-27 21:45:17 -04:00
|
|
|
self.log(channel, line)
|
|
|
|
|
2018-03-16 03:13:43 -04:00
|
|
|
print(f"Parted {channel}")
|
|
|
|
self.channels.pop(channel)
|
|
|
|
|
|
|
|
|
2018-05-27 21:45:17 -04:00
|
|
|
def noticed(self, user, channel, message):
|
|
|
|
"""Called when the bot receives a NOTICE from a user or channel."""
|
|
|
|
# TODO: something?
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2018-03-16 03:13:43 -04:00
|
|
|
def modeChanged(self, user, channel, add_mode, modes, args):
|
|
|
|
"""Called when users or channel's modes are changed."""
|
2019-10-08 11:45:21 -04:00
|
|
|
line = f"-!- mode/{channel} ["
|
2018-05-27 21:45:17 -04:00
|
|
|
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)
|
|
|
|
|
2018-05-27 14:16:50 -04:00
|
|
|
if not channel.startswith("#"): # server level
|
|
|
|
return
|
|
|
|
|
|
|
|
for n, mode in enumerate(modes):
|
|
|
|
if args[n] == None: # channel mode
|
|
|
|
if add_mode:
|
|
|
|
self.channels[channel].modes.add(mode)
|
|
|
|
else:
|
|
|
|
self.channels[channel].modes.remove(mode)
|
|
|
|
|
|
|
|
elif mode in tools.op_level.keys(): # user mode, op_level mode
|
|
|
|
nick = args[n]
|
2019-10-08 11:45:21 -04:00
|
|
|
user = self.channels[channel].users[nick]
|
2018-03-16 03:13:43 -04:00
|
|
|
if add_mode:
|
2019-10-08 11:45:21 -04:00
|
|
|
user.op_level += mode
|
2018-03-16 03:13:43 -04:00
|
|
|
else:
|
2019-10-08 11:45:21 -04:00
|
|
|
user.op_level.replace(mode, '', 1)
|
2018-05-27 14:16:50 -04:00
|
|
|
|
|
|
|
else: # user mode, non-op_level mode
|
|
|
|
continue
|
2018-03-16 03:13:43 -04:00
|
|
|
|
|
|
|
|
|
|
|
def signedOn(self):
|
|
|
|
"""Called when the bot successfully connects to the server."""
|
2019-10-08 12:39:13 -04:00
|
|
|
if config.oper_password:
|
|
|
|
self.sendLine("OPER " + config.nickname + ' ' + config.oper_password)
|
2018-03-16 03:13:43 -04:00
|
|
|
print(f"Signed on as {self.nickname}")
|
2018-05-27 21:45:17 -04:00
|
|
|
self.whois(self.nickname)
|
|
|
|
|
2019-10-08 11:45:21 -04:00
|
|
|
line = f"*** Signed onto {self.hostname} as "
|
|
|
|
line += f"{self.nickname}!{self.username}@{self.host}"
|
2018-05-27 21:45:17 -04:00
|
|
|
self.log(self.hostname, line)
|
|
|
|
|
2019-10-08 12:39:13 -04:00
|
|
|
for channel in config.channels:
|
2018-03-16 03:13:43 -04:00
|
|
|
self.join(channel)
|
|
|
|
|
|
|
|
|
|
|
|
def kickedFrom(self, channel, kicker, message):
|
|
|
|
"""Called when the bot is kicked from a channel."""
|
2019-10-08 11:45:21 -04:00
|
|
|
line = f"-!- {self.nickname} was kicked from {channel} "
|
|
|
|
line += f"by {kicker} [{message}]"
|
2018-05-27 21:45:17 -04:00
|
|
|
self.log(channel, line)
|
|
|
|
|
2018-03-16 03:13:43 -04:00
|
|
|
self.channels.pop(channel)
|
|
|
|
|
|
|
|
|
2019-03-17 13:31:36 -04:00
|
|
|
def nickChanged(self, nick):
|
|
|
|
"""Called when the bot changes it's nickname."""
|
2019-10-08 11:45:21 -04:00
|
|
|
line = f"-!- you are now known as {nick}"
|
2019-03-17 13:31:36 -04:00
|
|
|
|
2019-10-08 11:45:21 -04:00
|
|
|
for channel_name, channel in self.channels.items():
|
2019-10-09 18:35:38 -04:00
|
|
|
self.log(channel_name, line)
|
2019-03-17 13:31:36 -04:00
|
|
|
|
2019-10-08 11:45:21 -04:00
|
|
|
user = channel.users.pop(self.nickname)
|
|
|
|
user.nick = nick
|
|
|
|
channel.users[nick] = user
|
2019-03-17 13:31:36 -04:00
|
|
|
|
|
|
|
|
2018-03-16 03:13:43 -04:00
|
|
|
## Actions the bot observes other users doing in the channel.
|
|
|
|
## These will get called automatically.
|
|
|
|
|
|
|
|
def userJoined(self, user, channel):
|
|
|
|
"""Called when the bot sees another user join a channel."""
|
2018-05-27 21:45:17 -04:00
|
|
|
nick, _, host = user.partition("!")
|
2018-03-16 03:13:43 -04:00
|
|
|
|
2019-10-08 11:45:21 -04:00
|
|
|
line = f"-!- {nick} [{host}] has joined {channel}"
|
2018-05-27 21:45:17 -04:00
|
|
|
self.log(channel, line)
|
2018-03-16 03:13:43 -04:00
|
|
|
|
2019-10-09 18:35:38 -04:00
|
|
|
new_user = tools.User(nick)
|
|
|
|
self.channels[channel].users[nick] = new_user
|
2018-05-27 21:45:17 -04:00
|
|
|
|
2019-10-07 16:37:39 -04:00
|
|
|
for func in self._user_joined:
|
2020-01-07 18:58:19 -05:00
|
|
|
trigger = Trigger(user, channel, f"{user} has joined")
|
2019-10-07 16:37:39 -04:00
|
|
|
bot = FulviaWrapper(self, trigger)
|
2020-01-07 18:58:19 -05:00
|
|
|
t = threading.Thread(target=self.call, args=(func, bot, trigger))
|
2019-10-07 16:37:39 -04:00
|
|
|
t.start()
|
|
|
|
|
2018-05-27 21:45:17 -04:00
|
|
|
|
|
|
|
def userLeft(self, user, channel, reason=""):
|
|
|
|
"""Called when the bot sees another user leave a channel."""
|
|
|
|
nick, _, host = user.partition("!")
|
|
|
|
|
2019-10-08 11:45:21 -04:00
|
|
|
line = f"-!- {nick} [{host}] has left {channel} [{reason}]"
|
2018-05-27 21:45:17 -04:00
|
|
|
self.log(channel, line)
|
|
|
|
|
2019-10-08 11:45:21 -04:00
|
|
|
self.channels[channel].users.pop(nick)
|
2018-03-16 03:13:43 -04:00
|
|
|
|
|
|
|
|
|
|
|
def userQuit(self, user, quitMessage):
|
|
|
|
"""Called when the bot sees another user disconnect from the network."""
|
2018-05-27 21:45:17 -04:00
|
|
|
nick, _, host = user.partition("!")
|
2019-10-09 18:35:38 -04:00
|
|
|
line = f"-!- {nick} [{host}] has quit [{quitMessage}]"
|
2018-05-27 21:45:17 -04:00
|
|
|
|
2019-10-08 11:45:21 -04:00
|
|
|
for channel_name, channel in self.channels.items():
|
|
|
|
if not nick in channel.users:
|
|
|
|
continue
|
2019-10-09 18:35:38 -04:00
|
|
|
self.log(channel_name, line)
|
2018-05-27 21:45:17 -04:00
|
|
|
|
2019-10-08 11:45:21 -04:00
|
|
|
channel.users.pop(nick)
|
2018-03-16 03:13:43 -04:00
|
|
|
|
|
|
|
|
|
|
|
def userKicked(self, kickee, channel, kicker, message):
|
|
|
|
"""
|
|
|
|
Called when the bot sees another user getting kicked from the channel.
|
|
|
|
"""
|
2019-10-08 11:45:21 -04:00
|
|
|
line =f"-!- {kickee} was kicked from {channel} by {kicker} [{message}]"
|
2018-05-27 21:45:17 -04:00
|
|
|
self.log(channel, line)
|
|
|
|
|
2019-10-08 11:45:21 -04:00
|
|
|
self.channels[channel].users.pop(kickee)
|
2018-03-16 03:13:43 -04:00
|
|
|
|
|
|
|
|
|
|
|
def topicUpdated(self, user, channel, newTopic):
|
|
|
|
"""Called when the bot sees a user update the channel's topic."""
|
2019-10-08 11:45:21 -04:00
|
|
|
line = f"-!- {user} changed the topic of {channel} to: {newTopic}"
|
2018-05-27 21:45:17 -04:00
|
|
|
self.log(channel, line)
|
|
|
|
|
2018-03-16 03:13:43 -04:00
|
|
|
self.channels[channel].topic = newTopic
|
|
|
|
|
|
|
|
|
|
|
|
def userRenamed(self, oldname, newname):
|
2019-03-17 13:31:36 -04:00
|
|
|
"""Called when the bot sees a user change their nickname."""
|
2019-10-08 11:45:21 -04:00
|
|
|
line = "-!- {oldname} is now known as {newname}"
|
2018-05-27 21:45:17 -04:00
|
|
|
|
2019-10-08 11:45:21 -04:00
|
|
|
for channel_name, channel in self.channels.items():
|
2019-11-05 07:56:50 -05:00
|
|
|
if oldname not in channel.users.keys():
|
|
|
|
continue
|
2019-10-08 11:45:21 -04:00
|
|
|
self.log(channel_name, line)
|
2018-05-27 21:45:17 -04:00
|
|
|
|
2019-10-08 11:45:21 -04:00
|
|
|
user = channel.users.pop(oldname)
|
|
|
|
user.nick = newname
|
|
|
|
channel.users[newname] = user
|
2018-03-16 03:13:43 -04:00
|
|
|
|
|
|
|
|
|
|
|
def namesReceived(self, channel, channel_type, nicklist):
|
|
|
|
"""
|
|
|
|
Called when the bot receives a list of names in the channel from the
|
|
|
|
server.
|
|
|
|
"""
|
|
|
|
self.channels[channel].channel_type = channel_type
|
|
|
|
for nick in nicklist:
|
2019-10-08 11:45:21 -04:00
|
|
|
op_level = ''
|
|
|
|
if nick[0] in tools.op_level.keys():
|
|
|
|
op_level = nick[0]
|
2018-03-16 03:13:43 -04:00
|
|
|
nick = nick[1:]
|
2019-10-08 11:45:21 -04:00
|
|
|
user = tools.User(nick)
|
|
|
|
user.op_level = op_level
|
|
|
|
self.channels[channel].users[nick] = user
|
2018-03-16 03:13:43 -04:00
|
|
|
|
|
|
|
|
2018-05-27 21:45:17 -04:00
|
|
|
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
|
2019-10-08 11:45:21 -04:00
|
|
|
else:
|
|
|
|
for channel_name, channel in self.channels.items():
|
|
|
|
if nick in channel:
|
|
|
|
user = channel[nick]
|
|
|
|
user.ident = ident
|
|
|
|
user.host = host
|
|
|
|
user.realname = realname
|
2018-05-29 17:41:47 -04:00
|
|
|
|
|
|
|
|
|
|
|
def whoisIdle(self, nick, idle, signon):
|
|
|
|
"""
|
|
|
|
Called when the bot receives a WHOIS about a particular user.
|
|
|
|
nick - nickname of the user
|
|
|
|
idle - seconds since last activity from user
|
|
|
|
signon - unix timestamp when user signed on
|
|
|
|
"""
|
|
|
|
if self.memory.get("idle_callbacks"):
|
|
|
|
self.memory["idle_callbacks"][nick].callback((nick, idle, signon))
|
|
|
|
self.memory["idle_callbacks"].pop(nick)
|
2018-05-27 21:45:17 -04:00
|
|
|
|
|
|
|
|
2018-03-16 03:13:43 -04:00
|
|
|
## User commands, from client->server
|
|
|
|
|
2018-05-27 21:45:17 -04:00
|
|
|
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("#"):
|
2019-10-08 11:45:21 -04:00
|
|
|
opSym = self.channels[user].users[self.nickname].op_level
|
2018-05-27 21:45:17 -04:00
|
|
|
else:
|
2019-10-08 11:45:21 -04:00
|
|
|
opSym = ''
|
|
|
|
line = f"<{opSym}{self.nickname}> {message}"
|
2018-05-27 21:45:17 -04:00
|
|
|
self.log(user, line)
|
|
|
|
|
2019-10-09 18:35:38 -04:00
|
|
|
super().msg(user, message, length)
|
2018-05-27 21:45:17 -04:00
|
|
|
|
|
|
|
|
2018-03-16 03:13:43 -04:00
|
|
|
def reply(self, text, dest, reply_to, notice=False):
|
|
|
|
"""
|
2018-05-27 14:16:50 -04:00
|
|
|
Sends a message to 'dest' prefixed with 'reply_to' and a colon,
|
|
|
|
ala "reply_to: text". More useful with the wrapper class.
|
2018-03-16 03:13:43 -04:00
|
|
|
"""
|
|
|
|
text = reply_to + ": " + text
|
|
|
|
self.msg(dest, text)
|
|
|
|
|
|
|
|
|
|
|
|
def quit(self, message=""):
|
|
|
|
"""
|
|
|
|
Disconnects from quitting normally results in the factory
|
|
|
|
reconnecting us, so we need to include shutting down the factory.
|
|
|
|
"""
|
|
|
|
print("Quit from server")
|
|
|
|
irc.IRCClient.quit(self, message)
|
|
|
|
self.factory.stopTrying()
|
|
|
|
sys.exit(0) # why doesn't this work?
|
|
|
|
|
|
|
|
|
|
|
|
## Raw server->client messages
|
|
|
|
|
|
|
|
def irc_RPL_NAMREPLY(self, prefix, params):
|
|
|
|
"""
|
|
|
|
Called when we have received a list of names in the channel from the
|
|
|
|
server.
|
|
|
|
"""
|
|
|
|
channel_types = {"@": "secret", "*": "private", "=": "public"}
|
|
|
|
channel_type = channel_types[params[1]]
|
2018-03-16 03:56:45 -04:00
|
|
|
channel = params[2]
|
2018-03-16 03:13:43 -04:00
|
|
|
nicklist = params[3].split()
|
|
|
|
self.namesReceived(channel, channel_type, nicklist)
|
|
|
|
|
|
|
|
|
2018-05-27 21:45:17 -04:00
|
|
|
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]"
|
2018-05-29 17:41:47 -04:00
|
|
|
query. Contains hostmask information.
|
2018-05-27 21:45:17 -04:00
|
|
|
"""
|
|
|
|
_, nick, ident, host, _, realname = params
|
|
|
|
self.whoisUser(nick, ident, host, realname)
|
|
|
|
|
|
|
|
|
2018-05-29 17:41:47 -04:00
|
|
|
def irc_RPL_WHOISIDLE(self, prefix, params):
|
|
|
|
"""
|
|
|
|
Called when we receive the server's response from our "WHOIS [user]"
|
|
|
|
query. Contains idle and signon time information.
|
|
|
|
"""
|
|
|
|
_, nick, idle, signon, _ = params
|
|
|
|
self.whoisIdle(nick, idle, signon)
|
|
|
|
|
2018-05-27 21:45:17 -04:00
|
|
|
|
2019-09-13 19:01:08 -04:00
|
|
|
def lineReceived(self, line):
|
|
|
|
"""Overridden to fix broken unicode issues."""
|
|
|
|
if bytes != str and isinstance(line, bytes):
|
|
|
|
line = line.decode('utf-8', errors='surrogateescape')
|
|
|
|
super().lineReceived(line)
|
|
|
|
|
|
|
|
|
2018-03-16 03:13:43 -04:00
|
|
|
class FulviaWrapper():
|
2018-05-27 14:16:50 -04:00
|
|
|
"""
|
|
|
|
A wrapper class for Fulvia to provide default destinations for msg
|
|
|
|
methods and so forth.
|
|
|
|
"""
|
2018-03-16 03:13:43 -04:00
|
|
|
def __init__(self, fulvia, trigger):
|
|
|
|
object.__setattr__(self, '_bot', fulvia)
|
|
|
|
object.__setattr__(self, '_trigger', trigger)
|
|
|
|
# directly setting these values would cause a recursion loop
|
|
|
|
# overflow
|
|
|
|
|
|
|
|
def __getattr__(self, attr):
|
|
|
|
"""Redirects getting attributes to the master bot instance."""
|
|
|
|
return getattr(self._bot, attr)
|
|
|
|
|
|
|
|
def __setattr__(self, attr, value):
|
|
|
|
"""Redirects setting attributes to the master bot instance."""
|
|
|
|
return setattr(self._bot, attr, value)
|
|
|
|
|
|
|
|
def msg(self, user=None, message=None, length=None):
|
|
|
|
"""
|
|
|
|
If user is None it will default to the channel the message was
|
|
|
|
received on.
|
|
|
|
"""
|
|
|
|
if user == None:
|
|
|
|
raise TypeError("msg() missing 2 required positional " \
|
|
|
|
+ "arguments: 'user' and 'message'")
|
|
|
|
if message == None:
|
|
|
|
# assume the first argument is the message
|
|
|
|
message = user
|
|
|
|
user = self._trigger.channel
|
|
|
|
self._bot.msg(user, message, length)
|
|
|
|
|
|
|
|
def reply(self, message, destination=None, reply_to=None, notice=False):
|
|
|
|
"""
|
|
|
|
If destination is None, it defaults to the channel the message
|
|
|
|
was received on.
|
|
|
|
If reply_to is None, it defaults to the person who sent the
|
|
|
|
message.
|
|
|
|
"""
|
|
|
|
if destination is None:
|
|
|
|
destination = self._trigger.channel
|
|
|
|
if reply_to is None:
|
|
|
|
reply_to = self._trigger.nick
|
|
|
|
self._bot.reply(message, destination, reply_to, notice)
|
|
|
|
|
|
|
|
|
|
|
|
class FulviaFactory(protocol.ReconnectingClientFactory):
|
2019-10-08 12:39:13 -04:00
|
|
|
protocol = Fulvia
|