Vendor drastikbot/drastikbot+modules v2.1
This commit is contained in:
parent
82f4cddc27
commit
befb55fff3
142
src/dbot_tools.py
Normal file
142
src/dbot_tools.py
Normal file
@ -0,0 +1,142 @@
|
||||
# coding=utf-8
|
||||
|
||||
# Common tools used by the bot and it's modules.
|
||||
# Tools: - text_fix: decode message and remove whitespace
|
||||
# - Config : config file reader and writer
|
||||
# - Logger : Logger functions
|
||||
|
||||
'''
|
||||
Copyright (C) 2018-2019 drastik.org
|
||||
|
||||
This file is part of drastikbot.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, version 3 only.
|
||||
|
||||
This program 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def text_fix(line):
|
||||
if not isinstance(line, str):
|
||||
line = line.decode('utf8', errors='ignore')
|
||||
|
||||
# Remove "\r\n" and all whitespace.
|
||||
# line = line.replace("\\r", "").replace("\\n", "")
|
||||
# line = line.replace("\r", "").replace("\n", "")
|
||||
line = ''.join(line.splitlines())
|
||||
return line
|
||||
|
||||
|
||||
def p_truncate(text, whole, percent, ellipsis=False):
|
||||
if not isinstance(text, str):
|
||||
raise TypeError("'text' must be str, not bytes")
|
||||
return
|
||||
t = text.encode('utf-8')
|
||||
lim = int((whole * percent) / 100)
|
||||
if not len(t) > lim:
|
||||
return t.decode('utf8', errors='ignore')
|
||||
e = b'...'
|
||||
if ellipsis:
|
||||
t = t[:lim + len(e)].rsplit(b' ', 1)[0] + e
|
||||
else:
|
||||
t = t[:lim].rsplit(b' ', 1)[0]
|
||||
return t.decode('utf8', errors='ignore')
|
||||
|
||||
|
||||
class Config:
|
||||
"""
|
||||
The Config class provides easy reading and writing to the
|
||||
bot's configuration file.
|
||||
conf_dir : should be the configuration directory.
|
||||
"""
|
||||
def __init__(self, conf_dir):
|
||||
self.config_file = '{}/config.json'.format(conf_dir)
|
||||
|
||||
def read(self):
|
||||
with open(self.config_file, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
def write(self, value):
|
||||
"""
|
||||
value : must be the whole configuration and not just a
|
||||
setting, because the config file's contents are being
|
||||
replaced with 'value'
|
||||
"""
|
||||
with open(self.config_file, 'w') as f:
|
||||
json.dump(value, f, indent=4)
|
||||
|
||||
|
||||
class Logger:
|
||||
"""
|
||||
This class provides minimal logging functionality.
|
||||
It supports log rotation and two logging states: INFO, DEBUG.
|
||||
- Todo:
|
||||
- add gzip compression of rotated logs
|
||||
- IF PROBLEMS OCCURE: make it thread safe
|
||||
"""
|
||||
def __init__(self, conf_dir, log_filename):
|
||||
config = Config(conf_dir).read()
|
||||
try:
|
||||
log_dir = config['sys']['log_dir']
|
||||
except KeyError:
|
||||
log_dir = conf_dir + '/logs/'
|
||||
self.log_dir = log_dir
|
||||
if not Path(log_dir).exists():
|
||||
Path(log_dir).mkdir(parents=True, exist_ok=True)
|
||||
self.log_file = Path('{}/{}'.format(log_dir, log_filename))
|
||||
try:
|
||||
self.log_mode = config['sys']['log_level'].lower()
|
||||
except KeyError:
|
||||
self.log_mode = 'info'
|
||||
try:
|
||||
self.log_size = config['sys']['log_size'].lower()
|
||||
except KeyError:
|
||||
self.log_size = 5 * 1000000
|
||||
|
||||
def log_rotate(self):
|
||||
current_log_size = self.log_file.stat().st_size
|
||||
if current_log_size > self.log_size:
|
||||
while True:
|
||||
n = 1
|
||||
np = (self.log_dir + self.log_file.stem +
|
||||
str(n) + "".join(self.log_file.suffixes))
|
||||
rotate_p = Path(np)
|
||||
if rotate_p.exists():
|
||||
n += 1
|
||||
else:
|
||||
self.log_file.rename(rotate_p)
|
||||
break
|
||||
|
||||
def log_write(self, msg, line, debug=False):
|
||||
with open(str(self.log_file), 'a+') as log:
|
||||
log.write(line + '\n')
|
||||
if not debug:
|
||||
print(msg)
|
||||
else:
|
||||
print(line)
|
||||
self.log_rotate()
|
||||
|
||||
def info(self, msg):
|
||||
if self.log_mode == 'info' or self.log_mode == 'debug':
|
||||
line = '{} - INFO - {}'.format(
|
||||
datetime.datetime.now(), msg)
|
||||
self.log_write(msg, line)
|
||||
|
||||
def debug(self, msg):
|
||||
if self.log_mode == 'debug':
|
||||
caller_name = sys._getframe(1).f_code.co_name
|
||||
line = f'{datetime.datetime.now()} - DEBUG - {caller_name} - {msg}'
|
||||
self.log_write(msg, line, debug=True)
|
102
src/drastikbot.py
Executable file
102
src/drastikbot.py
Executable file
@ -0,0 +1,102 @@
|
||||
|
||||
#!/usr/bin/env python3
|
||||
# coding=utf-8
|
||||
|
||||
# This is the initialization file used to start the bot.
|
||||
# It parses command line arguments, verifies the state of the configuration.
|
||||
# file and calls the main bot functions.
|
||||
|
||||
'''
|
||||
Copyright (C) 2017-2019 drastik.org
|
||||
|
||||
This file is part of drastikbot.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, version 3 only.
|
||||
|
||||
This program 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import os
|
||||
import sys
|
||||
import signal
|
||||
import argparse
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
from toolbox import config_check
|
||||
from dbot_tools import Logger
|
||||
from irc.worker import Main
|
||||
|
||||
# Get the project's root directory
|
||||
proj_path = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def print_banner():
|
||||
banner = [
|
||||
"---------------------------------------------------------------",
|
||||
" Drastikbot 2.1",
|
||||
" An IRC bot focused on its extensibility and personalization",
|
||||
"",
|
||||
" License: GNU AGPLv3 only",
|
||||
" Drastikbot 2.1 comes WITHOUT ANY WARRANTY",
|
||||
""
|
||||
" Welcome!",
|
||||
"---------------------------------------------------------------"
|
||||
]
|
||||
for i in banner:
|
||||
print(i)
|
||||
|
||||
|
||||
def parser():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-c', '--conf', nargs='?',
|
||||
type=str, help='Specify the configuration directory')
|
||||
args = parser.parse_args()
|
||||
|
||||
# Check if a configuration directory is given or use the default one
|
||||
# "~/.drastikbot"
|
||||
if args.conf:
|
||||
path = Path(args.conf)
|
||||
if not path.is_dir():
|
||||
try:
|
||||
path.mkdir(parents=True, exist_ok=False)
|
||||
except FileExistsError:
|
||||
sys.exit("[Error] Making configuration directory at"
|
||||
f" '{args.conf}' failed. Another file with that name"
|
||||
" already exists.")
|
||||
conf_dir = str(path.expanduser().resolve())
|
||||
else:
|
||||
path = Path('~/.drastikbot').expanduser()
|
||||
if not path.is_dir():
|
||||
try:
|
||||
path.mkdir(parents=True, exist_ok=False)
|
||||
except FileExistsError:
|
||||
sys.exit("[Error] Making configuration directory at"
|
||||
" '~/.drastikbot' failed. Another file with that name"
|
||||
" already exists.")
|
||||
conf_dir = str(path)
|
||||
|
||||
config_check.config_check(conf_dir)
|
||||
logger = Logger(conf_dir, 'runtime.log')
|
||||
logger.info('\nStarting up...\n')
|
||||
return conf_dir
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print_banner()
|
||||
conf_dir = parser()
|
||||
c = Main(conf_dir, proj_path)
|
||||
try:
|
||||
signal.signal(signal.SIGINT, c.sigint_hdl)
|
||||
c.main()
|
||||
except Exception as e:
|
||||
logger = Logger(conf_dir, 'runtime.log')
|
||||
logger.debug(f'Exception on startIRC(): {e} {traceback.print_exc()}')
|
249
src/irc/irc.py
Normal file
249
src/irc/irc.py
Normal file
@ -0,0 +1,249 @@
|
||||
# coding=utf-8
|
||||
|
||||
# This file provides methods for connecting to and sending message
|
||||
# to an IRC server. Additionally it keeps track of every vital
|
||||
# runtime variable the bot needs for its connection to the IRC server
|
||||
# and the management of its features.
|
||||
|
||||
'''
|
||||
Copyright (C) 2017-2019 drastik.org
|
||||
|
||||
This file is part of drastikbot.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, version 3 only.
|
||||
|
||||
This program 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import socket
|
||||
import ssl
|
||||
import time
|
||||
import traceback
|
||||
|
||||
import dbot_tools
|
||||
|
||||
|
||||
class Settings:
|
||||
def __init__(self, conf_dir):
|
||||
self.cd = conf_dir
|
||||
self.proj_path = '' # Project root.
|
||||
self.log = None # Runtime Logging
|
||||
self.version = "2.1 (alpha)"
|
||||
self.reconnect_delay = 0
|
||||
self.sigint = 0
|
||||
# Message length used by irc.send
|
||||
# (TODO: The bot should dynamically change it based on the bot's
|
||||
# nickname and hostmask length)
|
||||
self.msg_len = 400
|
||||
|
||||
# Runtime Variables
|
||||
self.curr_nickname = '' # Nickname currently used
|
||||
self.bot_hostmask = '' # Hostmask issued by the server
|
||||
self.alt_nickname = False # Alternative nickname used
|
||||
self.connected_ip = '' # IP of the connected IRC server
|
||||
self.connected_host = '' # Hostname of the connected server
|
||||
self.ircv3_ver = '301' # IRCv3 version supported by the bot
|
||||
self.ircv3_cap_req = ('sasl') # IRCv3 Bot Capability Requirements
|
||||
self.ircv3_serv = False # True: IRCv3 supported by the server
|
||||
self.ircv3_cap_ls = [] # IRCv3 Server Capabilities
|
||||
self.ircv3_cap_ack = [] # IRCv3 Server Capabilities Acknowledged
|
||||
# sasl_state = 0: Not tried | 1: Success | 2: Fail | 3: In progress
|
||||
self.sasl_state = 0
|
||||
self.conn_state = 0 # 0: Disconnected | 1: Registering | 2: Connected
|
||||
self.namesdict = {} # {channel1: [["=","S"], {nick1: ["@"], , ...}]}
|
||||
self.botmodes = [] # [x,I] Modes returned after registration
|
||||
|
||||
def config_load(self):
|
||||
# (Re)loads the configuration file and sets the bot's variables.
|
||||
c = dbot_tools.Config(self.cd).read()
|
||||
c_conn = c['irc']['connection']
|
||||
self.owners = c['irc']['owners']
|
||||
self.host = c_conn['network']
|
||||
self.port = c_conn['port']
|
||||
self.ssl = c_conn.get('ssl', False)
|
||||
self.nickname = c_conn['nickname']
|
||||
self.username = c_conn['username']
|
||||
self.realname = c_conn['realname']
|
||||
self.authentication = c_conn.get('authentication', '')
|
||||
self.auth_password = c_conn['auth_password']
|
||||
self.net_password = c_conn.get('net_password', '')
|
||||
self.quitmsg = c_conn.get('quitmsg', f'drastikbot {self.version}')
|
||||
self.msg_delay = c_conn.get('msg_delay', 1)
|
||||
self.channels = c['irc']['channels']
|
||||
self.modules_obj = c['irc']['modules']
|
||||
self.modules_load = self.modules_obj['load']
|
||||
self.mod_glb_prefix = self.modules_obj['global_prefix']
|
||||
self.mod_chn_prefix = {} # {#channel: cmd_prefix}
|
||||
# User Access List
|
||||
try:
|
||||
self.user_acl = tuple(c['irc']['user_acl'])
|
||||
except KeyError:
|
||||
self.user_acl = ()
|
||||
# Channel Prefixes
|
||||
for chan in self.channels:
|
||||
try:
|
||||
mpref = c['irc']['modules']['channel_prefix'][chan]
|
||||
except KeyError:
|
||||
mpref = self.mod_glb_prefix
|
||||
self.mod_chn_prefix[chan] = mpref
|
||||
|
||||
|
||||
class Drastikbot():
|
||||
def __init__(self, conf_dir):
|
||||
self.cd = conf_dir
|
||||
self.log = dbot_tools.Logger(self.cd, 'runtime.log')
|
||||
self.var = Settings(self.cd)
|
||||
|
||||
def set_msg_len(self, nick_ls):
|
||||
u = f"{nick_ls[0]}!{nick_ls[1]}@{nick_ls[2]} "
|
||||
c = len(u.encode('utf-8'))
|
||||
self.var.msg_len = 512 - c
|
||||
|
||||
def send(self, cmds, text=None):
|
||||
m_len = self.var.msg_len
|
||||
cmds = [dbot_tools.text_fix(cmd) for cmd in cmds]
|
||||
if text:
|
||||
text = dbot_tools.text_fix(text)
|
||||
# https://tools.ietf.org/html/rfc2812.html#section-2.3
|
||||
# NOTE: 2) IRC messages are limited to 512 characters in length.
|
||||
# With CR-LF we are left with 510 characters to use
|
||||
tosend = f"{' '.join(cmds)} :{text}"
|
||||
else:
|
||||
tosend = ' '.join(cmds) # for commands
|
||||
try:
|
||||
tosend = tosend.encode('utf-8')
|
||||
multipart = False
|
||||
remainder = 0
|
||||
if len(tosend) + 2 > m_len:
|
||||
# Handle messages that are too long to fit in one message.
|
||||
# Truncate messages at the last space found to avoid breaking
|
||||
# utf-8.
|
||||
tosend = tosend[:m_len].rsplit(b' ', 1)
|
||||
remainder = len(tosend[1])
|
||||
multipart = True
|
||||
tosend = tosend[0]
|
||||
|
||||
tosend = tosend + b'\r\n'
|
||||
self.irc_socket.send(tosend)
|
||||
except Exception:
|
||||
self.log.debug(f'Exception on send() @ irc.py:'
|
||||
f'\n{traceback.format_exc()}')
|
||||
return self.irc_socket.close()
|
||||
# If the input text is longer than 510 send the rest.
|
||||
# "limit" decrements after every message and is used to control
|
||||
# the amount of messages the bot is allowed to send. The value
|
||||
# is set to -1 by default, which means it can send an infinite
|
||||
# amount of messages, since zero will never be met in the if
|
||||
# statement below.
|
||||
if multipart:
|
||||
time.sleep(self.var.msg_delay)
|
||||
tr = m_len - 2 - len(' '.join(cmds).encode('utf-8')) - remainder
|
||||
t = text.encode('utf-8')[tr:]
|
||||
self.send(cmds, t)
|
||||
|
||||
def privmsg(self, target, msg):
|
||||
self.send(('PRIVMSG', target), msg)
|
||||
|
||||
def notice(self, target, msg):
|
||||
self.send(('NOTICE', target), msg)
|
||||
|
||||
def join(self, channels):
|
||||
print("\n")
|
||||
for key, value in channels.items():
|
||||
self.send(('JOIN', key, value))
|
||||
self.log.info(f"Joined {key}")
|
||||
|
||||
def part(self, channel, msg):
|
||||
self.send(('PART', channel), msg)
|
||||
|
||||
def invite(self, nick, channel): # untested
|
||||
self.send(('INVITE', nick, channel))
|
||||
|
||||
def kick(self, channel, nick, msg): # untested
|
||||
self.send(('KICK', channel, nick, msg))
|
||||
|
||||
def nick(self, nick):
|
||||
self.send(('NICK', '{}'.format(nick)))
|
||||
|
||||
def quit(self, msg=''):
|
||||
if not msg:
|
||||
msg = self.var.quitmsg
|
||||
self.send(('QUIT',), msg)
|
||||
|
||||
def away(self, msg=''):
|
||||
self.send('AWAY', msg)
|
||||
|
||||
def reconn_wait(self):
|
||||
'''
|
||||
Incrementally Wait before reconnection to the server. The initial delay
|
||||
is zero, then it is set to ten seconds and keeps doubling after each
|
||||
attempt until it reaches ten minutes.
|
||||
'''
|
||||
time.sleep(self.var.reconnect_delay)
|
||||
if self.var.reconnect_delay == 0:
|
||||
self.var.reconnect_delay = 10 # 10 sec.
|
||||
elif self.var.reconnect_delay < 60 * 10: # 10 mins.
|
||||
self.var.reconnect_delay *= 2
|
||||
# Because the previous statement will end up with 640 seconds,
|
||||
# we set it to 600 seconds and keep it there until we connect:
|
||||
if self.var.reconnect_delay > 60 * 10: # 10 mins.
|
||||
self.var.reconnect_delay = 60 * 10
|
||||
|
||||
def connect(self):
|
||||
self.var.config_load()
|
||||
try:
|
||||
# Timeout on socket.create_connection should be above the irc
|
||||
# server's ping timeout setting
|
||||
self.irc_socket = socket.create_connection(
|
||||
(self.var.host, self.var.port), 300)
|
||||
if self.var.ssl:
|
||||
self.irc_socket = ssl.wrap_socket(self.irc_socket)
|
||||
except OSError:
|
||||
if self.var.sigint:
|
||||
return
|
||||
self.log.debug('Exception on connect() @ irc_sock.connect()'
|
||||
f'\n{traceback.format_exc()}')
|
||||
self.log.info(' - No route to host. Retrying in {} seconds'.format(
|
||||
self.var.reconnect_delay))
|
||||
try:
|
||||
self.irc_socket.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.reconn_wait() # Wait before next reconnection attempt.
|
||||
return self.connect()
|
||||
except IOError:
|
||||
try:
|
||||
self.irc_socket.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.log.debug(f'Exception on connect() @ irc_sock.connect():'
|
||||
f'\n{traceback.format_exc()}')
|
||||
return self.connect()
|
||||
|
||||
# SOCKET OPTIONS
|
||||
# self.irc_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
|
||||
if self.var.net_password:
|
||||
# Authenticate if the server is password protected
|
||||
self.send(('PASS', self.var.net_password))
|
||||
|
||||
# Set the connected variables, to inform the bot where exactly we are
|
||||
# connected
|
||||
try:
|
||||
# There is a possibility that an Exception is raised here.
|
||||
# Because they are not vital to the bot's operation we will just
|
||||
# ignore them.
|
||||
# Consider adding them to dbot_tools as functions.
|
||||
self.var.connected_ip = self.irc_socket.getpeername()[0]
|
||||
self.var.connected_host = socket.gethostbyaddr(
|
||||
self.var.connected_ip)[0]
|
||||
except Exception:
|
||||
pass
|
109
src/irc/message.py
Normal file
109
src/irc/message.py
Normal file
@ -0,0 +1,109 @@
|
||||
# coding=utf-8
|
||||
|
||||
# Parse messages recieved by the IRC server and pack them in
|
||||
# variables suitable for usage by the bot's functions.
|
||||
|
||||
'''
|
||||
Copyright (C) 2018-2019 drastik.org
|
||||
|
||||
This file is part of drastikbot.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, version 3 only.
|
||||
|
||||
This program 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
|
||||
class Message:
|
||||
"""
|
||||
Class used to parse IRC messages sent by the server and turn them into
|
||||
objects useable by the bot and its modules.
|
||||
"""
|
||||
def __init__(self, msg_raw):
|
||||
self.msg_raw = msg_raw
|
||||
# Decode utf-8 and remove CR and LR from 'self.msg_raw'.
|
||||
# self.msg = text_fix(self.msg_raw) # Prefer this if errors happen.
|
||||
self.msg = self.msg_raw.decode('utf8', errors='ignore')
|
||||
self.msg = ''.join(self.msg.splitlines())
|
||||
# Split the message in a list
|
||||
self.msg_ls = self.msg.split()
|
||||
# Split the message in [Prefix Command] and [Params]
|
||||
msg_sp = self.msg.split(" :", 1)
|
||||
# Split [Prefix Command] in [Prefix] [Command]
|
||||
prefcmd_sp = msg_sp[0].split(" ", 1)
|
||||
# Remove ":" from the prefix
|
||||
self.prefix = prefcmd_sp[0][1:]
|
||||
# Split the irc commands in a list
|
||||
try:
|
||||
self.cmd_ls = prefcmd_sp[1].split()
|
||||
except IndexError:
|
||||
self.cmd_ls = prefcmd_sp[0].split()
|
||||
# Get the params
|
||||
try:
|
||||
self.params = msg_sp[1]
|
||||
except IndexError:
|
||||
self.params = ''
|
||||
# Get the msgtype (PRIVMSG, NOTICE, JOIN)
|
||||
self.msgtype = self.cmd_ls[0]
|
||||
# Get user information.
|
||||
try:
|
||||
prefix_list = self.prefix.split('!', 1)
|
||||
self.nickname = prefix_list[0]
|
||||
self.username = prefix_list[1].split('@', 1)[0]
|
||||
self.hostname = prefix_list[1].split('@', 1)[1]
|
||||
except IndexError:
|
||||
# self.nickname = prefix_list[0] # Should be set in the try:
|
||||
self.username = ''
|
||||
self.hostname = ''
|
||||
|
||||
def channel_prep(self, irc):
|
||||
"""
|
||||
The channel is placed in different positions depending on the IRC
|
||||
command sent. For parsing we use a dictionary with the various
|
||||
commands and we call the matching sub-function to parse the channel.
|
||||
"""
|
||||
def _join():
|
||||
return self.msg_ls[2].lstrip(":")
|
||||
|
||||
def _353():
|
||||
return self.cmd_ls[3]
|
||||
|
||||
def _privmsg():
|
||||
return self.cmd_ls[1]
|
||||
|
||||
def __rest():
|
||||
'''
|
||||
This is used for msgtypes not specified in the 'self.channel'
|
||||
dict below.
|
||||
'''
|
||||
try:
|
||||
c = self.cmd_ls[1]
|
||||
except IndexError:
|
||||
c = ""
|
||||
return c
|
||||
|
||||
self.channel = {
|
||||
'JOIN': _join,
|
||||
'353': _353,
|
||||
'PRIVMSG': _privmsg
|
||||
}.get(self.msgtype, __rest)()
|
||||
|
||||
if self.channel == irc.var.curr_nickname:
|
||||
self.channel = self.nickname
|
||||
|
||||
try:
|
||||
self.params_nocmd = self.params.split(' ', 1)[1].strip()
|
||||
except IndexError:
|
||||
self.params_nocmd = ''
|
||||
try:
|
||||
self.chn_prefix = irc.var.mod_chn_prefix[self.channel]
|
||||
except KeyError:
|
||||
self.chn_prefix = irc.var.mod_glb_prefix
|
394
src/irc/modules.py
Normal file
394
src/irc/modules.py
Normal file
@ -0,0 +1,394 @@
|
||||
# coding=utf-8
|
||||
|
||||
# Methods for importing, reloading and calling drastikbot modules.
|
||||
# Features for modules such as Variable Memory, SQLite databases,
|
||||
# channel blacklist and whitelist checks and user access list checks
|
||||
# are defined here.
|
||||
|
||||
'''
|
||||
Copyright (C) 2017-2019 drastik.org
|
||||
|
||||
This file is part of drastikbot.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, version 3 only.
|
||||
|
||||
This program 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import sys
|
||||
from pathlib import Path, PurePath
|
||||
import importlib
|
||||
import traceback
|
||||
import inspect
|
||||
import sqlite3
|
||||
from dbot_tools import Config, Logger
|
||||
from toolbox import user_acl
|
||||
|
||||
|
||||
class VariableMemory:
|
||||
def varset(self, name, value):
|
||||
"""
|
||||
Set a variable to be kept in the bot's memory.
|
||||
|
||||
'name' is the name of the variable.
|
||||
'value' is the variable's value.
|
||||
|
||||
The name of the actual saved variable is not the actual name given, but
|
||||
<calling module's name>_<'name'>. e.g sed_msgdict.
|
||||
This is to allow different modules have any variable and make accessing
|
||||
those variables easier.
|
||||
"""
|
||||
# Get the caller module's name:
|
||||
frm = inspect.stack()[1]
|
||||
mod = inspect.getmodule(frm[0]).__name__
|
||||
# Check if it's a call from this module. (modules.py)
|
||||
if mod == __loader__.name:
|
||||
return
|
||||
else:
|
||||
name = f"{mod}_{name}"
|
||||
|
||||
setattr(self, name, value)
|
||||
|
||||
def varget(self, name, defval=False, raw=False):
|
||||
"""
|
||||
Get a variable from the bot's memory.
|
||||
'name' is the name of the variable.
|
||||
'defval' is the default to set if no variable is already set.
|
||||
'raw' if True will not append the module's name in frond of 'name'.
|
||||
This is to access variables from other modules.
|
||||
|
||||
If both 'defval' and 'raw' are non False but the variable cannon be
|
||||
found an AttributeError is raised.
|
||||
"""
|
||||
if not raw:
|
||||
# Get the caller module's name:
|
||||
frm = inspect.stack()[1]
|
||||
mod = inspect.getmodule(frm[0]).__name__
|
||||
# Check if it's a call from this module. (modules.py)
|
||||
if mod == __loader__.name:
|
||||
return
|
||||
name = f"{mod}_{name}"
|
||||
|
||||
try:
|
||||
return getattr(self, name)
|
||||
except AttributeError:
|
||||
if defval and not raw:
|
||||
self.varset(name, defval)
|
||||
return defval
|
||||
else:
|
||||
raise AttributeError(f"'{name}' has no value set. "
|
||||
"Try passing a default value"
|
||||
" or set 'raw=False'")
|
||||
|
||||
|
||||
class Info:
|
||||
"""
|
||||
This class is used for setting up message and runtime variables by
|
||||
Modules.info_prep() and is passed to the modules.
|
||||
"""
|
||||
__slots__ = ['cmd', 'channel', 'nickname', 'username', 'hostname', 'msg',
|
||||
'msg_nocmd', 'cmd_prefix', 'msgtype', 'is_pm', 'msg_raw',
|
||||
'db', 'msg_ls', 'msg_prefix', 'cmd_ls', 'msg_full', 'modules',
|
||||
'command_dict', 'auto_list', 'mod_import', 'blacklist',
|
||||
'whitelist', 'msg_params', 'varget', 'varset']
|
||||
|
||||
|
||||
class Modules:
|
||||
def __init__(self, irc):
|
||||
self.irc = irc
|
||||
self.cd = self.irc.var.cd
|
||||
self.log = Logger(self.cd, 'modules.log')
|
||||
self.varmem = VariableMemory()
|
||||
|
||||
self.modules = {} # {Module Name : Module Callable}
|
||||
|
||||
self.msgtype_dict = {} # {msgtype: [module1, ...]}
|
||||
self.auto_list = [] # [module1, ...]
|
||||
self.startup_list = [] # [module1, ...]
|
||||
self.command_dict = {} # {command1: module}
|
||||
# Databases #
|
||||
self.dbmem = sqlite3.connect(':memory:', check_same_thread=False)
|
||||
self.dbdisk = sqlite3.connect('{}/drastikbot.db'.format(self.cd),
|
||||
check_same_thread=False)
|
||||
self.mod_settings = {} # {Module Name : {setting : value}}
|
||||
|
||||
def mod_imp_prep(self, module_dir, auto=False):
|
||||
'''
|
||||
Search a directory for modules, check if they are listed in the config
|
||||
file and return a list of the modules.
|
||||
If 'auto' is True load all the modules without checking the config
|
||||
file (used for core modules needed for the bot's operation).
|
||||
'''
|
||||
path = Path(module_dir)
|
||||
load = Config(self.cd).read()['irc']['modules']['load']
|
||||
if not path.is_dir():
|
||||
# Check if the module directory exists under the
|
||||
# configuration directory and make it otherwise.
|
||||
path.mkdir(exist_ok=True)
|
||||
self.log.info(' - Module directory created at: {}'
|
||||
.format(module_dir))
|
||||
# Append the module directory in the sys.path variable
|
||||
sys.path.append(module_dir)
|
||||
files = [f for f in path.iterdir() if Path(
|
||||
PurePath(module_dir).joinpath(f)).is_file()]
|
||||
modimp_list = []
|
||||
for f in files:
|
||||
suffix = PurePath(f).suffix
|
||||
prefix = PurePath(f).stem
|
||||
if suffix == '.py':
|
||||
if auto:
|
||||
modimp_list.append(prefix)
|
||||
elif prefix in load:
|
||||
modimp_list.append(prefix)
|
||||
return modimp_list
|
||||
|
||||
def mod_import(self):
|
||||
"""
|
||||
Import modules specified in the configuration file.
|
||||
Check for values specified in the Module() class of every module and
|
||||
set the variables declared in __init__() for later use.
|
||||
"""
|
||||
self.log.info('\n> Loading Modules:\n')
|
||||
|
||||
importlib.invalidate_caches()
|
||||
|
||||
modimp_list = []
|
||||
module_dir = self.cd + '/modules'
|
||||
modimp_list.extend(self.mod_imp_prep(module_dir))
|
||||
module_dir = self.irc.var.proj_path + '/irc/modules'
|
||||
modimp_list.extend(self.mod_imp_prep(module_dir, auto=True))
|
||||
|
||||
# Empty variabled from previous import:
|
||||
self.modules = {} # {Module Name : Module Callable}
|
||||
self.msgtype_dict = {} # {msgtype: [module1, ...]}
|
||||
self.auto_list = [] # [module1, ...]
|
||||
self.startup_list = [] # [module1, ...]
|
||||
self.command_dict = {} # {command1: module}
|
||||
|
||||
for m in modimp_list:
|
||||
try:
|
||||
modimp = importlib.import_module(m)
|
||||
# Dictionary with the module name and it's callable
|
||||
self.modules[m] = modimp
|
||||
# Read the module's "Module()" class to
|
||||
# get the required runtime information,
|
||||
# such as: commands, sysmode
|
||||
try:
|
||||
mod = modimp.Module()
|
||||
except AttributeError:
|
||||
self.log.info(f'<!> Module "{m}" does not have a Module() '
|
||||
'class and was not loaded.')
|
||||
|
||||
commands = [c for c in getattr(mod, 'commands', [])]
|
||||
for c in commands:
|
||||
if c in self.command_dict:
|
||||
self.log.info(f'<!> Command "{c}" is already used by '
|
||||
f'"{self.command_dict[c]}.py", but is '
|
||||
f'also requested by "{m}.py".')
|
||||
sys.exit(1)
|
||||
self.command_dict[c] = m
|
||||
msgtypes = [m.upper() for m in getattr(
|
||||
mod, 'msgtypes', ['PRIVMSG'])]
|
||||
for i in msgtypes:
|
||||
self.msgtype_dict.setdefault(i, []).append(m)
|
||||
if getattr(mod, 'auto', False):
|
||||
self.auto_list.append(m)
|
||||
if getattr(mod, 'startup', False):
|
||||
self.startup_list.append(m)
|
||||
|
||||
self.log.info('> Loaded module: {}'.format(m))
|
||||
except Exception:
|
||||
self.log.debug(f'<!> Module "{m}" failed to load: '
|
||||
f'\n{traceback.format_exc()}')
|
||||
|
||||
def mod_reload(self):
|
||||
'''
|
||||
Reload the already imported modules.
|
||||
WARNING: Changes in the Module() class are not reloaded using this
|
||||
method. Reimport the modules (with self.mod_import()) to do that.
|
||||
'''
|
||||
for value in self.modules.values():
|
||||
importlib.reload(value)
|
||||
|
||||
def blacklist(self, module, channel):
|
||||
'''
|
||||
Read the configuration file and get the blacklist for the given module.
|
||||
[returns]
|
||||
True : if the channel is in the blacklist
|
||||
False: if the channel is not in the blacklist or if the blacklist
|
||||
is empty
|
||||
'''
|
||||
try:
|
||||
blacklist = self.irc.var.modules_obj['blacklist'][module]
|
||||
except KeyError as e:
|
||||
if e.args[0] == module:
|
||||
self.irc.var.modules_obj['blacklist'].update({module: []})
|
||||
return False
|
||||
if not blacklist:
|
||||
return False
|
||||
elif channel in blacklist:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def whitelist(self, module, channel):
|
||||
'''
|
||||
Read the configuration file and check if the channel is in the
|
||||
blacklist of the given module.
|
||||
[returns]
|
||||
True : if the channel is in the module's whitelist or if the
|
||||
whitelist is empty and
|
||||
False: if the whitelist is not empty and the channel is not in it.
|
||||
'''
|
||||
try:
|
||||
whitelist = self.irc.var.modules_obj['whitelist'][module]
|
||||
except KeyError as e:
|
||||
if e.args[0] == module:
|
||||
self.irc.var.modules_obj['whitelist'].update({module: []})
|
||||
return True
|
||||
if not whitelist:
|
||||
return True
|
||||
elif channel in whitelist:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def info_prep(self, msg):
|
||||
"""
|
||||
Set values in the Info() class and return that class.
|
||||
|
||||
--Notes:
|
||||
i.cmd :: The module command without the prefix.
|
||||
Values are set by mod_main() before calling the module.
|
||||
In "Auto" modules this will remain as an empty string.
|
||||
i.msg :: Instead of setting the msg.msg value, we set msg.params,
|
||||
to achieve a better looking API. Instead "i.msg_full" is
|
||||
set to msg.msg
|
||||
i.msg_params :: It is the same as i.msg, we use this to match the
|
||||
RFC's terminology.
|
||||
"""
|
||||
i = Info()
|
||||
i.cmd = ''
|
||||
i.channel = msg.channel
|
||||
i.nickname = msg.nickname
|
||||
i.username = msg.username
|
||||
i.hostname = msg.hostname
|
||||
i.msg_raw = msg.msg_raw
|
||||
i.msg_full = msg.msg
|
||||
i.msg = msg.params
|
||||
i.msg_nocmd = msg.params_nocmd
|
||||
i.msg_ls = msg.msg_ls
|
||||
i.msg_prefix = msg.prefix
|
||||
i.msg_params = msg.params
|
||||
i.cmd_ls = msg.cmd_ls
|
||||
i.cmd_prefix = msg.chn_prefix
|
||||
i.msgtype = msg.msgtype
|
||||
i.is_pm = i.channel == i.nickname
|
||||
i.db = [self.dbmem, self.dbdisk]
|
||||
i.varset = self.varmem.varset
|
||||
i.varget = self.varmem.varget
|
||||
i.modules = self.modules
|
||||
i.command_dict = self.command_dict
|
||||
i.auto_list = self.auto_list
|
||||
i.blacklist = self.blacklist
|
||||
i.whitelist = self.whitelist
|
||||
i.mod_import = self.mod_import
|
||||
return i
|
||||
|
||||
def mod_main(self, irc, msg, command):
|
||||
def cmd_modules():
|
||||
try:
|
||||
module = self.command_dict[command[1:]]
|
||||
except KeyError:
|
||||
return
|
||||
if self.blacklist(module, i.channel):
|
||||
return
|
||||
if not self.whitelist(module, i.channel):
|
||||
return
|
||||
if user_acl.is_banned(self.irc.var.user_acl, i.channel, i.nickname,
|
||||
i.username, i.hostname, module):
|
||||
return
|
||||
if module in md:
|
||||
# We set i.cmd to the command's name.
|
||||
i.cmd = command[1:]
|
||||
try:
|
||||
self.modules[module].main(*args)
|
||||
except Exception:
|
||||
self.log.debug(f'<!> Module "{module}" exitted with error:'
|
||||
f'\n{traceback.format_exc()}')
|
||||
|
||||
self.mod_reload() # Reload the bot's modules.
|
||||
i = self.info_prep(msg)
|
||||
args = (i, irc)
|
||||
|
||||
try:
|
||||
md = self.msgtype_dict[msg.msgtype]
|
||||
except KeyError:
|
||||
# No modules use this message type, return.
|
||||
return
|
||||
|
||||
if command[:1] == i.cmd_prefix:
|
||||
cmd_modules()
|
||||
|
||||
for m in list(set(self.auto_list).intersection(md)):
|
||||
if self.blacklist(m, i.channel):
|
||||
continue
|
||||
if not self.whitelist(m, i.channel):
|
||||
continue
|
||||
if user_acl.is_banned(self.irc.var.user_acl, i.channel, i.nickname,
|
||||
i.username, i.hostname, m):
|
||||
continue
|
||||
try:
|
||||
# We set i.cmd to False to indicate that it's an auto call.
|
||||
i.cmd = ""
|
||||
self.modules[m].main(*args)
|
||||
except Exception:
|
||||
self.log.debug(f'<!> Module "{m}" exitted with error: '
|
||||
f'\n{traceback.format_exc()}')
|
||||
|
||||
def mod_startup(self, irc):
|
||||
'''
|
||||
Run modules configured with the "self.startup = True" option.
|
||||
The bot doesn't manage the modules after they get started.
|
||||
This could be problematic for modules that are blocking and so they
|
||||
should periodically check the bot's connection state (e.g. by checking
|
||||
the value of "irc.var.conn_state") and handle a possible disconnect.
|
||||
The bot's whitelist/blacklist is not being taken into account.
|
||||
The "msgtype" and "cmd" passed to the modules is "STARTUP".
|
||||
The "info" tuple is perfectly matched by blank strings.
|
||||
'''
|
||||
def info_prep_startup():
|
||||
'''
|
||||
Special use of the Info class that includes only the variables
|
||||
available at startup.
|
||||
'''
|
||||
i = Info()
|
||||
i.cmd = ''
|
||||
i.msgtype = "STARTUP" # Indicate that this is a startup call.
|
||||
i.db = [self.dbmem, self.dbdisk]
|
||||
i.varget = self.varmem.varget
|
||||
i.varset = self.varmem.varset
|
||||
i.modules = self.modules
|
||||
i.mod_import = self.mod_import
|
||||
return i
|
||||
|
||||
self.mod_reload() # Reload the bot's modules.
|
||||
i = info_prep_startup()
|
||||
args = (i, irc)
|
||||
|
||||
for m in self.startup_list:
|
||||
try:
|
||||
# We set i.cmd to False to indicate that it's an auto call.
|
||||
i.cmd = ""
|
||||
self.modules[m].main(*args)
|
||||
except Exception:
|
||||
self.log.debug(f'<!> Module "{m}" exitted with error: '
|
||||
f'\n{traceback.format_exc()}')
|
674
src/irc/modules/COPYING
Normal file
674
src/irc/modules/COPYING
Normal file
@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
28
src/irc/modules/README.md
Normal file
28
src/irc/modules/README.md
Normal file
@ -0,0 +1,28 @@
|
||||
# drastikbot_modules
|
||||
|
||||
Modules for drastikbot.
|
||||
Visit https://drastik.org/drastikbot/ for more documentation.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
* The newest version of [drastikbot](https://github.com/olagood/drastikbot)
|
||||
* Python 3
|
||||
* GNU/Linux or any UNIX-like OS
|
||||
|
||||
### Installing
|
||||
|
||||
Check https://drastik.org/drastikbot/quickguide.html#install_modules for module installation and configuration instructions.
|
||||
|
||||
## Contributing
|
||||
|
||||
All code contributions must follow the PEP 8 styling guidelines. Use of flake8 is recommended. The code should be fully tested to ensure it does not break drastikbot or any of its modules.
|
||||
|
||||
## Authors
|
||||
|
||||
* **drastik** - [olagood](https://github.com/olagood) | [drastik.org](https://drastik.org)
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the GNU General Public License Version 3 - see the [COPYING](COPYING) file for details.
|
834
src/irc/modules/admin.py
Normal file
834
src/irc/modules/admin.py
Normal file
@ -0,0 +1,834 @@
|
||||
# coding=utf-8
|
||||
|
||||
# Module that provides an interface for managing the bot over IRC.
|
||||
|
||||
'''
|
||||
Copyright (C) 2018-2019 drastik.org
|
||||
|
||||
This file is part of drastikbot.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, version 3 only.
|
||||
|
||||
This program 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import datetime
|
||||
|
||||
from dbot_tools import Config
|
||||
from user_auth import user_auth
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.commands = ["join", "part", "privmsg", "notice",
|
||||
"acl_add", "acl_del", "acl_list",
|
||||
"mod_import",
|
||||
"mod_whitelist_add", "mod_whitelist_del",
|
||||
"mod_blacklist_add", "mod_blacklist_del",
|
||||
"mod_list",
|
||||
"mod_global_prefix_set", "mod_channel_prefix_set",
|
||||
"admin_help"]
|
||||
|
||||
|
||||
# --- Settings --- #
|
||||
user_modes = ['~', '&', '@', '%']
|
||||
####################
|
||||
|
||||
|
||||
#
|
||||
# Permission Checks
|
||||
#
|
||||
def is_bot_owner(irc, nickname):
|
||||
if nickname in irc.var.owners:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def is_channel_mod(irc, nickname, channel):
|
||||
try:
|
||||
for m in irc.var.namesdict[channel][1][nickname]:
|
||||
if m in user_modes:
|
||||
return True
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def is_allowed(i, irc, nickname, channel=""):
|
||||
if is_bot_owner(irc, nickname):
|
||||
if user_auth(i, irc, i.nickname):
|
||||
return True
|
||||
elif channel and is_channel_mod(irc, nickname, channel):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
elif channel and is_channel_mod(irc, nickname, channel):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
#
|
||||
# Channel Management
|
||||
#
|
||||
def _join(irc, channel, password=""):
|
||||
chan_dict = {channel: password}
|
||||
conf_r = Config(irc.cd).read()
|
||||
conf_r['irc']['channels'][channel] = password
|
||||
Config(irc.cd).write(conf_r)
|
||||
irc.var.config_load()
|
||||
irc.join(chan_dict)
|
||||
|
||||
|
||||
def join(i, irc):
|
||||
if not i.msg_nocmd:
|
||||
m = f"Usage: {i.cmd_prefix}join <channel> [password]"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
if not is_allowed(i, irc, i.nickname):
|
||||
m = "\x0304You are not authorized. Are you logged in?"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
args = i.msg_nocmd.split(" ", 1)
|
||||
channel = args[0]
|
||||
try:
|
||||
password = args[1]
|
||||
except IndexError:
|
||||
password = ""
|
||||
|
||||
if channel in irc.var.channels:
|
||||
m = f"\x0303The bot has already joined the channel: {channel}"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
_join(irc, channel, password)
|
||||
irc.notice(i.nickname, f"\x0303Joined {channel}")
|
||||
|
||||
|
||||
def _part(irc, channel, message=""):
|
||||
conf_r = Config(irc.cd).read()
|
||||
if channel not in conf_r['irc']['channels']:
|
||||
return False
|
||||
del conf_r['irc']['channels'][channel]
|
||||
Config(irc.cd).write(conf_r)
|
||||
irc.var.config_load()
|
||||
irc.part(channel, message)
|
||||
return True
|
||||
|
||||
|
||||
def part(i, irc):
|
||||
if not i.msg_nocmd:
|
||||
m = f"Usage: {i.cmd_prefix}part <channel> [message]"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
args = i.msg_nocmd.split(" ", 1)
|
||||
channel = args[0]
|
||||
try:
|
||||
message = args[1]
|
||||
except IndexError:
|
||||
message = ""
|
||||
|
||||
if channel not in irc.var.channels:
|
||||
m = f"\x0304The bot has not joined the channel: {channel}"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
if not is_allowed(i, irc, i.nickname, channel):
|
||||
m = f"\x0304You are not authorized. Are you an operator of {channel}?"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
if _part(irc, channel, message):
|
||||
irc.notice(i.nickname, f"\x0303Left {channel}")
|
||||
else:
|
||||
irc.notice(i.nickname, f"\x0304{channel} not joined")
|
||||
|
||||
|
||||
def privmsg(i, irc):
|
||||
m = f"Usage: {i.cmd_prefix}privmsg <channel> <message>"
|
||||
if not i.msg_nocmd:
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
args = i.msg_nocmd.split(" ", 1)
|
||||
if len(args) < 2:
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
channel = args[0]
|
||||
message = args[1]
|
||||
|
||||
if channel not in irc.var.channels:
|
||||
m = f"\x0304The bot has not joined the channel: {channel}"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
if not is_allowed(i, irc, i.nickname, channel):
|
||||
m = f"\x0304You are not authorized. Are you an operator of {channel}?"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
irc.privmsg(channel, message)
|
||||
irc.notice(i.nickname, "\x0303Message sent")
|
||||
|
||||
|
||||
def notice(i, irc):
|
||||
m = f"Usage: {i.cmd_prefix}notice <channel> <message>"
|
||||
if not i.msg_nocmd:
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
args = i.msg_nocmd.split(" ", 1)
|
||||
if len(args) < 2:
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
channel = args[0]
|
||||
message = args[1]
|
||||
|
||||
if channel not in irc.var.channels:
|
||||
m = f"\x0304The bot has not joined the channel: {channel}"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
if not is_allowed(i, irc, i.nickname, channel):
|
||||
m = f"\x0304You are not authorized. Are you an operator of {channel}?"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
irc.notice(channel, message)
|
||||
irc.notice(i.nickname, "\x0303Message sent")
|
||||
|
||||
|
||||
#
|
||||
# User ACL
|
||||
#
|
||||
def _get_future_unix_timestamp_from_str(duration_str):
|
||||
seconds = 0
|
||||
|
||||
tmp = 0
|
||||
for i in duration_str:
|
||||
if i.isdigit():
|
||||
tmp *= 10
|
||||
tmp += int(i)
|
||||
elif i == 'y':
|
||||
seconds += 31536000 * tmp # 365days * 24hours * 60mins * 60secs
|
||||
tmp = 0
|
||||
elif i == 'M':
|
||||
seconds += 2592000 * tmp # 30days * 24hours * 60mins * 60secs
|
||||
tmp = 0
|
||||
elif i == 'w':
|
||||
seconds += 604800 * tmp # 7days * 24hours * 60mins * 60secs
|
||||
tmp = 0
|
||||
elif i == 'd':
|
||||
seconds += 86400 * tmp # 24hours * 60mins * 60secs
|
||||
tmp = 0
|
||||
elif i == 'h':
|
||||
seconds += 3600 * tmp # 60mins * 60secs
|
||||
tmp = 0
|
||||
elif i == 'm':
|
||||
seconds += 60 * tmp # 60secs
|
||||
tmp = 0
|
||||
elif i == 's':
|
||||
seconds += tmp
|
||||
tmp = 0
|
||||
else:
|
||||
return False
|
||||
|
||||
now = datetime.datetime.now(datetime.timezone.utc).timestamp()
|
||||
return now + seconds
|
||||
|
||||
|
||||
def _check_usermask(usermask):
|
||||
try:
|
||||
t = usermask.split("!", 1)
|
||||
t[0]
|
||||
t = t[1].split("@", 1)
|
||||
t[0]
|
||||
t[1]
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _acl_add(irc, mask):
|
||||
c = Config(irc.cd).read()
|
||||
if mask in c['irc']['user_acl']:
|
||||
return False # The mask already exists
|
||||
c['irc']['user_acl'].append(mask)
|
||||
Config(irc.cd).write(c)
|
||||
irc.var.config_load()
|
||||
return len(c['irc']['user_acl']) - 1
|
||||
|
||||
|
||||
def acl_add(i, irc):
|
||||
m = (f"Usage: {i.cmd_prefix}acl_add "
|
||||
"<channel> <nickname>!<username>@<hostname> <duration> "
|
||||
f"<module1,module2,...> | See {i.cmd_prefix}admin_help for details.")
|
||||
if not i.msg_nocmd:
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
args = i.msg_nocmd.split(" ", 3)
|
||||
if len(args) < 4:
|
||||
return irc.notice(i.nickname, m)
|
||||
channel = args[0]
|
||||
usermask = args[1]
|
||||
duration = args[2]
|
||||
modules = args[3].replace(" ", "")
|
||||
|
||||
if channel not in irc.var.channels and channel != '*':
|
||||
m = f"\x0304The bot has not joined the channel: {channel}"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
if not _check_usermask(usermask):
|
||||
m = f"\x0304Invalid usermask: '{usermask}' given"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
if duration != '0':
|
||||
duration = _get_future_unix_timestamp_from_str(duration)
|
||||
if not duration:
|
||||
m = "\x0304Error while parsing the duration string"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
if modules != '*':
|
||||
module_list = modules.split(",")
|
||||
for mod in module_list:
|
||||
if mod not in i.modules:
|
||||
m = f"\x0304Error: Module {mod} is not loaded"
|
||||
|
||||
if channel == '*':
|
||||
if not is_allowed(i, irc, i.nickname):
|
||||
m = f"\x0304You are not authorized. Are you logged in?"
|
||||
return irc.notice(i.nickname, m)
|
||||
else:
|
||||
if not is_allowed(i, irc, i.nickname, channel):
|
||||
m = ("\x0304You are not authorized. "
|
||||
f"Are you an operator of {channel}?")
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
_acl_add(irc, i.msg_nocmd)
|
||||
irc.notice(i.nickname, f"\x0303User mask added in the ACL")
|
||||
|
||||
|
||||
def _acl_del(irc, idx):
|
||||
c = Config(irc.cd).read()
|
||||
if len(c['irc']['user_acl']) - 1 >= idx:
|
||||
del c['irc']['user_acl'][idx]
|
||||
Config(irc.cd).write(c)
|
||||
irc.var.config_load()
|
||||
return True
|
||||
else:
|
||||
return False # Index out of range
|
||||
|
||||
|
||||
def acl_del(i, irc):
|
||||
m = f"Usage: {i.cmd_prefix}acl_del <mask ID>"
|
||||
if not i.msg_nocmd:
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
if len(i.msg_nocmd.split()) > 1:
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
idx = int(i.msg_nocmd)
|
||||
|
||||
c = Config(irc.cd).read()
|
||||
try:
|
||||
mask = c['irc']['user_acl'][idx]
|
||||
except IndexError:
|
||||
m = "\x0304 This mask does not exist"
|
||||
irc.notice(i.nickname, m)
|
||||
return
|
||||
channel = mask.split(" ", 1)[0]
|
||||
if channel == '*':
|
||||
if not is_allowed(i, irc, i.nickname):
|
||||
m = f"\x0304You are not authorized. Are you logged in?"
|
||||
return irc.notice(i.nickname, m)
|
||||
else:
|
||||
if not is_allowed(i, irc, i.nickname, channel):
|
||||
m = ("\x0304You are not authorized. "
|
||||
f"Are you an operator of {channel}?")
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
if _acl_del(irc, idx):
|
||||
m = f"\x0303Deleted mask: '{mask}' from the ACL"
|
||||
irc.notice(i.nickname, m)
|
||||
else:
|
||||
m = "\x0304 This mask does not exist"
|
||||
irc.notice(i.nickname, m)
|
||||
|
||||
|
||||
def acl_list(i, irc):
|
||||
for idx, mask in enumerate(irc.var.user_acl):
|
||||
irc.privmsg(i.nickname, f"{idx}: {mask}")
|
||||
|
||||
|
||||
#
|
||||
# Module Management
|
||||
#
|
||||
def mod_import(i, irc):
|
||||
if not is_allowed(i, irc, i.nickname):
|
||||
m = f"\x0304You are not authorized. Are you logged in?"
|
||||
return irc.notice(i.nickname, m)
|
||||
i.mod_import()
|
||||
irc.notice(i.nickname, '\x0303New module were imported.')
|
||||
|
||||
|
||||
def _module_wb_list_add(i, irc, module, channel, mode):
|
||||
if mode == "whitelist":
|
||||
edom = "blacklist"
|
||||
elif mode == "blacklist":
|
||||
edom = "whitelist"
|
||||
else:
|
||||
raise ValueError("'mode' can only be 'whitelist' or 'blacklist'.")
|
||||
|
||||
c = Config(irc.cd).read()
|
||||
ls = c["irc"]["modules"][mode]
|
||||
|
||||
if module not in i.modules:
|
||||
return 1 # This module is not loaded
|
||||
elif (module in c["irc"]["modules"][edom]
|
||||
and channel in c["irc"]["modules"][edom][module]):
|
||||
return 2 # This module has a {edom}list set
|
||||
elif module not in ls:
|
||||
ls.update({module: []})
|
||||
elif channel in ls[module]:
|
||||
return 3 # This channel has already been added.
|
||||
|
||||
ls[module].append(channel)
|
||||
Config(irc.cd).write(c)
|
||||
irc.var.config_load()
|
||||
return 0
|
||||
|
||||
|
||||
def mod_whitelist_add(i, irc):
|
||||
m = f"Usage: {i.cmd_prefix}mod_whitelist_add <module> <channel>"
|
||||
if not i.msg_nocmd:
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
args = i.msg_nocmd.split(" ", 1)
|
||||
if len(args) < 2:
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
module = args[0]
|
||||
channel = args[1]
|
||||
|
||||
if channel not in irc.var.channels:
|
||||
m = f"\x0304The bot has not joined the channel: {channel}"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
if not is_allowed(i, irc, i.nickname, channel):
|
||||
m = f"\x0304You are not authorized. Are you an operator of {channel}?"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
ret = _module_wb_list_add(i, irc, module, channel, "whitelist")
|
||||
if ret == 1:
|
||||
irc.notice(i.nickname, f"\x0304The module: {module} is not loaded")
|
||||
elif ret == 2:
|
||||
m = (f"\x0304The module: {module} has a blacklist set. "
|
||||
"Clear the blacklist and try again.")
|
||||
irc.notice(i.nickname, m)
|
||||
elif ret == 3:
|
||||
m = f"\x0304{channel} has already been added in {module}'s whitelist"
|
||||
irc.notice(i.nickname, m)
|
||||
else:
|
||||
m = f"\x0303{channel} added in {module}'s whitelist"
|
||||
irc.notice(i.nickname, m)
|
||||
|
||||
|
||||
def mod_blacklist_add(i, irc):
|
||||
m = f"Usage: {i.cmd_prefix}mod_blacklist_add <module> <channel>"
|
||||
if not i.msg_nocmd:
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
args = i.msg_nocmd.split(" ", 1)
|
||||
if len(args) < 2:
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
module = args[0]
|
||||
channel = args[1]
|
||||
|
||||
if channel not in irc.var.channels:
|
||||
m = f"\x0304The bot has not joined the channel: {channel}"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
if not is_allowed(i, irc, i.nickname, channel):
|
||||
m = f"\x0304You are not authorized. Are you an operator of {channel}?"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
ret = _module_wb_list_add(i, irc, module, channel, "blacklist")
|
||||
if ret == 1:
|
||||
irc.notice(i.nickname, f"\x0304The module: {module} is not loaded")
|
||||
elif ret == 2:
|
||||
m = (f"\x0304The module: {module} has a whitelist set. "
|
||||
"Clear the whitelist and try again.")
|
||||
irc.notice(i.nickname, m)
|
||||
elif ret == 3:
|
||||
m = f"\x0304{channel} has already been added in {module}'s blacklist"
|
||||
irc.notice(i.nickname, m)
|
||||
else:
|
||||
m = f"\x0303{channel} added in {module}'s blacklist"
|
||||
irc.notice(i.nickname, m)
|
||||
|
||||
|
||||
def _module_wb_list_del(irc, module, channel, mode):
|
||||
if mode != "whitelist" and mode != "blacklist":
|
||||
raise ValueError("'mode' can only be 'whitelist' or 'blacklist'.")
|
||||
|
||||
c = Config(irc.cd).read()
|
||||
ls = c["irc"]["modules"][mode]
|
||||
if module in ls and channel in ls[module]:
|
||||
ls[module].remove(channel)
|
||||
Config(irc.cd).write(c)
|
||||
irc.var.config_load()
|
||||
return True
|
||||
else:
|
||||
return False # This channel has not been added.
|
||||
|
||||
|
||||
def mod_whitelist_del(i, irc):
|
||||
m = f"Usage: {i.cmd_prefix}mod_whitelist_del <module> <channel>"
|
||||
if not i.msg_nocmd:
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
args = i.msg_nocmd.split(" ", 1)
|
||||
if len(args) < 2:
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
module = args[0]
|
||||
channel = args[1]
|
||||
|
||||
if channel not in irc.var.channels:
|
||||
m = f"\x0304The bot has not joined the channel: {channel}"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
if not is_allowed(i, irc, i.nickname, channel):
|
||||
m = f"\x0304You are not authorized. Are you an operator of {channel}?"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
if _module_wb_list_del(irc, module, channel, "whitelist"):
|
||||
m = f"\x0303{channel} removed from {module}'s whitelist"
|
||||
return irc.notice(i.nickname, m)
|
||||
else:
|
||||
m = f"\x0304This channel has not been added in {module}'s whitelist"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
|
||||
def mod_blacklist_del(i, irc):
|
||||
m = f"Usage: {i.cmd_prefix}mod_blacklist_del <module> <channel>"
|
||||
if not i.msg_nocmd:
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
args = i.msg_nocmd.split(" ", 1)
|
||||
if len(args) < 2:
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
module = args[0]
|
||||
channel = args[1]
|
||||
|
||||
if channel not in irc.var.channels:
|
||||
m = f"\x0304The bot has not joined the channel: {channel}"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
if not is_allowed(i, irc, i.nickname, channel):
|
||||
m = f"\x0304You are not authorized. Are you an operator of {channel}?"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
if _module_wb_list_del(irc, module, channel, "blacklist"):
|
||||
m = f"\x0303{channel} removed from {module}'s blacklist"
|
||||
return irc.notice(i.nickname, m)
|
||||
else:
|
||||
m = f"\x0304This channel has not been added in {module}'s blacklist"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
|
||||
def _module_wb_list_list(i, irc, channel=""):
|
||||
c = Config(irc.cd).read()
|
||||
wl = c["irc"]["modules"]["whitelist"]
|
||||
bl = c["irc"]["modules"]["blacklist"]
|
||||
|
||||
wl_message = "\x0301,00WHITELIST\x0F :"
|
||||
if channel:
|
||||
wl_message += f" {channel} :"
|
||||
for module in wl:
|
||||
if not channel:
|
||||
wl_message += f" {module}: {wl[module]} /"
|
||||
else:
|
||||
if channel in wl[module]:
|
||||
wl_message += F" {module} /"
|
||||
|
||||
bl_message = "\x0300,01BLACKLIST\x0F :"
|
||||
if channel:
|
||||
bl_message += f" {channel} :"
|
||||
for module in bl:
|
||||
if not channel:
|
||||
bl_message += f" {module}: {bl[module]} /"
|
||||
else:
|
||||
if channel in wl[module]:
|
||||
bl_message += F" {module} /"
|
||||
|
||||
irc.privmsg(i.nickname, wl_message)
|
||||
irc.privmsg(i.nickname, bl_message)
|
||||
|
||||
|
||||
def mod_list(i, irc):
|
||||
if not i.msg_nocmd:
|
||||
if not is_allowed(i, irc, i.nickname):
|
||||
m = (f"\x0304You are not authorized.\x0F "
|
||||
f"Usage: {i.cmd_prefix}mod_list <channel> | "
|
||||
"Bot owners can ommit the <channel> argument.")
|
||||
return irc.notice(i.nickname, m)
|
||||
else:
|
||||
return _module_wb_list_list(i, irc)
|
||||
|
||||
if len(i.msg_nocmd.split(" ")) > 1:
|
||||
m = (f"Usage: {i.cmd_prefix}mod_list <channel> | "
|
||||
"Bot owners can ommit the <channel> argument.")
|
||||
if not is_allowed(i, irc, i.nickname):
|
||||
m = (f"\x0304You are not authorized. "
|
||||
f"Are you an operator of {i.msg_nocmd}?")
|
||||
return irc.notice(i.nickname, m)
|
||||
else:
|
||||
return _module_wb_list_list(i, irc, i.msg_nocmd)
|
||||
|
||||
|
||||
def _mod_global_prefix_set(irc, prefix):
|
||||
c = Config(irc.cd).read()
|
||||
c['irc']['modules']['global_prefix'] = prefix
|
||||
Config(irc.cd).write(c)
|
||||
irc.var.config_load()
|
||||
|
||||
|
||||
def mod_global_prefix_set(i, irc):
|
||||
if not is_allowed(i, irc, i.nickname):
|
||||
m = f"\x0304You are not authorized. Are you logged in?"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
m = f"Usage: {i.cmd_prefix}mod_global_prefix_set <prefix>"
|
||||
if not i.msg_nocmd:
|
||||
irc.notice(i.nickname, m)
|
||||
elif len(i.msg_nocmd.split(" ")) > 1:
|
||||
irc.notice(i.nickname, m)
|
||||
else:
|
||||
_mod_global_prefix_set(irc, i.msg_nocmd)
|
||||
m = f"\x0303Successfully changed the global_prefix to {i.msg_nocmd}"
|
||||
irc.notice(i.nickname, m)
|
||||
|
||||
|
||||
def _mod_channel_prefix_set(irc, channel, prefix):
|
||||
c = Config(irc.cd).read()
|
||||
c['irc']['modules']['channel_prefix'][channel] = prefix
|
||||
Config(irc.cd).write(c)
|
||||
irc.var.config_load()
|
||||
|
||||
|
||||
def mod_channel_prefix_set(i, irc):
|
||||
m = f"Usage: {i.cmd_prefix}mod_channel_prefix_set <channel> <prefix>"
|
||||
if not i.msg_nocmd:
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
args = i.msg_nocmd.split(" ", 1)
|
||||
if len(args) != 2:
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
channel = args[0]
|
||||
prefix = args[1]
|
||||
|
||||
if channel not in irc.var.channels:
|
||||
m = f"\x0304The bot has not joined the channel: {channel}"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
if not is_allowed(i, irc, i.nickname, channel):
|
||||
m = f"\x0304You are not authorized. Are you an operator of {channel}?"
|
||||
return irc.notice(i.nickname, m)
|
||||
|
||||
_mod_channel_prefix_set(irc, channel, prefix)
|
||||
m = ("\x0303Successfully changed the channel_prefix for "
|
||||
f"{channel} to {prefix}")
|
||||
irc.notice(i.nickname, m)
|
||||
|
||||
|
||||
#
|
||||
# Help
|
||||
#
|
||||
def admin_help(i, irc):
|
||||
join = [
|
||||
f"Usage: {i.cmd_prefix}join <channel> [password]",
|
||||
" Permission: Owners",
|
||||
"Join a channel."
|
||||
]
|
||||
part = [
|
||||
f"Usage: {i.cmd_prefix}part <channel> [message]",
|
||||
" Permission: Channel Operators",
|
||||
"Leave a channel. Channel Operators can only use this command on"
|
||||
" channels where they have operator privilages."
|
||||
]
|
||||
privmsg = [
|
||||
f"Usage: {i.cmd_prefix}privmsg <channel> <message>",
|
||||
" Permission: Channel Operators",
|
||||
"Have the bot send a private message to a channel or a person. You"
|
||||
" must be an operator of the channel you want to send a private"
|
||||
" message to. Only bot owners can use this command to send messages to"
|
||||
" other users."
|
||||
]
|
||||
notice = [
|
||||
f"Usage: {i.cmd_prefix}notice <channel> <message>",
|
||||
" Permission: Channel Operators",
|
||||
"Have the bot send a notice to a channel or a person. You must be an"
|
||||
" operator of the channel you want to send a notice to. Only bot"
|
||||
" owners can use this command to send notices to other users."
|
||||
]
|
||||
acl_add = [
|
||||
f"Usage: {i.cmd_prefix}acl_add <channel> "
|
||||
"<nickname>!<username>@<hostname> <duration> <module1,module2,...>",
|
||||
" Permission: Channel Operators",
|
||||
"Add a user access list rule.",
|
||||
"Explanation:",
|
||||
"<channel> : This can be a single channel or '*'. Users can only set"
|
||||
" this to a channel where they have operator privilages. '*' means all"
|
||||
" channels and can only be used by bot owners.",
|
||||
"<nickname>: This can be the exact nickname of an IRC user or '*'."
|
||||
" '*' means any nickname.",
|
||||
"<username>: This can be the exact username of an IRC user or '*' or"
|
||||
" or '*<word>'. '*' means match any username. '*<word>' would match"
|
||||
" everything that has <word> in the end. Note that using the *"
|
||||
" character in the middle or in the end of <word> would exactly"
|
||||
" match <word>* and won't expand to other usernames.",
|
||||
"<hostname>: It can be the exact hostname of an IRC user, '*',"
|
||||
" '*<word>' or '<word>*'. '*' means match any hostname. '*<word>' and"
|
||||
" '<word>*' would match everything that has <word> in the end or in"
|
||||
" the beginning. Every other placement of the * character would be an"
|
||||
" exact match.",
|
||||
"<duration>: Duration is used to specify for how long the rule should"
|
||||
" be in effect. The syntax used is yMwdhms. No spaces should be used."
|
||||
" To have the rule in effect forever set this to '0'."
|
||||
" Example: 1y2M3m would mean 1 year, 2 Months, 3 minutes.",
|
||||
"<mod1,...>: This is a list of modules that the rule will apply to."
|
||||
" The list is comma seperated. Use '*' to have the rule apply for all"
|
||||
" modules.",
|
||||
"Examples:",
|
||||
"#bots nick!*@* 0 * : Rule to block the user with the nickname 'nick'"
|
||||
" from using any module in the channel #bots forever.",
|
||||
"#bots *!*@*isp.IP 1M1w3h tell : This rule will block any user with"
|
||||
" the domain 'isp.IP' from using the tell module for 1 Month 1 week"
|
||||
" and 3 hours in the channel #bots."
|
||||
]
|
||||
acl_del = [
|
||||
f"Usage: {i.cmd_prefix}acl_del <mask ID>",
|
||||
" Permission: Channel Operators",
|
||||
"Remove a rule from the user access list using its ID. The ID can be"
|
||||
f" retrieved using the command: {i.cmd_prefix}acl_list. Note that the"
|
||||
" IDs might change everytime a rule is deleted. Users can only delete"
|
||||
" delete rules for channels where they have Operator rights. Rules"
|
||||
" that have their channel part set to '*' can only be deleted by the"
|
||||
" bot's owners."
|
||||
]
|
||||
acl_list = [
|
||||
f"Usage: {i.cmd_prefix}acl_list",
|
||||
" Permission: Everyone",
|
||||
"Get a list of every rule set. Rules are numbered. This number is"
|
||||
" their current ID and can be used in the acl_del command."
|
||||
]
|
||||
mod_import = [
|
||||
f"Usage: {i.cmd_prefix}mod_import",
|
||||
" Permission: Owners",
|
||||
"Import any new modules specified in the configuration file and reload"
|
||||
" the currently imported ones."
|
||||
]
|
||||
mod_whitelist_add = [
|
||||
f"Usage: {i.cmd_prefix}mod_whitelist_add <module> <channel>",
|
||||
" Permission: Channel Operators",
|
||||
"Add a channel to a module's whitelist. This will make the module only"
|
||||
" respond to the channels added in this whitelist. You cannot add a"
|
||||
" channel to a modules whitelist if a blacklist is already in place."
|
||||
]
|
||||
mod_blacklist_add = [
|
||||
f"Usage: {i.cmd_prefix}mod_blacklist_add <module> <channel>",
|
||||
" Permission: Channel Operators",
|
||||
"Add a channel to a module's blacklist. This will make the module not"
|
||||
" respond to the channels added in this blacklist. You cannot add a"
|
||||
" channel to a modules blacklist if a whitelist is already in place."
|
||||
]
|
||||
mod_whitelist_del = [
|
||||
f"Usage: {i.cmd_prefix}mod_whitelist_del <module> <channel>",
|
||||
" Permission: Channel Operators",
|
||||
"Delete a channel from a module's whitelist."
|
||||
]
|
||||
mod_blacklist_del = [
|
||||
f"Usage: {i.cmd_prefix}mod_blacklist_del <module> <channel>",
|
||||
" Permission: Channel Operators",
|
||||
"Delete a channel from a module's blacklist."
|
||||
]
|
||||
mod_list = [
|
||||
f"Usage: {i.cmd_prefix}mod_list <channel>",
|
||||
" Permission: Channel Operators",
|
||||
"Get a list of every module that has <channel> in its whitelist or"
|
||||
" blacklist. Bot owners can omit the <channel argument> and get a list"
|
||||
" of all channels that have been in a whitelist or a blacklist."
|
||||
]
|
||||
mod_global_prefix_set = [
|
||||
f"Usage: {i.cmd_prefix}mod_global_prefix_set <prefix>",
|
||||
" Permission: Owners",
|
||||
"Set the default prefix used for commands. This is the character used"
|
||||
" before a command name. E.g.: .command '.' is the prefix here."
|
||||
]
|
||||
mod_channel_prefix_set = [
|
||||
f"Usage: {i.cmd_prefix}mod_channel_prefix_set <channel> <prefix>",
|
||||
" Permission: Channel Operators",
|
||||
"Set the default prefix used for commands in a specific channel. This"
|
||||
" is the character used before a command name. E.g.: .command '.' is"
|
||||
" the prefix here. This prefix overrides the default value."
|
||||
]
|
||||
|
||||
help_d = {
|
||||
"join": join,
|
||||
"part": part,
|
||||
"privmsg": privmsg,
|
||||
"notice": notice,
|
||||
"acl_add": acl_add,
|
||||
"acl_del": acl_del,
|
||||
"acl_list": acl_list,
|
||||
"mod_import": mod_import,
|
||||
"mod_whitelist_add": mod_whitelist_add,
|
||||
"mod_blacklist_add": mod_blacklist_add,
|
||||
"mod_whitelist_del": mod_whitelist_del,
|
||||
"mod_blacklist_del": mod_blacklist_del,
|
||||
"mod_list": mod_list,
|
||||
"mod_global_prefix_set": mod_global_prefix_set,
|
||||
"mod_channel_prefix_set": mod_channel_prefix_set,
|
||||
"admin_help": admin_help
|
||||
}
|
||||
try:
|
||||
cmd_help_list = help_d[i.msg_nocmd]
|
||||
except KeyError:
|
||||
avail_cmd = ""
|
||||
for cmd in help_d:
|
||||
avail_cmd += f"{cmd}, "
|
||||
avail_cmd = avail_cmd[:-3]
|
||||
m = f"Usage: {i.cmd_prefix}admin_help <admin command>"
|
||||
irc.notice(i.nickname, m)
|
||||
m = f"Available commands are: {avail_cmd}"
|
||||
irc.notice(i.nickname, m)
|
||||
return
|
||||
|
||||
for line in cmd_help_list:
|
||||
irc.notice(i.nickname, line)
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
func_d = {
|
||||
"join": join,
|
||||
"part": part,
|
||||
"privmsg": privmsg,
|
||||
"notice": notice,
|
||||
"acl_add": acl_add,
|
||||
"acl_del": acl_del,
|
||||
"acl_list": acl_list,
|
||||
"mod_import": mod_import,
|
||||
"mod_whitelist_add": mod_whitelist_add,
|
||||
"mod_blacklist_add": mod_blacklist_add,
|
||||
"mod_whitelist_del": mod_whitelist_del,
|
||||
"mod_blacklist_del": mod_blacklist_del,
|
||||
"mod_list": mod_list,
|
||||
"mod_global_prefix_set": mod_global_prefix_set,
|
||||
"mod_channel_prefix_set": mod_channel_prefix_set,
|
||||
"admin_help": admin_help
|
||||
}
|
||||
func_d[i.cmd](i, irc)
|
88
src/irc/modules/clock.py
Normal file
88
src/irc/modules/clock.py
Normal file
@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding=utf-8
|
||||
|
||||
# Location module for drastikbot.
|
||||
#
|
||||
# It uses the geonames.org API to get time information about a
|
||||
# country/city/etc.
|
||||
#
|
||||
# Issues: geonames.org is slow and not very reliable, consider
|
||||
# rewriting it to use another API or an offline method.
|
||||
#
|
||||
# You may need to provide your own geonames.org username if the one
|
||||
# provided doesn't work.
|
||||
|
||||
'''
|
||||
Copyright (C) 2018 drastik (https://github.com/olagood)
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
any later version.
|
||||
|
||||
This program 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import urllib.parse
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
class Module():
|
||||
def __init__(self):
|
||||
self.commands = ["time"]
|
||||
self.manual = {
|
||||
"desc": "Get time information about a country, a city, or a state",
|
||||
"bot_commands": {
|
||||
"time": {"usage": lambda x: f"{x}time <country/city/state>"}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
username = "bugmenotuser"
|
||||
|
||||
|
||||
def location_info_from_name(query):
|
||||
api_url = ("http://api.geonames.org/searchJSON?"
|
||||
f"q={query}&maxRows=1&username={username}")
|
||||
r = requests.get(api_url, timeout=30)
|
||||
try:
|
||||
return r.json()["geonames"][0]
|
||||
except IndexError:
|
||||
return False
|
||||
|
||||
|
||||
def get_timezone_from_name(query):
|
||||
j = location_info_from_name(query)
|
||||
if not j:
|
||||
return f'Time: "{query}" is not a valid location'
|
||||
|
||||
lng = j['lng']
|
||||
lat = j['lat']
|
||||
name = j['name']
|
||||
|
||||
api_url = ("http://api.geonames.org/timezoneJSON?"
|
||||
f"lat={lat}&lng={lng}&username={username}")
|
||||
r = requests.get(api_url, timeout=30)
|
||||
j = r.json()
|
||||
|
||||
try:
|
||||
gmtOffset = j['gmtOffset']
|
||||
countryName = j['countryName']
|
||||
time = j['time']
|
||||
except KeyError:
|
||||
return f'Time: "{query}" is not a valid location'
|
||||
|
||||
ret = f"Time in {name}, {countryName}: {time} GMT {gmtOffset}"
|
||||
return ret
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
query = urllib.parse.quote_plus(i.msg_nocmd)
|
||||
irc.privmsg(i.channel, get_timezone_from_name(query))
|
158
src/irc/modules/coinmarketcap.py
Normal file
158
src/irc/modules/coinmarketcap.py
Normal file
@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding=utf-8
|
||||
|
||||
# Cryptocurrency price fetcher for Drastikbot that uses
|
||||
# https://coinmarketcap.com/api/
|
||||
|
||||
'''
|
||||
Copyright (C) 2018 drastik.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
any later version.
|
||||
|
||||
This program 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.commands = ['coin', 'cmc']
|
||||
self.helpmsg = [
|
||||
"Usage: .coin <COIN> [-p, --pair <FIAT/COIN>]",
|
||||
" ",
|
||||
"Get cryptocurrency prices from https://coinmarketcap.com",
|
||||
"The --pair option allows you to also get a Fiat price of the",
|
||||
"specified cryptocurrency.",
|
||||
"Examples: .coin BTC",
|
||||
" .cmc LTC",
|
||||
" .coin ETH -p EUR"]
|
||||
|
||||
|
||||
def fiat2usd_fetch(fiat):
|
||||
f = fiat.upper()
|
||||
u = f"https://api.fixer.io/latest?base={f}&symbols=USD"
|
||||
r = requests.get(u, timeout=5)
|
||||
try:
|
||||
j = r.json()["rates"]["USD"]
|
||||
except KeyError:
|
||||
return False
|
||||
return j
|
||||
|
||||
|
||||
def cmc_fetch(c, p):
|
||||
'''
|
||||
Use coinmarketcap.com API to get the required values.
|
||||
|
||||
Returns:
|
||||
1 : Cryptocurrency requested not found.
|
||||
2 : Fiat requested not found.
|
||||
Tuple : Tuple with the needed values.
|
||||
'''
|
||||
coin = c.upper()
|
||||
url = f"https://api.coinmarketcap.com/v1/ticker/?convert={p}&limit=0"
|
||||
r = requests.get(url, timeout=5)
|
||||
for i in r.json():
|
||||
if not (coin == i["symbol"] or coin == i["name"].upper()):
|
||||
continue
|
||||
name = i["name"]
|
||||
sym = i["symbol"]
|
||||
try:
|
||||
fiat = i[f'price_{p.lower()}']
|
||||
except KeyError:
|
||||
return 2
|
||||
btc = i["price_btc"]
|
||||
c24 = i["percent_change_24h"]
|
||||
if "-" in c24:
|
||||
c24 = f"\x0304{c24} %\x0F"
|
||||
else:
|
||||
c24 = f"\x0303{c24} %\x0F"
|
||||
c7d = i["percent_change_7d"]
|
||||
if "-" in c7d:
|
||||
c7d = f"\x0304{c7d} %\x0F"
|
||||
else:
|
||||
c7d = f"\x0303{c7d} %\x0F"
|
||||
cap = "{:,}".format(float(i[f"market_cap_{p.lower()}"]))
|
||||
cap = f"{cap} {p.upper()}"
|
||||
t_sup = i["total_supply"]
|
||||
p_usd = i["price_usd"]
|
||||
return (name, sym, fiat, btc, cap, c24, c7d, t_sup, p_usd)
|
||||
else:
|
||||
return 1
|
||||
|
||||
|
||||
def query(args):
|
||||
# Get the args list and the commands
|
||||
# Join the list to a string and return
|
||||
_args = args[:]
|
||||
cmds_args = ['--pair', '-p']
|
||||
if '--pair' in args or '-p' in args:
|
||||
try:
|
||||
idx = args.index('--pair')
|
||||
except ValueError:
|
||||
idx = args.index('-p')
|
||||
f = args[idx + 1]
|
||||
else:
|
||||
f = 'usd'
|
||||
for i in cmds_args:
|
||||
try:
|
||||
idx = _args.index(i)
|
||||
del _args[idx]
|
||||
del _args[idx]
|
||||
except ValueError:
|
||||
pass
|
||||
_args = ' '.join(_args)
|
||||
return (_args, f)
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
args = i.msg_nocmd.split()
|
||||
q = query(args)
|
||||
coin_ls = q[0].split()
|
||||
if len(coin_ls) != 1:
|
||||
# If no coin is given, send a help message.
|
||||
return irc.privmsg(i.channel,
|
||||
f"Usage: {i.cmd_prefix}{i.cmd} <COIN> "
|
||||
"[-p, --pair <FIAT/COIN>]"
|
||||
f" | e.g. {i.cmd_prefix}{i.cmd} BTC")
|
||||
coin = coin_ls[0]
|
||||
pair = q[1]
|
||||
res = cmc_fetch(coin, pair)
|
||||
# Invalid input error handling.
|
||||
if res == 1:
|
||||
return irc.privmsg(i.channel, f"\x0311{coin}\x0F is not an available "
|
||||
"cryptocurrency.")
|
||||
elif res == 2:
|
||||
return irc.privmsg(i.channel, f"Pair \x0311{pair}\x0F not found. "
|
||||
"Available Pairs: [All cryptocurrencies] + AUD, "
|
||||
"BRL, CAD, CHF, CLP, CNY, CZK, DKK, EUR, GBP, HKD, "
|
||||
"HUF, IDR, ILS, INR, JPY, KRW, MXN, MYR, NOK, NZD, "
|
||||
"PHP, PKR, PLN, RUB, SEK, SGD, THB, TRY, TWD, ZAR")
|
||||
if pair.lower() != 'usd':
|
||||
f2u = fiat2usd_fetch(pair)
|
||||
if not f2u:
|
||||
f2u = cmc_fetch(pair, 'usd')[2]
|
||||
rel_p = "{:,}".format(float(f2u) * float(res[2]))
|
||||
rel_p = f"\x02Relative price to USD:\x0F \x0311${rel_p} USD\x0F | "
|
||||
else:
|
||||
rel_p = ""
|
||||
# Main flow.
|
||||
p1po = "{:,}".format(float(res[7]) * 0.01 * float(res[8]))
|
||||
p1po = f"\x02Price of 1% ownership:\x0F \x0311${p1po} USD\x0F | "
|
||||
price = "{:,}".format(float(res[2]))
|
||||
irc.privmsg(i.channel, f"\x0311{res[0]} ({res[1]})\x0F: "
|
||||
f"\x02Price\x0F: \x0311{price} {pair.upper()}\x0F"
|
||||
f" , \x0311{res[3]} BTC\x0F"
|
||||
f" | {rel_p} {p1po}"
|
||||
f"\x02Market Cap\x0F: \x0311${res[4]}\x0F"
|
||||
f" | \x02Change (24h)\x0F: {res[5]}"
|
||||
f" | \x02Change (7d)\x0F: {res[6]}")
|
39
src/irc/modules/coronachan.py
Normal file
39
src/irc/modules/coronachan.py
Normal file
@ -0,0 +1,39 @@
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
url = 'https://www.worldometers.info/coronavirus/'
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.commands = ['corona', 'coronachan']
|
||||
self.manual = {
|
||||
"desc": "Useful stats related to 2019-nCov",
|
||||
"bot_commands": {
|
||||
"corona": {"usage": ".corona",
|
||||
"alias": ["coronachan"]},
|
||||
"coronachan": {"usage": ".coronachan",
|
||||
"alias": ["corona"]}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
page = requests.get(url)
|
||||
soup = BeautifulSoup(page.text, "html.parser")
|
||||
cases = extract_from_page("Coronavirus Cases:", soup)
|
||||
deaths = extract_from_page("Deaths:", soup)
|
||||
recovered = extract_from_page("Recovered:", soup)
|
||||
|
||||
irc.privmsg(
|
||||
i.channel,
|
||||
(f"\x02Total cases:\x0F {cases}"
|
||||
f" | \x02Dead:\x0F \x0304{deaths}\x0F"
|
||||
f" | \x02Recovered:\x0F \x0303{recovered}\x0F"
|
||||
f" | \x02Source:\x0F {url}")
|
||||
)
|
||||
|
||||
|
||||
def extract_from_page(text, soup):
|
||||
return soup.find(text=text).parent.parent.find(
|
||||
"div", {"class": "maincounter-number"}).find("span").text
|
68
src/irc/modules/dice.py
Normal file
68
src/irc/modules/dice.py
Normal file
@ -0,0 +1,68 @@
|
||||
# coding=utf-8
|
||||
|
||||
# Virtual D&D style dice rolling
|
||||
|
||||
'''
|
||||
Copyright (c) 2020 Tekdude <tekdude@gmail.com>
|
||||
Copyright (c) 2021 drastik <drastik.org> <derezzed@protonmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
'''
|
||||
|
||||
import random
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.commands = ['roll']
|
||||
self.manual = {
|
||||
"desc": "Virtual D&D style dice rolling.",
|
||||
"bot_commands": {
|
||||
"roll": {
|
||||
"usage": lambda x: f"{x}roll <n>d<s>",
|
||||
"info": ("Rolls <n> number of virtual dice, each with"
|
||||
" <S> number of sides and returns the result"
|
||||
". Example: \".roll 2d6\" rolls two six sided"
|
||||
" dice.")}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
values = i.msg_nocmd.split('d')
|
||||
if len(values) != 2:
|
||||
irc.notice(i.channel, f"Usage: {i.cmd_prefix}roll <n>d<s>")
|
||||
return
|
||||
|
||||
n_dice = int(values[0])
|
||||
n_sides = int(values[1])
|
||||
|
||||
if n_dice > 1000 or n_sides > 1000:
|
||||
irc.notice(i.channel, f"{i.nickname}: Too many dice or sides given.")
|
||||
return
|
||||
|
||||
results = [random.randint(1, n_sides) for i in range(n_dice)]
|
||||
|
||||
limit = 15 # ToDo: Let chanops choose the limit
|
||||
if len(results) > limit:
|
||||
results_p = ", ".join(map(str, results[:limit])) + "..."
|
||||
else:
|
||||
results_p = ", ".join(map(str, results))
|
||||
|
||||
m = (f"{i.nickname} rolled {n_dice} {'die' if n_dice == 1 else 'dice'}"
|
||||
f" with {n_sides} sides: {results_p} (Total: {sum(results)})")
|
||||
|
||||
irc.privmsg(i.channel, m)
|
181
src/irc/modules/events.py
Normal file
181
src/irc/modules/events.py
Normal file
@ -0,0 +1,181 @@
|
||||
# coding=utf-8
|
||||
|
||||
# This is core module for Drastikbot.
|
||||
# It handles events such as JOIN, PART, QUIT, NICK, MODE and updates irc.py's
|
||||
# variables for use by the other modules.
|
||||
|
||||
'''
|
||||
Copyright (C) 2018-2019 drastik.org
|
||||
|
||||
This file is part of drastikbot.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, version 3 only.
|
||||
|
||||
This program 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.msgtypes = ['JOIN', 'QUIT', 'PART', 'MODE', '353',
|
||||
'324']
|
||||
self.auto = True
|
||||
|
||||
|
||||
def dict_prep(irc, msg):
|
||||
'''
|
||||
Prepare 'irc.var.namesdict'
|
||||
This function inserts a new key with the channels name in
|
||||
'irc.var.namesdict' for the other functions to use.
|
||||
'''
|
||||
# Verify that this is indeed a channel before adding it to 'namesdict'.
|
||||
# This is so that we can avoid adding the users (or services) that send
|
||||
# privmsges with to bot.
|
||||
chan_prefix_ls = ['#', '&', '+', '!']
|
||||
if msg.channel[0] not in chan_prefix_ls:
|
||||
return
|
||||
|
||||
if msg.channel not in irc.var.namesdict:
|
||||
irc.var.namesdict[msg.channel] = [[], {}]
|
||||
|
||||
|
||||
def _rpl_namreply_353(irc, msg):
|
||||
dict_prep(irc, msg)
|
||||
namesdict = irc.var.namesdict[msg.channel]
|
||||
namesdict[0] = [msg.cmd_ls[2]]
|
||||
modes = ['~', '&', '@', '%', '+']
|
||||
for i in msg.msg_params.split():
|
||||
if i[:1] in modes:
|
||||
namesdict[1][i[1:]] = [i[:1]]
|
||||
else:
|
||||
namesdict[1][i] = []
|
||||
irc.send(('MODE', msg.channel)) # Reply handled by rpl_channelmodeis
|
||||
|
||||
|
||||
def _rpl_channelmodeis_324(irc, msg):
|
||||
'''Handle reply to: "MODE #channel" to save the channel modes'''
|
||||
channel = msg.cmd_ls[2]
|
||||
m = list(msg.cmd_ls[3][1:])
|
||||
for idx, mode in reversed(list(enumerate(m))):
|
||||
irc.var.namesdict[channel][0].append(mode)
|
||||
|
||||
|
||||
def _join(irc, msg):
|
||||
try:
|
||||
dict_prep(irc, msg)
|
||||
irc.var.namesdict[msg.channel][1][msg.nickname] = []
|
||||
except KeyError:
|
||||
# This occures when first joining a channel for the first time.
|
||||
# We take advantage of this to efficiently:
|
||||
# Get the hostmask and call a function to calculate and set
|
||||
# the irc.var.msg_len variable.
|
||||
if msg.nickname == irc.var.curr_nickname:
|
||||
nick_ls = (msg.nickname, msg.username, msg.hostname)
|
||||
irc.var.bot_hostmask = msg.hostname
|
||||
irc.set_msg_len(nick_ls)
|
||||
|
||||
|
||||
def _part(irc, msg):
|
||||
try:
|
||||
del irc.var.namesdict[msg.channel][1][msg.nickname]
|
||||
except KeyError:
|
||||
# This should not be needed now that @rpl_namreply()
|
||||
# is fixed, but the exception will be monitored for
|
||||
# possible future reoccurance, before it is removed.
|
||||
irc.var.log.debug('KeyError @Events.irc_part(). Err: 01')
|
||||
|
||||
|
||||
def _quit(irc, msg):
|
||||
for chan in irc.var.namesdict:
|
||||
if msg.nickname in irc.var.namesdict[chan][1]:
|
||||
del irc.var.namesdict[chan][1][msg.nickname]
|
||||
|
||||
|
||||
def _nick(irc, msg):
|
||||
for chan in irc.var.namesdict:
|
||||
try:
|
||||
k = irc.var.namesdict[chan][1]
|
||||
k[msg.params] = k.pop(msg.nickname)
|
||||
except KeyError:
|
||||
# This should not be needed now that @rpl_namreply()
|
||||
# is fixed, but the exception will be monitored for
|
||||
# possible future reoccurance, before it is removed.
|
||||
irc.var.log.debug('KeyError @Events.irc_part(). Err: 01')
|
||||
|
||||
|
||||
def user_mode(irc, msg):
|
||||
# MODE used on a user
|
||||
m_dict = {'q': '~', 'a': '&', 'o': '@', 'h': '%', 'v': '+'}
|
||||
channel = msg.cmd_ls[1]
|
||||
m = msg.cmd_ls[2] # '+ooo' or '-vvv'
|
||||
modes = list(m[1:]) # [o,o,o,o]
|
||||
if m[:1] == '+': # Add (+) modes
|
||||
for idx, mode in reversed(list(enumerate(modes))):
|
||||
nick = msg.cmd_ls[3+idx]
|
||||
try:
|
||||
irc.var.namesdict[channel][1][nick].append(
|
||||
m_dict[mode])
|
||||
except KeyError:
|
||||
# This should not be needed now that @rpl_namreply()
|
||||
# is fixed, but the exception will be monitored for
|
||||
# possible future reoccurance, before it is removed.
|
||||
irc.var.log.debug('KeyError @Events.irc_mode(). Err: 01')
|
||||
irc.var.namesdict[channel][1].update({nick: modes[idx]})
|
||||
elif m[:1] == '-': # Remove (-) modes
|
||||
for i, e in reversed(list(enumerate(modes))):
|
||||
try:
|
||||
irc.var.namesdict[channel][1][msg.cmd_ls[3+i]].remove(
|
||||
m_dict[modes[i]])
|
||||
except Exception:
|
||||
irc.var.log.debug('AttributeError @Events.irc_mode(). '
|
||||
'Err: 02')
|
||||
# Quick hack for to avoid crashes in cases where a mode
|
||||
# doesnt use a nickname. (For instance setting +b on a
|
||||
# hostmask). Should be properly handled, after this
|
||||
# method gets broken into smaller parts.
|
||||
|
||||
|
||||
def channel_mode(irc, msg):
|
||||
# MODE used on a channel
|
||||
channel = msg.cmd_ls[1]
|
||||
m = msg.cmd_ls[2] # '+ooo' or '-vvv'
|
||||
modes = list(m[1:]) # [o,o,o,o]
|
||||
if m[:1] == '+': # Add (+) modes
|
||||
for idx, mode in reversed(list(enumerate(modes))):
|
||||
irc.var.namesdict[channel][0].append(mode)
|
||||
elif m[:1] == '-': # Remove (-) modes
|
||||
for idx, mode in reversed(list(enumerate(modes))):
|
||||
irc.var.namesdict[channel][0].remove(mode)
|
||||
|
||||
|
||||
def _mode(irc, msg):
|
||||
dict_prep(irc, msg)
|
||||
if len(msg.cmd_ls) > 3:
|
||||
user_mode(irc, msg)
|
||||
elif msg.cmd_ls[1] == irc.var.curr_nickname:
|
||||
# Set the bot's server modes.
|
||||
irc.var.botmodes.extend(
|
||||
list(msg.msg_ls[3].replace("+", "").replace(":", "")))
|
||||
else:
|
||||
channel_mode(irc, msg)
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
d = {
|
||||
'353': _rpl_namreply_353,
|
||||
'324': _rpl_channelmodeis_324,
|
||||
'JOIN': _join,
|
||||
'PART': _part,
|
||||
'QUIT': _quit,
|
||||
'NICK': _nick,
|
||||
'MODE': _mode
|
||||
}
|
||||
d[i.msgtype](irc, i)
|
203
src/irc/modules/help.py
Normal file
203
src/irc/modules/help.py
Normal file
@ -0,0 +1,203 @@
|
||||
# coding=utf-8
|
||||
|
||||
# Help Module for drastikbot modules.
|
||||
#
|
||||
# This module provides help messages for the loaded drastikbot modules.
|
||||
#
|
||||
# Usage
|
||||
# -----
|
||||
# Calling the command without arguments returns a list of all the
|
||||
# loaded modules with help information. Example: .help
|
||||
#
|
||||
# Giving the name of a module as an argument returns the available
|
||||
# bot_commands of that module. Example: .help text
|
||||
#
|
||||
# Giving the name of a module followed by one of its bot_commands
|
||||
# returns a help message of that command: Example: .help text ae
|
||||
#
|
||||
# API
|
||||
# ---
|
||||
# To provide help messages through this module other modules must
|
||||
# include the manual variable in their Module class. If no such
|
||||
# variable is provided the module will be unlisted.
|
||||
|
||||
|
||||
# Copyright (C) 2021 drastik.org
|
||||
#
|
||||
# This file is part of drastikbot.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, version 3 only.
|
||||
#
|
||||
# This program 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 Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import itertools
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.commands = ["help"]
|
||||
|
||||
|
||||
def get_module_object(i, module_name):
|
||||
# drastikbot v2.2
|
||||
if hasattr(i, "mod"):
|
||||
for mod_object, mod_path in i.mod["modules_d"].values():
|
||||
if mod_path.stem == module_name:
|
||||
return mod_object
|
||||
|
||||
# drastikbot v2.1
|
||||
if hasattr(i, "modules") and module_name in i.modules:
|
||||
return i.modules[module_name]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def hidden_status(i, module_name: str) -> bool:
|
||||
"""Is ``module'' a hidden module?"""
|
||||
|
||||
module_object = get_module_object(i, module_name)
|
||||
if module_object is None:
|
||||
return True # Module not found
|
||||
|
||||
if not hasattr(module_object, "Module"):
|
||||
return True # No Module class, it's hidden
|
||||
|
||||
if hasattr(module_object.Module(), "manual"):
|
||||
return module_object.Module().manual
|
||||
elif hasattr(module_object.Module(), "helpmsg"): # Old method
|
||||
if "__hidden__" in module_object.Module().helpmsg:
|
||||
return "hidden"
|
||||
|
||||
|
||||
def is_hidden(i, module_name):
|
||||
return hidden_status(i, module_name) == "hidden"
|
||||
|
||||
|
||||
def module_checks(i, irc, module):
|
||||
if module not in i.modules.keys():
|
||||
irc.notice(i.channel, f"Help: `{module}' is not an imported module.")
|
||||
return False
|
||||
|
||||
try:
|
||||
module_bl = irc.var.modules_obj["blacklist"][module]
|
||||
if module_bl and i.channel in module_bl:
|
||||
irc.notice(i.channel, f"Help: This module has been disabled.")
|
||||
return False
|
||||
except KeyError:
|
||||
pass # No blacklist, move on
|
||||
|
||||
try:
|
||||
module_wl = irc.var.modules_obj["whitelist"][module]
|
||||
if module_wl and i.channel not in module_wl:
|
||||
irc.notice(i.channel, f"Help: This module has been disabled.")
|
||||
return False
|
||||
except KeyError:
|
||||
pass # No whitelist, move on
|
||||
|
||||
module_c = i.modules[module].Module()
|
||||
if not hasattr(module_c, "manual"):
|
||||
irc.notice(i.channel, "Help: This module does not have a manual.")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def module_help(i, irc, module):
|
||||
if not module_checks(i, irc, module):
|
||||
return
|
||||
|
||||
module_c = i.modules[module].Module()
|
||||
|
||||
commands = ""
|
||||
if hasattr(module_c, "commands"):
|
||||
commands = ", ".join(module_c.commands)
|
||||
commands = f"Commands: {commands} | "
|
||||
|
||||
info = ""
|
||||
if "desc" in module_c.manual:
|
||||
info = module_c.manual["desc"]
|
||||
info = f"Info: {info}"
|
||||
|
||||
t = f"\x0311{module}\x0F: {commands}{info}"
|
||||
t += f" | Use: {i.cmd_prefix}help <module> <command> for command info."
|
||||
irc.notice(i.channel, t)
|
||||
|
||||
|
||||
def command_help(i, irc, module, command):
|
||||
if not module_checks(i, irc, module):
|
||||
return
|
||||
|
||||
module_c = i.modules[module].Module()
|
||||
|
||||
if not hasattr(module_c, "commands"):
|
||||
irc.notice(i.channel, "Help: This module does not provide commands.")
|
||||
return
|
||||
|
||||
if "bot_commands" not in module_c.manual:
|
||||
irc.notice(i.channel, "Help: No manual entry for this command ")
|
||||
return
|
||||
|
||||
command_manual = module_c.manual["bot_commands"]
|
||||
|
||||
if command not in command_manual:
|
||||
irc.notice(i.channel, "Help: No manual entry for this command.")
|
||||
return
|
||||
|
||||
command_entry = command_manual[command]
|
||||
|
||||
t = []
|
||||
|
||||
if "usage" in command_entry:
|
||||
usage = command_entry["usage"](i.cmd_prefix)
|
||||
usage = f"Usage: {usage}"
|
||||
t.append(usage)
|
||||
|
||||
if "info" in command_entry:
|
||||
info = command_entry["info"]
|
||||
info = f"Info: {info}"
|
||||
t.append(info)
|
||||
|
||||
if "alias" in command_entry:
|
||||
alias = ", ".join(command_entry["alias"])
|
||||
alias = f"Aliases: {alias}"
|
||||
t.append(alias)
|
||||
|
||||
t = " | ".join(t)
|
||||
t = f"{command}: {t}"
|
||||
irc.notice(i.channel, t)
|
||||
|
||||
|
||||
def module_list(i, irc):
|
||||
m1 = filter(lambda x: not is_hidden(i, x) and i.whitelist(x, i.channel) \
|
||||
and not i.blacklist(x, i.channel),
|
||||
set(i.command_dict.values()))
|
||||
m2 = filter(lambda x: not is_hidden(i, x) and i.whitelist(x, i.channel) \
|
||||
and not i.blacklist(x, i.channel) and not x in m1,
|
||||
i.auto_list)
|
||||
m = itertools.chain(m1, m2)
|
||||
t = "Help: " + ", ".join(sorted(m))
|
||||
t += f" | Use: {i.cmd_prefix}help <module> for module info."
|
||||
irc.notice(i.channel, t)
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
if i.msg_nocmd:
|
||||
argv = i.msg_nocmd.split()
|
||||
argc = len(argv)
|
||||
if argc == 1:
|
||||
module_help(i, irc, argv[0])
|
||||
elif argc == 2:
|
||||
command_help(i, irc, argv[0], argv[1])
|
||||
else:
|
||||
m = f"Usage: {i.cmd_prefix}help [module] [command]"
|
||||
irc.notice(i.channel, m)
|
||||
else:
|
||||
module_list(i, irc)
|
211
src/irc/modules/ignore.py
Normal file
211
src/irc/modules/ignore.py
Normal file
@ -0,0 +1,211 @@
|
||||
# coding=utf-8
|
||||
|
||||
# This is core module for Drastikbot.
|
||||
# It provides an ignore list to other drastikbot modules and
|
||||
# an interface that allows the users to add or remove other users
|
||||
# from the ignore list.
|
||||
|
||||
'''
|
||||
Copyright (C) 2019 drastik.org
|
||||
|
||||
This file is part of drastikbot.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, version 3 only.
|
||||
|
||||
This program 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import json
|
||||
from user_auth import user_auth
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.commands = ["ignore", "unignore", "ignored", "ignore_mode"]
|
||||
self.helpmsg = [
|
||||
"Usage: .ignore <Nickname>",
|
||||
" .unignore <Nickname>",
|
||||
" .ignored",
|
||||
" .ignore_mode <mode> <value>",
|
||||
" ",
|
||||
"Ignore a list of users. The will not be able to use your",
|
||||
"nickname in the supported modules/commands.",
|
||||
" ",
|
||||
".ignored : The bot will PM you a list of your ignored users.",
|
||||
".ignore_mode : <mode> :",
|
||||
" ignore_all: to ignore everyone.",
|
||||
" registered_only: to ignore unidentified users.",
|
||||
" <value> : True / False",
|
||||
"Example: .ignore_mode registered_only true"
|
||||
]
|
||||
|
||||
|
||||
logo = "\x02ignore\x0F"
|
||||
|
||||
|
||||
def is_ignored(i, irc, user_nick, ignore_nick):
|
||||
dbc = i.db[1].cursor()
|
||||
try:
|
||||
dbc.execute("SELECT settings FROM ignore WHERE user = ?;",
|
||||
(user_nick,))
|
||||
j = dbc.fetchone()[0]
|
||||
o = json.loads(j)
|
||||
if o["ignore_all"]:
|
||||
return True
|
||||
elif o["registered_only"] and not user_auth(i, irc, ignore_nick):
|
||||
return True
|
||||
elif ignore_nick in o["ignored"]:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def ignore(dbc, user_nick, ignore_nick):
|
||||
try:
|
||||
dbc.execute("SELECT settings FROM ignore WHERE user = ?;",
|
||||
(user_nick,))
|
||||
j = dbc.fetchone()[0]
|
||||
o = json.loads(j)
|
||||
if ignore_nick not in o["ignored"]:
|
||||
o["ignored"].append(ignore_nick)
|
||||
else:
|
||||
return (f"{logo}: \x0311{ignore_nick}\x0F"
|
||||
" is already in your ignore list.")
|
||||
j = json.dumps(o)
|
||||
dbc.execute("UPDATE ignore SET settings = ? WHERE user = ?;",
|
||||
(j, user_nick))
|
||||
return (f"{logo}: \x0311{ignore_nick}\x0F "
|
||||
"was added to your ignore list.")
|
||||
except Exception:
|
||||
return (f"{logo}: An error occured while adding the user {ignore_nick}"
|
||||
" to your ignore list")
|
||||
|
||||
|
||||
def unignore(dbc, user_nick, unignore_nick):
|
||||
try:
|
||||
dbc.execute("SELECT settings FROM ignore WHERE user = ?;",
|
||||
(user_nick,))
|
||||
j = dbc.fetchone()[0]
|
||||
o = json.loads(j)
|
||||
o["ignored"].remove(unignore_nick)
|
||||
j = json.dumps(o)
|
||||
dbc.execute("UPDATE ignore SET settings = ? WHERE user = ?;",
|
||||
(j, user_nick))
|
||||
return (f"{logo}: \x0311{unignore_nick}\x0F"
|
||||
" was removed from your ignore list.")
|
||||
except Exception:
|
||||
return (f"{logo}: \x0311{unignore_nick}\x0F"
|
||||
" does not exist in your ignore list.")
|
||||
|
||||
|
||||
def ignored(dbc, user_nick):
|
||||
try:
|
||||
dbc.execute("SELECT settings FROM ignore WHERE user = ?;",
|
||||
(user_nick,))
|
||||
j = dbc.fetchone()[0]
|
||||
o = json.loads(j)
|
||||
ret = ""
|
||||
for i in o["ignored"]:
|
||||
ret = f"{ret}{i}, "
|
||||
return f'{logo}: {len(o["ignored"])} : {ret[:-2]}'
|
||||
except Exception:
|
||||
return f"{logo}: There are no ignored users."
|
||||
|
||||
|
||||
def registered_only(dbc, user_nick, value):
|
||||
try:
|
||||
dbc.execute("SELECT settings FROM ignore WHERE user = ?;",
|
||||
(user_nick,))
|
||||
j = dbc.fetchone()[0]
|
||||
o = json.loads(j)
|
||||
o["registered_only"] = value
|
||||
j = json.dumps(o)
|
||||
dbc.execute("UPDATE ignore SET settings = ? WHERE user = ?;",
|
||||
(j, user_nick))
|
||||
return f'{logo}: registered_only mode set to "{value}".'
|
||||
except Exception:
|
||||
return ("{logo}: An error occured while "
|
||||
"changing the registered_only mode.")
|
||||
|
||||
|
||||
def ignore_all(dbc, user_nick, value):
|
||||
try:
|
||||
dbc.execute("SELECT settings FROM ignore WHERE user = ?;",
|
||||
(user_nick,))
|
||||
j = dbc.fetchone()[0]
|
||||
o = json.loads(j)
|
||||
o["ignore_all"] = value
|
||||
j = json.dumps(o)
|
||||
dbc.execute("UPDATE ignore SET settings = ? WHERE user = ?;",
|
||||
(j, user_nick))
|
||||
return f'{logo}: ignore_all mode set to "{value}".'
|
||||
except Exception:
|
||||
return "{logo}: An error occured while changing the ignore_all mode."
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
dbc = i.db[1].cursor()
|
||||
dbc.execute("CREATE TABLE IF NOT EXISTS ignore "
|
||||
"(user TEXT COLLATE NOCASE PRIMARY KEY, "
|
||||
"settings TEXT COLLATE NOCASE);")
|
||||
dbc.execute(
|
||||
"INSERT OR IGNORE INTO ignore VALUES (?, ?);",
|
||||
(i.nickname,
|
||||
'{"ignored": [], "registered_only": false, "ignore_all": false}'))
|
||||
|
||||
args = i.msg_nocmd.split()
|
||||
|
||||
if i.cmd == "ignore" and len(args) == 1:
|
||||
if i.msg_nocmd.lower() == i.nickname.lower():
|
||||
return irc.privmsg(i.channel,
|
||||
f"{logo}: You cannot ignore yourself.")
|
||||
elif i.msg_nocmd.lower() == irc.var.curr_nickname.lower():
|
||||
return irc.privmsg(i.channel,
|
||||
f"{logo}: Ignoring the bot has no effect.")
|
||||
return irc.privmsg(i.channel, ignore(dbc, i.nickname, i.msg_nocmd))
|
||||
elif i.cmd == "unignore" and len(args) == 1:
|
||||
return irc.privmsg(i.channel, unignore(dbc, i.nickname, i.msg_nocmd))
|
||||
elif i.cmd == "ignored" and not i.msg_nocmd:
|
||||
return irc.privmsg(i.nickname, ignored(dbc, i.nickname))
|
||||
elif i.cmd == "ignore_mode" and i.msg_nocmd:
|
||||
if args[0] == "registered_only":
|
||||
if args[1].lower() == "true":
|
||||
return irc.privmsg(i.channel,
|
||||
registered_only(dbc, i.nickname, True))
|
||||
elif args[1].lower() == "false":
|
||||
return irc.privmsg(i.channel,
|
||||
registered_only(dbc, i.nickname, False))
|
||||
else:
|
||||
return irc.privmsg(
|
||||
i.channel,
|
||||
"Usage: .ignore_mode registered_only < true || false >")
|
||||
elif args[0] == "ignore_all":
|
||||
if args[1].lower() == "true":
|
||||
return irc.privmsg(i.channel,
|
||||
ignore_all(dbc, i.nickname, True))
|
||||
elif args[1].lower() == "false":
|
||||
return irc.privmsg(i.channel,
|
||||
ignore_all(dbc, i.nickname, False))
|
||||
else:
|
||||
return irc.privmsg(
|
||||
i.channel,
|
||||
"Usage: .ignore_mode ignore_all < true || false >")
|
||||
else:
|
||||
return irc.privmsg(
|
||||
i.channel,
|
||||
'Use ".help ignore" to learn how to use this command.')
|
||||
else:
|
||||
return irc.privmsg(
|
||||
i.channel,
|
||||
'Use ".help ignore" to learn how to use this command.')
|
||||
i.db[1].commit()
|
43
src/irc/modules/information.py
Normal file
43
src/irc/modules/information.py
Normal file
@ -0,0 +1,43 @@
|
||||
# coding=utf-8
|
||||
|
||||
# This is a core module for Drastikbot.
|
||||
# It returns information about the bot to it's users.
|
||||
|
||||
'''
|
||||
Copyright (C) 2019 drastik.org
|
||||
|
||||
This file is part of drastikbot.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, version 3 only.
|
||||
|
||||
This program 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.commands = ['bots', 'source']
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
if i.cmd == "bots":
|
||||
m = (f"\x0305,01drastikbot {irc.var.version}\x0F"
|
||||
" | \x0305Python 3.6\x0F"
|
||||
" | \x0305GNU AGPLv3 ONLY\x0F"
|
||||
" | \x0311http://drastik.org/drastikbot")
|
||||
irc.privmsg(i.channel, m)
|
||||
elif i.cmd == "source":
|
||||
if not i.msg_nocmd or i.msg_nocmd == irc.var.curr_nickname:
|
||||
m = ("\x0305,01drastikbot\x0F"
|
||||
" : \x0311https://github.com/olagood/drastikbot\x0F"
|
||||
" | \x0305,01Modules\x0F"
|
||||
" : \x0311https://github.com/olagood/drastikbot_modules\x0F")
|
||||
irc.privmsg(i.channel, m)
|
298
src/irc/modules/kernel.py
Normal file
298
src/irc/modules/kernel.py
Normal file
@ -0,0 +1,298 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding=utf-8
|
||||
|
||||
# Quotes from various historical versions of the Linux kernel
|
||||
|
||||
'''
|
||||
Copyright (C) 2021 Flisk <flisk@fastmail.de>
|
||||
Copyright (C) 2021 drastik.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
any later version.
|
||||
|
||||
This program 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import random
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.commands = ['kernel']
|
||||
self.manual = {
|
||||
"desc": (
|
||||
"Post quotes from various historical versions of the"
|
||||
" Linux kernel http://www.schwarzvogel.de/software/misc.html"
|
||||
),
|
||||
"bot_commands": {
|
||||
"usage": lambda x: f"{x}kernel"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Taken from the Kernelcookies fortune file curated by Tobias
|
||||
# Klausmann. These are quotes from various historical versions of the
|
||||
# Linux kernel.
|
||||
talk = [
|
||||
"/* This card is _fucking_ hot... */",
|
||||
# linux-2.6.6/drivers/net/sunhme.c
|
||||
|
||||
"If you don't see why, please stay the fuck away from my code.",
|
||||
# Rusty, in linux-2.6.6/Documentation/DocBook/kernel-locking.tmpl
|
||||
|
||||
"/* Fuck, we are miserable poor guys... */",
|
||||
# linux-2.6.6/net/xfrm/xfrm_algo.c
|
||||
|
||||
"/* Ugly, ugly fucker. */",
|
||||
# linux-2.6.6/include/linux/netfilter_ipv4/ipt_limit.h
|
||||
|
||||
"/* Remember: \"Different name, same old buggy as shit hardware.\" */",
|
||||
# linux-2.6.6/drivers/net/sunhme.c
|
||||
|
||||
"/* This is total bullshit: */",
|
||||
# linux-2.6.6/drivers/video/sis/init301.c
|
||||
|
||||
"/* The HME is the biggest piece of shit I have ever seen. */",
|
||||
# linux-2.6.6/drivers/scsi/esp.h
|
||||
|
||||
"printk(\"WE HAVE A BUG HERE!!! stk=0x%p\\n\", stk);",
|
||||
# linux-2.6.6/drivers/block/cciss_scsi.c
|
||||
|
||||
"printk(\"%s: huh ? Who issued this format command ?\\n\")",
|
||||
# linux-2.6.6/drivers/block/ps2esdi.c
|
||||
|
||||
"printk(\"whoops, seeking 0\\n\");",
|
||||
# linux-2.6.6/drivers/block/swim3.c
|
||||
|
||||
"printk(\"GSCD: magic ...\\n\");",
|
||||
# linux-2.6.6/drivers/cdrom/gscd.c
|
||||
|
||||
"printk(\" (Read error)\"); /* Bitch about the problem. */",
|
||||
# linux-2.6.6/drivers/cdrom/mcd.c
|
||||
|
||||
"printk(\" Speed now 1x\"); /* Pull my finger! */",
|
||||
# linux-2.6.6/drivers/cdrom/mcd.c
|
||||
|
||||
"panic(\"Alas, I survived.\\n\");",
|
||||
# linux-2.6.6/arch/ppc64/kernel/rtas.c
|
||||
|
||||
"panic(\"smp_callin() AAAAaaaaahhhh....\\n\");",
|
||||
# linux-2.6.6/arch/parisc/kernel/smp.c
|
||||
|
||||
"panic(\"Yeee, unsupported cache architecture.\");",
|
||||
# linux-2.6.6/arch/mips/mm/cache.c
|
||||
|
||||
"panic(\"\\n\");",
|
||||
# linux-2.6.6/arch/mips/tx4927/common/tx4927_irq.c
|
||||
|
||||
"panic(\"%s called. This Does Not Happen (TM).\", __FUNCTION__);",
|
||||
# linux-2.6.6/arch/mips/mm-64/tlb-dbg-r4k.c
|
||||
|
||||
"printk (KERN_ERR \"%s: Oops - your private data area is hosed!\\n\", ...)",
|
||||
# linux-2.6.6/drivers/net/ewrk3.c
|
||||
|
||||
"rio_dprintk (RIO_DEBUG_ROUTE, \"LIES! DAMN LIES! %d LIES!\\n\",Lies);",
|
||||
# linux-2.6.6/drivers/char/rio/rioroute.c
|
||||
|
||||
"printk(\"NONONONOO!!!!\\n\");",
|
||||
# linux-2.6.6/drivers/atm/zatm.c
|
||||
|
||||
"printk(\"@#*$!!!! (%08x)\\n\", ...)",
|
||||
# linux-2.6.6/drivers/atm/zatm.c
|
||||
|
||||
"fs_dprintk (FS_DEBUG_INIT, \"Ha! Initialized OK!\\n\");",
|
||||
# linux-2.6.6/drivers/atm/firestream.c
|
||||
|
||||
"DPRINTK(\"strange things happen ...\\n\");",
|
||||
# linux-2.6.6/drivers/atm/eni.c
|
||||
|
||||
"printk(KERN_WARNING \"Hey who turned the DMA off?\\n\");",
|
||||
# linux-2.6.6/drivers/net/wan/z85230.c
|
||||
|
||||
"printk(KERN_DEBUG \"%s: BUG... transmitter died. Kicking it.\\n\",...)",
|
||||
# linux-2.6.6/drivers/net/acenic.c
|
||||
|
||||
"printk(KERN_ERR \"%s: Something Wicked happened! %4.4x.\\n\",...);",
|
||||
# linux-2.6.6/drivers/net/sundance.c
|
||||
|
||||
"DPRINTK(\"FAILURE, CAPUT\\n\");",
|
||||
# linux-2.6.6/drivers/net/tokenring/ibmtr.c
|
||||
|
||||
"Dprintk(\"oh dear, we are idle\\n\");",
|
||||
# linux-2.6.6/drivers/net/ns83820.c
|
||||
|
||||
"printk(KERN_DEBUG \"%s: burped during tx load.\\n\", dev->name)",
|
||||
# linux-2.6.6/drivers/net/3c501.c
|
||||
|
||||
"printk(KERN_DEBUG \"%s: I'm broken.\\n\", dev->name);",
|
||||
# linux-2.6.6/drivers/net/plip.c
|
||||
|
||||
"printk(\"%s: TDR is ga-ga (status %04x)\\n\", ...);",
|
||||
# linux-2.6.6/drivers/net/eexpress.c
|
||||
|
||||
"printk(\"3c505 is sulking\\n\");",
|
||||
# linux-2.6.6/drivers/net/3c505.c
|
||||
|
||||
"printk(KERN_ERR \"happy meal: Transceiver and a coke please.\");",
|
||||
# linux-2.6.6/drivers/net/sunhme.c
|
||||
|
||||
"printk(\"Churning and Burning -\");",
|
||||
# linux-2.6.6/drivers/char/lcd.c
|
||||
|
||||
"printk (KERN_DEBUG \"Somebody wants the port\\n\");",
|
||||
# linux-2.6.6/drivers/parport/parport_pc.c
|
||||
|
||||
"printk(KERN_WARNING MYNAM \": (time to go bang on somebodies door)\\n\");",
|
||||
# linux-2.6.6/drivers/message/fusion/mptctl.c
|
||||
|
||||
"printk(\"NULL POINTER IDIOT\\n\");",
|
||||
# linux-2.6.6/drivers/media/dvb/dvb-core/dvb_filter.c
|
||||
|
||||
"dprintk(5, KERN_DEBUG \"Jotti is een held!\\n\");",
|
||||
# linux-2.6.6/drivers/media/video/zoran_card.c
|
||||
|
||||
"printk(KERN_CRIT \"Whee.. Swapped out page in kernel page table\\n\");",
|
||||
# linux-2.6.6/mm/vmalloc.c
|
||||
|
||||
"printk(\"----------- [cut here ] --------- [please bite here ] ---------\\n\");",
|
||||
# linux-2.6.6/arch/x86_64/kernel/traps.
|
||||
|
||||
"printk (KERN_ALERT \"You are screwed! \" ...);",
|
||||
# linux-2.6.6/arch/i386/kernel/efi.c
|
||||
|
||||
"printk(\"you lose buddy boy...\\n\");",
|
||||
# linux-2.6.6/arch/sparc/kernel/traps.c
|
||||
|
||||
"printk (\"Barf\\n\");",
|
||||
# linux-2.6.6/arch/v850/kernel/module.c
|
||||
|
||||
"printk(KERN_EMERG \"PCI: Tell willy he's wrong\\n\");",
|
||||
# linux-2.6.6/arch/parisc/kernel/pci.c
|
||||
|
||||
"printk(KERN_ERR \"Danger Will Robinson: failed to re-trigger IRQ%d\\n\", irq);",
|
||||
# linux-2.6.6/arch/arm/common/sa1111.c
|
||||
|
||||
"printk(\"; crashing the system because you wanted it\\n\");",
|
||||
# linux-2.6.6/fs/hpfs/super.c
|
||||
|
||||
"panic(\"sun_82072_fd_inb: How did I get here?\");",
|
||||
# linux-2.2.16/include/asm-sparc/floppy.h
|
||||
|
||||
"printk(KERN_ERR \"msp3400: chip reset failed, penguin on i2c bus?\\n\");",
|
||||
# linux-2.2.16/drivers/char/msp3400.c
|
||||
|
||||
"die_if_kernel(\"Whee... Hello Mr. Penguin\", current->tss.kregs);",
|
||||
# linux-2.2.16/arch/sparc/kernel/traps.c
|
||||
|
||||
"die_if_kernel(\"Penguin instruction from Penguin mode??!?!\", regs);",
|
||||
# linux-2.2.16/arch/sparc/kernel/traps.c
|
||||
|
||||
"printk(\"Entering UltraSMPenguin Mode...\\n\");",
|
||||
# linux-2.2.16/arch/sparc64/kernel/smp.c
|
||||
|
||||
"panic(\"mother...\");",
|
||||
# linux-2.2.16/drivers/block/cpqarray.c
|
||||
|
||||
"panic(\"Foooooooood fight!\");",
|
||||
# linux-2.2.16/drivers/scsi/aha1542.c
|
||||
|
||||
"panic(\"huh?\\n\");",
|
||||
# linux-2.2.16/arch/i386/kernel/smp.c
|
||||
|
||||
"panic(\"Oh boy, that early out of memory?\");",
|
||||
# linux-2.2.16/arch/mips/mm/init.c
|
||||
|
||||
"panic(\"CPU too expensive - making holiday in the ANDES!\");",
|
||||
# linux-2.2.16/arch/mips/kernel/traps.c
|
||||
|
||||
"printk(\"Illegal format on cdrom. Pester manufacturer.\\n\"); ",
|
||||
# linux-2.2.16/fs/isofs/inode.c
|
||||
|
||||
"/* Fuck me gently with a chainsaw... */",
|
||||
# linux-2.0.38/arch/sparc/kernel/ptrace.c
|
||||
|
||||
"/* Binary compatibility is good American knowhow fuckin' up. */",
|
||||
# linux-2.2.16/arch/sparc/kernel/sunos_ioctl.c
|
||||
|
||||
"/* Am I fucking pedantic or what? */",
|
||||
# linux-2.2.16/drivers/scsi/qlogicpti.h
|
||||
|
||||
"panic(\"Aarggh: attempting to free lock with active wait queue - shoot Andy\");",
|
||||
# linux-2.0.38/fs/locks.c
|
||||
|
||||
"panic(\"bad_user_access_length executed (not cool, dude)\");",
|
||||
# linux-2.0.38/kernel/panic.c
|
||||
|
||||
"printk(\"ufs_read_super: fucking Sun blows me\\n\");",
|
||||
# linux-2.0.38/fs/ufs/ufs_super.c
|
||||
|
||||
"printk(\"autofs: Out of inode numbers -- what the heck did you do??\\n\"); ",
|
||||
# linux-2.0.38/fs/autofs/root.c
|
||||
|
||||
"HARDFAIL(\"Not enough magic.\");",
|
||||
# linux-2.4.0-test2/drivers/block/nbd.c
|
||||
|
||||
"#ifdef STUPIDLY_TRUST_BROKEN_PCMD_ENA_BIT",
|
||||
# linux-2.4.0-test2/drivers/ide/cmd640.c
|
||||
|
||||
"/* Fuck. The f-word is here so you can grep for it :-) */",
|
||||
# linux-2.4.3/include/asm-mips/mmu_context.h
|
||||
|
||||
"panic (\"No CPUs found. System halted.\\n\");",
|
||||
# linux-2.4.3/arch/parisc/kernel/setup.c
|
||||
|
||||
"printk(\"What? oldfid != cii->c_fid. Call 911.\\n\");",
|
||||
# linux-2.4.3/fs/coda/cnode.c
|
||||
|
||||
"printk(\"Cool stuff's happening!\\n\")",
|
||||
# linux-2.4.3/fs/jffs/intrep.c
|
||||
|
||||
"printk(\"MASQUERADE: No route: Rusty's brain broke!\\n\");",
|
||||
# linux-2.4.3/net/ipv4/netfilter/ipt_MASQUERADE.c
|
||||
|
||||
"printk(\"CPU[%d]: Sending penguins to jail...\",smp_processor_id());",
|
||||
# linux-2.4.8/arch/sparc64/kernel/smp.c
|
||||
|
||||
"printk(\"CPU[%d]: Giving pardon to imprisoned penguins\\n\", smp_processor_id());",
|
||||
# linux-2.4.8/arch/sparc64/kernel/smp.c
|
||||
|
||||
"printk (KERN_INFO \"NM256: Congratulations. You're not running Eunice.\\n\");",
|
||||
# linux-2.6.19/sound/oss/nm256_audio.c
|
||||
|
||||
"printk (KERN_ERR \"NM256: Fire in the hole! Unknown status 0x%x\\n\", ...);",
|
||||
# linux-2.6.19/sound/oss/nm256_audio.c
|
||||
|
||||
"printk(\"Pretending it's a 3/80, but very afraid...\\n\");",
|
||||
# linux-2.6.19/arch/m68k/sun3x/prom.c
|
||||
|
||||
"printk(\"IOP: oh my god, they killed the ISM IOP!\\n\");",
|
||||
# linux-2.6.19/arch/m68k/mac/iop.c
|
||||
|
||||
"printk(\"starfire_translate: Are you kidding me?\\n\");",
|
||||
# linux-2.6.19/arch/sparc64/kernel/starfire.c
|
||||
|
||||
"raw_printk(\"Oops: bitten by watchdog\\n\");",
|
||||
# linux-2.6.19/arch/cris/arch-v32/kernel/time.c
|
||||
|
||||
"prom_printf(\"No VAC. Get some bucks and buy a real computer.\");",
|
||||
# linux-2.6.19/arch/sparc/mm/sun4c.c
|
||||
|
||||
"printk(KERN_ERR \"happy meal: Eieee, rx config register gets greasy fries.\\n\");",
|
||||
# linux-2.6.19/drivers/net/sunhme.c
|
||||
|
||||
"dprintk(\"NFSD: laundromat service - starting\\n\");"
|
||||
# linux-2.6.19/fs/nfsd/nfs4state.c
|
||||
]
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
msg = f"{i.nickname}: {random.SystemRandom().choice(talk)}"
|
||||
irc.privmsg(i.channel, msg)
|
488
src/irc/modules/lainstream.py
Normal file
488
src/irc/modules/lainstream.py
Normal file
@ -0,0 +1,488 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding=utf-8
|
||||
|
||||
# Lainstream for Drastikbot
|
||||
#
|
||||
# Update and get information about lainchan radio and video streams.
|
||||
#
|
||||
# Depends:
|
||||
# - requests :: $ pip3 install requests
|
||||
|
||||
# © 2018-2021 All Rights Reserved olagood (drastik) <derezzed@protonmail.com>
|
||||
|
||||
import pickle
|
||||
import xml.etree.ElementTree as ET
|
||||
# from bs4 import BeautifulSoup
|
||||
import requests
|
||||
|
||||
|
||||
lainchan_org_logo = "\x0304Lainstream\x0F"
|
||||
lainchan_org_logo_rtmp = "\x0304Lainstream\x0F RTMP"
|
||||
lainchan_org_logo_ogv = "\x0304Lainstream\x0F OGV"
|
||||
|
||||
lain_la_logo = "lain.la"
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.commands = ["stream",
|
||||
"radio",
|
||||
# lainchan.org
|
||||
"streamset", "streamset-rtmp", "streamset-ogv",
|
||||
"streamerset", "streamerset-rtmp", "streamerset-ogv",
|
||||
"streamunset", "streamunset-rtmp", "streamunset-ogv",
|
||||
"streamurls",
|
||||
# lain.la
|
||||
"streamset-la", "streamerset-la", "streamunset-la"]
|
||||
self.manual = {
|
||||
"desc": "Stream and radio information for lainchan.org.",
|
||||
"bot_commands": {
|
||||
"stream": {"usage": lambda x: f"{x}stream",
|
||||
"info": "Display the currently active streams."},
|
||||
"radio": {"usage": lambda x: f"{x}radio",
|
||||
"info": "Station information for lainchan radio."},
|
||||
"streamset": {
|
||||
"usage": lambda x: f"{x}streamset [title]",
|
||||
"info": ("Set the stream title for lainchan's RTMP"
|
||||
" stream. The streamer is also set to the"
|
||||
" nickname of the caller. If no title is"
|
||||
" provided, it unsets the stream."),
|
||||
"alias": ["streamset-rtmp"]},
|
||||
"streamset-rtmp": {
|
||||
"usage": lambda x: f"{x}streamset-rtmp [title]",
|
||||
"info": ("Set the stream title for lainchan's RTMP"
|
||||
" stream. The streamer is also set to the"
|
||||
" nickname of the caller. If no title is"
|
||||
" provided, it unsets the stream."),
|
||||
"alias": ["streamset"]},
|
||||
"streamset-ogv": {
|
||||
"usage": lambda x: f"{x}streamset-ogv [title]",
|
||||
"info": ("Set the stream title for lainchan's OGV"
|
||||
" stream. The streamer is also set to the"
|
||||
" nickname of the caller. If no title is"
|
||||
" provided, it unsets the stream.")},
|
||||
"streamerset": {
|
||||
"usage": lambda x: f"{x}streamerset <name>",
|
||||
"info": "Set the streamer for lainchan's RTMP stream",
|
||||
"alias": ["streamerset-rtmp"]},
|
||||
"streamerset-rtmp": {
|
||||
"usage": lambda x: f"{x}streamerset-rtmp <name>",
|
||||
"info": "Set the streamer for lainchan's RTMP stream",
|
||||
"alias": ["streamerset"]},
|
||||
"streamerset-ogv": {
|
||||
"usage": lambda x: f"{x}streamerset-ogv <name>",
|
||||
"info": "Set the streamer for lainchan's OGV stream"},
|
||||
"streamunset": {
|
||||
"usage": lambda x: f"{x}streamunset",
|
||||
"info": ("Unset the title and the streamer for lainchan's"
|
||||
" RTMP stream."),
|
||||
"alias": ["streamunset-rtmp"]},
|
||||
"streamunset-rtmp": {
|
||||
"usage": lambda x: f"{x}streamunset-rtmp",
|
||||
"info": ("Unset the title and the streamer for lainchan's"
|
||||
" RTMP stream."),
|
||||
"alias": ["streamunset"]},
|
||||
"streamunset-ogv": {
|
||||
"usage": lambda x: f"{x}streamunset-ogv",
|
||||
"info": ("Unset the title and the streamer for lainchan's"
|
||||
" OGV stream.")},
|
||||
"streamurls": {
|
||||
"usage": lambda x: f"{x}streamurls",
|
||||
"info": "Show lainchan's RTMP, HLS and OGV stream urls."},
|
||||
"streamset-la": {
|
||||
"usage": lambda x: f"{x}streamset-la [title]",
|
||||
"info": ("Set the stream title for the lain.la"
|
||||
" stream. The streamer is also set to the"
|
||||
" nickname of the caller. If no title is"
|
||||
" provided, it unsets the stream.")},
|
||||
"streamerset-la": {
|
||||
"usage": lambda x: f"{x}streamerset <name>",
|
||||
"info": "Set the streamer for lain.la's stream"},
|
||||
"streamunset-la": {
|
||||
"usage": lambda x: f"{x}streamunset-la",
|
||||
"info": ("Unset the title and the streamer for lain.la's"
|
||||
" stream.")}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ====================================================================
|
||||
# IRC: Arisu bot control
|
||||
# ====================================================================
|
||||
|
||||
def arisu_streamerset_rtmp(irc, streamer):
|
||||
irc.privmsg("Arisu", f"!streamerset {streamer}")
|
||||
|
||||
|
||||
def arisu_streamset_rtmp(irc, title, streamer):
|
||||
irc.privmsg("Arisu", f"!streamset {title}")
|
||||
irc.privmsg("Arisu", f"!streamerset {streamer}")
|
||||
|
||||
|
||||
def arisu_streamunset_rtmp(irc):
|
||||
irc.privmsg("Arisu", "!streamunset")
|
||||
irc.privmsg("Arisu", "!streamerunset")
|
||||
|
||||
|
||||
def arisu_streamerset_ogv(irc, streamer):
|
||||
irc.privmsg("Arisu", f"!streamerset --ogv {streamer}")
|
||||
|
||||
|
||||
def arisu_streamset_ogv(irc, title, streamer):
|
||||
irc.privmsg("Arisu", f"!streamset --ogv {title}")
|
||||
irc.privmsg("Arisu", f"!streamerset --ogv {streamer}")
|
||||
|
||||
|
||||
def arisu_streamunset_ogv(irc):
|
||||
irc.privmsg("Arisu", "!streamunset --ogv")
|
||||
irc.privmsg("Arisu", "!streamerunset --ogv")
|
||||
|
||||
|
||||
# ====================================================================
|
||||
# Radio: lainon.life
|
||||
# ====================================================================
|
||||
|
||||
# IRC callbacks
|
||||
|
||||
def radio(state, i, irc):
|
||||
m = "\x0306Lainchan Radio\x0F: "
|
||||
urls = ("https://lainon.life/playlist/cyberia.json",
|
||||
"https://lainon.life/playlist/cafe.json",
|
||||
"https://lainon.life/playlist/everything.json",
|
||||
"https://lainon.life/playlist/swing.json")
|
||||
for url in urls:
|
||||
channel = url.rsplit("/")[-1][:-5]
|
||||
r = requests.get(url, timeout=10)
|
||||
j = r.json()
|
||||
# live = j['stream_data']['live']
|
||||
c_artist = j['current']['artist']
|
||||
c_title = j['current']['title']
|
||||
listeners = j['listeners']['current']
|
||||
m += (f"\x0305{channel} {listeners}\x0F:"
|
||||
f" \x0302{c_artist} - {c_title}\x0F | https://lainon.life/")
|
||||
irc.privmsg(i.channel, m)
|
||||
|
||||
|
||||
# ====================================================================
|
||||
# Video: lainchan.org
|
||||
# ====================================================================
|
||||
|
||||
# State functions
|
||||
|
||||
def lainchan_org_rtmp_viewers():
|
||||
url = "https://lainchan.org:8080/stat"
|
||||
r = requests.get(url, timeout=10)
|
||||
xml_root = ET.fromstring(r.text)
|
||||
viewers = 0
|
||||
count = 0
|
||||
for i in xml_root.iter("live"):
|
||||
nclients = i.find("nclients")
|
||||
viewers += int(nclients.text)
|
||||
if viewers >= 2 and count == 0:
|
||||
viewers -= 2
|
||||
count += 1
|
||||
return viewers
|
||||
|
||||
|
||||
def lainchan_org_rtmp_status(state):
|
||||
title = state["lainchan.org"]["rtmp"]["title"]
|
||||
if not title:
|
||||
return ""
|
||||
viewers = lainchan_org_rtmp_viewers()
|
||||
streamer = state["lainchan.org"]["rtmp"]["streamer"]
|
||||
url = state["lainchan.org"]["url"]
|
||||
rtmp_url = state["lainchan.org"]["rtmp"]["url"]
|
||||
return (f"{lainchan_org_logo_rtmp}:"
|
||||
f" \x0311{title}\x0F by \x0302{streamer}\x0F"
|
||||
f" | Viewers: {viewers} | Watch at: {url} | RTMP: {rtmp_url}")
|
||||
|
||||
|
||||
def lainchan_org_ogv_status(state):
|
||||
title = state["lainchan.org"]["ogv"]["title"]
|
||||
if not title:
|
||||
return ""
|
||||
streamer = state["lainchan.org"]["ogv"]["streamer"]
|
||||
url = state["lainchan.org"]["url"]
|
||||
ogv_url = state["lainchan.org"]["ogv"]["url"]
|
||||
return (f"{lainchan_org_logo_ogv}:"
|
||||
f" \x0311{title}\x0F by \x0302{streamer}\x0F"
|
||||
f" | Watch at: {url} | OGV: {ogv_url}")
|
||||
|
||||
|
||||
def lainchan_org_rtmp_set(state, title, streamer):
|
||||
state["lainchan.org"]["rtmp"]["title"] = title
|
||||
state["lainchan.org"]["rtmp"]["streamer"] = streamer
|
||||
return state
|
||||
|
||||
|
||||
def lainchan_org_ogv_set(state, title, streamer):
|
||||
state["lainchan.org"]["ogv"]["title"] = title
|
||||
state["lainchan.org"]["ogv"]["streamer"] = streamer
|
||||
return state
|
||||
|
||||
|
||||
# IRC callbacks
|
||||
|
||||
def lainchan_org_stream_unset_rtmp(state, i, irc):
|
||||
state = lainchan_org_rtmp_set(state, "", "")
|
||||
m = f"{lainchan_org_logo_rtmp}: Stream information was unset!"
|
||||
irc.privmsg(i.channel, m)
|
||||
arisu_streamunset_rtmp(irc)
|
||||
return state
|
||||
|
||||
|
||||
def lainchan_org_stream_set_rtmp(state, i, irc):
|
||||
streamer = i.nickname
|
||||
title = i.msg_nocmd
|
||||
|
||||
if not title:
|
||||
return lainchan_org_stream_unset_rtmp(state, i, irc)
|
||||
|
||||
state = lainchan_org_rtmp_set(state, title, streamer)
|
||||
m = f"{lainchan_org_logo_rtmp}: Stream information updated!"
|
||||
irc.privmsg(i.channel, m)
|
||||
arisu_streamset_rtmp(irc, title, streamer)
|
||||
return state
|
||||
|
||||
|
||||
def lainchan_org_streamer_set_rtmp(state, i, irc):
|
||||
streamer = i.msg_nocmd
|
||||
title = state["lainchan.org"]["rtmp"]["title"]
|
||||
|
||||
if not streamer:
|
||||
m = (f"{lainchan_org_logo_rtmp}: Usage: "
|
||||
f"{i.cmd_prefix}{i.cmd} <streamer>")
|
||||
irc.privmsg(i.channel, m)
|
||||
return
|
||||
|
||||
if not title:
|
||||
m = (f"{lainchan_org_logo_rtmp}: Set a title using "
|
||||
f"`{i.cmd_prefix}{i.cmd}' first.")
|
||||
irc.privmsg(i.channel, m)
|
||||
return
|
||||
|
||||
state = lainchan_org_rtmp_set(state, title, streamer)
|
||||
m = f"{lainchan_org_logo_rtmp}: Streamer updated!"
|
||||
irc.privmsg(i.channel, m)
|
||||
arisu_streamerset_rtmp(irc, streamer)
|
||||
return state
|
||||
|
||||
|
||||
def lainchan_org_stream_unset_ogv(state, i, irc):
|
||||
state = lainchan_org_ogv_set(state, "", "")
|
||||
m = f"{lainchan_org_logo_ogv}: Stream information was unset!"
|
||||
irc.privmsg(i.channel, m)
|
||||
arisu_streamunset_ogv(irc)
|
||||
return state
|
||||
|
||||
|
||||
def lainchan_org_stream_set_ogv(state, i, irc):
|
||||
streamer = i.nickname
|
||||
title = i.msg_nocmd
|
||||
|
||||
if not title:
|
||||
return lainchan_org_stream_unset_ogv(state, i, irc)
|
||||
|
||||
state = lainchan_org_ogv_set(state, title, streamer)
|
||||
m = f"{lainchan_org_logo_ogv}: Stream information updated!"
|
||||
irc.privmsg(i.channel, m)
|
||||
arisu_streamset_ogv(irc, title, streamer)
|
||||
return state
|
||||
|
||||
|
||||
def lainchan_org_streamer_set_ogv(state, i, irc):
|
||||
streamer = i.msg_nocmd
|
||||
title = state["lainchan.org"]["ogv"]["title"]
|
||||
|
||||
if not streamer:
|
||||
m = (f"{lainchan_org_logo_ogv}: Usage: "
|
||||
f"{i.cmd_prefix}{i.cmd} <streamer>")
|
||||
irc.privmsg(i.channel, m)
|
||||
return
|
||||
|
||||
if not title:
|
||||
m = (f"{lainchan_org_logo_ogv}: Set a title using "
|
||||
f"`{i.cmd_prefix}{i.cmd}' first.")
|
||||
irc.privmsg(i.channel, m)
|
||||
return
|
||||
|
||||
state = lainchan_org_ogv_set(state, title, streamer)
|
||||
m = f"{lainchan_org_logo_ogv}: Streamer updated!"
|
||||
irc.privmsg(i.channel, m)
|
||||
arisu_streamerset_ogv(irc, streamer)
|
||||
return state
|
||||
|
||||
|
||||
def lainchan_org_stream_urls(state, i, irc):
|
||||
rtmp = state["lainchan.org"]["rtmp"]["url"]
|
||||
hls = state["lainchan.org"]["rtmp"]["url-hls"]
|
||||
ogv = state["lainchan.org"]["ogv"]["url"]
|
||||
m = (f"{lainchan_org_logo_rtmp}: {rtmp} / {hls}"
|
||||
f" | {lainchan_org_logo_ogv}: {ogv}")
|
||||
irc.privmsg(i.channel, m)
|
||||
|
||||
|
||||
# ====================================================================
|
||||
# Video: lain.la
|
||||
# ====================================================================
|
||||
|
||||
# State functions
|
||||
|
||||
def lain_la_status(state):
|
||||
title = state["lain.la"]["title"]
|
||||
if not title:
|
||||
return ""
|
||||
streamer = state["lain.la"]["streamer"]
|
||||
url = state["lain.la"]["url"]
|
||||
rtmp_url = state["lain.la"]["url-rtmp"]
|
||||
return (f"{lain_la_logo}:"
|
||||
f" \x0311{title}\x0F by \x0302{streamer}\x0F"
|
||||
f" | Watch at: {url} | RTMP: {rtmp_url}")
|
||||
|
||||
|
||||
def lain_la_set(state, title, streamer):
|
||||
state["lain.la"]["title"] = title
|
||||
state["lain.la"]["streamer"] = streamer
|
||||
return state
|
||||
|
||||
|
||||
# IRC callbacks
|
||||
|
||||
def lain_la_stream_unset(state, i, irc):
|
||||
state = lain_la_set(state, "", "")
|
||||
m = f"{lain_la_logo}: Stream information was unset!"
|
||||
irc.privmsg(i.channel, m)
|
||||
return state
|
||||
|
||||
|
||||
def lain_la_stream_set(state, i, irc):
|
||||
streamer = i.nickname
|
||||
title = i.msg_nocmd
|
||||
|
||||
if not title:
|
||||
return lain_la_stream_unset(state, i, irc)
|
||||
|
||||
state = lain_la_set(state, title, streamer)
|
||||
m = f"{lain_la_logo}: Stream information updated!"
|
||||
irc.privmsg(i.channel, m)
|
||||
return state
|
||||
|
||||
|
||||
def lain_la_streamer_set(state, i, irc):
|
||||
streamer = i.msg_nocmd
|
||||
title = state["lain.la"]["title"]
|
||||
|
||||
if not streamer:
|
||||
m = (f"{lain_la_logo}: Usage: "
|
||||
f"{i.cmd_prefix}{i.cmd} <streamer>")
|
||||
irc.privmsg(i.channel, m)
|
||||
return
|
||||
|
||||
if not title:
|
||||
m = (f"{lain_la_logo}: Set a title using "
|
||||
f"`{i.cmd_prefix}{i.cmd}' first.")
|
||||
irc.privmsg(i.channel, m)
|
||||
return
|
||||
|
||||
state = lain_la_set(state, title, streamer)
|
||||
m = f"{lain_la_logo}: Streamer updated!"
|
||||
irc.privmsg(i.channel, m)
|
||||
return state
|
||||
|
||||
|
||||
# ====================================================================
|
||||
# IRC: Generic callbacks
|
||||
# ====================================================================
|
||||
|
||||
def stream(state, i, irc):
|
||||
stream_status_list = [
|
||||
lainchan_org_rtmp_status(state),
|
||||
lainchan_org_ogv_status(state),
|
||||
lain_la_status(state)
|
||||
]
|
||||
|
||||
if all([not i for i in stream_status_list]):
|
||||
m = (f"{lainchan_org_logo}: Nothing is streaming right now."
|
||||
" Learn how you can stream at: "
|
||||
"https://lainchan.org/stream.html ")
|
||||
irc.privmsg(i.channel, m)
|
||||
|
||||
return
|
||||
|
||||
for s in stream_status_list:
|
||||
if s:
|
||||
irc.privmsg(i.channel, s)
|
||||
|
||||
|
||||
def stream_how(state, i, irc):
|
||||
pass
|
||||
|
||||
|
||||
# ====================================================================
|
||||
# Main
|
||||
# ====================================================================
|
||||
|
||||
callbacks = {
|
||||
"stream": stream,
|
||||
# lainon.life / lainchan radio
|
||||
"radio": radio,
|
||||
# lainchan.org
|
||||
"streamset": lainchan_org_stream_set_rtmp,
|
||||
"streamset-rtmp": lainchan_org_stream_set_rtmp,
|
||||
"streamset-ogv": lainchan_org_stream_set_ogv,
|
||||
"streamerset": lainchan_org_streamer_set_rtmp,
|
||||
"streamerset-rtmp": lainchan_org_streamer_set_rtmp,
|
||||
"streamerset-ogv": lainchan_org_streamer_set_ogv,
|
||||
"streamunset": lainchan_org_stream_unset_rtmp,
|
||||
"streamunset-rtmp": lainchan_org_stream_unset_rtmp,
|
||||
"streamunset-ogv": lainchan_org_stream_unset_ogv,
|
||||
"streamurls": lainchan_org_stream_urls,
|
||||
# lain.la
|
||||
"streamset-la": lain_la_stream_set,
|
||||
"streamerset-la": lain_la_streamer_set,
|
||||
"streamunset-la": lain_la_stream_unset
|
||||
}
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
state = {
|
||||
"lainchan.org": {
|
||||
"url": "https://lainchan.org/stream.html",
|
||||
"ogv": {
|
||||
"url": "https://lainchan.org/icecast/lainstream.ogg",
|
||||
"title": "",
|
||||
"streamer": ""
|
||||
},
|
||||
"rtmp": {
|
||||
"url": "rtmp://lainchan.org/show/stream",
|
||||
"url-hls": "https://lainchan.org:8080/hls/stream.m3u8",
|
||||
"title": "",
|
||||
"streamer": ""
|
||||
}
|
||||
},
|
||||
"lain.la": {
|
||||
"url": "https://stream.lain.la",
|
||||
"url-rtmp": "rtmp://lain.la",
|
||||
"title": "",
|
||||
"streamer": ""
|
||||
}
|
||||
}
|
||||
|
||||
db = i.db[1]
|
||||
dbc = db.cursor()
|
||||
try:
|
||||
dbc.execute('''SELECT value FROM stream;''')
|
||||
fetch = dbc.fetchone()
|
||||
state = pickle.loads(fetch[0])
|
||||
except Exception:
|
||||
dbc.execute('''CREATE TABLE IF NOT EXISTS stream (value BLOB);''')
|
||||
|
||||
callbacks[i.cmd](state, i, irc)
|
||||
|
||||
v = pickle.dumps(state)
|
||||
try:
|
||||
dbc.execute('''INSERT INTO stream (value) VALUES (?);''', (v,))
|
||||
except Exception:
|
||||
pass
|
||||
dbc.execute('''UPDATE stream SET value = ?''', (v,))
|
||||
db.commit()
|
210
src/irc/modules/lastfm.py
Normal file
210
src/irc/modules/lastfm.py
Normal file
@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding=utf-8
|
||||
|
||||
# Last.fm Now Playing Module for Drastikbot
|
||||
#
|
||||
# It uses the Last.fm API to get the user's currently playing song.
|
||||
# You need to provide your own API key to use this.
|
||||
#
|
||||
# Depends:
|
||||
# - requests :: $ pip3 install requests
|
||||
|
||||
'''
|
||||
Copyright (C) 2018, 2021 drastik.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import requests
|
||||
from user_auth import user_auth
|
||||
|
||||
|
||||
class Module(): # Request commands to be used by the module
|
||||
def __init__(self):
|
||||
self.commands = ['np', 'npset', 'npunset', 'npauth']
|
||||
self.manual = {
|
||||
"desc": "Display your currently playing song using last.fm.",
|
||||
"irc_commands": {
|
||||
"np": {"usage": lambda x: f"{x}np [nickname]",
|
||||
"info": "Show the song that is playing right now."},
|
||||
"npset": {"usage": lambda x: f"{x}npset <last.fm username>",
|
||||
"info": "Set your last.fm username."},
|
||||
"npunset": {"usage": lambda x: f"{x}npunset",
|
||||
"info": "Unset your last.fm username."},
|
||||
"npauth": {"usage": lambda x: f"{x}npauth",
|
||||
"info": "Toggle NickServ authentication for npset"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ----- Constants ----- #
|
||||
API_KEY = 'Add you lastfm API here'
|
||||
# --------------------- #
|
||||
|
||||
|
||||
def set_auth(i, irc, dbc, irc_nick):
|
||||
if not user_auth(i, irc, irc_nick):
|
||||
return f'{irc_nick}: You are not logged in with NickServ.'
|
||||
|
||||
dbc.execute('SELECT auth FROM lastfm WHERE irc_nick=?;',
|
||||
(irc_nick,))
|
||||
fetch = dbc.fetchone()
|
||||
|
||||
try:
|
||||
auth = fetch[0]
|
||||
except TypeError: # 'NoneType' object is not subscriptable
|
||||
auth = 0
|
||||
|
||||
if auth == 0:
|
||||
auth = 1
|
||||
msg = f'{irc_nick}: lastfm: Enabled NickServ authentication.'
|
||||
elif auth == 1:
|
||||
auth = 0
|
||||
msg = f'{irc_nick}: lastfm: Disabled NickServ authentication.'
|
||||
|
||||
dbc.execute("INSERT OR IGNORE INTO lastfm (irc_nick, auth) VALUES (?, ?);",
|
||||
(irc_nick, auth))
|
||||
dbc.execute("UPDATE lastfm SET auth=? WHERE irc_nick=?;", (auth, irc_nick))
|
||||
return msg
|
||||
|
||||
|
||||
def get_auth(i, irc, dbc, irc_nick):
|
||||
dbc.execute('SELECT auth FROM lastfm WHERE irc_nick=?;',
|
||||
(irc_nick,))
|
||||
fetch = dbc.fetchone()
|
||||
try:
|
||||
auth = fetch[0]
|
||||
except TypeError: # 'NoneType' object is not subscriptable
|
||||
auth = 0
|
||||
|
||||
if auth == 0:
|
||||
return True
|
||||
elif auth == 1 and user_auth(i, irc, irc_nick):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def set_user(i, irc, dbc, irc_nick, lfm_user):
|
||||
if not get_auth(i, irc, dbc, irc_nick):
|
||||
return f"{irc_nick}: lastfm: NickServ authentication is required."
|
||||
dbc.execute(
|
||||
'''INSERT OR IGNORE INTO lastfm (irc_nick, lfm_user)
|
||||
VALUES (?, ?);''', (irc_nick, lfm_user))
|
||||
dbc.execute('''UPDATE lastfm SET lfm_user=? WHERE irc_nick=?;''',
|
||||
(lfm_user, irc_nick))
|
||||
return f'{irc_nick}: Your last.fm username was set to "{lfm_user}"'
|
||||
|
||||
|
||||
def unset_user(i, irc, dbc, irc_nick):
|
||||
if not get_auth(i, irc, dbc, irc_nick):
|
||||
return f"{irc_nick}: lastfm: NickServ authentication is required."
|
||||
try:
|
||||
dbc.execute('''DELETE FROM lastfm WHERE irc_nick=?;''', (irc_nick,))
|
||||
except Exception:
|
||||
return f"{irc_nick}: You haven't set a last.fm username."
|
||||
return f'{irc_nick}: Your last.fm username was unset.'
|
||||
|
||||
|
||||
def get_user(dbc, irc_nick):
|
||||
try:
|
||||
dbc.execute('SELECT lfm_user FROM lastfm WHERE irc_nick=?;',
|
||||
(irc_nick,))
|
||||
fetch = dbc.fetchone()
|
||||
lfm_user = fetch[0]
|
||||
return lfm_user
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def now_playing(dbc, irc_nick, same=False):
|
||||
lfm_user = get_user(dbc, irc_nick)
|
||||
if not lfm_user:
|
||||
return False
|
||||
url = ('https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks'
|
||||
f'&user={lfm_user}&api_key={API_KEY}&format=json&limit=1')
|
||||
try:
|
||||
r = requests.get(url, timeout=10)
|
||||
except Exception:
|
||||
return
|
||||
out = r.json()['recenttracks']['track'][0]
|
||||
try:
|
||||
artist = out['artist']['#text']
|
||||
song = out['name']
|
||||
album = out['album']['#text']
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
np = out['@attr']['nowplaying']
|
||||
except KeyError:
|
||||
np = False
|
||||
|
||||
if np == 'true':
|
||||
if album:
|
||||
ret = (f'\x0304{irc_nick}\x0F is listening to \x0304{song}\x0F by '
|
||||
f'\x0304{artist}\x0F from the album \x0304{album}\x0F.')
|
||||
else:
|
||||
ret = (f'\x0304{irc_nick}\x0F is listening to \x0304{song}\x0F by '
|
||||
f'\x0304{artist}\x0F.')
|
||||
else:
|
||||
if same:
|
||||
ret = 'You are not playing anything right now.'
|
||||
else:
|
||||
ret = f'\x0304{irc_nick}\x0F is not playing anything right now.'
|
||||
return ret
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
dbc = i.db[1].cursor()
|
||||
args = i.msg_nocmd.split()
|
||||
|
||||
try:
|
||||
dbc.execute(
|
||||
'''CREATE TABLE IF NOT EXISTS lastfm (irc_nick TEXT COLLATE NOCASE
|
||||
PRIMARY KEY, lfm_user TEXT, auth INTEGER DEFAULT 0);''')
|
||||
except Exception:
|
||||
# sqlite3.OperationalError: cannot commit - no transaction is active
|
||||
pass
|
||||
|
||||
if 'np' == i.cmd:
|
||||
if not args:
|
||||
np = now_playing(dbc, i.nickname, same=True)
|
||||
if not np:
|
||||
help_msg = (f"You haven't set your last.fm user yet. Set it "
|
||||
f'with "{i.cmd_prefix}npset <Last.fm Username>".')
|
||||
return irc.privmsg(i.channel, help_msg)
|
||||
else:
|
||||
return irc.privmsg(i.channel, np)
|
||||
elif len(args) == 1:
|
||||
np = now_playing(dbc, args[0])
|
||||
return irc.privmsg(i.channel, np)
|
||||
else:
|
||||
help_msg = 'Usage: .np [nickname]'
|
||||
return irc.privmsg(i.channel, help_msg)
|
||||
elif 'npset' == i.cmd:
|
||||
if not args or len(args) > 1:
|
||||
help_msg = 'Usage: .npset <Last.fm Username>'
|
||||
return irc.privmsg(i.channel, help_msg)
|
||||
else:
|
||||
ret = set_user(i, irc, dbc, i.nickname, args[0])
|
||||
i.db[1].commit()
|
||||
return irc.privmsg(i.channel, ret)
|
||||
elif 'npunset' == i.cmd:
|
||||
ret = unset_user(i, irc, dbc, i.nickname)
|
||||
i.db[1].commit()
|
||||
return irc.privmsg(i.channel, ret)
|
||||
elif 'npauth' == i.cmd:
|
||||
ret = set_auth(i, irc, dbc, i.nickname)
|
||||
i.db[1].commit()
|
||||
irc.privmsg(i.channel, ret)
|
87
src/irc/modules/points.py
Normal file
87
src/irc/modules/points.py
Normal file
@ -0,0 +1,87 @@
|
||||
# coding=utf-8
|
||||
|
||||
# Point (karma) counting module for Drastikbot
|
||||
#
|
||||
# Give points/karma to users for performing certain actions
|
||||
|
||||
'''
|
||||
Copyright (C) 2019 drastik.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.commands = ["points"]
|
||||
self.auto = True
|
||||
self.manual = {
|
||||
"desc": "Count a user's GNU/Linux points.",
|
||||
"bot_commands": {
|
||||
"points": lambda x: f"{x}points",
|
||||
"info": "Show your total amount of points."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def set_gnu_linux_points(dbc, nickname, points):
|
||||
dbc.execute("CREATE TABLE IF NOT EXISTS points_gnu_linux "
|
||||
"(nickname TEXT COLLATE NOCASE, points INTEGER);")
|
||||
dbc.execute("INSERT OR IGNORE INTO points_gnu_linux VALUES (?, ?);",
|
||||
(nickname, points))
|
||||
dbc.execute("UPDATE points_gnu_linux SET points=? WHERE nickname=?;",
|
||||
(points, nickname))
|
||||
|
||||
|
||||
def get_gnu_linux_points(dbc, nickname):
|
||||
try:
|
||||
dbc.execute("SELECT points FROM points_gnu_linux WHERE nickname=?;",
|
||||
(nickname,))
|
||||
return dbc.fetchone()[0]
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
def gnu_linux_points_handler(dbc, nickname, mode=""):
|
||||
try:
|
||||
p = get_gnu_linux_points(dbc, nickname)
|
||||
except Exception:
|
||||
p = 0
|
||||
if "gnu" == mode:
|
||||
p += 1
|
||||
elif "linux" == mode:
|
||||
p -= 1
|
||||
set_gnu_linux_points(dbc, nickname, p)
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
dbc = i.db[0].cursor()
|
||||
|
||||
if i.cmd == "points":
|
||||
gl_p = get_gnu_linux_points(dbc, i.nickname)
|
||||
irc.privmsg(i.channel, f"GNU/Linux Points for {i.nickname}: {gl_p}")
|
||||
|
||||
if i.channel == i.nickname:
|
||||
return
|
||||
|
||||
last_nick = i.varget("last_nick", defval=irc.var.nickname)
|
||||
if last_nick == i.nickname:
|
||||
return
|
||||
else:
|
||||
i.varset("last_nick", i.nickname)
|
||||
|
||||
if "gnu/linux" in i.msg.lower() or "gnu+linux" in i.msg.lower():
|
||||
gnu_linux_points_handler(dbc, i.nickname, mode="gnu")
|
||||
elif "linux" in i.msg.lower() and "linux kernel" not in i.msg.lower():
|
||||
gnu_linux_points_handler(dbc, i.nickname, mode="linux")
|
303
src/irc/modules/quote.py
Normal file
303
src/irc/modules/quote.py
Normal file
@ -0,0 +1,303 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding=utf-8
|
||||
|
||||
# Quote Module for Drastikbot
|
||||
#
|
||||
# Save user quotes and send them to a channel upon request.
|
||||
|
||||
'''
|
||||
Copyright (C) 2019-2021 drastik.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
any later version.
|
||||
|
||||
This program 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import random
|
||||
import sqlite3
|
||||
|
||||
import requests
|
||||
|
||||
from admin import is_allowed
|
||||
|
||||
logo = "\x02quote\x0F"
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.commands = ['quote', 'findquote', 'addquote', 'delquote',
|
||||
'listquotes']
|
||||
self.manual = {
|
||||
"desc": "Saves user quotes and posts them when requested.",
|
||||
"bot_commands": {
|
||||
"quote": {
|
||||
"usage": lambda x: (
|
||||
f"[channels]: {x}quote [nickname/id/text]"
|
||||
f" | [queries]: {x}quote <#channel> [nickname/id/text]"
|
||||
),
|
||||
"info": ("Without any arguments, a random quote will be"
|
||||
" posted. If arguments are given the will try to"
|
||||
" first match a nickname, then an ID and then"
|
||||
" the text of a quote.")
|
||||
},
|
||||
"findquote": {
|
||||
"usage": lambda x: (
|
||||
f"[channels]: {x}findquote <text>"
|
||||
f" | [queries]: {x}findquote <#channel> <text>"
|
||||
),
|
||||
"info": ("Try to match the given text to a quote."
|
||||
" If a quote is found, it is posted.")
|
||||
},
|
||||
"addquote": {
|
||||
"usage": lambda x: f"{x}addquote <nickname> <quote>",
|
||||
"info": "Add a quote to the database."
|
||||
},
|
||||
"delquote": {
|
||||
"usage": lambda x: f"{x}delquote <id>",
|
||||
"info": "Delete a quote using its ID"
|
||||
},
|
||||
"listquotes": {
|
||||
"usage": lambda x: f"{x}listquotes <#channel>",
|
||||
"info": ("List all the quotes in which the the caller is"
|
||||
" mentioned in. The list is sent in a query.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def add(channel, quote, made_by, added_by, dbc):
|
||||
try:
|
||||
dbc.execute("INSERT INTO quote(channel, quote, made_by, added_by) "
|
||||
"VALUES (?, ?, ?, ?)", (channel, quote, made_by, added_by))
|
||||
return f"{logo}: #{dbc.lastrowid} Added!"
|
||||
except sqlite3.IntegrityError:
|
||||
return f"{logo}: This quote has already been added."
|
||||
|
||||
|
||||
def delete(quote_id, dbc):
|
||||
try:
|
||||
dbc.execute('''DELETE FROM quote WHERE num=?;''', (quote_id,))
|
||||
return f"{logo}: #{quote_id} deleted."
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def find(channel, query, dbc, export_all=False):
|
||||
try:
|
||||
dbc.execute(
|
||||
'''SELECT num FROM quote_index
|
||||
WHERE quote_index MATCH ? AND channel=?;''',
|
||||
(f"quote:{query}", channel))
|
||||
f = dbc.fetchall()
|
||||
num = random.choice(f)[0]
|
||||
dbc.execute('''SELECT * FROM quote WHERE num=?''', (num,))
|
||||
if export_all:
|
||||
return dbc.fetchall()
|
||||
f = dbc.fetchone()
|
||||
return f"{logo}: #{f[0]} | {f[2]} \x02-\x0F {f[3]} | Added by {f[4]}"
|
||||
except Exception:
|
||||
if export_all:
|
||||
return False
|
||||
return f"{logo}: No results."
|
||||
|
||||
|
||||
def _search_by_nick(channel, text, dbc, export_all=False):
|
||||
try:
|
||||
if not text:
|
||||
dbc.execute('SELECT * FROM quote WHERE channel=?', (channel,))
|
||||
else:
|
||||
dbc.execute('SELECT * FROM quote WHERE made_by=? AND channel=?;',
|
||||
(text, channel))
|
||||
f = dbc.fetchall()
|
||||
if export_all:
|
||||
return f
|
||||
f = random.choice(f)
|
||||
return f"{logo}: #{f[0]} | {f[2]} \x02-\x0F {f[3]} | Added by {f[4]}"
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _search_by_id(channel, text, dbc):
|
||||
if not text.isdigit():
|
||||
return False
|
||||
|
||||
try:
|
||||
dbc.execute('SELECT * FROM quote WHERE num=? AND channel=?;', (text, channel))
|
||||
f = dbc.fetchone()
|
||||
return f"{logo}: #{f[0]} | {f[2]} \x02-\x0F {f[3]} | Added by {f[4]}"
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def listquotes(channel, nickname, dbc, irc):
|
||||
data = ""
|
||||
|
||||
sbn = _search_by_nick(channel, nickname, dbc, export_all=True)
|
||||
if sbn:
|
||||
for f in sbn:
|
||||
m = f"#{f[0]} | {f[2]} - {f[3]} | Added by {f[4]}\n\n"
|
||||
data += m
|
||||
|
||||
rest = find(channel, nickname, dbc, export_all=True)
|
||||
if rest:
|
||||
for f in rest:
|
||||
m = f"#{f[0]} | {f[2]} - {f[3]} | Added by {f[4]}\n\n"
|
||||
data += m
|
||||
|
||||
if not rest and not sbn:
|
||||
irc.notice(nickname, f"{logo}: No results.")
|
||||
else:
|
||||
pomf_url = listquotes_pomf(data)
|
||||
m = f"{logo}: Your quotes can be found here: {pomf_url}"
|
||||
irc.notice(nickname, m)
|
||||
|
||||
|
||||
def listquotes_pomf(data):
|
||||
url = "https://pomf.lain.la/upload.php"
|
||||
files = {"files[]": ("quotes.txt", data, "text/plain")}
|
||||
r = requests.post(url, files=files)
|
||||
return r.json()["files"][0]["url"]
|
||||
|
||||
|
||||
|
||||
def quote(channel, text, dbc):
|
||||
sbn = _search_by_nick(channel, text, dbc)
|
||||
if sbn:
|
||||
return sbn
|
||||
sbi = _search_by_id(channel, text, dbc)
|
||||
if sbi:
|
||||
return sbi
|
||||
|
||||
return find(channel, text, dbc)
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
dbc = i.db[1].cursor()
|
||||
dbc.execute(
|
||||
'''
|
||||
CREATE TABLE IF NOT EXISTS quote
|
||||
(num INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
channel TEXT,
|
||||
quote TEXT COLLATE NOCASE UNIQUE,
|
||||
made_by TEXT COLLATE NOCASE,
|
||||
added_by TEXT COLLATE NOCASE);''')
|
||||
dbc.execute(
|
||||
'''
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS quote_index
|
||||
USING fts5(num, quote, channel, tokenize=porter);''')
|
||||
dbc.execute(
|
||||
'''
|
||||
CREATE TRIGGER IF NOT EXISTS quote_after_insert AFTER INSERT ON quote
|
||||
BEGIN
|
||||
INSERT INTO quote_index (num, quote, channel)
|
||||
VALUES (new.num, new.quote, new.channel);
|
||||
END;''')
|
||||
dbc.execute(
|
||||
'''
|
||||
CREATE TRIGGER IF NOT EXISTS quote_after_delete AFTER DELETE ON quote
|
||||
BEGIN
|
||||
DELETE FROM quote_index WHERE num = old.num;
|
||||
END;''')
|
||||
i.db[1].commit()
|
||||
|
||||
if 'quote' == i.cmd:
|
||||
if i.is_pm:
|
||||
if not i.msg_nocmd:
|
||||
m = (f"{logo}: Usage: {i.cmd_prefix}{i.cmd} "
|
||||
f"<#channel> <Nickname / ID / Query>")
|
||||
irc.privmsg(i.channel, m)
|
||||
return
|
||||
channel = i.msg_nocmd.split()[0]
|
||||
if not channel[:1] == '#':
|
||||
m = (f"{logo}: Usage: {i.cmd_prefix}{i.cmd} "
|
||||
f"<#channel> <Nickname / ID / Query>")
|
||||
irc.privmsg(i.channel, m)
|
||||
return
|
||||
if channel not in irc.var.namesdict:
|
||||
m = f"{logo}: The bot has not joined the channel: {channel}"
|
||||
irc.privmsg(i.channel, m)
|
||||
return
|
||||
query = " ".join(i.msg_nocmd.split()[1:])
|
||||
irc.privmsg(i.channel, quote(channel, query, dbc))
|
||||
else:
|
||||
irc.privmsg(i.channel, quote(i.channel, i.msg_nocmd, dbc))
|
||||
|
||||
elif 'findquote' == i.cmd:
|
||||
if i.is_pm:
|
||||
if not i.msg_nocmd or len(i.msg_nocmd.split()) < 2:
|
||||
m = f"{logo}: Usage: {i.cmd_prefix}{i.cmd} <#channel> <Query>"
|
||||
irc.privmsg(i.channel, m)
|
||||
return
|
||||
channel = i.msg_nocmd.split()[0]
|
||||
if not channel[:1] == '#':
|
||||
m = f"{logo}: Usage: {i.cmd_prefix}{i.cmd} <#channel> <Query>"
|
||||
irc.privmsg(i.channel, m)
|
||||
return
|
||||
if channel not in irc.var.namesdict:
|
||||
m = f"{logo}: The bot has not joined the channel: {channel}"
|
||||
irc.privmsg(i.channel, m)
|
||||
return
|
||||
query = " ".join(i.msg_nocmd.split()[1:])
|
||||
irc.privmsg(i.channel, find(channel, query, dbc))
|
||||
else:
|
||||
if not i.msg_nocmd:
|
||||
m = f"{logo}: Usage: {i.cmd_prefix}{i.cmd} <Query>"
|
||||
irc.privmsg(i.channel, m)
|
||||
return
|
||||
irc.privmsg(i.channel, find(i.channel, i.msg_nocmd, dbc))
|
||||
|
||||
elif 'delquote' == i.cmd:
|
||||
if i.is_pm:
|
||||
m = f"{logo}: This command can only be used within a channel."
|
||||
irc.privmsg(i.channel, m)
|
||||
return
|
||||
if not i.msg_nocmd or not i.msg_nocmd.isdigit():
|
||||
m = f"{logo}: Usage: {i.cmd_prefix}{i.cmd} <ID>"
|
||||
irc.privmsg(i.channel, m)
|
||||
return
|
||||
if is_allowed(i, irc, i.nickname, i.channel):
|
||||
irc.privmsg(i.channel, delete(i.msg_nocmd, dbc))
|
||||
else:
|
||||
m = f"{logo}: Only Channel Operators are allowed to delete quotes."
|
||||
irc.privmsg(i.channel, m)
|
||||
|
||||
elif 'addquote' == i.cmd:
|
||||
if i.is_pm:
|
||||
m = (f"{logo}: Please, submit the quote in the channel it was"
|
||||
" posted in.")
|
||||
irc.privmsg(i.channel, m)
|
||||
else:
|
||||
if not i.msg_nocmd or len(i.msg_nocmd.split()) < 2:
|
||||
m = f"{logo}: Usage: {i.cmd_prefix}{i.cmd} <Nickname> <Quote>"
|
||||
irc.privmsg(i.channel, m)
|
||||
return
|
||||
made_by = i.msg_nocmd.split()[0]
|
||||
quote_text = " ".join(i.msg_nocmd.split()[1:])
|
||||
irc.privmsg(i.channel,
|
||||
add(i.channel, quote_text, made_by, i.nickname, dbc))
|
||||
|
||||
elif 'listquotes' == i.cmd:
|
||||
if not i.msg_nocmd or len(i.msg_nocmd.split()) != 1:
|
||||
m = f"{logo}: Usage: {i.cmd_prefix}{i.cmd} <#channel>"
|
||||
irc.privmsg(i.channel, m)
|
||||
return
|
||||
channel = i.msg_nocmd.split()[0]
|
||||
if not channel[:1] == '#':
|
||||
m = f"{logo}: Usage: {i.cmd_prefix}{i.cmd} <#channel>"
|
||||
irc.privmsg(i.channel, m)
|
||||
return
|
||||
if channel not in irc.var.namesdict:
|
||||
m = f"{logo}: The bot has not joined the channel: {channel}"
|
||||
irc.privmsg(i.channel, m)
|
||||
return
|
||||
listquotes(channel, i.nickname, dbc, irc)
|
||||
i.db[1].commit()
|
215
src/irc/modules/search.py
Normal file
215
src/irc/modules/search.py
Normal file
@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding=utf-8
|
||||
|
||||
# Search Module for Drastikbot
|
||||
#
|
||||
# Provides the results of various search engines like:
|
||||
# Google, Bing, Duckduckgo, Searx, Startpage
|
||||
#
|
||||
# Depends:
|
||||
# - requests :: $ pip3 install requests
|
||||
# - beautifulsoup :: $ pip3 install beautifulsoup4
|
||||
# - url :: included with drastikbot_modules, should be loaded.
|
||||
|
||||
'''
|
||||
Copyright (C) 2018, 2021 drastik.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
any later version.
|
||||
|
||||
This program 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import urllib.parse
|
||||
import json
|
||||
import requests
|
||||
import bs4
|
||||
import url # drastikbot_modules: url.py
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.commands = ['g', 'bing', 'ddg', 'searx', 'sp']
|
||||
self.manual = {
|
||||
"desc": ("Get search results from Duckduckgo, Google, Bing"
|
||||
", Searx and Startpage."),
|
||||
"bot_commands": {
|
||||
"g": {"usage": lambda x: f"{x}g <query>"},
|
||||
"bing": {"usage": lambda x: f"{x}bing <query>"},
|
||||
"ddg": {"usage": lambda x: f"{x}ddg <query>"},
|
||||
"searx": {"usage": lambda x: f"{x}searx <query>"},
|
||||
"sp": {"usage": lambda x: f"{x}sp <query>"}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ----- Constants ----- #
|
||||
opt_title_tag = True
|
||||
parser = 'html.parser'
|
||||
lang = "en-US"
|
||||
|
||||
ua_chrome_90 = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||
" (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36")
|
||||
|
||||
|
||||
# --------------------- #
|
||||
|
||||
|
||||
# --- Helper Functions --- #
|
||||
def url2str(query):
|
||||
return urllib.parse.unquote_plus(query)
|
||||
|
||||
|
||||
def url_extract(url):
|
||||
u = urllib.parse.urlparse(url)
|
||||
u = urllib.parse.parse_qs(u.query)
|
||||
try:
|
||||
u = u['v'][0]
|
||||
except Exception:
|
||||
u = u['video_id'][0]
|
||||
return ''.join(u.split())
|
||||
|
||||
|
||||
def urlfix(url):
|
||||
url = url.replace(' ', '')
|
||||
if not (url.startswith('http://') or url.startswith('https://')):
|
||||
url = 'http://' + url
|
||||
return url
|
||||
|
||||
|
||||
# ====================================================================
|
||||
# Google
|
||||
# ====================================================================
|
||||
|
||||
def google(query):
|
||||
search = f'https://www.google.com/search?q={query}'
|
||||
return "google", "This search engine is not supported yet."
|
||||
|
||||
|
||||
# ====================================================================
|
||||
# Bing
|
||||
# ====================================================================
|
||||
|
||||
def bing(args):
|
||||
query = urllib.parse.quote(args, safe="")
|
||||
u = f"https://bing.com/search?q={query}"
|
||||
h = headers={
|
||||
"Accept-Language": lang,
|
||||
"user-agent": ua_chrome_90,
|
||||
}
|
||||
r = requests.get(u, headers=h, timeout=10)
|
||||
soup = bs4.BeautifulSoup(r.text, parser)
|
||||
|
||||
results_l = soup.find_all("li", {"class": "b_algo"})
|
||||
result = results_l[0].find("a").get("href")
|
||||
|
||||
return "bing", result
|
||||
|
||||
|
||||
# ====================================================================
|
||||
# Duckduckgo
|
||||
# ====================================================================
|
||||
|
||||
def duckduckgo(args):
|
||||
query = urllib.parse.quote(args, safe="")
|
||||
|
||||
if args[0] == '!':
|
||||
return "duckduckgo", duckduckgo_bang(query)
|
||||
|
||||
return "duckduckgo", duckduckgo_search(query)
|
||||
|
||||
|
||||
def duckduckgo_bang(query):
|
||||
u = f"https://api.duckduckgo.com/?q={query}&format=json&no_redirect=1"
|
||||
h = {
|
||||
"Accept-Language": lang
|
||||
}
|
||||
r = requests.get(u, headers=h, timeout=10)
|
||||
return r.json()["Redirect"]
|
||||
|
||||
|
||||
def duckduckgo_search(query):
|
||||
u = ("https://html.duckduckgo.com/html/"
|
||||
f"?q={query}&kl=wt-wt&kp=-2&kaf=1&kh=1&k1=-1&kd=-1")
|
||||
h = {
|
||||
"user-agent": ua_chrome_90,
|
||||
"Accept-Language": lang
|
||||
}
|
||||
r = requests.get(u, headers=h, timeout=10)
|
||||
soup = bs4.BeautifulSoup(r.text, parser)
|
||||
|
||||
result = soup.find("a", {"class": ["result__url"]})
|
||||
return result.get("href")
|
||||
|
||||
|
||||
# ====================================================================
|
||||
# Searx
|
||||
# ====================================================================
|
||||
|
||||
def searx(query):
|
||||
search = f'https://searx.me/?q={query}'
|
||||
return "searx", "This search engine is not supported yet."
|
||||
|
||||
|
||||
# ====================================================================
|
||||
# Startpage
|
||||
# ====================================================================
|
||||
|
||||
def startpage(query):
|
||||
search = f'https://www.startpage.com/do/asearch?q={query}'
|
||||
return "startpage", "This search engine is not supported yet."
|
||||
|
||||
|
||||
# ====================================================================
|
||||
# Main
|
||||
# ====================================================================
|
||||
|
||||
dispatch = {
|
||||
"g": google,
|
||||
"bing": bing,
|
||||
"ddg": duckduckgo,
|
||||
"searx": searx,
|
||||
"sp": startpage
|
||||
}
|
||||
|
||||
logo_d = {
|
||||
"google": "\x0302G\x0304o\x0308o\x0302g\x0309l\x0304e\x0F",
|
||||
"bing": "\x0315Bing\x0F",
|
||||
"duckduckgo": "\x0315DuckDuckGo\x0F",
|
||||
"searx": "\x0315sear\x0311X\x0F",
|
||||
"startpage": "\x0304start\x0302page\x0F"
|
||||
}
|
||||
|
||||
# err = f'{logo}: \x0308Sorry, i could not find any results for:\x0F {query}'
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
args = i.msg_nocmd
|
||||
botcmd = i.cmd
|
||||
receiver = i.channel
|
||||
|
||||
engine, result = dispatch[botcmd](args)
|
||||
|
||||
title = None
|
||||
if opt_title_tag:
|
||||
title = url.get_title(result)
|
||||
|
||||
logo = logo_d[engine]
|
||||
|
||||
if title:
|
||||
m = f"{logo}: {result} | < title: {title}"
|
||||
else:
|
||||
m = f"{logo}: {result}"
|
||||
|
||||
# Truncate the output just in case. We can't send 512 bytes anyway.
|
||||
m = m[:512]
|
||||
|
||||
irc.notice(receiver, m)
|
161
src/irc/modules/sed.py
Normal file
161
src/irc/modules/sed.py
Normal file
@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding=utf-8
|
||||
|
||||
# Sed Module for Drastikbot
|
||||
#
|
||||
# Replace text using sed.
|
||||
#
|
||||
# This module keeps a buffer of the last posted messages
|
||||
# and when the substitution command is issued it calls sed
|
||||
# and sends the result.
|
||||
#
|
||||
# Depends:
|
||||
# - sed :: default unix program, should be in the repos
|
||||
|
||||
'''
|
||||
Copyright (C) 2018 drastik.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
any later version.
|
||||
|
||||
This program 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import subprocess
|
||||
import re
|
||||
from dbot_tools import p_truncate
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.auto = True
|
||||
self.manual = {
|
||||
"desc": (
|
||||
"Usage: s/regexp/replacement/flags"
|
||||
" | Try to match a 'regexp' with one of the previous"
|
||||
" messages posted and replace it with 'replacement'."
|
||||
" For flags and a detailed explanation see:"
|
||||
" https://www.gnu.org/software/sed/manual/html_node/"
|
||||
"The-_0022s_0022-Command.html"
|
||||
" | Extensions: \"s///-n\" n is the number of matches to skip"
|
||||
" If the 'number' flag is used -n should be used after it.")
|
||||
}
|
||||
|
||||
|
||||
def write(varget, varset, channel, msg):
|
||||
msgdict = varget('msgdict', defval={channel: [msg]})
|
||||
|
||||
try:
|
||||
msgdict[channel].append(msg)
|
||||
except KeyError:
|
||||
msgdict.update({channel: [msg]})
|
||||
|
||||
if len(msgdict[channel]) > 50:
|
||||
del msgdict[channel][0]
|
||||
|
||||
varset('msgdict', msgdict)
|
||||
|
||||
|
||||
def read(varget, channel):
|
||||
return varget('msgdict')[channel]
|
||||
|
||||
|
||||
def call_sed(msg, sed_args):
|
||||
echo = ['echo', msg]
|
||||
p = subprocess.run(echo, stdout=subprocess.PIPE)
|
||||
echo_outs = p.stdout
|
||||
sed = ['sed', '-r', '--sandbox',
|
||||
f's/{sed_args[1]}/{sed_args[2]}/{sed_args[3]}']
|
||||
p = subprocess.run(sed, stdout=subprocess.PIPE, input=echo_outs)
|
||||
return p.stdout.decode('utf-8')
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
sed_parse = re.compile('(?<!\\\\)/')
|
||||
sed_cmd = re.compile('^s/.*/.*')
|
||||
sed_out = ''
|
||||
|
||||
if not sed_cmd.match(i.msg):
|
||||
write(i.varget, i.varset, i.channel, i.msg)
|
||||
return
|
||||
|
||||
sed_args = sed_parse.split(i.msg)
|
||||
|
||||
if len(sed_args) < 4:
|
||||
# check if the last / is missed etc.
|
||||
return
|
||||
|
||||
msglist = read(i.varget, i.channel)
|
||||
|
||||
# Extension to allow the user match previous messages.
|
||||
# It uses the special syntax: "s///-n" where n is the
|
||||
# number of matches to skip.
|
||||
# Sideffect: because it matches upto two decimals after -,
|
||||
# sed string positioning (or other commands
|
||||
# with decimals in them) should be issued
|
||||
# before the - command or two characters after
|
||||
# the - .
|
||||
# The following code is for parsing the command. It
|
||||
# edits the "goback" variable which will be used in the
|
||||
# loop below.
|
||||
# We use two decimals because the queue save upto 50
|
||||
# messages.
|
||||
goback = False
|
||||
if '-' in sed_args[3]:
|
||||
s = sed_args[3]
|
||||
idx = s.index('-')
|
||||
if len(s[idx:]) > 2 and s[idx + 2].isdecimal():
|
||||
goback = int(f'{s[idx + 1]}{s[idx + 2]}')
|
||||
sed_args[3] = f'{s[:idx]}{s[idx + 3:]}'
|
||||
elif len(s[idx:]) > 1 and s[idx + 1].isdecimal():
|
||||
goback = int(s[idx + 1])
|
||||
sed_args[3] = f'{s[:idx]}{s[idx + 2:]}'
|
||||
|
||||
n = 1
|
||||
while n <= 50:
|
||||
if 'i' in sed_args[3]:
|
||||
db_search = re.search(sed_args[1], msglist[-n], re.I)
|
||||
else:
|
||||
db_search = re.search(sed_args[1], msglist[-n])
|
||||
if db_search:
|
||||
if goback:
|
||||
# Check if the goback command was issued.
|
||||
goback -= 1
|
||||
n = n + 1
|
||||
continue
|
||||
a = n
|
||||
break
|
||||
else:
|
||||
a = False
|
||||
n = n + 1
|
||||
|
||||
if a:
|
||||
if '\x01ACTION' in msglist[-a][:7]:
|
||||
msg_len = irc.var.msg_len - 9 - len(i.channel) - 10 - 2
|
||||
sed_out = call_sed(msglist[-a][7:], sed_args)
|
||||
sed_out = sed_out.rstrip('\n').replace('\x01', "").replace('\ca', "")
|
||||
sed_out = p_truncate(sed_out, msg_len, 98, True)
|
||||
irc.privmsg(i.channel, f'\x01ACTION {sed_out}')
|
||||
else:
|
||||
msg_len = irc.var.msg_len - 9 - len(i.channel) - 2
|
||||
sed_out = call_sed(msglist[-a], sed_args).strip()
|
||||
sed_out = sed_out.rstrip('\n').replace('\x01', "").replace('\ca', "")
|
||||
sed_out = p_truncate(sed_out, msg_len, 98, True)
|
||||
irc.privmsg(i.channel, sed_out)
|
||||
|
||||
if sed_out:
|
||||
# We try to limit the string saved in the queue to avoid:
|
||||
# OSError: [Errno 7] Argument list too long: 'echo'
|
||||
# when calling 'echo' in @call_sed .
|
||||
# 512 chars are more than enough, since the bot will
|
||||
# never be able to send a message with that many.
|
||||
write(i.varget, i.varset, i.channel, sed_out)
|
||||
# write(dbc, i.channel, i.msg) # save commands
|
136
src/irc/modules/seen.py
Normal file
136
src/irc/modules/seen.py
Normal file
@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding=utf-8
|
||||
|
||||
# Seen Module for Drastikbot
|
||||
#
|
||||
# It logs the last activity of every user posting and returns it upon request.
|
||||
# It logs time, post, nickname, channel.
|
||||
#
|
||||
# user = The one used the bot (or posted a random line)
|
||||
# nick = The nickname requested by the user
|
||||
|
||||
'''
|
||||
Seen module for drastikbot.
|
||||
Copyright (C) 2018, 2021 drastik.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
any later version.
|
||||
|
||||
This program 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import datetime
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.auto = True
|
||||
self.commands = ["seen"]
|
||||
self.manual = {
|
||||
"desc": ("See when a user was last seen posting in a channel the"
|
||||
" bot has joined. If the user posted in channel other"
|
||||
" than the one the command was given from, then the bot"
|
||||
" will show that channel. Private messages with the bot"
|
||||
" are NOT saved."),
|
||||
"bot_commands": {
|
||||
"seen": {"usage": lambda x: f"{x}seen <nickname>"},
|
||||
"info": ("Example: <Alice>: .seen Bob / <Bot>:"
|
||||
" Bob was last seen 0:21:09 ago"
|
||||
" [2018-06-25 13:36:42 UTC], saying .help seen")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def update(channel, nick, msg, time, dbc):
|
||||
try:
|
||||
dbc.execute(
|
||||
'''CREATE TABLE IF NOT EXISTS seen (nick TEXT COLLATE NOCASE
|
||||
PRIMARY KEY, msg TEXT, time TEXT, channel TEXT);''')
|
||||
except Exception:
|
||||
# sqlite3.OperationalError: cannot commit - no transaction is active
|
||||
pass
|
||||
dbc.execute(
|
||||
'''INSERT OR IGNORE INTO seen (nick, msg, time, channel)
|
||||
VALUES (?, ?, ?, ?);''', (nick, msg, str(time), channel))
|
||||
dbc.execute('''UPDATE seen SET msg=?, time=?, channel=? WHERE nick=?;''',
|
||||
(msg, str(time), channel, nick))
|
||||
|
||||
|
||||
def fetch(nick, dbc):
|
||||
try:
|
||||
dbc.execute('SELECT nick, msg, time, channel '
|
||||
'FROM seen WHERE nick=?;''', (nick,))
|
||||
fetch = dbc.fetchone()
|
||||
nickFnd = fetch[0]
|
||||
msgFnd = fetch[1]
|
||||
timeFnd = datetime.datetime.strptime(fetch[2], "%Y-%m-%d %H:%M:%S")
|
||||
channelFnd = fetch[3]
|
||||
return (msgFnd, timeFnd, channelFnd, nickFnd)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
dbc = i.db[1].cursor()
|
||||
timestamp = datetime.datetime.utcnow().replace(microsecond=0)
|
||||
args = i.msg_nocmd.split()
|
||||
|
||||
if not i.cmd and not i.channel == i.nickname:
|
||||
# Avoid saving privmsges with the bot.
|
||||
update(i.channel, i.nickname, i.msg, timestamp, dbc)
|
||||
i.db[1].commit()
|
||||
return
|
||||
|
||||
if i.cmd == 'seen' and ((len(args) == 1 and len(i.msg) <= 30)
|
||||
or i.msg_nocmd == ''):
|
||||
|
||||
if not i.msg_nocmd:
|
||||
# If no nickname is given set the args[0] to the user's nickname
|
||||
args = [i.nickname]
|
||||
|
||||
get = fetch(args[0], dbc)
|
||||
if get:
|
||||
ago = timestamp - get[1]
|
||||
if '\x01ACTION' in get[0][:10]:
|
||||
toSend = (f'\x0312{get[3]}\x0F was last seen '
|
||||
f'\x0312{ago} ago\x0F [{get[1]} UTC], '
|
||||
f'doing \x0312{i.msg_nocmd} {get[0][8:]}\x0F')
|
||||
else:
|
||||
toSend = (f'\x0312{get[3]}\x0F was last seen '
|
||||
f'\x0312{ago} ago\x0F [{get[1]} UTC], '
|
||||
f'saying \x0312{get[0]}\x0F')
|
||||
if args[0].lower() == i.nickname.lower():
|
||||
# Check if the requested nickname is the user's nickname
|
||||
toSend = (f'\x0312You\x0F were last seen \x0312{ago} ago\x0F'
|
||||
f' [{get[1]} UTC], saying \x0312{get[0]}\x0F')
|
||||
else:
|
||||
toSend = f"Sorry, I haven't seen \x0312{args[0]}\x0F around"
|
||||
if args[0] == irc.var.nickname:
|
||||
# Check if the requested nickname is the bot's nickname
|
||||
toSend = "\x0304Who?\x0F"
|
||||
self_nick = True
|
||||
else:
|
||||
self_nick = False
|
||||
try:
|
||||
# If 'get' check the channel the user was last seen and send a
|
||||
# privmsg
|
||||
if get[2] == i.channel or self_nick:
|
||||
irc.privmsg(i.channel, toSend)
|
||||
else:
|
||||
toSend = f'{toSend} in \x0312{get[2]}\x0F'
|
||||
irc.privmsg(i.channel, toSend)
|
||||
except TypeError:
|
||||
# if not 'get' just send a privmsg
|
||||
irc.privmsg(i.channel, toSend)
|
||||
# Update the database
|
||||
if not i.channel == i.nickname: # Avoid saving privmsges with the bot.
|
||||
update(i.channel, i.nickname, i.msg, timestamp, dbc)
|
||||
i.db[1].commit()
|
76
src/irc/modules/tarot.py
Normal file
76
src/irc/modules/tarot.py
Normal file
@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding=utf-8
|
||||
|
||||
# Tarot Module for drastikbot
|
||||
#
|
||||
# Draws three cards from the tarot deck.
|
||||
# Usage: .tarot
|
||||
|
||||
'''
|
||||
Copyright (C) 2018 Newt Vodyanoy
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import random
|
||||
|
||||
|
||||
class Module():
|
||||
def __init__(self):
|
||||
self.commands = ['tarot']
|
||||
self.manual = {
|
||||
"desc": "Draws three cards from the tarot deck.",
|
||||
"bot_commands": {"tarot": {"usage": lambda x: f"{x}tarot"}}
|
||||
}
|
||||
|
||||
|
||||
major_arcana = [
|
||||
"The Fool",
|
||||
"The Magician",
|
||||
"The High Priestess",
|
||||
"The Empress",
|
||||
"The Emperor",
|
||||
"The Hierophant",
|
||||
"The Lovers",
|
||||
"The Chariot",
|
||||
"Strength",
|
||||
"The Hermit",
|
||||
"Wheel of Fortune",
|
||||
"Justice",
|
||||
"The Hanged Man",
|
||||
"Death",
|
||||
"Temperance",
|
||||
"Devil",
|
||||
"The Tower",
|
||||
"The Star",
|
||||
"The Moon",
|
||||
"The Sun",
|
||||
"Judgement",
|
||||
"The World"
|
||||
]
|
||||
suits = ["Swords", "Wands", "Coins", "Cups"]
|
||||
suit_cards = [
|
||||
"Ace", "Two", "Three", "Four", "Five", "Six", "Seven",
|
||||
"Eight", "Nine", "Ten", "Page", "Knight", "Queen", "King"
|
||||
]
|
||||
minor_arcana = list()
|
||||
for card in ((x, y) for x in suit_cards for y in suits):
|
||||
minor_arcana.append("The %s of %s" % card)
|
||||
deck = major_arcana + minor_arcana
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
cards = random.sample(deck, 3)
|
||||
if 'tarot' == i.cmd:
|
||||
irc.privmsg(i.channel, f'{cards[0]}, {cards[1]}, {cards[2]}')
|
98
src/irc/modules/tell.py
Normal file
98
src/irc/modules/tell.py
Normal file
@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding=utf-8
|
||||
|
||||
# Tell Module for Drastikbot
|
||||
#
|
||||
# It works "like" memoserv.
|
||||
# A user tells the bot to tell a message to a nick when that nick is seen.
|
||||
# .tell drastik drastikbot is down | .tell <NICKNAME> <MESSAGE>
|
||||
|
||||
'''
|
||||
Copyright (C) 2017 drastik.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
any later version.
|
||||
|
||||
This program 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import datetime
|
||||
from ignore import is_ignored
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.commands = ['tell']
|
||||
self.auto = True
|
||||
self.manual = {
|
||||
"desc": (
|
||||
"Send a message to a user through the bot. This is used to"
|
||||
" send messages to users that are AFK or not connected to "
|
||||
"the server. The bot will message the receiver as soon as "
|
||||
"they post to a channel that the bot has joined."
|
||||
),
|
||||
"bot_commands": {
|
||||
"tell": {"usage": lambda x: f"{x}tell <receiver> <message>"}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def add(receiver, msg, sender, dbc):
|
||||
timestamp = datetime.datetime.utcnow().replace(microsecond=0)
|
||||
dbc.execute("CREATE TABLE IF NOT EXISTS tell "
|
||||
"(receiver TEXT COLLATE NOCASE, msg TEXT, sender TEXT, "
|
||||
"timestamp TEXT, date INTEGER);")
|
||||
dbc.execute("INSERT INTO tell VALUES (?, ?, ?, ?, strftime('%s', 'now'));",
|
||||
# These are wrong but handled correctly. Fix it someday.
|
||||
(receiver, msg, str(timestamp), sender))
|
||||
|
||||
|
||||
def find(nick, irc, dbc):
|
||||
try:
|
||||
dbc.execute('SELECT sender, msg, timestamp FROM tell '
|
||||
'WHERE receiver=?;', (nick,))
|
||||
fetch = dbc.fetchall()
|
||||
for i in fetch:
|
||||
irc.privmsg(nick, f'\x0302{i[2]} [{i[0]} UTC]:\x0F')
|
||||
irc.privmsg(nick, i[1])
|
||||
dbc.execute('''DELETE FROM tell WHERE receiver=?;''', (nick,))
|
||||
# Delete messages older than 3600 * 24 * 30 * 3 seconds.
|
||||
dbc.execute('''DELETE FROM tell WHERE
|
||||
(strftime('%s', 'now') - date) > (3600 * 24 * 30 * 3);''')
|
||||
except Exception:
|
||||
# Nothing in the db yet, ignore errors.
|
||||
pass
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
dbc = i.db[1].cursor()
|
||||
if 'tell' == i.cmd:
|
||||
try:
|
||||
arg_list = i.msg_nocmd.split(' ', 1)
|
||||
receiver = arg_list[0]
|
||||
msg = arg_list[1]
|
||||
except IndexError:
|
||||
help_msg = f"Usage: {i.cmd_prefix}{i.cmd} <Receiver> <Message>"
|
||||
return irc.privmsg(i.channel, help_msg)
|
||||
if i.nickname.lower() == receiver.lower():
|
||||
return irc.privmsg(i.channel, 'You can tell yourself that.')
|
||||
if irc.var.curr_nickname.lower() == receiver.lower():
|
||||
return irc.privmsg(i.channel,
|
||||
f'{i.nickname}: I am here now, tell me.')
|
||||
if is_ignored(i, irc, receiver, i.nickname):
|
||||
return # say nothing
|
||||
add(receiver, msg, i.nickname, dbc)
|
||||
irc.privmsg(i.channel,
|
||||
f'{i.nickname}: I will tell {receiver} '
|
||||
'when they are around.')
|
||||
|
||||
find(i.nickname, irc, dbc)
|
||||
i.db[1].commit()
|
152
src/irc/modules/text.py
Normal file
152
src/irc/modules/text.py
Normal file
@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding=utf-8
|
||||
|
||||
# Text Module for Drastikbot
|
||||
#
|
||||
# Transform textual input to various other styles.
|
||||
|
||||
'''
|
||||
Copyright (C) 2019 drastik.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
any later version.
|
||||
|
||||
This program 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import random
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.commands = [
|
||||
"ae", "text-c", "text-nc", "text-s", "text-ns", "flag", "cirrus",
|
||||
"strike", "strikethrough"
|
||||
]
|
||||
self.manual = {
|
||||
"desc": "Text transformation tools",
|
||||
"bot_commands": {
|
||||
"ae": {"usage": lambda p: f"{p}ae <text>",
|
||||
"info": "Example: Hello, World!"},
|
||||
"text-c": {"usage": lambda p: f"{p}text-c <text>",
|
||||
"info": "Example: ⒽⒺⓁⓁⓄ, ⓌⓄⓇⓁⒹ!"},
|
||||
"text-nc": {"usage": lambda p: f"{p}text-nc <text>",
|
||||
"info": "Example: 🅗🅔🅛🅛🅞, 🅦🅞🅡🅛🅓!"},
|
||||
"text-s": {"usage": lambda p: f"{p}text-s <text>",
|
||||
"info": "Example: 🄷🄴🄻🄻🄾, 🅆🄾🅁🄻🄳!"},
|
||||
"text-ns": {"usage": lambda p: f"{p}text-ns <text>",
|
||||
"info": "Example: 🅷🅴🅻🅻🅾, 🆆🅾🆁🅻🅳!"},
|
||||
"flag": {"usage": lambda p: f"{p}flag <text>",
|
||||
"info": ("Transforms two letter country codes to"
|
||||
" regional indicator symbols.")},
|
||||
"cirrus": {"usage": lambda p: f"{p}cirrus <text>",
|
||||
"info": "Example: Hello, WWorld!"},
|
||||
"strike": {"usage": lambda p: f"{p}strike <text>",
|
||||
"info": "Example: H̶e̶l̶l̶o̶,̶ ̶W̶o̶r̶l̶d̶!̶",
|
||||
"alias": ["strikethrough"]},
|
||||
"strikethrough": {
|
||||
"usage": lambda p: f"{p}strikethrough <text>",
|
||||
"info": "Example: H̶e̶l̶l̶o̶,̶ ̶W̶o̶r̶l̶d̶!̶",
|
||||
"alias": ["strike"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# https://en.wikipedia.org/wiki/Halfwidth_and_fullwidth_forms_(Unicode_block)
|
||||
FULLWIDTH_MAP = dict((i, i + 0xFEE0) for i in range(0x21, 0x7F))
|
||||
FULLWIDTH_MAP[0x20] = 0x3000
|
||||
|
||||
# https://en.wikipedia.org/wiki/Enclosed_Alphanumerics
|
||||
_CIRCLED_NUM_MAP = dict((i, (i - 0x31) + 0x2460) for i in range(0x30, 0x3A))
|
||||
_CIRCLED_NUM_MAP[0x30] = 0x24EA # Set the actual Circled digit zero character
|
||||
_CIRCLED_ALP_U_MAP = dict((i, (i - 0x41) + 0x24B6) for i in range(0x41, 0x5B))
|
||||
_CIRCLED_ALP_L_MAP = dict((i, (i - 0x61) + 0x24B6) for i in range(0x61, 0x7B))
|
||||
CIRCLED_MAP = {**_CIRCLED_NUM_MAP, **_CIRCLED_ALP_U_MAP, **_CIRCLED_ALP_L_MAP}
|
||||
|
||||
# https://en.wikipedia.org/wiki/Enclosed_Alphanumeric_Supplement
|
||||
_NEGATIVE_CIRCLED_ALP_U_MAP = dict(
|
||||
(i, (i - 0x41) + 0x1F150) for i in range(0x41, 0x5B))
|
||||
_NEGATIVE_CIRCLED_ALP_L_MAP = dict(
|
||||
(i, (i - 0x61) + 0x1F150) for i in range(0x61, 0x7B))
|
||||
NEGATIVE_CIRCLED_MAP = {
|
||||
**_NEGATIVE_CIRCLED_ALP_U_MAP,
|
||||
**_NEGATIVE_CIRCLED_ALP_L_MAP
|
||||
}
|
||||
|
||||
_SQUARED_ALP_U_MAP = dict((i, (i - 0x41) + 0x1F130) for i in range(0x41, 0x5B))
|
||||
_SQUARED_ALP_L_MAP = dict((i, (i - 0x61) + 0x1F130) for i in range(0x61, 0x7B))
|
||||
SQUARED_MAP = {**_SQUARED_ALP_U_MAP, **_SQUARED_ALP_L_MAP}
|
||||
|
||||
_NEGATIVE_SQUARED_ALP_U_MAP = dict(
|
||||
(i, (i - 0x41) + 0x1F170) for i in range(0x41, 0x5B))
|
||||
_NEGATIVE_SQUARED_ALP_L_MAP = dict(
|
||||
(i, (i - 0x61) + 0x1F170) for i in range(0x61, 0x7B))
|
||||
NEGATIVE_SQUARED_MAP = {
|
||||
**_NEGATIVE_SQUARED_ALP_U_MAP,
|
||||
**_NEGATIVE_SQUARED_ALP_L_MAP
|
||||
}
|
||||
|
||||
# https://en.wikipedia.org/wiki/Regional_Indicator_Symbol
|
||||
_REGIONAL_INDICATOR_SYMBOL_U_MAP = dict(
|
||||
(i, (i - 0x41) + 0x1F1E6) for i in range(0x41, 0x5B))
|
||||
_REGIONAL_INDICATOR_SYMBOL_L_MAP = dict(
|
||||
(i, (i - 0x61) + 0x1F1E6) for i in range(0x61, 0x7B))
|
||||
REGIONAL_INDICATOR_SYMBOL_MAP = {
|
||||
**_REGIONAL_INDICATOR_SYMBOL_U_MAP,
|
||||
**_REGIONAL_INDICATOR_SYMBOL_L_MAP
|
||||
}
|
||||
|
||||
command_map_d = {
|
||||
"ae": FULLWIDTH_MAP,
|
||||
"text-c": CIRCLED_MAP,
|
||||
"text-nc": NEGATIVE_CIRCLED_MAP,
|
||||
"text-s": SQUARED_MAP,
|
||||
"text-ns": NEGATIVE_SQUARED_MAP,
|
||||
"flag": REGIONAL_INDICATOR_SYMBOL_MAP,
|
||||
}
|
||||
|
||||
|
||||
def cirrus(text):
|
||||
words = text.split()
|
||||
wc = len(words)
|
||||
cc = 0
|
||||
for i in range(wc):
|
||||
if random.uniform(0, 1) < 0.38:
|
||||
cc += 1
|
||||
words[i] = f"{words[i][0]}{words[i]}"
|
||||
|
||||
if cc == 0:
|
||||
i = random.randint(0, wc - 1)
|
||||
words[i] = f"{words[i][0]}{words[i]}"
|
||||
|
||||
return " ".join(words)
|
||||
|
||||
|
||||
def strikethrough(text):
|
||||
return "\u0336".join(text) + '\u0336'
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
if not i.msg_nocmd:
|
||||
return
|
||||
s = i.msg_nocmd
|
||||
|
||||
if i.cmd == "cirrus":
|
||||
return irc.privmsg(i.channel, cirrus(s))
|
||||
if i.cmd == "strike" or i.cmd == "strikethrough":
|
||||
return irc.privmsg(i.channel, strikethrough(s))
|
||||
|
||||
t = s.translate(command_map_d[i.cmd])
|
||||
if i.cmd == "ae" and t == s:
|
||||
t = s.replace("", " ")[1: -1]
|
||||
|
||||
irc.privmsg(i.channel, t)
|
160
src/irc/modules/theo.py
Normal file
160
src/irc/modules/theo.py
Normal file
@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding=utf-8
|
||||
|
||||
# Quotes from mg's theo.c
|
||||
# http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/mg/Attic/theo.c
|
||||
|
||||
'''
|
||||
Copyright (C) 2018 drastik.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
any later version.
|
||||
|
||||
This program 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import random
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.commands = ['theo']
|
||||
self.manual = {
|
||||
"desc": ("Post quotes from OpenBSD mg's theo.c mode. This mode has"
|
||||
" been removed from mg. This module includes all the"
|
||||
" quotes from the last commit to theo.c before it was"
|
||||
" removed: "
|
||||
"http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/"
|
||||
"mg/Attic/theo.c"),
|
||||
"bot_commands": {"theo": {"usage": lambda x: f"{x}theo"}}
|
||||
}
|
||||
|
||||
|
||||
talk = [
|
||||
"Write more code.",
|
||||
"Make more commits.",
|
||||
"That's because you have been slacking.",
|
||||
"slacker!",
|
||||
"That's what happens when you're lazy.",
|
||||
"idler!",
|
||||
"slackass!",
|
||||
"lazy bum!",
|
||||
"Stop slacking you lazy bum!",
|
||||
"slacker slacker lazy bum bum bum slacker!",
|
||||
"I could search... but I'm a lazy bum ;)",
|
||||
"sshutup sshithead, ssharpsshooting susshi sshplats ssharking assholes.",
|
||||
"Lazy bums slacking on your asses.",
|
||||
"35 commits an hour? That's pathetic!",
|
||||
"Fine software takes time to prepare. Give a little slack.",
|
||||
"I am just stating a fact",
|
||||
"you bring new meaning to the terms slackass. I will have to invent a new term.",
|
||||
"if they cut you out, muddy their back yards",
|
||||
"Make them want to start over, and play nice the next time.",
|
||||
"It is clear that this has not been thought through.",
|
||||
"avoid using abort(). it is not nice.",
|
||||
"That's the most ridiculous thing I've heard in the last two or three minutes!",
|
||||
"I'm not just doing this for crowd response. I need to be right.",
|
||||
"I'd put a fan on my bomb.. And blinking lights...",
|
||||
"I love to fight",
|
||||
"No sane people allowed here. Go home.",
|
||||
"you have to stop peeing on your breakfast",
|
||||
"feature requests come from idiots",
|
||||
"henning and darren / sitting in a tree / t o k i n g / a joint or three",
|
||||
"KICK ASS. TIME FOR A JASON LOVE IN! WE CAN ALL GET LOST IN HIS HAIR!",
|
||||
"shame on you for following my rules.",
|
||||
"altq's parser sucks dead whale farts through the finest chemistry pipette's",
|
||||
"screw this operating system shit, i just want to drive!",
|
||||
"Search for fuck. Anytime you see that word, you have a paragraph to write.",
|
||||
"Yes, but the ports people are into S&M.",
|
||||
"Buttons are for idiots.",
|
||||
"We are not hackers. We are turd polishing craftsmen.",
|
||||
"who cares. style(9) can bite my ass",
|
||||
"It'd be one fucking happy planet if it wasn't for what's under this fucking sticker.",
|
||||
"I would explain, but I am too drunk.",
|
||||
"you slackers don't deserve pictures yet",
|
||||
"Vegetarian my ass",
|
||||
"Wait a minute, that's a McNally's!",
|
||||
"don't they recognize their moral responsibility to entertain me?",
|
||||
"#ifdef is for emacs developers.",
|
||||
"Many well known people become net-kooks in their later life, because they lose touch with reality.",
|
||||
"You're not allowed to have an opinion.",
|
||||
"tweep tweep tweep",
|
||||
"Quite frankly, SSE's alignment requirement is the most utterly retarded idea since eating your own shit.",
|
||||
"Holy verbose prom startup Batman.",
|
||||
"Any day now, when we sell out.",
|
||||
"optimism in man kind does not belong here",
|
||||
"First user who tries to push this button, he pounds into the ground with a rant of death.",
|
||||
"we did farts. now we do sperm. we are cutting edge.",
|
||||
"the default configuration is a mixture of piss, puke, shit, and bloody entrails.",
|
||||
"Stop wasting your time reading people's licenses.",
|
||||
"doing it with environment variables is OH SO SYSTEM FIVE LIKE OH MY GOD PASS ME THE SPOON",
|
||||
"Linux is fucking POO, not just bad, bad REALLY REALLY BAD",
|
||||
"penguins are not much more than chickens that swim.",
|
||||
"i am a packet sniffing fool, let me wipe my face with my own poo",
|
||||
"Whiners. They scale really well.",
|
||||
"in your world, you would have a checklist of 50 fucking workarounds just to make a coffee.",
|
||||
"for once, I have nothing to say.",
|
||||
"You have no idea how fucked we are",
|
||||
"You can call it fart if you want to.",
|
||||
"wavelan is a battle field",
|
||||
"You are in a maze of gpio pins, all alike, all undocumented, and a few are wired to bombs.",
|
||||
"And that is why humppa sucks... cause it has no cause.",
|
||||
"cache aliasing is a problem that would have stopped in 1992 if someone had killed about 5 people who worked at Sun.",
|
||||
"Don't spread rumours about me being gentle.",
|
||||
"If municipal water filtering equipment was built by the gcc developers, the western world would be dead by now.",
|
||||
"kettenis supported a new machine in my basement and all I got to do was fix a 1 character typo in his html page commit.",
|
||||
"industry told us a lesson: when you're an asshole, they mail you hardware",
|
||||
"I was joking, really. I think I am funny :-)",
|
||||
"the kernel is a harsh mistress",
|
||||
"Have I ever been subtle? If my approach ever becomes subtle, shoot me.",
|
||||
"the acpi stabs you in the back. the acpi stabs you in the back. you die ...",
|
||||
"My cats are more observant than you.",
|
||||
"our kernels have no bugs",
|
||||
"style(9) has all these fascist rules, and i have a problem with some of them because i didn't come up with them",
|
||||
"I'm not very reliable",
|
||||
"I don't like control",
|
||||
"You aren't being conservative -- you are trying to be a caveman.",
|
||||
"nfs loves everyone",
|
||||
"basically, dung beetles fucking. that's what kerberosV + openssl is like",
|
||||
"I would rather run Windows than use vi.",
|
||||
"if you assign that responsibility to non-hikers I will walk over and cripple you now.",
|
||||
"i ojbect two yoru splelng of achlhlocis.",
|
||||
"We have two kinds of developers - those that deal with their own shit and those that deal with other people's shit.",
|
||||
"If people keep adding such huge stuff, soon mg will be bigger than emacs.",
|
||||
"this change comes down to: This year, next year, 5 years from now, 10 years from now, or Oh fuck.",
|
||||
"backwards compatibility is king, and will remain king, until 2038.",
|
||||
"I don't know if the Internet's safe yet.",
|
||||
"Those who don't understand Unix are condemned to reinvent Multics in a browser",
|
||||
"Don't tell anybody I said that.",
|
||||
"Complaint forms are handled in another department.",
|
||||
"You'd be safer using Windows than the code which was just deleted.",
|
||||
"Shit should not be shared.",
|
||||
"the randomization in this entire codebase is a grand experiment in stupid",
|
||||
"My mailbox is full of shock.",
|
||||
"my integer overflow spidey senses are tingling.",
|
||||
"I'm just trying to improve the code...",
|
||||
"It's a pleasure to work on code you can't make worse.",
|
||||
"It's largely bad style to do (int)sizeof",
|
||||
"When I see Makefile.in, I know that \"in\" is short for \"insane\".",
|
||||
"This is the beer. And that's why we need a hackathon.",
|
||||
"Kill the past with fire, and declare Duran Duran is less cool today. Await remixes of the same thing performed by new talent.",
|
||||
"Where did my \"fuck backwards compat\" compatriots go?",
|
||||
"I want a new vax, one that's not so slow.",
|
||||
"This sausage is made from unsound meat.",
|
||||
"The people who wrote this code are not on your side.",
|
||||
"Well finally everyone can see that the shit is really shitty.",
|
||||
"All that complexity stopped us from getting flying cars by today."
|
||||
]
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
msg = f"{i.nickname}: {random.SystemRandom().choice(talk)}"
|
||||
irc.privmsg(i.channel, msg)
|
98
src/irc/modules/urbandict.py
Normal file
98
src/irc/modules/urbandict.py
Normal file
@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding=utf-8
|
||||
|
||||
# Urban Dictionary Module for Drastikbot.
|
||||
#
|
||||
# It uses api.urbandictionary.com/v0/.
|
||||
#
|
||||
# Depends:
|
||||
# - requests :: $ pip3 install requests
|
||||
|
||||
'''
|
||||
Copyright (C) 2018 drastik.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
any later version.
|
||||
|
||||
This program 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import requests
|
||||
from dbot_tools import p_truncate
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.commands = ['ud']
|
||||
self.manual = {
|
||||
"desc": "Search https://www.urbandictionary.com/ for definitions.",
|
||||
"bot_commands": {
|
||||
"ud": {"usage": lambda x: f"{x}ud <query> [--def <num>]",
|
||||
"info": ("The --def option allows you to select other"
|
||||
" definitions. Example: .ud irc --def 2")}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def ud(query, res):
|
||||
u = f'http://api.urbandictionary.com/v0/define?term={query}'
|
||||
r = requests.get(u, timeout=5)
|
||||
j = r.json()['list'][res]
|
||||
word = j['word']
|
||||
definition = p_truncate(j['definition'], msg_len, 71, True)
|
||||
example = p_truncate(j['example'], msg_len, 15, True)
|
||||
author = j['author']
|
||||
t_up = j['thumbs_up']
|
||||
t_down = j['thumbs_down']
|
||||
pl = j['permalink'].rsplit('/', 1)[0]
|
||||
return (word, definition, example, author, t_up, t_down, pl)
|
||||
|
||||
|
||||
def query(args):
|
||||
# Get the args list and the commands
|
||||
# Join the list to a string and return
|
||||
_args = args[:]
|
||||
try:
|
||||
idx = _args.index('--def')
|
||||
del _args[idx]
|
||||
del _args[idx]
|
||||
except ValueError:
|
||||
pass
|
||||
return ' '.join(_args)
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
logo = '\x0300,01Urban\x0F\x0308,01Dictionary\x0F'
|
||||
args = i.msg_nocmd.split()
|
||||
if not args:
|
||||
help_msg = f"Usage: {i.cmd_prefix}{i.cmd} <QUERY> [--def <NUM>]"
|
||||
return irc.privmsg(i.channel, help_msg)
|
||||
if '--def' in args:
|
||||
idx = args.index('--def')
|
||||
res = int(args[idx + 1]) - 1
|
||||
else:
|
||||
res = 0
|
||||
q = query(args)
|
||||
try:
|
||||
global msg_len
|
||||
# msg_len = msg_len - "PRIVMSG :" - chars - \r\n
|
||||
msg_len = irc.var.msg_len - 9 - 101 - 2
|
||||
u = ud(q, res)
|
||||
rpl = (f"{logo}: \x02{u[0]}\x0F"
|
||||
f" | {u[1]}"
|
||||
f" | \x02E.g:\x0F {u[2]}"
|
||||
f" | \x02Author:\x0F {u[3]}"
|
||||
f" | \x0303+{u[4]}\x0F"
|
||||
f" | \x0304-{u[5]}\x0F"
|
||||
f" | \x02Link:\x0F {u[6]}")
|
||||
except IndexError:
|
||||
rpl = (f"{logo}: No definition was found for \x02{q}\x0F")
|
||||
irc.privmsg(i.channel, rpl)
|
355
src/irc/modules/url.py
Normal file
355
src/irc/modules/url.py
Normal file
@ -0,0 +1,355 @@
|
||||
# coding=utf-8
|
||||
|
||||
# URL Module for Drastikbot
|
||||
#
|
||||
# Depends:
|
||||
# - requests :: $ pip3 install requests
|
||||
# - beautifulsoup :: $ pip3 install beautifulsoup4
|
||||
|
||||
'''
|
||||
Copyright (C) 2017-2020 drastik.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, version 3 only.
|
||||
|
||||
This program 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import re
|
||||
import math
|
||||
import json
|
||||
import urllib.parse
|
||||
import requests
|
||||
import bs4
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.auto = True
|
||||
|
||||
|
||||
# ----- Constants ----- #
|
||||
parser = 'html.parser'
|
||||
user_agent = "w3m/0.52"
|
||||
accept_lang = "en-US"
|
||||
nsfw_tag = "\x0304[NSFW]\x0F"
|
||||
data_limit = 69120
|
||||
# --------------------- #
|
||||
|
||||
|
||||
def remove_formatting(msg):
|
||||
'''Remove IRC String formatting codes'''
|
||||
# - Regex -
|
||||
# Capture "x03N,M". Should be the first called:
|
||||
# (\\x03[0-9]{0,2},{1}[0-9]{1,2})
|
||||
# Capture "x03N". Catch all color codes.
|
||||
# (\\x03[0-9]{0,2})
|
||||
# Capture the other formatting codes
|
||||
line = re.sub(r'(\\x03[0-9]{0,2},{1}[0-9]{1,2})', '', msg)
|
||||
line = re.sub(r'(\\x03[0-9]{1,2})', '', line)
|
||||
line = line.replace("\\x03", "")
|
||||
line = line.replace("\\x02", "")
|
||||
line = line.replace("\\x1d", "")
|
||||
line = line.replace("\\x1D", "")
|
||||
line = line.replace("\\x1f", "")
|
||||
line = line.replace("\\x1F", "")
|
||||
line = line.replace("\\x16", "")
|
||||
line = line.replace("\\x0f", "")
|
||||
line = line.replace("\\x0F", "")
|
||||
return line
|
||||
|
||||
|
||||
def convert_size(size_bytes):
|
||||
# https://stackoverflow.com/
|
||||
# questions/5194057/better-way-to-convert-file-sizes-in-python
|
||||
if size_bytes == 0:
|
||||
return "0B"
|
||||
size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
|
||||
i = int(math.floor(math.log(size_bytes, 1024)))
|
||||
p = math.pow(1024, i)
|
||||
s = round(size_bytes / p, 2)
|
||||
return "%s %s" % (s, size_name[i])
|
||||
|
||||
|
||||
def get_url(msg):
|
||||
'''Search a string for urls and return a list of them.'''
|
||||
str_l = msg.split()
|
||||
req_l = ["http://", "https://"] # add "." for parse urls without a scheme
|
||||
urls = [u for u in str_l if any(r in u for r in req_l)]
|
||||
# Avoid parsing IPv4s that are not complete (IPs like: 1.1):
|
||||
# Useful when a scheme is not required to parse a URL.
|
||||
# urls = [u for u in urls if u.count('.') == 3 or u.upper().isupper()]
|
||||
return urls
|
||||
|
||||
|
||||
def default_parser(u):
|
||||
'''
|
||||
Visit each url and check if there is html content
|
||||
served. If there is try to get the <title></title>
|
||||
tag. If there is not try to read the http headers
|
||||
to find 'content-type' and 'content-length'.
|
||||
'''
|
||||
data = ""
|
||||
output = ""
|
||||
try:
|
||||
r = requests.get(u, stream=True,
|
||||
headers={"user-agent": user_agent,
|
||||
"Accept-Language": accept_lang},
|
||||
timeout=5)
|
||||
except Exception:
|
||||
return False
|
||||
for i in r.iter_content(chunk_size=512, decode_unicode=False):
|
||||
data += i.decode('utf-8', errors='ignore')
|
||||
if len(data) > data_limit or '</head>' in data.lower():
|
||||
break
|
||||
r.close()
|
||||
soup = bs4.BeautifulSoup(data, parser)
|
||||
try:
|
||||
output += soup.head.title.text.strip()
|
||||
except Exception:
|
||||
try:
|
||||
output += r.headers['content-type']
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
h_length = convert_size(float(r.headers['content-length']))
|
||||
if output:
|
||||
output += f", Size: {h_length}"
|
||||
else:
|
||||
output += h_length
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
if "RTA-5042-1996-1400-1577-RTA" in data:
|
||||
output = f"{nsfw_tag} {output}"
|
||||
elif r.headers["Rating"] == "RTA-5042-1996-1400-1577-RTA":
|
||||
output = f"{nsfw_tag} {output}"
|
||||
except KeyError:
|
||||
pass
|
||||
return output, data
|
||||
|
||||
|
||||
# #
|
||||
# BEGIN: Website Handling Functions (by url) #
|
||||
# #
|
||||
def youtube(url):
|
||||
'''Visit a video and get it's information.'''
|
||||
logo = "\x0300,04 ► \x0F"
|
||||
u = f"https://www.youtube.com/oembed?url={url}"
|
||||
r = requests.get(u, timeout=10)
|
||||
if r:
|
||||
j = r.json()
|
||||
return (f"{logo}: {j['title']}"
|
||||
f" | \x02Channel:\x0F {j['author_name']}")
|
||||
else:
|
||||
out = default_parser(url)[0]
|
||||
return f"{logo}: {out}"
|
||||
|
||||
|
||||
def lainchan(url):
|
||||
logo = "\x0309lainchan\x0F"
|
||||
if "/res/" in url:
|
||||
board = url.split("lainchan.org/")[1].split("/", 1)[0]
|
||||
board = urllib.parse.unquote(board)
|
||||
u = url.replace(".html", ".json")
|
||||
post_no = False
|
||||
if ".html#" in url:
|
||||
post_no = url.split("#")[1][1:]
|
||||
r = requests.get(u, timeout=10).json()
|
||||
try:
|
||||
title = r["posts"][0]["sub"]
|
||||
except KeyError:
|
||||
title = f'{r["posts"][0]["com"][:80]}...'
|
||||
replies = len(r["posts"]) - 1
|
||||
files = 0
|
||||
for i in r["posts"]:
|
||||
if "filename" in i:
|
||||
files += 1
|
||||
if "extra_files" in i:
|
||||
files += len(i["extra_files"])
|
||||
if post_no:
|
||||
for i in r["posts"]:
|
||||
if int(post_no) != i["no"]:
|
||||
continue
|
||||
post_text = bs4.BeautifulSoup(i["com"], parser).get_text()[:50]
|
||||
return (f"{logo} \x0306/{board}/\x0F {title} "
|
||||
f"\x02->\x0F \x0302{post_text}...\x0F | "
|
||||
f"\x02Replies:\x0F {replies} - \x02Files:\x0F {files}")
|
||||
|
||||
return (f"{logo} \x0306/{board}/\x0F {title} | "
|
||||
f"\x02Replies:\x0F {replies} - \x02Files:\x0F {files}")
|
||||
else:
|
||||
out = default_parser(url)[0]
|
||||
return f"{logo}: {out}"
|
||||
|
||||
|
||||
def imgur(url):
|
||||
try:
|
||||
up = urllib.parse.urlparse(url)
|
||||
host = up.hostname
|
||||
path = up.path
|
||||
if host[:2] == "i.":
|
||||
host = host[2:]
|
||||
path = path.rsplit(".", 1)[0]
|
||||
u = f"https://{host}{path}"
|
||||
else:
|
||||
u = url
|
||||
|
||||
r = requests.get(u, timeout=10)
|
||||
s = "widgetFactory.mergeConfig('gallery', "
|
||||
b = r.text.index(s) + len(s)
|
||||
e = r.text.index(");", b)
|
||||
t = r.text[b:e]
|
||||
|
||||
s = "image :"
|
||||
b = t.index(s) + len(s)
|
||||
e = t.index("},", b)
|
||||
t = t[b:e] + "}"
|
||||
|
||||
j = json.loads(t)
|
||||
title = j["title"]
|
||||
mimetype = j["mimetype"]
|
||||
size = j["size"]
|
||||
width = j["width"]
|
||||
height = j["height"]
|
||||
nsfw = j["nsfw"]
|
||||
|
||||
output = ""
|
||||
if nsfw:
|
||||
output += f"{nsfw_tag} "
|
||||
output += f"{title} - Imgur"
|
||||
output += f" | {mimetype}, Size: {convert_size(size)}"
|
||||
output += f", {width}x{height}"
|
||||
return output
|
||||
except Exception:
|
||||
return default_parser(url)[0]
|
||||
|
||||
|
||||
def nitter(url):
|
||||
logo = "\x02Nitter\x0f"
|
||||
output, data = default_parser(url)
|
||||
try:
|
||||
soup = bs4.BeautifulSoup(data, parser)
|
||||
user = soup.find(attrs={"property": "og:title"})['content']
|
||||
post = soup.find(attrs={"property": "og:description"})['content']
|
||||
if post:
|
||||
return f"{logo}: \x0305{user}\x0f {post}"
|
||||
return output
|
||||
except Exception:
|
||||
return output
|
||||
|
||||
|
||||
def twitter(url):
|
||||
logo = "\x0311twitter\x0F"
|
||||
u = f"https://publish.twitter.com/oembed?url={url}"
|
||||
r = requests.get(u, timeout=10,
|
||||
headers={"user-agent": user_agent,
|
||||
"Accept-Language": accept_lang})
|
||||
if r:
|
||||
j = r.json()
|
||||
html = j["html"]
|
||||
soup = bs4.BeautifulSoup(html, parser)
|
||||
tweet = soup.get_text(separator=" ")
|
||||
return f"{logo}: {tweet}"
|
||||
else:
|
||||
out = default_parser(url)[0]
|
||||
return f"{logo}: {out}"
|
||||
# #
|
||||
# END: Website Handling Functions (by url) #
|
||||
# #
|
||||
|
||||
|
||||
hosts_d = {
|
||||
"youtube.com": youtube,
|
||||
"youtu.be": youtube,
|
||||
"lainchan.org": lainchan,
|
||||
"i.imgur.com": imgur,
|
||||
"imgur.com": imgur,
|
||||
"nitter.net": nitter,
|
||||
"twitter.com": twitter
|
||||
}
|
||||
|
||||
|
||||
def _get_title_from_host(u):
|
||||
host = urllib.parse.urlparse(u).hostname
|
||||
if host[:4] == "www.":
|
||||
host = host[4:]
|
||||
if host not in hosts_d:
|
||||
return default_parser(u) # It's a tuple
|
||||
else:
|
||||
return hosts_d[host](u), False
|
||||
|
||||
|
||||
# #
|
||||
# BEGIN: Website Handling Functions (by title) #
|
||||
# #
|
||||
def pleroma(data):
|
||||
logo = "\x0308Pleroma\x0F"
|
||||
soup = bs4.BeautifulSoup(data, parser)
|
||||
t = soup.find(attrs={"property": "og:description"})['content']
|
||||
t = t.split(": ", 1)
|
||||
poster = t[0]
|
||||
post = t[1]
|
||||
return f"{logo}: \x0305{poster}\x0F {post}"
|
||||
# #
|
||||
# END: Website Handling Functions (by title) #
|
||||
# #
|
||||
|
||||
|
||||
titles_d = {
|
||||
"Pleroma": pleroma
|
||||
}
|
||||
|
||||
|
||||
def _get_title_from_title(title, data):
|
||||
'''
|
||||
Used to get data from the <head> when the <title> isn't very helpful
|
||||
'''
|
||||
if title in titles_d:
|
||||
try:
|
||||
return titles_d[title](data)
|
||||
except Exception:
|
||||
return title
|
||||
else:
|
||||
return title
|
||||
|
||||
|
||||
def get_title(u):
|
||||
title, data = _get_title_from_host(u)
|
||||
if data:
|
||||
title = _get_title_from_title(title, data)
|
||||
return title
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
# - Raw undecoded message clean up.
|
||||
# Remove /r/n and whitespace
|
||||
msg = i.msg_raw.strip()
|
||||
# Convert the bytes to a string,
|
||||
# split the irc commands from the text message,
|
||||
# remove ' character from the end of the string.
|
||||
msg = str(msg).split(' :', 1)[1][:-1]
|
||||
# Remove all IRC formatting codes
|
||||
msg = remove_formatting(msg)
|
||||
# msg = info[2]
|
||||
|
||||
urls = get_url(msg)
|
||||
prev_u = set() # Already visited URLs, used to avoid spamming.
|
||||
for u in urls:
|
||||
if not (u.startswith('http://') or u.startswith('https://')):
|
||||
u = f'http://{u}'
|
||||
if u in prev_u:
|
||||
return
|
||||
title = get_title(u)
|
||||
if not title:
|
||||
continue
|
||||
|
||||
irc.privmsg(i.channel, title)
|
||||
prev_u.add(u)
|
64
src/irc/modules/user_auth.py
Normal file
64
src/irc/modules/user_auth.py
Normal file
@ -0,0 +1,64 @@
|
||||
# coding=utf-8
|
||||
|
||||
# This is a core module for Drastikbot.
|
||||
# It provides functions for checking a user's authentication status.
|
||||
|
||||
'''
|
||||
Copyright (C) 2018-2019 drastik.org
|
||||
|
||||
This file is part of drastikbot.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, version 3 only.
|
||||
|
||||
This program 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import time
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.msgtypes = ['NOTICE']
|
||||
self.commands = ['whois']
|
||||
self.auto = True
|
||||
|
||||
|
||||
def user_auth(i, irc, nickname, timeout=10):
|
||||
to = time.time() + timeout
|
||||
irc.privmsg("NickServ", f"ACC {nickname}")
|
||||
irc.privmsg("NickServ", f"STATUS {nickname}")
|
||||
i.varset(nickname, "_pending")
|
||||
while True:
|
||||
time.sleep(.5)
|
||||
auth = i.varget(nickname)
|
||||
if auth != "_pending":
|
||||
return auth
|
||||
if time.time() > to:
|
||||
return False
|
||||
|
||||
|
||||
def nickserv_handler(i):
|
||||
ls = i.msg_params.split()
|
||||
if ls[1] == 'ACC' and i.varget(ls[0]) == "_pending":
|
||||
if '3' in i.msg_params:
|
||||
i.varset(ls[0], True)
|
||||
else:
|
||||
i.varset(ls[0], False)
|
||||
elif ls[0] == 'STATUS' and i.varget(ls[1]) == "_pending":
|
||||
if '3' in i.msg_params:
|
||||
i.varset(ls[1], True)
|
||||
else:
|
||||
i.varset(ls[1], False)
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
if i.nickname == 'NickServ':
|
||||
nickserv_handler(i)
|
400
src/irc/modules/weather.py
Normal file
400
src/irc/modules/weather.py
Normal file
@ -0,0 +1,400 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding=utf-8
|
||||
|
||||
# Weather Module for Drastikbot
|
||||
#
|
||||
# Provides weather information from http://wttr.in
|
||||
#
|
||||
# Depends:
|
||||
# - requests :: $ pip3 install requests
|
||||
|
||||
'''
|
||||
Copyright (C) 2018 drastik.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
any later version.
|
||||
|
||||
This program 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import urllib.parse
|
||||
import requests
|
||||
from user_auth import user_auth
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.commands = ['weather', 'weather_set', 'weather_auth']
|
||||
self.manual = {
|
||||
"desc": "Show weather information from http://wttr.in",
|
||||
"bot_commands": {
|
||||
"weather": {
|
||||
"usage": lambda x: (
|
||||
f"{x}weather <location / airport code / @domain"
|
||||
" / IP address / area code / GPS coordinates>"
|
||||
),
|
||||
"info": "Get weather information."
|
||||
},
|
||||
"weather_set": {
|
||||
"usage": lambda x: (
|
||||
f"{x}weather_set <location / airport code / @domain"
|
||||
" / IP address / area code / GPS coordinates>"
|
||||
),
|
||||
"info": (
|
||||
"Set your default location. If a location has been"
|
||||
" set, calling the weather command without arguments"
|
||||
" will return the weather for that location, otherwise"
|
||||
" you will be asked to provide a location. To unset"
|
||||
" your location use .weather_set without any"
|
||||
" arguements."
|
||||
)
|
||||
},
|
||||
"weather_auth": {
|
||||
"usage": lambda x: f"{x}weather_auth",
|
||||
"info": "Toggle NickServ authentication for weather_set"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Helper functions:
|
||||
|
||||
def unit_swap(unit):
|
||||
"""Change the unit string from °C to °F or from km/h to mph and opposite"""
|
||||
unit_d = {
|
||||
"°C": "°F",
|
||||
"°F": "°C",
|
||||
"km/h": "mph",
|
||||
"mph": "km/h"
|
||||
}
|
||||
return unit_d.get(unit)
|
||||
|
||||
|
||||
# Temperature
|
||||
|
||||
def temperature_color(temperature, unit_in, unit_out):
|
||||
"""Colorize and convert the temperature."""
|
||||
tempcolor_d = { # celsius: color
|
||||
-12: "02", -9: "12", -6: "11", 2: "10",
|
||||
10: "03", 19: "09", 28: "08", 37: "07"
|
||||
}
|
||||
if "°C" == unit_in:
|
||||
celsius = int(temperature.split("(")[0])
|
||||
fahrenheit = celsius * 1.8 + 32
|
||||
fahrenheit = int(round(fahrenheit, 0))
|
||||
elif "°F" == unit_in:
|
||||
fahrenheit = int(temperature.split("(")[0])
|
||||
celsius = (fahrenheit - 32) / 1.8
|
||||
celsius = int(round(celsius, 0))
|
||||
else:
|
||||
return "invalid input unit"
|
||||
|
||||
for temp, color in tempcolor_d.items():
|
||||
if celsius <= temp:
|
||||
if "°C" == unit_out:
|
||||
return f"\x03{color} {celsius}\x0F"
|
||||
elif "°F" == unit_out:
|
||||
return f"\x03{color} {fahrenheit}\x0F"
|
||||
|
||||
# Fallback for when the temperature is too high.
|
||||
if "°C" == unit_out:
|
||||
return f"\x0304 {celsius}\x0F"
|
||||
elif "°F" == unit_out:
|
||||
return f"\x0304 {fahrenheit}\x0F"
|
||||
|
||||
|
||||
def temp_format_range(temp_list, unit, unit_s):
|
||||
ret = "Temp:"
|
||||
ret += f"{temperature_color(temp_list[0], unit, unit)} -"
|
||||
ret += f"{temperature_color(temp_list[1], unit, unit)} {unit} /"
|
||||
ret += f"{temperature_color(temp_list[0], unit, unit_s)} -"
|
||||
ret += f"{temperature_color(temp_list[1], unit, unit_s)} {unit_s}"
|
||||
return ret
|
||||
|
||||
|
||||
def temp_format(txt):
|
||||
temperature_list = txt.split() # ['25..27', '°C']
|
||||
temperature = temperature_list[0]
|
||||
# dashes = temperature.count('-')
|
||||
unit = temperature_list[1]
|
||||
unit_s = unit_swap(unit)
|
||||
ret = "Temp:"
|
||||
|
||||
if '..' in temperature:
|
||||
temp_list = temperature.split('..')
|
||||
ret = temp_format_range(temp_list, unit, unit_s)
|
||||
else:
|
||||
ret += f"{temperature_color(temperature, unit, unit)} {unit} /"
|
||||
ret += f"{temperature_color(temperature, unit, unit_s)} {unit_s}"
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
# Wind
|
||||
|
||||
def wind_color(wind, unit_in, unit_out):
|
||||
windcolor_d = { # km/h: color
|
||||
4: "03", 10: "09", 20: "08", 32: "07"
|
||||
}
|
||||
if "km/h" == unit_in:
|
||||
kmh = int(wind)
|
||||
mph = kmh * 0.6213711922
|
||||
mph = int(round(mph, 0))
|
||||
elif "mph" == unit_in:
|
||||
mph = int(wind)
|
||||
kmh = mph * 1.609344
|
||||
kmh = int(round(kmh, 0))
|
||||
# elif "m/s" == unit_in:
|
||||
# ms = int(wind)
|
||||
# kmh = ms * 3.6
|
||||
|
||||
for k, color in windcolor_d.items():
|
||||
if kmh < k:
|
||||
if "km/h" == unit_out:
|
||||
return f"\x03{color} {kmh}\x0F"
|
||||
elif "mph" == unit_out:
|
||||
return f"\x03{color} {mph}\x0F"
|
||||
|
||||
# Fallback for when the wind speed is too high.
|
||||
if "km/h" == unit_out:
|
||||
return f"\x0304 {kmh}\x0F"
|
||||
elif "mph" == unit_out:
|
||||
return f"\x0304 {mph}\x0F"
|
||||
|
||||
|
||||
def wind_format(txt):
|
||||
def range_hdl(t, tempstr):
|
||||
for idx, i in enumerate(t):
|
||||
coltemp = wind_color(i, unit)
|
||||
tempstr += f'{coltemp}'
|
||||
if idx == 0:
|
||||
tempstr += ' -'
|
||||
return tempstr
|
||||
|
||||
wind_list = txt.split() # ['↑', '23', 'km/h']
|
||||
icon = wind_list[0]
|
||||
wind = wind_list[1]
|
||||
unit = wind_list[2]
|
||||
unit_s = unit_swap(unit)
|
||||
dashes = wind.count('-')
|
||||
ret = f'Wind: {icon}'
|
||||
|
||||
if 1 == dashes:
|
||||
# NEEDLESS?
|
||||
wind_list = wind.split('-')
|
||||
ret += f"{wind_color(wind_list[0], unit, unit)} -"
|
||||
ret += f"{wind_color(wind_list[1], unit, unit)} {unit} /"
|
||||
ret += f"{wind_color(wind_list[0], unit, unit_s)} -"
|
||||
ret += f"{wind_color(wind_list[1], unit, unit_s)} {unit_s}"
|
||||
else: # 17 km/h
|
||||
ret += f"{wind_color(wind, unit, unit)} {unit} /"
|
||||
ret += f"{wind_color(wind, unit, unit_s)} {unit_s}"
|
||||
|
||||
return f'{ret}'
|
||||
|
||||
|
||||
def handler(txt):
|
||||
if ('°C' in txt) or ('°F' in txt):
|
||||
return temp_format(txt)
|
||||
elif ('km/h' in txt) or ('mph' in txt) or ('m/s' in txt):
|
||||
return wind_format(txt)
|
||||
elif ('km' in txt) or ('mi' in txt):
|
||||
return f'Visibility: {txt}'
|
||||
elif ('mm' in txt) or ('in' in txt):
|
||||
return f'Rainfall: {txt}'
|
||||
elif '%' in txt:
|
||||
return f'Rain Prob: {txt}'
|
||||
else:
|
||||
return txt
|
||||
|
||||
|
||||
# Ascii art set from:
|
||||
# https://github.com/schachmat/wego/blob/master/frontends/ascii-art-table.go
|
||||
art = (
|
||||
' ',
|
||||
' .-. ',
|
||||
' __) ',
|
||||
' ( ',
|
||||
' `-᾿ ',
|
||||
' • ',
|
||||
' .--. ',
|
||||
' .-( ). ',
|
||||
' (___.__)__) ',
|
||||
' _ - _ - _ ',
|
||||
' _ - _ - _ - ',
|
||||
' ( ). ',
|
||||
' (___(__) ',
|
||||
' ‚ʻ‚ʻ‚ʻ‚ʻ ',
|
||||
' _`/"".-. ',
|
||||
' ,\\_( ). ',
|
||||
' /(___(__) ',
|
||||
' ‚ʻ‚ʻ‚ʻ‚ʻ ',
|
||||
' ‚’‚’‚’‚’ ',
|
||||
' ‚‘‚‘‚‘‚‘ ',
|
||||
' .-. ',
|
||||
' * * * * ',
|
||||
' * * * * ',
|
||||
' * * * * ',
|
||||
' ʻ ʻ ʻ ʻ ',
|
||||
' ʻ ʻ ʻ ʻ ',
|
||||
' ʻ ʻ ʻ ʻ ',
|
||||
' ‘ ‘ ‘ ‘ ',
|
||||
' ʻ * ʻ * ',
|
||||
' * ʻ * ʻ ',
|
||||
' ʻ * ʻ * ',
|
||||
' * ʻ * ʻ ',
|
||||
' * * * ',
|
||||
' * * * ',
|
||||
' * * * ',
|
||||
' \\ / ',
|
||||
' _ /"".-. ',
|
||||
' \\_( ). ',
|
||||
' \\ / ',
|
||||
' ‒ ( ) ‒ ',
|
||||
' / \\ ',
|
||||
' ‚ʻ⚡ʻ‚⚡‚ʻ ',
|
||||
' ‚ʻ‚ʻ⚡ʻ‚ʻ ',
|
||||
' ⚡ʻ ʻ⚡ʻ ʻ ',
|
||||
' *⚡ *⚡ * ',
|
||||
' ― ( ) ― ',
|
||||
' `-’ ',
|
||||
' ⚡‘‘⚡‘‘ '
|
||||
)
|
||||
|
||||
|
||||
def wttr(irc, channel, location):
|
||||
if location.lower() == 'moon' or 'moon@' in location.lower():
|
||||
irc.privmsg(channel, 'This is not supported yet '
|
||||
'(add ,+US or ,+France for these cities)')
|
||||
return
|
||||
|
||||
location = urllib.parse.quote_plus(location)
|
||||
url = f'http://wttr.in/{location}?0Tm'
|
||||
r = requests.get(url, timeout=10).text.splitlines()
|
||||
text = ''
|
||||
for line in r:
|
||||
for i in art:
|
||||
line = line.replace(i, '')
|
||||
if line:
|
||||
line = handler(line)
|
||||
text += f'{line} | '
|
||||
|
||||
text = " ".join(text.split()) # Remove additional spaces.
|
||||
text = text.lstrip("Rainfall: ") # Remove 'Rainfall: ' from the front.
|
||||
if "ERROR: Unknown location:" in text:
|
||||
text = f'\x0304wttr.in: Location "{location}" could not be found.'
|
||||
elif "API key has reached calls per day allowed limit." in text\
|
||||
or ("Sorry, we are running out of queries to the weather service at "
|
||||
"the moment.") in text:
|
||||
text = "\x0304wttr.in: API call limit reached. Try again tomorrow."
|
||||
|
||||
irc.privmsg(channel, text)
|
||||
|
||||
|
||||
# Authentication
|
||||
|
||||
def set_auth(i, irc, dbc):
|
||||
if not user_auth(i, irc, i.nickname):
|
||||
return f"{i.nickname}: You are not logged in with NickServ."
|
||||
|
||||
dbc.execute('SELECT auth FROM weather WHERE nickname=?;',
|
||||
(i.nickname,))
|
||||
fetch = dbc.fetchone()
|
||||
|
||||
try:
|
||||
auth = fetch[0]
|
||||
except TypeError: # 'NoneType' object is not subscriptable
|
||||
auth = 0
|
||||
|
||||
if auth == 0:
|
||||
auth = 1
|
||||
msg = f'{i.nickname}: weather: Enabled NickServ authentication.'
|
||||
elif auth == 1:
|
||||
auth = 0
|
||||
msg = f'{i.nickname}: weather: Disabled NickServ authentication.'
|
||||
|
||||
dbc.execute(
|
||||
"INSERT OR IGNORE INTO weather (nickname, auth) VALUES (?, ?);",
|
||||
(i.nickname, auth))
|
||||
dbc.execute("UPDATE weather SET auth=? WHERE nickname=?;",
|
||||
(auth, i.nickname))
|
||||
return msg
|
||||
|
||||
|
||||
def get_auth(i, irc, dbc):
|
||||
dbc.execute('SELECT auth FROM weather WHERE nickname=?;', (i.nickname,))
|
||||
fetch = dbc.fetchone()
|
||||
try:
|
||||
auth = fetch[0]
|
||||
except TypeError: # 'NoneType' object is not subscriptable
|
||||
auth = 0
|
||||
|
||||
if auth == 0:
|
||||
return True
|
||||
elif auth == 1 and user_auth(i, irc, i.nickname):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def set_location(i, irc, dbc, location):
|
||||
if not get_auth(i, irc, dbc):
|
||||
return f"{i.nickname}: weather: NickServ authentication is required."
|
||||
dbc.execute(
|
||||
'''INSERT OR IGNORE INTO weather (nickname, location)
|
||||
VALUES (?, ?);''', (i.nickname, location))
|
||||
dbc.execute('''UPDATE weather SET location=? WHERE nickname=?;''',
|
||||
(location, i.nickname))
|
||||
return f'{i.nickname}: weather: Your location was set to "{location}"'
|
||||
|
||||
|
||||
def get_location(dbc, nickname):
|
||||
try:
|
||||
dbc.execute(
|
||||
'SELECT location FROM weather WHERE nickname=?;', (nickname,))
|
||||
return dbc.fetchone()[0]
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
dbc = i.db[1].cursor()
|
||||
|
||||
try:
|
||||
dbc.execute(
|
||||
'''CREATE TABLE IF NOT EXISTS weather (nickname TEXT COLLATE NOCASE
|
||||
PRIMARY KEY, location TEXT, auth INTEGER DEFAULT 0);''')
|
||||
except Exception:
|
||||
# sqlite3.OperationalError: cannot commit - no transaction is active
|
||||
pass
|
||||
|
||||
if "weather" == i.cmd:
|
||||
|
||||
if not i.msg_nocmd:
|
||||
location = get_location(dbc, i.nickname)
|
||||
if location:
|
||||
wttr(irc, i.channel, location)
|
||||
else:
|
||||
msg = (f'Usage: {i.cmd_prefix}{i.cmd} '
|
||||
'<Location / Airport code / @domain / '
|
||||
'IP address / Area code / GPS coordinates>')
|
||||
irc.privmsg(i.channel, msg)
|
||||
else:
|
||||
wttr(irc, i.channel, i.msg_nocmd)
|
||||
|
||||
elif "weather_set" == i.cmd:
|
||||
ret = set_location(i, irc, dbc, i.msg_nocmd)
|
||||
i.db[1].commit()
|
||||
irc.privmsg(i.channel, ret)
|
||||
elif "weather_auth" == i.cmd:
|
||||
ret = set_auth(i, irc, dbc)
|
||||
i.db[1].commit()
|
||||
irc.privmsg(i.channel, ret)
|
310
src/irc/modules/wikipedia.py
Normal file
310
src/irc/modules/wikipedia.py
Normal file
@ -0,0 +1,310 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding=utf-8
|
||||
|
||||
# Wikipedia Module for Drastikbot
|
||||
#
|
||||
# NOTE: This module is making use of the MediaWiki API,
|
||||
# so it should work with other MediaWiki based websites.
|
||||
#
|
||||
# Depends:
|
||||
# - requests :: $ pip3 install requests
|
||||
# - beautifulsoup4 :: $ pip3 install beautifulsoup4
|
||||
|
||||
'''
|
||||
Copyright (C) 2017 drastik.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
any later version.
|
||||
|
||||
This program 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import requests
|
||||
import bs4
|
||||
import urllib.parse
|
||||
from dbot_tools import Config, p_truncate
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.commands = ['wikipedia', 'wiki', 'w']
|
||||
|
||||
usage = lambda x, y: (f"{x}{y} <article> [--full] [--search]"
|
||||
" [--sections] [-l <lang>] [--resuult <num>]")
|
||||
info = ("--info: Get the full section in a query."
|
||||
" / --search: Search and get the results in a query."
|
||||
" / --sections: Get all the sections of an article in a query."
|
||||
" / -l: Post an article from a specific language"
|
||||
" / --result: Select specific result."
|
||||
" <num> is the index of the result returned by --search"
|
||||
" / Use #section after the article's name to get a specific"
|
||||
" section. Example: .w irc#Technical information")
|
||||
self.manual = {
|
||||
"desc": ("Search wikipedia and post a snippet from the resulting"
|
||||
" article."),
|
||||
"bot_commands": {
|
||||
"wikipedia": {"usage": lambda x: usage(x, "wikipedia"),
|
||||
"info": info,
|
||||
"alias": ["w", "wiki"]},
|
||||
"wiki": {"usage": lambda x: usage(x, "wiki"),
|
||||
"info": info,
|
||||
"alias": ["w", "wikipedia"]},
|
||||
"w": {"usage": lambda x: usage(x, "w"),
|
||||
"info": info,
|
||||
"alias": ["wikipedia", "wiki"]}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ----- Global Constants ----- #
|
||||
r_timeout = 10
|
||||
bs4_parser = 'html.parser'
|
||||
# ---------------------------- #
|
||||
|
||||
|
||||
def language(args, config, channel):
|
||||
'''Set the language used to search for wikipedia articles'''
|
||||
if '-l' in args:
|
||||
# Check if the language command has been used and
|
||||
# use the given value instead of the configuration
|
||||
index = args.index('-l')
|
||||
return args[index + 1]
|
||||
else:
|
||||
# Try loading from the configuration
|
||||
try:
|
||||
# Check the configuration file for per channel language settings.
|
||||
return config['irc']['modules']['wikipedia']['channels'][channel]
|
||||
except KeyError:
|
||||
try:
|
||||
# Check the configuration file for global language settings
|
||||
return config['irc']['modules']['wikipedia']['lang']
|
||||
except KeyError:
|
||||
# Return English if all above fails
|
||||
return 'en'
|
||||
|
||||
|
||||
def mw_opensearch(query, url, max_results=1):
|
||||
'''
|
||||
Uses the MediaWiki API:Opensearch
|
||||
https://en.wikipedia.org/w/api.php?action=help&modules=opensearch
|
||||
|
||||
Search MediaWiki for articles relevant to the search 'query'
|
||||
It returns a [query,[titles],[descriptions],[urls]] of relevant
|
||||
results to the search query.
|
||||
|
||||
'query' is the string to search for
|
||||
'url' is the url of the MediaWiki website
|
||||
'max_results' is the maximum amount of results to get
|
||||
'''
|
||||
u = (f'{url}/w/api.php'
|
||||
f'?action=opensearch&format=json&limit={max_results}&search={query}')
|
||||
r = requests.get(u, timeout=r_timeout)
|
||||
return r.json()
|
||||
|
||||
|
||||
def mw_list_sections(page, url):
|
||||
'''
|
||||
Uses the MediaWiki API:Parsing_wikitext#parse
|
||||
https://www.mediawiki.org/wiki/Special:MyLanguage/API:Parsing_wikitext#parse
|
||||
|
||||
Get a list of all the available sections for a given article.
|
||||
Returns a tuple with the title of the article and a
|
||||
list [[sections],[indexes]]
|
||||
|
||||
'page' should be the name of the MediaWiki article as returned
|
||||
by mw_opensearch()
|
||||
'url' is the url of the MediaWiki website
|
||||
'''
|
||||
u = f'{url}/w/api.php?action=parse&format=json&prop=sections&page={page}'
|
||||
r = requests.get(u, timeout=r_timeout)
|
||||
parse = r.json()
|
||||
title = parse['parse']['title']
|
||||
sections_ = parse['parse']['sections']
|
||||
section_list = [[], []]
|
||||
for i in sections_:
|
||||
section_list[0].append(i['line'])
|
||||
section_list[1].append(i['index'])
|
||||
return (title, section_list)
|
||||
|
||||
|
||||
def text_cleanup(soup):
|
||||
try:
|
||||
for sup in soup('sup'):
|
||||
soup.sup.decompose()
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
for small in soup('small'):
|
||||
soup.small.decompose()
|
||||
except AttributeError:
|
||||
pass
|
||||
return soup
|
||||
|
||||
|
||||
def mw_parse_intro(url, page, limit):
|
||||
'''
|
||||
Uses the MediaWiki API:Parsing_wikitext#parse
|
||||
https://www.mediawiki.org/wiki/Special:MyLanguage/API:Parsing_wikitext#parse
|
||||
|
||||
This function calls the MediaWiki API, which returns a JSON
|
||||
document containing the html of the introduction section
|
||||
of an article, which is parsed by beautifulsoup4, limited
|
||||
to the specified amount of characters and returned.
|
||||
|
||||
'url' is the url of the MediaWiki website
|
||||
'page' should be the name of the MediaWiki article as
|
||||
returned by mw_opensearch()
|
||||
'limit' if True truncate the text
|
||||
'''
|
||||
u = (f'{url}/w/api.php'
|
||||
f'?action=parse&format=json&prop=text§ion=0&page={page}')
|
||||
r = requests.get(u, timeout=r_timeout)
|
||||
html = r.json()['parse']['text']['*']
|
||||
soup = bs4.BeautifulSoup(html, bs4_parser)
|
||||
soup = text_cleanup(soup)
|
||||
text = soup.find('p').text
|
||||
if text == 'Redirect to:':
|
||||
n_title = soup.find('a').text
|
||||
n_text = mw_parse_intro(url, n_title, limit)
|
||||
text = f'\x0302[Redirect to: {n_title}]\x0F {n_text}'
|
||||
if limit:
|
||||
text = p_truncate(text, msg_len, 85, True)
|
||||
return text
|
||||
|
||||
|
||||
def mw_parse_section(url, section_list, page, sect, limit):
|
||||
'''
|
||||
Uses the MediaWiki API:Parsing_wikitext#parse
|
||||
https://www.mediawiki.org/wiki/Special:MyLanguage/API:Parsing_wikitext#parse
|
||||
|
||||
This function finds the position of the section ('sect')
|
||||
requested in 'section_list' and calls the MediaWiki API,
|
||||
which returns a JSON document containing the html of the
|
||||
requested section which is parsed by beautifulsoup4,
|
||||
limited to the specified amount of characters and returned.
|
||||
|
||||
'url' is the url of the MediaWiki website
|
||||
'section_list' is the second item returned by mw_list_sections()
|
||||
'page' should be the name of the MediaWiki article as
|
||||
returned by mw_opensearch()
|
||||
'sect' is the section requested to be viewed
|
||||
'limit' if True truncate the text
|
||||
'''
|
||||
id_index = section_list[0].index(sect)
|
||||
u = (f'{url}/w/api.php'
|
||||
'?action=parse&format=json&prop=text'
|
||||
f'§ion={section_list[1][id_index]}&page={page}')
|
||||
r = requests.get(u, timeout=r_timeout)
|
||||
html = r.json()['parse']['text']['*']
|
||||
soup = bs4.BeautifulSoup(html, bs4_parser)
|
||||
soup = text_cleanup(soup)
|
||||
text = soup.find('span', id=sect)
|
||||
text = text.find_next('p').text
|
||||
if limit:
|
||||
text = p_truncate(text, msg_len, 85, True)
|
||||
return text
|
||||
|
||||
|
||||
def str2url(url):
|
||||
return urllib.parse.quote_plus(url)
|
||||
|
||||
|
||||
def query(args):
|
||||
# Get the args list and the commands
|
||||
# Join the list to a string and return
|
||||
_args = args[:]
|
||||
cmds = ['--search', '--sections', '--full']
|
||||
cmds_args = ['--result', '-r', '-l']
|
||||
for i in cmds_args:
|
||||
try:
|
||||
idx = _args.index(i)
|
||||
del _args[idx]
|
||||
del _args[idx]
|
||||
except ValueError:
|
||||
pass
|
||||
for i in cmds:
|
||||
try:
|
||||
idx = _args.index(i)
|
||||
del _args[idx]
|
||||
except ValueError:
|
||||
pass
|
||||
return ' '.join(_args)
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
if not i.msg_nocmd:
|
||||
msg = (f'Usage: {i.cmd_prefix}{i.cmd} <Article> '
|
||||
'[--full, --search, --sections -l], [--result <NUM>]')
|
||||
return irc.privmsg(i.channel, msg)
|
||||
|
||||
channel = i.channel
|
||||
args = i.msg_nocmd.split()
|
||||
config = Config(irc.cd).read()
|
||||
lang = language(args, config, i.channel)
|
||||
# Do not put a "/" slash at the end of the url
|
||||
mediawiki_url = f'https://{lang}.wikipedia.org'
|
||||
logo = '\x0301,00Wikipedia\x0F'
|
||||
limit = True
|
||||
search_q = query(args)
|
||||
global msg_len
|
||||
msg_len = irc.var.msg_len - 9 - 22
|
||||
|
||||
if '--search' in args:
|
||||
opensearch = mw_opensearch(search_q, mediawiki_url, 10)
|
||||
rs_string = ''
|
||||
for n in opensearch[1]:
|
||||
rs_string += f'[{opensearch[1].index(n) + 1}:{n}] '
|
||||
msg = (f'{logo}: \x0302[search results for: '
|
||||
f'{search_q}]\x0F: {rs_string}')
|
||||
return irc.privmsg(i.nickname, msg)
|
||||
|
||||
if '--full' in args:
|
||||
limit = False
|
||||
channel = i.nickname
|
||||
|
||||
if '--result' in args or '-r' in args:
|
||||
try:
|
||||
r_index = args.index('--result')
|
||||
except ValueError:
|
||||
r_index = args.index('-r')
|
||||
os_limit = int(args[r_index + 1])
|
||||
opensearch = mw_opensearch(search_q, mediawiki_url, os_limit)
|
||||
try:
|
||||
title = opensearch[1][os_limit - 1]
|
||||
except IndexError:
|
||||
msg = f'{logo}: No article was found for \x02{search_q}\x0F'
|
||||
return irc.privmsg(channel, msg)
|
||||
else:
|
||||
opensearch = mw_opensearch(search_q, mediawiki_url)
|
||||
try:
|
||||
title = opensearch[1][0]
|
||||
except IndexError:
|
||||
msg = f'{logo}: No article was found for \x02{search_q}\x0F'
|
||||
return irc.privmsg(channel, msg)
|
||||
wikiurl = f'{mediawiki_url}/wiki/{title.replace(" ", "_")}'
|
||||
|
||||
if '--sections' in args:
|
||||
sections_out = mw_list_sections(title, mediawiki_url)
|
||||
sec_out_str = ' | '.join(sections_out[1][0])
|
||||
msg = (f'{logo}: \x0302 [sections for {sections_out[0]}]\x0F: '
|
||||
f'{sec_out_str} [ {wikiurl} ]')
|
||||
irc.privmsg(i.nickname, msg)
|
||||
elif '#' in search_q:
|
||||
ts_list = search_q.split('#')
|
||||
sections_out = mw_list_sections(title, mediawiki_url)
|
||||
snippet = mw_parse_section(mediawiki_url, sections_out[1],
|
||||
title, ts_list[1], limit)
|
||||
msg = f'{logo}: \x02{title}#{ts_list[1]}\x0F | {snippet} | {wikiurl}'
|
||||
irc.privmsg(channel, msg)
|
||||
else:
|
||||
snippet = mw_parse_intro(mediawiki_url, title, limit)
|
||||
msg = f'{logo}: \x02{title}\x0F | {snippet} | {wikiurl}'
|
||||
irc.privmsg(channel, msg)
|
173
src/irc/modules/wiktionary.py
Normal file
173
src/irc/modules/wiktionary.py
Normal file
@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding=utf-8
|
||||
|
||||
# Wiktionary Module for Drastikbot
|
||||
#
|
||||
# Depends:
|
||||
# - requests :: $ pip3 install requests
|
||||
# - beautifulsoup4 :: $ pip3 install beautifulsoup4
|
||||
|
||||
'''
|
||||
Copyright (C) 2018 drastik.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
any later version.
|
||||
|
||||
This program 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import requests
|
||||
import bs4
|
||||
import re
|
||||
from dbot_tools import p_truncate
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.commands = ['wiktionary', 'wt']
|
||||
|
||||
usage = lambda x, y: f"{x}{y} <word> [-e <num>]"
|
||||
info = ("The -e option allows you to choose other defintions."
|
||||
" The number of definitions is listed in parenthesis in the"
|
||||
" result. In a query, the bot will post the full definitions"
|
||||
" without truncating the text.")
|
||||
self.manual = {
|
||||
"desc": "Search https://en.wiktionary.org/ for word definitions.",
|
||||
"bot_commands": {
|
||||
"wiktionary": {"usage": lambda x: usage(x, "wiktionary"),
|
||||
"info": info,
|
||||
"alias": ["wt"]},
|
||||
"wt": {"usage": lambda x: usage(x, "wt"),
|
||||
"info": info,
|
||||
"alias": ["wiktionary"]}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ----- Global Constants ----- #
|
||||
r_timeout = 10
|
||||
bs4_parser = 'html.parser'
|
||||
# ---------------------------- #
|
||||
|
||||
|
||||
def get_text(html, etymology):
|
||||
soup = bs4.BeautifulSoup(html, bs4_parser)
|
||||
|
||||
result = {}
|
||||
result[etymology] = {}
|
||||
|
||||
s_et = soup.find('span', id=etymology)
|
||||
result[etymology]["Etymology"] = s_et.find_next('p').text
|
||||
|
||||
ids = ("Noun", "Verb", "Adjective", "Adverb",
|
||||
"Interjection", "Particle", "Preposition")
|
||||
for i in ids:
|
||||
s = s_et.find_next('span', string=i)
|
||||
try:
|
||||
txt = s.find_next('ol').text
|
||||
result[etymology][i] = txt
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def extract_etymologies(html):
|
||||
result = {}
|
||||
count = 1
|
||||
while(True):
|
||||
if 'id="Etymology"' in html:
|
||||
result.update(get_text(html, "Etymology"))
|
||||
break
|
||||
elif f'id="Etymology_{count}"' in html:
|
||||
result.update(get_text(html, f"Etymology_{count}"))
|
||||
count += 1
|
||||
else:
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def wiktionary(url, res):
|
||||
r = requests.get(url, timeout=r_timeout)
|
||||
|
||||
# Extract the html of a single language section.
|
||||
section_end = '<hr'
|
||||
html = ""
|
||||
for t in r.text.splitlines():
|
||||
html += t
|
||||
if section_end in t:
|
||||
break
|
||||
|
||||
# Remove quotations so that beautifulsoup doesn't catch them.
|
||||
html = re.compile(r'(?ims)<ul>.*?</ul>').sub('', html)
|
||||
html = re.compile(r'(?ims)<dl>.*?</dl>').sub('', html)
|
||||
html = re.compile(r'(?ims)<span class="defdate">.*?</span>').sub('', html)
|
||||
return extract_etymologies(html)
|
||||
|
||||
|
||||
def query(args):
|
||||
# Get the args list and the commands
|
||||
# Join the list to a string and return
|
||||
_args = args[:]
|
||||
try:
|
||||
idx = _args.index('-e')
|
||||
del _args[idx]
|
||||
del _args[idx]
|
||||
except ValueError:
|
||||
pass
|
||||
return ' '.join(_args)
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
if not i.msg_nocmd:
|
||||
msg = (f'Usage: {i.cmd_prefix}{i.cmd} <Word> [-e <NUM>]')
|
||||
return irc.privmsg(i.channel, msg)
|
||||
|
||||
args = i.msg_nocmd.split()
|
||||
|
||||
if '-e' in args:
|
||||
idx = args.index('-e')
|
||||
res = int(args[idx + 1])
|
||||
else:
|
||||
res = 1
|
||||
|
||||
q = query(args)
|
||||
q_web = q.replace(" ", "_")
|
||||
url = f"https://en.wiktionary.org/wiki/{q_web}"
|
||||
result = wiktionary(url, res)
|
||||
result_length = len(result)
|
||||
|
||||
if res not in range(1, result_length + 1):
|
||||
msg = f'Wiktionary: No definition was found for "{q}" | {url}'
|
||||
return irc.privmsg(i.channel, msg)
|
||||
|
||||
if res == 1:
|
||||
try:
|
||||
result = result["Etymology"]
|
||||
except KeyError:
|
||||
result = result["Etymology_1"]
|
||||
else:
|
||||
result = result[f"Etymology_{res}"]
|
||||
|
||||
msg_len = (irc.var.msg_len - 9 - 8 - len(str(result_length))
|
||||
- (len(result) * 5) - len(url))
|
||||
p_tr_percent = 100 / len(result)
|
||||
|
||||
txt = ""
|
||||
for part, cont in result.items():
|
||||
rslt = f"{part}: {cont}"
|
||||
if not i.nickname == i.channel:
|
||||
rslt = p_truncate(rslt, msg_len, p_tr_percent, True)
|
||||
txt += f"{rslt} | "
|
||||
|
||||
rpl = f"{q} | {txt}({result_length}) | {url}"
|
||||
irc.privmsg(i.channel, rpl)
|
63
src/irc/modules/wolframalpha.py
Normal file
63
src/irc/modules/wolframalpha.py
Normal file
@ -0,0 +1,63 @@
|
||||
# coding=utf-8
|
||||
|
||||
# WolframAlpha module for drastikbot
|
||||
#
|
||||
# It makes use of the Short Answers API to get answers for user
|
||||
# queries.
|
||||
#
|
||||
# You need to use you own AppID to use this module. Find out more:
|
||||
# http://products.wolframalpha.com/short-answers-api/documentation/
|
||||
|
||||
'''
|
||||
Copyright (C) 2019 drastik.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, version 3 only.
|
||||
|
||||
This program 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import urllib.parse
|
||||
import requests
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.commands = ['wa', 'wolfram', 'wolframalpha']
|
||||
self.manual = {
|
||||
"desc": "Get results from the Wolfram|Alpha short answers API.",
|
||||
"bot_commands": {
|
||||
"wa": {"usage": lambda x: f"{x}wa <query>",
|
||||
"alias": ["wolfram", "wolframalpha"]},
|
||||
"wolfram": {"usage": lambda x: f"{x}wolfram <query>",
|
||||
"alias": ["w", "wolframalpha"]},
|
||||
"wolframalpha": {"usage": lambda x: f"{x}wolframalpha <query>",
|
||||
"alias": ["wolfram", "w"]}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
AppID = "Enter your AppID here"
|
||||
|
||||
|
||||
def short_answers(query):
|
||||
url = f"http://api.wolframalpha.com/v1/result?appid={AppID}&i={query}"
|
||||
try:
|
||||
r = requests.get(url, timeout=10)
|
||||
except Exception:
|
||||
return False
|
||||
return r.text
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
query = urllib.parse.quote_plus(i.msg_nocmd)
|
||||
r = short_answers(query)
|
||||
r = f"Wolfram|Alpha: {r}"
|
||||
irc.privmsg(i.channel, r)
|
46
src/irc/modules/yn.py
Normal file
46
src/irc/modules/yn.py
Normal file
@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding=utf-8
|
||||
|
||||
# Virtual coin flip
|
||||
|
||||
'''
|
||||
Copyright (c) 2018 Flisk <flisk@fastmail.de>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
'''
|
||||
|
||||
import random
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.commands = ['yn']
|
||||
self.manual = {
|
||||
"desc": "Flip a virtual coin",
|
||||
"bot_commands": {
|
||||
"yn": {"usage": lambda x: f"{x}yn"}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
irc.privmsg(
|
||||
i.channel,
|
||||
f"{i.nickname}: {'Yes' if random.randint(0, 1) == 1 else 'No'}."
|
||||
)
|
190
src/irc/modules/youtube.py
Normal file
190
src/irc/modules/youtube.py
Normal file
@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding=utf-8
|
||||
|
||||
# YouTube Module for Drastikbot
|
||||
#
|
||||
# Search YouTube and return the resulting video.
|
||||
# When a YouTube url is posted return the video's information.
|
||||
#
|
||||
# If you are planning to use the url module or a url bot, consider adding the
|
||||
# following blacklist: ['youtu.be/', 'youtube.com/watch']
|
||||
#
|
||||
# Depends:
|
||||
# - requests :: $ pip3 install requests
|
||||
# - beautifulsoup :: $ pip3 install beautifulsoup4
|
||||
|
||||
'''
|
||||
Copyright (C) 2018-2020 drastik.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
any later version.
|
||||
|
||||
This program 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import urllib.parse
|
||||
import json
|
||||
import requests
|
||||
import bs4
|
||||
|
||||
import url
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self):
|
||||
self.commands = ['yt']
|
||||
self.manual = {
|
||||
"desc": "Search YouTube and return the resulting video url.",
|
||||
"bot_commands": {"yt": {"usage": lambda x: f"{x}yt <video title>"}}
|
||||
}
|
||||
|
||||
|
||||
# ----- Constants ----- #
|
||||
parser = 'html.parser'
|
||||
lang = "en-US"
|
||||
user_agent = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
|
||||
" AppleWebKit/537.36 (KHTML, like Gecko)"
|
||||
" Chrome/83.0.4103.116 Safari/537.36")
|
||||
# --------------------- #
|
||||
|
||||
|
||||
#
|
||||
# New Parser
|
||||
#
|
||||
|
||||
def yt_vid_info(v):
|
||||
yt_id = v["videoId"]
|
||||
short_url = f"https://youtu.be/{yt_id}"
|
||||
name = v["title"]["runs"][0]["text"]
|
||||
# Usually youtube's automated music uploads dont have dates.
|
||||
try:
|
||||
date = v["publishedTimeText"]["simpleText"]
|
||||
except KeyError:
|
||||
date = False
|
||||
duration = v["lengthText"]["simpleText"]
|
||||
views = v["viewCountText"]["simpleText"]
|
||||
channel = v["ownerText"]["runs"][0]["text"]
|
||||
|
||||
return {
|
||||
'short_url': short_url, 'name': name, 'date': date, 'views': views,
|
||||
'channel': channel, 'duration': duration, 'yt_id': yt_id
|
||||
}
|
||||
|
||||
|
||||
def yt_search(query):
|
||||
search = (f'https://www.youtube.com/results?search_query={query}'
|
||||
'&app=desktop')
|
||||
r = requests.get(search, timeout=10,
|
||||
headers={"Accept-Language": lang,
|
||||
"user-agent": user_agent})
|
||||
|
||||
try:
|
||||
st = 'var ytInitialData = '
|
||||
st_i = r.text.index(st) + len(st)
|
||||
except ValueError:
|
||||
st = 'window["ytInitialData"] = '
|
||||
st_i = r.text.index(st) + len(st)
|
||||
|
||||
j_data = r.text[st_i:]
|
||||
|
||||
st = '};'
|
||||
st_i = j_data.index(st)
|
||||
|
||||
j_data = j_data[:st_i+1]
|
||||
j = json.loads(j_data)
|
||||
|
||||
res = j["contents"]['twoColumnSearchResultsRenderer']['primaryContents'][
|
||||
'sectionListRenderer']['contents'][0]['itemSectionRenderer'][
|
||||
'contents'] # What in the world?
|
||||
|
||||
for vid in res:
|
||||
if "videoRenderer" in vid:
|
||||
v = vid["videoRenderer"] # Video information
|
||||
return yt_vid_info(v)
|
||||
elif 'searchPyvRenderer' in vid:
|
||||
continue # Skip promoted videos
|
||||
|
||||
|
||||
def output(i):
|
||||
'''Format the output message to be returned.'''
|
||||
# logo_yt = "\x0301,00You\x0300,04Tube\x0F"
|
||||
logo_yt = "\x0300,04 ► \x0F"
|
||||
|
||||
if i["date"]:
|
||||
date = f" | \x02Uploaded:\x0F {i['date']}"
|
||||
else:
|
||||
date = ""
|
||||
|
||||
return (f"{logo_yt}: {i['short_url']} | "
|
||||
f"\x02{i['name']}\x0F ({i['duration']})"
|
||||
f" | \x02Views:\x0F {i['views']}"
|
||||
f" | \x02Channel\x0F: {i['channel']}"
|
||||
f"{date}")
|
||||
|
||||
|
||||
#
|
||||
# Old parser
|
||||
#
|
||||
|
||||
def yt_search_legacy(query):
|
||||
'''
|
||||
Search YouTube for 'query' and get a video from the search results.
|
||||
It tries to visit the video found to ensure that it is valid.
|
||||
Returns:
|
||||
- 'yt_id' : The YouTube ID of the result video.
|
||||
- False : If no video is found for 'query'.
|
||||
'''
|
||||
search = (f'https://m.youtube.com/results?search_query={query}')
|
||||
r = requests.get(search, headers={"Accept-Language": lang}, timeout=10)
|
||||
#print(r.text, file=open("output.html", "a"))
|
||||
soup = bs4.BeautifulSoup(r.text, parser)
|
||||
for s in soup.find_all('a', {'class': ['yt-uix-tile-link']}):
|
||||
yt_id = urllib.parse.urlparse(s.get('href')).query
|
||||
print(yt_id)
|
||||
yt_id = urllib.parse.parse_qs(yt_id)
|
||||
print(yt_id)
|
||||
try:
|
||||
yt_id = yt_id['v'][0]
|
||||
except KeyError:
|
||||
try:
|
||||
yt_id = yt_id['video_id'][0]
|
||||
except KeyError:
|
||||
continue
|
||||
yt_id = ''.join(yt_id.split())
|
||||
# Try to visit the url to make sure it's a valid one.
|
||||
try:
|
||||
u = f'https://m.youtube.com/watch?v={yt_id}'
|
||||
requests.get(u, timeout=10)
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
return False
|
||||
return yt_id
|
||||
|
||||
|
||||
def output_legacy(yt_id):
|
||||
'''Format the output message to be returned.'''
|
||||
logo_yt = "\x0300,04 ► \x0F (legacy)"
|
||||
short_url = f"https://youtu.be/{yt_id}"
|
||||
title = url.youtube(short_url)
|
||||
return f"{logo_yt}: {short_url} | {title}"
|
||||
|
||||
|
||||
def main(i, irc):
|
||||
if i.cmd:
|
||||
query = urllib.parse.quote_plus(i.msg_nocmd)
|
||||
try:
|
||||
out = output(yt_search(query))
|
||||
except Exception as e:
|
||||
print(e)
|
||||
out = output_legacy(yt_search_legacy(query))
|
||||
irc.privmsg(i.channel, out)
|
293
src/irc/worker.py
Normal file
293
src/irc/worker.py
Normal file
@ -0,0 +1,293 @@
|
||||
# coding=utf-8
|
||||
|
||||
# Functionality for connecting, reconnecting, registering and pinging to the
|
||||
# IRC server. Recieving messages from the server and calling the module handler
|
||||
# is also done in this file.
|
||||
|
||||
'''
|
||||
Copyright (C) 2017-2020 drastik.org
|
||||
|
||||
This file is part of drastikbot.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, version 3 only.
|
||||
|
||||
This program 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
from threading import Thread
|
||||
import os
|
||||
import re
|
||||
import base64
|
||||
import traceback
|
||||
|
||||
from irc.message import Message
|
||||
from dbot_tools import Logger
|
||||
from irc.irc import Drastikbot
|
||||
from irc.modules import Modules
|
||||
|
||||
|
||||
class Register:
|
||||
def __init__(self, irc):
|
||||
self.irc = irc
|
||||
self.msg = ''
|
||||
# self.cap is used to indicate the status IRCv3 capability
|
||||
# negotiation.
|
||||
# Values:
|
||||
# -1: Check for IRCv3 | 0: Ended or none
|
||||
# 1: In progress | 2: Send CAP END
|
||||
self.cap = -1
|
||||
self.nickserv_ghost_status = 0 # 0: None | 1: In progress
|
||||
self.nickserv_recover_status = 0 # 0: None | 1: In progress
|
||||
self.motd = False
|
||||
|
||||
# --- IRCv3 --- #
|
||||
def cap_ls(self):
|
||||
self.irc.var.ircv3_serv = True
|
||||
self.cap = 1
|
||||
self.irc.var.ircv3_cap_ls = re.search(r"(?:CAP .* LS :)(.*)",
|
||||
self.msg.msg).group(1).split(' ')
|
||||
cap_req = [i for i in self.irc.var.ircv3_cap_ls
|
||||
if i in self.irc.var.ircv3_cap_req]
|
||||
|
||||
# If the server doesnt support any capabilities we support end the
|
||||
# registration now
|
||||
if cap_req == []:
|
||||
self.cap_end()
|
||||
return
|
||||
|
||||
self.irc.send(('CAP', 'REQ', ':{}'.format(' '.join(cap_req))))
|
||||
|
||||
def cap_ack(self):
|
||||
cap_ack = re.search(r"(?:CAP .* ACK :)(.*)", self.msg.msg).group(1)
|
||||
self.irc.var.ircv3_cap_ack = cap_ack.split()
|
||||
|
||||
def cap_end(self):
|
||||
self.irc.send(('CAP', 'END'))
|
||||
self.cap = 0
|
||||
|
||||
# -- SASL #
|
||||
def sasl_succ(self):
|
||||
self.irc.var.sasl_state = 1
|
||||
self.cap = 2
|
||||
|
||||
def sasl_fail(self):
|
||||
self.irc.var.sasl_state = 2
|
||||
self.cap = 2
|
||||
|
||||
def sasl_init(self):
|
||||
if not self.irc.var.authentication.lower() == 'sasl' \
|
||||
or 'sasl' not in self.irc.var.ircv3_cap_ack:
|
||||
self.sasl_fail()
|
||||
else:
|
||||
self.irc.send(('AUTHENTICATE', 'PLAIN'))
|
||||
self.irc.var.sasl_state = 3
|
||||
|
||||
def sasl_auth(self):
|
||||
username = self.irc.var.username
|
||||
password = self.irc.var.auth_password
|
||||
sasl_pass = f'{username}\0{username}\0{password}'
|
||||
self.irc.send(('AUTHENTICATE',
|
||||
base64.b64encode(sasl_pass.encode('utf-8'))))
|
||||
|
||||
# --- NickServ --- #
|
||||
def nickserv_identify(self):
|
||||
nickname = self.irc.var.nickname
|
||||
password = self.irc.var.auth_password
|
||||
self.irc.privmsg('NickServ', f'IDENTIFY {nickname} {password}')
|
||||
|
||||
def nickserv_recover(self):
|
||||
nickname = self.irc.var.nickname
|
||||
password = self.irc.var.auth_password
|
||||
if self.irc.var.authentication and self.irc.var.auth_password \
|
||||
and self.nickserv_recover_status == 0:
|
||||
self.irc.privmsg('NickServ', f'RECOVER {nickname} {password}')
|
||||
self.nickserv_recover_status = 1
|
||||
elif "You have regained control" in self.msg.msg:
|
||||
self.nickserv_recover_status = 0
|
||||
self.irc.var.curr_nickname = nickname
|
||||
self.irc.var.alt_nickname = False
|
||||
|
||||
def nickserv_ghost(self):
|
||||
nickname = self.irc.var.nickname
|
||||
password = self.irc.var.auth_password
|
||||
if self.irc.var.authentication and self.irc.var.auth_password \
|
||||
and self.nickserv_ghost_status == 0:
|
||||
self.irc.privmsg('NickServ', f'GHOST {nickname} {password}')
|
||||
self.nickserv_ghost_status = 1
|
||||
elif "has been ghosted" in self.msg.params \
|
||||
and self.nickserv_ghost_status == 1:
|
||||
self.irc.nick(nickname)
|
||||
self.nickserv_identify()
|
||||
self.nickserv_ghost_status = 0
|
||||
self.irc.var.curr_nickname = nickname
|
||||
self.irc.var.alt_nickname = False
|
||||
elif "/msg NickServ help" in self.msg.msg or \
|
||||
"/msg NickServ HELP" in self.msg.msg:
|
||||
self.nickserv_ghost_status = 0
|
||||
self.nickserv_recover()
|
||||
elif "is not a registered nickname" in self.msg.msg:
|
||||
self.nickserv_ghost_status = 0
|
||||
|
||||
# --- Error Handlers --- #
|
||||
def err_nicnameinuse_433(self):
|
||||
self.irc.var.curr_nickname = self.irc.var.curr_nickname + '_'
|
||||
self.irc.nick(self.irc.var.curr_nickname)
|
||||
self.irc.var.alt_nickname = True
|
||||
|
||||
# --- Registration Handlers --- #
|
||||
def reg_init(self):
|
||||
self.irc.send(('CAP', 'LS', self.irc.var.ircv3_ver))
|
||||
self.irc.send(('USER', self.irc.var.username, '0', '*',
|
||||
f':{self.irc.var.realname}'))
|
||||
self.irc.var.curr_nickname = self.irc.var.nickname
|
||||
self.irc.nick(self.irc.var.nickname)
|
||||
|
||||
def ircv3_fn_caller(self):
|
||||
a = len(self.msg.cmd_ls)
|
||||
cmd_ls = self.msg.cmd_ls
|
||||
if a > 2 and 'CAP LS' == f'{cmd_ls[0]} {cmd_ls[2]}':
|
||||
self.cap_ls()
|
||||
elif a > 2 and 'CAP ACK' == f'{cmd_ls[0]} {cmd_ls[2]}':
|
||||
self.cap_ack()
|
||||
# ERR_NICKNAMEINUSE
|
||||
if '433' in self.msg.cmd_ls[0]:
|
||||
self.err_nicnameinuse_433()
|
||||
# SASL
|
||||
if self.irc.var.ircv3_cap_ack and self.irc.var.sasl_state == 0:
|
||||
self.sasl_init()
|
||||
elif 'AUTHENTICATE' in self.msg.msg: # AUTHENTICATE +
|
||||
self.sasl_auth()
|
||||
elif '903' == cmd_ls[0]: # SASL authentication successful
|
||||
self.sasl_succ()
|
||||
elif '904' == cmd_ls[0]: # SASL authentication failed
|
||||
self.sasl_fail()
|
||||
# End capability negotiation.
|
||||
if self.cap == 2:
|
||||
self.cap_end()
|
||||
|
||||
def reg_fn_caller(self):
|
||||
if '433' in self.msg.cmd_ls[0]: # ERR_NICKNAMEINUSE
|
||||
self.err_nicnameinuse_433()
|
||||
if '376' in self.msg.cmd_ls[0]: # RPL_ENDOFMOTD
|
||||
self.motd = True
|
||||
|
||||
if self.motd and self.irc.var.alt_nickname \
|
||||
and self.irc.var.authentication and self.nickserv_ghost_status == 0\
|
||||
and self.nickserv_recover_status == 0:
|
||||
self.nickserv_ghost()
|
||||
if self.motd and self.irc.var.authentication.lower() == 'nickserv':
|
||||
self.nickserv_identify()
|
||||
if self.nickserv_ghost_status == 1:
|
||||
self.nickserv_ghost()
|
||||
if self.nickserv_recover_status == 1:
|
||||
self.nickserv_recover()
|
||||
if self.motd and not self.nickserv_ghost_status == 1 \
|
||||
and not self.nickserv_recover_status == 1:
|
||||
self.irc.var.conn_state = 2
|
||||
self.irc.join(self.irc.var.channels)
|
||||
|
||||
def reg_main(self, msg):
|
||||
self.msg = msg
|
||||
if self.cap != 0:
|
||||
# Check for IRCv3 methods
|
||||
self.ircv3_fn_caller()
|
||||
else:
|
||||
# Run normal IRC registration methods
|
||||
self.reg_fn_caller()
|
||||
|
||||
|
||||
class Main:
|
||||
def __init__(self, conf_dir, proj_path, mod=False):
|
||||
self.irc = Drastikbot(conf_dir)
|
||||
self.irc.var.proj_path = proj_path
|
||||
if mod:
|
||||
self.mod = mod
|
||||
mod.irc = self.irc # Update the irc variable
|
||||
else:
|
||||
self.mod = Modules(self.irc)
|
||||
self.reg = Register(self.irc)
|
||||
self.log = Logger(conf_dir, 'runtime.log')
|
||||
self.irc.var.log = self.log
|
||||
|
||||
def conn_lost(self):
|
||||
if self.irc.var.sigint:
|
||||
return
|
||||
self.log.info('<!> Connection Lost. Retrying in {} seconds.'
|
||||
.format(self.irc.var.reconnect_delay))
|
||||
self.irc.irc_socket.close()
|
||||
self.irc.var.conn_state = 0
|
||||
self.irc.reconn_wait() # Wait before next reconnection attempt.
|
||||
self.log.info('> Reconnecting...')
|
||||
# Reload the class
|
||||
self.__init__(self.irc.cd, self.irc.var.proj_path, mod=self.mod)
|
||||
self.main(reconnect=True) # Restart the bot
|
||||
|
||||
def recieve(self):
|
||||
while self.irc.var.conn_state != 0:
|
||||
try:
|
||||
msg_raw = self.irc.irc_socket.recv(4096)
|
||||
except Exception:
|
||||
self.log.debug('<!!!> Exception on recieve().'
|
||||
f'\n{traceback.format_exc()}')
|
||||
self.conn_lost()
|
||||
break
|
||||
|
||||
msg_raw_ls = msg_raw.split(b'\r\n')
|
||||
for line in msg_raw_ls:
|
||||
if line:
|
||||
msg = Message(line)
|
||||
if self.irc.var.conn_state == 2:
|
||||
self.service(msg)
|
||||
if 'PING' == msg.cmd_ls[0]:
|
||||
self.irc.send(('PONG', msg.params))
|
||||
elif self.irc.var.conn_state == 1:
|
||||
self.service(msg)
|
||||
self.reg.reg_main(msg)
|
||||
|
||||
if len(msg_raw) == 0:
|
||||
self.log.info('<!> recieve() exiting...')
|
||||
self.conn_lost()
|
||||
break
|
||||
|
||||
def service(self, msg):
|
||||
msg.channel_prep(self.irc)
|
||||
self.thread_make(self.mod.mod_main,
|
||||
(self.irc, msg, msg.params.split(' ')[0]))
|
||||
|
||||
def thread_make(self, target, args='', daemon=False):
|
||||
thread = Thread(target=target, args=(args))
|
||||
thread.daemon = daemon
|
||||
thread.start()
|
||||
return thread
|
||||
|
||||
def sigint_hdl(self, signum, frame):
|
||||
if self.irc.var.sigint == 0:
|
||||
self.log.info("\n> Quiting...")
|
||||
self.irc.var.sigint += 1
|
||||
self.irc.var.conn_state = 0
|
||||
self.irc.quit()
|
||||
else:
|
||||
self.log.info("\n Force Quit.")
|
||||
os._exit(1)
|
||||
|
||||
def main(self, reconnect=False):
|
||||
self.irc.connect()
|
||||
if not reconnect:
|
||||
self.mod.mod_import()
|
||||
if self.irc.var.sigint:
|
||||
return
|
||||
self.irc.var.conn_state = 1
|
||||
self.thread_make(self.recieve)
|
||||
reg_t = self.thread_make(self.reg.reg_init)
|
||||
reg_t.join() # wait until registered
|
||||
self.log.info(f"\nNickname: {self.irc.var.curr_nickname}")
|
||||
if not reconnect:
|
||||
self.thread_make(self.mod.mod_startup, (self.irc,))
|
213
src/toolbox/config_check.py
Normal file
213
src/toolbox/config_check.py
Normal file
@ -0,0 +1,213 @@
|
||||
# coding=utf-8
|
||||
|
||||
# Check the bot's configuration file, verify it, add any missing enties
|
||||
# either by requesting user input or by setting default values.
|
||||
|
||||
'''
|
||||
Copyright (C) 2019 drastik.org
|
||||
|
||||
This file is part of drastikbot.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, version 3 only.
|
||||
|
||||
This program 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from dbot_tools import Logger, Config
|
||||
from toolbox import user_acl
|
||||
|
||||
|
||||
def _check_irc(conf_dir):
|
||||
log = Logger(conf_dir, 'runtime.log')
|
||||
c = Config(conf_dir).read()
|
||||
if "irc" not in c:
|
||||
c.update({"irc": {}})
|
||||
Config(conf_dir).write(c)
|
||||
log.info("<*> Configuration: created 'irc' section.")
|
||||
|
||||
|
||||
def _check_owners(conf_dir):
|
||||
log = Logger(conf_dir, 'runtime.log')
|
||||
c = Config(conf_dir).read()
|
||||
if "owners" not in c["irc"]:
|
||||
print("\nSetting up the bot's owners. Please enter a comma seperated "
|
||||
"list of their IRC nicknames. The nicknames must be "
|
||||
"registered with nickserv.")
|
||||
o = input("> ")
|
||||
o.replace(" ", "")
|
||||
ls = o.split(",")
|
||||
c["irc"].update({"owners": ls})
|
||||
Config(conf_dir).write(c)
|
||||
log.info("<*> Configuration: setting up 'irc.owners' ...")
|
||||
|
||||
|
||||
def _check_connection(conf_dir):
|
||||
log = Logger(conf_dir, 'runtime.log')
|
||||
c = Config(conf_dir).read()
|
||||
if "connection" not in c["irc"]:
|
||||
print("\nSetting up the IRC server connection details.")
|
||||
|
||||
network = input("Hostname [chat.freenode.net]: ").replace(" ", "")
|
||||
if not network:
|
||||
network = "chat.freenode.net"
|
||||
|
||||
port = input("Port [6697]: ").replace(" ", "")
|
||||
if not port:
|
||||
port = 6697
|
||||
else:
|
||||
port = int(port)
|
||||
|
||||
while (1):
|
||||
ssl = input("SSL: (true, false) [true]").replace(" ", "").lower()
|
||||
if not ssl or ssl == "true":
|
||||
ssl = True
|
||||
break
|
||||
elif ssl == "false":
|
||||
ssl = False
|
||||
break
|
||||
print("\nError Invalid input. Try again.\n")
|
||||
|
||||
net_password = input("Network Password []: ")
|
||||
|
||||
nickname = input("Nickname [drastikbot]: ").replace(" ", "")
|
||||
if not nickname:
|
||||
nickname = "drastikbot"
|
||||
|
||||
username = input("Username [drastikbot]: ").replace(" ", "")
|
||||
if not username:
|
||||
username = "drastikbot"
|
||||
|
||||
realname = input("Realname [drastikbot2]: ")
|
||||
if not realname:
|
||||
realname = "drastikbot2"
|
||||
|
||||
while (1):
|
||||
authentication = input("Authentication mode (nickserv, sasl)"
|
||||
" [sasl]: ").replace(" ", "").lower()
|
||||
if not authentication or authentication == "sasl":
|
||||
authentication = "sasl"
|
||||
break
|
||||
elif authentication == "nickserv":
|
||||
break
|
||||
print("\nError Invalid input. Try again.\n")
|
||||
|
||||
auth_password = input("Authentication Password []: ")
|
||||
c["irc"].update({"connection": {"network": network,
|
||||
"port": port,
|
||||
"ssl": ssl,
|
||||
"net_password": net_password,
|
||||
"nickname": nickname,
|
||||
"username": username,
|
||||
"realname": realname,
|
||||
"authentication": authentication,
|
||||
"auth_password": auth_password}})
|
||||
Config(conf_dir).write(c)
|
||||
log.info("<*> Configuration: setting up 'irc.connection' ...")
|
||||
|
||||
|
||||
def _check_channels(conf_dir):
|
||||
log = Logger(conf_dir, 'runtime.log')
|
||||
c = Config(conf_dir).read()
|
||||
if "channels" not in c["irc"]:
|
||||
c["irc"].update({"channels": {}})
|
||||
if not c["irc"]["channels"]:
|
||||
chan_list = {}
|
||||
print("\nEnter the channels you want to join.")
|
||||
while True:
|
||||
ch = input("Channel (with #) (leave empty to exit): ")
|
||||
ch.replace(" ", "")
|
||||
if not ch:
|
||||
break
|
||||
ps = input("Password (leave empty if there is none): ")
|
||||
chan_list[ch] = ps
|
||||
c["irc"]["channels"].update(chan_list)
|
||||
Config(conf_dir).write(c)
|
||||
log.info("<*> Configuration: setting up 'irc.channels' ...")
|
||||
|
||||
|
||||
def _check_modules(conf_dir):
|
||||
log = Logger(conf_dir, 'runtime.log')
|
||||
c = Config(conf_dir).read()
|
||||
conf_w = Config(conf_dir).write
|
||||
if "modules" not in c["irc"]:
|
||||
c["irc"].update({"modules": {}})
|
||||
log.info("<*> Configuration: 'irc.modules' not found. "
|
||||
"Creating...")
|
||||
if "load" not in c["irc"]["modules"]:
|
||||
c["irc"]["modules"].update({"load": []})
|
||||
log.info("<*> Configuration: 'irc.modules.load' not found. "
|
||||
"Creating...")
|
||||
if not c["irc"]["modules"]["load"]:
|
||||
log.info("<*> Configuration: 'irc.modules.load' is empty. "
|
||||
"No modules will be loaded. Edit the configuration file's "
|
||||
"'irc.modules.load' section with the modules you want "
|
||||
"to use and restart the bot.")
|
||||
if "global_prefix" not in c["irc"]["modules"]:
|
||||
c["irc"]["modules"].update({"global_prefix": "."})
|
||||
log.info("<*> Configuration: 'irc.modules.global_prefix' not found. "
|
||||
"Setting default prefix '.'")
|
||||
if "channel_prefix" not in c["irc"]["modules"]:
|
||||
c["irc"]["modules"].update({"channel_prefix": {}})
|
||||
log.info("<*> Configuration: creating "
|
||||
"'irc.modules.channel_prefix' ...")
|
||||
if "blacklist" not in c["irc"]["modules"]:
|
||||
c["irc"]["modules"].update({"blacklist": {}})
|
||||
log.info("<*> Configuration: creating 'irc.modules.blacklist' ...")
|
||||
if "whitelist" not in c["irc"]["modules"]:
|
||||
c["irc"]["modules"].update({"whitelist": {}})
|
||||
log.info("<*> Configuration: creating 'irc.modules.whitelist' ...")
|
||||
conf_w(c)
|
||||
|
||||
|
||||
def _check_user_acl(conf_dir):
|
||||
log = Logger(conf_dir, 'runtime.log')
|
||||
c = Config(conf_dir).read()
|
||||
if "user_acl" not in c["irc"]:
|
||||
c["irc"].update({"user_acl": []})
|
||||
log.info("<*> Configuration: setting up 'irc.user_acl' ...")
|
||||
for i in c["irc"]["user_acl"]:
|
||||
if user_acl.is_expired(i):
|
||||
c["irc"]["user_acl"].remove(i)
|
||||
log.info(f"<*> Configuration: removed expired mask: '{i}' from "
|
||||
"'irc.user_acl' ...")
|
||||
Config(conf_dir).write(c)
|
||||
|
||||
|
||||
def _check_sys(conf_dir):
|
||||
#log = Logger(conf_dir, 'runtime.log')
|
||||
c = Config(conf_dir).read()
|
||||
if "sys" not in c:
|
||||
c.update({"sys": {}})
|
||||
#log.info("<*> Configuration: created 'sys' section.")
|
||||
if "log_level" not in c["sys"]:
|
||||
c["sys"].update({"log_level": "info"})
|
||||
#log.info("<*> Configuration: created 'sys.log_level'.")
|
||||
Config(conf_dir).write(c)
|
||||
|
||||
|
||||
def _check_exists(conf_dir):
|
||||
p = Path(f"{conf_dir}/config.json")
|
||||
if p.is_file():
|
||||
return
|
||||
with open(p, "w") as f:
|
||||
f.write("{}")
|
||||
|
||||
|
||||
def config_check(conf_dir):
|
||||
_check_exists(conf_dir)
|
||||
_check_sys(conf_dir)
|
||||
_check_irc(conf_dir)
|
||||
_check_owners(conf_dir)
|
||||
_check_connection(conf_dir)
|
||||
_check_channels(conf_dir)
|
||||
_check_modules(conf_dir)
|
118
src/toolbox/user_acl.py
Normal file
118
src/toolbox/user_acl.py
Normal file
@ -0,0 +1,118 @@
|
||||
# coding=utf-8
|
||||
|
||||
# This file implements checks for user access list rules.
|
||||
|
||||
'''
|
||||
Copyright (C) 2019 drastik.org
|
||||
|
||||
This file is part of drastikbot.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, version 3 only.
|
||||
|
||||
This program 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
'''
|
||||
|
||||
import datetime
|
||||
|
||||
|
||||
def _m_user(m, user):
|
||||
if m == user or m == "*":
|
||||
return True
|
||||
elif "*" == m[0] and m[1:] == user[-len(m[1:]):]:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def _m_host(m, hostmask):
|
||||
if m == hostmask or m == "*":
|
||||
return True
|
||||
elif "*" == m[0] and m[1:] == hostmask[-len(m[1:]):]:
|
||||
return True
|
||||
elif "*" == m[-1] and m[:-1] == hostmask[:len(m[:-1])]:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def _check_time(timestamp):
|
||||
if timestamp == 0:
|
||||
return True
|
||||
now = datetime.datetime.now(datetime.timezone.utc).timestamp()
|
||||
if timestamp > now:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def _check_module(m, module):
|
||||
if m == '*':
|
||||
return True
|
||||
ls = m.split(',')
|
||||
if module in ls:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def _is_banned(mask, channel, nick, user, hostmask, module):
|
||||
'''
|
||||
Check if a user is banned from using the bot.
|
||||
mask : is a mask following the following format:
|
||||
channel nickname!username@hostmask time modules.
|
||||
The mask allows for the * wildcard in frond of
|
||||
the username and hostmask.
|
||||
|
||||
'''
|
||||
tmp = mask.split(" ", 1)
|
||||
c = tmp[0]
|
||||
tmp = tmp[1].split("!", 1)
|
||||
n = tmp[0]
|
||||
tmp = tmp[1].split("@", 1)
|
||||
u = tmp[0]
|
||||
tmp = tmp[1].split(" ", 1)
|
||||
h = tmp[0]
|
||||
tmp = tmp[1].split(" ", 1)
|
||||
t = int(tmp[0])
|
||||
m = tmp[1]
|
||||
|
||||
# Bans are not case sensitive
|
||||
if (c.lower() == channel.lower() or c == '*') \
|
||||
and (n.lower() == nick.lower() or n == '*') \
|
||||
and _m_user(u.lower(), user.lower()) and _check_time(t) \
|
||||
and _m_host(h, hostmask) \
|
||||
and _check_module(m, module):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def is_banned(user_acl, channel, nick, user, hostmask, module):
|
||||
for i in user_acl:
|
||||
if _is_banned(i, channel, nick, user, hostmask, module):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_expired(mask):
|
||||
tmp = mask.split(" ", 1)
|
||||
tmp = tmp[1].split("!", 1)
|
||||
tmp = tmp[1].split("@", 1)
|
||||
tmp = tmp[1].split(" ", 1)
|
||||
tmp = tmp[1].split(" ", 1)
|
||||
t = int(tmp[0])
|
||||
if t == 0:
|
||||
return False
|
||||
now = datetime.datetime.now(datetime.timezone.utc).timestamp()
|
||||
if t < now:
|
||||
return True
|
||||
else:
|
||||
return False
|
Loading…
x
Reference in New Issue
Block a user