diff --git a/50_bubble.py b/50_bubble.py

index 961f2e6..4eff66b 100644

--- a/50_bubble.py

+++ b/50_bubble.py

@@ -38,6 +38,8 @@ class Bubble:

     self.user_register = cfg.getboolean('user.register', True)

     self.user_subspaces = cfg.getboolean('user.subspaces', True)

     self.user_role_limited = cfg.getboolean('user.role.limited', True)

+ self.rate_register = max(1, cfg.getint('rate.register', 20)) # per hour, any remote

+ self.rate_post = max(1, cfg.getint('rate.post', 10)) # per hour, per remote

     self.admin_certpass = cfg.get('admin.certpass', '')

     self.antenna_url = cfg.get('antenna.url', 'gemini://warmedal.se/~antenna/submit')

     self.version = __version__

@@ -448,6 +450,8 @@ Bubble is open source:

             if req.path.startswith(self.path + 'register/'):

                 if not session.bubble.user_register:

                     return 61, 'User registration is closed'

+ if db.get_access_rate(3600, None, LogEntry.ACCOUNT_CREATED) >= session.bubble.rate_register:

+ return 44, str(int(3600 / session.bubble.rate_register))

                 if not db.verify_token(session.user, req.path[len(self.path + 'register/'):]):

                     return 61, 'Expired'

                 try:

@@ -458,6 +462,7 @@ Bubble is open source:

                     db.create_user(username, req.identity,

                                    User.LIMITED if session.bubble.user_role_limited else \

                                    User.BASIC)

+ db.add_log_entry(req.remote_address, LogEntry.ACCOUNT_CREATED)



                     page = f'# Welcome, {username}!\n\n'

                     if session.bubble.user_role_limited:

diff --git a/feeds.py b/feeds.py

index 06e92c3..983a164 100644

--- a/feeds.py

+++ b/feeds.py

@@ -1,6 +1,6 @@

import re

import urllib.parse as urlparse

-from model import User, Post, Segment, Subspace, Commit, Crossref, \

+from model import User, Post, Segment, Subspace, LogEntry, Commit, Crossref, \

 FOLLOW_SUBSPACE, FOLLOW_USER, FOLLOW_POST, \

 MUTE_SUBSPACE, MUTE_USER, MUTE_POST

from subspace import subspace_admin_actions

@@ -99,11 +99,15 @@ def make_post_page_or_configure_feed(session):

     if session.is_context_locked:

         return 61, "Subspace is locked"

     if session.user.role == User.LIMITED and (not session.c_user or

- not session.c_user.id != session.user.id):

+ session.c_user.id != session.user.id):

         return 61, "Not authorized"

- if session.user.role == User.LIMITED and not db.verify_token(session.user, arg2):

+ if session.user.role == User.LIMITED and not db.verify_token(session.user, arg):

         return 61, "Expired"

+ if session.user.role == User.LIMITED and \

+ db.get_access_rate(3600, req.remote_address, LogEntry.POST_CREATED) >= session.bubble.rate_post:

+ return 44, str(int(3600 / session.bubble.rate_post))

     draft_id = db.create_post(session.user, session.context.id)

+ db.add_log_entry(req.remote_address, LogEntry.POST_CREATED)

     return 30, '/edit/%d' % draft_id



 elif action == 'post':

@@ -115,8 +119,11 @@ def make_post_page_or_configure_feed(session):

         return 61, "Subspace is locked"

     if session.user.role == User.LIMITED and not session.c_user:

         return 61, "Not authorized"

- if session.user.role == User.LIMITED and not db.verify_token(session.user, arg2):

+ if session.user.role == User.LIMITED and not db.verify_token(session.user, arg):

         return 61, "Expired"

+ if session.user.role == User.LIMITED and \

+ db.get_access_rate(3600, req.remote_address, LogEntry.POST_CREATED) >= session.bubble.rate_post:

+ return 44, str(int(3600 / session.bubble.rate_post))



     if session.is_gemini:

         if is_empty_query(req):

@@ -181,6 +188,7 @@ def make_post_page_or_configure_feed(session):

         return 30, f'{session.server_root()}/edit/{post.id}'



     db.publish_post(post)

+ db.add_log_entry(req.remote_address, LogEntry.POST_CREATED)

     return 30, session.server_root() + post.page_url()



 if session.user:

@@ -629,12 +637,15 @@ def make_feed_page(session):

         page += f'=> /s/ {session.bubble.site_icon} Subspaces\n'

         page += session.FOOTER_MENU

     else:

- token = session.get_token()

+ if session.user.role == User.LIMITED:

+ link_suffix = '/' + session.get_token()

