reset the hitomi project, remove django, rebuild deps
This commit is contained in:
parent
8be039da80
commit
f9baa76a24
|
@ -1,7 +1,3 @@
|
||||||
*.log
|
*.log
|
||||||
*.pyc
|
*.pyc
|
||||||
.idea/
|
.idea/
|
||||||
db.sqlite3
|
|
||||||
localsettings.py
|
|
||||||
parser.out
|
|
||||||
parsetab.py
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
### Overview
|
### Overview
|
||||||
|
|
||||||
dr.botzo is a Discord bot written in Python, using Django as a backend and the
|
hitomi is a Discord bot written in Python, using dr.botzo as a backend and the
|
||||||
discord.py library to handle most of the protocol stuff. It is mostly a fun
|
discord.py library to handle most of the protocol stuff. It is mostly a fun
|
||||||
project for a couple of us to hack around on, but you may find it useful.
|
project for a couple of us to hack around on, but you may find it useful.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
"""Start the Discord bot and connect to Discord."""
|
||||||
|
import importlib
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import hitomi.config as config
|
||||||
|
from hitomi import bot
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def run_bot():
|
||||||
|
"""Initialize plugins and begin the bot."""
|
||||||
|
for plugin in config.BOT_PLUGINS:
|
||||||
|
importlib.import_module(plugin)
|
||||||
|
|
||||||
|
bot.run(config.BOT_TOKEN)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run_bot()
|
|
@ -1,18 +0,0 @@
|
||||||
"""Discord bot Hitomi and its library functions."""
|
|
||||||
from discord.ext import commands
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
BOT_DESCRIPTION = "A simple Discord bot."
|
|
||||||
|
|
||||||
|
|
||||||
class Hitomi(commands.Bot):
|
|
||||||
"""Extend the discord.py Bot, to add more cool stuff."""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
"""Initialize bot, and cool stuff."""
|
|
||||||
super(Hitomi, self).__init__(*args, **kwargs)
|
|
||||||
self.on_message_handlers = []
|
|
||||||
|
|
||||||
|
|
||||||
hitomi = Hitomi(command_prefix=settings.DISCORD_BOT_COMMAND_PREFIX, description=BOT_DESCRIPTION,
|
|
||||||
max_messages=settings.DISCORD_BOT_MAX_MESSAGES)
|
|
22
bot/api.py
22
bot/api.py
|
@ -1,22 +0,0 @@
|
||||||
"""Create a simple API to the bot."""
|
|
||||||
from aiohttp import web
|
|
||||||
|
|
||||||
|
|
||||||
async def index(request):
|
|
||||||
"""Provide a basic index to the API."""
|
|
||||||
return web.Response(text="hitomi")
|
|
||||||
|
|
||||||
|
|
||||||
async def greet(request):
|
|
||||||
"""Provide a simple greeting."""
|
|
||||||
name = request.match_info.get('name', 'Anonymous')
|
|
||||||
text = "Hello {0:s}".format(name)
|
|
||||||
return web.Response(text=text)
|
|
||||||
|
|
||||||
# initialize the API
|
|
||||||
hitomi_api = web.Application()
|
|
||||||
|
|
||||||
# add our basic stuff above
|
|
||||||
hitomi_api.router.add_get('/', index)
|
|
||||||
hitomi_api.router.add_get('/greet', greet)
|
|
||||||
hitomi_api.router.add_get('/greet/{name}', greet)
|
|
|
@ -1,54 +0,0 @@
|
||||||
"""Start the Discord bot and connect to Discord."""
|
|
||||||
import asyncio
|
|
||||||
import importlib
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
|
|
||||||
from bot import hitomi
|
|
||||||
from bot.api import hitomi_api
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@hitomi.event
|
|
||||||
async def on_ready():
|
|
||||||
"""Print some basic login info to the console, when the bot connects."""
|
|
||||||
logger.info("Logged in as {0:s} ({1:s})".format(hitomi.user.name, hitomi.user.id))
|
|
||||||
|
|
||||||
# now load bot plugins
|
|
||||||
for plugin in settings.BOT_PLUGINS:
|
|
||||||
importlib.import_module(plugin)
|
|
||||||
|
|
||||||
|
|
||||||
@hitomi.event
|
|
||||||
async def on_message(message):
|
|
||||||
"""Call all registered on_message handlers."""
|
|
||||||
for handler in hitomi.on_message_handlers:
|
|
||||||
handler(message)
|
|
||||||
|
|
||||||
# let other stuff happen
|
|
||||||
await hitomi.process_commands(message)
|
|
||||||
|
|
||||||
|
|
||||||
async def run_bot():
|
|
||||||
"""Initialize and begin the bot in an asyncio context."""
|
|
||||||
await hitomi.login(settings.DISCORD_BOT_TOKEN)
|
|
||||||
await hitomi.connect()
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = "Start the Discord bot and connect to Discord."
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
handler = hitomi_api.make_handler()
|
|
||||||
api_server = loop.create_server(handler, '0.0.0.0', settings.API_PORT)
|
|
||||||
futures = asyncio.gather(run_bot(), api_server)
|
|
||||||
try:
|
|
||||||
loop.run_until_complete(futures)
|
|
||||||
except:
|
|
||||||
loop.run_until_complete(hitomi.logout())
|
|
||||||
finally:
|
|
||||||
loop.close()
|
|
20
dice/bot.py
20
dice/bot.py
|
@ -1,20 +0,0 @@
|
||||||
"""Roll dice and do other randomization type operations."""
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from bot import hitomi
|
|
||||||
from dice import lib
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
logger.info("loading dice plugin")
|
|
||||||
|
|
||||||
|
|
||||||
@hitomi.command()
|
|
||||||
async def choose(*choices: str):
|
|
||||||
"""Randomly select one item from multiple choices."""
|
|
||||||
await hitomi.say(lib.choose(*choices))
|
|
||||||
|
|
||||||
|
|
||||||
@hitomi.command()
|
|
||||||
async def roll(*, roll_str: str):
|
|
||||||
"""Provided a dice string, roll the dice and return the result."""
|
|
||||||
await hitomi.say(lib.roll(roll_str))
|
|
278
dice/lib.py
278
dice/lib.py
|
@ -1,278 +0,0 @@
|
||||||
"""Functions for use in the dice module."""
|
|
||||||
import logging
|
|
||||||
import random
|
|
||||||
|
|
||||||
import ply.lex as lex
|
|
||||||
import ply.yacc as yacc
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class DiceRoller(object):
|
|
||||||
|
|
||||||
tokens = ['NUMBER', 'TEXT', 'ROLLSEP']
|
|
||||||
literals = ['#', '/', '+', '-', 'd']
|
|
||||||
|
|
||||||
t_TEXT = r'\s+[^;]+'
|
|
||||||
t_ROLLSEP = r';\s*'
|
|
||||||
|
|
||||||
def build(self):
|
|
||||||
lex.lex(module=self)
|
|
||||||
yacc.yacc(module=self)
|
|
||||||
|
|
||||||
def t_NUMBER(self, t):
|
|
||||||
r'\d+'
|
|
||||||
t.value = int(t.value)
|
|
||||||
return t
|
|
||||||
|
|
||||||
def t_error(self, t):
|
|
||||||
t.lexer.skip(1)
|
|
||||||
|
|
||||||
precedence = (
|
|
||||||
('left', 'ROLLSEP'),
|
|
||||||
('left', '+', '-'),
|
|
||||||
('right', 'd'),
|
|
||||||
('left', '#'),
|
|
||||||
('left', '/')
|
|
||||||
)
|
|
||||||
|
|
||||||
output = ""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Build the parsing tables."""
|
|
||||||
self.build()
|
|
||||||
|
|
||||||
def roll_dice(self, keep, dice, size):
|
|
||||||
"""Takes the parsed dice string for a single roll (eg 3/4d20) and performs
|
|
||||||
the actual roll. Returns a string representing the result.
|
|
||||||
"""
|
|
||||||
|
|
||||||
a = list(range(dice))
|
|
||||||
for i in range(dice):
|
|
||||||
a[i] = random.randint(1, size)
|
|
||||||
if keep != dice:
|
|
||||||
b = sorted(a, reverse=True)
|
|
||||||
b = b[0:keep]
|
|
||||||
else:
|
|
||||||
b = a
|
|
||||||
total = sum(b)
|
|
||||||
outstr = "[" + ",".join(str(i) for i in a) + "]"
|
|
||||||
|
|
||||||
return (total, outstr)
|
|
||||||
|
|
||||||
def process_roll(self, trials, mods, comment):
|
|
||||||
"""Processes rolls coming from the parser.
|
|
||||||
|
|
||||||
This generates the inputs for the roll_dice() command, and returns
|
|
||||||
the full string representing the whole current dice string (the part
|
|
||||||
up to a semicolon or end of line).
|
|
||||||
"""
|
|
||||||
|
|
||||||
output = ""
|
|
||||||
repeat = 1
|
|
||||||
if trials != None:
|
|
||||||
repeat = trials
|
|
||||||
for i in range(repeat):
|
|
||||||
mode = 1
|
|
||||||
total = 0
|
|
||||||
curr_str = ""
|
|
||||||
if i > 0:
|
|
||||||
output += ", "
|
|
||||||
for m in mods:
|
|
||||||
keep = 0
|
|
||||||
dice = 1
|
|
||||||
res = 0
|
|
||||||
# if m is a tuple, then it is a die roll
|
|
||||||
# m[0] = (keep, num dice)
|
|
||||||
# m[1] = num faces on the die
|
|
||||||
if type(m) == tuple:
|
|
||||||
if m[0] != None:
|
|
||||||
if m[0][0] != None:
|
|
||||||
keep = m[0][0]
|
|
||||||
dice = m[0][1]
|
|
||||||
size = m[1]
|
|
||||||
if keep > dice or keep == 0:
|
|
||||||
keep = dice
|
|
||||||
if size < 1:
|
|
||||||
output = "# of sides for die is incorrect: %d" % size
|
|
||||||
return output
|
|
||||||
if dice < 1:
|
|
||||||
output = "# of dice is incorrect: %d" % dice
|
|
||||||
return output
|
|
||||||
res = self.roll_dice(keep, dice, size)
|
|
||||||
curr_str += "%d%s" % (res[0], res[1])
|
|
||||||
res = res[0]
|
|
||||||
elif m == "+":
|
|
||||||
mode = 1
|
|
||||||
curr_str += "+"
|
|
||||||
elif m == "-":
|
|
||||||
mode = -1
|
|
||||||
curr_str += "-"
|
|
||||||
else:
|
|
||||||
res = m
|
|
||||||
curr_str += str(m)
|
|
||||||
total += mode * res
|
|
||||||
if repeat == 1:
|
|
||||||
if comment != None:
|
|
||||||
output = "%d %s (%s)" % (total, comment.strip(), curr_str)
|
|
||||||
else:
|
|
||||||
output = "%d (%s)" % (total, curr_str)
|
|
||||||
else:
|
|
||||||
output += "%d (%s)" % (total, curr_str)
|
|
||||||
if i == repeat - 1:
|
|
||||||
if comment != None:
|
|
||||||
output += " (%s)" % (comment.strip())
|
|
||||||
return output
|
|
||||||
|
|
||||||
def p_roll_r(self, p):
|
|
||||||
# Chain rolls together.
|
|
||||||
|
|
||||||
# General idea I had when creating this grammar: A roll string is a chain
|
|
||||||
# of modifiers, which may be repeated for a certain number of trials. It can
|
|
||||||
# have a comment that describes the roll
|
|
||||||
# Multiple roll strings can be chained with semicolon
|
|
||||||
|
|
||||||
'roll : roll ROLLSEP roll'
|
|
||||||
global output
|
|
||||||
p[0] = p[1] + "; " + p[3]
|
|
||||||
output = p[0]
|
|
||||||
|
|
||||||
def p_roll(self, p):
|
|
||||||
# Parse a basic roll string.
|
|
||||||
|
|
||||||
'roll : trial modifier comment'
|
|
||||||
global output
|
|
||||||
mods = []
|
|
||||||
if type(p[2]) == list:
|
|
||||||
mods = p[2]
|
|
||||||
else:
|
|
||||||
mods = [p[2]]
|
|
||||||
p[0] = self.process_roll(p[1], mods, p[3])
|
|
||||||
output = p[0]
|
|
||||||
|
|
||||||
def p_roll_no_trials(self, p):
|
|
||||||
# Parse a roll string without trials.
|
|
||||||
|
|
||||||
'roll : modifier comment'
|
|
||||||
global output
|
|
||||||
mods = []
|
|
||||||
if type(p[1]) == list:
|
|
||||||
mods = p[1]
|
|
||||||
else:
|
|
||||||
mods = [p[1]]
|
|
||||||
p[0] = self.process_roll(None, mods, p[2])
|
|
||||||
output = p[0]
|
|
||||||
|
|
||||||
def p_comment(self, p):
|
|
||||||
# Parse a comment.
|
|
||||||
|
|
||||||
'''comment : TEXT
|
|
||||||
|'''
|
|
||||||
if len(p) == 2:
|
|
||||||
p[0] = p[1]
|
|
||||||
else:
|
|
||||||
p[0] = None
|
|
||||||
|
|
||||||
def p_modifier(self, p):
|
|
||||||
# Parse a modifier on a roll string.
|
|
||||||
|
|
||||||
'''modifier : modifier "+" modifier
|
|
||||||
| modifier "-" modifier'''
|
|
||||||
# Use append to prevent nested lists (makes dealing with this easier)
|
|
||||||
if type(p[1]) == list:
|
|
||||||
p[1].append(p[2])
|
|
||||||
p[1].append(p[3])
|
|
||||||
p[0] = p[1]
|
|
||||||
elif type(p[3]) == list:
|
|
||||||
p[3].insert(0, p[2])
|
|
||||||
p[3].insert(0, p[1])
|
|
||||||
p[0] = p[3]
|
|
||||||
else:
|
|
||||||
p[0] = [p[1], p[2], p[3]]
|
|
||||||
|
|
||||||
def p_die(self, p):
|
|
||||||
# Return the left side before the "d", and the number of faces.
|
|
||||||
|
|
||||||
'modifier : left NUMBER'
|
|
||||||
p[0] = (p[1], p[2])
|
|
||||||
|
|
||||||
def p_die_num(self, p):
|
|
||||||
'modifier : NUMBER'
|
|
||||||
p[0] = p[1]
|
|
||||||
|
|
||||||
def p_left(self, p):
|
|
||||||
# Parse the number of dice we are rolling, and how many we are keeping.
|
|
||||||
'left : keep dice'
|
|
||||||
if p[1] == None:
|
|
||||||
p[0] = [None, p[2]]
|
|
||||||
else:
|
|
||||||
p[0] = [p[1], p[2]]
|
|
||||||
|
|
||||||
def p_left_all(self, p):
|
|
||||||
'left : dice'
|
|
||||||
p[0] = [None, p[1]]
|
|
||||||
|
|
||||||
def p_left_e(self, p):
|
|
||||||
'left :'
|
|
||||||
p[0] = None
|
|
||||||
|
|
||||||
def p_total(self, p):
|
|
||||||
'trial : NUMBER "#"'
|
|
||||||
if len(p) > 1:
|
|
||||||
p[0] = p[1]
|
|
||||||
else:
|
|
||||||
p[0] = None
|
|
||||||
|
|
||||||
def p_keep(self, p):
|
|
||||||
'keep : NUMBER "/"'
|
|
||||||
if p[1] != None:
|
|
||||||
p[0] = p[1]
|
|
||||||
else:
|
|
||||||
p[0] = None
|
|
||||||
|
|
||||||
def p_dice(self, p):
|
|
||||||
'dice : NUMBER "d"'
|
|
||||||
p[0] = p[1]
|
|
||||||
|
|
||||||
def p_dice_one(self, p):
|
|
||||||
'dice : "d"'
|
|
||||||
p[0] = 1
|
|
||||||
|
|
||||||
def p_error(self, p):
|
|
||||||
# Provide the user with something (albeit not much) when the roll can't be parsed.
|
|
||||||
global output
|
|
||||||
output = "Unable to parse roll"
|
|
||||||
|
|
||||||
def get_result(self):
|
|
||||||
global output
|
|
||||||
return output
|
|
||||||
|
|
||||||
def do_roll(self, dicestr):
|
|
||||||
"""
|
|
||||||
Roll some dice and get the result (with broken out rolls).
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
dicestr - format:
|
|
||||||
N#X/YdS+M label
|
|
||||||
N#: do the following roll N times (optional)
|
|
||||||
X/: take the top X rolls of the Y times rolled (optional)
|
|
||||||
Y : roll the die specified Y times (optional, defaults to 1)
|
|
||||||
dS: roll a S-sided die
|
|
||||||
+M: add M to the result (-M for subtraction) (optional)
|
|
||||||
"""
|
|
||||||
logger.info("rolling {0:s}".format(dicestr))
|
|
||||||
yacc.parse(dicestr)
|
|
||||||
return self.get_result()
|
|
||||||
|
|
||||||
|
|
||||||
roller = DiceRoller()
|
|
||||||
|
|
||||||
|
|
||||||
def choose(*choices: str):
|
|
||||||
"""Randomly choose an item from the provided multiple choices."""
|
|
||||||
return random.choice(choices)
|
|
||||||
|
|
||||||
|
|
||||||
def roll(roll_str: str):
|
|
||||||
"""Parse a roll string with lex and yacc and return the result."""
|
|
||||||
return roller.do_roll(roll_str)
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
"""Create the basic bot for plugins to hook onto."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
import hitomi.config
|
||||||
|
|
||||||
|
BOT_DESCRIPTION = "A simple Discord bot."
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Hitomi(commands.Bot):
|
||||||
|
"""Extend the discord.py Bot, to add more cool stuff."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Initialize bot, and cool stuff."""
|
||||||
|
super(Hitomi, self).__init__(*args, **kwargs)
|
||||||
|
self.on_message_handlers = []
|
||||||
|
|
||||||
|
|
||||||
|
bot = Hitomi(command_prefix=hitomi.config.BOT_COMMAND_PREFIX, description=BOT_DESCRIPTION,
|
||||||
|
max_messages=hitomi.config.BOT_MAX_MESSAGES)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.event
|
||||||
|
async def on_ready():
|
||||||
|
"""Print some basic login info to the console, when the bot connects."""
|
||||||
|
logger.info("Logged in as %s (%d)", bot.user.name, bot.user.id)
|
|
@ -0,0 +1,36 @@
|
||||||
|
"""Configuration dials for the hitomi bot."""
|
||||||
|
import logging.config
|
||||||
|
import os
|
||||||
|
|
||||||
|
BOT_TOKEN = os.environ.get('HITOMI_BOT_TOKEN')
|
||||||
|
|
||||||
|
BOT_COMMAND_PREFIX = os.environ.get('HITOMI_BOT_COMMAND_PREFIX', '!')
|
||||||
|
BOT_MAX_MESSAGES = int(os.environ.get('HITOMI_BOT_MAX_MESSAGES', '5000'))
|
||||||
|
|
||||||
|
BOT_PLUGIN_STR = os.environ.get('HITOMI_BOT_PLUGINS', '')
|
||||||
|
BOT_PLUGINS = BOT_PLUGIN_STR.split(',') if BOT_PLUGIN_STR != '' else []
|
||||||
|
|
||||||
|
logging.config.dictConfig({
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': False,
|
||||||
|
'formatters': {
|
||||||
|
'standard': {
|
||||||
|
'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'handlers': {
|
||||||
|
'default': {
|
||||||
|
'level': 'INFO',
|
||||||
|
'formatter': 'standard',
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
'stream': 'ext://sys.stdout',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'loggers': {
|
||||||
|
'': {
|
||||||
|
'handlers': ['default'],
|
||||||
|
'level': 'INFO',
|
||||||
|
'propagate': True
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
|
@ -1,149 +0,0 @@
|
||||||
"""
|
|
||||||
Django settings for hitomi project.
|
|
||||||
|
|
||||||
Generated by 'django-admin startproject' using Django 2.0.1.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/2.0/topics/settings/
|
|
||||||
|
|
||||||
For the full list of settings and their values, see
|
|
||||||
https://docs.djangoproject.com/en/2.0/ref/settings/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
|
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
|
||||||
# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
|
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
|
||||||
SECRET_KEY = 'ckbvtgrnz3-wm1jnkx)7obtc(l=n&vvb7*yf7gg#8eowr7hfun'
|
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
|
||||||
DEBUG = True
|
|
||||||
|
|
||||||
ALLOWED_HOSTS = []
|
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
|
||||||
'django.contrib.admin',
|
|
||||||
'django.contrib.auth',
|
|
||||||
'django.contrib.contenttypes',
|
|
||||||
'django.contrib.sessions',
|
|
||||||
'django.contrib.messages',
|
|
||||||
'django.contrib.staticfiles',
|
|
||||||
'bot',
|
|
||||||
'dice',
|
|
||||||
'logger',
|
|
||||||
'markov',
|
|
||||||
'weather',
|
|
||||||
]
|
|
||||||
|
|
||||||
MIDDLEWARE = [
|
|
||||||
'django.middleware.security.SecurityMiddleware',
|
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
|
||||||
'django.middleware.common.CommonMiddleware',
|
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|
||||||
]
|
|
||||||
|
|
||||||
ROOT_URLCONF = 'hitomi.urls'
|
|
||||||
|
|
||||||
TEMPLATES = [
|
|
||||||
{
|
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
|
||||||
'DIRS': [
|
|
||||||
os.path.join(BASE_DIR, 'templates'),
|
|
||||||
],
|
|
||||||
'APP_DIRS': True,
|
|
||||||
'OPTIONS': {
|
|
||||||
'context_processors': [
|
|
||||||
'django.template.context_processors.debug',
|
|
||||||
'django.template.context_processors.request',
|
|
||||||
'django.contrib.auth.context_processors.auth',
|
|
||||||
'django.contrib.messages.context_processors.messages',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
WSGI_APPLICATION = 'hitomi.wsgi.application'
|
|
||||||
|
|
||||||
|
|
||||||
# Database
|
|
||||||
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
|
|
||||||
|
|
||||||
DATABASES = {
|
|
||||||
'default': {
|
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
|
||||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Password validation
|
|
||||||
# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
|
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# Internationalization
|
|
||||||
# https://docs.djangoproject.com/en/2.0/topics/i18n/
|
|
||||||
|
|
||||||
LANGUAGE_CODE = 'en-us'
|
|
||||||
|
|
||||||
TIME_ZONE = 'CST6CDT'
|
|
||||||
|
|
||||||
USE_I18N = True
|
|
||||||
|
|
||||||
USE_L10N = True
|
|
||||||
|
|
||||||
USE_TZ = True
|
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
|
||||||
# https://docs.djangoproject.com/en/2.0/howto/static-files/
|
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
|
||||||
|
|
||||||
|
|
||||||
# default bot settings
|
|
||||||
|
|
||||||
DISCORD_BOT_TOKEN = 'token'
|
|
||||||
|
|
||||||
DISCORD_BOT_COMMAND_PREFIX = '!'
|
|
||||||
DISCORD_BOT_MAX_MESSAGES = 5000
|
|
||||||
LOGGER_BASE_DIR = './logs'
|
|
||||||
WEATHER_WEATHER_UNDERGROUND_API_KEY = 'key'
|
|
||||||
|
|
||||||
BOT_PLUGINS = []
|
|
||||||
|
|
||||||
API_PORT = 7777
|
|
||||||
|
|
||||||
|
|
||||||
# get local settings
|
|
||||||
|
|
||||||
try:
|
|
||||||
from hitomi.localsettings import *
|
|
||||||
except ImportError:
|
|
||||||
print("WARNING: no localsettings.py found!")
|
|
|
@ -1,24 +0,0 @@
|
||||||
"""hitomi URL Configuration
|
|
||||||
|
|
||||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
|
||||||
https://docs.djangoproject.com/en/2.0/topics/http/urls/
|
|
||||||
Examples:
|
|
||||||
Function views
|
|
||||||
1. Add an import: from my_app import views
|
|
||||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
|
||||||
Class-based views
|
|
||||||
1. Add an import: from other_app.views import Home
|
|
||||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
|
||||||
Including another URLconf
|
|
||||||
1. Import the include() function: from django.urls import include, path
|
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
|
||||||
"""
|
|
||||||
from django.contrib import admin
|
|
||||||
from django.urls import path
|
|
||||||
|
|
||||||
from hitomi.views import IndexView
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path('', IndexView.as_view(), name='index'),
|
|
||||||
path('admin/', admin.site.urls),
|
|
||||||
]
|
|
|
@ -1,8 +0,0 @@
|
||||||
"""Views for miscellaneous site stuff."""
|
|
||||||
from django.views.generic import TemplateView
|
|
||||||
|
|
||||||
|
|
||||||
class IndexView(TemplateView):
|
|
||||||
"""Serve a very basic index page."""
|
|
||||||
|
|
||||||
template_name = 'index.html'
|
|
|
@ -1,16 +0,0 @@
|
||||||
"""
|
|
||||||
WSGI config for hitomi project.
|
|
||||||
|
|
||||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "hitomi.settings")
|
|
||||||
|
|
||||||
application = get_wsgi_application()
|
|
|
@ -1,80 +0,0 @@
|
||||||
"""Log messages showing up on Discord."""
|
|
||||||
import logging
|
|
||||||
from os import makedirs
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
from bot import hitomi
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
logger.info("loading logger plugin")
|
|
||||||
|
|
||||||
|
|
||||||
def on_message(message):
|
|
||||||
"""Log the seen message."""
|
|
||||||
try:
|
|
||||||
log_file = log_file_for_message(message)
|
|
||||||
log_line = "{0:s}\t{1:s}\t{2:s}\t{3:s}".format(str(message.timestamp), str(message.author), str(message.id),
|
|
||||||
str(message.content))
|
|
||||||
logger.info("{0:s} - {1:s}".format(log_file, log_line))
|
|
||||||
with open(log_file, 'a') as log_fd:
|
|
||||||
print(log_line, file=log_fd)
|
|
||||||
except Exception as ex:
|
|
||||||
logger.error("error in creating log line")
|
|
||||||
logger.exception(ex)
|
|
||||||
|
|
||||||
|
|
||||||
hitomi.on_message_handlers.append(on_message)
|
|
||||||
|
|
||||||
|
|
||||||
@hitomi.event
|
|
||||||
async def on_message_delete(message):
|
|
||||||
"""Log the message deletion."""
|
|
||||||
try:
|
|
||||||
log_file = log_file_for_message(message)
|
|
||||||
log_line = "{0:s}\t{1:s}\tD:{2:s}\tDELETED: {3:s}".format(str(message.timestamp), str(message.author),
|
|
||||||
str(message.id), str(message.content))
|
|
||||||
logger.info("{0:s} - {1:s}".format(log_file, log_line))
|
|
||||||
with open(log_file, 'a') as log_fd:
|
|
||||||
print(log_line, file=log_fd)
|
|
||||||
except Exception as ex:
|
|
||||||
logger.error("error in creating log line")
|
|
||||||
logger.exception(ex)
|
|
||||||
|
|
||||||
# let other stuff happen
|
|
||||||
await hitomi.process_commands(message)
|
|
||||||
|
|
||||||
|
|
||||||
@hitomi.event
|
|
||||||
async def on_message_edit(before, after):
|
|
||||||
"""Log the message deletion."""
|
|
||||||
try:
|
|
||||||
log_file = log_file_for_message(after)
|
|
||||||
log_line = "{0:s}\t{1:s}\tE:{2:s}\tEDIT: {3:s} (was \"{4:s}\")".format(str(after.timestamp), str(after.author),
|
|
||||||
str(after.id), str(after.content),
|
|
||||||
str(before.content))
|
|
||||||
logger.info("{0:s} - {1:s}".format(log_file, log_line))
|
|
||||||
with open(log_file, 'a') as log_fd:
|
|
||||||
print(log_line, file=log_fd)
|
|
||||||
except Exception as ex:
|
|
||||||
logger.error("error in creating log line")
|
|
||||||
logger.exception(ex)
|
|
||||||
|
|
||||||
# let other stuff happen
|
|
||||||
await hitomi.process_commands(after)
|
|
||||||
|
|
||||||
|
|
||||||
def log_file_for_message(message):
|
|
||||||
"""For the given message, figure out where we'd log the message."""
|
|
||||||
if message.channel.is_private:
|
|
||||||
log_directory = 'DM/{0:s}/'.format(str(message.channel.user.name))
|
|
||||||
log_file = '{0:s}-{1:s}.log'.format(str(message.channel.user.name), str(message.timestamp.strftime('%Y%m')))
|
|
||||||
else:
|
|
||||||
log_directory = '{0:s}-{1:s}/{2:s}/'.format(str(message.server.id), str(message.server),
|
|
||||||
str(message.channel))
|
|
||||||
log_file = '{0:s}-{1:s}.log'.format(str(message.channel), str(message.timestamp.strftime('%Y%m')))
|
|
||||||
|
|
||||||
destination_dir = '{0:s}/{1:s}'.format(settings.LOGGER_BASE_DIR, log_directory)
|
|
||||||
makedirs(destination_dir, exist_ok=True)
|
|
||||||
|
|
||||||
return '{0:s}{1:s}'.format(destination_dir, log_file)
|
|
15
manage.py
15
manage.py
|
@ -1,15 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "hitomi.settings")
|
|
||||||
try:
|
|
||||||
from django.core.management import execute_from_command_line
|
|
||||||
except ImportError as exc:
|
|
||||||
raise ImportError(
|
|
||||||
"Couldn't import Django. Are you sure it's installed and "
|
|
||||||
"available on your PYTHONPATH environment variable? Did you "
|
|
||||||
"forget to activate a virtual environment?"
|
|
||||||
) from exc
|
|
||||||
execute_from_command_line(sys.argv)
|
|
|
@ -1,7 +0,0 @@
|
||||||
"""Display Markov data elements in the Django admin."""
|
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
from markov.models import MarkovContext, MarkovState
|
|
||||||
|
|
||||||
admin.site.register(MarkovContext)
|
|
||||||
admin.site.register(MarkovState)
|
|
|
@ -1,6 +0,0 @@
|
||||||
"""App config for the markov module."""
|
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class MarkovConfig(AppConfig):
|
|
||||||
name = 'markov'
|
|
|
@ -1,40 +0,0 @@
|
||||||
"""Observe Markov chains from Discord."""
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
from bot import hitomi
|
|
||||||
from markov import lib
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
logger.info("loading markov plugin")
|
|
||||||
|
|
||||||
|
|
||||||
def on_message(message):
|
|
||||||
"""Keep the observed Markov chains."""
|
|
||||||
# ignore self
|
|
||||||
if message.author == hitomi.user:
|
|
||||||
logger.debug("markov ignoring message authored by self")
|
|
||||||
return
|
|
||||||
|
|
||||||
# ignore commands
|
|
||||||
if message.content[0] == settings.DISCORD_BOT_COMMAND_PREFIX:
|
|
||||||
logger.debug("markov ignoring message that looks like a command")
|
|
||||||
return
|
|
||||||
|
|
||||||
if message.channel.is_private:
|
|
||||||
# DMs have a context of the author
|
|
||||||
context_name = message.author
|
|
||||||
else:
|
|
||||||
# channels have a context of the server, but we should ignore channels that override @everyone's read ability
|
|
||||||
for changed_role in message.channel.changed_roles:
|
|
||||||
if changed_role.is_everyone:
|
|
||||||
if not changed_role.permissions.read_messages:
|
|
||||||
logger.debug("markov ignoring channel that @everyone can't read")
|
|
||||||
return
|
|
||||||
context_name = message.server.id
|
|
||||||
|
|
||||||
lib.learn_line(message.content, context_name)
|
|
||||||
|
|
||||||
|
|
||||||
hitomi.on_message_handlers.append(on_message)
|
|
|
@ -1,30 +0,0 @@
|
||||||
"""Shared methods for the Markov module."""
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from markov.models import MarkovContext, MarkovState
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def learn_line(line, context_name):
|
|
||||||
"""Create a bunch of MarkovStates for a given line of text."""
|
|
||||||
logger.debug("learning %s...", line[:40])
|
|
||||||
|
|
||||||
context, created = MarkovContext.objects.get_or_create(name=context_name)
|
|
||||||
|
|
||||||
words = line.split()
|
|
||||||
words = [MarkovState.start1, MarkovState.start2] + words + [MarkovState.stop]
|
|
||||||
|
|
||||||
for word in words:
|
|
||||||
if len(word) > MarkovState._meta.get_field('k1').max_length:
|
|
||||||
return
|
|
||||||
|
|
||||||
for i, word in enumerate(words):
|
|
||||||
logger.debug("'{0:s}','{1:s}' -> '{2:s}'".format(words[i], words[i + 1], words[i + 2]))
|
|
||||||
state, created = MarkovState.objects.get_or_create(context=context, k1=words[i], k2=words[i + 1],
|
|
||||||
v=words[i + 2])
|
|
||||||
state.count += 1
|
|
||||||
state.save()
|
|
||||||
|
|
||||||
if i > len(words) - 4:
|
|
||||||
break
|
|
|
@ -1,41 +0,0 @@
|
||||||
# Generated by Django 2.0.1 on 2018-01-09 19:57
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='MarkovContext',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(max_length=200, unique=True)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='MarkovState',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('k1', models.CharField(max_length=256)),
|
|
||||||
('k2', models.CharField(max_length=256)),
|
|
||||||
('v', models.CharField(max_length=256)),
|
|
||||||
('count', models.IntegerField(default=0)),
|
|
||||||
('context', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='markov.MarkovContext')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.AlterUniqueTogether(
|
|
||||||
name='markovstate',
|
|
||||||
unique_together={('context', 'k1', 'k2', 'v')},
|
|
||||||
),
|
|
||||||
migrations.AlterIndexTogether(
|
|
||||||
name='markovstate',
|
|
||||||
index_together={('context', 'k1', 'k2'), ('context', 'v')},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,43 +0,0 @@
|
||||||
"""Database objects for storing Markov chains."""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class MarkovContext(models.Model):
|
|
||||||
"""Define contexts for Markov chains."""
|
|
||||||
|
|
||||||
name = models.CharField(max_length=200, unique=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
"""String representation."""
|
|
||||||
return "{0:s}".format(self.name)
|
|
||||||
|
|
||||||
|
|
||||||
class MarkovState(models.Model):
|
|
||||||
"""One element in a Markov chain, some text or something."""
|
|
||||||
|
|
||||||
start1 = '__start1'
|
|
||||||
start2 = '__start2'
|
|
||||||
stop = '__stop'
|
|
||||||
|
|
||||||
k1 = models.CharField(max_length=256)
|
|
||||||
k2 = models.CharField(max_length=256)
|
|
||||||
v = models.CharField(max_length=256)
|
|
||||||
|
|
||||||
count = models.IntegerField(default=0)
|
|
||||||
context = models.ForeignKey(MarkovContext, on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
index_together = [
|
|
||||||
['context', 'k1', 'k2'],
|
|
||||||
['context', 'v'],
|
|
||||||
]
|
|
||||||
unique_together = ('context', 'k1', 'k2', 'v')
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
"""String representation."""
|
|
||||||
return "{0:s},{1:s} -> {2:s} (count: {3:d})".format(self.k1, self.k2, self.v, self.count)
|
|
|
@ -2,44 +2,43 @@
|
||||||
# This file is autogenerated by pip-compile
|
# This file is autogenerated by pip-compile
|
||||||
# To update, run:
|
# To update, run:
|
||||||
#
|
#
|
||||||
# pip-compile --output-file requirements-dev.txt requirements-dev.in
|
# pip-compile --output-file=requirements/requirements-dev.txt requirements/requirements-dev.in
|
||||||
#
|
#
|
||||||
aiohttp==1.0.5
|
aiohttp==3.5.4 # via discord.py
|
||||||
astroid==1.6.0 # via pylint, pylint-celery, pylint-flask, pylint-plugin-utils, requirements-detector
|
astroid==2.2.5 # via pylint, pylint-celery, pylint-flask, requirements-detector
|
||||||
async-timeout==2.0.0 # via aiohttp
|
async-timeout==3.0.1 # via aiohttp
|
||||||
certifi==2017.11.5 # via requests
|
attrs==19.1.0 # via aiohttp
|
||||||
|
certifi==2019.6.16 # via requests
|
||||||
chardet==3.0.4 # via aiohttp, requests
|
chardet==3.0.4 # via aiohttp, requests
|
||||||
click==6.7 # via pip-tools
|
click==7.0 # via pip-tools
|
||||||
colorama==0.3.9 # via pylint
|
discord.py==1.2.2
|
||||||
discord.py==0.16.12
|
|
||||||
django==2.0.1
|
|
||||||
dodgy==0.1.9 # via prospector
|
dodgy==0.1.9 # via prospector
|
||||||
first==2.0.1 # via pip-tools
|
idna-ssl==1.1.0 # via aiohttp
|
||||||
idna==2.6 # via requests
|
idna==2.8 # via idna-ssl, requests, yarl
|
||||||
isort==4.2.15 # via pylint
|
isort==4.3.20 # via pylint
|
||||||
lazy-object-proxy==1.3.1 # via astroid
|
lazy-object-proxy==1.4.1 # via astroid
|
||||||
mccabe==0.6.1 # via prospector, pylint
|
mccabe==0.6.1 # via prospector, pylint
|
||||||
multidict==3.3.2 # via aiohttp
|
multidict==4.5.2 # via aiohttp, yarl
|
||||||
pep8-naming==0.4.1 # via prospector
|
pep8-naming==0.4.1 # via prospector
|
||||||
pip-tools==1.11.0
|
pip-tools==3.8.0
|
||||||
ply==3.10
|
prospector==1.1.6.4
|
||||||
prospector==0.12.7
|
pycodestyle==2.4.0 # via prospector
|
||||||
pycodestyle==2.0.0 # via prospector
|
pydocstyle==3.0.0 # via prospector
|
||||||
pydocstyle==2.1.1 # via prospector
|
|
||||||
pyflakes==1.6.0 # via prospector
|
pyflakes==1.6.0 # via prospector
|
||||||
pylint-celery==0.3 # via prospector
|
pylint-celery==0.3 # via prospector
|
||||||
pylint-common==0.2.5 # via prospector
|
pylint-django==2.0.9 # via prospector
|
||||||
pylint-django==0.7.2 # via prospector
|
pylint-flask==0.6 # via prospector
|
||||||
pylint-flask==0.5 # via prospector
|
pylint-plugin-utils==0.5 # via prospector, pylint-celery, pylint-django, pylint-flask
|
||||||
pylint-plugin-utils==0.2.6 # via prospector, pylint-celery, pylint-common, pylint-django, pylint-flask
|
pylint==2.3.1 # via prospector, pylint-celery, pylint-django, pylint-flask, pylint-plugin-utils
|
||||||
pylint==1.8.1 # via prospector, pylint-celery, pylint-common, pylint-django, pylint-flask, pylint-plugin-utils
|
pyyaml==5.1.1 # via prospector
|
||||||
pytz==2017.3 # via django
|
requests==2.22.0
|
||||||
pyyaml==3.12 # via prospector
|
requirements-detector==0.6 # via prospector
|
||||||
requests==2.18.4
|
|
||||||
requirements-detector==0.5.2 # via prospector
|
|
||||||
setoptconf==0.2.0 # via prospector
|
setoptconf==0.2.0 # via prospector
|
||||||
six==1.11.0 # via astroid, pip-tools, pydocstyle, pylint
|
six==1.12.0 # via astroid, pip-tools, pydocstyle
|
||||||
snowballstemmer==1.2.1 # via pydocstyle
|
snowballstemmer==1.2.1 # via pydocstyle
|
||||||
urllib3==1.22 # via requests
|
typed-ast==1.4.0 # via astroid
|
||||||
websockets==3.4 # via discord.py
|
typing-extensions==3.7.4 # via aiohttp
|
||||||
wrapt==1.10.11 # via astroid
|
urllib3==1.25.3 # via requests
|
||||||
|
websockets==6.0 # via discord.py
|
||||||
|
wrapt==1.11.2 # via astroid
|
||||||
|
yarl==1.3.0 # via aiohttp
|
||||||
|
|
|
@ -1,5 +1,2 @@
|
||||||
aiohttp # API server
|
|
||||||
discord.py # discord client
|
discord.py # discord client
|
||||||
Django # DB backend/management commands
|
requests # HTTP requests, for accessing the dr.botzo RPC API
|
||||||
ply # lex and yacc, for the dice module
|
|
||||||
requests # HTTP requests, for the weather module and likely others
|
|
||||||
|
|
|
@ -2,18 +2,19 @@
|
||||||
# This file is autogenerated by pip-compile
|
# This file is autogenerated by pip-compile
|
||||||
# To update, run:
|
# To update, run:
|
||||||
#
|
#
|
||||||
# pip-compile --output-file requirements.txt requirements.in
|
# pip-compile --output-file=requirements/requirements.txt requirements/requirements.in
|
||||||
#
|
#
|
||||||
aiohttp==1.0.5
|
aiohttp==3.5.4 # via discord.py
|
||||||
async-timeout==2.0.0 # via aiohttp
|
async-timeout==3.0.1 # via aiohttp
|
||||||
certifi==2017.11.5 # via requests
|
attrs==19.1.0 # via aiohttp
|
||||||
|
certifi==2019.6.16 # via requests
|
||||||
chardet==3.0.4 # via aiohttp, requests
|
chardet==3.0.4 # via aiohttp, requests
|
||||||
discord.py==0.16.12
|
discord.py==1.2.2
|
||||||
django==2.0.1
|
idna-ssl==1.1.0 # via aiohttp
|
||||||
idna==2.6 # via requests
|
idna==2.8 # via idna-ssl, requests, yarl
|
||||||
multidict==3.3.2 # via aiohttp
|
multidict==4.5.2 # via aiohttp, yarl
|
||||||
ply==3.10
|
requests==2.22.0
|
||||||
pytz==2017.3 # via django
|
typing-extensions==3.7.4 # via aiohttp
|
||||||
requests==2.18.4
|
urllib3==1.25.3 # via requests
|
||||||
urllib3==1.22 # via requests
|
websockets==6.0 # via discord.py
|
||||||
websockets==3.4 # via discord.py
|
yarl==1.3.0 # via aiohttp
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
"""Provide weather information over Discord."""
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from bot import hitomi
|
|
||||||
from weather import lib
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
logger.info("loading weather plugin")
|
|
||||||
|
|
||||||
|
|
||||||
@hitomi.command()
|
|
||||||
async def forecast(query: str):
|
|
||||||
await hitomi.say(lib.get_forecast_for_query([query]))
|
|
||||||
|
|
||||||
|
|
||||||
@hitomi.command()
|
|
||||||
async def weather(query: str):
|
|
||||||
await hitomi.say(lib.get_conditions_for_query([query]))
|
|
198
weather/lib.py
198
weather/lib.py
|
@ -1,198 +0,0 @@
|
||||||
# coding: utf-8
|
|
||||||
"""Library methods for looking up and presenting weather."""
|
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
|
|
||||||
wu_base_url = 'http://api.wunderground.com/api/{0:s}/'.format(settings.WEATHER_WEATHER_UNDERGROUND_API_KEY)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def get_conditions_for_query(queryitems):
|
|
||||||
"""Make a wunderground conditions call, return as string."""
|
|
||||||
|
|
||||||
# recombine the query into a string
|
|
||||||
query = ' '.join(queryitems)
|
|
||||||
query = query.replace(' ', '_')
|
|
||||||
|
|
||||||
try:
|
|
||||||
logger.info("looking up conditions for {0:s}".format(query))
|
|
||||||
url = wu_base_url + ('{0:s}/q/{1:s}.json'.format('conditions', query))
|
|
||||||
logger.debug("calling %s", url)
|
|
||||||
resp = requests.get(url)
|
|
||||||
condition_data = resp.json()
|
|
||||||
except IOError as e:
|
|
||||||
logger.error("error while making conditions query")
|
|
||||||
logger.exception(e)
|
|
||||||
raise
|
|
||||||
|
|
||||||
# condition data is loaded. the rest of this is obviously specific to
|
|
||||||
# http://www.wunderground.com/weather/api/d/docs?d=data/conditions
|
|
||||||
logger.debug(condition_data)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# just see if we have current_observation data
|
|
||||||
current = condition_data['current_observation']
|
|
||||||
except KeyError as e:
|
|
||||||
# ok, try to see if the ambiguous results stuff will help
|
|
||||||
logger.debug(e)
|
|
||||||
logger.debug("potentially ambiguous results, checking")
|
|
||||||
try:
|
|
||||||
results = condition_data['response']['results']
|
|
||||||
reply = "Multiple results, try one of the following zmw codes:"
|
|
||||||
for res in results[:-1]:
|
|
||||||
q = res['l'].strip('/q/')
|
|
||||||
reply += " {0:s} ({1:s}, {2:s}),".format(q, res['name'], res['country_name'])
|
|
||||||
q = results[-1]['l'].strip('/q/')
|
|
||||||
reply += " or {0:s} ({1:s}, {2:s}).".format(q, results[-1]['name'], results[-1]['country_name'])
|
|
||||||
return reply
|
|
||||||
except KeyError as e:
|
|
||||||
# now we really know something is wrong
|
|
||||||
logger.error("error or bad query in conditions lookup")
|
|
||||||
logger.exception(e)
|
|
||||||
return "No results."
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
location = current['display_location']['full']
|
|
||||||
reply = "Conditions for **{0:s}**: ".format(location)
|
|
||||||
|
|
||||||
weather_str = current['weather']
|
|
||||||
if weather_str != '':
|
|
||||||
reply += "{0:s}, ".format(weather_str)
|
|
||||||
|
|
||||||
temp_f = current['temp_f']
|
|
||||||
temp_c = current['temp_c']
|
|
||||||
temp_str = current['temperature_string']
|
|
||||||
if temp_f != '' and temp_c != '':
|
|
||||||
reply += "{0:.1f}°F ({1:.1f}°C)".format(temp_f, temp_c)
|
|
||||||
elif temp_str != '':
|
|
||||||
reply += "{0:s}".format(temp_str)
|
|
||||||
|
|
||||||
# append feels like if we have it
|
|
||||||
feelslike_f = current['feelslike_f']
|
|
||||||
feelslike_c = current['feelslike_c']
|
|
||||||
feelslike_str = current['feelslike_string']
|
|
||||||
if feelslike_f != '' and feelslike_c != '':
|
|
||||||
reply += ", feels like {0:s}°F ({1:s}°C)".format(feelslike_f, feelslike_c)
|
|
||||||
elif feelslike_str != '':
|
|
||||||
reply += ", feels like {0:s}".format(feelslike_str)
|
|
||||||
|
|
||||||
# whether this is current or current + feelslike, terminate sentence
|
|
||||||
reply += ". "
|
|
||||||
|
|
||||||
humidity_str = current['relative_humidity']
|
|
||||||
if humidity_str != '':
|
|
||||||
reply += "**Humidity**: {0:s}. ".format(humidity_str)
|
|
||||||
|
|
||||||
wind_str = current['wind_string']
|
|
||||||
if wind_str != '':
|
|
||||||
reply += "**Wind**: {0:s}. ".format(wind_str)
|
|
||||||
|
|
||||||
pressure_in = current['pressure_in']
|
|
||||||
pressure_trend = current['pressure_trend']
|
|
||||||
if pressure_in != '':
|
|
||||||
reply += "**Pressure**: {0:s}\"".format(pressure_in)
|
|
||||||
if pressure_trend != '':
|
|
||||||
reply += " and {0:s}".format("dropping" if pressure_trend == '-' else "rising")
|
|
||||||
reply += ". "
|
|
||||||
|
|
||||||
heat_index_str = current['heat_index_string']
|
|
||||||
if heat_index_str != '' and heat_index_str != 'NA':
|
|
||||||
reply += "**Heat index**: {0:s}. ".format(heat_index_str)
|
|
||||||
|
|
||||||
windchill_str = current['windchill_string']
|
|
||||||
if windchill_str != '' and windchill_str != 'NA':
|
|
||||||
reply += "**Wind chill**: {0:s}. ".format(windchill_str)
|
|
||||||
|
|
||||||
visibility_mi = current['visibility_mi']
|
|
||||||
if visibility_mi != '':
|
|
||||||
reply += "**Visibility**: {0:s} miles. ".format(visibility_mi)
|
|
||||||
|
|
||||||
precip_in = current['precip_today_in']
|
|
||||||
if precip_in != '':
|
|
||||||
reply += "**Precipitation today**: {0:s}\". ".format(precip_in)
|
|
||||||
|
|
||||||
observation_time = current['observation_time']
|
|
||||||
if observation_time != '':
|
|
||||||
reply += "{0:s}. ".format(observation_time)
|
|
||||||
|
|
||||||
return _prettify_weather_strings(reply.rstrip())
|
|
||||||
except KeyError as e:
|
|
||||||
logger.error("error or unexpected results in conditions reply")
|
|
||||||
logger.exception(e)
|
|
||||||
return "Error parsing results."
|
|
||||||
|
|
||||||
|
|
||||||
def get_forecast_for_query(queryitems):
|
|
||||||
"""Make a wunderground forecast call, return as string."""
|
|
||||||
|
|
||||||
# recombine the query into a string
|
|
||||||
query = ' '.join(queryitems)
|
|
||||||
query = query.replace(' ', '_')
|
|
||||||
|
|
||||||
try:
|
|
||||||
logger.info("looking up forecast for {0:s}".format(query))
|
|
||||||
url = wu_base_url + ('{0:s}/q/{1:s}.json'.format('forecast', query))
|
|
||||||
resp = requests.get(url)
|
|
||||||
forecast_data = resp.json()
|
|
||||||
except IOError as e:
|
|
||||||
logger.error("error while making forecast query")
|
|
||||||
logger.exception(e)
|
|
||||||
raise
|
|
||||||
|
|
||||||
# forecast data is loaded. the rest of this is obviously specific to
|
|
||||||
# http://www.wunderground.com/weather/api/d/docs?d=data/forecast
|
|
||||||
logger.debug(forecast_data)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# just see if we have forecast data
|
|
||||||
forecasts = forecast_data['forecast']['txt_forecast']
|
|
||||||
except KeyError as e:
|
|
||||||
# ok, try to see if the ambiguous results stuff will help
|
|
||||||
logger.debug(e)
|
|
||||||
logger.debug("potentially ambiguous results, checking")
|
|
||||||
try:
|
|
||||||
results = forecast_data['response']['results']
|
|
||||||
reply = "Multiple results, try one of the following zmw codes:"
|
|
||||||
for res in results[:-1]:
|
|
||||||
q = res['l'].strip('/q/')
|
|
||||||
reply += " {0:s} ({1:s}, {2:s}),".format(q, res['name'],
|
|
||||||
res['country_name'])
|
|
||||||
q = results[-1]['l'].strip('/q/')
|
|
||||||
reply += " or {0:s} ({1:s}, {2:s}).".format(q, results[-1]['name'],
|
|
||||||
results[-1]['country_name'])
|
|
||||||
return reply
|
|
||||||
except KeyError as e:
|
|
||||||
# now we really know something is wrong
|
|
||||||
logger.error("error or bad query in forecast lookup")
|
|
||||||
logger.exception(e)
|
|
||||||
return "No results."
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
reply = "Forecast: "
|
|
||||||
for forecast in forecasts['forecastday'][0:5]:
|
|
||||||
reply += "**{0:s}**: {1:s} ".format(forecast['title'],
|
|
||||||
forecast['fcttext'])
|
|
||||||
|
|
||||||
return _prettify_weather_strings(reply.rstrip())
|
|
||||||
except KeyError as e:
|
|
||||||
logger.error("error or unexpected results in forecast reply")
|
|
||||||
logger.exception(e)
|
|
||||||
return "Error parsing results."
|
|
||||||
|
|
||||||
|
|
||||||
def _prettify_weather_strings(weather_str):
|
|
||||||
"""
|
|
||||||
Clean up output strings.
|
|
||||||
|
|
||||||
For example, turn 32F into 32°F in input string.
|
|
||||||
|
|
||||||
Input:
|
|
||||||
weather_str --- the string to clean up
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
return re.sub(r'(\d+)\s*([FC])', r'\1°\2', weather_str)
|
|
Loading…
Reference in New Issue