[1mdiff --git a/50_bubble.py b/50_bubble.py[m
[1mindex 961f2e6..4eff66b 100644[m
[1m--- a/50_bubble.py[m
[1m+++ b/50_bubble.py[m
[36m@@ -38,6 +38,8 @@[m [mclass Bubble:[m
self.user_register = cfg.getboolean('user.register', True)[m
self.user_subspaces = cfg.getboolean('user.subspaces', True)[m
self.user_role_limited = cfg.getboolean('user.role.limited', True)[m
[32m+[m[32m self.rate_register = max(1, cfg.getint('rate.register', 20)) # per hour, any remote[m
[32m+[m[32m self.rate_post = max(1, cfg.getint('rate.post', 10)) # per hour, per remote[m
self.admin_certpass = cfg.get('admin.certpass', '')[m
self.antenna_url = cfg.get('antenna.url', 'gemini://warmedal.se/~antenna/submit')[m
self.version = __version__[m
[36m@@ -448,6 +450,8 @@[m [mBubble is open source:[m
if req.path.startswith(self.path + 'register/'):[m
if not session.bubble.user_register:[m
return 61, 'User registration is closed'[m
[32m+[m[32m if db.get_access_rate(3600, None, LogEntry.ACCOUNT_CREATED) >= session.bubble.rate_register:[m
[32m+[m[32m return 44, str(int(3600 / session.bubble.rate_register))[m
if not db.verify_token(session.user, req.path[len(self.path + 'register/'):]):[m
return 61, 'Expired'[m
try:[m
[36m@@ -458,6 +462,7 @@[m [mBubble is open source:[m
db.create_user(username, req.identity,[m
User.LIMITED if session.bubble.user_role_limited else \[m
User.BASIC)[m
[32m+[m[32m db.add_log_entry(req.remote_address, LogEntry.ACCOUNT_CREATED)[m
[m
page = f'# Welcome, {username}!\n\n'[m
if session.bubble.user_role_limited:[m
[1mdiff --git a/feeds.py b/feeds.py[m
[1mindex 06e92c3..983a164 100644[m
[1m--- a/feeds.py[m
[1m+++ b/feeds.py[m
[36m@@ -1,6 +1,6 @@[m
import re[m
import urllib.parse as urlparse[m
[31m-from model import User, Post, Segment, Subspace, Commit, Crossref, \[m
[32m+[m[32mfrom model import User, Post, Segment, Subspace, LogEntry, Commit, Crossref, \[m
FOLLOW_SUBSPACE, FOLLOW_USER, FOLLOW_POST, \[m
MUTE_SUBSPACE, MUTE_USER, MUTE_POST[m
from subspace import subspace_admin_actions[m
[36m@@ -99,11 +99,15 @@[m [mdef make_post_page_or_configure_feed(session):[m
if session.is_context_locked:[m
return 61, "Subspace is locked"[m
if session.user.role == User.LIMITED and (not session.c_user or[m
[31m- not session.c_user.id != session.user.id):[m
[32m+[m[32m session.c_user.id != session.user.id):[m
return 61, "Not authorized"[m
[31m- if session.user.role == User.LIMITED and not db.verify_token(session.user, arg2):[m
[32m+[m[32m if session.user.role == User.LIMITED and not db.verify_token(session.user, arg):[m
return 61, "Expired"[m
[32m+[m[32m if session.user.role == User.LIMITED and \[m
[32m+[m[32m db.get_access_rate(3600, req.remote_address, LogEntry.POST_CREATED) >= session.bubble.rate_post:[m
[32m+[m[32m return 44, str(int(3600 / session.bubble.rate_post))[m
draft_id = db.create_post(session.user, session.context.id)[m
[32m+[m[32m db.add_log_entry(req.remote_address, LogEntry.POST_CREATED)[m
return 30, '/edit/%d' % draft_id[m
[m
elif action == 'post':[m
[36m@@ -115,8 +119,11 @@[m [mdef make_post_page_or_configure_feed(session):[m
return 61, "Subspace is locked"[m
if session.user.role == User.LIMITED and not session.c_user:[m
return 61, "Not authorized"[m
[31m- if session.user.role == User.LIMITED and not db.verify_token(session.user, arg2):[m
[32m+[m[32m if session.user.role == User.LIMITED and not db.verify_token(session.user, arg):[m
return 61, "Expired"[m
[32m+[m[32m if session.user.role == User.LIMITED and \[m
[32m+[m[32m db.get_access_rate(3600, req.remote_address, LogEntry.POST_CREATED) >= session.bubble.rate_post:[m
[32m+[m[32m return 44, str(int(3600 / session.bubble.rate_post))[m
[m
if session.is_gemini:[m
if is_empty_query(req):[m
[36m@@ -181,6 +188,7 @@[m [mdef make_post_page_or_configure_feed(session):[m
return 30, f'{session.server_root()}/edit/{post.id}'[m
[m
db.publish_post(post)[m
[32m+[m[32m db.add_log_entry(req.remote_address, LogEntry.POST_CREATED)[m
return 30, session.server_root() + post.page_url()[m
[m
if session.user:[m
[36m@@ -629,12 +637,15 @@[m [mdef make_feed_page(session):[m
page += f'=> /s/ {session.bubble.site_icon} Subspaces\n'[m
page += session.FOOTER_MENU[m
else:[m
[31m- token = session.get_token()[m
[32m+[m[32m if session.user.role == User.LIMITED:[m
[32m+[m[32m link_suffix = '/' + session.get_token()[m
[32m+[m[32m else:[m
[32m+[m[32m link_suffix = ''[m
page += session.dashboard_link()[m
if not session.is_context_locked:[m
if c_user and c_user.id == user.id:[m
[31m- page += f'=> /u/{user.name}/post/{token} 💬 New post\n'[m
[31m- page += f'=> /u/{user.name}/compose/{token} ✏️ Compose draft\n'[m
[32m+[m[32m page += f'=> /u/{user.name}/post{link_suffix} 💬 New post\n'[m
[32m+[m[32m page += f'=> /u/{user.name}/compose{link_suffix} ✏️ Compose draft\n'[m
elif context and context.owner == 0:[m
if is_issue_tracker:[m
if session.user.role != User.LIMITED:[m
[36m@@ -642,10 +653,10 @@[m [mdef make_feed_page(session):[m
else:[m
if session.user.role != User.LIMITED:[m
page += f'=> /{context.title()}/post 💬 New post in s/{context.name}\n'[m
[31m- page += f'=> /{context.title()}/compose/{token} ✏️ Compose draft in s/{context.name}\n'[m
[32m+[m[32m page += f'=> /{context.title()}/compose{link_suffix} ✏️ Compose draft in s/{context.name}\n'[m
else:[m
[31m- page += f'=> /u/{user.name}/post/{token} 💬 New post in u/{user.name}\n'[m
[31m- page += f'=> /u/{user.name}/compose/{token} ✏️ Compose draft in u/{user.name}\n'[m
[32m+[m[32m page += f'=> /u/{user.name}/post{link_suffix} 💬 New post in u/{user.name}\n'[m
[32m+[m[32m page += f'=> /u/{user.name}/compose{link_suffix} ✏️ Compose draft in u/{user.name}\n'[m
page += f'=> /s/ {session.bubble.site_icon} Subspaces\n'[m
[m
if is_issue_tracker:[m
[1mdiff --git a/model.py b/model.py[m
[1mindex e1e9a40..ca39e44 100644[m
[1m--- a/model.py[m
[1m+++ b/model.py[m
[36m@@ -1,4 +1,5 @@[m
import datetime[m
[32m+[m[32mimport hashlib[m
import mariadb[m
import os[m
import random[m
[36m@@ -25,11 +26,21 @@[m [mdef parse_asn1_time(asn1_bytes):[m
return None[m
[m
[m
[32m+[m[32mdef address_hash(from_addr):[m
[32m+[m[32m m = hashlib.sha256()[m
[32m+[m[32m m.update(from_addr[0].encode('utf-8'))[m
[32m+[m[32m return m.hexdigest()[m
[32m+[m
[32m+[m
FOLLOW_USER, FOLLOW_POST, FOLLOW_SUBSPACE = range(3)[m
MUTE_USER, MUTE_POST, MUTE_SUBSPACE = range(3)[m
[m
[m
[32m+[m[32mclass LogEntry:[m
[32m+[m[32m ACCOUNT_CREATED, POST_CREATED = range(2)[m
[32m+[m
[32m+[m
class Segment:[m
TEXT, LINK, IMAGE, ATTACHMENT, POLL = range(5)[m
[m
[36m@@ -637,6 +648,14 @@[m [mclass Database:[m
INDEX (dst_issueid)[m
)""")[m
[m
[32m+[m[32m db.execute("""CREATE TABLE IF NOT EXISTS log ([m
[32m+[m[32m remote CHAR(64) NOT NULL,[m
[32m+[m[32m type INT NOT NULL,[m
[32m+[m[32m ts TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,[m
[32m+[m[32m INDEX (remote),[m
[32m+[m[32m INDEX (type)[m
[32m+[m[32m )""")[m
[32m+[m
db.execute('INSERT IGNORE INTO users (name, avatar, role, password, ts_password) '[m
'VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP())',[m
('admin', '🚀', User.ADMIN, admin_certpass))[m
[36m@@ -2402,6 +2421,30 @@[m [mclass Database:[m
commits.append(Commit(repo.id, hash, msg, ts))[m
return commits[m
[m
[32m+[m[32m def get_access_rate(self, window_seconds, from_addr, entry_type):[m
[32m+[m[32m cond = ['type=?'][m
[32m+[m[32m values = [entry_type][m
[32m+[m[32m if from_addr != None:[m
[32m+[m[32m cond.append('remote=?')[m
[32m+[m[32m values.append(address_hash(from_addr))[m
[32m+[m[32m cond.append('TIMESTAMPDIFF(SECOND, ts, CURRENT_TIMESTAMP())<?')[m
[32m+[m[32m values.append(window_seconds)[m
[32m+[m
[32m+[m[32m cur = self.conn.cursor()[m
[32m+[m[32m cur.execute(f"SELECT COUNT(*) FROM log WHERE {' AND '.join(cond)}", values)[m
[32m+[m[32m for (rate,) in cur:[m
[32m+[m[32m pass[m
[32m+[m
[32m+[m[32m cur.execute("DELETE FROM log WHERE TIMESTAMPDIFF(MINUTE, ts, CURRENT_TIMESTAMP())>=60")[m
[32m+[m[32m self.commit()[m
[32m+[m
[32m+[m[32m return rate[m
[32m+[m
[32m+[m[32m def add_log_entry(self, from_addr, type):[m
[32m+[m[32m cur = self.conn.cursor()[m
[32m+[m[32m cur.execute("INSERT INTO log (remote, type) VALUES (?, ?)", (address_hash(from_addr), type))[m
[32m+[m[32m self.commit()[m
[32m+[m
[m
class Search:[m
def __init__(self, db):[m
text/plain
This content has been proxied by September (3851b).