Initial commit
This commit is contained in:
commit
5a0b829369
1
.env-example
Normal file
1
.env-example
Normal file
@ -0,0 +1 @@
|
||||
PASSWORD=password
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
data/
|
||||
.env
|
||||
.idea/
|
||||
__pycache__/
|
34
config.json
Normal file
34
config.json
Normal 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
46
nxy/bot.py
Normal 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
32
nxy/plugins/__init__.py
Normal 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
22
nxy/plugins/admin.py
Normal 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
31
nxy/plugins/bitcoin.py
Normal 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
79
nxy/plugins/ctcp.py
Normal 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
59
nxy/plugins/mcmaniac.py
Normal 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
62
nxy/plugins/quotes.py
Normal 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
41
nxy/plugins/reminder.py
Normal 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
77
nxy/plugins/useless.py
Normal 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
100
nxy/utils.py
Normal 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
4
requirements.txt
Normal file
@ -0,0 +1,4 @@
|
||||
irc3==1.0.0
|
||||
aiocron==0.6
|
||||
requests==2.14.2
|
||||
python_dotenv==0.6.4
|
Loading…
Reference in New Issue
Block a user