from __future__ import annotations import html from urllib.parse import parse_qs from twisted.web import resource from DB import Database from hatena import client_ip, request_args from reximemo import ppm_thumbnail_png from server_config import load_config from web_ui import page_number, pagination, protect_forms, valid_csrf from web_features import WebFeatures, staff def _fields(request): if request.args and any(key in request.args for key in (b"csrf", "csrf")): return {(key.decode("utf-8", "replace") if isinstance(key, bytes) else key): (values[-1].decode("utf-8", "replace") if isinstance(values[-1], bytes) else values[-1]) for key, values in request.args.items() if values and key not in {b"logo", "logo"}} try: raw = request.content.read(131073).decode("utf-8", "replace") except Exception: raw = "" return {k: v[-1] if v else "" for k, v in parse_qs(raw, keep_blank_values=True).items()} def _redirect(request, path): request.setResponseCode(303) request.setHeader(b"Location", str(path).encode("utf-8")) return b"" def _fmt_time(ts): import datetime try: return datetime.datetime.fromtimestamp(int(ts)).strftime("%Y-%m-%d %H:%M") except Exception: return "" class WebRoot(WebFeatures, resource.Resource): isLeaf = True def __init__(self, docs_enabled=False): super().__init__() self.docs_enabled = bool(docs_enabled) def _account(self, request): return Database.account_for_ip(client_ip(request)) def _page(self, request, title, body, *, account=None, active=""): config = load_config() account = account if account is not None else self._account(request) browse_class = " active" if active == "browse" else "" home_class = " active" if active == "home" else "" account_class = " active" if active == "account" else "" admin_class = " active" if active == "admin" else "" about_class = " active" if active == "about" else "" docs_class = " active" if active == "docs" else "" nav_html = ( f'Home' f'Browse' 'ChannelsSearch' f'About' ) if self.docs_enabled: nav_html += f'Docs' if account: actions = f'{html.escape(account["username"])}' if int(account.get("is_admin") or 0): actions += f'Admin' else: actions = 'Sign in' if config["registration_enabled"]: actions += 'Create account' site_name = html.escape(config["site_name"]) full_title = site_name if title == "Home" else f"{html.escape(title)} · {site_name}" docs_footer = 'Docs' if self.docs_enabled else "" page = f"""
{html.escape(config["welcome_message"])}
No Flipnotes have been posted yet.
' return self._page(request, "Home", body, account=account, active="home") if path == "/about": body = ( 'Open-source projects and community work used by RexiMemo OSE.
RexiMemo OSE builds on earlier Flipnote preservation and replacement-server work. The projects below are included directly or used by the server.
' 'The original open-source Flipnote Hatena replacement server and the main foundation for the older public server code.
Utilities for PPM/TMB, UGO, NTFT and related Nintendo DSi and Flipnote formats. The bundled copy has been ported to Python 3.
Browser-side PPM decoding, animation rendering and audio playback on Flipnote pages.
The language and runtime used by RexiMemo OSE.
The networking and HTTP framework used by the server.
Used by the legacy Flipnote media and format tools.
Used for image decoding and thumbnail generation.
Credit also goes to the Flipnote preservation and reverse-engineering community, including the people who documented Flipnote formats, DSi image formats, UGO menus, audio codecs and the original service's network behaviour.
" 'The Hatenatools sources included in this repository also credit Steven, Remark, JSAfive, Austin Burk, Midmad and WDLmaster for format research and sample files.
' 'RexiMemo is an independent project and is not affiliated with Nintendo or Hatena. Third-party software and names remain the property of their respective owners.
' ) return self._page(request, "About", body, account=account, active="about") if path == "/docs" and self.docs_enabled: from docs_page import render_docs return self._page(request, "Documentation", render_docs(), account=account, active="docs") if path.startswith("/thumb/") and path.endswith(".png"): public_id = path[len("/thumb/"):-4] fn = Database.get_flipnote_by_public_id(public_id, include_hidden=include_hidden) if not fn: request.setResponseCode(404); return b"" try: first = request_args(request).get("first", [""])[0] == "1" data = ppm_thumbnail_png(Database.GetFlipnotePPM(fn["creator_fsid"], fn["filename"], include_hidden=include_hidden), (320, 240), first_frame=first) except Exception: request.setResponseCode(500); return b"" request.setHeader(b"content-type", b"image/png") request.setHeader(b"Cache-Control", b"private, no-store") return data if path.startswith("/comment-thumb/") and path.endswith(".png"): raw_id = path[len("/comment-thumb/"):-4] try: comment_id = int(raw_id) except ValueError: request.setResponseCode(404); return b"" raw = Database.comment_ppm(comment_id, include_hidden=include_hidden) if raw is None: request.setResponseCode(404); return b"" try: data = ppm_thumbnail_png(raw, (256, 192)) except Exception: request.setResponseCode(500); return b"" request.setHeader(b"content-type", b"image/png") request.setHeader(b"Cache-Control", b"private, no-store") return data if path.startswith("/media/") and path.endswith(".ppm"): public_id = path[len("/media/"):-4] fn = Database.get_flipnote_by_public_id(public_id, include_hidden=include_hidden) if not fn: request.setResponseCode(404); return b"" request.setHeader(b"content-type", b"application/octet-stream") request.setHeader(b"content-disposition", b"inline") request.setHeader(b"Cache-Control", b"private, no-store") return Database.GetFlipnotePPM(fn["creator_fsid"], fn["filename"], include_hidden=include_hidden) if path.startswith("/flipnote/") and path.endswith(".ppm"): public_id = path[len("/flipnote/"):-4] fn = Database.get_flipnote_by_public_id(public_id, include_hidden=include_hidden) if not fn: request.setResponseCode(404); return b"" Database.AddDownload(fn["creator_fsid"], fn["filename"]) request.setHeader(b"content-type", b"application/octet-stream") request.setHeader(b"content-disposition", (f'attachment; filename="{fn["filename"]}.ppm"').encode("ascii", "ignore")) request.setHeader(b"Cache-Control", b"private, no-store") return Database.GetFlipnotePPM(fn["creator_fsid"], fn["filename"], include_hidden=include_hidden) if path.startswith("/watch/"): public_id = path[len("/watch/"):].strip("/") fn = Database.get_flipnote_by_public_id(public_id, include_hidden=include_hidden) if not fn: request.setResponseCode(404) return self._page(request, "Not found", 'Hidden by moderation. Only administrators can see this comment.
' + content if staff(account) or Database.owns_comment(account["id"] if account else None, client_ip(request), c): content += f'' rendered_comments.append( 'Sign in to leave a comment.
' comment_form += pagination(comment_page, comment_total, 25, "/watch/" + public_id) related_html = "".join(self._flipnote_card(r) for r in related) body = ( 'No comments yet.
', "related_section": ( '' + html.escape(fn["description"]) + '
This Flipnote is hidden. Only administrators can see it.
' + controls if controls: body = 'OSE login state is tied to your IP address.
' 'Account
%s
' % html.escape(str(exc)), account=None) return _redirect(request, "/account") if path.startswith("/watch/") and path.endswith("/comment"): if not account and not load_config()["guest_posting"]: return self._error(request, "Sign in to leave a comment.", 403) public_id = path[len("/watch/"):-len("/comment")].strip("/") fn = Database.get_flipnote_by_public_id(public_id) if not fn: request.setResponseCode(404); return b"" try: Database.add_text_comment(fn["id"], fields.get("text", ""), ip_address=ip, account_id=(account["id"] if account else None)) except ValueError: pass return _redirect(request, "/watch/" + public_id) if path.startswith("/admin/"): if not account or not int(account.get("is_admin") or 0): request.setResponseCode(403); return b"" if path == "/admin/ban": try: Database.ban_ip(fields.get("ip", ""), fields.get("reason", "")) except ValueError: pass elif path == "/admin/unban": Database.unban_ip(fields.get("ip", "")) elif path == "/admin/delete": try: Database.delete_flipnote(int(fields.get("id", "0"))) except ValueError: pass return _redirect(request, "/admin") request.setResponseCode(404) return b""