From 5a0b82936984b3153d687754323639bdc18910d2 Mon Sep 17 00:00:00 2001 From: mrhanky Date: Tue, 16 May 2017 00:26:10 +0200 Subject: [PATCH] Initial commit --- .env-example | 1 + .gitignore | 4 ++ README.md | 0 config.json | 34 ++++++++++++++ nxy/bot.py | 46 ++++++++++++++++++ nxy/plugins/__init__.py | 32 +++++++++++++ nxy/plugins/admin.py | 22 +++++++++ nxy/plugins/bitcoin.py | 31 +++++++++++++ nxy/plugins/ctcp.py | 79 +++++++++++++++++++++++++++++++ nxy/plugins/mcmaniac.py | 59 ++++++++++++++++++++++++ nxy/plugins/quotes.py | 62 +++++++++++++++++++++++++ nxy/plugins/reminder.py | 41 ++++++++++++++++ nxy/plugins/useless.py | 77 +++++++++++++++++++++++++++++++ nxy/utils.py | 100 ++++++++++++++++++++++++++++++++++++++++ requirements.txt | 4 ++ 15 files changed, 592 insertions(+) create mode 100644 .env-example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 config.json create mode 100644 nxy/bot.py create mode 100644 nxy/plugins/__init__.py create mode 100644 nxy/plugins/admin.py create mode 100644 nxy/plugins/bitcoin.py create mode 100644 nxy/plugins/ctcp.py create mode 100644 nxy/plugins/mcmaniac.py create mode 100644 nxy/plugins/quotes.py create mode 100644 nxy/plugins/reminder.py create mode 100644 nxy/plugins/useless.py create mode 100644 nxy/utils.py create mode 100644 requirements.txt diff --git a/.env-example b/.env-example new file mode 100644 index 0000000..7d0c1ef --- /dev/null +++ b/.env-example @@ -0,0 +1 @@ +PASSWORD=password diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f8ab1fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +data/ +.env +.idea/ +__pycache__/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/config.json b/config.json new file mode 100644 index 0000000..55c1e0e --- /dev/null +++ b/config.json @@ -0,0 +1,34 @@ +{ + "username": "nxy", + "host": "unterschicht.tv", + "port": 56791, + "ssl": true, + "raw": true, + "autojoins": ["#nxy-dev"], + "storage": "json://data/db.json", + "flood_burst": 1, + "flood_rate": 4, + "flood_rate_delay": 1, + "includes": [ + "irc3.plugins.async", + "irc3.plugins.cron", + "irc3.plugins.command", + "irc3.plugins.storage", + "irc3.plugins.uptime", + "nxy.plugins.admin", + "nxy.plugins.bitcoin", + "nxy.plugins.ctcp", + "nxy.plugins.mcmaniac", + "nxy.plugins.quotes", + "nxy.plugins.reminder", + "nxy.plugins.useless" + ], + "irc3.plugins.command": { + "cmd": ".", + "guard": "irc3.plugins.command.mask_based_policy" + }, + "irc3.plugins.command.masks": { + "*!ceo@cocaine-import.agency": "all_permissions", + "*": "view" + } +} diff --git a/nxy/bot.py b/nxy/bot.py new file mode 100644 index 0000000..1757428 --- /dev/null +++ b/nxy/bot.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +import sqlite3 +import json +import sys +import os + +# noinspection PyPackageRequirements +from dotenv import load_dotenv +from irc3 import IrcBot + +CFG_DEV = { + 'nick': 'nxy', + 'autojoins': ['#dev'], + 'host': 'localhost', + 'port': 6667, + 'ssl': False, + 'raw': True, + 'debug': True, + 'verbose': True, + "irc3.plugins.command.masks": { + "*!admin@127.0.0.1": "all_permissions", + "*": "view" + } +} + + +# TODO: imdb, youtube, intensifies, pay, owe, rape (owe) +def main(cfg_file): + load_dotenv('.env') + with open(cfg_file, 'r') as fp: + cfg = json.load(fp) + if bool(os.environ.get('DEV')): + cfg.update(CFG_DEV) + elif 'PASSWORD' in os.environ: + cfg['password'] = os.environ['PASSWORD'] + data = os.path.dirname(cfg['storage'].split('://', 1)[1]) + if not os.path.exists(data): + os.makedirs(data) + bot = IrcBot.from_config(cfg) + if bool(os.environ.get('DEV')): + bot.con = sqlite3.connect('nxy.db') + bot.run() + + +if __name__ == '__main__': + main(sys.argv[1]) diff --git a/nxy/plugins/__init__.py b/nxy/plugins/__init__.py new file mode 100644 index 0000000..6ee8b17 --- /dev/null +++ b/nxy/plugins/__init__.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +from irc3 import IrcBot + +MODULE = __name__ + + +class BasePlugin(object): + def __init__(self, bot: IrcBot): + self.bot = bot + self.log = bot.log + + +class Plugin(BasePlugin): + @classmethod + def reload(cls, old: BasePlugin): + return cls(old.bot) + + +class DatabasePlugin(Plugin): + def __init__(self, bot: IrcBot): + super().__init__(bot) + # noinspection PyUnresolvedReferences + self._db = bot.db + self.prepare_db() + + @property + def db(self): + return self._db[self] + + def prepare_db(self): + if self not in self._db: + self._db[self] = {} diff --git a/nxy/plugins/admin.py b/nxy/plugins/admin.py new file mode 100644 index 0000000..09487db --- /dev/null +++ b/nxy/plugins/admin.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from docopt import Dict as DocoptDict +from irc3 import IrcBot +from irc3.utils import IrcString +from irc3.plugins.command import command + +from . import MODULE, Plugin + + +# noinspection PyUnusedLocal +@command(permission='admin', show_in_help_list=False) +def reload(bot: IrcBot, mask: IrcString, channel: IrcString, args: DocoptDict): + """Reload a plugin + %%reload + """ + plugin = args[''] + bot.reload('{module}.{plugin}'.format(plugin=plugin, module=MODULE)) + bot.notice(mask.nick, 'Reloaded plugin "{plugin}"'.format(plugin=plugin)) + + +class Admin(Plugin): + pass diff --git a/nxy/plugins/bitcoin.py b/nxy/plugins/bitcoin.py new file mode 100644 index 0000000..6bbe2e8 --- /dev/null +++ b/nxy/plugins/bitcoin.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +from docopt import Dict as DocOptDict +from irc3.plugins.command import command +from irc3.utils import IrcString + +from . import Plugin +from ..utils import fmt, req + + +# noinspection PyUnusedLocal +class Bitcoin(Plugin): + requires = [ + 'irc3.plugins.command', + ] + + @command + async def btc(self, mask: IrcString, channel: IrcString, + args: DocOptDict) -> str: + """Bitcoin command. + %%btc + """ + data = (await req('get', 'https://www.bitstamp.net/api/ticker')).json() + return fmt('{bold}[BitStamp, 24h]{reset} ' + 'Current: {bold}{color}{orange}${last:,.2f}{reset} - ' + 'High: {bold}{color}{green}${high:,.2f}{reset} - ' + 'Low: {bold}{color}{maroon}${low:,.2f}{reset} - ' + 'Volume: {bold}฿{volume:,.2f}', + last=float(data['last']), + high=float(data['high']), + low=float(data['low']), + volume=float(data['volume'])) diff --git a/nxy/plugins/ctcp.py b/nxy/plugins/ctcp.py new file mode 100644 index 0000000..23496fe --- /dev/null +++ b/nxy/plugins/ctcp.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +from docopt import Dict as DocOptDict +from irc3.plugins.command import command +from irc3.utils import IrcString +import time + +from . import Plugin +from ..utils import fmt + + +# noinspection PyUnusedLocal +class CTCP(Plugin): + TIMEOUT = 5 + + requires = [ + 'irc3.plugins.async', + 'irc3.plugins.command', + ] + + async def ctcp(self, ctcp: str, mask: IrcString, args: DocOptDict) -> str: + nick = args.get('') or mask.nick + name = ctcp.upper() + ctcp = await self.bot.ctcp_async(nick, name, timeout=self.TIMEOUT) + if not ctcp or ctcp['timeout']: + reply = 'timeout' + elif not ctcp['success']: + reply = 'Error: {reply}'.format(reply=ctcp['reply']) + else: + reply = ctcp['reply'] + return fmt('{bold}[{ctcp}]{reset} {nick}: {reply}', + ctcp=name, + nick=nick, + reply=reply) + + @command + async def ping(self, mask: IrcString, channel: IrcString, + args: DocOptDict) -> str: + """CTCP ping command. + %%ping [] + """ + nick = args.get('') or mask.nick + ctcp = await self.bot.ctcp_async(nick, 'PING {0}'.format(time.time()), + timeout=self.TIMEOUT) + if not ctcp or ctcp['timeout']: + reply = 'timeout' + elif not ctcp['success']: + reply = 'Error: {reply}'.format(reply=ctcp['reply']) + else: + delta = time.time() - float(ctcp['reply']) + reply = '{delta:.9f} {unit}'.format( + unit='ms' if delta < 0 else 's', + delta=delta) + return fmt('{bold}[PING]{reset} {nick}: {text}', + nick=nick, + text=reply) + + @command + async def finger(self, mask: IrcString, channel: IrcString, + args: DocOptDict) -> str: + """CTCP finger command. + %%finger [] + """ + return await self.ctcp('FINGER', mask, args) + + @command + async def time(self, mask: IrcString, channel: IrcString, + args: DocOptDict) -> str: + """CTCP time command. + %%time [] + """ + return await self.ctcp('TIME', mask, args) + + @command + async def ver(self, mask: IrcString, channel: IrcString, + args: DocOptDict) -> str: + """CTCP version command. + %%ver [] + """ + return await self.ctcp('VERSION', mask, args) diff --git a/nxy/plugins/mcmaniac.py b/nxy/plugins/mcmaniac.py new file mode 100644 index 0000000..e66c924 --- /dev/null +++ b/nxy/plugins/mcmaniac.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +from docopt import Dict as DocOptDict +from irc3.utils import IrcString +from irc3.plugins.command import command +import random +import irc3 + +from . import DatabasePlugin +from ..utils import parse_int + + +@irc3.plugin +class McManiac(DatabasePlugin): + requires = [ + 'irc3.plugins.command', + ] + + @property + def items(self): + return self.db['items'] + + # noinspection PyUnusedLocal + @command(options_first=True) + def mcmaniac(self, mask: IrcString, channel: IrcString, args: DocOptDict): + """Manage McManiacs + %%mcmaniac + %%mcmaniac [] + """ + cmd = args.get('') + mcmaniac = args.get('') + index = args.get('') + if cmd and mcmaniac: + if cmd == 'add': + if mcmaniac not in self.items: + self.items.append(mcmaniac) + if cmd == 'del': + try: + self.items.pop(parse_int(mcmaniac)) + except (IndexError, TypeError): + return + self._db.SIGINT() + elif self.items: + if index: + try: + mcmaniac = self.items[parse_int(index)] + except (IndexError, TypeError): + return + else: + mcmaniac = random.choice(self.items) + return '[{index}/{total}] {item}'.format( + index=self.items.index(mcmaniac) + 1, + total=len(self.items), + item=mcmaniac + ) + + def prepare_db(self): + super().prepare_db() + self._db.setdefault(self, items=[]) + self._db.SIGINT() diff --git a/nxy/plugins/quotes.py b/nxy/plugins/quotes.py new file mode 100644 index 0000000..ee72f6f --- /dev/null +++ b/nxy/plugins/quotes.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +from docopt import Dict as DocOptDict +from irc3.utils import IrcString +from irc3.plugins.command import command +import random +import irc3 +import re + +from . import DatabasePlugin +from ..utils import parse_int + + +@irc3.plugin +class Quotes(DatabasePlugin): + requires = [ + 'irc3.plugins.command', + ] + + REGEX = re.compile(r'?') + RESPONSE = '[{index}/{total}] <{nick}> {quote}' + + # noinspection PyUnusedLocal + @command(options_first=True) + def q(self, mask: IrcString, channel: IrcString, args: DocOptDict): + """ + Manage quotes. + %%q ... + %%q [...] + """ + cmd = args.get('') + nick = args[''] + quote = args.get('') + if cmd: + if cmd == 'add': + nick = self.REGEX.match(nick).group(1) + if nick not in self.db: + self.db[nick] = [] + quote = ' '.join(quote) + if quote not in self.db[nick]: + self.db[nick].append(quote) + elif cmd == 'del': + try: + self.db[nick].pop(parse_int(quote)) + if not self.db[nick]: + del self.db[nick] + except (KeyError, IndexError, TypeError): + return + self._db.SIGINT() + else: + if quote: + try: + quote = self.db[nick][parse_int(quote)] + except (KeyError, IndexError, TypeError): + return + else: + quote = random.choice(self.db[nick]) + self.bot.privmsg(channel, self.RESPONSE.format( + index=self.db[nick].index(quote) + 1, + total=len(self.db[nick]), + nick=nick, + quote=quote, + )) diff --git a/nxy/plugins/reminder.py b/nxy/plugins/reminder.py new file mode 100644 index 0000000..ec83748 --- /dev/null +++ b/nxy/plugins/reminder.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +from docopt import Dict as DocOptDict +from irc3.plugins.command import command +from irc3.utils import IrcString +import asyncio +import irc3 + +from . import Plugin +from ..utils import fmt, time_to_sec + + +@irc3.plugin +class Reminder(Plugin): + requires = [ + 'irc3.plugins.command', + ] + + @command + def remind(self, mask: IrcString, channel: IrcString, args: DocOptDict): + """Remind command. + %%remind ... + """ + delay = time_to_sec(args['']) + if delay: + asyncio.ensure_future(self.timer(delay, mask, channel, args)) + else: + self.bot.notice(mask.nick, 'Invalid delay "{delay}" for "remind"' + .format(delay=args[''])) + + async def timer(self, delay, mask: IrcString, channel: IrcString, + args: DocOptDict): + """ + Actually the reminder function. Sleeps `delay` seconds and then sends + message to `channel` after that. + """ + await asyncio.sleep(delay) + text = fmt('{bold}[Reminder]{reset} {nick}: {message} ({delay})', + message=' '.join(args['']), + delay=args[''], + nick=mask.nick) + self.bot.privmsg(channel, text) diff --git a/nxy/plugins/useless.py b/nxy/plugins/useless.py new file mode 100644 index 0000000..79fa430 --- /dev/null +++ b/nxy/plugins/useless.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +from docopt import Dict as DocOptDict +from irc3.plugins.command import command +from irc3.utils import IrcString +import random + +from . import Plugin +from ..utils import fmt + +RAINBOW = ('maroon', 'red', 'orange', 'yellow', 'ltgreen', 'green', 'teal', + 'ltblue', 'blue', 'purple', 'pink') +RAINBOW_LEN = len(RAINBOW) + + +# noinspection PyUnusedLocal +class Useless(Plugin): + requires = [ + 'irc3.plugins.command', + ] + + @command + def jn(self, mask: IrcString, channel: IrcString, args: DocOptDict) -> str: + """Yes or no command. + %%jn + """ + choice = random.choice(['{green}Ja', '{maroon}Nein']) + return fmt('{nick}: {bold}{color}%s' % choice, nick=mask.nick) + + @command + def kiss(self, mask: IrcString, channel: IrcString, args: DocOptDict): + """Kiss command. + %%kiss + """ + return fmt('(づ。◕‿‿◕。)づ{color}{red}。。・゜゜・。。・゜❤{reset} {nick} ' + '{color}{red}❤', + nick=args['']) + + @command + def hug(self, mask: IrcString, channel: IrcString, + args: DocOptDict) -> str: + """Hug command. + %%hug + """ + return fmt('{color}{red}♥♡❤♡♥{reset} {nick} {color}{red}♥♡❤♡♥', + nick=args['']) + + @command + def gay(self, mask: IrcString, channel: IrcString, + args: DocOptDict) -> str: + """Gay command (alias to rainbow). + %%gay ... + """ + return self.rainbow(mask, channel, args) + + @command + def rainbow(self, mask: IrcString, channel: IrcString, + args: DocOptDict) -> str: + """Rainbow command. + %%rainbow ... + """ + last = 0 + word = [] + for i, char in enumerate(' '.join(args.get(''))): + if char != ' ': + char = '{color}{%s}%s' % (RAINBOW[last % RAINBOW_LEN], char) + last += 1 + word.append(char) + return fmt(''.join(word)) + + @command + def wrainbow(self, mask: IrcString, channel: IrcString, + args: DocOptDict) -> str: + """Word Rainbow command. + %%wrainbow ... + """ + return fmt(' '.join(['{color}{%s}%s' % (RAINBOW[i % RAINBOW_LEN], word) + for i, word in enumerate(args[''])])) diff --git a/nxy/utils.py b/nxy/utils.py new file mode 100644 index 0000000..4b1e5a3 --- /dev/null +++ b/nxy/utils.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +from requests import request, Response +from pprint import pprint +import re + +# @formatter:off +COLORS = dict( + white='00', # white + black='01', # black + blue='02', # blue (navy) + green='03', # green + red='04', # red + maroon='05', # brown (maroon) + purple='06', # purple + orange='07', # orange (olive) + yellow='08', # yellow + ltgreen='09', # light green (lime) + teal='10', # teal (a green/blue cyan) + ltcyan='11', # light cyan (cyan / aqua) + ltblue='12', # light blue (royal) + pink='13', # pink (light purple / fuchsia) + grey='14', # grey + ltgrey='15', # light grey (silver) +) + +FORMATTING = dict( + bold='\x02', # bold + color='\x03', # colored text + italic='\x1D', # italic text + underline='\x1F', # underlined text + swap='\x16', # swap bg and fg colors ("reverse video") + reset='\x0F', # reset all formatting + # COLORS + # white='\x0300', # white + # black='\x0301', # black + # blue='\x0302', # blue (navy) + # green='\x0303', # green + # red='\x0304', # red + # maroon='\x0305', # brown (maroon) + # purple='\x0306', # purple + # orange='\x0307', # orange (olive) + # yellow='\x0308', # yellow + # ltgreen='\x0309', # light green (lime) + # teal='\x0310', # teal (a green/blue cyan) + # ltcyan='\x0311', # light cyan (cyan / aqua) + # ltblue='\x0312', # light blue (royal) + # pink='\x0313', # pink (light purple / fuchsia) + # grey='\x0314', # grey + # ltgrey='\x0315', # light grey (silver) + **COLORS +) +# @formatter:on + +# Debug helper +pp = pprint + + +def fmt(__text: str, **kwargs) -> str: + """Formats a str with `kwargs` and `FORMATTING`.""" + return __text.format(**kwargs, **FORMATTING) + + +async def req(method: str, url: str, **kwargs) -> Response: + return request(method, url, **kwargs) + + +def time_to_sec(text: str) -> int: + match = re.match(r'(\d+)([smhdwy])', text) + if match: + num, unit = match.groups() + num = int(num) + if unit == 's': + return num + elif unit == 'm': + return num * 60 + elif unit == 'h': + return num * 3600 + elif unit == 'd': + return num * 86400 + elif unit == 'w': + return num * 604800 + elif unit == 'y': + return num * 52 * 604800 + + +def parse_int(val: list) -> int: + """ + Parses an int from list, decremts by -1 if positiv. + Only returns if value from list is not 0. + """ + try: + val = int(''.join(val)) + # Ignore val == 0 + if val is not 0: + # Decrement to match list index + if val > 0: + val -= 1 + return val + except ValueError: + pass diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3d7ae9a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +irc3==1.0.0 +aiocron==0.6 +requests==2.14.2 +python_dotenv==0.6.4