From e387d056e84ee30c683072d22c50a08029a74f41 Mon Sep 17 00:00:00 2001
From: Sangye Ince-Johannsen <sij@sij.law>
Date: Fri, 28 Mar 2025 15:56:25 +0000
Subject: [PATCH] Update changes

---
 .gitignore                                    |   5 +-
 ...rnames.txt => banned_usernames.txt_example |   0
 canary.py                                     | 332 ++++++++++++++++++
 canary.txt                                    |  34 ++
 cleanup.py                                    | 133 +++++++
 conduwuit_logs.txt                            |  74 ++++
 refresh_token.sh                              |  84 +++--
 registration.py                               |  16 +
 relaunch_without_refresh.sh                   |  61 ++++
 update_conduwuit.sh                           |  19 +
 10 files changed, 719 insertions(+), 39 deletions(-)
 rename example-banned_usernames.txt => banned_usernames.txt_example (100%)
 create mode 100644 canary.py
 create mode 100644 canary.txt
 create mode 100644 cleanup.py
 create mode 100644 conduwuit_logs.txt
 create mode 100755 relaunch_without_refresh.sh
 create mode 100644 update_conduwuit.sh

diff --git a/.gitignore b/.gitignore
index 48cbcbb..6060618 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,12 +29,15 @@ Thumbs.db
 .classpath
 
 # Project-specific sensitive files
+*.txt
 .registration_token
 config.yaml
 registrations.json
 banned_emails.txt
 banned_ips.txt
-refresh_token.shbanned_usernames.txt
+refresh_token.sh
+banned_usernames.txt
+testbench/
 
 # Backup directories
 backup/
