Working on post preview; file attachments
This commit is contained in:
parent
efb8645afa
commit
5bafb9116a
2 changed files with 308 additions and 59 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
|||
/__pycache__
|
||||
bubble.ini
|
||||
server.sh
|
||||
.certs/
|
||||
.vscode/
|
||||
|
|
365
50_bubble.py
365
50_bubble.py
|
@ -1,7 +1,9 @@
|
|||
"""Bubble - Bulletin Boards for Gemini"""
|
||||
|
||||
import datetime
|
||||
import mariadb
|
||||
import re
|
||||
import time
|
||||
import urllib.parse as urlparse
|
||||
|
||||
|
||||
|
@ -90,11 +92,26 @@ class Post:
|
|||
return self.title
|
||||
else:
|
||||
return '(untitled post)'
|
||||
|
||||
def ymd_date(self):
|
||||
dt = datetime.datetime.fromtimestamp(self.ts_created)
|
||||
return dt.strftime('%Y-%m-%d')
|
||||
|
||||
|
||||
class File:
|
||||
def __init__(self, id, segment, user, name, mimetype, data):
|
||||
self.id = id
|
||||
self.segment = segment
|
||||
self.user = user
|
||||
self.name = name
|
||||
self.mimetype = mimetype
|
||||
self.data = data
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self, cfg):
|
||||
self.cfg = cfg
|
||||
self.max_summary = 500
|
||||
self.conn = mariadb.connect(
|
||||
user=cfg.get('db.user'),
|
||||
password=cfg.get('db.password'),
|
||||
|
@ -118,6 +135,7 @@ class Database:
|
|||
db.execute("DROP TABLE IF EXISTS segments")
|
||||
db.execute("DROP TABLE IF EXISTS notifs")
|
||||
db.execute("DROP TABLE IF EXISTS follow")
|
||||
db.execute("DROP TABLE IF EXISTS files")
|
||||
|
||||
db.execute("""CREATE TABLE users (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
|
@ -195,6 +213,15 @@ class Database:
|
|||
UNIQUE KEY (user, type, target)
|
||||
)""")
|
||||
|
||||
db.execute("""CREATE TABLE files (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
segment INT,
|
||||
user INT NOT NULL,
|
||||
name VARCHAR(1000) DEFAULT '',
|
||||
mimetype VARCHAR(1000),
|
||||
data MEDIUMBLOB
|
||||
)""")
|
||||
|
||||
db.execute('INSERT INTO users (name, avatar, role) VALUES (?, ?, ?)',
|
||||
('admin', '🚀', User.ADMIN))
|
||||
db.execute('INSERT INTO subspaces (name, info, owner) VALUES (?, ?, 1)',
|
||||
|
@ -222,13 +249,47 @@ class Database:
|
|||
def destroy_user():
|
||||
pass
|
||||
|
||||
def create_file(self, user: User, name: str, mimetype: str, data: bytes) -> int:
|
||||
cur = self.conn.cursor()
|
||||
cur.execute("""
|
||||
INSERT INTO files (segment, user, name, mimetype, data)
|
||||
VALUES (?, ?, ?, ?, ?)""", (0, user.id, name, mimetype, data))
|
||||
self.commit()
|
||||
return cur.lastrowid
|
||||
|
||||
def get_file(self, id):
|
||||
cur = self.conn.cursor()
|
||||
cur.execute("""
|
||||
SELECT segment, user, name, mimetype, data
|
||||
FROM files
|
||||
WHERE id=?""", (id,))
|
||||
|
||||
for (segment, user, name, mimetype, data) in cur:
|
||||
return File(id, segment, user, name, mimetype, data)
|
||||
|
||||
return None
|
||||
|
||||
def set_file_segment(self, file_id, segment_id):
|
||||
print('set_file_segment', segment_id, file_id)
|
||||
cur = self.conn.cursor()
|
||||
cur.execute("UPDATE files SET segment=? WHERE id=?", (segment_id, file_id))
|
||||
self.commit()
|
||||
|
||||
def destroy_post(self, post):
|
||||
cur = self.conn.cursor()
|
||||
cur.execute('DELETE FROM files WHERE segment IN (SELECT id FROM segments WHERE post=?)',
|
||||
(post.id,))
|
||||
cur.execute('DELETE FROM segments WHERE post=?', (post.id,))
|
||||
cur.execute('DELETE FROM posts WHERE id=?', (post.id,))
|
||||
self.commit()
|
||||
|
||||
def create_subspace():
|
||||
pass
|
||||
|
||||
def destroy_subspace():
|
||||
pass
|
||||
|
||||
def create_post(self, user, subspace_id, title=''):
|
||||
def create_post(self, user: User, subspace_id: int, title=''):
|
||||
assert type(user) is User
|
||||
cur = self.conn.cursor()
|
||||
cur.execute("""
|
||||
|
@ -238,7 +299,48 @@ class Database:
|
|||
self.commit()
|
||||
return cur.lastrowid
|
||||
|
||||
def create_segment(self, post, type, content=None, url=None):
|
||||
def update_post_summary(self, post):
|
||||
# Render a concise version of the segments to be displayed in feeds,
|
||||
# save in the `render` field.
|
||||
segments = self.get_segments(post)
|
||||
render = ''
|
||||
|
||||
# Use only the first link/attachment.
|
||||
for seg in filter(lambda s: s.type in [Segment.LINK,
|
||||
Segment.ATTACHMENT], segments):
|
||||
render += f'=> {seg.url} — {seg.content}\n'
|
||||
break
|
||||
|
||||
if len(post.title):
|
||||
render += post.title
|
||||
|
||||
first = True
|
||||
for text in filter(lambda s: s.type == Segment.TEXT, segments):
|
||||
str = clean_title(text.content)
|
||||
if len(str) == 0: continue
|
||||
if len(post.title) and first:
|
||||
# Separate title from the body text.
|
||||
render += ' — '
|
||||
first = False
|
||||
if len(str) > self.max_summary:
|
||||
render += str[:self.max_summary] + "..."
|
||||
break
|
||||
render += str + ' '
|
||||
|
||||
for seg in filter(lambda s: s.type == Segment.IMAGE, segments):
|
||||
render += f'\n=> {seg.url} {seg.content}\n'
|
||||
break
|
||||
|
||||
if len(render) and not render.endswith('\n'):
|
||||
render += '\n'
|
||||
|
||||
post.render = render
|
||||
|
||||
cur = self.conn.cursor()
|
||||
cur.execute('UPDATE posts SET render=? WHERE id=?', (render, post.id))
|
||||
self.commit()
|
||||
|
||||
def create_segment(self, post, type, content=None, url=None) -> int:
|
||||
cur = self.conn.cursor()
|
||||
cur.execute("""
|
||||
INSERT INTO segments (post, type, content, url)
|
||||
|
@ -249,17 +351,22 @@ class Database:
|
|||
cur.execute("UPDATE segments SET pos=(SELECT MAX(pos) FROM segments) + 1 WHERE id=?",
|
||||
(seg_id,))
|
||||
self.commit()
|
||||
return cur.lastrowid
|
||||
return seg_id
|
||||
|
||||
def get_user(self, identity=None, name=None):
|
||||
def get_user(self, id=None, identity=None, name=None):
|
||||
cur = self.conn.cursor()
|
||||
cond = 'fp_cert=?' if identity else 'name=?'
|
||||
cond_value = identity.fp_cert if identity else name
|
||||
if id != None:
|
||||
cond = 'id=?'
|
||||
cond_value = id
|
||||
else:
|
||||
cond = 'fp_cert=?' if identity else 'name=?'
|
||||
cond_value = identity.fp_cert if identity else name
|
||||
cur.execute(f"""
|
||||
SELECT
|
||||
id, name, info, avatar, role, flags,
|
||||
notif, email, email_notif,
|
||||
ts_created, ts_active,
|
||||
UNIX_TIMESTAMP(ts_created),
|
||||
UNIX_TIMESTAMP(ts_active),
|
||||
num_notifs, sort_post, sort_cmt
|
||||
FROM users
|
||||
WHERE {cond}""", (cond_value,))
|
||||
|
@ -271,7 +378,7 @@ class Database:
|
|||
|
||||
return None
|
||||
|
||||
def get_posts(self, id=None, user=None, draft=False):
|
||||
def get_posts(self, id=None, user=None, draft=None):
|
||||
cur = self.conn.cursor()
|
||||
where_stm = []
|
||||
values = []
|
||||
|
@ -281,18 +388,19 @@ class Database:
|
|||
if user != None:
|
||||
where_stm.append('user=?')
|
||||
values.append(user.id)
|
||||
where_stm.append('draft=?')
|
||||
values.append(draft)
|
||||
if draft != None:
|
||||
where_stm.append('is_draft=?')
|
||||
values.append(draft)
|
||||
cur.execute(f"""
|
||||
SELECT
|
||||
subspace, parent, user, title, is_draft, is_pinned, ts_created, render
|
||||
id, subspace, parent, user, title, is_draft, is_pinned, UNIX_TIMESTAMP(ts_created), render
|
||||
FROM posts
|
||||
WHERE {' AND '.join(where_stm)}
|
||||
ORDER BY ts_created
|
||||
""", tuple(values))
|
||||
|
||||
posts = []
|
||||
for (subspace, parent, user, title, is_draft, is_pinned, ts_created, render) in cur:
|
||||
for (id, subspace, parent, user, title, is_draft, is_pinned, ts_created, render) in cur:
|
||||
posts.append(Post(id, subspace, parent, user, title, is_draft, is_pinned,
|
||||
ts_created, render))
|
||||
return posts
|
||||
|
@ -361,8 +469,9 @@ class Database:
|
|||
|
||||
return segments
|
||||
|
||||
def delete_segment(self, segment):
|
||||
def destroy_segment(self, segment):
|
||||
cur = self.conn.cursor()
|
||||
cur.execute('DELETE FROM files WHERE segment=?', (segment.id,))
|
||||
cur.execute('DELETE FROM segments WHERE id=?', (segment.id,))
|
||||
self.commit()
|
||||
|
||||
|
@ -389,7 +498,7 @@ class Database:
|
|||
cond_value = name
|
||||
|
||||
cur.execute(f"""
|
||||
SELECT id, name, info, flags, owner, ts_created, ts_active
|
||||
SELECT id, name, info, flags, owner, UNIX_TIMESTAMP(ts_created), UNIX_TIMESTAMP(ts_active)
|
||||
FROM subspaces
|
||||
WHERE {cond}""",
|
||||
(cond_value,))
|
||||
|
@ -406,6 +515,10 @@ def is_valid_name(name):
|
|||
return re.match(r'^[\w-]+$', name) != None
|
||||
|
||||
|
||||
def plural_s(i):
|
||||
return '' if i == 1 else 's'
|
||||
|
||||
|
||||
def clean_text(text):
|
||||
# TODO: Clean up the text. No => links, # H1s.
|
||||
|
||||
|
@ -417,11 +530,28 @@ def clean_text(text):
|
|||
|
||||
|
||||
def clean_title(title):
|
||||
title = title.replace('\n', ' ').replace('\t', ' ').strip()
|
||||
if len(title) and title[0] in '>#*':
|
||||
title = title[1:].strip()
|
||||
if len(title) and title[:2] == '=>':
|
||||
title = title[2:].strip()
|
||||
# Strip `=>` and other Gemini syntax.
|
||||
cleaned = []
|
||||
syntax = re.compile(r'^(\s*=>\s*|\* |>\s*|##?#?)')
|
||||
pre = False
|
||||
for line in title.split('\n'):
|
||||
if line[:3] == '```':
|
||||
if not pre:
|
||||
pre_label = line[3:].strip()
|
||||
if len(pre_label) == 0:
|
||||
pre_label = 'preformatted'
|
||||
line = f'[{pre_label}]'
|
||||
cleaned.append(line)
|
||||
pre = not pre
|
||||
continue
|
||||
if pre:
|
||||
continue
|
||||
found = syntax.match(line)
|
||||
if found:
|
||||
line = line[found.end():]
|
||||
line = line.replace('\t', ' ')
|
||||
cleaned.append(line)
|
||||
title = ' '.join(cleaned).strip()
|
||||
return title
|
||||
|
||||
|
||||
|
@ -450,9 +580,11 @@ class Bubble:
|
|||
self.hostname = hostname
|
||||
self.path = path # ends with /
|
||||
|
||||
# TODO: Read these from the configuration.
|
||||
self.site_name = 'Geminispace.org'
|
||||
self.site_info = 'A Small Social Hub'
|
||||
self.site_icon = '🌒'
|
||||
self.max_file_size = 100 * 1024
|
||||
|
||||
def __call__(self, req):
|
||||
db = Database(self.cfg)
|
||||
|
@ -460,6 +592,64 @@ class Bubble:
|
|||
db.close()
|
||||
return response
|
||||
|
||||
def dashboard_link(self, db, user):
|
||||
#notifs = ' — 0 notifications'
|
||||
notifs = ''
|
||||
|
||||
num_drafts = db.count_posts(user=user, draft=True)
|
||||
if num_drafts > 0:
|
||||
notifs += f' — ✏️ {num_drafts} draft{plural_s(num_drafts)}'
|
||||
|
||||
return f'=> /dashboard {user.avatar} {user.name}{notifs}\n'
|
||||
|
||||
def feed_entry(self, db, post, user, context=None):
|
||||
poster = db.get_user(id=post.user) # Pre-fetch the poster info.
|
||||
|
||||
src = f'=> /u/{poster.name} {poster.avatar} {poster.name}\n'
|
||||
src += post.render
|
||||
cmt = 'Comment'
|
||||
|
||||
if post.is_draft:
|
||||
age = 'Now'
|
||||
else:
|
||||
age_seconds = max(0, int(time.time()) - post.ts_created)
|
||||
age = f'{age_seconds} seconds'
|
||||
|
||||
sub = ''
|
||||
if not context or context.id != post.subspace:
|
||||
sub = ' · ' + db.get_subspace(id=post.subspace).title()
|
||||
|
||||
src += f'=> /u/{poster.name}/{post.id} 💬 {cmt}{sub} · {age}\n'
|
||||
|
||||
# TODO: Show if there if a notification related to this.
|
||||
|
||||
return src
|
||||
|
||||
def render_post(self, db, post):
|
||||
"""Render the final full presentation of the post, with all segments included."""
|
||||
src = ''
|
||||
if len(post.title):
|
||||
src += f'# {post.title}\n\n'
|
||||
# TODO: Metadata?
|
||||
|
||||
last_type = None
|
||||
for segment in db.get_segments(post):
|
||||
# Optionally, separate by newline.
|
||||
if last_type != None:
|
||||
if last_type != segment.type or (last_type == Segment.TEXT and
|
||||
segment.type == Segment.TEXT):
|
||||
src += '\n'
|
||||
|
||||
if segment.type == Segment.TEXT:
|
||||
src += segment.content + '\n'
|
||||
last_type = segment.type
|
||||
|
||||
elif segment.type in [Segment.LINK, Segment.IMAGE, Segment.ATTACHMENT]:
|
||||
src += f'=> {segment.url} {segment.content}\n'
|
||||
last_type = segment.type
|
||||
|
||||
return src
|
||||
|
||||
def respond(self, db, req):
|
||||
user = None
|
||||
|
||||
|
@ -521,7 +711,7 @@ class Bubble:
|
|||
if not user:
|
||||
return 60, 'Must be signed in to edit posts'
|
||||
|
||||
found = re.match(r'^(edit|move)-segment/(\d)$', req.path[len(self.path):])
|
||||
found = re.match(r'^(edit|move)-segment/(\d+)$', req.path[len(self.path):])
|
||||
seg_action = found.group(1)
|
||||
seg_id = int(found.group(2))
|
||||
segment = db.get_segment(seg_id)
|
||||
|
@ -548,11 +738,15 @@ class Bubble:
|
|||
seg_text = clean_text(urlparse.unquote(req.query))
|
||||
else:
|
||||
seg_text = clean_text(req.content.decode('utf-8'))
|
||||
db.update_segment(segment, content=seg_text)
|
||||
db.update_segment(segment, content=seg_text)
|
||||
|
||||
elif segment.type == Segment.IMAGE or segment.type == Segment.ATTACHMENT:
|
||||
seg_text = clean_title(urlparse.unquote(req.query))
|
||||
db.update_segment(segment, content=seg_text)
|
||||
else:
|
||||
arg = urlparse.unquote(req.query).strip()
|
||||
if arg.upper() == 'X':
|
||||
db.delete_segment(segment)
|
||||
db.destroy_segment(segment)
|
||||
else:
|
||||
db.move_segment(post, segment, int(arg) - 1)
|
||||
|
||||
|
@ -568,7 +762,7 @@ class Bubble:
|
|||
|
||||
elif req.path.startswith(self.path + 'edit/'):
|
||||
try:
|
||||
found = re.match(r'^(\d)(/([\w-]+))?$', req.path[len(self.path + 'edit/'):])
|
||||
found = re.match(r'^(\d+)(/([\w-]+))?$', req.path[len(self.path + 'edit/'):])
|
||||
post_id = int(found.group(1))
|
||||
post_action = found.group(3)
|
||||
post = db.get_post(post_id)
|
||||
|
@ -600,18 +794,76 @@ class Bubble:
|
|||
return 30, link
|
||||
return 10, 'Add link: (URL followed by label, separated with space)'
|
||||
|
||||
if post_action == 'add-file' and is_titan:
|
||||
if len(req.content) > self.max_file_size:
|
||||
return 50, f'File attachments must be less than {int(self.max_file_size / 1024)} KB'
|
||||
if req.content_token:
|
||||
fn = req.content_token.strip()
|
||||
else:
|
||||
fn = ''
|
||||
mime = 'application/octet-stream'
|
||||
if req.content_mime:
|
||||
mime = req.content_mime.lower().split(';')[0]
|
||||
file_id = db.create_file(user, fn, mime, req.content)
|
||||
#fn = str(file_id)
|
||||
is_image = mime.startswith('image/')
|
||||
#if is_image:
|
||||
|
||||
url_path = '/u/' + user.name + '/'
|
||||
url_path += 'image' if is_image else 'file'
|
||||
url_path += f'/{file_id}'
|
||||
if len(fn):
|
||||
# TODO: Clean up the filename.
|
||||
url_path += '/' + fn
|
||||
EXTENSIONS = {
|
||||
'image/jpeg': '.jpeg',
|
||||
'image/jpg': '.jpg',
|
||||
'image/png': '.png',
|
||||
'image/gif': '.gif'
|
||||
}
|
||||
if len(fn) == 0 and mime in EXTENSIONS:
|
||||
url_path += EXTENSIONS[mime]
|
||||
segment_id = db.create_segment(post,
|
||||
Segment.IMAGE if is_image else Segment.ATTACHMENT,
|
||||
url=url_path, content=fn)
|
||||
db.set_file_segment(file_id, segment_id)
|
||||
return 30, gemini_link
|
||||
|
||||
if post_action == 'title':
|
||||
if len(req.query):
|
||||
title_text = clean_title(urlparse.unquote(req.query))
|
||||
db.update_post(post, title=title_text)
|
||||
return 30, link
|
||||
return 10, 'Enter post title:'
|
||||
|
||||
if post_action == 'preview':
|
||||
db.update_post_summary(post)
|
||||
page = f'=> {link}/publish ➠ Publish now\n'
|
||||
page += f'=> {link} Edit draft\n'
|
||||
page += '\n# Feed Preview\n'
|
||||
page += self.feed_entry(db, post, user)
|
||||
if not post.title:
|
||||
page += '\n# Post Preview\n'
|
||||
else:
|
||||
page += '\n\n'
|
||||
page += self.render_post(db, post)
|
||||
return page
|
||||
|
||||
if post_action == 'delete':
|
||||
if req.query.upper() == 'YES':
|
||||
dst = '/dashboard' if post.is_draft else '/u/' + user.name
|
||||
db.destroy_post(post)
|
||||
return 30, dst
|
||||
elif len(req.query) > 0:
|
||||
return 30, f'/edit/{post.id}'
|
||||
return 10, f'Delete post {post.id}: {post.ymd_date()} {post.title_text()}? (Enter YES to confirm)'
|
||||
|
||||
# The Post Composer.
|
||||
page = f'# {post.title_text()}\n'
|
||||
page += f'=> {link}/title Edit title\n'
|
||||
page += f'=> /{subspace.title()} {subspace.title()}\n\n'
|
||||
|
||||
page = self.dashboard_link(db, user)
|
||||
page += f'=> {link}/preview 👁️ Preview post\n'
|
||||
page += f'\n# {post.title_text()}\n'
|
||||
page += f'=> {link}/title Edit post title\n'
|
||||
|
||||
# Segments.
|
||||
##SEPARATOR = '\n\n'
|
||||
segments = db.get_segments(post)
|
||||
|
@ -621,7 +873,7 @@ class Bubble:
|
|||
sid += 1
|
||||
|
||||
if len(segments) > 1:
|
||||
page += f'\n—— § {sid} ——\n\n'
|
||||
page += f'\n## — § {sid} —\n\n'
|
||||
else:
|
||||
page += '\n'
|
||||
|
||||
|
@ -634,12 +886,12 @@ class Bubble:
|
|||
page += f"=> {seg_link}/{segment.id} Edit link\n"
|
||||
|
||||
elif segment.type == Segment.IMAGE:
|
||||
page += '\n(image)\n'
|
||||
page += f"=> titan://{self.hostname}/{seg_link}/{segment.id} Replace image\n"
|
||||
page += f'=> {segment.url} {segment.content}\n'
|
||||
page += f'=> {seg_link}/{segment.id} Edit caption\n'
|
||||
|
||||
elif segment.type == Segment.ATTACHMENT:
|
||||
page += '\n(attachment\n'
|
||||
page += f"=> titan://{self.hostname}/{seg_link}/{segment.id} Replace attachment\n"
|
||||
page += f'=> {segment.url} {segment.content}\n'
|
||||
page += f'=> {seg_link}/{segment.id} Edit label\n'
|
||||
|
||||
elif segment.type == Segment.POLL:
|
||||
page += f'\n* {segment.content}\n'
|
||||
|
@ -654,11 +906,9 @@ class Bubble:
|
|||
page += f'=> {link}/add-text Add text\n'
|
||||
page += f'=> titan://{self.hostname}/edit/{post.id}/add-text Add long text\n'
|
||||
page += f'=> {link}/add-link Add link\n'
|
||||
page += f'=> titan://{self.hostname}/edit/{post.id}/add-image Add image\n'
|
||||
page += f'=> titan://{self.hostname}/edit/{post.id}/add-file Add attachment\n'
|
||||
page += f'=> {link}/add-link Add poll option\n'
|
||||
page += f'=> titan://{self.hostname}/edit/{post.id}/add-file Add image or file attachment\nOptionally, you can set a filename with the token field.\n'
|
||||
page += f'=> {link}/add-poll Add poll option\n'
|
||||
|
||||
page += f'\n=> {link}/publish Preview and publish\n'
|
||||
page += f'\n=> {link}/delete ❌ Delete post\n'
|
||||
return page
|
||||
|
||||
|
@ -671,26 +921,27 @@ class Bubble:
|
|||
return 51, 'Not found'
|
||||
|
||||
elif req.path == self.path + 'dashboard':
|
||||
page = f'# {user.name}\n'
|
||||
page += f'\n## Notifications\n'
|
||||
page += 'No notifications.\n'
|
||||
page += f'\n## Drafts\n'
|
||||
page = f'# {user.avatar} {user.name}\n'
|
||||
|
||||
page += f'\n## 0 Notifications\n'
|
||||
|
||||
drafts = db.get_posts(user=user, draft=True)
|
||||
n = len(drafts)
|
||||
page += f'\n## {n} Draft{plural_s(n)}\n'
|
||||
if len(drafts) > 0:
|
||||
for post in drafts:
|
||||
page += f'=> /edit/{post.id} {post.ymd_date()} {post.title_text()}\n'
|
||||
else:
|
||||
page += 'No drafts.\n'
|
||||
page += f'=> /u/{user.name} u/{user.name}\n'
|
||||
page += f'\n=> /u/{user.name} u/{user.name}\n'
|
||||
return page
|
||||
|
||||
elif req.path.startswith(self.path + 'u/'):
|
||||
try:
|
||||
# User feed.
|
||||
path = req.path[len(self.path):]
|
||||
found = re.match(r'u/([\w-]+)(/(post))?', path)
|
||||
found = re.match(r'u/([\w-]+)(/(post|image|file))?(/(\d+).*)?', path)
|
||||
u_name = found.group(1)
|
||||
u_action = found.group(3)
|
||||
arg = found.group(5)
|
||||
c_user = db.get_user(name=u_name)
|
||||
context = db.get_subspace(owner=c_user.id)
|
||||
|
||||
|
@ -698,6 +949,10 @@ class Bubble:
|
|||
draft_id = db.create_post(user, context.id)
|
||||
return 30, '/edit/%d' % draft_id
|
||||
|
||||
if u_action == 'image' or u_action == 'file':
|
||||
file = db.get_file(int(arg))
|
||||
return 20, file.mimetype, file.data
|
||||
|
||||
page += f'# {context.title()}\n'
|
||||
|
||||
except Exception as x:
|
||||
|
@ -735,14 +990,7 @@ class Bubble:
|
|||
page += f'=> /s/ {self.site_icon} Subspaces\n'
|
||||
page += FOOTER_MENU
|
||||
else:
|
||||
#notifs = ' — 0 notifications'
|
||||
notifs = ''
|
||||
|
||||
num_drafts = db.count_posts(user=user, draft=True)
|
||||
if num_drafts > 0:
|
||||
notifs += f' — ✏️ {num_drafts} draft{"s" if num_drafts != 1 else ""}'
|
||||
|
||||
page += f'=> /u/{user.name}/dashboard {user.avatar} {user.name}{notifs}\n'
|
||||
page += self.dashboard_link(db, user)
|
||||
if c_user and c_user.id == user.id:
|
||||
page += f'=> /u/{user.name}/post ✏️ New post\n'
|
||||
elif context and context.owner == 0:
|
||||
|
@ -775,16 +1023,15 @@ class Bubble:
|
|||
def init(capsule):
|
||||
cfg = capsule.config()
|
||||
try:
|
||||
mod_cfg = cfg.section('bubble')
|
||||
mod_cfg = cfg.section('bubble')
|
||||
|
||||
hostname = mod_cfg.get('host', fallback=cfg.hostnames()[0])
|
||||
path = mod_cfg.get('path', fallback='/')
|
||||
|
||||
path = mod_cfg.get('path', fallback='/')
|
||||
if not path.endswith('/'): path += '/'
|
||||
|
||||
responder = Bubble(hostname, path, mod_cfg)
|
||||
capsule.add(path + '*', responder, hostname, protocol='gemini')
|
||||
capsule.add(path + '*', responder, hostname, protocol='titan')
|
||||
for hostname in cfg.hostnames():
|
||||
responder = Bubble(hostname, path, mod_cfg)
|
||||
capsule.add(path + '*', responder, hostname, protocol='gemini')
|
||||
capsule.add(path + '*', responder, hostname, protocol='titan')
|
||||
|
||||
except KeyError:
|
||||
pass
|
Loading…
Reference in a new issue