Massive refactoring/-structuring

This commit is contained in:
mrhanky
2017-07-07 02:11:20 +02:00
parent a82175a44b
commit 59b67d9570
41 changed files with 202 additions and 1011 deletions

32
bot/plugins/__init__.py Normal file
View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
from irc3 import IrcBot
from irc3.plugins.command import Commands
MODULE = __name__
class BasePlugin(object):
def __init__(self, bot: IrcBot):
self.bot = bot
self.log = bot.log
self.guard = bot.get_plugin(Commands).guard
class Plugin(BasePlugin):
@classmethod
def reload(cls, old: BasePlugin):
return cls(old.bot)
# Import the PgSQL storage plugin
from .storage import Storage # noqa: E402
class DatabasePlugin(Plugin):
def __init__(self, bot: IrcBot):
super().__init__(bot)
# Get PgSQL storage instance and connection + cursor
self.db = bot.get_plugin(Storage)
self.con = self.db.con
self.cur = self.db.cur

32
bot/plugins/admin.py Normal file
View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
import logging
import irc3
from docopt import Dict as DocOptDict
from irc3.plugins.command import command
from irc3.utils import IrcString
from . import MODULE, Plugin
logger = logging.getLogger(__name__)
@command(permission='admin', show_in_help_list=False)
def reload(bot: irc3.IrcBot, mask: IrcString, target: IrcString,
args: DocOptDict):
"""Reloads a plugin or the whole bot.
%%reload [<plugin>]
"""
plugin = args.get('<plugin>')
if plugin:
bot.reload('{module}.{plugin}'.format(plugin=plugin, module=MODULE))
bot.privmsg(target, 'Reloaded plugin "{plugin}"'.format(plugin=plugin))
else:
bot.reload()
bot.privmsg(target, 'Reloaded the bot')
@irc3.plugin
class Admin(Plugin):
pass

60
bot/plugins/coins.py Normal file
View File

