From f9baa76a24bf4ceafd1f7d602149a4d7be50fc11 Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Sat, 22 Jun 2019 10:39:56 -0500 Subject: [PATCH] reset the hitomi project, remove django, rebuild deps --- .gitignore | 4 - README.md | 2 +- bot.py | 20 ++ bot/__init__.py | 18 -- bot/api.py | 22 -- bot/management/__init__.py | 0 bot/management/commands/__init__.py | 0 bot/management/commands/starthitomi.py | 54 ----- dice/__init__.py | 0 dice/bot.py | 20 -- dice/lib.py | 278 ------------------------- hitomi/__init__.py | 29 +++ hitomi/config.py | 36 ++++ hitomi/settings.py | 149 ------------- hitomi/urls.py | 24 --- hitomi/views.py | 8 - hitomi/wsgi.py | 16 -- logger/__init__.py | 0 logger/bot.py | 80 ------- manage.py | 15 -- markov/__init__.py | 0 markov/admin.py | 7 - markov/apps.py | 6 - markov/bot.py | 40 ---- markov/lib.py | 30 --- markov/migrations/0001_initial.py | 41 ---- markov/migrations/__init__.py | 0 markov/models.py | 43 ---- requirements-dev.txt | 63 +++--- requirements.in | 5 +- requirements.txt | 27 +-- templates/index.html | 0 weather/__init__.py | 0 weather/bot.py | 18 -- weather/lib.py | 198 ------------------ 35 files changed, 132 insertions(+), 1121 deletions(-) create mode 100644 bot.py delete mode 100644 bot/__init__.py delete mode 100644 bot/api.py delete mode 100644 bot/management/__init__.py delete mode 100644 bot/management/commands/__init__.py delete mode 100644 bot/management/commands/starthitomi.py delete mode 100644 dice/__init__.py delete mode 100644 dice/bot.py delete mode 100644 dice/lib.py create mode 100644 hitomi/config.py delete mode 100644 hitomi/settings.py delete mode 100644 hitomi/urls.py delete mode 100644 hitomi/views.py delete mode 100644 hitomi/wsgi.py delete mode 100644 logger/__init__.py delete mode 100644 logger/bot.py delete mode 100644 manage.py delete mode 100644 markov/__init__.py delete mode 100644 markov/admin.py delete mode 100644 markov/apps.py delete mode 100644 markov/bot.py delete mode 100644 markov/lib.py delete mode 100644 markov/migrations/0001_initial.py delete mode 100644 markov/migrations/__init__.py delete mode 100644 markov/models.py delete mode 100644 templates/index.html delete mode 100644 weather/__init__.py delete mode 100644 weather/bot.py delete mode 100644 weather/lib.py diff --git a/.gitignore b/.gitignore index 08f90fe..8a2c97e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,3 @@ *.log *.pyc .idea/ -db.sqlite3 -localsettings.py -parser.out -parsetab.py diff --git a/README.md b/README.md index 122c0ed..4b3fb0b 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..2ff55f3 --- /dev/null +++ b/bot.py @@ -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() diff --git a/bot/__init__.py b/bot/__init__.py deleted file mode 100644 index 6a52448..0000000 --- a/bot/__init__.py +++ /dev/null @@ -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) diff --git a/bot/api.py b/bot/api.py deleted file mode 100644 index 8d5f36c..0000000 --- a/bot/api.py +++ /dev/null @@ -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) diff --git a/bot/management/__init__.py b/bot/management/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/bot/management/commands/__init__.py b/bot/management/commands/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/bot/management/commands/starthitomi.py b/bot/management/commands/starthitomi.py deleted file mode 100644 index 934a6bb..0000000 --- a/bot/management/commands/starthitomi.py +++ /dev/null @@ -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() diff --git a/dice/__init__.py b/dice/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/dice/bot.py b/dice/bot.py deleted file mode 100644 index cc1f4b7..0000000 --- a/dice/bot.py +++ /dev/null @@ -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)) diff --git a/dice/lib.py b/dice/lib.py deleted file mode 100644 index 78a93c6..0000000 --- a/dice/lib.py +++ /dev/null @@ -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) diff --git a/hitomi/__init__.py b/hitomi/__init__.py index e69de29..280625d 100644 --- a/hitomi/__init__.py +++ b/hitomi/__init__.py @@ -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) diff --git a/hitomi/config.py b/hitomi/config.py new file mode 100644 index 0000000..5c2104d --- /dev/null +++ b/hitomi/config.py @@ -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 + }, + } +}) diff --git a/hitomi/settings.py b/hitomi/settings.py deleted file mode 100644 index 7a0599f..0000000 --- a/hitomi/settings.py +++ /dev/null @@ -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!") diff --git a/hitomi/urls.py b/hitomi/urls.py deleted file mode 100644 index 07ade2d..0000000 --- a/hitomi/urls.py +++ /dev/null @@ -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), -] diff --git a/hitomi/views.py b/hitomi/views.py deleted file mode 100644 index 36927f7..0000000 --- a/hitomi/views.py +++ /dev/null @@ -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' diff --git a/hitomi/wsgi.py b/hitomi/wsgi.py deleted file mode 100644 index f9c4a22..0000000 --- a/hitomi/wsgi.py +++ /dev/null @@ -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() diff --git a/logger/__init__.py b/logger/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/logger/bot.py b/logger/bot.py deleted file mode 100644 index bdd96ac..0000000 --- a/logger/bot.py +++ /dev/null @@ -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) diff --git a/manage.py b/manage.py deleted file mode 100644 index 7bbe328..0000000 --- a/manage.py +++ /dev/null @@ -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) diff --git a/markov/__init__.py b/markov/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/markov/admin.py b/markov/admin.py deleted file mode 100644 index c9dc6a1..0000000 --- a/markov/admin.py +++ /dev/null @@ -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) diff --git a/markov/apps.py b/markov/apps.py deleted file mode 100644 index 19f954a..0000000 --- a/markov/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -"""App config for the markov module.""" -from django.apps import AppConfig - - -class MarkovConfig(AppConfig): - name = 'markov' diff --git a/markov/bot.py b/markov/bot.py deleted file mode 100644 index 9d2ffa7..0000000 --- a/markov/bot.py +++ /dev/null @@ -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) diff --git a/markov/lib.py b/markov/lib.py deleted file mode 100644 index 1b2048e..0000000 --- a/markov/lib.py +++ /dev/null @@ -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 diff --git a/markov/migrations/0001_initial.py b/markov/migrations/0001_initial.py deleted file mode 100644 index e273fbd..0000000 --- a/markov/migrations/0001_initial.py +++ /dev/null @@ -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')}, - ), - ] diff --git a/markov/migrations/__init__.py b/markov/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/markov/models.py b/markov/models.py deleted file mode 100644 index bf3ffdd..0000000 --- a/markov/models.py +++ /dev/null @@ -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) diff --git a/requirements-dev.txt b/requirements-dev.txt index ba13cbd..c35ae8b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -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 diff --git a/requirements.in b/requirements.in index 26eb168..871edf0 100644 --- a/requirements.in +++ b/requirements.in @@ -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 diff --git a/requirements.txt b/requirements.txt index 45975d4..5eb49f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index e69de29..0000000 diff --git a/weather/__init__.py b/weather/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/weather/bot.py b/weather/bot.py deleted file mode 100644 index 6409aa3..0000000 --- a/weather/bot.py +++ /dev/null @@ -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])) diff --git a/weather/lib.py b/weather/lib.py deleted file mode 100644 index 27909a5..0000000 --- a/weather/lib.py +++ /dev/null @@ -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)