import math import os from datetime import datetime from urllib.parse import quote, unquote import jinja2 from jetforce import Request, Response, Status, JetforceApplication from . import constants from .models import ( compute_verbose, compute_requested_results_page, GUS, process_seed_request, ) from gus.excludes import EXCLUDED_URL_PREFIXES, EXCLUDED_URL_PATHS TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "templates") template_env = jinja2.Environment( loader=jinja2.FileSystemLoader(TEMPLATE_DIR), undefined=jinja2.StrictUndefined, trim_blocks=True, lstrip_blocks=True, ) def datetimeformat(value, format="%Y-%m-%d"): return value.strftime(format) template_env.filters["datetimeformat"] = datetimeformat def render_template(name: str, *args, **kwargs) -> str: """ Render a gemini directory using the Jinja2 template engine. """ return template_env.get_template(name).render(*args, **kwargs) app = JetforceApplication() gus = GUS() @app.route("/status", strict_trailing_slash=False) def status(request): return Response(Status.SUCCESS, "text/plain", "ok") @app.route("/robots.txt", strict_trailing_slash=False) def status(request): return Response(Status.SUCCESS, "text/plain", """User-agent: researcher User-agent: indexer User-agent: archiver Disallow: /v/search Disallow: /searce Disallow: /backlinks User-agent: * Disallow: /add-seed """) @app.route("/favicon.txt", strict_trailing_slash=False) def favicon(request): return Response(Status.SUCCESS, "text/plain", "🔭") @app.route("/add-seed", strict_trailing_slash=False) def add_seed(request): if request.query: process_seed_request(request.query) body = render_template("add_seed.gmi", seed_url=request.query) return Response(Status.SUCCESS, "text/gemini", body) else: return Response(Status.INPUT, "Gemini URL") @app.route("/statistics", strict_trailing_slash=False) def statistics(request): body = render_template( "statistics.gmi", statistics=gus.statistics, index_modification_time=gus.statistics["index_modification_time"] ) return Response(Status.SUCCESS, "text/gemini", body) @app.route("/statistics/historical/overall", strict_trailing_slash=False) def statistics(request): body = render_template( "statistics_historical_overall.gmi", statistics_historical_overall=gus.statistics_historical_overall[::-1], index_modification_time=gus.statistics["index_modification_time"] ) return Response(Status.SUCCESS, "text/gemini", body) @app.route("/known-hosts", strict_trailing_slash=False) def known_hosts(request): body = render_template( "known_hosts.gmi", known_hosts=gus.hosts, index_modification_time=gus.statistics["index_modification_time"] ) return Response(Status.SUCCESS, "text/gemini", body) @app.route("/newest-hosts", strict_trailing_slash=False) def newest_hosts(request): body = render_template( "newest_hosts.gmi", newest_hosts=gus.newest_hosts, index_modification_time=gus.statistics["index_modification_time"] ) return Response(Status.SUCCESS, "text/gemini", body) @app.route("/newest-pages", strict_trailing_slash=False) def newest_pages(request): body = render_template( "newest_pages.gmi", newest_pages=gus.newest_pages, index_modification_time=gus.statistics["index_modification_time"] ) return Response(Status.SUCCESS, "text/gemini", body) @app.route("/known-feeds", strict_trailing_slash=False) def known_feeds(request): body = render_template( "known_feeds.gmi", known_feeds=gus.feeds, index_modification_time=gus.statistics["index_modification_time"] ) return Response(Status.SUCCESS, "text/gemini", body) @app.route("", strict_trailing_slash=False) def index(request): body = render_template( "index.gmi", index_modification_time=gus.statistics["index_modification_time"] ) return Response(Status.SUCCESS, "text/gemini", body) @app.route("/about", strict_trailing_slash=False) def index(request): body = render_template( "about.gmi", index_modification_time=gus.statistics["index_modification_time"] ) return Response(Status.SUCCESS, "text/gemini", body) @app.route("/documentation/searching", strict_trailing_slash=False) def documentation_searching(request): body = render_template( "documentation/searching.gmi", index_modification_time=gus.statistics["index_modification_time"] ) return Response(Status.SUCCESS, "text/gemini", body) @app.route("/documentation/indexing", strict_trailing_slash=False) def documentation_indexing(request): body = render_template( "documentation/indexing.gmi", index_modification_time=gus.statistics["index_modification_time"] ) return Response(Status.SUCCESS, "text/gemini", body) @app.route("/documentation/filters", strict_trailing_slash=False) def documentation_filters(request): body = render_template( "documentation/filters.gmi", index_modification_time=gus.statistics["index_modification_time"], filters=sorted(EXCLUDED_URL_PREFIXES) ) return Response(Status.SUCCESS, "text/gemini", body) @app.route("/documentation/backlinks", strict_trailing_slash=False) def documentation_backlinks(request): body = render_template( "documentation/backlinks.gmi", index_modification_time=gus.statistics["index_modification_time"] ) return Response(Status.SUCCESS, "text/gemini", body) @app.route("/news", strict_trailing_slash=False) def index(request): body = render_template( "news.gmi", index_modification_time=gus.statistics["index_modification_time"] ) return Response(Status.SUCCESS, "text/gemini", body) @app.route("/v/search(/\d+)?") @app.route("/search(/\d+)?") def search(request): if request.query: verbose = compute_verbose(request.path) requested_page = compute_requested_results_page(request.path) num_results, results = gus.search_index(request.query, requested_page) #if num_results > 0: num_pages = math.ceil(num_results / 10) current_page = min(requested_page, num_pages) if num_results == 0: current_page = 0 body = render_template( "search.gmi", query=request.query, quoted_query=quote(request.query), verbose=verbose, num_results=num_results, results=results, current_page=current_page, num_pages=num_pages, index_modification_time=gus.statistics["index_modification_time"] ) #else: # search_suggestions = gus.get_search_suggestions(request.query) # body = render_template( # "search_suggestions.gmi", # query=request.query, # search_suggestions=search_suggestions, # index_modification_time=gus.statistics["index_modification_time"] # ) return Response(Status.SUCCESS, "text/gemini", body) else: return Response(Status.INPUT, "Search query") @app.route("/search/jump/(?P.+)") @app.route("/v/search/jump/(?P.+)") def search_jump(request, query): if request.query: if request.query.isdigit(): jump_page = request.query else: jump_page = 1 verbose_segment = "/v" if request.path.startswith("/v") else "" jump_url = f"{verbose_segment}/search/{jump_page}?{query}" return Response(Status.REDIRECT_TEMPORARY, jump_url) else: return Response(Status.INPUT, "Jump to page") @app.route("/backlinks") def backlinks(request): if request.query: url = unquote(request.query) if not url.startswith("gemini://"): url = "gemini://{}".format(url) internal_backlinks, external_backlinks = gus.get_backlinks(url) body = render_template( "backlinks.gmi", url=url, internal_backlinks=internal_backlinks, external_backlinks=external_backlinks, index_modification_time=gus.statistics["index_modification_time"] ) return Response(Status.SUCCESS, "text/gemini", body) else: return Response(Status.INPUT, "Gemini URL")