diff --git a/example-banned_usernames.txt b/banned_usernames.txt_example
similarity index 100%
rename from example-banned_usernames.txt
rename to banned_usernames.txt_example
diff --git a/canary.py b/canary.py
new file mode 100644
index 0000000..db744f5
--- /dev/null
+++ b/canary.py
@@ -0,0 +1,332 @@
+#!/usr/bin/env python3
+
+import yaml
+import requests
+import feedparser
+import datetime
+import subprocess
+import json
+import os
+import sys
+import asyncio
+from time import sleep
+from pathlib import Path
+
+# File paths
+CONFIG_FILE = "config.yaml"
+OUTPUT_FILE = "canary.txt"
+TEMP_MESSAGE_FILE = "temp_canary_message.txt"
+
+# Possible attestations
+ATTESTATIONS = [
+    "We have not received any National Security Letters.",
+    "We have not received any court orders under the Foreign Intelligence Surveillance Act.",
+    "We have not received any gag orders that prevent us from stating we have received legal process.",
+    "We have not been required to modify our systems to facilitate surveillance.",
+    "We have not been subject to any searches or seizures of our servers."
+]
+
+def load_config():
+    """Load configuration from YAML file."""
+    try:
+        if not os.path.exists(CONFIG_FILE):
+            print(f"Error: Configuration file '{CONFIG_FILE}' not found.")
+            print("Please create a configuration file with the following structure:")
+            print("""
+gpg:
+  key_id: YOUR_GPG_KEY_ID
+matrix:
+  enabled: true
+  homeserver: https://we2.ee
+  username: @canary:we2.ee
+  password: YOUR_PASSWORD
+  room_id: !l7XTTF6tudReoEJEvr:we2.ee
+            """)
+            sys.exit(1)
+            
+        with open(CONFIG_FILE, 'r') as file:
+            config = yaml.safe_load(file)
+            
+        # Check for required fields
+        required_fields = [
+            ('gpg', 'key_id')
+        ]
+        
+        for section, field in required_fields:
+            if section not in config or field not in config[section]:
+                print(f"Error: Required configuration field '{section}.{field}' is missing.")
+                sys.exit(1)
+                
+        return config
+    except Exception as e:
+        print(f"Error loading configuration: {e}")
+        sys.exit(1)
+
+def get_current_date():
+    """Return the current date in YYYY-MM-DD format."""
+    return datetime.datetime.now().strftime("%Y-%m-%d")
+
+def get_nist_time():
+    """Get the current time from NIST time server."""
+    try:
+        response = requests.get("https://timeapi.io/api/Time/current/zone?timeZone=UTC", timeout=10)
+        if response.status_code == 200:
+            time_data = response.json()
+            return f"{time_data['dateTime']} UTC"
+        else:
+            print(f"Error fetching NIST time: HTTP {response.status_code}")
+            return None
+    except Exception as e:
+        print(f"Error fetching NIST time: {e}")
+        return None
+
+def get_democracy_now_headline():
+    """Get the latest headline from Democracy Now! RSS feed."""
+    try:
+        feed = feedparser.parse("https://www.democracynow.org/democracynow.rss")
+        if feed.entries and len(feed.entries) > 0:
+            return feed.entries[0].title
+        else:
+            print("No entries found in Democracy Now! RSS feed")
+            return None
+    except Exception as e:
+        print(f"Error fetching Democracy Now! headline: {e}")
+        return None
+
+def get_bitcoin_latest_block():
+    """Get the latest Bitcoin block hash and number."""
+    try:
+        response = requests.get("https://blockchain.info/latestblock", timeout=10)
+        if response.status_code == 200:
+            data = response.json()
+            # Get block details
+            block_response = requests.get(f"https://blockchain.info/rawblock/{data['hash']}", timeout=10)
+            if block_response.status_code == 200:
+                block_data = block_response.json()
+                return {
+                    "height": data["height"],
+                    "hash": data["hash"],
+                    "time": datetime.datetime.fromtimestamp(block_data["time"]).strftime("%Y-%m-%d %H:%M:%S UTC")
+                }
+        print(f"Error fetching Bitcoin block: HTTP {response.status_code}")
+        return None
+    except Exception as e:
+        print(f"Error fetching Bitcoin block data: {e}")
+        return None
+
+def collect_attestations():
+    """Prompt user for each attestation."""
+    selected_attestations = []
+    
+    print("\nPlease confirm each attestation separately:")
+    for i, attestation in enumerate(ATTESTATIONS, 1):
+        while True:
+            response = input(f"Confirm attestation {i}: '{attestation}' (y/n): ").lower()
+            if response in ['y', 'n']:
+                break
+            print("Please answer 'y' or 'n'.")
+        
+        if response == 'y':
+            selected_attestations.append(attestation)
+    
+    return selected_attestations
+
+def create_warrant_canary_message(config):
+    """Create the warrant canary message with attestations and verification elements."""
+    current_date = get_current_date()
+    nist_time = get_nist_time()
+    democracy_now_headline = get_democracy_now_headline()
+    bitcoin_block = get_bitcoin_latest_block()
+    
+    # Check if all required elements are available
+    if not all([nist_time, democracy_now_headline, bitcoin_block]):
+        missing = []
+        if not nist_time: missing.append("NIST time")
+        if not democracy_now_headline: missing.append("Democracy Now! headline")
+        if not bitcoin_block: missing.append("Bitcoin block data")
+        print(f"Error: Could not fetch: {', '.join(missing)}")
+        return None
+    
+    # Collect attestations from user
+    attestations = collect_attestations()
+    if not attestations:
+        print("Warning: No attestations were confirmed.")
+        proceed = input("Do you want to proceed without any attestations? (y/n): ").lower()
+        if proceed != 'y':
+            print("Operation cancelled")
+            return None
+    
+    # Create the message
+    message = f"""We2.ee Warrant Canary
+Date: {current_date}
+
+"""
+    
+    # Add attestations
+    for i, attestation in enumerate(attestations, 1):
+        message += f"{i}. {attestation}\n"
+    
+    message += f"""
+Proofs:
+NIST time: {nist_time}
+Democracy Now! headline: "{democracy_now_headline}"
+Bitcoin block #{bitcoin_block['height']} hash: {bitcoin_block['hash']}
+Bitcoin block time: {bitcoin_block['time']}
+
+"""
+    return message
+
+def sign_with_gpg(message, gpg_key_id):
+    """Sign the warrant canary message with GPG."""
+    try:
+        # Write message to temporary file
+        with open(TEMP_MESSAGE_FILE, "w") as f:
+            f.write(message)
+        
+        # Sign the message with GPG
+        cmd = ["gpg", "--clearsign", "--default-key", gpg_key_id, TEMP_MESSAGE_FILE]
+        subprocess.run(cmd, check=True)
+        
+        # Read the signed message
+        with open(f"{TEMP_MESSAGE_FILE}.asc", "r") as f:
+            signed_message = f.read()
+            
+        # Clean up temporary files
+        os.remove(TEMP_MESSAGE_FILE)
+        os.remove(f"{TEMP_MESSAGE_FILE}.asc")
+        
+        return signed_message
+    except subprocess.CalledProcessError as e:
+        print(f"GPG signing error: {e}")
+        return None
+    except Exception as e:
+        print(f"Error during GPG signing: {e}")
+        return None
+
+def save_warrant_canary(signed_message):
+    """Save the signed warrant canary to a file."""
+    try:
+        with open(OUTPUT_FILE, "w") as f:
+            f.write(signed_message)
+        print(f"Warrant canary saved to {OUTPUT_FILE}")
+        return True
+    except Exception as e:
+        print(f"Error saving warrant canary: {e}")
+        return False
+
+async def post_to_matrix(config, signed_message):
+    """Post the signed warrant canary to Matrix room using nio library."""
+    if not config.get('matrix', {}).get('enabled', False):
+        print("Matrix posting is disabled in config")
+        return False
+    
+    try:
+        from nio import AsyncClient, LoginResponse
+        
+        # Get Matrix config
+        homeserver = config['matrix']['homeserver']
+        username = config['matrix']['username']
+        password = config['matrix']['password']
+        room_id = config['matrix']['room_id']
+        
+        # Extract username without domain for login
+        user_id = username
+        if username.startswith('@'):
+            user_id = username[1:]  # Remove @ prefix
+        if ':' in user_id:
+            user_id = user_id.split(':')[0]  # Remove server part
+        
+        # Create client
+        client = AsyncClient(homeserver, username)
+        
+        # Login
+        print(f"Logging in as {username} on {homeserver}...")
+        response = await client.login(password)
+        
+        if isinstance(response, LoginResponse):
+            print("Login successful")
+        else:
+            print(f"Matrix login failed: {response}")
+            await client.close()
+            return False
+        
+        # Format message for Matrix
+        print(f"Posting canary to room {room_id}...")
+        try:
+            # Use HTML formatting for the message
+            content = {
+                "msgtype": "m.text",
+                "body": signed_message,  # Plain text version
+                "format": "org.matrix.custom.html",
+                "formatted_body": f"<pre>{signed_message}</pre>"  # HTML version with preformatted text
+            }
+            
+            response = await client.room_send(
+                room_id=room_id,
+                message_type="m.room.message",
+                content=content
+            )
+            
+            print("Successfully posted warrant canary to Matrix room")
+        except Exception as e:
+            print(f"Error sending message: {e}")
+            await client.close()
+            return False
+        
+        # Logout and close
+        await client.logout()
+        await client.close()
+        
+        return True
+    except ImportError:
+        print("Error: matrix-nio library not installed. Install with: pip install matrix-nio")
+        return False
+    except Exception as e:
+        print(f"Error posting to Matrix: {e}")
+        return False
+
+def main():
+    print("Generating We2.ee warrant canary...")
+    
+    # Load configuration
+    config = load_config()
+    gpg_key_id = config['gpg']['key_id']
+    
+    # Create message
+    message = create_warrant_canary_message(config)
+    if not message:
+        print("Failed to create warrant canary message")
+        sys.exit(1)
+    
+    # Display the message
+    print("\nWarrant Canary Message Preview:")
+    print("-" * 50)
+    print(message)
+    print("-" * 50)
+    
+    # Confirm with user
+    user_input = input("\nDo you want to sign this message with GPG? (y/n): ")
+    if user_input.lower() != 'y':
+        print("Operation cancelled")
+        sys.exit(0)
+    
+    # Sign and save
+    signed_message = sign_with_gpg(message, gpg_key_id)
+    if not signed_message:
+        print("Failed to sign warrant canary message")
+        sys.exit(1)
+    
+    if save_warrant_canary(signed_message):
+        print("Warrant canary generated successfully!")
+    else:
+        print("Failed to save warrant canary")
+        sys.exit(1)
+    
+    # Post to Matrix if enabled
+    if config.get('matrix', {}).get('enabled', False):
+        post_to_matrix_input = input("\nDo you want to post the warrant canary to Matrix? (y/n): ")
+        if post_to_matrix_input.lower() == 'y':
+            asyncio.run(post_to_matrix(config, signed_message))
+
+if __name__ == "__main__":
+    main()
diff --git a/canary.txt b/canary.txt
new file mode 100644
index 0000000..bc6b80f
--- /dev/null
+++ b/canary.txt
@@ -0,0 +1,34 @@
+-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA512
+
+We2.ee Warrant Canary
+Date: 2025-03-27
+
+1. We have not received any National Security Letters.
+2. We have not received any court orders under the Foreign Intelligence Surveillance Act.
+3. We have not received any gag orders that prevent us from stating we have received legal process.
+4. We have not been required to modify our systems to facilitate surveillance.
+5. We have not been subject to any searches or seizures of our servers.
+
+Proofs:
+NIST time: 2025-03-27T00:32:57.229589 UTC
+Democracy Now! headline: "1,400+ Arrested in Turkey as Erdoğan Jails Istanbul Mayor & Intensifies Authoritarian Crackdown"
+Bitcoin block #889596 hash: 000000000000000000018c38ea9043fd8710fa40d1cf90d5e541d050cd22b89d
+Bitcoin block time: 2025-03-26 23:49:42 UTC
+
+-----BEGIN PGP SIGNATURE-----
+
+iQIzBAEBCgAdFiEEMjqKLEezdiJLNhO3U1smWu2+W0QFAmfknNcACgkQU1smWu2+
+W0RrlxAAinoE3ZsIKAEpt/qzKygQyUx06VozLL82wzLPQrICia+jOkzo6UHuYGmY
+to4sj4SIOBaEyrdIhLvPG7Q6QRnrbn7NVasawRD484KsiO1+caPrnROFKJWyW/II
+UNlAnmOCxGttu14SlKYPpgp/a6LnLQtciNTHEsj6A0i/JgP1kAPRjqOiM0UCXTKf
+2MnNgwHHdjJt3f7AVJewzw5EPsW9ouh7VcIiIu9kZeuGotf0Gux5R8iTg9j2Cpum
+FrsHhdfwgyFFasTtp+sTnsWvmtw86OpIYuqPpopkIe70e3w4m/+C7ybejqNiNlWh
+1HCcFSyP17B6d516BCAKDJlrmCEKEQVz9MkTrqjpEKpZrVzo6Rl9bxQgN0QrohjV
+buUQO9Zyu6Xl7BZSD4qPqGgGeTzRt8pi4BTWtrMMs+JKTel4TimzPONqLh8exYBa
+Go5uDsbOAwnzbK/0VF9KIYqHc2t9pP5IgtUF3HGVZ0IputxTeDCF3uYJMiwO52cK
+XWaSvSlXB+Nc6OIjHHxG35hflk4ch8ZSEchp8OmXIYiy0zC640YwnnAnosg1WCOA
+UAeEvTO+QGyN7uP4rzGn9rtZgyoj5WT9GYGaiHFxrToCo9o3npOOQBAumcXLvP+B
+6Wkd0RKajppKCVEtEKH0/aH57YGC9V5XdZ9o0aa1yDLpWXw7Ag8=
+=5ZtT
+-----END PGP SIGNATURE-----
diff --git a/cleanup.py b/cleanup.py
new file mode 100644
index 0000000..33e3c0f
--- /dev/null
+++ b/cleanup.py
@@ -0,0 +1,133 @@
+import os
+import json
+import yaml
+import httpx
+import logging
+from typing import List, Dict
+from datetime import datetime, timedelta
+
+# Setup logging
+logging.basicConfig(
+    level=logging.INFO,
+    format='%(asctime)s - %(levelname)s - %(message)s'
+)
+logger = logging.getLogger(__name__)
+
+# Load paths and config
+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+CONFIG_PATH = os.path.join(BASE_DIR, "config.yaml")
+REGISTRATIONS_PATH = os.path.join(BASE_DIR, "registrations.json")
+
+def load_config() -> dict:
+    """Load configuration from yaml file."""
+    with open(CONFIG_PATH, "r") as f:
+        return yaml.safe_load(f)
+
+def load_registrations() -> List[Dict]:
+    """Load current registrations from JSON file."""
+    try:
+        with open(REGISTRATIONS_PATH, "r") as f:
+            return json.load(f)
+    except (FileNotFoundError, json.JSONDecodeError):
+        return []
+
+def save_registrations(registrations: List[Dict]):
+    """Save updated registrations back to JSON file."""
+    with open(REGISTRATIONS_PATH, "w") as f:
+        json.dump(registrations, f, indent=2)
+
+async def check_username_exists(username: str, homeserver: str) -> bool:
+    """
+    Check if a username exists on the Matrix server.
+    Returns True if the username exists, False otherwise.
+    """
+    url = f"https://{homeserver}/_matrix/client/v3/register/available?username={username}"
+    async with httpx.AsyncClient() as client:
+        try:
+            response = await client.get(url, timeout=5)
+            if response.status_code == 200:
+                # 200 OK with available=true means username exists
+                return response.json().get("available", False)
+            elif response.status_code == 400:
+                # 400 Bad Request means username is not available
+                return False
+        except httpx.RequestError as ex:
+            logger.error(f"Error checking username {username}: {ex}")
+            return False
+    return False
+
+async def cleanup_registrations(min_age_hours: int = 24):
+    """
+    Clean up registrations by removing entries for usernames that don't exist on the server,
+    but only if they're older than min_age_hours.
+    
+    Never removes entries for existing Matrix users regardless of age.
+    """
+    config = load_config()
+    registrations = load_registrations()
+    
+    if not registrations:
+        logger.info("No registrations found to clean up")
+        return
+
+    logger.info(f"Starting cleanup of {len(registrations)} registrations")
+    logger.info(f"Will only remove non-existent users registered more than {min_age_hours} hours ago")
+    
+    # Track which entries to keep
+    entries_to_keep = []
+    removed_count = 0
+    too_new_count = 0
+    exists_count = 0
+
+    current_time = datetime.utcnow()
+
+    for entry in registrations:
+        username = entry["requested_name"]
+        reg_date = datetime.fromisoformat(entry["datetime"])
+        age = current_time - reg_date
+        
+        # First check if the user exists on Matrix
+        exists = await check_username_exists(username, config["homeserver"])
+        
+        if exists:
+            # Always keep entries for existing Matrix users
+            entries_to_keep.append(entry)
+            exists_count += 1
+            logger.info(f"Keeping registration for existing user: {username}")
+            continue
+            
+        # For non-existent users, check if they're old enough to remove
+        if age < timedelta(hours=min_age_hours):
+            # Keep young entries even if user doesn't exist yet
+            entries_to_keep.append(entry)
+            too_new_count += 1
+            logger.info(f"Keeping recent registration: {username} (age: {age.total_seconds()/3600:.1f} hours)")
+        else:
+            # Remove old entries where user doesn't exist
+            logger.info(f"Removing old registration: {username} (age: {age.total_seconds()/3600:.1f} hours)")
+            removed_count += 1
+
+    # Save updated registrations
+    save_registrations(entries_to_keep)
+    
+    logger.info(f"Cleanup complete:")
+    logger.info(f"- Kept {exists_count} entries for existing Matrix users")
+    logger.info(f"- Kept {too_new_count} entries younger than {min_age_hours} hours")
+    logger.info(f"- Removed {removed_count} old entries for non-existent users")
+    logger.info(f"- Total remaining entries: {len(entries_to_keep)}")
+
+if __name__ == "__main__":
+    import asyncio
+    import argparse
+    
+    parser = argparse.ArgumentParser(description="Clean up Matrix registration entries")
+    parser.add_argument(
+        "--min-age-hours", 
+        type=int,
+        default=24,
+        help="Minimum age in hours before removing non-existent users (default: 24)"
+    )
+    
+    args = parser.parse_args()
+    
+    asyncio.run(cleanup_registrations(args.min_age_hours))
diff --git a/conduwuit_logs.txt b/conduwuit_logs.txt
new file mode 100644
index 0000000..2efa955
--- /dev/null
+++ b/conduwuit_logs.txt
@@ -0,0 +1,74 @@
+2025-03-24T04:11:24.174302Z  WARN conduwuit_core::config::check: Config parameter "log_level" is unknown to conduwuit, ignoring.
+2025-03-24T04:11:24.174516Z  INFO conduwuit::server: 0.5.0 (b6e9dc3) server_name=we2.ee database_path="/var/lib/conduwuit/conduwuit.db" log_levels=info
+2025-03-24T04:11:24.999106Z  INFO main:start:open: conduwuit_database::engine::open: Opened database. columns=87 sequence=1216675526 time=788.193091ms
+2025-03-24T04:11:25.232053Z  INFO main:start: conduwuit_service::migrations: Loaded RocksDB database with schema version 17
+2025-03-24T04:11:25.625176Z  INFO conduwuit_router::serve::plain: Listening on [0.0.0.0:8008]
+  2025-03-24T04:11:25.690807Z ERROR conduwuit_service::sending::sender: Missing pusher, user_id: "@sij:we2.ee", pushkey: "SlcxERw6XkNKx6Vu2GR4F4U1cvf98nyBM/Q6MIZ9TJg="
+    at src/service/sending/sender.rs:761 on conduwuit:worker ThreadId(3)
+    in conduwuit_service::sending::sender::push with user_id="@sij:we2.ee" pushkey="SlcxERw6XkNKx6Vu2GR4F4U1cvf98nyBM/Q6MIZ9TJg=" events=1
+
+  2025-03-24T04:11:25.696119Z ERROR conduwuit_service::sending::sender: Missing pusher, user_id: "@tomasz:we2.ee", pushkey: "https://ntfy.schildi.chat/up66QHbupxHL1u?up=1"
+    at src/service/sending/sender.rs:761 on conduwuit:worker ThreadId(7)
+    in conduwuit_service::sending::sender::push with user_id="@tomasz:we2.ee" pushkey="https://ntfy.schildi.chat/up66QHbupxHL1u?up=1" events=1
+
+  2025-03-24T04:11:25.724057Z ERROR conduwuit_service::sending::sender: Missing pusher, user_id: "@tomasz:we2.ee", pushkey: "https://ntfy.schildi.chat/upd06nnOhELBFx?up=1"
+    at src/service/sending/sender.rs:761 on conduwuit:worker ThreadId(14)
+    in conduwuit_service::sending::sender::push with user_id="@tomasz:we2.ee" pushkey="https://ntfy.schildi.chat/upd06nnOhELBFx?up=1" events=1
+
+  2025-03-24T04:11:25.806002Z ERROR conduwuit_service::sending::sender: Missing pusher, user_id: "@tomasz:we2.ee", pushkey: "https://ntfy.schildi.chat/upclfCDJ0va5d9?up=1"
+    at src/service/sending/sender.rs:761 on conduwuit:worker ThreadId(7)
+    in conduwuit_service::sending::sender::push with user_id="@tomasz:we2.ee" pushkey="https://ntfy.schildi.chat/upclfCDJ0va5d9?up=1" events=1
+
+  2025-03-24T04:11:25.835727Z ERROR conduwuit_service::sending::sender: Missing pusher, user_id: "@sij:we2.ee", pushkey: "M8ltVIFwxX/M1kKNuCGB7tQWAYppEJO6dkdY0JHJEvg="
+    at src/service/sending/sender.rs:761 on conduwuit:worker ThreadId(14)
+    in conduwuit_service::sending::sender::push with user_id="@sij:we2.ee" pushkey="M8ltVIFwxX/M1kKNuCGB7tQWAYppEJO6dkdY0JHJEvg=" events=1
+
+  2025-03-24T04:11:25.910755Z ERROR conduwuit_service::sending::sender: Missing pusher, user_id: "@tomasz:we2.ee", pushkey: "https://ntfy.schildi.chat/upkzMxi8WiQ5U5?up=1"
+    at src/service/sending/sender.rs:761 on conduwuit:worker ThreadId(14)
+    in conduwuit_service::sending::sender::push with user_id="@tomasz:we2.ee" pushkey="https://ntfy.schildi.chat/upkzMxi8WiQ5U5?up=1" events=1
+
+  2025-03-24T04:11:25.912156Z ERROR conduwuit_service::sending::sender: Missing pusher, user_id: "@sij:we2.ee", pushkey: "ly2ipat4bxsv/fQ0TRswYlU9zD5yhVb9mnrQ2NZ5e1A="
+    at src/service/sending/sender.rs:761 on conduwuit:worker ThreadId(14)
+    in conduwuit_service::sending::sender::push with user_id="@sij:we2.ee" pushkey="ly2ipat4bxsv/fQ0TRswYlU9zD5yhVb9mnrQ2NZ5e1A=" events=1
+
+  2025-03-24T04:11:25.933229Z ERROR conduwuit_service::sending::sender: Missing pusher, user_id: "@tomasz:we2.ee", pushkey: "https://ntfy.schildi.chat/upfxUUwV0ecpTR?up=1"
+    at src/service/sending/sender.rs:761 on conduwuit:worker ThreadId(7)
+    in conduwuit_service::sending::sender::push with user_id="@tomasz:we2.ee" pushkey="https://ntfy.schildi.chat/upfxUUwV0ecpTR?up=1" events=1
+
+  2025-03-24T04:11:26.118584Z ERROR conduwuit_service::sending::sender: Missing pusher, user_id: "@tomasz:we2.ee", pushkey: "https://ntfy.schildi.chat/upxH2vJWhUrLsD?up=1"
+    at src/service/sending/sender.rs:761 on conduwuit:worker ThreadId(14)
+    in conduwuit_service::sending::sender::push with user_id="@tomasz:we2.ee" pushkey="https://ntfy.schildi.chat/upxH2vJWhUrLsD?up=1" events=1
+
+  2025-03-24T04:11:26.206792Z ERROR conduwuit_service::sending::sender: Missing pusher, user_id: "@tomasz:we2.ee", pushkey: "https://ntfy.sh/up4KrJgOMfmGxD?up=1"
+    at src/service/sending/sender.rs:761 on conduwuit:worker ThreadId(7)
+    in conduwuit_service::sending::sender::push with user_id="@tomasz:we2.ee" pushkey="https://ntfy.sh/up4KrJgOMfmGxD?up=1" events=1
+
+  2025-03-24T04:11:26.488520Z ERROR conduwuit_service::sending::sender: Missing pusher, user_id: "@tomasz:we2.ee", pushkey: "https://ntfy.schildi.chat/up2nJSKdtIoisj?up=1"
+    at src/service/sending/sender.rs:761 on conduwuit:worker ThreadId(14)
+    in conduwuit_service::sending::sender::push with user_id="@tomasz:we2.ee" pushkey="https://ntfy.schildi.chat/up2nJSKdtIoisj?up=1" events=1
+
+  2025-03-24T04:11:26.528252Z ERROR conduwuit_service::sending::sender: Missing pusher, user_id: "@sij:we2.ee", pushkey: "TjhzIT+QrIPqAAdaxzmfxtraX3xlYN9CRx3T8snGJsY="
+    at src/service/sending/sender.rs:761 on conduwuit:worker ThreadId(14)
+    in conduwuit_service::sending::sender::push with user_id="@sij:we2.ee" pushkey="TjhzIT+QrIPqAAdaxzmfxtraX3xlYN9CRx3T8snGJsY=" events=1
+
+  2025-03-24T04:11:26.898903Z ERROR conduwuit_service::sending::sender: Missing pusher, user_id: "@sij:we2.ee", pushkey: "kVAJHyqTY6nD3fXuMOz5uFQy11KF6nNw9jzZT9vH8lc="
+    at src/service/sending/sender.rs:761 on conduwuit:worker ThreadId(2)
+    in conduwuit_service::sending::sender::push with user_id="@sij:we2.ee" pushkey="kVAJHyqTY6nD3fXuMOz5uFQy11KF6nNw9jzZT9vH8lc=" events=1
+
+  2025-03-24T04:11:26.905588Z ERROR conduwuit_service::sending::sender: Missing pusher, user_id: "@oddlid:we2.ee", pushkey: "oUT1KtxnLMQxuG7otnPxZlY28ZFP7dyC0B3LQlQJm70="
+    at src/service/sending/sender.rs:761 on conduwuit:worker ThreadId(7)
+    in conduwuit_service::sending::sender::push with user_id="@oddlid:we2.ee" pushkey="oUT1KtxnLMQxuG7otnPxZlY28ZFP7dyC0B3LQlQJm70=" events=1
+
+  2025-03-24T04:11:27.008417Z ERROR conduwuit_service::sending::sender: Missing pusher, user_id: "@sij:we2.ee", pushkey: "yBBcAp+lJHry3qIkOnjtuXQ+fvXsmOqIJT3s+723Wf0="
+    at src/service/sending/sender.rs:761 on conduwuit:worker ThreadId(7)
+    in conduwuit_service::sending::sender::push with user_id="@sij:we2.ee" pushkey="yBBcAp+lJHry3qIkOnjtuXQ+fvXsmOqIJT3s+723Wf0=" events=1
+
+  2025-03-24T04:11:27.111117Z ERROR conduwuit_service::sending::sender: Missing pusher, user_id: "@coldsideofyourpillow:we2.ee", pushkey: "https://updates.push.services.mozilla.com/wpush/v1/gAAAAABngkOarCdQHaOOcbWvEq2NRTaqanvveX2x7tORFgnhjomYiTMOjaPw9zTrQVe8ConiPBHKvNAMd96-jLQURG5Q83zk1d3lT2mPW6ek6MatC7-f3ipoxJC-8hwiXLtARI5WfNl-"
+    at src/service/sending/sender.rs:761 on conduwuit:worker ThreadId(2)
+    in conduwuit_service::sending::sender::push with user_id="@coldsideofyourpillow:we2.ee" pushkey="https://updates.push.services.mozilla.com/wpush/v1/gAAAAABngkOarCdQHaOOcbWvEq2NRTaqanvveX2x7tORFgnhjomYiTMOjaPw9zTrQVe8ConiPBHKvNAMd96-jLQURG5Q83zk1d3lT2mPW6ek6MatC7-f3ipoxJC-8hwiXLtARI5WfNl-" events=1
+
+  2025-03-24T04:11:27.141576Z ERROR conduwuit_service::sending::sender: Missing pusher, user_id: "@tomasz:we2.ee", pushkey: "https://ntfy.schildi.chat/upqkpYOQT8t3jd?up=1"
+    at src/service/sending/sender.rs:761 on conduwuit:worker ThreadId(14)
+    in conduwuit_service::sending::sender::push with user_id="@tomasz:we2.ee" pushkey="https://ntfy.schildi.chat/upqkpYOQT8t3jd?up=1" events=1
+
+2025-03-24T04:11:36.014821Z  WARN hickory_proto::udp::udp_client_stream: expected message id: 35283 got: 31381, dropped
diff --git a/refresh_token.sh b/refresh_token.sh
index 8ff8b49..82df071 100755
--- a/refresh_token.sh
+++ b/refresh_token.sh
@@ -6,29 +6,21 @@ TOKEN_FILE="$BASE_PATH/.registration_token"
 LOG_FILE="$BASE_PATH/token_refresh.log"
 BACKUP_PATH="/home/sij/conduwuit_backup"
 