+ else:

+ link_suffix = ''

         page += session.dashboard_link()

         if not session.is_context_locked:

             if c_user and c_user.id == user.id:

- page += f'=> /u/{user.name}/post/{token} 💬 New post\n'

- page += f'=> /u/{user.name}/compose/{token} ✏️ Compose draft\n'

+ page += f'=> /u/{user.name}/post{link_suffix} 💬 New post\n'

+ page += f'=> /u/{user.name}/compose{link_suffix} ✏️ Compose draft\n'

             elif context and context.owner == 0:

                 if is_issue_tracker:

                     if session.user.role != User.LIMITED:

@@ -642,10 +653,10 @@ def make_feed_page(session):

                 else:

                     if session.user.role != User.LIMITED:

                         page += f'=> /{context.title()}/post 💬 New post in s/{context.name}\n'

- page += f'=> /{context.title()}/compose/{token} ✏️ Compose draft in s/{context.name}\n'

+ page += f'=> /{context.title()}/compose{link_suffix} ✏️ Compose draft in s/{context.name}\n'

             else:

- page += f'=> /u/{user.name}/post/{token} 💬 New post in u/{user.name}\n'

- page += f'=> /u/{user.name}/compose/{token} ✏️ Compose draft in u/{user.name}\n'

+ page += f'=> /u/{user.name}/post{link_suffix} 💬 New post in u/{user.name}\n'

+ page += f'=> /u/{user.name}/compose{link_suffix} ✏️ Compose draft in u/{user.name}\n'

         page += f'=> /s/ {session.bubble.site_icon} Subspaces\n'



         if is_issue_tracker:

diff --git a/model.py b/model.py

index e1e9a40..ca39e44 100644

--- a/model.py

+++ b/model.py

@@ -1,4 +1,5 @@

import datetime

+import hashlib

import mariadb

import os

import random

@@ -25,11 +26,21 @@ def parse_asn1_time(asn1_bytes):

 return None





+def address_hash(from_addr):

+ m = hashlib.sha256()

+ m.update(from_addr[0].encode('utf-8'))

+ return m.hexdigest()

+

+

NOTE: All enum values are used in database, don't change them!

FOLLOW_USER, FOLLOW_POST, FOLLOW_SUBSPACE = range(3)

MUTE_USER, MUTE_POST, MUTE_SUBSPACE = range(3)





+class LogEntry:

+ ACCOUNT_CREATED, POST_CREATED = range(2)

+

+

class Segment:

 TEXT, LINK, IMAGE, ATTACHMENT, POLL = range(5)



@@ -637,6 +648,14 @@ class Database:

         INDEX (dst_issueid)

     )""")



+ db.execute("""CREATE TABLE IF NOT EXISTS log (

+ remote CHAR(64) NOT NULL,

+ type INT NOT NULL,

+ ts TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,

+ INDEX (remote),

+ INDEX (type)

+ )""")

+

     db.execute('INSERT IGNORE INTO users (name, avatar, role, password, ts_password) '

                'VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP())',

                ('admin', '🚀', User.ADMIN, admin_certpass))

@@ -2402,6 +2421,30 @@ class Database:

         commits.append(Commit(repo.id, hash, msg, ts))

     return commits



+ def get_access_rate(self, window_seconds, from_addr, entry_type):

+ cond = ['type=?']

+ values = [entry_type]

+ if from_addr != None:

+ cond.append('remote=?')

+ values.append(address_hash(from_addr))

+ cond.append('TIMESTAMPDIFF(SECOND, ts, CURRENT_TIMESTAMP())<?')

+ values.append(window_seconds)

+

+ cur = self.conn.cursor()

+ cur.execute(f"SELECT COUNT(*) FROM log WHERE {' AND '.join(cond)}", values)

+ for (rate,) in cur:

+ pass

+

+ cur.execute("DELETE FROM log WHERE TIMESTAMPDIFF(MINUTE, ts, CURRENT_TIMESTAMP())>=60")

+ self.commit()

+

+ return rate

+

+ def add_log_entry(self, from_addr, type):

+ cur = self.conn.cursor()

+ cur.execute("INSERT INTO log (remote, type) VALUES (?, ?)", (address_hash(from_addr), type))

+ self.commit()

+



class Search:

 def __init__(self, db):

Proxy Information
Original URL
gemini://git.skyjake.fi/bubble/main/pcdiff/7e0673d018696c70bc0c471e344cb7e0c879a673
Status Code
Success (20)
Meta
text/plain
Capsule Response Time
30.383047 milliseconds
Gemini-to-HTML Time
4.13395 milliseconds

This content has been proxied by September (3851b).