def wrapped(request: Request, **kwargs) -> Response: if "REMOTE_USER" not in request.environ: msg = "Attach your client certificate to continue." return Response(Status.CLIENT_CERTIFICATE_REQUIRED, msg) if request.environ["TLS_CLIENT_AUTHORISED"]: # Old-style verified certificate serial_number = request.environ["TLS_CLIENT_SERIAL_NUMBER"] fingerprint = f"{serial_number:032X}" # Convert to hex else: # New-style self signed certificate fingerprint = typing.cast(str, request.environ["TLS_CLIENT_HASH_B64"]) cert = User.login(fingerprint) if cert is None: body = render_template( "register.gmi", request=request, fingerprint=fingerprint, cert=request.environ["client_certificate"], ) return Response(Status.SUCCESS, "text/gemini", body) request = AuthenticatedRequest(request.environ, cert) response = func(request, **kwargs) return response
def docs(request, module, page=1): page = int(page) if module not in modules: return Response(Status.NOT_FOUND, f"Invalid module {module}") help_text_lines = modules[module].splitlines() page_count = math.ceil(len(help_text_lines) / paginate_by) page_count = max(page_count, 1) if page > page_count: return Response(Status.NOT_FOUND, "Invalid page number") offset = (page - 1) * paginate_by items = help_text_lines[offset:offset + paginate_by] lines = [f"[{module}]", f"(page {page} of {page_count})", ""] lines.extend(items) lines.append("") if page < page_count: lines.append(f"=>/docs/{module}/p/{page+1} Next {paginate_by} lines") if page > 1: lines.append(f"=>/docs/{module} Back to the beginning") return Response(Status.SUCCESS, "text/gemini", "\n".join(lines))
def pond_tribute_view(request, color: str): pond = Pond(request.user) if color not in pond.petal_map: return Response(Status.BAD_REQUEST, "Not Found") request.session["alert"] = pond.tribute(color) return Response(Status.REDIRECT_TEMPORARY, "/app/pond/")
def callback(request: AstrobotanyRequest, **kwargs): if "REMOTE_USER" not in request.environ: if request.path != "/app": # Redirect the user to the correct "path scope" first return Response(Status.REDIRECT_TEMPORARY, "/app") else: msg = "Attach your client certificate to continue." return Response(Status.CLIENT_CERTIFICATE_REQUIRED, msg) if request.environ["TLS_CLIENT_AUTHORISED"]: # Old-style verified certificate serial_number = request.environ["TLS_CLIENT_SERIAL_NUMBER"] fingerprint = f"{serial_number:032X}" # Convert to hex else: # New-style self signed certificate fingerprint = typing.cast(str, request.environ["TLS_CLIENT_HASH_B64"]) cert = User.login(fingerprint) if cert is None: body = render_template( "register.gmi", request=request, fingerprint=fingerprint, cert=request.environ["client_certificate"], ) return Response(Status.SUCCESS, "text/gemini", body) request = AstrobotanyRequest(request.environ, cert) request.plant.refresh() response = func(request, **kwargs) request.plant.save() return response
def synth_listen_view(request): song = request.user.get_song() if not song: return Response(Status.BAD_REQUEST, "You shouldn't be here!") synthesizer = Synthesizer.from_song(song) data = synthesizer.get_raw_data() return Response(Status.SUCCESS, "audio/ogg", data)
def mailbox_compose(request, postcard_id): postcard = items.Postcard.lookup(postcard_id) if postcard is None: return Response(Status.NOT_FOUND, "Postcard was not found") data = PostcardData.from_request(request) body = render_template("mailbox_compose.gmi", request=request, postcard=postcard, data=data) return Response(Status.SUCCESS, "text/gemini", body)
def mailbox_compose_subject(request, postcard_id): subject = request.query if not subject: return Response(Status.INPUT, f"Enter subject:") data = PostcardData.from_request(request) data.subject = subject return Response(Status.REDIRECT_TEMPORARY, f"/app/mailbox/outgoing/{postcard_id}")
def name(request): if not request.query: return Response(Status.INPUT, "Enter a new nickname for your plant:") request.plant.name = request.query[:40] msg = f'Your plant shall henceforth be known as "{request.plant.name}".' request.session["alert"] = msg return Response(Status.REDIRECT_TEMPORARY, "/app/plant")
def proxy_request(request): command = [b"w3m", b"-dump", request.url.encode()] try: out = subprocess.run(command, stdout=subprocess.PIPE) out.check_returncode() except Exception: return Response(Status.CGI_ERROR, "Failed to load URL") else: return Response(Status.SUCCESS, "text/plain", out.stdout)
def submit(request): if request.query: message = request.query[:256] created = datetime.now() ip_address = request.environ["REMOTE_HOST"] values = (ip_address, created, message) db.execute("INSERT INTO guestbook VALUES (?, ?, ?)", values) return Response(Status.REDIRECT_TEMPORARY, "") else: return Response(Status.INPUT, "Enter your message (max 256 characters)")
def submit(request): if request.query: message = request.query[:256] created = datetime.utcnow() with guestbook.open("a") as fp: fp.write(f"\n[{created:%Y-%m-%d %I:%M %p}]\n{message}\n") return Response(Status.REDIRECT_TEMPORARY, "") else: return Response(Status.INPUT, "Enter your message (max 256 characters)")
def synth_beat_view(request, beat_str: str): beat = int(beat_str) song = request.user.get_song() if not song: return Response(Status.BAD_REQUEST, "You shouldn't be here!") synthesizer = Synthesizer.from_song(song) body = request.render_template("synth_note.gmi", synthesizer=synthesizer, beat=beat) return Response(Status.SUCCESS, "text/gemini", body)
def mailbox_compose_line(request, postcard_id, line_number): line = request.query if not line: return Response( Status.INPUT, f"Enter message line {line_number} (or submit a blank space to erase):" ) data = PostcardData.from_request(request) data.lines[int(line_number) - 1] = line.rstrip() return Response(Status.REDIRECT_TEMPORARY, f"/app/mailbox/outgoing/{postcard_id}")
def mailbox_view(request, message_id): message = Inbox.get_or_none(id=message_id, user_to=request.user) if message is None: return Response(Status.BAD_REQUEST, "You shouldn't be here!") message.is_seen = True message.save() body = render_template("mailbox_view.gmi", request=request, message=message) return Response(Status.SUCCESS, "text/gemini", body)
def xmas_view(request): plant = request.user.plant plant.refresh() if not plant.can_use_christmas_cheer(): return Response(Status.BAD_REQUEST, "You shouldn't be here!") request.session["alert"] = plant.use_christmas_cheer() plant.save() return Response(Status.REDIRECT_TEMPORARY, "/app/plant")
def search_view(request): plant = request.user.plant plant.refresh() if not plant.can_search(): return Response(Status.BAD_REQUEST, "You shouldn't be here!") request.session["alert"] = plant.pick_petal() plant.save() return Response(Status.REDIRECT_TEMPORARY, "/app/plant")
def message_board_submit(request): if not request.query: return Response(Status.INPUT, "What would you like to say? ") rate_limit_resp = message_rate_limiter.check(request) if rate_limit_resp: return rate_limit_resp message = Message(user=request.user, text=request.query) message.save() return Response(Status.REDIRECT_TEMPORARY, "/app/message-board")
def visit_plant_water(request, user_id): user = User.get_or_none(user_id=user_id) if user is None: return Response(Status.NOT_FOUND, "User not found") elif request.user == user: return Response(Status.REDIRECT_TEMPORARY, "/app/plant") user.plant.refresh() request.session["alert"] = user.plant.water(request.user) user.plant.save() return Response(Status.REDIRECT_TEMPORARY, f"/app/visit/{user_id}")
def song_view(request): plant = request.user.plant plant.refresh() if not plant.can_play_song(): return Response(Status.BAD_REQUEST, "You shouldn't be here!") text = "You play the tune that you wrote for your plant." link = "=> /app/plant/song/audio.ogg Listen (download audio)" request.session["alert"] = f"{text}\n{link}" return Response(Status.REDIRECT_TEMPORARY, "/app/plant")
def visit_plant(request, user_id): user = User.get_or_none(user_id=user_id) if user is None: return Response(Status.NOT_FOUND, "User not found") elif request.user == user: return Response(Status.REDIRECT_TEMPORARY, "/app/plant") user.plant.refresh() user.plant.save() alert = request.session.pop("alert", None) body = render_template("visit_plant.gmi", request=request, plant=user.plant, alert=alert) return Response(Status.SUCCESS, "text/gemini", body)
def mailbox_preview(request, postcard_id): postcard = items.Postcard.lookup(postcard_id) if postcard is None: return Response(Status.NOT_FOUND, "Postcard was not found") data = PostcardData.from_request(request) if data.user is None: return Response(Status.BAD_REQUEST, "Cannot proceed without a user defined") if not data.subject: return Response(Status.BAD_REQUEST, "Cannot proceed without a subject defined") body = render_template("mailbox_preview.gmi", request=request, postcard=postcard, data=data) return Response(Status.SUCCESS, "text/gemini", body)
def plant_rename_view(request): plant = request.user.plant plant.refresh() if not request.query: return Response(Status.INPUT, "Enter a new nickname for your plant:") plant.name = request.query[:40] msg = f'Your plant shall henceforth be known as "{plant.name}".' request.session["alert"] = msg plant.save() return Response(Status.REDIRECT_TEMPORARY, "/app/plant")
def message_board(request, page=1): page = int(page) paginate_by = 10 page_count = int(math.ceil(Message.select().count() / paginate_by)) page_count = max(page_count, 1) if page > page_count: return Response(Status.NOT_FOUND, "Invalid page number") items = Message.by_date().paginate(page, paginate_by) body = render_template( "message_board.gmi", request=request, items=items, page=page, page_count=page_count, ) return Response(Status.SUCCESS, "text/gemini", body)
def visit_song_audio_view(request, user_id: str): user = User.get_or_none(User.user_id == user_id) if user is None: return Response(Status.NOT_FOUND, "Not Found") elif user == request.user: return Response(Status.REDIRECT_TEMPORARY, "/app/plant") song = user.get_song() if not song: return Response(Status.BAD_REQUEST, "You shouldn't be here!") synthesizer = Synthesizer.from_song(song) data = synthesizer.get_raw_data() return Response(Status.SUCCESS, "audio/ogg", data)
def synth_note_view(request, beat_str: str, note_str: str): beat = int(beat_str) note = int(note_str) song = request.user.get_song() if not song: return Response(Status.BAD_REQUEST, "You shouldn't be here!") data = song.get_data() data["notes"][beat] = Synthesizer.note_char_map[note] song.set_data(data) song.save() return Response(Status.REDIRECT_TEMPORARY, "/app/synth")
def settings_emoji_mode_view(request): if not request.query: prompt = f"Set emoji display mode (0/1/2): " return Response(Status.INPUT, prompt) answer = request.query.strip() if answer in ("0", "1", "2"): request.cert.emoji_mode = int(answer) request.cert.save() else: return Response(Status.BAD_REQUEST, f"Invalid query value: {request.query}") return Response(Status.REDIRECT_TEMPORARY, "/app/settings")
def mailbox_compose_item_view(request, postcard_id, item_id=None): postcard = items.Postcard.lookup(postcard_id) if postcard is None: return Response(Status.NOT_FOUND, "Postcard was not found") if item_id is None: item_slots = [slot for slot in request.user.inventory if slot.item.can_gift(request.user)] item_slots.sort(key=lambda x: x.item.name) body = request.render_template("mailbox_item.gmi", postcard=postcard, item_slots=item_slots) return Response(Status.SUCCESS, "text/gemini", body) data = PostcardData.from_request(request) data.item = items.Item.lookup(item_id) return Response(Status.REDIRECT_TEMPORARY, f"/app/mailbox/outgoing/{postcard_id}")
def register_existing(request, user_id=None): if "REMOTE_USER" not in request.environ: msg = "Attach your client certificate to continue." return Response(Status.CLIENT_CERTIFICATE_REQUIRED, msg) fingerprint = request.environ["TLS_CLIENT_HASH_B64"] if Certificate.select().where( Certificate.fingerprint == fingerprint).exists(): msg = "This certificate has already been linked to an account." return Response(Status.CERTIFICATE_NOT_AUTHORISED, msg) if user_id is None: username = request.query if not username: msg = "Enter your existing username" return Response(Status.INPUT, msg) try: user = User.select().where(User.username == username).get() except User.DoesNotExist: msg = f"No existing user was found with the name '{username}'." return Response(Status.BAD_REQUEST, msg) return Response(Status.REDIRECT_TEMPORARY, f"/app/register-existing/{user.id}") user = User.get_by_id(int(user_id)) if not user.password: msg = "Unable to add a certificate because this account does not have a password set." return Response(Status.BAD_REQUEST, msg) password = request.query if not password: msg = "Enter your password" return Response(Status.SENSITIVE_INPUT, msg) rate_limit_resp = password_failed_rate_limiter.check(request) if rate_limit_resp: return rate_limit_resp if not user.check_password(password): msg = "Invalid password, try again" return Response(Status.SENSITIVE_INPUT, msg) cert = request.environ["client_certificate"] Certificate.create( user=user, fingerprint=fingerprint, subject=cert.subject.rfc4514_string(), not_valid_before_utc=cert.not_valid_before, not_valid_after_utc=cert.not_valid_after, ) return Response(Status.REDIRECT_TEMPORARY, "/app")
def inventory_view(request, item_slot_id): item_slot_id = int(item_slot_id) try: item_slot = ItemSlot.get_by_id(item_slot_id) except ItemSlot.DoesNotExist: return Response(Status.NOT_FOUND, "Not Found") if not item_slot.user == request.user: return Response(Status.NOT_FOUND, "Not Found") description = item_slot.item.get_inventory_description(request.user) body = render_template( "inventory_view.gmi", request=request, item_slot=item_slot, description=description ) return Response(Status.SUCCESS, "text/gemini", body)
def visit_plant_search(request, user_id): user = User.get_or_none(user_id=user_id) if user is None: return Response(Status.NOT_FOUND, "User not found") elif request.user == user: return Response(Status.REDIRECT_TEMPORARY, "/app/plant") if user.plant.dead or user.plant.stage_str != "flowering": return Response(Status.BAD_REQUEST, "You shouldn't be here!") user.plant.refresh() request.session["alert"] = user.plant.pick_petal(request.user) user.plant.save() return Response(Status.REDIRECT_TEMPORARY, f"/app/visit/{user_id}")