-# Server configuration
+# Server/domain info
 SERVER_DOMAIN="we2.ee"
-HOST_PORT=8448
-CONTAINER_PORT=6167
 CONTAINER_NAME="conduwuit"
-CONTAINER_IMAGE="ghcr.io/girlbossceo/conduwuit:v0.5.0-rc3-b6e9dc3d98704c56027219d3775336910a0136c6"
-
-# Performance tuning
-DB_READ_CACHE_MB=16384        # 16GB for read cache
-DB_WRITE_BUFFER_MB=2048       # 2GB write buffer
-CACHE_MODIFIER=4.0            # 4x default LRU caches
-DB_POOL_WORKERS=128           # Optimized for NVMe
-STREAM_WIDTH_SCALE=2.0        # Concurrent operations scaling
-STREAM_AMPLIFICATION=4096     # Batch size for operations
-MAX_REQUEST_SIZE=104857600    # 100MB uploads
-BLURHASH_MAX_SIZE=134217728   # 128MB for blurhash processing
+CONTAINER_IMAGE="conduwuit:custom"
+ADDRESS='["0.0.0.0", "::"]'
+PORT=8008
 
 # Auto-join room configuration
-AUTO_JOIN_ROOMS="[\"#pub:$SERVER_DOMAIN\",\"#home:$SERVER_DOMAIN\"]"
+AUTO_JOIN_ROOMS='["#server:we2.ee"]'
 
