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:
parent
fdfc760865
commit
69ada15278
7 changed files with 74 additions and 24 deletions
|
|
@ -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
|
||||
==============================================
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue