From 807df47534e276fbecdbbaee94cf958581bbbd0a Mon Sep 17 00:00:00 2001 From: rekushi <1+rekushi@noreply.flipnote.world> Date: Thu, 29 Oct 2026 04:32:40 +0000 Subject: [PATCH] feat(dsi): add creator controls and comment pagination Allow owners to edit details or remove uploads, and paginate comments without changing original play/save responses. --- hatenadir/ds/v2-xx/movie.py | 85 +++++++++++++++++++++++++++++++------ 1 file changed, 73 insertions(+), 12 deletions(-) diff --git a/hatenadir/ds/v2-xx/movie.py b/hatenadir/ds/v2-xx/movie.py index 82df00b..99322bb 100644 --- a/hatenadir/ds/v2-xx/movie.py +++ b/hatenadir/ds/v2-xx/movie.py @@ -7,7 +7,7 @@ from twisted.web import resource from DB import Database from Hatenatools import TMB from hatena import NotFound, as_text, client_ip, request_headers -from reximemo import current_account, dialog, dsi_url, forward, html_page, keyboard_value +from reximemo import current_account, dialog, dsi_text_html, dsi_url, forward, html_page, keyboard_value, page_number, pagination_html, posting_account, read_ppm_upload class PyResource(resource.Resource): @@ -29,7 +29,7 @@ class CreatorNode(resource.Resource): text = as_text(name) if "." in text: base, ext = text.rsplit(".", 1) - if ext in {"ppm", "tmb", "info", "htm", "star", "dl", "reply"} and Database.FlipnoteExists(self.fsid, base): + if ext in {"ppm", "tmb", "info", "htm", "star", "dl", "reply", "delete"} and Database.FlipnoteExists(self.fsid, base): return FlipnoteFile(self.fsid, base, ext) if Database.FlipnoteExists(self.fsid, text): return FlipnoteActionNode(self.fsid, text) @@ -43,6 +43,8 @@ class FlipnoteActionNode(resource.Resource): def getChild(self, name, request): if as_text(name) == "keyboard.kbd": return TextComment(self.fsid, self.filename) + if as_text(name) in {"title.kbd", "description.kbd"}: + return MetadataKeyboard(self.fsid, self.filename, as_text(name).split(".", 1)[0]) return NotFound @@ -54,7 +56,9 @@ class TextComment(resource.Resource): fn = Database.get_flipnote(self.fsid, self.filename) if not fn: return NotFound.render(request) - account = current_account(request) + account, denied = posting_account(request) + if denied is not None: + return denied try: Database.add_text_comment( fn["id"], keyboard_value(request), ip_address=client_ip(request), @@ -68,6 +72,36 @@ class TextComment(resource.Resource): request.setResponseCode(405); return b"" +class MetadataKeyboard(resource.Resource): + isLeaf = True + + def __init__(self, fsid, filename, field): + super().__init__() + self.fsid, self.filename, self.field = fsid, filename, field + + def render_POST(self, request): + flipnote = Database.get_flipnote(self.fsid, self.filename) + if not flipnote: + return NotFound.render(request) + account = current_account(request) + if not Database.owns_flipnote(account["id"] if account else None, client_ip(request), flipnote): + return dialog(request, "You can only edit Flipnotes you posted.") + value = keyboard_value(request) + fields = {"title": flipnote["title"], "description": flipnote.get("description") or "", + "channel_id": flipnote.get("channel_id") or 1} + fields[self.field] = value + try: + Database.update_flipnote(flipnote["id"], **fields) + except ValueError as exc: + return dialog(request, str(exc)) + forward(request, dsi_url(request, f"movie/{self.fsid}/{self.filename}.htm")) + return b"OK" + + def render_GET(self, request): + request.setResponseCode(405) + return b"" + + class FlipnoteFile(resource.Resource): isLeaf = True def __init__(self, fsid, filename, ext): @@ -106,14 +140,26 @@ class FlipnoteFile(resource.Resource): fn = Database.get_flipnote(self.fsid, self.filename) if not fn: return NotFound.render(request) + if self.ext == "delete": + account = current_account(request) + if not Database.owns_flipnote(account["id"] if account else None, client_ip(request), fn): + return dialog(request, "You can only remove Flipnotes you posted.") + Database.delete_flipnote(fn["id"]) + forward(request, dsi_url(request, f"creator/{fn['creator_room_id']}/movies.uls")) + return b"OK" if self.ext == "reply": - data = request.content.read() + account, denied = posting_account(request) + if denied is not None: + return denied + try: + data = read_ppm_upload(request) + except ValueError as exc: + return dialog(request, str(exc)) tmb = TMB().Read(data) if not tmb: return dialog(request, "That mini Flipnote is invalid.") if int(getattr(tmb, "FrameCount", 1)) != 1: return dialog(request, "Mini Flipnote comments must contain one frame.") - account = current_account(request) try: Database.add_memo_comment( fn["id"], data, ip_address=client_ip(request), @@ -148,7 +194,18 @@ def details_page(request, fn): "star": html.escape(star_url, quote=True), "reply": html.escape(reply_url, quote=True), } - comments = Database.list_comments(fn["id"], limit=30) + account = current_account(request) + owner = Database.owns_flipnote(account["id"] if account else None, client_ip(request), fn) + editing = "" + if owner: + extra += '' % html.escape(dsi_url(request, f"movie/{fn['creator_fsid']}/{fn['filename']}.delete"), quote=True) + editing = '
' % ( + html.escape(dsi_url(request, f"movie/{fn['creator_fsid']}/{fn['filename']}/title.kbd"), quote=True), + html.escape(dsi_url(request, f"movie/{fn['creator_fsid']}/{fn['filename']}/description.kbd"), quote=True), + ) + comment_total = Database.count_comments(fn["id"]) + comment_page = page_number(request, comment_total, 30, "comments_page") + comments = Database.list_comments(fn["id"], limit=30, offset=(comment_page - 1) * 30) comment_html = [] for c in comments: if c["type"] == "memo": @@ -156,10 +213,10 @@ def details_page(request, fn): ppm = dsi_url(request, f"comment/{c['id']}.ppm") content = '%s
' % dsi_text_html(fn["description"], preserve_newlines=True) if fn.get("description") else "", + "editing": editing, } return html_page(request, fn.get("title") or "Flipnote", body, extra)
%s