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 def _fields(request): try: raw = request.content.read().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(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=""): 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' 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 inCreate account' full_title = "RexiMemo OSE" if title == "Home" else f"{html.escape(title)} · RexiMemo OSE" docs_footer = 'Docs' if self.docs_enabled else "" page = f"""
A small Flipnote Hatena-compatible server.
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 == "/browse": sort = request_args(request).get("sort", ["new"])[0] if sort not in {"new", "popular", "top"}: sort = "new" rows = Database.list_flipnotes(sort=sort, limit=100) body = ( 'Flipnotes posted to this server.
No Flipnotes found.
' return self._page(request, "Browse", body, account=account, active="browse") if path.startswith("/thumb/") and path.endswith(".png"): public_id = path[len("/thumb/"):-4] fn = Database.get_flipnote_by_public_id(public_id) if not fn: request.setResponseCode(404); return b"" try: data = ppm_thumbnail_png(Database.GetFlipnotePPM(fn["creator_fsid"], fn["filename"]), (320, 240)) except Exception: request.setResponseCode(500); return b"" request.setHeader(b"content-type", b"image/png") request.setHeader(b"Cache-Control", b"public, max-age=3600") 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) 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"public, max-age=3600") 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) 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"public, max-age=3600") return Database.GetFlipnotePPM(fn["creator_fsid"], fn["filename"]) if path.startswith("/flipnote/") and path.endswith(".ppm"): public_id = path[len("/flipnote/"):-4] fn = Database.get_flipnote_by_public_id(public_id) 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")) return Database.GetFlipnotePPM(fn["creator_fsid"], fn["filename"]) if path.startswith("/watch/"): public_id = path[len("/watch/"):].strip("/") fn = Database.get_flipnote_by_public_id(public_id) if not fn: request.setResponseCode(404) return self._page(request, "Not found", 'No comments yet.
', "related_section": ( '%s · Room created when this creator first posted a Flipnote.
OSE login state is tied to your IP address.
' 'Account
%sIP bans and Flipnote deletion.
| IP | Reason |
|---|
| Title | Creator |
|---|
%s
' % html.escape(str(exc)), account=None) return _redirect(request, "/account") if path.startswith("/watch/") and path.endswith("/comment"): 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""