reset the hitomi project, remove django, rebuild deps
This commit is contained in:
parent
8be039da80
commit
f9baa76a24
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,7 +1,3 @@
|
||||
*.log
|
||||
*.pyc
|
||||
.idea/
|
||||
db.sqlite3
|
||||
localsettings.py
|
||||
parser.out
|
||||
parsetab.py
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
### 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
|
||||
project for a couple of us to hack around on, but you may find it useful.
|
||||
|
||||
|
20
bot.py
Normal file
20
bot.py
Normal file
@ -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)
|
36
hitomi/config.py
Normal file
36
hitomi/config.py
Normal file
@ -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
|
||||
# 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
|
||||
astroid==1.6.0 # via pylint, pylint-celery, pylint-flask, pylint-plugin-utils, requirements-detector
|
||||
async-timeout==2.0.0 # via aiohttp
|
||||
certifi==2017.11.5 # via requests
|
||||
aiohttp==3.5.4 # via discord.py
|
||||
astroid==2.2.5 # via pylint, pylint-celery, pylint-flask, requirements-detector
|
||||
async-timeout==3.0.1 # via aiohttp
|
||||
attrs==19.1.0 # via aiohttp
|
||||
certifi==2019.6.16 # via requests
|
||||
chardet==3.0.4 # via aiohttp, requests
|
||||
click==6.7 # via pip-tools
|
||||
colorama==0.3.9 # via pylint
|
||||
discord.py==0.16.12
|
||||
django==2.0.1
|
||||
click==7.0 # via pip-tools
|
||||
discord.py==1.2.2
|
||||
dodgy==0.1.9 # via prospector
|
||||
first==2.0.1 # via pip-tools
|
||||
idna==2.6 # via requests
|
||||
isort==4.2.15 # via pylint
|
||||
lazy-object-proxy==1.3.1 # via astroid
|
||||
idna-ssl==1.1.0 # via aiohttp
|
||||
idna==2.8 # via idna-ssl, requests, yarl
|
||||
isort==4.3.20 # via pylint
|
||||
lazy-object-proxy==1.4.1 # via astroid
|
||||
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
|
||||
pip-tools==1.11.0
|
||||
ply==3.10
|
||||
prospector==0.12.7
|
||||
pycodestyle==2.0.0 # via prospector
|
||||
pydocstyle==2.1.1 # via prospector
|
||||
pip-tools==3.8.0
|
||||
prospector==1.1.6.4
|
||||
pycodestyle==2.4.0 # via prospector
|
||||
pydocstyle==3.0.0 # via prospector
|
||||
pyflakes==1.6.0 # via prospector
|
||||
pylint-celery==0.3 # via prospector
|
||||
pylint-common==0.2.5 # via prospector
|
||||
pylint-django==0.7.2 # via prospector
|
||||
pylint-flask==0.5 # via prospector
|
||||
pylint-plugin-utils==0.2.6 # via prospector, pylint-celery, pylint-common, pylint-django, pylint-flask
|
||||
pylint==1.8.1 # via prospector, pylint-celery, pylint-common, pylint-django, pylint-flask, pylint-plugin-utils
|
||||
pytz==2017.3 # via django
|
||||
pyyaml==3.12 # via prospector
|
||||
requests==2.18.4
|
||||
requirements-detector==0.5.2 # via prospector
|
||||
pylint-django==2.0.9 # via prospector
|
||||
pylint-flask==0.6 # via prospector
|
||||
pylint-plugin-utils==0.5 # via prospector, pylint-celery, pylint-django, pylint-flask
|
||||
pylint==2.3.1 # via prospector, pylint-celery, pylint-django, pylint-flask, pylint-plugin-utils
|
||||
pyyaml==5.1.1 # via prospector
|
||||
requests==2.22.0
|
||||
requirements-detector==0.6 # 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
|
||||
urllib3==1.22 # via requests
|
||||
websockets==3.4 # via discord.py
|
||||
wrapt==1.10.11 # via astroid
|
||||
typed-ast==1.4.0 # via astroid
|
||||
typing-extensions==3.7.4 # via aiohttp
|
||||
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
|
||||
Django # DB backend/management commands
|
||||
ply # lex and yacc, for the dice module
|
||||
requests # HTTP requests, for the weather module and likely others
|
||||
requests # HTTP requests, for accessing the dr.botzo RPC API
|
||||
|
@ -2,18 +2,19 @@
|
||||
# This file is autogenerated by pip-compile
|
||||
# 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
|
||||
async-timeout==2.0.0 # via aiohttp
|
||||
certifi==2017.11.5 # via requests
|
||||
aiohttp==3.5.4 # via discord.py
|
||||
async-timeout==3.0.1 # via aiohttp
|
||||
attrs==19.1.0 # via aiohttp
|
||||
certifi==2019.6.16 # via requests
|
||||
chardet==3.0.4 # via aiohttp, requests
|
||||
discord.py==0.16.12
|
||||
django==2.0.1
|
||||
idna==2.6 # via requests
|
||||
multidict==3.3.2 # via aiohttp
|
||||
ply==3.10
|
||||
pytz==2017.3 # via django
|
||||
requests==2.18.4
|
||||
urllib3==1.22 # via requests
|
||||
websockets==3.4 # via discord.py
|
||||
discord.py==1.2.2
|
||||
idna-ssl==1.1.0 # via aiohttp
|
||||
idna==2.8 # via idna-ssl, requests, yarl
|
||||
multidict==4.5.2 # via aiohttp, yarl
|
||||
requests==2.22.0
|
||||
typing-extensions==3.7.4 # via aiohttp
|
||||
urllib3==1.25.3 # via requests
|
||||
websockets==6.0 # 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
Block a user