@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
import irc3
import requests
from docopt import Dict as DocOptDict
from irc3.plugins.command import command
from irc3.utils import IrcString
from . import Plugin
@irc3.plugin
class Coins(Plugin):
requires = ['irc3.plugins.command']
API_URL = 'https://api.cryptowat.ch/markets/coinbase/{crypto}{currency}/summary'
CURRENCIES = {
'usd': '$',
'eur': '',
'eth': 'Ξ',
'btc': '฿',
}
@command
def btc(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Gets the Bitcoin values from cryptowatch.
%%btc [<currency>]
"""
return self._cryptowat_summary('btc', args.get('<currency>') or 'usd')
@command
def eth(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Gets the Ethereum values from cryptowatch.
%%eth [<currency>]
"""
return self._cryptowat_summary('eth', args.get('<currency>') or 'usd')
def _cryptowat_summary(self, crypto: str, currency: str = 'usd'):
# Check if valid currency + crypto2currency
if currency not in self.CURRENCIES or crypto == currency:
return
# Send request to api
data = requests.get(self.API_URL.format(crypto=crypto, currency=currency))
if data:
result = data.json()['result']
return '\x02[{crypto}]\x0F ' \
'Current: \x02\x0307{currency}{last:,.2f}\x0F - ' \
'High: \x02\x0303{currency}{high:,.2f}\x0F - ' \
'Low: \x02\x0304{currency}{low:,.2f}\x0F - ' \
'Change: \x02\x0307{change:,.2f}%\x0F - ' \
'Volume: \x02\x0307{volume}\x0F' \
.format(crypto=crypto.upper(),
currency=self.CURRENCIES[currency],
last=result['price']['last'],
high=result['price']['high'],
low=result['price']['low'],
change=result['price']['change']['percentage'] * 100,
volume=result['volume'])

84
bot/plugins/ctcp.py Normal file
View File

@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
import time
import irc3
from docopt import Dict as DocOptDict
from irc3.plugins.command import command
from irc3.utils import IrcString
from . import Plugin
@irc3.plugin
class CTCP(Plugin):
requires = ['irc3.plugins.async',
'irc3.plugins.command']
# noinspection PyMethodMayBeStatic
def _ctcp(self, name: str, nick: str, reply: str):
return '\x02[{}]\x0F {}: {}'.format(name.upper(), nick, reply)
async def ctcp(self, name: str, mask: IrcString, args: DocOptDict):
nick = args.get('<nick>') or mask.nick
name = name.upper()
data = await self.bot.ctcp_async(nick, name)
if not data or data['timeout']:
reply = 'timeout'
elif not data['success']:
reply = 'Error: {}'.format(data['reply'])
else:
reply = data['reply']
return self._ctcp(name, nick, reply)
@command
async def ping(self, mask: IrcString, target: IrcString,
args: DocOptDict):
"""Sends ping via CTCP to user and sends the time needed
%%ping [<nick>]
"""
nick = args.get('<nick>') or mask.nick
data = await self.bot.ctcp_async(nick, 'PING {}'.format(time.time()))
if not data or data['timeout']:
reply = 'timeout'
elif not data['success']:
reply = 'Error: {}'.format(data['reply'])
else:
delta = time.time() - float(data['reply'])
if delta < 1.0:
delta *= 1000
unit = 'ms'
else:
unit = 's'
reply = '{0:.3f} {1}'.format(delta, unit)
return self._ctcp('PING', nick, reply)
@command
async def finger(self, mask: IrcString, target: IrcString,
args: DocOptDict):
"""Gets the client response for finger nick user via CTCP
%%finger [<nick>]
"""
return await self.ctcp('FINGER', mask, args)
@command
async def time(self, mask: IrcString, target: IrcString,
args: DocOptDict):
"""Gets the client time from nick via CTCP
%%time [<nick>]
"""
return await self.ctcp('TIME', mask, args)
@command
async def ver(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Gets the client version from nick via CTCP
%%ver [<nick>]
"""
return await self.ctcp('VERSION', mask, args)

34
bot/plugins/isup.py Normal file
View File

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
from urllib.parse import urlparse
import irc3
import requests
from docopt import Dict as DocOptDict
from irc3.plugins.command import command
from irc3.utils import IrcString
from . import DatabasePlugin
@irc3.plugin
class Useless(DatabasePlugin):
requires = ['irc3.plugins.command',
'bot.plugins.storage']
@command
def isup(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Checks if a address is up.
%%isup <address>
"""
address = args['<address>']
if not address.startswith('http://'):
address = 'https://{}'.format(address)
parsed = urlparse(address)
try:
requests.head(address)
state = 'up'
except requests.ConnectionError:
state = 'down'
return '{}://{} seems to be {}'.format(parsed.scheme, parsed.netloc, state)

60
bot/plugins/linux.py Normal file
View File

@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
import random
import irc3
import feedparser
from docopt import Dict as DocOptDict
from irc3.plugins.command import command
from irc3.utils import IrcString
from . import Plugin
from ..utils import re_generator
GNU_LINUX = """I'd Just Like To Interject For A Moment. What you're referring
to as Linux, is in fact, GNU/Linux, or as I've recently taken to calling it,
GNU plus Linux. Linux is not an operating system unto itself, but rather
another free component of a fully functioning GNU system made useful by the
GNU corelibs, shell utilities and vital system components comprising a full
OS as defined by POSIX."""
class Linux(Plugin):
KERNEL_FEED = 'https://www.kernel.org/feeds/kdist.xml'
@irc3.event(r'(?i)^:\S+ PRIVMSG (?P<target>\S+) :.*(debian|ubuntu|apt|dpkg).*')
def debillian(self, target: str):
"""Annoying RE trigger for debian with variable count of E."""
if random.randint(0, 3) is 0:
self.bot.privmsg(target, re_generator())
@irc3.event(r'(?i)^:\S+ PRIVMSG (?P<target>\S+) :.*(?<!gnu[/+])linux(?! kernel).*')
def linux(self, target: str):
"""Super annoying, useless 'Stallman is mad' trigger."""
if random.randint(0, 12) is 0:
self.bot.privmsg(target, GNU_LINUX)
@command
def kernel(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Get Linux kernel releases.
%%kernel
"""
feed = feedparser.parse(self.KERNEL_FEED)
# Cancel if no feed or entries
if not feed or not feed.get('entries'):
self.log.error('Error fetching kernel.org releases feed')
return
# Make list of releases
releases = []
for e in feed['entries']:
version, branch = e['title'].split(': ')
if '(EOL)' in e['description']:
branch = '{}, \x1DEOL\x0F'.format(branch)
releases.append('\x02{}\x0F ({})'.format(version, branch))
return '\x02[Kernel]\x0F {}'.format(', '.join(releases))

72
bot/plugins/mcmaniac.py Normal file
View File

@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
import irc3
from docopt import Dict as DocOptDict
from irc3.plugins.command import command
from irc3.utils import IrcString
from psycopg2 import Error
from . import DatabasePlugin
from ..utils import parse_int
@irc3.plugin
class McManiac(DatabasePlugin):
requires = ['irc3.plugins.command',
'bot.plugins.storage']
@command(options_first=True)
def mcmaniac(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Get a random McManiaC or by index.
%%mcmaniac [<index>]
"""
index = args.get('<index>')
if index:
index = parse_int(index)
if not index:
return
index, order = index
order = 'id {order}'.format(order=order)
offset = 'offset %s'
else:
order = 'random()'
offset = ''
# Fetch result from database
self.cur.execute('''
select
item,
rank() over (order by id),
count(*) over (rows between unbounded preceding
and unbounded following) as total
from
mcmaniacs
order by
{order}
limit
1
{offset}
'''.format(order=order, offset=offset), [index])
result = self.cur.fetchone()
if result:
return '[{rank}/{total}] {item}'.format(**result)
# TODO: fix regex ("McFooiaC McBariaC" adds "Mc\S+iaC")
@irc3.event(r'^:(?P<mask>\S+) PRIVMSG \S+ :.*(?P<item>Mc\S+iaC).*')
def save(self, mask: str, item: str):
if IrcString(mask).nick != self.bot.nick:
try:
# Insert into database
self.cur.execute('''
insert into
mcmaniacs (item)
values
(%s)
''', [item])
self.con.commit()
except Error:
# Rollback transaction on error
self.con.rollback()

130
bot/plugins/quotes.py Normal file
View File

@ -0,0 +1,130 @@
# -*- coding: utf-8 -*-
import re
import irc3
from docopt import Dict as DocOptDict
from irc3.plugins.command import command
from irc3.utils import IrcString
from psycopg2 import Error
from . import DatabasePlugin
from ..utils import parse_int
@irc3.plugin
class Quotes(DatabasePlugin):
requires = ['irc3.plugins.command',
'bot.plugins.storage']
def add_quote(self, mask, nick, quote, channel):
# Parse nick from "<@foobar>" like strings
nick = re.match(r'<?[~&@%+]?([a-zA-Z0-9_\-^`|\\\[\]{}]+)>?', nick).group(1)
if not nick:
self.bot.notice(mask.nick, '[Quotes] Error parsing nick')
else:
# Insert quote into database
self.cur.execute('''
insert into
quotes (nick, item, channel, created_by)
values
(%s, %s, %s, %s)
''', [nick, quote, channel, mask.nick])
def delete_quote(self, nick, quote):
index, order = parse_int(quote, select=False)
if index:
# Delete from database
self.cur.execute('''
with ranked_quotes as (
select
id,
rank() over (partition by nick order by id {order})
from
quotes
where
nick = %s
)
delete from
quotes
where
id = (
select
id
from
ranked_quotes
where
rank = %s
)
'''.format(order=order), [nick, index])
@command(options_first=True)
def q(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Get, add or delete quotes for an user.
%%q <cmd> <nick> <quote>...
%%q <nick> [<index>]
"""
cmd = args.get('<cmd>')
nick = args['<nick>']
quote = ' '.join(args.get('<quote>'))
if cmd and quote:
try:
# Anybody can add
if cmd == 'add':
self.add_quote(mask, nick, quote, target)
# But only admins can delete
elif cmd == 'del' and self.guard.has_permission(mask, 'admin'):
self.delete_quote(nick, quote)
self.con.commit()
except Error as ex:
# Rollback transaction on error
self.con.rollback()
else:
index = args.get('<index>')
values = [nick]
if index:
index = parse_int(index)
if not index:
return
index, order = index
order = 'rank {order}'.format(order=order)
offset = 'offset %s'
values.append(index)
else:
order = 'random()'
offset = ''
# Fetch quote from database
self.cur.execute("""
with ranked_quotes as (
select
nick,
item,
rank() over (partition by nick order by id),
count(*) over (partition by nick) as total
from
quotes
)
select
*
from
ranked_quotes
where
lower(nick) like lower(%s)
order by
{order}
limit
1
{offset}
""".format(order=order, offset=offset), values)
result = self.cur.fetchone()
if result:
return '[{rank}/{total}] <{nick}> {item}'.format(**result)

87
bot/plugins/rape.py Normal file
View File

@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
import random
import irc3
from docopt import Dict as DocOptDict
from irc3.plugins.command import command
from irc3.utils import IrcString
from psycopg2 import Error
from . import DatabasePlugin
@irc3.plugin
class Rape(DatabasePlugin):
requires = ['irc3.plugins.command',
'bot.plugins.storage']
@command
def owe(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Shows how much a nick owes.
%%owe [<nick>]
"""
nick = args.get('<nick>') or mask.nick
# Fetch result from database
self.cur.execute('''
select
fines
from
users
where
lower(nick) = lower(%s)
''', [nick])
owes = self.cur.fetchone()
# Colorize owe amount and return string
if owes:
fines = '4${fines}'.format(fines=owes['fines'])
else:
fines = '3$0'
# Return total owes
return '{nick} owes: \x03{fines}\x03'.format(nick=nick, fines=fines)
@command
def rape(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Rapes a nick and eventually charge for it.
%%rape <nick>
"""
nick = args.get('<nick>') or mask.nick
rand = random.randint(0, 3)
if rand not in (0, 1):
self.bot.action(target, 'rapes {nick}'.format(nick=nick))
else:
fine = random.randint(1, 500)
try:
# Insert or add fine to database and return total owe
self.cur.execute('''
insert into
users (nick, fines)
values
(lower(%s), %s)
on conflict (nick) do update set
fines = users.fines + excluded.fines
returning
fines
''', [mask.nick, fine])
self.con.commit()
# Get reason based on rand value
reason = ('raping', 'being too lewd and getting raped')[rand]
# Print fine and total owe
self.bot.action(target,
'fines {nick} \x02${fine}\x02 for {reason}. '
'You owe: \x0304${total}\x03'
.format(nick=mask.nick,
fine=fine,
reason=reason,
total=self.cur.fetchone()['fines']))
except Error:
# Rollback transaction on error
self.con.rollback()

59
bot/plugins/regex.py Normal file
View File

@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
import re
import irc3
from irc3.utils import IrcString
from . import DatabasePlugin
@irc3.plugin
class Useless(DatabasePlugin):
requires = ['irc3.plugins.command',
'bot.plugins.storage']
@irc3.event(r'^:(?P<mask>\S+) PRIVMSG (?P<target>#\S+) :s/'
r'(?P<search>(?:[^/\\]|\\.)*)/'
r'(?P<replace>(?:.*?))'
r'(?:/ ?(?P<nick>.*))?$')
def regex(self, mask: str, target: str, search: str, replace: str,
nick: str = None):
nick = (nick or IrcString(mask).nick).strip()
if nick == self.bot.nick:
return
self.cur.execute('''
select
item
from
last_messages
where
nick = lower(%s)
and channel = lower(%s)
''', [nick, target])
result = self.cur.fetchone()
if result:
old = result['item']
msg = old.replace(search, '\x02{}\x0F'.format(replace), 1)
msg = re.sub(r'\x01ACTION (.*)\x01', r'/me \1', msg)
if old != msg:
self.bot.privmsg(target, '<{nick}> {msg}'.format(nick=nick,
msg=msg))
@irc3.event(r'(?i)^:(?P<mask>\S+) PRIVMSG (?P<target>#\S+) :(?P<msg>.*)$')
def last_message(self, mask: str, target: str, msg: str):
"""Saves the last message of a user for each channel (for regex)."""
mask = IrcString(mask)
self.cur.execute('''
insert into
last_messages (nick, host, channel, item)
values
(lower(%s), %s, lower(%s), %s)
on conflict (nick, channel) do update set
host = excluded.host,
item = excluded.item
''', [mask.nick, mask.host, target, msg])
self.con.commit()

74
bot/plugins/seen.py Normal file
View File

@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
import re
from datetime import datetime
import irc3
from docopt import Dict as DocOptDict
from irc3.plugins.command import command
from irc3.utils import IrcString
from psycopg2 import Error
from . import DatabasePlugin
@irc3.plugin
class Seen(DatabasePlugin):
requires = ['irc3.plugins.command',
'bot.plugins.storage']
@command
def seen(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Get last seen date and message for a nick.
%%seen [<nick>]
"""
nick = args.get('<nick>') or mask.nick
# Don't be stupid
if nick == mask.nick:
return '{nick}, have you seen in the mirror?'.format(nick=nick)
# Fetch seen from database
self.cur.execute('''
select
*
from
seens
where
lower(nick) = lower(%s)
''', [nick])
seen = self.cur.fetchone()
# No result
if not seen:
return 'I\'ve never seen {nick}'.format(nick=nick)
# Return result
return '{nick} was last seen {delta} saying: {message}'.format(
nick=seen['nick'],
# TODO: relative string delta?
delta=seen['seen_at'],
message=re.sub(r'\x01ACTION (.*)\x01', r'/me \1', seen['message']),
)
@irc3.event(r'(?i)^:(?P<mask>\S+) PRIVMSG (?P<target>\S+) :(?P<msg>.*)')
def save(self, mask: str, target: str, msg: str):
mask = IrcString(mask)
try:
# Insert or update if user writes a message
self.cur.execute('''
insert into
seens (nick, host, channel, message, seen_at)
values
(%s, %s, %s, %s, %s)
on conflict (nick) do update set
host = excluded.host,
channel = excluded.channel,
seen_at = excluded.seen_at,
message = excluded.message
''', [mask.nick, mask.host, target, msg, datetime.now()])
self.con.commit()
except Error:
# Rollback transaction on error
self.con.rollback()

18
bot/plugins/storage.py Normal file
View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
import os
import irc3
import psycopg2
from psycopg2.extras import DictCursor
from . import Plugin
@irc3.plugin
class Storage(Plugin):
def __init__(self, bot: irc3.IrcBot):
super().__init__(bot)
# Create database connection
self.con = psycopg2.connect(os.environ['DATABASE_URI'])
# Create database cursor (with dict factory to access rows by name)
self.cur = self.con.cursor(cursor_factory=DictCursor)

98
bot/plugins/tell.py Normal file
View File

@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
from datetime import datetime
import irc3
from docopt import Dict as DocOptDict
from irc3.plugins.command import command
from irc3.utils import IrcString
from psycopg2 import Error
from . import DatabasePlugin
@irc3.plugin
class Tell(DatabasePlugin):
requires = ['irc3.plugins.command',
'bot.plugins.storage']
def __init__(self, bot: irc3.IrcBot):
super().__init__(bot)
self.tell_queue = {}
# Fetch tells from database
self.cur.execute('''
select
to_nick, from_nick, message, created_at
from
tells
''')
# Add tells to queue
for res in self.cur.fetchall():
nick = res['to_nick'].lower()
# Create list in queue and add tell
if nick not in self.tell_queue:
self.tell_queue[nick] = []
self.tell_queue[nick].append(res[1:])
@command
def tell(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Saves a message for nick to forward on activity
%%tell <nick> <message>...
"""
nick = args['<nick>']
nick_lower = nick.lower()
tell = [nick, mask.nick, ' '.join(args['<message>']).strip(),
datetime.now()]
# Create list in queue and add tell
if nick_lower not in self.tell_queue:
self.tell_queue[nick_lower] = []
self.tell_queue[nick_lower].append(tell[1:])
try:
# Insert tell into database
self.cur.execute('''
insert into
tells (to_nick, from_nick, message, created_at)
values
(%s, %s, %s, %s)
''', tell)
self.con.commit()
except Error:
# Rollback transaction on error
self.con.rollback()
@irc3.event(r'(?i)^:(?P<mask>.*) PRIVMSG .* :.*')
def check(self, mask: str):
"""If activ nick has tells, forward and delete them."""
nick = IrcString(mask).nick
nick_lower = nick.lower()
if nick_lower in self.tell_queue:
# Forward all tells for nick
for tell in self.tell_queue[nick_lower]:
# TODO: format time
self.bot.privmsg(nick, '[Tell] Message from {nick} at {time}: '
'{message}'.format(nick=tell[0],
time=tell[2],
message=tell[1]))
# Remove nick from queue
del self.tell_queue[nick_lower]
try:
# Remove tells from database
self.cur.execute('''
delete from
tells
where
lower(to_nick) = lower(%s)
''', [nick])
self.con.commit()
except Error as ex:
print(ex)
# Rollback transaction on error
self.con.rollback()

106
bot/plugins/timer.py Normal file
View File

@ -0,0 +1,106 @@
# -*- coding: utf-8 -*-
import asyncio
from datetime import datetime, timedelta
import irc3
from docopt import Dict as DocOptDict
from irc3.plugins.command import command
from irc3.utils import IrcString
from psycopg2 import Error
from . import DatabasePlugin
from ..utils import time_delta
@irc3.plugin
class Timer(DatabasePlugin):
requires = ['irc3.plugins.command',
'bot.plugins.storage']
def __init__(self, bot: irc3.IrcBot):
super().__init__(bot)
# Fetch timers from database
self.cur.execute('''
select
id, mask, target, message, delay, ends_at
from
timers
''')
# Recreate timers
for res in self.cur.fetchall():
self.start_timer(IrcString(res['mask']),
res['target'],
res['message'],
res['delay'],
res['ends_at'] - datetime.now(),
res['id'])
@command
def timer(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Sets a timer, delay can be: s, m, h, w, mon, y(
%%timer <delay> <message>...
"""
delay = args['<delay>']
delta = time_delta(delay)
if not delta:
self.bot.privmsg(target, 'Invalid timer delay')
else:
message = ' '.join(args['<message>'])
values = [mask, target, message, delay]
try:
# Insert into database (add now + delta)
self.cur.execute('''
insert into
timers (mask, target, message, delay, ends_at)
values
(%s, %s, %s, %s, %s)
returning id
''', values + [datetime.now() + delta])
self.con.commit()
# Add delta and id from inserted and start timer
values.extend([delta, self.cur.fetchone()['id']])
self.start_timer(*values)
# Send notice to user that timer has been set
self.bot.notice(mask.nick, 'Timer in {delay} set: {message}'
.format(delay=delay, message=message))
except Error:
# Rollback transaction on error
self.con.rollback()
def start_timer(self, mask: IrcString, target: IrcString, message: str,
delay: str, delta: timedelta, row_id: int):
"""Async function, sleeps for `delay` seconds and sends notification"""
async def callback():
# Sleep if necessary until timed
seconds = delta.total_seconds()
if seconds > 0:
await asyncio.sleep(seconds)
# Send reminder
self.bot.privmsg(target, '\x02[Timer]\x0F {nick}: {message} '
'({delay})'.format(message=message,
nick=mask.nick,
delay=delay))
try:
# Delete timer from database
self.cur.execute('''
delete from
timers
where
id = %s
''', [row_id])
self.con.commit()
except Error:
# Rollback transaction on error
self.con.rollback()
asyncio.ensure_future(callback())

51
bot/plugins/urban.py Normal file
View File

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
import irc3
import requests
from docopt import Dict as DocOptDict
from irc3.plugins.command import command
from irc3.utils import IrcString
from . import Plugin
@irc3.plugin
class Urban(Plugin):
requires = ['irc3.plugins.command']
URL = 'https://api.urbandictionary.com/v0/define'
@command
def ud(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Searches for a term on YouTube and returns first result.
%%ud <term>...
"""
# Clean, strip and split term by whitespace
term = ' '.join(args.get('<term>')).lower().strip().split()
if term[-1].isdigit():
# Parse number at end as index - 1 and slice term
index = int(term[-1]) - 1
term = ' '.join(term[:-1])
else:
# Set index to 0
index = 0
# Fetch data for term from urban dictionary as json
data = requests.get(self.URL, params=dict(term=term)).json()
# No results
if data['result_type'] == 'no_results':
return 'Term not found'
# Get result by index
res = data['list'][index]
# Format and return result
return '[{idx}/{len}] \x02{word}\x02: {definition} - {example}'.format(
idx=index + 1,
len=len(data['list']),
word=res['word'],
definition=res['definition'].replace('\r\n', ' '),
example=res['example'].replace('\r\n', ' '),
)

298
bot/plugins/useless.py Normal file
View File

@ -0,0 +1,298 @@
# -*- coding: utf-8 -*-
import random
import re
import irc3
from docopt import Dict as DocOptDict
from irc3.plugins.command import command
from irc3.utils import IrcString
from psycopg2 import Error
from . import DatabasePlugin
from ..utils import re_generator
RAINBOW = (5, 7, 8, 9, 3, 10, 12, 2, 6, 13)
RAINBOW_LEN = len(RAINBOW)
@irc3.plugin
class Useless(DatabasePlugin):
requires = ['irc3.plugins.command',
'bot.plugins.storage']
WOAH = (
'woah',
'woah indeed',
'woah woah woah!',
'keep your woahs to yourself',
)
@irc3.event(r'(?i)^:(?P<mask>\S+) JOIN :(?P<target>#\S+)$')
def tehkuh(self, mask, target):
if re.search(r'(?i).*(tehkuh).*@.*(telefonica|vodafone|kabel|unity-media).*', mask):
self.bot.privmsg(target, '{}: Bouncer'.format(IrcString(mask).nick))
@irc3.event(r'(?i)^:\S+ PRIVMSG (?P<target>\S+) :.*(woah|whoa).*$')
def woah(self, target: str):
"""Colorize words in a sentence with rainbow colors."""
if random.randint(0, 4) is 0:
self.bot.privmsg(target, random.choice(self.WOAH))
@irc3.event(r'(?i)^:\S+ PRIVMSG (?P<target>\S+) :(?P<msg>huehuehue)$')
def huehuehue(self, target: str, msg: str):
"""Returns a huehuehue when someone writes it."""
self.bot.privmsg(target, msg)
@irc3.event(r'(?i)^:\S+ PRIVMSG (?P<target>\S+) :re+$')
def reeeeee(self, target: str):
"""Returns a REEEE."""
self.bot.privmsg(target, re_generator())
@irc3.event(r'(?i)^:\S+ PRIVMSG (?P<target>\S+) :same$')
def same(self, target: str):
"""Returns a plain same when a user writes same."""
self.bot.privmsg(target, 'same')
@irc3.event(r'(?i)^:\S+ PRIVMSG (?P<target>\S+) :\[(?P<msg>.*)\]$')
def intensifies(self, target: str, msg: str):
"""String with brackets around will be returned with INTENSIFIES."""
self.bot.privmsg(target, '\x02[{} INTENSIFIES]'.format(msg.upper()))
@command
def kill(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Kills a user with a random message.
%%kill [<nick>]
"""
self.cur.execute('''
select
item
from
kills
order by
random()
limit
1
''')
nick = args.get('<nick>') or mask.nick
self.bot.action(target, self.cur.fetchone()['item'].format(nick))
@command
def yiff(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Yiffs a user with a random message.
%%yiff [<nick>]
"""
self.cur.execute('''
select
item
from
yiffs
order by
random()
limit
1
''')
nick = args.get('<nick>') or mask.nick
self.bot.action(target, self.cur.fetchone()['item'].format(nick))
@command
def waifu(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Get waifu for a user.
%%waifu [<nick>...]
"""
nick = ' '.join(args.get('<nick>')) or mask.nick
if nick.startswith('='):
waifu = nick[1:]
try:
self.cur.execute('''
insert into
users (nick, waifu)
values
(lower(%s), %s)
on conflict (nick) do update set
waifu = excluded.waifu
''', [mask.nick, waifu])
self.bot.notice(mask.nick, 'Waifu set to: {}'.format(waifu))
except Error as ex:
print(ex)
self.con.rollback()
else:
self.cur.execute('''
select
waifu
from
users
where
lower(nick) = lower(%s)
''', [nick])
result = self.cur.fetchone()
if result and result['waifu']:
return '\x02[Waifu]\x0F {}: {}'.format(nick, result['waifu'])
@command
def husbando(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Get husbando for a user.
%%husbando [<nick>...]
"""
nick = ' '.join(args.get('<nick>')) or mask.nick
if nick.startswith('='):
nick = nick[1:]
try:
self.cur.execute('''
insert into
users (nick, husbando)
values
(lower(%s), %s)
on conflict (nick) do update set
husbando = excluded.husbando
''', [mask.nick, nick])
self.bot.notice(mask.nick, 'Husbando set to: {}'.format(nick))
except Error as ex:
print(ex)
self.con.rollback()
else:
self.cur.execute('''
select
husbando
from
users
where
lower(nick) = lower(%s)
''', [nick])
result = self.cur.fetchone()
if result and result['husbando']:
return '\x02[Husbando]\x0F {}: {}'.format(nick, result['husbando'])
@command
def storyofpomfface(self, mask: IrcString, target: IrcString,
args: DocOptDict):
"""Story of pomf face.
%%storyofpomfface
"""
for face in (':O C==3', ':OC==3', ':C==3', ':C=3', ':C3', ':3'):
self.bot.privmsg(target, face)
@command
def choose(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Decides between items separated by comma.
%%choose <items>...
"""
choice = random.choice(' '.join(args['<items>']).split(','))
return '{}: {}'.format(mask.nick, choice.strip())
@command
def jn(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Decides between yes and no (for a question).
%%jn <question>...
%%jn
"""
choice = random.choice(['3Ja', '4Nein'])
return '{}: \x02\x030{}'.format(mask.nick, choice)
@command
def kiss(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Kisses a user.
%%kiss <nick>
"""
return '(づ。◕‿‿◕。)\x0304。。・゜゜・。。・゜❤\x0F {} \x0304❤'.format(args['<nick>'])
@command
def hug(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Hugs a user.
%%hug <nick>
"""
return '\x0304♥♡❤♡♥\x0F {} \x0304♥♡❤♡♥'.format(args['<nick>'])
@command
def bier(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Gives nick a beer.
%%bier [<nick>]
"""
nick = args.get('<nick>') or mask.nick
self.bot.action(target, 'schenkt ein kühles Blondes an {} aus.'.format(nick))
@command
def fucken(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Kills and fucks a nick.
%%fucken [<nick>]
"""
nick = args.get('<nick>') or mask.nick
self.bot.action(target, 'fuckt {0} und tötet {0} anschließend.'.format(nick, nick))
@command
def anhero(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Kicks a nick.
%%anhero
"""
self.bot.privmsg(target, 'Sayonara bonzai-chan...')
self.bot.kick(target, mask.nick)
@command
def sudoku(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Kicks a nick.
%%sudoku
"""
self.anhero(mask, target, args)
@command
def hack(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Hacks (a user).
%%hack [<nick>]
"""
nick = args.get('<nick>') or ''
return 'hacking{}...'.format(' %s' % nick if nick else '')
@command
def gay(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Make someone gay (alias to rainbow)
%%gay <word>...
"""
return self.rainbow(mask, target, args)
@command
def rainbow(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Colorize a word in rainbow colors.
%%rainbow <word>...
"""
last = 0
word = []
for char in ' '.join(args.get('<word>')):
if char != ' ':
char = self._rainbow(last, char)
last += 1
word.append(char)
return ''.join(word)
@command
def wrainbow(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Colorize words in a sentence with rainbow colors.
%%wrainbow <words>...
"""
return ' '.join([self._rainbow(i, word) for i, word in enumerate(args['<words>'])])
# noinspection PyMethodMayBeStatic
def _rainbow(self, i, char):
return '\x030{}{}'.format(RAINBOW[i % RAINBOW_LEN], char)

44
bot/plugins/weather.py Normal file
View File

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
import irc3
import requests
from docopt import Dict as DocOptDict
from irc3.plugins.command import command
from irc3.utils import IrcString
from . import Plugin
@irc3.plugin
class Weather(Plugin):
requires = ['irc3.plugins.command']
URL = 'https://query.yahooapis.com/v1/public/yql?format=json&q=' \
'select * from weather.forecast where u="c" and woeid in ' \
'(select woeid from geo.places(1) where text="{}")'
@command
def weather(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Gets the weather from Yahoo weather API.
%%weather <location>...
"""
req = requests.get(self.URL.format(' '.join(args['<location>'])))
data = req.json()
if 'error' in data:
return '\x02[Weather]\x0F Error'
elif not data['query']['results']:
return '\x02[Weather]\x0F Location not found'
else:
res = data['query']['results']['channel']
return '\x02[Weather]\x0F {city}, {region}, {country}: ' \
'\x02{temp}°{unit_temp} {text}\x0F, ' \
'\x02{direction} {speed} {unit_speed}\x0F' \
.format(city=res['location']['city'],
region=res['location']['region'].strip(),
country=res['location']['country'],
temp=res['item']['condition']['temp'],
text=res['item']['condition']['text'],
direction='↑↗→↘↓↙←↖'[round(int(res['wind']['direction']) / 45) % 8],
speed=res['wind']['speed'],
unit_temp=res['units']['temperature'],
unit_speed=res['units']['speed'])

83
bot/plugins/youtube.py Normal file
View File

@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
import os
import re
import irc3
import requests
from docopt import Dict as DocOptDict
from irc3.plugins.command import command
from irc3.utils import IrcString
from . import Plugin
from ..utils import date_from_iso
@irc3.plugin
class YouTube(Plugin):
requires = ['irc3.plugins.command']
URL = 'https://www.googleapis.com/youtube/v3'
API = '{}/videos?part=snippet,statistics,contentDetails'.format(URL)
SEARCH = '{}/search?part=id'.format(URL)
def get_video_data(self, video_id: str):
"""Requests the infos for a video id and formats them."""
data = self._api(self.API, id=video_id)
if not data['items']:
return
item = data['items'][0]
date = date_from_iso(item['snippet']['publishedAt'])
length = re.findall('(\d+[DHMS])', item['contentDetails']['duration'])
views = int(item['statistics'].get('viewCount', 0))
likes = int(item['statistics'].get('likeCount', 0))
dislikes = int(item['statistics'].get('dislikeCount', 0))
try:
score = 100 * float(likes) / (likes + dislikes)
except ZeroDivisionError:
score = 0
return '{title} - length {length} -\x033 {likes:,}\x03 /' \
'\x034 {dislikes:,}\x03 ({score:,.1f}%) - {views:,} ' \
'views - {channel} on {date}' \
.format(title=item['snippet']['title'],
channel=item['snippet']['channelTitle'],
length=' '.join([l.lower() for l in length]),
likes=likes,
dislikes=dislikes,
score=score,
views=views,
date=date.strftime('%Y.%m.%d'))
@irc3.event(r'(?i)^:.* PRIVMSG (?P<target>.*) :.*(?:youtube.*?(?:v=|/v/)'
r'|youtu\.be/)(?P<video_id>[-_a-zA-Z0-9]+).*')
def youtube_parser(self, target: str, video_id: str):
data = self.get_video_data(video_id)
if data:
self.bot.privmsg(target, data)
@command
def yt(self, mask: IrcString, target: IrcString, args: DocOptDict):
"""Searches for query on YouTube and returns first result.
%%yt <query>...
"""
data = self._api(self.SEARCH, q=' '.join(args['<query>']))
if 'error' in data:
return '\x02[YouTube]\x0F Error performing search'
elif data['pageInfo']['totalResults'] is 0:
return '\x02[YouTube]\x0F No results found'
else:
video_id = data['items'][0]['id']['videoId']
data = self.get_video_data(video_id)
return '{} - https://youtu.be/{}'.format(data, video_id)
@staticmethod
def _api(url: str, **kwargs):
"""Wrapper around requests.get which adds the Google API key."""
kwargs['key'] = os.environ['GOOGLE_API_KEY']
return requests.get(url, params=kwargs).json()