Archipelago/WebHostLib/__init__.py

198 lines
6.5 KiB
Python
Raw Normal View History

import os
import uuid
import base64
2020-12-04 23:43:18 +00:00
import socket
import jinja2.exceptions
2020-06-20 18:03:06 +00:00
from pony.flask import Pony
from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory
from flask_caching import Cache
2020-06-30 05:32:05 +00:00
from flask_compress import Compress
2020-06-20 18:03:06 +00:00
from .models import *
UPLOAD_FOLDER = os.path.relpath('uploads')
LOGS_FOLDER = os.path.relpath('logs')
os.makedirs(LOGS_FOLDER, exist_ok=True)
app = Flask(__name__)
2020-06-20 18:03:06 +00:00
Pony(app)
app.jinja_env.filters['any'] = any
app.jinja_env.filters['all'] = all
2020-07-10 15:42:22 +00:00
app.config["SELFHOST"] = True
app.config["GENERATORS"] = 8 # maximum concurrent world gens
2020-07-10 15:42:22 +00:00
app.config["SELFLAUNCH"] = True
app.config["DEBUG"] = False
app.config["PORT"] = 80
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 4 * 1024 * 1024 # 4 megabyte limit
2020-12-04 23:43:18 +00:00
# if you want to deploy, make sure you have a non-guessable secret key
app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
2020-09-08 23:41:37 +00:00
# at what amount of worlds should scheduling be used, instead of rolling in the webthread
app.config["JOB_THRESHOLD"] = 2
2020-06-20 18:03:06 +00:00
app.config['SESSION_PERMANENT'] = True
2020-07-27 02:05:42 +00:00
# waitress uses one thread for I/O, these are for processing of views that then get sent
2021-01-03 13:32:32 +00:00
# archipelago.gg uses gunicorn + nginx; ignoring this option
2020-07-27 02:05:42 +00:00
app.config["WAITRESS_THREADS"] = 10
2021-01-03 13:32:32 +00:00
# a default that just works. archipelago.gg runs on mariadb
2020-06-14 05:44:59 +00:00
app.config["PONY"] = {
'provider': 'sqlite',
'filename': os.path.abspath('ap.db3'),
2020-06-14 05:44:59 +00:00
'create_db': True
}
2020-08-02 20:11:52 +00:00
app.config["MAX_ROLL"] = 20
app.config["CACHE_TYPE"] = "simple"
2020-11-03 04:26:10 +00:00
app.config["JSON_AS_ASCII"] = False
2021-04-10 13:26:30 +00:00
app.config["PATCH_TARGET"] = "archipelago.gg"
2020-11-03 04:26:10 +00:00
cache = Cache(app)
2020-06-30 05:32:05 +00:00
Compress(app)
from werkzeug.routing import BaseConverter
class B64UUIDConverter(BaseConverter):
def to_python(self, value):
return uuid.UUID(bytes=base64.urlsafe_b64decode(value + '=='))
def to_url(self, value):
return base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii')
# short UUID
app.url_map.converters["suuid"] = B64UUIDConverter
app.jinja_env.filters['suuid'] = lambda value: base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii')
2020-06-20 20:48:38 +00:00
@app.before_request
2020-06-20 18:03:06 +00:00
def register_session():
session.permanent = True # technically 31 days after the last visit
if not session.get("_id", None):
session["_id"] = uuid4() # uniquely identify each session without needing a login
@app.errorhandler(404)
@app.errorhandler(jinja2.exceptions.TemplateNotFound)
def page_not_found(err):
return render_template('404.html'), 404
2021-06-15 00:35:40 +00:00
games_list = {
"zelda3": ("The Legend of Zelda: A Link to the Past",
"""
The Legend of Zelda: A Link to the Past is an action/adventure game. Take on the role of Link,
a boy who is destined to save the land of Hyrule. Delve through three palaces and nine dungeons on
your quest to rescue the descendents of the seven wise men and defeat the evil Ganon!"""),
"factorio": ("Factorio",
"""
Factorio is a game about automation. You play as an engineer who has crash landed on the planet
Nauvis, an inhospitable world filled with dangerous creatures called biters. Build a factory,
research new technologies, and become more efficient in your quest to build a rocket and return home.
"""),
"minecraft": ("Minecraft",
"""
Minecraft is a game about creativity. In a world made entirely of cubes, you explore, discover, mine,
craft, and try not to explode. Delve deep into the earth and discover abandoned mines, ancient
structures, and materials to create a portal to another world. Defeat the Ender Dragon, and claim
victory!""")
}
# Game sub-pages
@app.route('/games/<string:game>/<string:page>')
def game_pages(game, page):
return render_template(f"/games/{game}/{page}.html")
2021-06-15 02:27:43 +00:00
# Game landing pages
2021-06-15 00:35:40 +00:00
@app.route('/games/<game>')
def game_page(game):
2021-06-15 02:27:43 +00:00
return render_template(f"/games/{game}/{game}.html")
2021-06-15 02:27:43 +00:00
# List of supported games
@app.route('/games')
def games():
return render_template("games/games.html", games_list=games_list)
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
def tutorial(game, file, lang):
return render_template("tutorial.html", game=game, file=file, lang=lang)
@app.route('/tutorial')
def tutorial_landing():
return render_template("tutorialLanding.html")
2020-12-03 23:27:32 +00:00
@app.route('/weighted-settings')
2021-06-15 02:27:43 +00:00
def weighted_settings():
2020-12-03 23:27:32 +00:00
return render_template("weightedSettings.html")
@app.route('/seed/<suuid:seed>')
def viewSeed(seed: UUID):
2020-06-20 18:03:06 +00:00
seed = Seed.get(id=seed)
if not seed:
abort(404)
return render_template("viewSeed.html", seed=seed,
rooms=[room for room in seed.rooms if room.owner == session["_id"]])
2020-06-20 18:03:06 +00:00
@app.route('/new_room/<suuid:seed>')
def new_room(seed: UUID):
2020-06-20 18:03:06 +00:00
seed = Seed.get(id=seed)
if not seed:
abort(404)
room = Room(seed=seed, owner=session["_id"], tracker=uuid4())
2020-06-20 18:03:06 +00:00
commit()
return redirect(url_for("hostRoom", room=room.id))
2020-06-20 18:03:06 +00:00
def _read_log(path: str):
2020-06-14 06:11:56 +00:00
if os.path.exists(path):
2020-06-22 01:53:00 +00:00
with open(path, encoding="utf-8-sig") as log:
2020-06-14 06:11:56 +00:00
yield from log
else:
yield f"Logfile {path} does not exist. " \
f"Likely a crash during spinup of multiworld instance or it is still spinning up."
@app.route('/log/<suuid:room>')
def display_log(room: UUID):
# noinspection PyTypeChecker
2020-06-20 18:03:06 +00:00
return Response(_read_log(os.path.join("logs", str(room) + ".txt")), mimetype="text/plain;charset=UTF-8")
@app.route('/room/<suuid:room>', methods=['GET', 'POST'])
def hostRoom(room: UUID):
2020-06-20 18:03:06 +00:00
room = Room.get(id=room)
2020-06-23 00:50:07 +00:00
if room is None:
return abort(404)
2020-06-20 18:03:06 +00:00
if request.method == "POST":
if room.owner == session["_id"]:
cmd = request.form["cmd"]
Command(room=room, commandtext=cmd)
commit()
2020-06-14 05:44:59 +00:00
2020-07-10 15:42:22 +00:00
with db_session:
room.last_activity = datetime.utcnow() # will trigger a spinup, if it's not already running
return render_template("hostRoom.html", room=room)
@app.route('/favicon.ico')
def favicon():
return send_from_directory(os.path.join(app.root_path, 'static/static'),
'favicon.ico', mimetype='image/vnd.microsoft.icon')
2020-08-02 20:11:52 +00:00
from WebHostLib.customserver import run_server_process
from . import tracker, upload, landing, check, generate, downloads, api # to trigger app routing picking up on it
2020-12-03 23:27:32 +00:00
app.register_blueprint(api.api_endpoints)