modified: README.rst

modified:   index.html
	modified:   index2.html
	modified:   remarkbox/__init__.py
	modified:   remarkbox/lib/mail.py
	modified:   remarkbox/models/meta.py
	modified:   remarkbox/views/__init__.py
This commit is contained in:
Russell Ballestrini 2025-10-12 08:37:07 -04:00
parent fdfc760865
commit 69ada15278
7 changed files with 74 additions and 24 deletions

View file

@ -72,6 +72,14 @@ What is Remarkbox?
------------------
Remarkbox is a standalone question and answer site (forum) or an embedded comments/product reviews service that works anywhere HTML is supported.
Features
--------
- **Dark Mode Support:** User-configurable theme preferences with automatic theme detection for embedded contexts
- **Passwordless Authentication:** One-time-password codes via email for secure registration and login
- **Multi-tenant Architecture:** Host multiple forums and comment systems on a single installation
- **Customizable Themes:** Plugin-based theme system supporting custom branding and styling
- **Embed Anywhere:** Works with static sites, WordPress, or any platform that supports HTML
Project Goals
==============================================

View file

@ -57,11 +57,11 @@ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem
var thread_title = window.document.title;
var thread_fragment = window.location.hash;
<!-- rb owner was here -->
var rb_src = "http://127.0.0.1:6543/embed" +
"?rb_owner_key=" + rb_owner_key +
"&thread_title=" + escape(thread_title) +
"&thread_uri=" + encodeURIComponent(thread_uri) +
"&mode=light" +
thread_fragment;
function create_remarkbox_iframe() {

View file

@ -60,7 +60,7 @@ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem
var thread_fragment = window.location.hash;
var email = "russell.ballestrini@gmail.com";
function create_remarkbox_iframe() {
var src = "http://127.0.0.1:6543/embed?rb_owner_key=" + rb_owner_key + "&email=" + email + "&thread_uri=" + thread_uri;
var src = "http://127.0.0.1:6543/embed?rb_owner_key=" + rb_owner_key + "&email=" + email + "&thread_uri=" + thread_uri + "&mode=light";
var ifrm = document.createElement("iframe");
ifrm.setAttribute("id", "remarkbox-iframe");
ifrm.setAttribute("scrolling", "no");
@ -86,5 +86,4 @@ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem
</div>
</body>
</html>

View file

@ -28,9 +28,6 @@ import re
# needed to load themes.
from pkg_resources import iter_entry_points
# needed to support expanding ENV vars from ini.
from os.path import expandvars
import logging
log = logging.getLogger(__name__)
@ -43,18 +40,47 @@ def get_int_or_bool_or_none_or_str(value):
Given a string value pulled from a configuration file,
this function attempts to return the value with the proper type.
"""
# Handle non-string values
if not isinstance(value, str):
return value
# Handle string values
try:
return int(value)
except ValueError:
if value.lower() in {"yes", "y", "true", "y"}:
value_lower = value.lower()
if value_lower in {"yes", "y", "true", "t", "1"}:
return True
elif value.lower() in {"no", "n", "false", "f"}:
elif value_lower in {"no", "n", "false", "f", "0"}:
return False
elif value.lower() == "none":
elif value_lower in {"none", "null"}:
return None
return str(value)
def expand_env_vars(value):
"""Expand environment variables including ${VAR:-default} syntax."""
if not isinstance(value, str):
return value
import os
import re
# Handle ${VAR:-default} syntax
# Use a non-greedy match to stop at the first closing brace
pattern = r"\$\{([^:}]*)(?::-([^}]*?))?\}"
def replacer(match):
var_name = match.group(1)
# Handle empty variable name case ${:-default}
if not var_name:
return match.group(2) if match.group(2) is not None else match.group(0)
default_value = match.group(2) if match.group(2) is not None else ""
return os.environ.get(var_name, default_value)
return re.sub(pattern, replacer, value)
def get_children_settings(settings, parent_key):
"""
Accept a settings dict and parent key, return dict of children
@ -73,18 +99,14 @@ def get_children_settings(settings, parent_key):
{'hashalg': 'md5'}
"""
# needed to support expanding ENV vars from ini.
from os.path import expandvars
# the +1 is the . between parent and child settings.
parent_len = len(parent_key) + 1
children = {}
for key, value in settings.items():
if parent_key in key:
# expandvars replaces template with ENV vars.
children[key[parent_len:]] = get_int_or_bool_or_none_or_str(
expandvars(value)
)
# Expand environment variables with support for defaults
expanded_value = expand_env_vars(value)
children[key[parent_len:]] = get_int_or_bool_or_none_or_str(expanded_value)
return children
@ -155,6 +177,11 @@ def maybe_root_domain(string):
def main(global_config, **settings):
"""This function returns a Pyramid WSGI application."""
# Expand environment variables in all settings using our custom function
for key, value in list(settings.items()):
if isinstance(value, str):
settings[key] = expand_env_vars(value)
app_settings = get_children_settings(settings, "app")
session_settings = get_children_settings(settings, "session")
@ -175,7 +202,18 @@ def main(global_config, **settings):
session_settings["domain"] = root_domain
factory = SignedCookieSessionFactory(**session_settings)
return factory(request)
session = factory(request)
# Log session contents and size on every request
import pickle
session_dict = dict(session)
serialized = pickle.dumps(session_dict)
log.info(f"Session total size: {len(serialized)} bytes, keys: {list(session_dict.keys())}")
for key, value in session_dict.items():
item_size = len(pickle.dumps({key: value}))
log.info(f" Session['{key}'] = {item_size} bytes (type: {type(value).__name__})")
return session
# setup session factory to use unencrypted but signed cookies.
# session_factory = SignedCookieSessionFactory(**session_settings)

View file

@ -187,12 +187,12 @@ def send_verification_digits_to_email(request, to_email, raw_digits):
message_text = WELCOME_1_TEXT.format(raw_digits)
message_html = WELCOME_1_HTML.format(subject, raw_digits)
if not request.user.verified:
message_text = WELCOME_1_TEXT.format(raw_digits)
message_html = WELCOME_1_HTML.format(subject, raw_digits)
else:
if request.user and request.user.verified:
message_text = WELCOME_2_TEXT.format(raw_digits)
message_html = WELCOME_2_HTML.format(subject, raw_digits)
else:
message_text = WELCOME_1_TEXT.format(raw_digits)
message_html = WELCOME_1_HTML.format(subject, raw_digits)
send_pyramid_email(request, to_email, subject, message_text, message_html)

View file

@ -7,6 +7,7 @@ from sqlalchemy import ForeignKey
from time import time
import base64
import uuid
from sqlalchemy_utils import UUIDType as TempUUIDType
@ -75,10 +76,10 @@ def short_id_to_bytes(short_id):
"""Accept a short_id (sanitized url safe base64 string) and return a byte string.
>>> short_id_to_bytes('dbHeSEFLEeeuz5xONpxxWA')
'u\xb1\xdeHAK\x11\xe7\xae\xcf\x9cN6\x9cqX'
b'u\xb1\xdeHAK\x11\xe7\xae\xcf\x9cN6\x9cqX'
"""
return (short_id + "===").replace("_", "/").replace("-", "+").decode("base64")
return base64.b64decode((short_id + "===").replace("_", "/").replace("-", "+"))
def id_to_uuid(the_id):

View file

@ -113,7 +113,11 @@ def nodes_pending_verify(request):
def set_node_to_pending_in_session(request, node):
"""Update session to make node go into pending_verify state."""
nodes_pending_verify(request).append(str(node.id_without_dashes))
pending = nodes_pending_verify(request)
node_id = str(node.id_without_dashes)
# Only add if not already present and list isn't too large
if node_id not in pending and len(pending) < 50:
pending.append(node_id)
def verify_pending_nodes_in_session(request, user):