-# Function to log with timestamp
+# Function to log with timestamp to both file and terminal
 log() {
-    echo "$(date --iso-8601=seconds) $1" >> "$LOG_FILE"
+    local message="$(date --iso-8601=seconds) $1"
+    echo "$message" >> "$LOG_FILE"  # Write to log file
+    echo "$message"  # Print to terminal
 }
 
 # Generate new token (6 random hex characters)
@@ -43,13 +35,13 @@ fi
 
 log "Generated new registration token"
 
-# Recreate conduwuit container
-docker stop $CONTAINER_NAME
-docker rm $CONTAINER_NAME
+# Stop and remove existing container
+docker stop "$CONTAINER_NAME" 2>/dev/null
+docker rm "$CONTAINER_NAME" 2>/dev/null
 
+# Launch new container
 docker run -d \
-  -p 0.0.0.0:${HOST_PORT}:${CONTAINER_PORT} \
-  -v db:/var/lib/conduwuit/ \
+  -v "db:/var/lib/conduwuit/" \
   -v "${TOKEN_FILE}:/.registration_token:ro" \
   -v "${BACKUP_PATH}:/backup" \
   -e CONDUWUIT_SERVER_NAME="$SERVER_DOMAIN" \
@@ -57,28 +49,44 @@ docker run -d \
   -e CONDUWUIT_DATABASE_BACKUP_PATH="/backup" \
   -e CONDUWUIT_ALLOW_REGISTRATION=true \
   -e CONDUWUIT_REGISTRATION_TOKEN_FILE="/.registration_token" \
-  -e CONDUWUIT_PORT=$CONTAINER_PORT \
-  -e CONDUWUIT_ADDRESS="0.0.0.0" \
+  -e CONDUWUIT_ADDRESS="$ADDRESS" \
+  -e CONDUWUIT_PORT="$PORT" \
   -e CONDUWUIT_NEW_USER_DISPLAYNAME_SUFFIX="" \
+  -e CONDUWUIT_AUTO_JOIN_ROOMS="$AUTO_JOIN_ROOMS" \
+  -e CONDUWUIT_FORGET_FORCED_UPON_LEAVE=true \
+  -e CONDUWUIT_DB_CACHE_CAPACITY_MB=1024 \
+  -e CONDUWUIT_DB_WRITE_BUFFER_CAPACITY_MB=256 \
+  -e CONDUWUIT_DB_POOL_WORKERS=64 \
+  -e CONDUWUIT_DB_POOL_WORKERS_LIMIT=128 \
+  -e CONDUWUIT_STREAM_AMPLIFICATION=8192 \
+  -e CONDUWUIT_MAX_REQUEST_SIZE=33554432 \
+  -e CONDUWUIT_CACHE_CAPACITY_MODIFIER=1.5 \
+  -e CONDUWUIT_ALLOW_FEDERATION=true \
   -e CONDUWUIT_ALLOW_PUBLIC_ROOM_DIRECTORY_OVER_FEDERATION=true \
   -e CONDUWUIT_ALLOW_PUBLIC_ROOM_DIRECTORY_WITHOUT_AUTH=true \
