Initial commit

This commit is contained in:
mrhanky 2017-05-16 00:26:10 +02:00
commit 5a0b829369
No known key found for this signature in database
GPG Key ID: 67D772C481CB41B8
15 changed files with 592 additions and 0 deletions

1
.env-example Normal file
View File

@ -0,0 +1 @@
PASSWORD=password

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
data/
.env
.idea/
__pycache__/

0
README.md Normal file
View File

34
config.json Normal file
View File

@ -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"
}
}

46
nxy/bot.py Normal file
View File

@ -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])

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

@ -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] = {}

22
nxy/plugins/admin.py Normal file
View File

@ -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>
"""
plugin = args['<plugin>']
bot.reload('{module}.{plugin}'.format(plugin=plugin, module=MODULE))
bot.notice(mask.nick, 'Reloaded plugin "{plugin}"'.format(plugin=plugin))
class Admin(Plugin):
pass

31
nxy/plugins/bitcoin.py Normal file
View File

@ -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']))

79
nxy/plugins/ctcp.py Normal file
View File

@ -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('<nick>') 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>]
"""
nick = args.get('<nick>') 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 [<nick>]
"""
return await self.ctcp('FINGER', mask, args)
@command
async def time(self, mask: IrcString, channel: IrcString,
args: DocOptDict) -> str:
"""CTCP time command.
%%time [<nick>]
"""
return await self.ctcp('TIME', mask, args)
@command
async def ver(self, mask: IrcString, channel: IrcString,
args: DocOptDict) -> str:
"""CTCP version command.
%%ver [<nick>]
"""
return await self.ctcp('VERSION', mask, args)

59
nxy/plugins/mcmaniac.py Normal file
View File

@ -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 <cmd> <mcmaniac>
%%mcmaniac [<index>]
"""
cmd = args.get('<cmd>')
mcmaniac = args.get('<mcmaniac>')
index = args.get('<index>')
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()

62
nxy/plugins/quotes.py Normal file
View File

@ -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'<?[~&@%+]?([a-zA-Z0-9_\-^`|\\\[\]{}]+)>?')
RESPONSE = '[{index}/{total}] <{nick}> {quote}'
# noinspection PyUnusedLocal
@command(options_first=True)
def q(self, mask: IrcString, channel: IrcString, args: DocOptDict):
"""
Manage quotes.
%%q <cmd> <nick> <quote>...
%%q <nick> [<quote>...]
"""
cmd = args.get('<cmd>')
nick = args['<nick>']
quote = args.get('<quote>')
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,
))

41
nxy/plugins/reminder.py Normal file
View File

@ -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> <message>...
"""
delay = time_to_sec(args['<delay>'])
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['<delay>']))
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['<message>']),
delay=args['<delay>'],
nick=mask.nick)
self.bot.privmsg(channel, text)

77
nxy/plugins/useless.py Normal file
View File

@ -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 <question>
"""
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 <nick>
"""
return fmt('(づ。◕‿‿◕。)づ{color}{red}。。・゜゜・。。・゜❤{reset} {nick} '
'{color}{red}',
nick=args['<nick>'])
@command
def hug(self, mask: IrcString, channel: IrcString,
args: DocOptDict) -> str:
"""Hug command.
%%hug <nick>
"""
return fmt('{color}{red}♥♡❤♡♥{reset} {nick} {color}{red}♥♡❤♡♥',
nick=args['<nick>'])
@command
def gay(self, mask: IrcString, channel: IrcString,
args: DocOptDict) -> str:
"""Gay command (alias to rainbow).
%%gay <word>...
"""
return self.rainbow(mask, channel, args)
@command
def rainbow(self, mask: IrcString, channel: IrcString,
args: DocOptDict) -> str:
"""Rainbow command.
%%rainbow <word>...
"""
last = 0
word = []
for i, char in enumerate(' '.join(args.get('<word>'))):
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 <words>...
"""
return fmt(' '.join(['{color}{%s}%s' % (RAINBOW[i % RAINBOW_LEN], word)
for i, word in enumerate(args['<words>'])]))

100
nxy/utils.py Normal file
View File

@ -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

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
irc3==1.0.0
aiocron==0.6
requests==2.14.2
python_dotenv==0.6.4