diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2024-12-22 21:53:57 +0100 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2024-12-22 21:53:57 +0100 |
| commit | e3481a4a35091b32b6fbee80c1c9ba2b6d7b50d6 (patch) | |
| tree | 58f90b32cbd89599acfaab07377cc0447f1190c1 /halfnarp2.py | |
Rework of halfnarp and fullnarp into a self contained repository. Still WIP
Diffstat (limited to 'halfnarp2.py')
| -rwxr-xr-x | halfnarp2.py | 289 |
1 files changed, 289 insertions, 0 deletions
diff --git a/halfnarp2.py b/halfnarp2.py new file mode 100755 index 0000000..ebd6f6b --- /dev/null +++ b/halfnarp2.py | |||
| @@ -0,0 +1,289 @@ | |||
| 1 | #!venv/bin/python | ||
| 2 | |||
| 3 | from flask import Flask, render_template, jsonify, request, abort, send_file, url_for | ||
| 4 | from flask_sqlalchemy import SQLAlchemy | ||
| 5 | from flask_cors import CORS | ||
| 6 | from lxml import etree | ||
| 7 | from argparse import ArgumentParser | ||
| 8 | import requests | ||
| 9 | import json | ||
| 10 | import uuid | ||
| 11 | import markdown | ||
| 12 | from html_sanitizer import Sanitizer | ||
| 13 | from hashlib import sha256 | ||
| 14 | |||
| 15 | app = Flask(__name__) | ||
| 16 | app.config["SQLALCHEMY_DATABASE_URI"] = "postgresql://halfnarp@localhost:5432/halfnarp" | ||
| 17 | app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False | ||
| 18 | app.config["SERVER_NAME"] = "halfnarp.events.ccc.de" | ||
| 19 | app.config["SECRET_KEY"] = "<YOUR SERVER SECRET HERE>" | ||
| 20 | app.jinja_env.trim_blocks = True | ||
| 21 | app.jinja_env.lstrip_blocks = True | ||
| 22 | CORS(app) | ||
| 23 | |||
| 24 | db = SQLAlchemy(app) | ||
| 25 | |||
| 26 | |||
| 27 | class TalkPreference(db.Model): | ||
| 28 | """A preference of halfnarp frontend. An array of strings""" | ||
| 29 | |||
| 30 | uid = db.Column(db.String, primary_key=True) | ||
| 31 | public_uid = db.Column(db.String, index=True) | ||
| 32 | talk_ids = db.Column(db.String) | ||
| 33 | |||
| 34 | |||
| 35 | @app.route("/") | ||
| 36 | def root(): | ||
| 37 | return render_template("index.html") | ||
| 38 | |||
| 39 | |||
| 40 | @app.route("/-/talkpreferences", methods=["GET"]) | ||
| 41 | def sessions(): | ||
| 42 | return send_file("var/talks_local", mimetype="application/json") | ||
| 43 | |||
| 44 | |||
| 45 | @app.route("/-/talkpreferences/<uid>", methods=["GET"]) | ||
| 46 | def get_own_preferences(uid): | ||
| 47 | pref = db.session.get(TalkPreference, uid) | ||
| 48 | if pref == None: | ||
| 49 | abort(404) | ||
| 50 | |||
| 51 | return jsonify( | ||
| 52 | { | ||
| 53 | "hashed_uid": pref.public_uid, | ||
| 54 | "public_url": url_for( | ||
| 55 | "get_preferences", | ||
| 56 | public_uid=public_uid, | ||
| 57 | _external=True, | ||
| 58 | _scheme="https", | ||
| 59 | ), | ||
| 60 | "talk_ids": json.loads(pref.talk_ids), | ||
| 61 | "uid": pref.uid, | ||
| 62 | } | ||
| 63 | ) | ||
| 64 | |||
| 65 | |||
| 66 | @app.route("/-/talkpreferences/", methods=["POST"]) | ||
| 67 | def store_preferences(): | ||
| 68 | print(request.json) | ||
| 69 | try: | ||
| 70 | content = request.json | ||
| 71 | talk_ids = content["talk_ids"] | ||
| 72 | except: | ||
| 73 | abort(400) | ||
| 74 | |||
| 75 | if not all(isinstance(elem, str) for elem in talk_ids): | ||
| 76 | abort(400) | ||
| 77 | |||
| 78 | uid = str(uuid.uuid4()) | ||
| 79 | public_uid = str(sha256(uid.encode("utf-8")).hexdigest()) | ||
| 80 | |||
| 81 | db.session.add( | ||
| 82 | TalkPreference(uid=uid, public_uid=public_uid, talk_ids=json.dumps(talk_ids)) | ||
| 83 | ) | ||
| 84 | db.session.commit() | ||
| 85 | return jsonify( | ||
| 86 | { | ||
| 87 | "uid": uid, | ||
| 88 | "hashed_uid": str(public_uid), | ||
| 89 | "public_url": url_for( | ||
| 90 | "get_preferences", | ||
| 91 | public_uid=public_uid, | ||
| 92 | _external=True, | ||
| 93 | _scheme="https", | ||
| 94 | ), | ||
| 95 | "update_url": url_for( | ||
| 96 | "update_preferences", uid=uid, _external=True, _scheme="https" | ||
| 97 | ), | ||
| 98 | } | ||
| 99 | ) | ||
| 100 | |||
| 101 | |||
| 102 | @app.route("/-/talkpreferences/<uid>", methods=["POST", "PUT"]) | ||
| 103 | def update_preferences(uid): | ||
| 104 | pref = db.session.get(TalkPreference, uid) | ||
| 105 | if pref == None: | ||
| 106 | abort(404) | ||
| 107 | |||
| 108 | content = request.json | ||
| 109 | pref.talk_ids = json.dumps(content["talk_ids"]) | ||
| 110 | db.session.commit() | ||
| 111 | |||
| 112 | return jsonify({"uid": pref.uid, "hashed_uid": pref.public_uid}) | ||
| 113 | |||
| 114 | |||
| 115 | @app.route("/-/talkpreferences/public/<public_uid>", methods=["GET"]) | ||
| 116 | def get_preferences(public_uid): | ||
| 117 | pref = ( | ||
| 118 | db.session.query(TalkPreference) | ||
| 119 | .filter(TalkPreference.public_uid == public_uid) | ||
| 120 | .first() | ||
| 121 | ) | ||
| 122 | if pref == None: | ||
| 123 | abort(404) | ||
| 124 | |||
| 125 | return jsonify({"hash": pref.public_uid, "talk_ids": json.loads(pref.talk_ids)}) | ||
| 126 | |||
| 127 | |||
| 128 | def filter_keys_halfnarp(session): | ||
| 129 | abstract_html = markdown.markdown(submission["abstract"], enable_attributes=False) | ||
| 130 | abstract_clean_html = Sanitizer().sanitize(abstract_html) | ||
| 131 | slot = submission["slot"] | ||
| 132 | |||
| 133 | return { | ||
| 134 | "title": submission.get("title", "!!! NO TITLE !!!"), | ||
| 135 | "duration": 60 * submission.get("duration", 40), | ||
| 136 | "event_id": submission["code"], | ||
| 137 | "language": submission.get("content_locale", "de"), | ||
| 138 | "track_id": submission["track_id"], | ||
| 139 | "speaker_names": ", ".join( | ||
| 140 | [ | ||
| 141 | speaker.get("name", "unnamed") | ||
| 142 | for speaker in submission.get("speakers", {}) | ||
| 143 | ] | ||
| 144 | ), | ||
| 145 | "abstract": abstract_clean_html, | ||
| 146 | "room_id": slot.get("room_id", "room_unknown"), | ||
| 147 | "start_time": slot.get("start", "1970-01-01"), | ||
| 148 | } | ||
| 149 | |||
| 150 | |||
| 151 | def filter_keys_fullnarp(session, speakers): | ||
| 152 | abstract_html = markdown.markdown(session["abstract"], enable_attributes=False) | ||
| 153 | abstract_clean_html = Sanitizer().sanitize(abstract_html) | ||
| 154 | slot = submission["slot"] | ||
| 155 | |||
| 156 | speaker_info = [] | ||
| 157 | for speaker in submission.get("speakers", {}): | ||
| 158 | speaker_info.append(speakers[speaker["code"]]) | ||
| 159 | # if len(speakers[speaker['code']]['availabilities']) == 0: | ||
| 160 | # print ( "Track " + str(submission['track_id']) + ": Speaker " + speaker.get('name', 'unname') + " on session: " + session.get('title', '!!! NO TITLE !!!') + " without availability. https://cfp.cccv.de/orga/event/38c3/submissions/" + session['code'] ) | ||
| 161 | |||
| 162 | """This fixes availabilites ranging to or from exactly midnight to the more likely start of availibility at 8am and end of availibility at 3am""" | ||
| 163 | |||
| 164 | for avail in speakers[speaker["code"]]["availabilities"]: | ||
| 165 | start_new = datetime.fromisoformat(avail["start"]) | ||
| 166 | end_new = datetime.fromisoformat(avail["end"]) | ||
| 167 | |||
| 168 | start = datetime.fromisoformat(avail["start"]) | ||
| 169 | end = datetime.fromisoformat(avail["end"]) | ||
| 170 | if start.time() == time(0, 0): | ||
| 171 | start_new = start + timedelta(hours=10) | ||
| 172 | if end.time() == time(0, 0): | ||
| 173 | end_new = end + timedelta(hours=3) | ||
| 174 | |||
| 175 | if start != start_new or end != end_new: | ||
| 176 | print( | ||
| 177 | "Fixing " | ||
| 178 | + str(start) | ||
| 179 | + " - " | ||
| 180 | + str(end) | ||
| 181 | + " to " | ||
| 182 | + str(start_new) | ||
| 183 | + " - " | ||
| 184 | + str(end_new) | ||
| 185 | ) | ||
| 186 | avail["start"] = str(start_new) | ||
| 187 | avail["end"] = str(end_new) | ||
| 188 | |||
| 189 | return { | ||
| 190 | "title": submission.get("title", "!!! NO TITLE !!!"), | ||
| 191 | "duration": 60 * submission.get("duration", 40), | ||
| 192 | "event_id": submission["code"], | ||
| 193 | "language": submission.get("content_locale", "de"), | ||
| 194 | "track_id": submission["track_id"], | ||
| 195 | "speaker_names": ", ".join( | ||
| 196 | [ | ||
| 197 | speaker.get("name", "unnamed") | ||
| 198 | for speaker in submission.get("speakers", {}) | ||
| 199 | ] | ||
| 200 | ), | ||
| 201 | "abstract": abstract_clean_html, | ||
| 202 | "room_id": slot.get("room_id", "room_unknown"), | ||
| 203 | "start_time": slot.get("start", "1970-01-01"), | ||
| 204 | } | ||
| 205 | |||
| 206 | |||
| 207 | def fetch_talks(config): | ||
| 208 | sess = requests.Session() | ||
| 209 | |||
| 210 | response = sess.get( | ||
| 211 | config["pretalx-api-url"] + "/submissions/?format=json&limit=20000", | ||
| 212 | stream=True, | ||
| 213 | headers={"Authorization": "Token " + config["pretalx-token"]}, | ||
| 214 | ) | ||
| 215 | # with open('dump.txt', mode='wb') as localfile: | ||
| 216 | # localfile.write(response.content) | ||
| 217 | talks_json = json.loads(response.text) | ||
| 218 | |||
| 219 | response = sess.get( | ||
| 220 | config["pretalx-api-url"] + "/speakers/?format=json&limit=20000", | ||
| 221 | stream=True, | ||
| 222 | headers={"Authorization": "Token " + config["pretalx-token"]}, | ||
| 223 | ) | ||
| 224 | speakers_json = json.loads(response.text) | ||
| 225 | speakers = dict((speaker["code"], speaker) for speaker in speakers_json["results"]) | ||
| 226 | |||
| 227 | sessions = [ | ||
| 228 | filter_keys(submission) | ||
| 229 | for submission in talks_json["results"] | ||
| 230 | if submission["state"] == "confirmed" | ||
| 231 | and not "non-public" in submission.get("tags", {}) | ||
| 232 | ] | ||
| 233 | with open("var/talks_local", mode="w", encoding="utf8") as sessionsfile: | ||
| 234 | json.dump(sessions, sessionsfile) | ||
| 235 | |||
| 236 | sessions = [ | ||
| 237 | filter_keys_fullnarp(submission, speakers) | ||
| 238 | for submission in talks_json["results"] | ||
| 239 | if submission["state"] == "confirmed" or submission["state"] == "accepted" | ||
| 240 | ] | ||
| 241 | with open("var/talks_local_fullnarp", mode="w", encoding="utf8") as sessionsfile: | ||
| 242 | json.dump(sessions, sessionsfile) | ||
| 243 | |||
| 244 | |||
| 245 | def export_prefs(config): | ||
| 246 | print("[") | ||
| 247 | for preference in TalkPreference.query.all(): | ||
| 248 | print(preference.talk_ids + ",") | ||
| 249 | print("[]]") | ||
| 250 | |||
| 251 | |||
| 252 | if __name__ == "__main__": | ||
| 253 | parser = ArgumentParser(description="halfnarp2") | ||
| 254 | parser.add_argument( | ||
| 255 | "-i", | ||
| 256 | action="store_true", | ||
| 257 | dest="pretalx_import", | ||
| 258 | default=False, | ||
| 259 | help="import events from pretalx", | ||
| 260 | ) | ||
| 261 | parser.add_argument( | ||
| 262 | "-e", | ||
| 263 | action="store_true", | ||
| 264 | dest="fullnarp_export", | ||
| 265 | default=False, | ||
| 266 | help="export preferences to json", | ||
| 267 | ) | ||
| 268 | parser.add_argument( | ||
| 269 | "-c", "--config", help="Config file location", default="./config.json" | ||
| 270 | ) | ||
| 271 | args = parser.parse_args() | ||
| 272 | |||
| 273 | with open(args.config, mode="r", encoding="utf-8") as json_file: | ||
| 274 | config = json.load(json_file) | ||
| 275 | config["pretalx-api-url"] = ( | ||
| 276 | config["pretalx-url"] + "api/events/" + config["pretalx-conference"] | ||
| 277 | ) | ||
| 278 | |||
| 279 | with app.app_context(): | ||
| 280 | db.create_all() | ||
| 281 | if args.pretalx_import: | ||
| 282 | fetch_talks(config) | ||
| 283 | elif args.fullnarp_export: | ||
| 284 | export_prefs(config) | ||
| 285 | else: | ||
| 286 | app.run( | ||
| 287 | host=config.get("host", "127.0.0.1"), | ||
| 288 | port=int(config.get("port", "8080")), | ||
| 289 | ) | ||