-  -e CONDUWUIT_ALLOW_FEDERATION=true \
-  -e CONDUWUIT_AUTO_JOIN_ROOMS="$AUTO_JOIN_ROOMS" \
-  -e CONDUWUIT_DB_CACHE_CAPACITY_MB=$DB_READ_CACHE_MB \
-  -e CONDUWUIT_DB_WRITE_BUFFER_CAPACITY_MB=$DB_WRITE_BUFFER_MB \
-  -e CONDUWUIT_CACHE_CAPACITY_MODIFIER=$CACHE_MODIFIER \
-  -e CONDUWUIT_DB_POOL_WORKERS=$DB_POOL_WORKERS \
-  -e CONDUWUIT_STREAM_WIDTH_SCALE=$STREAM_WIDTH_SCALE \
-  -e CONDUWUIT_STREAM_AMPLIFICATION=$STREAM_AMPLIFICATION \
-  -e CONDUWUIT_MAX_REQUEST_SIZE=$MAX_REQUEST_SIZE \
-  -e CONDUWUIT_BLURHASH_MAX_RAW_SIZE=$BLURHASH_MAX_SIZE \
-  --name $CONTAINER_NAME \
+  -e CONDUWUIT_WELL_KNOWN_CONN_TIMEOUT=30 \
+  -e CONDUWUIT_FEDERATION_TIMEOUT=600 \
+  -e CONDUWUIT_FEDERATION_IDLE_TIMEOUT=60 \
+  -e CONDUWUIT_SENDER_TIMEOUT=600 \
+  -e CONDUWUIT_SENDER_IDLE_TIMEOUT=360 \
+  -e CONDUWUIT_SENDER_SHUTDOWN_TIMEOUT=30 \
+  -e CONDUWUIT_DNS_CACHE_ENTRIES=1000 \
+  -e CONDUWUIT_DNS_MIN_TTL=300 \
+  -e CONDUWUIT_DNS_MIN_TTL_NXDOMAIN=600 \
+  -e CONDUWUIT_DNS_TCP_FALLBACK=true \
+  -e CONDUWUIT_IP_LOOKUP_STRATEGY=3 \
+  -e RUST_LOG="conduwuit=trace,reqwest=trace,hickory_proto=trace" \
+  --network host \
+  --name "$CONTAINER_NAME" \
   --restart unless-stopped \
