[RELEASE] RexiMemo: Open Source Edition
This commit is contained in:
1 parent
5e2acc31c5
commit
a438dfc231
45 files changed
+8516
-169
No files matched your search
@@ -0,0 +1,20 @@
|
||||
from twisted.web import resource
|
||||
from DB import Database
|
||||
from reximemo import dsi_url, flipnote_buttons, make_menu, pagination_buttons, query_int
|
||||
|
||||
|
||||
class PyResource(resource.Resource):
|
||||
isLeaf = True
|
||||
def render_GET(self, request):
|
||||
page = query_int(request, "page", 1)
|
||||
total = Database.count_flipnotes()
|
||||
rows = Database.list_flipnotes(page=page, per_page=50)
|
||||
pager = pagination_buttons(request, "allflipnotes.uls", page, total)
|
||||
prev = [b for b in pager if b["label"].startswith("Previous")]
|
||||
nxt = [b for b in pager if b["label"].startswith("Next")]
|
||||
request.setHeader(b"content-type", b"text/plain")
|
||||
return make_menu(
|
||||
layout=(2, 1), title="Browse", subtitle_top="All Flipnotes",
|
||||
left="Flipnotes", right=str(total), buttons=prev + flipnote_buttons(request, rows) + nxt,
|
||||
corner={"label": "Post", "url": dsi_url(request, "ch/1.post")},
|
||||
)
|
||||
@@ -0,0 +1,52 @@
|
||||
from twisted.web import resource
|
||||
|
||||
from DB import Database
|
||||
from hatena import NotFound, as_text, client_ip
|
||||
from reximemo import current_account, dialog, dsi_url, flipnote_buttons, html_page, make_menu
|
||||
|
||||
|
||||
class PyResource(resource.Resource):
|
||||
isLeaf = False
|
||||
def getChild(self, name, request):
|
||||
text = as_text(name)
|
||||
base, dot, ext = text.partition(".")
|
||||
if not dot or not base.isdigit() or ext not in {"ugo", "uls", "htm", "post"}:
|
||||
return NotFound
|
||||
channel = Database.get_channel(int(base))
|
||||
return ChannelResource(int(base), ext) if channel else NotFound
|
||||
|
||||
def render(self, request):
|
||||
request.setResponseCode(403); return b""
|
||||
|
||||
|
||||
class ChannelResource(resource.Resource):
|
||||
isLeaf = True
|
||||
def __init__(self, channel_id, ext):
|
||||
super().__init__(); self.channel_id = channel_id; self.ext = ext
|
||||
|
||||
def render_GET(self, request):
|
||||
channel = Database.get_channel(self.channel_id)
|
||||
if self.ext in {"ugo", "uls"}:
|
||||
rows = Database.list_flipnotes(channel_id=self.channel_id, limit=50)
|
||||
request.setHeader(b"content-type", b"text/plain")
|
||||
return make_menu(
|
||||
layout=(2,1), title=channel["title"], subtitle_top="Channel", subtitle_bottom=channel["description"],
|
||||
left="Flipnotes", right=str(Database.count_flipnotes(channel_id=self.channel_id)),
|
||||
buttons=flipnote_buttons(request, rows), corner={"label":"Post", "url":dsi_url(request, f"ch/{self.channel_id}.post")},
|
||||
)
|
||||
if self.ext == "htm":
|
||||
return html_page(request, channel["title"], '<div class="title">%s</div><p>%s</p><p><a href="%s">Browse</a></p>' % (channel["title"], channel["description"], dsi_url(request, f"ch/{self.channel_id}.uls")))
|
||||
request.setResponseCode(405); return b""
|
||||
|
||||
def render_POST(self, request):
|
||||
if self.ext != "post":
|
||||
request.setResponseCode(405); return b""
|
||||
data = request.content.read()
|
||||
account = current_account(request)
|
||||
try:
|
||||
result = Database.AddFlipnote(data, str(self.channel_id), account["id"] if account else None, client_ip(request))
|
||||
except Exception:
|
||||
result = False
|
||||
if not result:
|
||||
return dialog(request, "Unable to post this Flipnote. It may already exist or be invalid.")
|
||||
return b""
|
||||
@@ -0,0 +1,38 @@
|
||||
from twisted.web import resource
|
||||
|
||||
from DB import Database
|
||||
from hatena import NotFound, as_text, request_args
|
||||
from reximemo import comment_npf
|
||||
|
||||
|
||||
class PyResource(resource.Resource):
|
||||
isLeaf = False
|
||||
def getChild(self, name, request):
|
||||
text = as_text(name)
|
||||
base, dot, ext = text.partition(".")
|
||||
if not dot or not base.isdigit() or ext not in {"npf", "ppm"}:
|
||||
return NotFound
|
||||
return CommentFile(int(base), ext) if Database.get_comment(int(base)) else NotFound
|
||||
|
||||
|
||||
class CommentFile(resource.Resource):
|
||||
isLeaf = True
|
||||
def __init__(self, cid, ext):
|
||||
super().__init__(); self.cid = cid; self.ext = ext
|
||||
def render_GET(self, request):
|
||||
if self.ext == "ppm":
|
||||
data = Database.comment_ppm(self.cid)
|
||||
if data is None:
|
||||
return NotFound.render(request)
|
||||
request.setHeader(b"content-type", b"application/octet-stream")
|
||||
return data
|
||||
args = request_args(request)
|
||||
try: w = max(1, min(256, int(args.get("w", ["128"])[0])))
|
||||
except ValueError: w = 128
|
||||
try: h = max(1, min(192, int(args.get("h", ["64"])[0])))
|
||||
except ValueError: h = 64
|
||||
data = comment_npf(self.cid, w, h)
|
||||
if data is None:
|
||||
return NotFound.render(request)
|
||||
request.setHeader(b"content-type", b"application/octet-stream")
|
||||
return data
|
||||
@@ -0,0 +1,61 @@
|
||||
import html
|
||||
from twisted.web import resource
|
||||
|
||||
from DB import Database
|
||||
from hatena import NotFound, as_text
|
||||
from reximemo import dsi_url, flipnote_buttons, html_page, make_menu
|
||||
|
||||
|
||||
class PyResource(resource.Resource):
|
||||
isLeaf = False
|
||||
def getChild(self, name, request):
|
||||
try:
|
||||
room_id = int(as_text(name))
|
||||
except ValueError:
|
||||
return NotFound
|
||||
return CreatorNode(room_id) if Database.get_creator_room(room_id) else NotFound
|
||||
|
||||
|
||||
class CreatorNode(resource.Resource):
|
||||
isLeaf = False
|
||||
def __init__(self, room_id):
|
||||
super().__init__(); self.room_id = room_id
|
||||
def getChild(self, name, request):
|
||||
text = as_text(name)
|
||||
aliases = {"room.ugo":"room.uls", "movies.ugo":"movies.uls"}
|
||||
text = aliases.get(text, text)
|
||||
if text in {"profile.htm", "room.uls", "movies.uls"}:
|
||||
return CreatorLeaf(self.room_id, text)
|
||||
return NotFound
|
||||
|
||||
|
||||
class CreatorLeaf(resource.Resource):
|
||||
isLeaf = True
|
||||
def __init__(self, room_id, action):
|
||||
super().__init__(); self.room_id = room_id; self.action = action
|
||||
|
||||
def render_GET(self, request):
|
||||
room = Database.get_creator_room(self.room_id)
|
||||
if not room:
|
||||
return NotFound.render(request)
|
||||
notes = Database.list_creator_flipnotes(self.room_id, limit=50)
|
||||
if self.action in {"room.uls", "movies.uls"}:
|
||||
request.setHeader(b"content-type", b"text/plain")
|
||||
return make_menu(
|
||||
layout=(2,1), title=room["display_name"], subtitle_top="Creator's Room",
|
||||
left="Flipnotes", right=str(len(notes)), buttons=flipnote_buttons(request, notes),
|
||||
corner={"label":"Post", "url":dsi_url(request, "ch/1.post")},
|
||||
)
|
||||
profile = (
|
||||
'<div class="title" align="center">%s</div>'
|
||||
'<div class="notice2" align="center">Creator\'s Room</div>'
|
||||
'<table width="226" class="detail">'
|
||||
'<tr><th>Flipnotes</th><td>%d</td></tr>'
|
||||
'<tr><th>Identity</th><td>%s</td></tr>'
|
||||
'</table><p><a href="%s">View Flipnotes</a></p>'
|
||||
) % (
|
||||
html.escape(room["display_name"]), len(notes),
|
||||
"Account" if room.get("account_id") else "Guest/IP",
|
||||
html.escape(dsi_url(request, f"creator/{self.room_id}/movies.uls"), quote=True),
|
||||
)
|
||||
return html_page(request, room["display_name"], profile)
|
||||
@@ -0,0 +1 @@
|
||||
RQBuAGcAbABpAHMAaAA= en
|
||||
|
@@ -0,0 +1,8 @@
|
||||
from twisted.web import resource
|
||||
from reximemo import make_menu
|
||||
|
||||
class PyResource(resource.Resource):
|
||||
isLeaf = True
|
||||
def render_GET(self, request):
|
||||
request.setHeader(b"content-type", b"text/plain")
|
||||
return make_menu(layout=(4,), title="Inbox", subtitle_bottom="Notifications are not included in OSE.", buttons=[])
|
||||
@@ -0,0 +1,30 @@
|
||||
from twisted.web import resource
|
||||
|
||||
from DB import Database
|
||||
from reximemo import current_account, dsi_url, flipnote_buttons, make_menu, placeholder_icon
|
||||
|
||||
|
||||
class PyResource(resource.Resource):
|
||||
isLeaf = True
|
||||
def render_GET(self, request):
|
||||
rows = Database.list_flipnotes(limit=8)
|
||||
account = current_account(request)
|
||||
buttons = [
|
||||
{"label": "Browse Flipnotes", "url": dsi_url(request, "allflipnotes.uls"), "icon_ntft": placeholder_icon("browse"), "embed_name": "browse.ntft"},
|
||||
{"label": "Account", "url": dsi_url(request, "sa/account.htm"), "icon_ntft": placeholder_icon("account"), "embed_name": "account.ntft"},
|
||||
]
|
||||
room = Database.room_for_account(account["id"]) if account else Database.room_for_ip(__import__('hatena').client_ip(request))
|
||||
if room:
|
||||
buttons.append({"label": "Creator's Room", "url": dsi_url(request, f"creator/{room['id']}/profile.htm"), "icon_ntft": placeholder_icon("room"), "embed_name": "room.ntft"})
|
||||
buttons.extend(flipnote_buttons(request, rows))
|
||||
request.setHeader(b"content-type", b"text/plain")
|
||||
return make_menu(
|
||||
layout=(2, 1),
|
||||
title="RexiMemo OSE",
|
||||
subtitle_top="Open Source Edition",
|
||||
subtitle_bottom="Proxy build",
|
||||
left="Recent",
|
||||
right=str(Database.count_flipnotes()),
|
||||
buttons=buttons,
|
||||
corner={"label": "Post", "url": dsi_url(request, "ch/1.post")},
|
||||
)
|
||||
@@ -0,0 +1,185 @@
|
||||
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_url, forward, html_page, keyboard_value
|
||||
|
||||
|
||||
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"} 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)
|
||||
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 = current_account(request)
|
||||
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 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 == "reply":
|
||||
data = request.content.read()
|
||||
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),
|
||||
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),
|
||||
}
|
||||
comments = Database.list_comments(fn["id"], limit=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 = html.escape(c.get("text_content") or "")
|
||||
comment_html.append(
|
||||
'<div class="comment"><strong>%s</strong><br>%s</div><div class="hr"></div>' % (
|
||||
html.escape(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>'
|
||||
'<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</div>%(comments)s'
|
||||
) % {
|
||||
"title": html.escape(fn.get("title") or "Untitled Flipnote"),
|
||||
"creator_url": html.escape(creator_url, quote=True),
|
||||
"creator": html.escape(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>',
|
||||
}
|
||||
return html_page(request, fn.get("title") or "Flipnote", body, extra)
|
||||
@@ -0,0 +1,58 @@
|
||||
import html
|
||||
from twisted.web import resource
|
||||
|
||||
from DB import Database
|
||||
from hatena import NotFound, as_text, client_ip
|
||||
from reximemo import current_account, dialog, dsi_url, forward, html_page, keyboard_value
|
||||
|
||||
|
||||
class PyResource(resource.Resource):
|
||||
isLeaf = False
|
||||
def getChild(self, name, request):
|
||||
action = as_text(name)
|
||||
if action in {"account.htm", "login.kbd", "register.kbd", "logout.htm"}:
|
||||
return AccountLeaf(action)
|
||||
return NotFound
|
||||
|
||||
|
||||
class AccountLeaf(resource.Resource):
|
||||
isLeaf = True
|
||||
def __init__(self, action):
|
||||
super().__init__(); self.action = action
|
||||
|
||||
def render_GET(self, request):
|
||||
account = current_account(request)
|
||||
if self.action == "logout.htm":
|
||||
Database.logout_ip(client_ip(request))
|
||||
forward(request, dsi_url(request, "index.ugo"))
|
||||
return b"OK"
|
||||
if self.action != "account.htm":
|
||||
request.setResponseCode(405); return b""
|
||||
if account:
|
||||
room = Database.room_for_account(account["id"])
|
||||
room_link = '<p><a href="%s">Creator\'s Room</a></p>' % html.escape(dsi_url(request, f"creator/{room['id']}/profile.htm"), quote=True) if room else '<p>Your Creator\'s Room is created after your first post.</p>'
|
||||
body = '<div class="title">Account</div><p>Signed in as <strong>%s</strong>.</p>%s<p><a href="%s">Sign out</a></p>' % (html.escape(account["username"]), room_link, html.escape(dsi_url(request, "sa/logout.htm"), quote=True))
|
||||
else:
|
||||
body = '<div class="title">Account</div><p>OSE uses one simple IP login. Enter <strong>username:password</strong> in the keyboard.</p><p><a href="%s">Sign in</a></p><p><a href="%s">Register</a></p>' % (html.escape(dsi_url(request, "sa/login.kbd"), quote=True), html.escape(dsi_url(request, "sa/register.kbd"), quote=True))
|
||||
return html_page(request, "Account", body)
|
||||
|
||||
def render_POST(self, request):
|
||||
if self.action not in {"login.kbd", "register.kbd"}:
|
||||
request.setResponseCode(405); return b""
|
||||
value = keyboard_value(request)
|
||||
if ":" not in value:
|
||||
return dialog(request, "Enter username:password")
|
||||
username, password = value.split(":", 1)
|
||||
if self.action == "login.kbd":
|
||||
account = Database.verify_account(username, password)
|
||||
if not account:
|
||||
return dialog(request, "Sign in failed.", 403)
|
||||
Database.login_ip(client_ip(request), account["id"])
|
||||
else:
|
||||
try:
|
||||
aid = Database.create_account(username, password)
|
||||
Database.login_ip(client_ip(request), aid)
|
||||
except ValueError as exc:
|
||||
return dialog(request, str(exc))
|
||||
forward(request, dsi_url(request, "sa/account.htm"))
|
||||
return b"OK"
|
||||
Reference in new issue
Block a user
