Files
rekushi 807df47534 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.
2026-10-29 04:32:40 +00:00

247 lines
11 KiB
Python

from __future__ import annotations
import html
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_text_html, dsi_url, forward, html_page, keyboard_value, page_number, pagination_html, posting_account, read_ppm_upload
class PyResource(resource.Resource):
isLeaf = False
def getChild(self, name, request):
fsid = as_text(name).upper()
return CreatorNode(fsid) if Database.CreatorExists(fsid) else NotFound
def render(self, request):
request.setResponseCode(403); return b""
class CreatorNode(resource.Resource):
isLeaf = False
def __init__(self, fsid):
super().__init__(); self.fsid = fsid
def getChild(self, name, request):
text = as_text(name)
if "." in text:
base, ext = text.rsplit(".", 1)
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)
return NotFound
class FlipnoteActionNode(resource.Resource):
isLeaf = False
def __init__(self, fsid, filename):
super().__init__(); self.fsid = fsid; self.filename = filename
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
class TextComment(resource.Resource):
isLeaf = True
def __init__(self, fsid, filename):
super().__init__(); self.fsid = fsid; self.filename = filename
def render_POST(self, request):
fn = Database.get_flipnote(self.fsid, self.filename)
if not fn:
return NotFound.render(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),
account_id=(account["id"] if account else None),
)
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 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):
super().__init__(); self.fsid = fsid; self.filename = filename; self.ext = ext
def render_GET(self, request):
fn = Database.get_flipnote(self.fsid, self.filename)
if not fn:
return NotFound.render(request)
if self.ext == "ppm":
Database.AddView(self.fsid, self.filename)
request.setHeader(b"content-type", b"application/octet-stream")
return Database.GetFlipnotePPM(self.fsid, self.filename)
if self.ext == "tmb":
request.setHeader(b"content-type", b"application/octet-stream")
return Database.GetFlipnoteTMB(self.fsid, self.filename)
if self.ext == "info":
request.setHeader(b"content-type", b"text/plain")
return b"0\n0\n"
if self.ext == "dl":
Database.AddDownload(self.fsid, self.filename)
return b"OK"
if self.ext == "star":
headers = request_headers(request)
try:
amount = int(headers.get("x-hatena-star-count", "1"))
except ValueError:
amount = 1
Database.AddStar(self.fsid, self.filename, amount)
return b"Success"
if self.ext == "htm":
return details_page(request, fn)
request.setResponseCode(405); return b""
def render_POST(self, request):
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":
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.")
try:
Database.add_memo_comment(
fn["id"], data, ip_address=client_ip(request),
account_id=(account["id"] if account else None),
)
except ValueError as exc:
return dialog(request, str(exc))
forward(request, dsi_url(request, f"movie/{self.fsid}/{self.filename}.htm"))
return b"OK"
request.setResponseCode(405); return b""
def details_page(request, fn):
try:
tmb = TMB().Read(Database.GetFlipnoteTMB(fn["creator_fsid"], fn["filename"]))
except Exception:
tmb = None
creator_name = fn.get("creator_name") or (getattr(tmb, "Username", "") if tmb else "Creator")
creator_url = dsi_url(request, f"creator/{fn['creator_room_id']}/profile.htm")
ppm_url = dsi_url(request, f"movie/{fn['creator_fsid']}/{fn['filename']}.ppm")
star_url = dsi_url(request, f"movie/{fn['creator_fsid']}/{fn['filename']}.star")
reply_url = dsi_url(request, f"movie/{fn['creator_fsid']}/{fn['filename']}.reply")
text_url = dsi_url(request, f"movie/{fn['creator_fsid']}/{fn['filename']}/keyboard.kbd")
extra = (
'<meta name="upperlink" content="%(ppm)s">'
'<meta name="savebutton" content="%(ppm)s">'
'<meta name="playcontrolbutton" content="">'
'<meta name="starbutton" content="%(star)s">'
'<meta name="commentbutton" content="%(reply)s">'
) % {
"ppm": html.escape(ppm_url, quote=True),
"star": html.escape(star_url, quote=True),
"reply": html.escape(reply_url, quote=True),
}
account = current_account(request)
owner = Database.owns_flipnote(account["id"] if account else None, client_ip(request), fn)
editing = ""
if owner:
extra += '<meta name="deletebutton" content="%s">' % html.escape(dsi_url(request, f"movie/{fn['creator_fsid']}/{fn['filename']}.delete"), quote=True)
editing = '<p><a href="%s">Edit title</a> | <a href="%s">Edit description</a></p>' % (
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":
npf = dsi_url(request, f"comment/{c['id']}.npf?w=128&h=64")
ppm = dsi_url(request, f"comment/{c['id']}.ppm")
content = '<a href="%s"><img src="%s" width="128" height="64"></a>' % (html.escape(ppm, quote=True), html.escape(npf, quote=True))
else:
content = dsi_text_html(c.get("text_content") or "", preserve_newlines=True)
comment_html.append(
'<div class="comment"><strong>%s</strong><br>%s</div><div class="hr"></div>' % (
dsi_text_html(c.get("creator_name") or "Guest"), content,
)
)
body = (
'<div class="title">%(title)s</div>'
'<table width="226" class="detail">'
'<tr><th>Creator</th><td><a href="%(creator_url)s">%(creator)s</a></td></tr>'
'<tr><th>Stars</th><td>%(stars)d</td></tr>'
'<tr><th>Views</th><td>%(views)d</td></tr>'
'</table>'
'%(description)s%(editing)s<p><a href="%(ppm)s">Play Flipnote</a></p>'
'<p><a href="%(text)s">Text Comment</a></p>'
'<div class="hr"></div><div class="subtitle">Comments (%(comment_count)d)</div>%(comments)s%(comment_pager)s'
) % {
"title": dsi_text_html(fn.get("title") or "Untitled Flipnote"),
"creator_url": html.escape(creator_url, quote=True),
"creator": dsi_text_html(creator_name),
"stars": int(fn.get("stars") or 0),
"views": int(fn.get("views") or 0),
"ppm": html.escape(ppm_url, quote=True),
"text": html.escape(text_url, quote=True),
"comments": "".join(comment_html) or '<div class="notice">No comments yet.</div>',
"comment_count": comment_total,
"comment_pager": pagination_html(request, f"movie/{fn['creator_fsid']}/{fn['filename']}.htm", comment_page, comment_total, 30, name="comments_page"),
"description": '<p>%s</p>' % 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)