-  $CONTAINER_IMAGE
-
+  "$CONTAINER_IMAGE"
 if [ $? -ne 0 ]; then
     log "ERROR: Failed to create new conduwuit container"
     exit 1
 fi
 
-log "Successfully recreated conduwuit container with new token"
+log "Successfully recreated container \"$CONTAINER_NAME\" with image \"$CONTAINER_IMAGE\" and these parameters:"
+log " - domain: $SERVER_DOMAIN"
+log " - address: $ADDRESS"
+log " - port: $PORT"
+log " - auto-join rooms: $AUTO_JOIN_ROOMS"
diff --git a/registration.py b/registration.py
index 8f5323a..dcb0e93 100644
--- a/registration.py
+++ b/registration.py
@@ -368,6 +368,8 @@ async def register(
         raise HTTPException(status_code=500, detail="Registration token file not found.")
 
     time_until_reset = get_time_until_reset_str(now)
+    
+    # Plain text email body
     email_body = config["email_body"].format(
         homeserver=config["homeserver"],
         registration_token=token,
@@ -375,9 +377,23 @@ async def register(
         utc_time=now.strftime("%H:%M:%S"),
         time_until_reset=time_until_reset
     )
+    
+    # HTML email body
+    email_body_html = config.get("email_body_html", "").format(
+        homeserver=config["homeserver"],
+        registration_token=token,
+        requested_username=requested_username,
+        utc_time=now.strftime("%H:%M:%S"),
+        time_until_reset=time_until_reset
+    )
 
     msg = EmailMessage()
     msg.set_content(email_body)
+    
+    # Add HTML version if configured
+    if email_body_html:
+        msg.add_alternative(email_body_html, subtype='html')
+        
     msg["Subject"] = config["email_subject"].format(homeserver=config["homeserver"])
     msg["From"] = config["smtp"]["username"]
     msg["To"] = email
diff --git a/relaunch_without_refresh.sh b/relaunch_without_refresh.sh
new file mode 100755
index 0000000..7ef9016
--- /dev/null
+++ b/relaunch_without_refresh.sh
@@ -0,0 +1,61 @@
+#!/bin/bash
+
+# File paths
+BASE_PATH="/home/sij/hand_of_morpheus"
+TOKEN_FILE="$BASE_PATH/.registration_token"
+BACKUP_PATH="/home/sij/conduwuit_backup"
+
+# Server/domain info
+SERVER_DOMAIN="we2.ee"
+HOST="127.0.0.1"
+HOST_PORT=8448
+CONTAINER_PORT=6167
+CONTAINER_NAME="conduwuit"
+CONTAINER_IMAGE="ghcr.io/girlbossceo/conduwuit:v0.5.0-rc3-b6e9dc3d98704c56027219d3775336910a0136c6"
+
+# Keep max request size
+MAX_REQUEST_SIZE=33554432  # 32MB
+
+# Auto-join room configuration
+AUTO_JOIN_ROOMS="[\"#pub:we2.ee\",\"#home:we2.ee\"]"
+TRUSTED_SERVERS="[\"matrix.org\",\"envs.net\",\"tchncs.de\"]"
+BANNED_SERVERS="[\"tzchat.org\"]"
+NO_MEDIA_FROM="[\"bark.lgbt\",\"cutefunny.art\",\"tzchat.org\",\"nitro.chat\",\"lolispace.moe\",\"lolisho.chat\",\"midov.pl\"]"
+
+# Recreate Conduwuit container
+docker stop "$CONTAINER_NAME"
+docker rm "$CONTAINER_NAME"
+
+docker run -d \
+  -p "${HOST}:${HOST_PORT}:${CONTAINER_PORT}" \
+  -v "db:/var/lib/conduwuit/" \
+  -v "${TOKEN_FILE}:/.registration_token:ro" \
+  -v "${BACKUP_PATH}:/backup" \
+  -e CONDUWUIT_SERVER_NAME="$SERVER_DOMAIN" \
+  -e CONDUWUIT_DATABASE_PATH="/var/lib/conduwuit/conduwuit.db" \
+  -e CONDUWUIT_DATABASE_BACKUP_PATH="/backup" \
+  -e CONDUWUIT_ALLOW_REGISTRATION=true \
+  -e CONDUWUIT_REGISTRATION_TOKEN_FILE="/.registration_token" \
+  -e CONDUWUIT_PORT=$CONTAINER_PORT \
+  -e CONDUWUIT_ADDRESS="0.0.0.0" \
+  -e CONDUWUIT_NEW_USER_DISPLAYNAME_SUFFIX="" \
+  -e CONDUWUIT_ALLOW_PUBLIC_ROOM_DIRECTORY_OVER_FEDERATION=true \
+  -e CONDUWUIT_ALLOW_PUBLIC_ROOM_DIRECTORY_WITHOUT_AUTH=false \
+  -e CONDUWUIT_ALLOW_FEDERATION=true \
+  -e CONDUWUIT_AUTO_JOIN_ROOMS="$AUTO_JOIN_ROOMS" \
+  -e CONDUWUIT_MAX_REQUEST_SIZE=$MAX_REQUEST_SIZE \
+  -e CONDUWUIT_LOG=debug \
+  -e CONDUWUIT_LOG_SPAN_EVENTS=all \
+  -e CONDUWUIT_LOG_COLORS=true \
+  -e CONDUWUIT_TRUSTED_SERVERS=$TRUSTED_SERVERS \
+  -e CONDUWUIT_PRUNE_MISSING_MEDIA=true \
+  -e CONDUWUIT_ALLOW_LEGACY_MEDIA=false \
+  -e CONDUWUIT_IP_RANGE_DENYLIST="[]" \
+  -e CONDUWUIT_AUTO_DEACTIVATE_BANNED_ROOM_ATTEMPTS=true \
+  -e CONDUWUIT_PREVENT_MEDIA_DOWNLOADS_FROM=$NO_MEDIA_FROM \
+  -e CONDUWUIT_IP_LOOKUP_STRATEGY="1" \
+  -e CONDUWUIT_QUERY_OVER_TCP_ONLY=true \
+  -e CONDUWUIT_QUERY_ALL_NAMESERVERS=false \
+  --name "$CONTAINER_NAME" \
+  --restart unless-stopped \
+  "$CONTAINER_IMAGE"
diff --git a/update_conduwuit.sh b/update_conduwuit.sh
new file mode 100644
index 0000000..c0ac7e2
--- /dev/null
+++ b/update_conduwuit.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+# Navigate to the repository directory
+cd "$HOME/conduwuit" || exit
+
+# Pull the latest changes
+git pull
+
+# Build the Docker image using Nix
+nix build -L --extra-experimental-features "nix-command flakes" .#oci-image-x86_64-linux-musl-all-features
+
+# Extract the image tarball path from the build result
+IMAGE_TAR_PATH=$(nix path-info -r .#oci-image-x86_64-linux-musl-all-features)/image.tar.gz
+
+# Load the image into Docker and tag it
+docker load < "$IMAGE_TAR_PATH" | awk '/Loaded image:/ { print $3 }' | xargs -I {} docker tag {} conduwuit:custom
+
+# Confirm tagging
+echo "Docker image tagged as conduwuit:custom"