inquisitor/inquisitor/app.py

195 lines
6.0 KiB
Python

# Standard library imports
from datetime import datetime, timedelta
import os
import traceback
# Third party imports
from flask import Flask, render_template, request, jsonify, abort, redirect, url_for
# Application imports
from inquisitor.configs import (
DUNGEON_PATH,
SOURCES_PATH,
CACHE_PATH,
subfeeds,
get_subfeed_overrides,
logger,
init_default_logging)
from inquisitor import sources, loader, timestamp
# Globals
app = Flask(__name__)
def make_query_link(text, wl, bl):
wlp = "only=" + ",".join(wl)
blp = "not=" + ",".join(bl)
params = [p for p in (wlp, blp) if not p.endswith("=")]
query = "?{}".format("&".join(params))
return '<a href="{1}">{0}</a>'.format(text, query)
@app.template_filter("datetimeformat")
def datetimeformat(value):
return timestamp.stamp_to_readable(value) if value is not None else ""
@app.route("/")
def root():
return redirect(url_for('feed'))
@app.route("/feed/")
def feed():
return feed_for_sources(source_names=None)
@app.route("/feed/<string:feed_name>/")
def subfeed(feed_name):
# Check for and apply subfeed overrides
subfeed_overrides = get_subfeed_overrides()
subfeed_config = subfeed_overrides or subfeeds or {}
# The built-in inquisitor subfeed contains sources not in another subfeed
if feed_name == 'inquisitor':
all_sources = os.listdir(DUNGEON_PATH)
for subfeed, sources in subfeed_config.items():
for source_name in sources:
if source_name in all_sources:
all_sources.remove(source_name)
return feed_for_sources(all_sources)
if feed_name not in subfeed_config:
return abort(404)
return feed_for_sources(subfeed_config[feed_name])
def feed_for_sources(source_names):
# Determine exclusion filters
filters = []
wl_param = request.args.get('only')
wl = wl_param.split(",") if wl_param else []
bl_param = request.args.get('not')
bl = bl_param.split(",") if bl_param else []
if wl:
filters.append(lambda item: not any([tag in wl for tag in item['tags']]))
if bl:
filters.append(lambda item: any([tag in bl for tag in item['tags']]))
# Get all active+filtered items and all active tags
total = 0
items, errors = loader.load_active_items(source_names)
active_items = []
active_tags = {}
for item in items:
if item['active']:
for tag in item['tags']:
if tag not in active_tags: active_tags[tag] = 0
active_tags[tag] += 1
# active_tags |= set(item['tags'])
total += 1
if not any(map(lambda f: f(item), filters)):
active_items.append(item)
# Sort items by time
active_items.sort(key=lambda i: i['time'] if 'time' in i and i['time'] else i['created'] if 'created' in i and i['created'] else 0)
logger.info("Returning {} of {} items".format(len(active_items), total))
if errors:
read_ex = {
'title': 'Read errors',
'body': "<pre>{}</pre>".format("\n\n".join(errors)),
'created': None,
}
active_items.insert(0, read_ex)
if total > 0:
# Create the feed control item
link_table = ["<tr><td>{0}</td><td>{1}</td><td></td><td></td></tr>".format(
total, make_query_link("all", [], []))]
for tag, count in sorted(active_tags.items(), key=lambda i: i[0].lower()):
links = [count]
links.append(make_query_link(tag, [tag], []))
if tag in wl:
new_wl = [t for t in wl if t != tag]
links.append(make_query_link("-only", new_wl, bl))
else:
new_bl = [t for t in bl if t != tag]
links.append(make_query_link("+only", wl + [tag], new_bl))
if tag in bl:
new_bl = [t for t in bl if t != tag]
links.append(make_query_link("-not", wl, new_bl))
else:
new_wl = [t for t in wl if t != tag]
links.append(make_query_link("+not", new_wl, bl + [tag]))
link_table.append("<tr><td>{0}</td><td>{1}</td><td>{2}</td><td>{3}</td></tr>".format(*links))
body = '<table class="feed-control">{}</table>'.format("\n".join(link_table))
feed_control = {
'title': 'Feed Control [{}/{}]'.format(len(active_items), total),
'body': body,
}
active_items.insert(0, feed_control)
selection = active_items[:100]
return render_template("feed.jinja2",
items=selection,
mdeac=[
{'source': item['source'], 'itemid': item['id']}
for item in selection
if 'id' in item])
@app.route("/deactivate/", methods=['POST'])
def deactivate():
params = request.get_json()
if 'source' not in params and 'itemid' not in params:
logger.error("Bad request params: {}".format(params))
item = loader.load_item(params['source'], params['itemid'])
if item['active']:
logger.debug(f"Deactivating {params['source']}/{params['itemid']}")
item['active'] = False
return jsonify({'active': item['active']})
@app.route("/punt/", methods=['POST'])
def punt():
params = request.get_json()
if 'source' not in params and 'itemid' not in params:
logger.error("Bad request params: {}".format(params))
item = loader.load_item(params['source'], params['itemid'])
tomorrow = datetime.now() + timedelta(days=1)
morning = datetime(tomorrow.year, tomorrow.month, tomorrow.day, 6, 0, 0)
til_then = morning.timestamp() - item['created']
item['tts'] = til_then
return jsonify(item.item)
@app.route("/mass-deactivate/", methods=['POST'])
def mass_deactivate():
params = request.get_json()
if 'items' not in params:
logger.error("Bad request params: {}".format(params))
for info in params.get('items', []):
source = info['source']
itemid = info['itemid']
item = loader.load_item(source, itemid)
if item['active']:
logger.debug(f"Deactivating {info['source']}/{info['itemid']}")
item['active'] = False
return jsonify({})
@app.route("/callback/", methods=['POST'])
def callback():
params = request.get_json()
if 'source' not in params and 'itemid' not in params:
logger.error("Bad request params: {}".format(params))
logger.info('Executing callback for {}/{}'.format(params['source'], params['itemid']))
sources.item_callback(params['source'], params['itemid'])
return jsonify({})
@app.route('/cache/<path:cache_path>')
def cache(cache_path):
path = os.path.join(CACHE_PATH, cache_path)
if not os.path.isfile(path):
return abort(404)
with open(path, 'rb') as f:
return f.read()
def wsgi():
init_default_logging()
return app