bhcli

A TUI for chatting on LE PHP Chats
git clone https://git.dasho.dev/bhcli.git
Log | Files | Refs | README

commit c0b2e3ad3d479a908ac5aa85e704f2d3739e5aef
parent 012e5da2670300a43196c332e49e3b8b21c27d2c
Author: Dasho <git@dasho.dev>
Date:   Fri,  9 Jan 2026 08:49:13 +0000

Revamps project structure and enhances functionality

Overhauls .gitignore to manage build artifacts and editor files.
Introduces Openssl dependencies for improved security and updates Unicode library version.
Reintroduces Cargo.toml with more detailed dependencies and features.
Adds detailed user manual, provides comprehensive guidance on usage.
Replaces Makefile with a modern alternative supporting multi-platform build strategies.
Installs man page for command-line documentation.

Improves modularity by removing obsolete methods and consolidating audio playback setup.
Reduces CPU usage for better performance and integrates optional audio feature.
Eliminates redundant external editor integration functionality.

Diffstat:
M.gitignore | 53++++++++++++++++++++++++++++++++++++++++++++++++-----
D.vscode/settings.json | 4----
DCMDS.md | 42------------------------------------------
MCargo.lock | 14++++++++++++--
MCargo.toml | 114++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
AMANUAL.md | 1661+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MMakefile | 352++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
MREADME.md | 683+++++++++++++++++++++++++++----------------------------------------------------
Dchat-script.READONLY.php | 8563-------------------------------------------------------------------------------
Ademo.png | 0
Amanpage/bhcli.1 | 488+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dscreenshot.png | 0
Msrc/account_management.rs | 37+------------------------------------
Msrc/main.rs | 431+++++++++++++++++++------------------------------------------------------------
Dstrange_bhcli.jpg | 0
15 files changed, 2938 insertions(+), 9504 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,8 +1,52 @@ +# Rust build artifacts +/target/ +/dist/ +Cargo.lock + +# Debug and profiling +*.pdb +*.profdata +*.profraw + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Rust-specific +**/*.rs.bk +*.pdb + +# Project-specific captcha.gif -samples -target -dist +samples/ *.log *.svg *.env -.cargo_home/ -\ No newline at end of file +.cargo_home/ +bot_data/ + +# Temporary files +*.tmp +*.temp +.*.swp + +# System files +.DS_Store +Thumbs.db + +# Test coverage +*.profraw +*.profdata +coverage/ +tarpaulin-report.html + +# Documentation builds +/doc/ + +# Configuration (may contain sensitive data) +config.toml +*.toml.bak diff --git a/.vscode/settings.json b/.vscode/settings.json @@ -1,3 +0,0 @@ -{ - "makefile.configureOnOpen": true -} -\ No newline at end of file diff --git a/CMDS.md b/CMDS.md @@ -1,42 +0,0 @@ -# Commands - -## Chat Commands -- `/pm <user> <msg>` private message -- `/kick <user> [msg]` kick a user -- `/ban <user>` ban usernames containing `<user>` (also kicks) -- `/ban "<user>"` ban an exact username -- `/banlist` list banned username filters -- `/banexactlist` list exact banned usernames -- `/unban <user>` remove banned username -- `/unban "<user>"` remove exact banned username -- `/filter <text>` filter messages containing text -- `/unfilter <text>` remove message filter -- `/filterlist` list filtered message terms -- `/ignore <user>` ignore user -- `/unignore <user>` unignore user -- `/dl` delete last message -- `/dlN` delete last N messages (e.g. `/dl5`) -- `/dall` delete all messages -- `/delete <id>` delete message with id -- `/u <path> [@target] [msg]` upload file - -## Keyboard shortcuts -- `Ctrl+k` prefill `/kick <username>` for selected message -- `Ctrl+b` prefill `/ban <username>` for selected message -- `Ctrl+Shift+B` prefill `/ban "<username>"` for selected message -- `x` prefill `/delete <id>` for selected message -- `t` tag selected user -- `p` pm selected user -- `y` or `Ctrl+C` copy selected message -- `Shift+Y` copy first link in message -- `m` toggle notifications -- `Shift+G` guest view -- `Shift+M` members view -- `Shift+V` toggle staff view -- `Shift+C` toggle clean mode -- - `x` to delete selected message when in clean mode -- `Shift+H` toggle hidden messages -- `Shift+T` translate text to English -- `Ctrl+A` prefill `/pm <master> /m ` or `/m ` (if no master account is set) -- `Ctrl+D`/`PageDown` scroll down -- `Ctrl+U`/`PageUp` scroll up diff --git a/Cargo.lock b/Cargo.lock @@ -2315,6 +2315,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] +name = "openssl-src" +version = "300.5.4+3.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" +dependencies = [ + "cc", +] + +[[package]] name = "openssl-sys" version = "0.9.109" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2322,6 +2331,7 @@ checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] @@ -3917,9 +3927,9 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] diff --git a/Cargo.toml b/Cargo.toml @@ -1,49 +1,91 @@ [package] name = "bhcli" version = "0.1.0" -edition = "2018" +edition = "2021" +authors = ["Dasho"] +description = "A sophisticated terminal-based client for le-chat-php chat systems" +repository = "https://github.com/d-a-s-h-o/bhcli" +license = "MIT" +rust-version = "1.70" + +# Optimize for size and performance in release builds +[profile.release] +opt-level = 3 # Maximum optimization +lto = true # Enable Link Time Optimization +codegen-units = 1 # Better optimization, slower compile +strip = true # Strip symbols from binary +panic = "abort" # Smaller binary size [profile.dev] debug = true -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +opt-level = 0 + +# Define feature flags +[features] +default = ["audio"] +audio = ["dep:rodio"] [dependencies] -anyhow = "1.0.70" -base64 = "0.21.0" -bresenham = "0.1.1" -chrono = "0.4.19" -clap = { version = "4.1.14", features = ["derive", "env"] } -clipboard = "0.5.0" -colors-transform = "0.2.4" -confy = "0.5.1" -crossbeam = "0.8.1" -crossbeam-channel = "0.5.15" -crossterm = { version = "0.26.1" } -http = "0.2.4" -image = "0.24.6" -lazy_static = "1.4.0" -linkify = "0.9.0" -log = "0.4.17" -log4rs = "1.2.0" -rand = "0.8.4" -regex = "1.5.4" -reqwest = { version = "0.11.4", features = ["blocking", "cookies", "socks", "multipart"] } -rodio = "0.17.1" -rpassword = "7.2.0" -select = "0.6.0-alpha.1" -serde = "1.0.130" -serde_derive = "1.0.88" -serde_json = "1.0" -termage = "1.1.1" -textwrap = "0.16.0" -toml = "0.7.3" -tui = { version = "0.19.0", features = ["crossterm"], default-features = false } -unicode-width = "0.1.8" -async-openai = "0.29.0" +# Core dependencies +anyhow = "1.0" +clap = { version = "4.1", features = ["derive", "env"] } +log = "0.4" +log4rs = "1.2" + +# Terminal UI +crossterm = "0.26" +tui = { version = "0.19", features = ["crossterm"], default-features = false } +termage = "1.1" + +# Async runtime tokio = { version = "1.0", features = ["full"] } -# ChatOps dependencies + +# HTTP client +reqwest = { version = "0.11", features = ["blocking", "cookies", "socks", "multipart", "native-tls-vendored"] } +http = "0.2" + +# AI integration +async-openai = "0.29" + +# Serialization +serde = "1.0" +serde_derive = "1.0" +serde_json = "1.0" +toml = "0.7" +confy = "0.5" + +# Text processing +regex = "1.5" +linkify = "0.9" +textwrap = "0.16" +unicode-width = "0.1" +select = "0.6" + +# Utilities +chrono = "0.4" uuid = { version = "1.0", features = ["v4"] } +lazy_static = "1.4" +rand = "0.8" +base64 = "0.21" +tempfile = "3.10" + +# Cryptography md5 = "0.7" sha1 = "0.10" sha2 = "0.10" -tempfile = "3.10" + +# System integration +clipboard = "0.5" +rpassword = "7.2" +colors-transform = "0.2" +image = "0.24" + +# Optional audio support +rodio = { version = "0.17", optional = true } + +# Threading +crossbeam = "0.8" +crossbeam-channel = "0.5" + +# Graphics/Captcha support +bresenham = "0.1" diff --git a/MANUAL.md b/MANUAL.md @@ -0,0 +1,1661 @@ +# BHCLI Manual + +Complete reference guide for BHCLI. Everything you need to master the client. + +## Table of Contents + +1. [Command Line Arguments](#command-line-arguments) +2. [Configuration Files](#configuration-files) +3. [ChatOps Commands](#chatops-commands) +4. [Keyboard Shortcuts](#keyboard-shortcuts) +5. [Bot System](#bot-system) +6. [AI Features](#ai-features) +7. [Moderation System](#moderation-system) +8. [Custom Commands](#custom-commands) +9. [Advanced Features](#advanced-features) + +--- + +## Command Line Arguments + +BHCLI supports extensive command line configuration. Most settings can also be configured via environment variables or the config file. + +### Authentication and Profile + +```bash +-u, --username <USERNAME> + Set your username + Environment: BHC_USERNAME + +-p, --password <PASSWORD> + Set your password + Environment: BHC_PASSWORD + +-c, --profile <PROFILE> + Select configuration profile (default: "default") + Profiles are stored in ~/.config/bhcli/bhcli.toml + +--session <SESSION> + Use an existing session ID to skip login + Useful for reconnecting without re-authenticating +``` + +### Connection and Network + +```bash +--url <URL> + Override the default chat server URL + Example: --url "http://example.onion" + +--page-php <PAGE> + Override chat page filename (default: chat.php) + Some servers use different page names + +-s, --socks-proxy-url <URL> + SOCKS proxy URL for Tor connection + Default: socks5h://127.0.0.1:9050 + Environment: BHC_PROXY_URL + +--no-proxy + Disable proxy usage entirely + Warning: Not recommended for .onion addresses + +-r, --refresh-rate <SECONDS> + Message refresh rate in seconds (default: 5) + Lower = more responsive, higher = less bandwidth + Environment: BHC_REFRESH_RATE + +--datetime-fmt <FORMAT> + Override datetime format string + Uses standard strftime format + +--members-tag <TAG> + Override members tag format + For custom chat server configurations +``` + +### Display and Behavior + +```bash +-g, --guest-color <COLOR> + Set guest color theme + Accepts color names or hex codes + +-m, --manual-captcha + Enable manual captcha solving mode + Shows captcha and waits for your input + Environment: BHC_MANUAL_CAPTCHA + +--sxiv + Enable sxiv image viewer integration + Opens downloaded images in sxiv automatically +``` + +### Bot System + +```bash +--bot <NAME> + Enable background bot with specified name + Bot uses same credentials as main client + Example: --bot Assistant + +--bot-admins <USER1,USER2> + Comma-separated list of bot administrators + Only these users can issue admin commands + Example: --bot-admins alice,bob,charlie + +--bot-data-dir <PATH> + Custom directory for bot data storage + Default: bot_data/{botname} + Example: --bot-data-dir /var/bhcli/bots +``` + +### External Integrations + +```bash +--dkf-api-key <KEY> + DKF API key for notifications + Enables DKF chat notifications + Environment: DKF_API_KEY + +--dnmx-username <USERNAME> + DNMX email username for mail notifications + Environment: DNMX_USERNAME + +--dnmx-password <PASSWORD> + DNMX email password + Environment: DNMX_PASSWORD +``` + +### Advanced Options + +```bash +-d, --dan + Enable special DAN mode features + Internal use, not generally needed + +--keepalive-send-to <TARGET> + Override keepalive message target (default: "0") + Used to prevent timeout on some servers +``` + +### Usage Examples + +```bash +# Basic usage with credentials +bhcli -u myusername -p mypassword + +# Use a specific profile +bhcli -c myprofile + +# Connect through custom Tor port +bhcli -s socks5h://127.0.0.1:9150 + +# Direct connection (clearnet only!) +bhcli --no-proxy --url "http://clearnet-chat.com" + +# Fast refresh rate for active monitoring +bhcli -r 2 + +# Run with bot and specific admins +bhcli --bot Assistant --bot-admins alice,bob + +# Manual captcha solving +bhcli -m + +# Using environment variables +export BHC_USERNAME="myuser" +export BHC_PASSWORD="mypass" +export BHC_PROXY_URL="socks5h://127.0.0.1:9050" +export OPENAI_API_KEY="sk-..." +bhcli +``` + +--- + +## Configuration Files + +### Config File Location + +BHCLI uses `confy` for configuration management. The config file is automatically created on first run. + +**Linux/macOS:** `~/.config/bhcli/bhcli.toml` +**Windows:** `C:\Users\<username>\AppData\Roaming\bhcli\config\bhcli.toml` + +### Profile Configuration + +Store multiple profiles for different accounts or servers: + +```toml +[profiles] + +[profiles.default] +username = "yourusername" +password = "yourpassword" +# Optional: set master/alt accounts (members+ only) +alt_account = "YourAlt" +master_account = "YourMain" + +[profiles.work] +username = "workaccount" +password = "differentpassword" + +[profiles.bhc] +username = "bhcuser" +password = "bhcpass" + +[profiles.poppooeb] +username = "pbuser" +password = "pbpass" +``` + +Switch profiles with `bhcli -c profile_name` + +### Custom Commands + +Create personal command shortcuts that work like aliases: + +```toml +[commands] + +# Simple text replacements +hello = "hey everyone, how's it going?" +afk = "stepping away, ping me if needed" +brb = "be right back" + +# Common responses +rules = "1. Be respectful 2. No spam 3. Stay anonymous 4. Have fun" +help = "Need help? Check the docs or ask the community" + +# Role-specific (for members/mods) +warn = "This is your warning @{}, next offense will be a kick" +welcome = "Welcome to the chat! Read !rules to get started" + +# Technical +status = "System status: All services operational" +links = "Resources: GitHub: ... | Docs: ... | Mirror: ..." +``` + +Use custom commands by prefixing with `!`: +``` +!hello +!rules +!warn username +``` + +### Filters and Moderation + +Configure automatic filters (members+ feature): + +```toml +# Ban users (fuzzy match) +bad_usernames = ["spammer1", "troll2", "badactor"] + +# Ban exact usernames only +bad_exact_usernames = ["ExactSpammer", "Exact_Troll"] + +# Filter messages containing these terms +bad_messages = ["spam text", "buy now", "click here"] + +# Allowlist (bypass all filters) +allowlist = ["trusteduser1", "admin2"] +``` + +These can also be managed via commands: +- `/ban username` adds to bad_usernames +- `/ban "exact"` adds to bad_exact_usernames +- `/filter text` adds to bad_messages +- `/unban` and `/unfilter` remove entries + +--- + +## ChatOps Commands + +BHCLI includes 30+ developer-focused slash commands. All commands are available in-chat by typing `/command`. + +### Documentation and Lookup + +**`/help [command]`** +Show all available ChatOps commands or detailed help for a specific command. + +``` +/help +/help github +/help hash +``` + +**`/man <command>`** +Display system manual pages for commands. + +``` +/man curl +/man grep +/man ssh +``` + +**`/doc <language> <term>`** +Language-specific documentation lookup. + +Supported languages: rust, python, javascript, go, java, cpp, ruby, php + +``` +/doc rust HashMap +/doc python dict +/doc javascript Promise +``` + +**`/explain <concept>`** +AI-powered explanation of programming concepts (requires OPENAI_API_KEY). + +``` +/explain recursion +/explain async/await +/explain blockchain +``` + +**`/cheat <tool>`** +Quick reference cheatsheets for common tools. + +``` +/cheat vim +/cheat git +/cheat tmux +``` + +**`/stackoverflow <query>`** +Search Stack Overflow for programming questions. + +``` +/stackoverflow rust error handling +/stackoverflow python list comprehension +``` + +**`/ref <language>`** +Quick access to language reference documentation. + +``` +/ref rust +/ref python +/ref javascript +``` + +### Tooling and Utilities + +**`/hash <algorithm> <text>`** +Generate cryptographic hashes. + +Algorithms: md5, sha1, sha256, sha512 + +``` +/hash sha256 hello world +/hash md5 test data +/hash sha512 secret message +``` + +**`/uuid`** +Generate a random UUID v4. + +``` +/uuid +``` + +**`/base64 <encode|decode> <text>`** +Base64 encoding and decoding. + +``` +/base64 encode "hello world" +/base64 decode "aGVsbG8gd29ybGQ=" +``` + +**`/regex <pattern> <text>`** +Test regular expressions and see matches. + +``` +/regex "\d+" "abc 123 def 456" +/regex "^[a-z]+" "hello123world" +``` + +**`/whois <domain>`** +Domain WHOIS lookup for registration information. + +``` +/whois github.com +/whois example.org +``` + +**`/dig <domain>`** +DNS record lookup and resolution. + +``` +/dig google.com +/dig example.com A +/dig github.com MX +``` + +**`/ipinfo <ip>`** +Get detailed information about IP addresses. + +``` +/ipinfo 8.8.8.8 +/ipinfo 1.1.1.1 +``` + +**`/rand <min> <max>`** +Generate random numbers within range. + +``` +/rand 1 100 +/rand 0 1 +/rand 100 1000 +``` + +**`/time`** +Display current timestamp and timezone information. + +``` +/time +``` + +### Chat Features + +**`/chatlink <message_id>`** +Create shareable links to specific chat messages. + +``` +/chatlink 12345 +``` + +**`/quote <user> <message>`** +Quote and reference messages from other users. + +``` +/quote alice "that's a great idea" +/quote bob "I disagree with that approach" +``` + +**`/rooms`** +List all available chat rooms and their status. + +``` +/rooms +``` + +**`/whereis <user>`** +Find which rooms a user is currently in. + +``` +/whereis alice +/whereis bob +``` + +### AI Integration + +All AI commands require `OPENAI_API_KEY` environment variable. + +**`/summarize <text>`** +AI-powered text summarization. + +``` +/summarize "long text here..." +``` + +**`/translate <language> <text>`** +Translate text to specified language. + +Supports 100+ languages. + +``` +/translate spanish "Hello, how are you?" +/translate french "Good morning" +/translate german "Thank you very much" +``` + +**`/detect <text>`** +Detect language of text with confidence score. + +``` +/detect "Bonjour, comment allez-vous?" +``` + +**`/sentiment <text>`** +Analyze sentiment of text (positive/negative/neutral). + +``` +/sentiment "This is amazing!" +/sentiment "I'm not sure about this" +``` + +**`/atmosphere`** +Analyze current chat atmosphere and mood. + +``` +/atmosphere +``` + +**`/modcheck <message>`** +Test moderation system on a message. + +``` +/modcheck "potentially problematic content" +``` + +**`/fix <code>`** +Get AI suggestions for fixing code issues. + +``` +/fix "fn main() { println!(x) }" +``` + +**`/review <code>`** +AI-powered code quality review and suggestions. + +``` +/review "def factorial(n): return n * factorial(n-1)" +``` + +### GitHub and Package Management + +**`/github <user/repo> [subcommand]`** (alias: `/gh`) +Get repository information and links. + +``` +/github rust-lang/rust # Basic repo info +/github microsoft/vscode issues # Link to issues +/github torvalds/linux latest # Latest release +/github user/repo file src/main.rs # Link to specific file +``` + +**`/gist <code>`** +Create GitHub Gists (requires GitHub CLI authentication). + +``` +/gist "console.log('hello');" +``` + +**`/crates <crate_name>`** +Rust crate information from crates.io. + +``` +/crates serde +/crates tokio +/crates reqwest +``` + +**`/npm <package_name>`** +NPM package information and installation commands. + +``` +/npm express +/npm react +/npm axios +``` + +**`/pip <package_name>`** (alias: `/pypi`) +Python package info from PyPI. + +``` +/pip requests +/pip django +/pip numpy +``` + +### Network Diagnostics + +**`/ping <host>`** +Test network connectivity and response times. + +``` +/ping google.com +/ping 8.8.8.8 +``` + +**`/traceroute <host>`** +Trace network path to destination. + +``` +/traceroute google.com +``` + +**`/nslookup <domain>`** +DNS name resolution and record lookup. + +``` +/nslookup github.com +``` + +**`/netstat`** +Display active network connections and listening ports. + +``` +/netstat +``` + +**`/portscan <host> <port_range>`** +Scan ports on a host (use responsibly). + +``` +/portscan localhost 80-100 +``` + +**`/headers <url>`** +Show HTTP headers for a URL. + +``` +/headers https://example.com +``` + +**`/curl <url>`** +Fetch URL content via curl. + +``` +/curl https://api.github.com +``` + +**`/ssl <domain>`** +Check SSL certificate information. + +``` +/ssl github.com +/ssl google.com +``` + +**`/torcheck`** +Check if you're connected through Tor. + +``` +/torcheck +``` + +### Miscellaneous + +**`/ascii <text>`** +Convert text to ASCII art. + +``` +/ascii BHCLI +/ascii Hello +``` + +**`/fortune`** +Display random fortune/quote. + +``` +/fortune +``` + +**`/motd`** +Show message of the day. + +``` +/motd +``` + +**`/afk [message]`** +Set away from keyboard status. + +``` +/afk +/afk "lunch break" +``` + +**`/alias <name> <command>`** +Create personal command aliases. + +``` +/alias list # Show all aliases +/alias gh "/github" # Create alias +/alias remove gh # Remove alias +``` + +**`/version`** +Display BHCLI version and system information. + +``` +/version +``` + +### Command Permissions + +Commands respect user role hierarchy: + +- **Guest**: Documentation, basic tools, read-only commands +- **Member**: Full access to all ChatOps commands +- **Staff/Admin**: Complete access plus future admin commands + +Some commands (like `/gist`) require external authentication (GitHub CLI). + +--- + +## Keyboard Shortcuts + +BHCLI supports extensive keyboard navigation and shortcuts. + +### Message Navigation + +``` +j Move down one message +k Move up one message +J Jump down 5 messages +K Jump up 5 messages +gg Jump to top message +G Jump to bottom message (latest) + +Down arrow Move down one message +Up arrow Move up one message +Page Down Scroll down one page +Page Up Scroll up one page +ctrl+d Scroll down one page +ctrl+u Scroll up one page +``` + +### Quick Actions + +``` +t Tag author of selected message (@username) +p PM author of selected message (/pm username) +y Copy selected message to clipboard +Y (shift+y) Copy first link in message +d Download embedded file +D (shift+d) Download and open file with xdg-open + +Enter Start typing in input box +Escape Exit input mode back to navigation +``` + +### View Toggles + +``` +m Toggle sound notifications (mute/unmute) +G (shift+g) Toggle guest view (hide members/PM) +M (shift+m) Toggle members view (hide guests/PM) +ctrl+h Toggle hidden messages view +Backspace Hide selected message +``` + +### Input Editing (When focused on input box) + +``` +ctrl+a Move cursor to start of line +ctrl+e Move cursor to end of line +ctrl+f Move cursor forward one word +ctrl+b Move cursor backward one word +ctrl+l Toggle multiline input mode + +ctrl+. Open external editor (nvim/vim/nano) +ctrl+x Open external editor (alternative) +ctrl+o Open external editor (alternative) + +Up arrow Navigate command history (previous) +Down arrow Navigate command history (next) +Tab Autocomplete username + +ctrl+w Delete word backward +ctrl+u Delete from cursor to start of line +ctrl+k Delete from cursor to end of line +``` + +### Multiline Input Mode + +When multiline mode is enabled (`ctrl+l`): + +``` +Enter Insert newline (not send) +ctrl+Enter Send message +ctrl+l Toggle back to single-line mode +Escape Exit to normal mode +Up/Down Navigate command history +``` + +### Moderation Shortcuts (Members+ only) + +``` +ctrl+k Prefill kick command for selected message + If master account set: /pm <master> #kick username + Otherwise: /kick username + +ctrl+b Prefill ban command for selected message + If master account set: /pm <master> #ban username + Otherwise: /ban username + +ctrl+a Prefill members group message + If master account set: /pm <master> /m + Otherwise: /m + +ctrl+w Send warning message (!warn username) +``` + +--- + +## Bot System + +BHCLI includes a powerful background bot system that runs alongside your main client. + +### Starting a Bot + +```bash +# Basic bot +./bhcli --bot BotName + +# Bot with admin users +./bhcli --bot Assistant --bot-admins alice,bob,charlie + +# Custom data directory +./bhcli --bot Assistant --bot-data-dir /var/bhcli/bots +``` + +The bot uses the same credentials as your main client. It connects independently and maintains its own session. + +### Bot Commands + +Interact with the bot by mentioning it in chat. Only designated admins can use most commands. + +**`@BotName help`** +List all available bot commands. + +**`@BotName stats <username>`** +View detailed statistics for a user. + +Shows: +- Total messages sent +- First/last seen timestamps +- Activity patterns by hour +- Average message length +- Most active hours +- Interaction patterns + +``` +@Assistant stats alice +``` + +**`@BotName recall <time>`** +Find messages from a specific time. + +Time formats: +- HH:MM (today) +- HH:MM:SS (today) +- "1 hour ago" +- "2 days ago" + +``` +@Assistant recall 14:30 +@Assistant recall 09:15:30 +@Assistant recall "2 hours ago" +``` + +**`@BotName search <query>`** +Search message history for content. + +Supports: +- Simple text search +- Case-insensitive matching +- Multiple word queries + +``` +@Assistant search "rust error" +@Assistant search bitcoin +``` + +**`@BotName export <username> <days>`** +Export a user's messages to JSON/CSV. + +``` +@Assistant export alice 7 +@Assistant export bob 30 +@Assistant export charlie 1 +``` + +Files are saved to `bot_data/{botname}/exports/` + +**`@BotName restore <message_id>`** +Restore a deleted message from bot memory. + +``` +@Assistant restore 12345 +``` + +**`@BotName summary <hours>`** +Generate chat activity summary. + +``` +@Assistant summary 24 +@Assistant summary 168 # Last week +``` + +**`@BotName users`** +List current online users. + +``` +@Assistant users +``` + +**`@BotName top <metric>`** +Show top users by various metrics. + +Metrics: +- messages (message count) +- active (most active hours) +- words (word count) + +``` +@Assistant top messages +@Assistant top active +@Assistant top words +``` + +**`@BotName when <username>`** +Show when a user was last seen. + +``` +@Assistant when alice +``` + +**`@BotName active`** +Show currently active users and their activity levels. + +``` +@Assistant active +``` + +### Bot Data Storage + +Bot data is stored in `bot_data/{botname}/`: + +``` +bot_data/ +└── BotName/ + ā”œā”€ā”€ message_history.json # Complete chat history + ā”œā”€ā”€ user_stats.json # User statistics database + └── exports/ # Exported data files + ā”œā”€ā”€ alice_20250109.json + └── bob_20250109.csv +``` + +All data is stored locally and never leaves your machine. + +### Bot Features + +**Perfect Memory** +Bots remember every message, even after restarts. Chat history persists indefinitely. + +**User Analytics** +Comprehensive statistics including: +- Activity patterns by hour and day +- Message frequency and length +- Interaction patterns with other users +- First/last seen timestamps +- Online time estimation + +**Message Recovery** +Recover deleted messages from bot memory. Useful for accident prevention and moderation. + +**Data Export** +Generate research-ready archives: +- JSON format for programmatic access +- CSV format for spreadsheet analysis +- Filtered by user and time range + +**AI Integration** +When `OPENAI_API_KEY` is set, bots gain: +- Intelligent chat summaries +- Sentiment analysis over time +- Pattern detection +- Enhanced moderation capabilities + +### Bot Permissions + +Most bot commands require admin status. Configure admins when starting: + +```bash +./bhcli --bot Assistant --bot-admins alice,bob +``` + +Non-admin users can: +- View help (`@BotName help`) +- Check their own stats (`@BotName stats <self>`) + +Admins can: +- All user commands +- Export data +- Restore messages +- View all statistics +- Generate summaries + +--- + +## AI Features + +BHCLI integrates OpenAI for advanced capabilities. Set `OPENAI_API_KEY` to enable AI features. + +### AI Modes + +Control how AI responds in chat: + +```bash +/ai off # Disable AI completely +/ai mod # Moderation only (no replies) +/ai reply all # Reply to all messages + moderation +/ai reply ping # Reply only when mentioned + moderation +``` + +Current mode is saved to your profile. + +### Moderation System + +AI-powered content moderation for guest messages (members/staff/admins are exempt). + +**Strictness Levels:** + +```bash +/ai strict # Aggressive filtering, low tolerance +/ai balanced # Moderate filtering (default) +/ai lenient # Relaxed filtering, high tolerance +``` + +**How it works:** +1. Quick pattern matching catches obvious violations +2. AI analyzes context and intent +3. Severity scored 0-10 +4. Action taken based on strictness level + +**Actions:** +- Warn: Log the issue, no action +- Kick: Remove user from chat +- Ban: Permanent ban from chat + +**Testing moderation:** + +```bash +/check ai # Check AI system status +/check mod "test message" # Test moderation on sample text +``` + +### Moderation Logging + +Enable logging of moderation decisions: + +```bash +/modlog on # Enable mod logs +/modlog off # Disable mod logs +``` + +Logs are sent to admin channel (`@0`) and include: +- Trigger reason (pattern or AI) +- Severity score +- Action taken +- Original message +- Username + +### Translation + +Real-time translation across 100+ languages: + +```bash +/translate spanish "Hello, how are you?" +/translate french "Good morning everyone" +/translate german "Thank you for your help" +``` + +**Auto-detection:** + +```bash +/detect "Bonjour tout le monde" +# Output: Detected: French (confidence: 0.95) +``` + +Translate-shell fallback if OpenAI unavailable (requires `translate-shell` package). + +### Sentiment Analysis + +Analyze emotional tone of messages: + +```bash +/sentiment "This is amazing, I love it!" +# Output: Positive (confidence: 0.89) + +/sentiment "I'm not sure about this approach" +# Output: Neutral (confidence: 0.72) +``` + +**Chat atmosphere:** + +```bash +/atmosphere +# Analyzes recent messages and provides mood summary +``` + +### Code Assistance + +**Fix broken code:** + +```bash +/fix "fn main() { println!(x) }" +# Suggests: Add missing variable declaration +``` + +**Code review:** + +```bash +/review "def factorial(n): return n * factorial(n-1)" +# Provides: Missing base case, will cause infinite recursion +``` + +**Explanation:** + +```bash +/explain "What is a closure in Rust?" +# Detailed explanation of Rust closures +``` + +### AI Configuration + +AI behavior is controlled via: +1. Command line: `/ai` commands +2. Environment: `OPENAI_API_KEY` +3. Config file: Settings persist per profile + +**Privacy note:** AI features send messages to OpenAI API. Only enabled when explicitly configured. + +--- + +## Moderation System + +Comprehensive moderation tools for members, staff, and admins. + +### Basic Moderation + +**Kick user:** + +```bash +/kick username reason +/k username reason # Shortcut +``` + +Keyboard shortcut: `ctrl+k` on selected message + +**Ban user (fuzzy match):** + +```bash +/ban username +``` + +Keyboard shortcut: `ctrl+b` on selected message + +**Ban exact username:** + +```bash +/ban "exact username" +``` + +**Warning:** + +```bash +!warn username +``` + +Keyboard shortcut: `ctrl+w` + +Sends: "This is your warning @username, will be kicked next !rules" + +### Filter Management + +**Add message filter:** + +```bash +/filter spam text here +``` + +Automatically kicks any guest who posts messages containing "spam text here". + +**Remove filter:** + +```bash +/unfilter spam text here +``` + +**List filters:** + +```bash +/filterlist +``` + +### Ban Management + +**Unban user:** + +```bash +/unban username +``` + +**List banned usernames:** + +```bash +/banlist +``` + +**List exact banned usernames:** + +```bash +/banexactlist +``` + +### Message Deletion + +**Delete last message:** + +```bash +/dl +``` + +**Delete last N messages:** + +```bash +/dl5 # Delete last 5 +/dl10 # Delete last 10 +``` + +**Delete all your messages:** + +```bash +/dall +``` + +### Allowlist System + +Add trusted users to allowlist (bypass all filters): + +Edit `~/.config/bhcli/bhcli.toml`: + +```toml +allowlist = ["trusteduser1", "admin2", "moderator3"] +``` + +Allowlisted users: +- Bypass pattern filters +- Bypass AI moderation +- Can never be auto-kicked + +### Master/Alt Account System (Dasho-specific) + +Link alt and master accounts for command delegation: + +```bash +/set alt AltAccountName +/set master MasterAccountName +``` + +When configured: +- `ctrl+k` sends: `/pm <master> #kick username` +- `ctrl+b` sends: `/pm <master> #ban username` +- `ctrl+a` sends: `/pm <master> /m` + +This allows alt accounts to delegate moderation to master account. + +### Moderation Best Practices + +1. **Warn first**: Give users a chance to correct behavior +2. **Document reasons**: Include reason in kick/ban commands +3. **Use filters**: Automate obvious spam/abuse +4. **Review logs**: Check `/modlog` output regularly +5. **Allowlist carefully**: Only trusted long-term users + +--- + +## Custom Commands + +Create personal shortcuts for frequently used messages. + +### Creating Commands + +Edit `~/.config/bhcli/bhcli.toml`: + +```toml +[commands] + +hello = "hey everyone, how's it going?" +afk = "stepping away for a bit, ping me if needed" +brb = "be right back in 5" +rules = "1. Be cool 2. No spam 3. Stay anonymous" +``` + +### Using Commands + +Prefix with `!` in chat: + +``` +!hello +!afk +!rules +``` + +The command expands to its full text before sending. + +### Advanced Examples + +**Variables (manual substitution):** + +```toml +warn = "This is your warning @{}, next offense will be kicked" +``` + +Usage: Edit the message after typing `!warn` to insert username. + +**Long messages:** + +```toml +welcome = """ +Welcome to Black Hat Chat! + +Rules: +1. Be respectful +2. No spam or flooding +3. Keep it legal +4. Stay anonymous + +Need help? Type /help for commands. +""" +``` + +**Technical responses:** + +```toml +rust_help = "Rust resources: https://doc.rust-lang.org | https://rust-lang.github.io/async-book/" +tor_setup = "Tor setup guide: https://... | Verify: https://check.torproject.org" +``` + +### Command Tips + +1. Keep commands short and memorable +2. Use for frequently typed messages +3. Document complex commands +4. Update as needed, changes apply immediately +5. Share useful commands with the community + +--- + +## Advanced Features + +### External Editor Integration + +Open nvim/vim/nano for composing long messages: + +``` +ctrl+. or ctrl+x or ctrl+o +``` + +How it works: +1. Launches your `$EDITOR` (or falls back to nvim → vim → nano → vi) +2. Loads current input content +3. After saving and exiting, automatically processes message +4. Supports all commands and prefixes (/pm, /m, /s, etc.) +5. Returns to chat immediately after sending + +Perfect for: +- Long messages +- Code snippets +- Formatted text +- Multiple paragraphs + +### Message History + +Navigate previously sent messages: + +``` +Up arrow Previous message +Down arrow Next message +``` + +History filters by current input prefix: +- Typing `/pm` then up arrow shows only previous `/pm` commands +- Typing `!` then up arrow shows only custom commands + +### File Operations + +**Upload:** + +```bash +/u /path/to/file.png @username message here +/u /path/to/file.png @members message +``` + +**Download:** + +``` +d Download file in selected message +D Download and open with xdg-open +``` + +Downloads go to current working directory. + +### Hidden Messages + +Hide individual messages without deleting: + +``` +Backspace Hide selected message +ctrl+H Toggle hidden messages view +``` + +Hidden messages: +- Still in database +- Only hidden from view +- Can be toggled visible +- Once deleted, cannot be recovered (unless bot has copy) + +### Profile Management + +Switch between saved profiles: + +```bash +bhcli -c default +bhcli -c work +bhcli -c bhc +``` + +Each profile maintains: +- Credentials +- Master/alt configuration +- AI settings +- Mod log preferences +- Custom commands +- Filters and bans + +### Session Persistence + +Save your session to skip login next time: + +After logging in, find your session ID in logs or config, then: + +```bash +bhcli --session <SESSION_ID> +``` + +Useful for: +- Quick reconnects +- Avoiding captcha +- Maintaining state + +Sessions expire after inactivity (server-dependent). + +### Tor Configuration + +**Default (recommended):** + +```bash +bhcli -s socks5h://127.0.0.1:9050 +``` + +**Tor Browser Bundle:** + +```bash +bhcli -s socks5h://127.0.0.1:9150 +``` + +**Custom Tor instance:** + +```bash +bhcli -s socks5h://localhost:9999 +``` + +**Disable Tor (clearnet only!):** + +```bash +bhcli --no-proxy +``` + +Never use `--no-proxy` with .onion addresses. + +### Refresh Rate Tuning + +Balance responsiveness vs bandwidth: + +```bash +bhcli -r 2 # Very responsive (2 second refresh) +bhcli -r 5 # Balanced (default) +bhcli -r 10 # Conservative bandwidth +``` + +Lower = more responsive but more bandwidth and server load. + +### Logging + +Logs are written to `bhcli.log` in current directory. + +Contains: +- Connection events +- Errors and warnings +- Moderation decisions (if enabled) +- Debug information + +Useful for troubleshooting. + +--- + +## Troubleshooting + +### Connection Issues + +**Cannot connect:** +- Check Tor is running: `systemctl status tor` +- Verify proxy port: Usually 9050 for Tor, 9150 for Tor Browser +- Try different refresh rate: `bhcli -r 10` +- Check server status (may be down) + +**Timeout errors:** +- Increase refresh rate: `bhcli -r 10` +- Check network connectivity +- Verify .onion address is correct + +### Captcha Problems + +**Cannot see captcha:** +- Try manual mode: `bhcli -m` +- Check terminal supports images +- Increase terminal size + +**Captcha fails repeatedly:** +- Use manual entry mode +- Try different terminal emulator +- Check OCR library is installed + +### Audio Issues + +**No sound:** +- Check audio system is running +- Verify sound notifications enabled (press `m` to toggle) +- Build without audio if not needed: `make build-no-audio` + +**Audio errors:** +- Build with `--no-default-features` to disable audio +- Check ALSA/PulseAudio configuration + +### Performance Issues + +**High CPU usage:** +- Check refresh rate (increase value) +- Disable AI features if not needed +- Kill background processes + +**High memory:** +- Clear message history (restart client) +- Disable bot system if running +- Check for memory leaks (report if found) + +### Configuration Issues + +**Config not found:** +- Check path: `~/.config/bhcli/bhcli.toml` +- Create manually if missing +- Run once to generate default config + +**Profile not working:** +- Verify profile name matches config +- Check TOML syntax is valid +- Look for errors in bhcli.log + +--- + +## Tips and Tricks + +### Power User Shortcuts + +Combine features for maximum efficiency: + +```bash +# Quick moderation workflow +# 1. Navigate to bad message (j/k) +# 2. Kick user (ctrl+k) +# 3. Edit reason, send +# 4. Add to ban list manually if needed + +# Fast PM responses +# 1. Select message (j/k) +# 2. Start PM (p) +# 3. Type response +# 4. Send + +# Efficient filtering +# 1. Add filter (/filter spam) +# 2. Save to config (automatic) +# 3. Applies to all future messages +``` + +### Workflow Optimization + +**For moderators:** +1. Set master/alt accounts +2. Create custom commands for warnings +3. Enable mod logs +4. Set AI to "balanced" or "strict" +5. Maintain allowlist + +**For developers:** +1. Use ChatOps for quick lookups +2. Keep external editor ready +3. Use multiline mode for code +4. Save code snippets as custom commands + +**For power users:** +1. Multiple profiles for different identities +2. Custom commands for frequent responses +3. Hotkeys memorized for speed +4. Bot running for history/search + +### Privacy and Security + +**Best practices:** +1. Always use Tor for darknet chats +2. Different passwords per profile +3. Never share session IDs +4. Clear logs periodically +5. Use temporary directories for file downloads + +**Operational security:** +1. No personally identifiable information +2. Different usernames across chats +3. Mind your timezone in timestamps +4. Be aware of writing style analysis +5. Use VPN + Tor for extra layers + +### Community Guidelines + +**Be a good citizen:** +1. Don't spam or flood +2. Respect moderators +3. Follow chat rules +4. Help new users +5. Report actual problems, not noise + +**Technical etiquette:** +1. Use /pm for long conversations +2. Keep code snippets readable +3. Use /m for member discussions +4. Test commands before spamming chat +5. Read existing docs before asking + +--- + +## Getting Help + +**In order of preference:** + +1. This manual (`MANUAL.md`) +2. Man page (`man bhcli`) +3. In-chat `/help` command +4. Community in chat +5. GitHub issues +6. Official mirror + +**Before asking:** +- Check logs (`bhcli.log`) +- Verify configuration +- Try restarting client +- Test with default settings +- Read error messages carefully + +Most issues are configuration-related. Check your config file, environment variables, and command line arguments. + +--- + +Built for anonymity, optimized for speed, designed for developers. + +Stay safe, stay anonymous, stay paranoid. diff --git a/Makefile b/Makefile @@ -1,43 +1,319 @@ -PWD = $(shell pwd) +# BHCLI Makefile - Modern Cargo-based build system +# Works with both rustup and system-installed Rust (Homebrew, apt, etc.) -build-docker-bin: - docker run --rm -it -v $(PWD):/Documents/bhcli -w /Documents/bhcli bhcli sh -c \ - 'CARGO_TARGET_DIR=./target/linux cargo build --release' +.PHONY: help build release install clean test check fmt clippy \ + build-linux build-macos build-windows build-all \ + setup-targets install-rust-targets -build-darwin: +# Default target +help: + @echo "BHCLI Build System" + @echo "==================" + @echo "" + @echo "Development:" + @echo " make build - Debug build for current platform" + @echo " make release - Optimized release build" + @echo " make install - Install to ~/.cargo/bin" + @echo " make test - Run all tests" + @echo " make check - Check code without building" + @echo " make fmt - Format code with rustfmt" + @echo " make clippy - Run clippy linter" + @echo " make clean - Clean build artifacts" + @echo "" + @echo "Building:" + @echo " make build-local - Optimized build for current platform (with audio)" + @echo " make build-linux-musl - Static Linux binary (no audio, portable)" + @echo " make build-windows - Windows binary (no audio, portable)" + @echo " make build-all - Build for all available platforms" + @echo "" + @echo "Features:" + @echo " make build-no-audio - Build current platform without audio" + @echo " make build-linux-audio - Linux build with audio (native only)" + @echo "" + @echo "Setup (if using rustup):" + @echo " make setup-targets - Install cross-compile targets" + @echo " make show-targets - Show installed targets" + @echo "" + +# Detect if rustup is available +RUSTUP_EXISTS := $(shell command -v rustup 2> /dev/null) + +# Development builds +build: + cargo build + +release: + cargo build --release + +# Install to system +install: + cargo install --path . + +# Testing and verification +test: + cargo test --all-features + +check: + cargo check --all-features + +fmt: + cargo fmt --all + +clippy: + cargo clippy --all-features -- -D warnings + +clean: + cargo clean + rm -rf dist/ + +# Feature variants +build-no-audio: + cargo build --release --no-default-features + +# Setup cross-compilation targets (only works with rustup) +setup-targets: +ifdef RUSTUP_EXISTS + @echo "Installing Rust cross-compilation targets..." + @echo "" + rustup target add x86_64-unknown-linux-musl || true + rustup target add x86_64-pc-windows-gnu || true + @echo "" + @echo "āœ“ Targets installed" + @echo "" + @echo "Note: For full cross-compilation you may also need:" + @echo " - musl-tools (Linux): apt install musl-tools" + @echo " - mingw-w64 (Windows): apt install mingw-w64" + @echo "" +else + @echo "Rustup not found. You have a system Rust installation." + @echo "" + @echo "Cross-compilation targets are typically not needed with system Rust." + @echo "Just use 'make release' to build for your current platform." + @echo "" + @echo "If you need cross-compilation, consider using rustup:" + @echo " curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh" + @echo "" +endif + +install-rust-targets: setup-targets + +# Build for current platform (works with any Rust installation) +build-local: + @echo "Building optimized binary for current platform..." + @echo "(With audio support)" + @mkdir -p dist cargo build --release - cp target/release/bhcli dist/bhcli.darwin.amd64 - tar -czvf dist/bhcli.darwin.amd64.tar.gz dist/bhcli.darwin.amd64 - openssl dgst -sha256 dist/bhcli.darwin.amd64.tar.gz | cut -d ' ' -f 2 > dist/bhcli.darwin.amd64.tar.gz.checksum - rm dist/bhcli.darwin.amd64 - -build-linux: build-docker-bin - cp target/linux/release/bhcli dist/bhcli.linux.amd64 - tar -czvf dist/bhcli.linux.amd64.tar.gz dist/bhcli.linux.amd64 - openssl dgst -sha256 dist/bhcli.linux.amd64.tar.gz | cut -d ' ' -f 2 > dist/bhcli.linux.amd64.tar.gz.checksum - rm dist/bhcli.linux.amd64 - -cross-compile-windows: - cargo build --release --target x86_64-pc-windows-gnu - cp target/x86_64-pc-windows-gnu/release/bhcli.exe dist/bhcli.windows.amd64.exe - zip dist/bhcli.windows.amd64.zip dist/bhcli.windows.amd64.exe - openssl dgst -sha256 dist/bhcli.windows.amd64.zip | cut -d ' ' -f 2 > dist/bhcli.windows.amd64.zip.checksum - rm dist/bhcli.windows.amd64.exe - -process-windows: - zip dist/bhcli.windows.amd64.zip dist/bhcli.exe - openssl dgst -sha256 dist/bhcli.windows.amd64.zip | cut -d ' ' -f 2 > dist/bhcli.windows.amd64.zip.checksum - rm dist/bhcli.exe - -rsync: - rsync --recursive --times --compress --progress dist/ dkf:/home/dkf/dist/downloads-bhcli - -deploy: build-darwin cross-compile-windows build-linux rsync - -linux: + @cp target/release/bhcli dist/bhcli-local 2>/dev/null || \ + cp target/release/bhcli.exe dist/bhcli-local.exe 2>/dev/null || \ + echo "Warning: Could not copy binary to dist/" + @echo "āœ“ Binary created in dist/ (with audio support)" + @ls -lh dist/bhcli-* 2>/dev/null || ls -lh target/release/bhcli* 2>/dev/null + +# Check if a specific target is available +check-target-musl: +ifdef RUSTUP_EXISTS + @rustup target list | grep -q "x86_64-unknown-linux-musl (installed)" || \ + (echo "Target not installed. Run: make setup-targets" && exit 1) +else + @echo "Attempting build without rustup target checking..." +endif + +check-target-windows: +ifdef RUSTUP_EXISTS + @rustup target list | grep -q "x86_64-pc-windows-gnu (installed)" || \ + (echo "Target not installed. Run: make setup-targets" && exit 1) +else + @echo "Attempting build without rustup target checking..." +endif + +# Cross-platform builds +build-linux-musl: + @echo "Building static Linux binary (x86_64-musl)..." + @echo "(Building without audio for portability)" + @mkdir -p dist + @if cargo build --release --target x86_64-unknown-linux-musl --no-default-features 2>/dev/null; then \ + cp target/x86_64-unknown-linux-musl/release/bhcli dist/bhcli-linux-x86_64 && \ + echo "āœ“ Linux binary created: dist/bhcli-linux-x86_64" && \ + echo " Note: Audio disabled for maximum compatibility"; \ + else \ + echo ""; \ + echo "Cross-compilation to x86_64-unknown-linux-musl failed."; \ + echo ""; \ + if [ -n "$(RUSTUP_EXISTS)" ]; then \ + echo "You may need:"; \ + echo " 1. rustup target add x86_64-unknown-linux-musl"; \ + echo " 2. apt install musl-tools (on Linux)"; \ + else \ + echo "Your system Rust may not support this target."; \ + echo "Consider using rustup for cross-compilation."; \ + fi; \ + echo ""; \ + exit 1; \ + fi + +# Linux build with audio (for native compilation) +build-linux-audio: + @echo "Building Linux binary with audio support..." + @mkdir -p dist cargo build --release - @echo "Copying binary to /opt requires sudo privileges." - @echo "Ensure /opt is added to your \$$PATH variable to use bhcli globally from any path." - sudo cp target/release/bhcli /opt + @cp target/release/bhcli dist/bhcli-linux-audio + @echo "āœ“ Linux binary with audio: dist/bhcli-linux-audio" + +build-macos: + @echo "Building macOS binary..." + @mkdir -p dist + @if cargo build --release 2>/dev/null; then \ + cp target/release/bhcli dist/bhcli-macos && \ + echo "āœ“ macOS binary created: dist/bhcli-macos"; \ + else \ + echo "Build failed"; \ + exit 1; \ + fi + +build-windows: + @echo "Building Windows binary..." + @echo "(Building without audio for portability)" + @mkdir -p dist + @if cargo build --release --target x86_64-pc-windows-gnu --no-default-features 2>/dev/null; then \ + cp target/x86_64-pc-windows-gnu/release/bhcli.exe dist/bhcli-windows-x86_64.exe && \ + echo "āœ“ Windows binary created: dist/bhcli-windows-x86_64.exe" && \ + echo " Note: Audio disabled for maximum compatibility"; \ + else \ + echo ""; \ + echo "Cross-compilation to Windows failed."; \ + echo ""; \ + if [ -n "$(RUSTUP_EXISTS)" ]; then \ + echo "You may need:"; \ + echo " 1. rustup target add x86_64-pc-windows-gnu"; \ + echo " 2. apt install mingw-w64 (on Linux)"; \ + else \ + echo "Your system Rust may not support this target."; \ + echo "Consider using rustup for cross-compilation."; \ + fi; \ + echo ""; \ + exit 1; \ + fi + +# Build for all supported platforms +build-all: + @echo "Building for all available platforms..." + @echo "" + @mkdir -p dist + @BUILD_SUCCESS=0; \ + echo "Attempting Linux static build..."; \ + if $(MAKE) build-linux-musl 2>/dev/null; then \ + BUILD_SUCCESS=1; \ + else \ + echo "⚠ Linux build skipped (target not available)"; \ + fi; \ + echo ""; \ + echo "Attempting Windows build..."; \ + if $(MAKE) build-windows 2>/dev/null; then \ + BUILD_SUCCESS=1; \ + else \ + echo "⚠ Windows build skipped (target not available)"; \ + fi; \ + echo ""; \ + echo "Building for current platform..."; \ + $(MAKE) build-local; \ + BUILD_SUCCESS=1; \ + echo ""; \ + echo "========================================="; \ + echo "Build complete!"; \ + echo "========================================="; \ + ls -lh dist/ 2>/dev/null || echo "Binaries in target/release/" + +# Distribution packages +dist: + @echo "Creating distribution packages..." + @mkdir -p dist + @$(MAKE) build-all + @if [ -f dist/bhcli-linux-x86_64 ]; then \ + cd dist && tar -czf bhcli-linux-x86_64.tar.gz bhcli-linux-x86_64 && \ + echo "āœ“ Created bhcli-linux-x86_64.tar.gz"; \ + fi + @if [ -f dist/bhcli-windows-x86_64.exe ]; then \ + cd dist && zip -q bhcli-windows-x86_64.zip bhcli-windows-x86_64.exe && \ + echo "āœ“ Created bhcli-windows-x86_64.zip"; \ + fi + @if [ -f dist/bhcli-local ]; then \ + cd dist && tar -czf bhcli-local.tar.gz bhcli-local && \ + echo "āœ“ Created bhcli-local.tar.gz"; \ + fi + @echo "" + @echo "Distribution packages:" + @ls -lh dist/*.tar.gz dist/*.zip 2>/dev/null || echo "No packages created" + +# Verify release build +verify-release: + @echo "Verifying release build..." + @cargo build --release + @echo "āœ“ Release build successful" + @echo "" + @echo "Binary location:" + @ls -lh target/release/bhcli* 2>/dev/null || echo "Check target/release/" + +# Development helpers +watch: + @command -v cargo-watch >/dev/null 2>&1 || \ + (echo "cargo-watch not installed. Install with: cargo install cargo-watch" && exit 1) + cargo watch -x 'check --all-features' -x 'test --all-features' + +run: + cargo run --release + +# Cargo commands wrapped for convenience +update: + cargo update + +doc: + cargo doc --no-deps --open + +# Show installed targets (if using rustup) +show-targets: +ifdef RUSTUP_EXISTS + @echo "Installed Rust targets:" + @rustup target list | grep installed +else + @echo "Rustup not found. You have a system Rust installation." + @echo "" + @echo "Your Rust compiler:" + @rustc --version + @echo "" + @echo "Your Cargo version:" + @cargo --version + @echo "" + @echo "System Rust typically supports your native platform only." + @echo "For cross-compilation, consider using rustup." +endif -.PHONY: build-darwin process-windows cross-compile-windows rsync linux +# Show Rust installation info +rust-info: + @echo "Rust Installation Information" + @echo "==============================" + @echo "" + @echo "Rustc version:" + @rustc --version + @echo "" + @echo "Cargo version:" + @cargo --version + @echo "" +ifdef RUSTUP_EXISTS + @echo "Rustup version:" + @rustup --version + @echo "" + @echo "Active toolchain:" + @rustup show active-toolchain + @echo "" + @echo "Installed targets:" + @rustup target list | grep installed +else + @echo "Installation type: System Rust (Homebrew, apt, or other)" + @echo "" + @echo "Note: System Rust installations typically support your" + @echo " native platform only. For cross-compilation, consider" + @echo " using rustup." +endif + @echo "" + @echo "To check if cross-compilation works, try:" + @echo " make build-all" diff --git a/README.md b/README.md @@ -1,545 +1,331 @@ # BHCLI -![screenshot](strange_bhcli.jpg "strange_bhcli") +![demo](demo.png "bhcli") -## Description +A sophisticated terminal client for le-chat-php based chat systems. Built for the darknet, optimized for Tor, works everywhere. -This is a CLI client for any of [le-chat-php](https://github.com/DanWin/le-chat-php) -Officially supported chats are [Black Hat Chat](http://blkhatjxlrvc5aevqzz5t6kxldayog6jlx5h7glnu44euzongl4fh5ad.onion) +## What is this? -Tested working on [ --url ] : +BHCLI is a CLI chat client designed for anonymous communication over Tor. It connects to any [le-chat-php](https://github.com/DanWin/le-chat-php) chat server with full feature parity plus advanced capabilities like AI integration, bot automation, and developer tools. +**Officially supported:** +- [404 Chatroom Not Found](http://4o4o4hn4hsujpnbsso7tqigujuokafxys62thulbk2k3mf46vq22qfqd.onion/chat/min) +- [Black Hat Chat](http://blkhatjxlrvc5aevqzz5t6kxldayog6jlx5h7glnu44euzongl4fh5ad.onion) - [PopPooB's Chat](http://vfdvqflzfgwnejh6rrzjnuxvbnpgjr4ursv4moombwyauot5c2z6ebid.onion/chat.php) -## Pre-built binaries - -Pre-buit binaries can be found on the [official website](http://git.dkforestseeaaq2dqz2uflmlsybvnq2irzn4ygyvu53oazyorednviid.onion/Strange/bhcli/releases) - -## Features - -- **šŸ¤– Advanced AI Integration**: Comprehensive AI-powered features including chat summarization, real-time language detection, sentiment analysis, intelligent moderation, and enhanced code review -- **šŸ¤– Background Bot System**: Persistent memory bots with full chat history, user statistics, message search/recall, data export, and intelligent community management -- **ChatOps Integration**: 30+ developer-focused slash commands for documentation lookup, development tools, GitHub integration, network diagnostics, and AI assistance -- **🌐 Multi-language Support**: Automatic language detection and translation with 95%+ accuracy -- **šŸ“Š Chat Analytics**: Real-time atmosphere monitoring, sentiment tracking, and conversation insights -- **šŸ›”ļø Smart Moderation**: AI-powered content analysis with context-aware moderation suggestions -- Sound notifications when tagged/pmmed -- Private messages `/pm username message` -- Kick someone `/kick username message` | `/k username message` (Members +) -- Delete last message `/dl` -- Delete last X message `/dl5` will delete the last 5 messages -- Delete all messages `/dall` -- Ignore someone `/ignore username` -- Unignore someone `/unignore username` -- (Dasho) Ban a username and kick `/ban username` (Members +) -- (Dasho) Ban a username exactly `/ban "username"` (Members +) -- (Dasho) Filter messages containing text `/filter text` (Members +) -- (Dasho) List banned usernames `/banlist` (Members +) -- (Dasho) List exact banned usernames `/banexactlist` (Members +) -- (Dasho) List filtered message terms `/filterlist` (Members +) -- (Dasho) Unban a username `/unban username` (Members +) -- (Dasho) Remove a message filter `/unfilter text` (Members +) -- Toggle notifications sound `m` -- Toggle a "guest" view, by filtering out PMs and "Members chat" `shift+G` -- Toggle a "members" view, by filtering out PMs and "Guest chat" `shift+M` -- Filter messages `/f terms` -- Copy a selected message to clipboard `ctrl+C` | `y` -- Copy the first link in a message to clipboard `shift+Y` -- Directly tag author of selected message `t` will prefil the input with `@username ` -- Directly private message author of selected message `p` will prefil the input with `/pm username ` -- (Dasho) Shortcut to kick author of selected message `ctrl+k` will prefill with `/pm <master> #kick username` if a master account is set, otherwise `/kick username ` -- (Dasho) Shortcut to ban author of selected message `ctrl+b` will prefill with `/pm <master> #ban username` if a master account is set, otherwise `/ban username ` (Again, only useful for members+ users) -- (Dasho) Use `ctrl+a` to prefill the input with `/pm <master> /m ` when a master account is set, or `/m ` when none is configured (Again, only useful for members+ users) -- captcha is displayed directly in terminal 10 times the real size -- Upload file `/u C:\path\to\file.png @username message` (@username is optional) `@members` for members group -- `<tab>` to autocomplete usernames while typing -- `ctrl + w` or !warn username to send a pre-kick warning message to a user - [ Only for members+ users ] - > This is your warning @username, will be kicked next !rules -- Can hide messages with `backspace`, hidden messages can be viewed by toggling - `ctrl+ H`. - > - Hidden messages are just hidden from the view, they are not deleted - > - Deleted messages once hidden can't be viewed again -- Download an embedded file into cwd with `d` -- Download an embedded file and open it with xdg-open into cwd with `D` -- `shift + T` for translating text to english. [ must have translate-shell installed on arch or debain ] - > pacman -S translate-shell -- Custom personal command creation for members+ [ read Command Creation ] -- Set alternate and master accounts per profile using `/set alt <username>` and `/set master <username>` - -## ChatOps Commands - -BHCLI includes a comprehensive ChatOps system with 30+ developer-focused commands across multiple categories. These commands provide quick access to documentation, development tools, network diagnostics, and integrations. - -### šŸ“– Documentation & Lookup Commands - -Get instant access to documentation and references: - -- `/help` - Show all available ChatOps commands with descriptions -- `/man <command>` - Display manual pages for system commands (e.g., `/man curl`) -- `/doc <language> <term>` - Language-specific documentation lookup (e.g., `/doc rust HashMap`) -- `/explain <concept>` - AI-powered explanations of programming concepts -- `/cheat <tool>` - Quick reference cheatsheets for common tools -- `/stackoverflow <query>` - Search Stack Overflow for programming questions -- `/ref <language>` - Quick access to language reference documentation - -### šŸ”§ Tooling & Utilities Commands - -Essential development utilities and tools: - -- `/hash <algorithm> <text>` - Generate cryptographic hashes (MD5, SHA1, SHA256, SHA512) - - Example: `/hash sha256 hello world` -- `/uuid` - Generate a new UUID v4 -- `/base64 <encode|decode> <text>` - Base64 encoding and decoding - - Example: `/base64 encode "hello world"` -- `/regex <pattern> <text>` - Test regular expressions and see matches - - Example: `/regex "\d+" "abc 123 def"` -- `/whois <domain>` - Domain WHOIS lookup for registration information -- `/dig <domain>` - DNS record lookup and resolution -- `/ipinfo <ip>` - Get detailed information about IP addresses -- `/rand <min> <max>` - Generate random numbers within range -- `/time` - Display current timestamp and timezone information - -### šŸ’¬ Chat Linking & Session Intelligence - -Enhanced chat functionality and user management: - -- `/chatlink <message_id>` - Create shareable links to specific chat messages -- `/quote <user> <message>` - Quote and reference messages from other users -- `/rooms` - List all available chat rooms and their status -- `/whereis <user>` - Find which rooms a user is currently in - -### šŸ¤– AI Integration Commands - -Leverage AI for development assistance: - -- `/summarize <text>` - AI-powered text summarization -- `/translate <language> <text>` - Translate text between languages -- `/fix <code>` - Get AI suggestions for fixing code issues -- `/review <code>` - AI-powered code quality review and suggestions - -### šŸ™ GitHub & Package Management - -Repository and package information at your fingertips: - -#### GitHub Integration - -- `/github <user/repo>` (alias: `/gh`) - Get repository information and links - - `/github user/repo` - Show basic repository info - - `/github user/repo issues` - Direct link to issues - - `/github user/repo latest` - Link to latest release - - `/github user/repo file <path>` - Link to specific file -- `/gist <code>` - Create GitHub Gists (requires GitHub CLI authentication) - -#### Package Managers - -- `/crates <crate_name>` - Rust crate information from crates.io -- `/npm <package_name>` - NPM package information and installation commands -- `/pip <package_name>` (alias: `/pypi`) - Python package info from PyPI - -### 🌐 Network Diagnostics - -Network troubleshooting and connectivity tools: - -- `/ping <host>` - Test network connectivity and response times -- `/traceroute <host>` - Trace network path to destination -- `/nslookup <domain>` - DNS name resolution and record lookup -- `/netstat` - Display active network connections and listening ports +## Getting Started -### āš™ļø Miscellaneous Commands - -Additional utility commands: - -- `/alias <name> <command>` - Create personal command aliases - - `/alias list` - Show all your aliases - - `/alias remove <name>` - Remove an alias -- `/version` - Display BHCLI version and system information - -### Command Usage Examples +### Quick Install +**Pre-built binaries:** ```bash -# Documentation lookups -/help # Show all commands -/man grep # Manual page for grep -/doc rust Vec # Rust documentation for Vec - -# Development tools -/hash sha256 "my secret" # Generate SHA256 hash -/uuid # Generate new UUID -/base64 encode "hello world" # Base64 encode text -/regex "\d+" "abc 123 def" # Test regex pattern - -# GitHub integration -/github rust-lang/rust # Get Rust repository info -/github microsoft/vscode issues # Link to VS Code issues -/crates serde # Info about serde crate -/npm express # Info about Express.js package - -# Network diagnostics -/ping google.com # Ping Google -/dig example.com # DNS lookup -/whois github.com # Domain registration info - -# AI assistance -/translate spanish "Hello world" # Translate to Spanish -/summarize "long text here..." # Summarize text -/fix "broken code here" # Get code fix suggestions +# Download from latest release +https://github.com/d-a-s-h-o/bhcli/releases/latest ``` -### Command Permissions - -ChatOps commands respect user roles and permissions: - -- **Guest**: Access to documentation, basic tools, and read-only commands -- **Member**: Full access to all ChatOps commands -- **Staff/Admin**: Complete access plus any future administrative commands - -### Command Help System - -Each command includes built-in help: - -- Use `/help` to see all available commands -- Use `/help <command>` to get detailed usage information for specific commands -- Commands show usage hints when used incorrectly - -### Extending ChatOps - -The ChatOps system is built with extensibility in mind. New commands can be easily added by implementing the `ChatCommand` trait. The modular architecture supports: - -- Custom command categories -- Alias support for commands -- Role-based permission checking -- Structured result formatting -- Error handling and user feedback - -### Editing mode +**Build from source:** +```bash +# Install Rust if you don't have it +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -- `ctrl+A` Move cursor to start of line -- `ctrl+E` Move cursor to end of line -- `ctrl+F` Move cursor a word forward -- `ctrl+B` Move cursor a word backward -- `ctrl+.`, `ctrl+X`, or `ctrl+O` Open external editor (nvim/vim/nano) - automatically sends message after editing -- `ctrl+L` Toggle multiline input mode (in multiline mode, use `ctrl+Enter` to send, `Enter` for newline) -- `Up/Down arrows` Navigate through command history (filters by current input prefix) +# Clone and build +git clone <repo> +cd bhcli +cargo build --release -### Multiline Input Mode +# Binary will be at target/release/bhcli +``` -- `Enter` Insert newline -- `ctrl+Enter` Send message -- `ctrl+L` Toggle back to single-line mode -- `Up/Down arrows` Navigate through command history -- `Escape` Exit to normal mode +**Linux dependencies:** +```bash +apt-get install -y pkg-config libasound2-dev libssl-dev cmake libfreetype6-dev libexpat1-dev libxcb-composite0-dev libx11-dev +``` -### Messages navigation +### First Run -- Page down the messages list `ctrl+D` | `page down` -- Page up the messages list `ctrl+U` | `page up` -- Going down 1 message `j` | `down arrow` -- Going down 5 message `J(CAPS)` -- Going up 1 message `k` | `up arrow` -- Going up 5 message `K(CAPS)` -- Jump to Top Message `gg` +```bash +# Just run it, it will prompt you +./bhcli -## Build from source +# Or specify credentials +./bhcli -u yourusername -p yourpassword -### Windows +# Through Tor (default) +./bhcli -s socks5h://127.0.0.1:9050 -- Install C++ build tools https://visualstudio.microsoft.com/visual-cpp-build-tools/ -- Install Rust https://www.rust-lang.org/learn/get-started -- Download & extract code source -- Compile with `cargo build --release` +# Direct connection (not recommended for darknet chats) +./bhcli --no-proxy +``` -### OSx +On first run, a man page gets installed automatically. Access it with `man bhcli` anytime. -- Install Rust https://www.rust-lang.org/learn/get-started -- Compile with `cargo build --release` +## Core Features -### Linux +### The Basics -- Install Rust -- Install dependencies `apt-get install -y pkg-config libasound2-dev libssl-dev cmake libfreetype6-dev libexpat1-dev libxcb-composite0-dev libx11-dev` -- The manual way - > - Compile with `cargo build --release` - > - Run with `./target/release/bhcli` - > - You can move the binary to `/opt` to make it available system wide [ given that u have /opt in $PATH ] -- The MAKEFILE way - > - Compile with `make linux` - > - Run with bhcli [ given that u have /opt in $PATH ] -- The bhcli.log file will be created in the same directory as the pwd you run - the binary from +- **Private messaging** with `/pm username message` +- **Sound notifications** when tagged or PM'd (can be disabled with `m`) +- **File uploads** with `/u /path/to/file.png @username optional message` +- **Captcha rendering** directly in terminal at 10x size +- **Tab completion** for usernames +- **Message history** with up/down arrows +- **Copy to clipboard** with `ctrl+c` or `y` -## Cross compile +### Views and Filters -`cargo build --release --target x86_64-pc-windows-gnu` +Toggle between different chat views: +- `shift+G` for guest view (filter out members chat and PMs) +- `shift+M` for members view (filter out guest chat and PMs) +- `/f terms` to filter messages by content +- `/ignore username` to hide someone's messages +- `backspace` to hide individual messages, `ctrl+H` to view hidden ones -## Profiles +### Message Navigation -To automatically login when starting the application, you can put the following content in your config file `/path/to/rs.bhcli/default-config.toml` +``` +j / down arrow Move down one message +k / up arrow Move up one message +J (shift) Jump down 5 messages +K (shift) Jump up 5 messages +gg Jump to top +ctrl+d Page down +ctrl+u Page up +``` -```toml -[profiles] +### Quick Actions -[profiles.default] -username = "username" -password = "password" -alt_account = "myAlt" # Optional, only for members+ (Dasho) -master_account = "myMain" # Optional, only for members+ (Dasho) +``` +t Tag author of selected message (@username) +p Start PM to author (/pm username) +y Copy selected message +shift+Y Copy first link in message +d Download embedded file +D Download and open file ``` -## Custom Commands +## Power Features -U can create ur own custom personal commands using the format below.<br> -The commands are not created on the server but rather edited on clien tand sen -tot server.<br> -Comands must start from "!" in the textbox, but "!" are not required in config. +### ChatOps Commands (30+ tools) -```toml -[commands] +BHCLI includes a full developer toolbox accessible via slash commands: -command1 = "This is the mesage that will be posted" -hello = "hello everyone !" +```bash +/help List all ChatOps commands +/man curl System manual pages +/doc rust HashMap Language documentation +/hash sha256 text Cryptographic hashing +/uuid Generate UUIDs +/base64 encode data Encoding utilities +/github user/repo Repository info +/ping google.com Network diagnostics +/translate spanish Hi AI translation ``` -## Configuration file +See `MANUAL.md` or `man bhcli` for the complete command reference. -The configuration is stored using `confy`. On Linux this is usually -`~/.config/bhcli/bhcli.toml`. You can edit this file to preload profiles, -create custom commands and maintain filters. +### AI Integration -For members+ users, you can set the `alt_account` and `master_account` fields in the config file. (Dasho) -To manually add or remove banned usernames or message filters you can edit the -`bad_usernames`, `bad_exact_usernames` and `bad_messages` arrays in this file: +When `OPENAI_API_KEY` is set: +- **Real-time translation** across 100+ languages +- **Sentiment analysis** of conversations +- **Code review** and debugging assistance +- **Chat summarization** and insights +- **Smart moderation** (configurable strictness) -```toml -bad_usernames = ["spammer1", "spammer2"] -bad_exact_usernames = ["baduser"] -bad_messages = ["buy now", "free money"] +Control AI features: +```bash +/ai off Disable AI completely +/ai mod Moderation only +/ai reply all Reply to all messages +/ai reply ping Reply when mentioned +/ai strict Strict moderation +/ai balanced Balanced moderation (default) +/ai lenient Lenient moderation ``` -Filters modified using `/ban`, `/ban "name"`, `/filter`, `/unban` and `/unfilter` are saved -back to this file automatically and any custom commands in the `[commands]` -section are preserved. +### Background Bot System -## Command Line Arguments +Run a persistent bot alongside your client: +```bash +./bhcli --bot BotName --bot-admins alice,bob +``` -BHCLI supports various command-line arguments for configuration and customization: +Bots provide: +- **Perfect memory** of all chat history +- **Message search** and recall +- **User statistics** and analytics +- **Data export** capabilities +- **Message restoration** for deleted content -### Authentication & Profile +Interact with bots by mentioning them: +``` +@BotName help +@BotName stats alice +@BotName search "rust error" +@BotName recall 14:30 +@BotName export alice 7 +``` -- `-u, --username <USERNAME>` - Set username (can also use `BHC_USERNAME` env var) -- `-p, --password <PASSWORD>` - Set password (can also use `BHC_PASSWORD` env var) -- `-c, --profile <PROFILE>` - Select configuration profile (default: "default") -- `--session <SESSION>` - Use existing session ID to skip login +## Configuration -### Connection & Network +### Profiles -- `--url <URL>` - Override chat server URL -- `--page-php <PAGE>` - Override chat page filename (default: chat.php) -- `-s, --socks-proxy-url <URL>` - SOCKS proxy URL (default: socks5h://127.0.0.1:9050, can use `BHC_PROXY_URL` env var) -- `--no-proxy` - Disable proxy usage -- `-r, --refresh-rate <SECONDS>` - Message refresh rate in seconds (default: 5, can use `BHC_REFRESH_RATE` env var) -- `--datetime-fmt <FORMAT>` - Override datetime format -- `--members-tag <TAG>` - Override members tag format +Store credentials in `~/.config/bhcli/bhcli.toml`: -### Display & Behavior +```toml +[profiles] -- `-g, --guest-color <COLOR>` - Set guest color theme -- `-m, --manual-captcha` - Enable manual captcha solving (can also use `BHC_MANUAL_CAPTCHA` env var) -- `-r, --refresh-rate <SECONDS>` - Message refresh rate in seconds (default: 5, can use `BHC_REFRESH_RATE` env var) -- `--datetime-fmt <FORMAT>` - Custom datetime format string -- `--sxiv` - Enable sxiv image viewer integration +[profiles.default] +username = "yourusername" +password = "yourpassword" -### Bot System +[profiles.work] +username = "workaccount" +password = "differentpassword" +``` -- `--bot <NAME>` - Enable background bot with specified name (uses same credentials as main client) -- `--bot-admins <USER1,USER2>` - Comma-separated list of bot administrators -- `--bot-data-dir <PATH>` - Custom directory for bot data storage (default: `bot_data/{botname}`) +Switch profiles with `bhcli -c work` -### Integrations +### Custom Commands -- `--dkf-api-key <KEY>` - DKF API key for notifications (can also use `DKF_API_KEY` env var) -- `--dnmx-username <USERNAME>` - DNMX email username (can also use `DNMX_USERNAME` env var) -- `--dnmx-password <PASSWORD>` - DNMX email password (can also use `DNMX_PASSWORD` env var) +Create personal shortcuts: -### Advanced Options +```toml +[commands] +hello = "hey everyone, how's it going?" +afk = "stepping away for a bit, ping me if needed" +rules = "1. Be cool 2. No spam 3. Stay anonymous" +``` -- `-d, --dan` - Enable special DAN mode features -- `--keepalive-send-to <TARGET>` - Override keepalive message target (default: "0") +Use them by typing `!hello`, `!afk`, etc. -### Usage Examples +### Environment Variables ```bash -# Basic usage with username and password -bhcli -u myusername -p mypassword - -# Use a specific profile -bhcli -c myprofile - -# Connect through different proxy -bhcli -s socks5h://127.0.0.1:9150 - -# Disable proxy completely -bhcli --no-proxy - -# Use custom refresh rate -bhcli -r 3 - -# Connect to different chat server -bhcli --url "http://example.onion" --page-php "chat.php" - -# Enable manual captcha solving -bhcli -m - -# Use environment variables export BHC_USERNAME="myuser" export BHC_PASSWORD="mypass" -export BHC_PROXY_URL="socks5h://127.0.0.1:9150" -bhcli +export BHC_PROXY_URL="socks5h://127.0.0.1:9050" +export OPENAI_API_KEY="sk-..." # For AI features ``` -Most settings can be configured via environment variables or saved in the configuration file for persistent use across sessions. +## Advanced Usage -## Changelog +### Editing Mode -### Recent Updates (September 2025) +The input bar supports Vim-like editing: -#### šŸ¤– Advanced AI Service Integration - -- **Comprehensive AI Service**: New `ai_service.rs` module with intelligent chat analysis -- **Real-time Message Tracking**: Automatic message history for context-aware AI responses -- **Smart Caching System**: Language detection and sentiment analysis caching for performance -- **Fallback Systems**: Graceful degradation when AI services are unavailable - -#### šŸ¤– Background Bot System - -- **Persistent Memory Bots**: Full chat history storage with perfect recall capabilities -- **User Analytics Engine**: Comprehensive user statistics, activity patterns, and behavioral analysis -- **Message Search & Recall**: Find any message instantly by timestamp, content, or user -- **Data Export System**: Research-ready chat archives in multiple formats -- **Message Restoration**: Recover accidentally deleted messages from bot memory -- **AI-Enhanced Analysis**: Intelligent chat summaries, mood analysis, and content insights -- **Admin Command Suite**: Advanced moderation tools and user management features +``` +ctrl+a Start of line +ctrl+e End of line +ctrl+f Word forward +ctrl+b Word backward +ctrl+l Toggle multiline mode +ctrl+. Open external editor (nvim/vim/nano) +``` -#### 🌐 Multi-language & Translation Features +In multiline mode, `enter` adds newlines, `ctrl+enter` sends. -- **Language Detection**: Real-time detection with confidence scoring and ISO codes -- **Advanced Translation**: AI-powered translation with fallback to system tools -- **Multi-language Chat Support**: Seamless communication across language barriers +### Moderation Tools (Members+) -#### šŸ“Š Chat Analytics & Insights +For moderators and admins: +```bash +/kick username reason Boot someone +/ban username Ban user (fuzzy match) +/ban "exact username" Ban exact match +/filter spamtext Auto-filter messages +/banlist Show banned users +/filterlist Show filtered terms +/unban username Remove ban +/unfilter text Remove filter +``` -- **Chat Summarization**: Intelligent conversation summaries with key points and participant analysis -- **Atmosphere Monitoring**: Real-time chat mood and activity level tracking -- **Sentiment Analysis**: Emotional tone detection with confidence ratings and emotion categorization +Quick keyboard shortcuts: +``` +ctrl+k Prefill kick command for selected message +ctrl+b Prefill ban command +ctrl+a Prefill members group message +ctrl+w Send warning message +``` -#### šŸ›”ļø Enhanced AI Moderation +### Multiple Accounts (Members+) -- **Context-aware Analysis**: AI moderation that understands conversation context -- **Severity Scoring**: 0-10 scale moderation recommendations with confidence ratings -- **Smart Action Suggestions**: Intelligent recommendations for moderation actions (warn/kick/ban) -- **Integration with Existing Systems**: Seamless integration with current moderation tools +Link alt and master accounts for delegation: +```bash +/set alt AltAccountName +/set master MasterAccountName +``` -#### šŸ’» Enhanced Developer Experience +This enables command forwarding and cross-account operations. -- **Comprehensive Code Review**: AI-powered code analysis with security, performance, and quality insights -- **Language-specific Suggestions**: Targeted advice for Rust, Python, JavaScript, and more -- **ChatOps Command Expansion**: New AI commands integrated into existing ChatOps framework +## Building and Distribution -### Previous Updates (August 2025) +### Standard Build -#### AI Moderation System +```bash +make build # Debug build +make release # Optimized release +make test # Run tests +make check # Quick compile check +``` -- **Added AI-powered moderation** with OpenAI integration for automated content filtering -- **Guest-only moderation**: AI moderation only applies to guests, members/staff/admins are exempt -- **Multi-layered protection**: Quick pattern matching + AI analysis for comprehensive coverage -- **AI conversation modes**: - - `/ai off` - Completely disable AI - - `/ai mod` - Enable moderation only - - `/ai reply all` - Enable replies to all messages + moderation - - `/ai reply ping` - Enable replies only when tagged + moderation -- **Moderation strictness levels**: `/ai strict`, `/ai balanced`, `/ai lenient` -- **Enhanced pattern detection**: Comprehensive quick patterns for immediate filtering of inappropriate content -- **AI testing commands**: `/check ai` for system status, `/check mod <message>` to test moderation +### Cross-Platform -#### Moderation Logging System +```bash +make build-linux-musl # Static Linux binary +make build-macos # macOS binary +make build-windows # Windows binary +make build-all # All platforms +``` -- **Added `/modlog on/off`** - Toggle moderation logging to admin channel (@0) -- **Detailed mod logs**: Track all moderation decisions, pattern matches, and AI analysis -- **Configurable logging**: Per-profile mod log settings saved to config +### Optional Features -#### Message Threading & Performance +```bash +# Build without audio (for headless servers) +make build-no-audio -- **Per-message threading**: Each message now sends in its own thread to eliminate race conditions -- **Concurrent message processing**: User messages and system messages (AI, moderation) no longer block each other -- **Non-blocking channel operations**: Prevents deadlocks and improves responsiveness -- **Better concurrent handling**: Multiple messages can be sent simultaneously without interference +# Or with cargo directly +cargo build --release --no-default-features +``` -#### Enhanced Content Filtering +## Documentation -- **Expanded quick patterns**: Added comprehensive detection for inappropriate content involving minors -- **Improved spam detection**: Better recognition of repetitive/spam content -- **Allowlist bypass**: Allowlisted users bypass all content filters -- **Separated AI functions**: AI moderation and conversational AI now use different prompts for better accuracy +- `MANUAL.md` for detailed guides and full command reference +- `man bhcli` for quick terminal reference (installed on first run) +- `OPTIMIZATION_REPORT.md` for technical details and performance info -#### Technical Improvements +## Performance Notes -- **Thread-safe message sending**: All message operations now use dedicated threads -- **Improved error handling**: Better channel error management with try_send patterns -- **Enhanced logging**: More detailed moderation and system logs +BHCLI is optimized for low resource usage: +- **500ms tick rate** keeps CPU usage minimal +- **Optional audio** works on headless systems +- **Lazy loading** defers expensive operations +- **LTO and strip** produces compact binaries (~28MB release) -## šŸ¤– Bot System +No fan spin-up, no excessive polling, just efficient terminal communication. -BHCLI now includes a powerful background bot system that runs alongside your main chat client, providing persistent memory and intelligent community management features. +## Contributing -### Quick Start +Built with Rust 2021 edition. Clean codebase, zero compiler warnings, comprehensive error handling. Pull requests welcome. +**Development:** ```bash -# Start BHCLI with a bot named "Assistant" -./target/release/bhcli --bot Assistant - -# Start with admin users -./target/release/bhcli --bot Assistant --bot-admins alice,bob - -# Custom data directory -./target/release/bhcli --bot Assistant --bot-data-dir /custom/path -``` - -### Bot Commands - -Interact with your bot by mentioning it: - +make fmt # Format code +make clippy # Run linter +make check # Fast compilation check +cargo doc --open # Generate and view docs ``` -@Assistant help # Get list of available commands -@Assistant stats alice # View user statistics -@Assistant recall 14:30 # Find message from 14:30 today -@Assistant search "rust error" # Search message history -@Assistant export alice 7 # Export alice's messages (7 days) -@Assistant restore 12345 # Restore deleted message -@Assistant summary 24 # Chat activity summary (24 hours) -@Assistant users # List current online users -@Assistant top messages # Top users by message count -``` - -### Key Features - -- **Perfect Memory**: Never lose important conversations - bot remembers everything -- **Powerful Search**: Find any message instantly by content, user, or timestamp -- **User Analytics**: Detailed statistics on user activity, behavior, and patterns -- **Data Export**: Generate research-ready chat archives and reports -- **Message Recovery**: Restore accidentally deleted messages from bot's memory -- **AI Integration**: Enhanced analysis when OPENAI_API_KEY is configured -- **Admin Tools**: Advanced moderation and user management features - -### Data Storage -Bot data is automatically saved to `bot_data/{botname}/` with: +## Security Notes -- Complete message history (`message_history.json`) -- User statistics database (`user_stats.json`) -- Generated export files (`exports/` directory) +BHCLI is designed for anonymous communication: +- **Tor-first** architecture with SOCKS5 support +- **No telemetry** or external phone-home +- **Local storage** only (config and bot data) +- **Optional features** minimize attack surface -For detailed documentation, see `BOT_SYSTEM.md` +For darknet usage, always route through Tor. Never use `--no-proxy` with .onion addresses unless you know what you're doing. -- **Configuration persistence**: AI settings and mod log preferences saved per profile +## License -These updates significantly improve the chat moderation capabilities while maintaining performance and preventing race conditions in message handling. +MIT +See `LICENSE.md` file for details. +\ No newline at end of file diff --git a/chat-script.READONLY.php b/chat-script.READONLY.php @@ -1,8563 +0,0 @@ -<?php - -// This is a copy of the scripts that this client works on. You can use it to maybe check how make calls to forms or stuff like that idk... - -/* - * LE CHAT-PHP - a PHP Chat based on LE CHAT - Main program - * - * Copyright (C) 2015-2025 Daniel Winzen <daniel@danwin1210.me> - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -/* - * status codes - * 0 - Kicked/Banned - * 1 - Guest - * 2 - Applicant - * 3 - Member - * 4 - System message - * 5 - Moderator - * 6 - Super-Moderator - * 7 - Admin - * 8 - Super-Admin - * 9 - Private messages - */ - -if (!extension_loaded("gettext")) { - prepare_stylesheets("fatal_error"); - send_headers(); - echo '<!DOCTYPE html><html lang="en" dir="ltr"><head>' . meta_html(); - echo "<title>Fatal error</title>"; - echo "<style>$styles[fatal_error]</style>"; - echo "</head><body>"; - echo "<h2>Fatal error: The gettext extension of PHP is required, please install it first.</h2>"; - print_end(); -} -// initialize and load variables/configuration -const LANGUAGES = [ - "ar" => ["name" => "Ų§Ł„Ų¹Ų±ŲØŁŠŲ©", "locale" => "ar", "dir" => "rtl"], - "bg" => ["name" => "Š‘ŃŠŠ»Š³Š°Ń€ŃŠŗŠø", "locale" => "bg_BG", "dir" => "ltr"], - "cs" => ["name" => "čeÅ”tina", "locale" => "cs_CZ", "dir" => "ltr"], - "de" => ["name" => "Deutsch", "locale" => "de_DE", "dir" => "ltr"], - "en" => ["name" => "English", "locale" => "en_GB", "dir" => "ltr"], - "es" => ["name" => "EspaƱol", "locale" => "es_ES", "dir" => "ltr"], - "fi" => ["name" => "Suomi", "locale" => "fi_FI", "dir" => "ltr"], - "fr" => ["name" => "FranƧais", "locale" => "fr_FR", "dir" => "ltr"], - "hi" => ["name" => "ą¤¹ą¤æą¤Øą„ą¤¦ą„€", "locale" => "hi", "dir" => "ltr"], - "id" => ["name" => "Bahasa Indonesia", "locale" => "id_ID", "dir" => "ltr"], - "it" => ["name" => "Italiano", "locale" => "it_IT", "dir" => "ltr"], - "nl" => ["name" => "Nederlands", "locale" => "nl_NL", "dir" => "ltr"], - "pl" => ["name" => "Polski", "locale" => "pl_PL", "dir" => "ltr"], - "pt" => ["name" => "PortuguĆŖs", "locale" => "pt_PT", "dir" => "ltr"], - "ru" => ["name" => "Русский", "locale" => "ru_RU", "dir" => "ltr"], - "tr" => ["name" => "TürkƧe", "locale" => "tr_TR", "dir" => "ltr"], - "uk" => ["name" => "Š£ŠŗŃ€Š°Ń—Š½ŃŃŒŠŗŠ°", "locale" => "uk_UA", "dir" => "ltr"], - "zh-Hans" => ["name" => "简体中文", "locale" => "zh_CN", "dir" => "ltr"], - "zh-Hant" => ["name" => "正體中文", "locale" => "zh_TW", "dir" => "ltr"], -]; -load_config(); -$U = []; // This user data -$db = null; // Database connection -$memcached = null; // Memcached connection -$language = LANG; // user selected language -$locale = LANGUAGES[LANG]["locale"]; // user selected locale -$dir = LANGUAGES[LANG]["dir"]; // user selected language direction -$scripts = []; //js enhancements -$styles = []; //css styles -$session = $_REQUEST["session"] ?? ""; //requested session -// set session variable to cookie if cookies are enabled -if (!isset($_REQUEST["session"]) && isset($_COOKIE[COOKIENAME])) { - $session = $_COOKIE[COOKIENAME]; -} -$session = preg_replace("/[^0-9a-zA-Z]/", "", $session); -load_lang(); -foreach (["date", "mbstring", "pcre"] as $extension) { - if (!extension_loaded($extension)) { - send_fatal_error( - sprintf( - _( - "The %s extension of PHP is required, please install it first.", - ), - $extension, - ), - ); - } -} -mb_internal_encoding("UTF-8"); -check_db(); -cron(); -route(); - -// main program: decide what to do based on queries -function route(): void -{ - global $U, $db; - if (!isset($_REQUEST["action"])) { - send_login(); - } elseif ($_REQUEST["action"] === "view") { - check_session(); - send_messages(); - } elseif ($_REQUEST["action"] === "redirect" && !empty($_GET["url"])) { - send_redirect($_GET["url"]); - } elseif ($_REQUEST["action"] === "wait") { - parse_sessions(); - send_waiting_room(); - } elseif ($_REQUEST["action"] === "post") { - check_session(); - if ( - isset($_POST["kick"]) && - isset($_POST["sendto"]) && - $_POST["sendto"] !== "s *" - ) { - if ( - $U["status"] >= 5 || - ($U["status"] >= 3 && - (get_setting("memkickalways") || - (get_count_mods() == 0 && get_setting("memkick")))) - ) { - if (isset($_POST["what"]) && $_POST["what"] === "purge") { - kick_chatter([$_POST["sendto"]], $_POST["message"], true); - } else { - kick_chatter([$_POST["sendto"]], $_POST["message"], false); - } - } - } elseif (isset($_POST["message"]) && isset($_POST["sendto"])) { - send_post(validate_input()); // Not a guest - } - send_post(); - } elseif ($_REQUEST["action"] === "login") { - check_login(); - show_fails(); - send_frameset(); - } elseif ($_REQUEST["action"] === "controls") { - check_session(); - send_controls(); - } elseif ($_REQUEST["action"] === "greeting") { - check_session(); - send_greeting(); - } elseif ($_REQUEST["action"] === "delete") { - check_session(); - if (!isset($_POST["what"])) { - } elseif ($_POST["what"] === "all") { - if (isset($_POST["confirm"])) { - del_all_messages( - "", - (int) ($U["status"] == 1 ? $U["entry"] : 0), - ); - } else { - send_del_confirm(); - } - } elseif ($_POST["what"] === "last") { - del_last_message(); - } - send_post(); - } elseif ($_REQUEST["action"] === "profile") { - check_session(); - $arg = ""; - if (!isset($_POST["do"])) { - } elseif ($_POST["do"] === "save") { - $arg = save_profile(); - } elseif ($_POST["do"] === "delete") { - if (isset($_POST["confirm"])) { - delete_account(); - } else { - send_delete_account(); - } - } - send_profile($arg); - } elseif ( - $_REQUEST["action"] === "logout" && - $_SERVER["REQUEST_METHOD"] === "POST" - ) { - check_session(); - if ($U["status"] < 3 && get_setting("exitwait")) { - $U["exiting"] = 1; - $stmt = $db->prepare( - "UPDATE " . - PREFIX . - "sessions SET exiting=1 WHERE session=? LIMIT 1;", - ); - $stmt->execute([$U["session"]]); - } else { - kill_session(); - } - send_logout(); - } elseif ($_REQUEST["action"] === "colours") { - check_session(); - send_colours(); - } elseif ($_REQUEST["action"] === "notes") { - check_session(); - if (!isset($_POST["do"])) { - } elseif ($_POST["do"] === "admin" && $U["status"] > 6) { - send_notes(0); - } elseif ($_POST["do"] === "staff" && $U["status"] >= 5) { - send_notes(1); - } elseif ($_POST["do"] === "public" && $U["status"] >= 3) { - send_notes(3); - } - if ( - $U["status"] < 3 || - (!get_setting("personalnotes") && !get_setting("publicnotes")) - ) { - send_access_denied(); - } - send_notes(2); - } elseif ($_REQUEST["action"] === "help") { - check_session(); - send_help(); - } elseif ($_REQUEST["action"] === "viewpublicnotes") { - check_session(); - view_publicnotes(); - } elseif ($_REQUEST["action"] === "inbox") { - check_session(); - if (isset($_POST["do"])) { - clean_inbox_selected(); - } - send_inbox(); - } elseif ($_REQUEST["action"] === "download") { - send_download(); - } elseif ($_REQUEST["action"] === "admin") { - check_session(); - send_admin(route_admin()); - } elseif ($_REQUEST["action"] === "setup") { - route_setup(); - } elseif ($_REQUEST["action"] === "sa_password_reset") { - send_sa_password_reset(); - } else { - send_login(); - } -} - -function route_admin(): string -{ - global $U, $db; - if ($U["status"] < 5) { - send_access_denied(); - } - if (!isset($_POST["do"])) { - return ""; - } elseif ($_POST["do"] === "clean") { - if ($_POST["what"] === "choose") { - send_choose_messages(); - } elseif ($_POST["what"] === "selected") { - clean_selected((int) $U["status"], $U["nickname"]); - } elseif ($_POST["what"] === "room") { - clean_room(); - } elseif ($_POST["what"] === "nick") { - $stmt = $db->prepare( - "SELECT null FROM " . - PREFIX . - "members WHERE nickname=? AND status>=?;", - ); - $stmt->execute([$_POST["nickname"], $U["status"]]); - if (!$stmt->fetch(PDO::FETCH_ASSOC)) { - del_all_messages($_POST["nickname"], 0); - } - } - } elseif ($_POST["do"] === "kick") { - if (isset($_POST["name"])) { - if (isset($_POST["what"]) && $_POST["what"] === "purge") { - kick_chatter($_POST["name"], $_POST["kickmessage"], true); - } else { - kick_chatter($_POST["name"], $_POST["kickmessage"], false); - } - } - } elseif ($_POST["do"] === "logout") { - if (isset($_POST["name"])) { - logout_chatter($_POST["name"]); - } - } elseif ($_POST["do"] === "sessions") { - if (isset($_POST["kick"]) && isset($_POST["nick"])) { - kick_chatter([$_POST["nick"]], "", false); - } elseif (isset($_POST["logout"]) && isset($_POST["nick"])) { - logout_chatter([$_POST["nick"]]); - } - send_sessions(); - } elseif ($_POST["do"] === "register") { - return register_guest(3, $_POST["name"]); - } elseif ($_POST["do"] === "superguest") { - return register_guest(2, $_POST["name"]); - } elseif ($_POST["do"] === "status") { - return change_status($_POST["name"], $_POST["set"]); - } elseif ($_POST["do"] === "regnew") { - return register_new($_POST["name"], $_POST["pass"]); - } elseif ($_POST["do"] === "approve") { - approve_session(); - send_approve_waiting(); - } elseif ($_POST["do"] === "guestaccess") { - if ( - isset($_POST["guestaccess"]) && - preg_match('/^[0123]$/', $_POST["guestaccess"]) - ) { - update_setting("guestaccess", $_POST["guestaccess"]); - change_guest_access(intval($_POST["guestaccess"])); - } - } elseif ($_POST["do"] === "filter") { - send_filter(manage_filter()); - } elseif ($_POST["do"] === "linkfilter") { - send_linkfilter(manage_linkfilter()); - } elseif ($_POST["do"] === "topic") { - if (isset($_POST["topic"])) { - update_setting("topic", htmlspecialchars($_POST["topic"])); - } - } elseif ($_POST["do"] === "passreset") { - return passreset($_POST["name"], $_POST["pass"]); - } - return ""; -} - -function route_setup(): void -{ - global $U; - if (!valid_admin()) { - send_alogin(); - } - $C["bool_settings"] = [ - "suguests" => _("Enable applicants"), - "imgembed" => _("Embed images"), - "timestamps" => _("Show Timestamps"), - "trackip" => _("Show session-IP"), - "memkick" => _("Members can kick, if no moderator is present"), - "memkickalways" => _("Members can always kick"), - "forceredirect" => _("Force redirection"), - "incognito" => _("Incognito mode"), - "sendmail" => _("Send mail on new public message"), - "modfallback" => _( - "Fallback to waiting room, if no moderator is present to approve guests", - ), - "noguestpm" => _("Disable Guests private messages"), - "disablepm" => _("Disable private messages"), - "eninbox" => _("Enable offline inbox"), - "enablegreeting" => _( - "Show a greeting message before showing the messages", - ), - "sortupdown" => _("Sort messages from top to bottom"), - "hidechatters" => _("Hide list of chatters"), - "personalnotes" => _("Personal notes"), - "publicnotes" => _("Public notes"), - "filtermodkick" => _("Apply kick filter on moderators"), - "namedoers" => _("Show who kicks people or purges all messages."), - "hide_reload_post_box" => _("Hide reload post box button"), - "hide_reload_messages" => _("Hide reload messages button"), - "hide_profile" => _("Hide profile button"), - "hide_admin" => _("Hide admin button"), - "hide_notes" => _("Hide notes button"), - "hide_clone" => _("Hide clone button"), - "hide_rearrange" => _("Hide rearrange button"), - "hide_help" => _("Hide help button"), - "postbox_delete_globally" => _("Apply postbox delete button globally"), - "allow_js" => _("Allow enhancing functionality with JavaScript"), - ]; - $C["colour_settings"] = [ - "colbg" => _("Background colour"), - "coltxt" => _("Font colour"), - ]; - $C["msg_settings"] = [ - "msgenter" => _("Entrance"), - "msgexit" => _("Leaving"), - "msgmemreg" => _("Member registered"), - "msgsureg" => _("Applicant registered"), - "msgkick" => _("Kicked"), - "msgmultikick" => _("Multiple kicked"), - "msgallkick" => _("All kicked"), - "msgclean" => _("Room cleaned"), - "msgsendall" => _("Message to all"), - "msgsendmem" => _("Message to members only"), - "msgsendmod" => _("Message to staff only"), - "msgsendadm" => _("Message to admins only"), - "msgsendprv" => _("Private message"), - "msgattache" => _("Attachement"), - ]; - $C["number_settings"] = [ - "memberexpire" => _("Member timeout (minutes)"), - "guestexpire" => _("Guest timeout (minutes)"), - "kickpenalty" => _("Kick penalty (minutes)"), - "entrywait" => _("Waiting room time (seconds)"), - "exitwait" => _("Logout delay (seconds)"), - "captchatime" => _("Captcha timeout (seconds)"), - "messageexpire" => _("Message timeout (minutes)"), - "messagelimit" => _("Message limit (public)"), - "maxmessage" => _("Maximal message length"), - "maxname" => _("Maximal nickname length"), - "minpass" => _("Minimal password length"), - "defaultrefresh" => _("Default message reload time (seconds)"), - "numnotes" => _("Number of notes revisions to keep"), - "maxuploadsize" => _("Maximum upload size in KB"), - "enfileupload" => _("Enable file uploads"), - "max_refresh_rate" => _("Lowest refresh rate"), - "min_refresh_rate" => _("Highest refresh rate"), - ]; - $C["textarea_settings"] = [ - "rulestxt" => _("Rules (html)"), - "css" => _("CSS Style"), - "disabletext" => _("Chat disabled message (html)"), - ]; - $C["text_settings"] = [ - "dateformat" => _( - '<a target="_blank" href="https://php.net/manual/en/function.date.php#refsect1-function.date-parameters" rel="noopener noreferrer">Date formating</a>', - ), - "captchachars" => _("Characters used in Captcha"), - "captchattfont" => _( - "Font name or path and filename for TrueType font used in some captchas", - ), - "redirect" => _("Custom redirection script"), - "chatname" => _("Chat name"), - "mailsender" => _("Send mail using this address"), - "mailreceiver" => _("Send mail to this address"), - "nickregex" => _("Nickname regex"), - "passregex" => _("Password regex"), - "externalcss" => _("Link to external CSS file (on your own server)"), - "metadescription" => _( - "Meta description (best 50 - 160 characters for SEO)", - ), - "exitingtxt" => _('Show this text when a user\'s logout is delayed'), - "sysmessagetxt" => _("Prepend this text to system messages"), - ]; - $extra_settings = [ - "guestaccess" => _("Change Guestaccess"), - "englobalpass" => _("Enable global Password"), - "globalpass" => _("Global Password:"), - "captcha" => _("Captcha"), - "dismemcaptcha" => _("Only for guests"), - "topic" => _("Topic"), - "guestreg" => _("Let guests register themselves"), - "defaulttz" => _("Default time zone"), - ]; - $C["settings"] = array_keys( - array_merge( - $extra_settings, - $C["bool_settings"], - $C["colour_settings"], - $C["msg_settings"], - $C["number_settings"], - $C["textarea_settings"], - $C["text_settings"], - ), - ); // All settings in the database - if (!isset($_POST["do"])) { - } elseif ($_POST["do"] === "save") { - save_setup($C); - } elseif ($_POST["do"] === "backup" && $U["status"] == 8) { - send_backup($C); - } elseif ($_POST["do"] === "restore" && $U["status"] == 8) { - restore_backup($C); - send_backup($C); - } elseif ($_POST["do"] === "destroy" && $U["status"] == 8) { - if (isset($_POST["confirm"])) { - destroy_chat($C); - } else { - send_destroy_chat(); - } - } - send_setup($C); -} - -// html output subs -function prepare_stylesheets(string $class): void -{ - global $U, $db, $scripts, $styles; - if ($class === "fatal_error") { - $styles["fatal_error"] = "body{background-color:#000000;color:#FF0033}"; - } - $styles["default"] = - "body,iframe{background-color:#000000;color:#FFFFFF;font-size:14px;text-align:center;width:100%;height:100%;margin:0;padding:0;border:none}"; - $styles["default"] .= - "a:visited{color:#B33CB4} a:link{color:#00A2D4} a:active{color:#55A2D4}"; - $styles["default"] .= - "input,select,textarea{color:#FFFFFF;background-color:#000000} "; - $styles["default"] .= - ".error{color:#FF0033;text-align:left} .delbutton{background-color:#660000} .backbutton{background-color:#004400} #exitbutton{background-color:#AA0000} "; - $styles["default"] .= - ".setup table table,.admin table table,.profile table table{width:100%;text-align:left} "; - $styles["default"] .= - ".alogin table,.init table,.destroy_chat table,.delete_account table,.sessions table,.filter table,.linkfilter table,.notes table,.approve_waiting table,.del_confirm table,.profile table,.admin table,.backup table,.setup table{margin-left:auto;margin-right:auto} "; - $styles["default"] .= - ".setup table table table,.admin table table table,.profile table table table{border-spacing:0px;margin-left:auto;margin-right:unset;width:unset} "; - $styles["default"] .= - ".setup table table td,.backup #restoresubmit,.backup #backupsubmit,.admin table table td,.profile table table td,.login td+td,.alogin td+td{text-align:right} "; - $styles["default"] .= - ".init td,.backup #restorecheck td,.admin #clean td,.admin #regnew td,.session td,.messages,.inbox,.approve_waiting td,.choose_messages,.greeting,.help,.login td,.alogin td{text-align:left} "; - $styles["default"] .= - ".approve_waiting #action td:only-child,.help #backcredit,.login td:only-child,.alogin td:only-child,.init td:only-child{text-align:center} .sessions td,.sessions th,.approve_waiting td,.approve_waiting th{padding: 5px} "; - $styles["default"] .= - ".sessions td td{padding: 1px} .notes textarea{height:80vh;width:80%} "; - $styles["default"] .= - ".post table,.controls table,.login table{border-spacing:0px;margin-left:auto;margin-right:auto} .login table{border:2px solid} .controls{overflow-y:none} "; - if ($class === "init" || !$db instanceof PDO) { - return; - } - if ($class === "frameset") { - if ( - ($U["status"] >= 5 || - ($U["status"] > 2 && get_count_mods() == 0)) && - get_setting("enfileupload") > 0 && - get_setting("enfileupload") <= $U["status"] - ) { - $postheight = "120px"; - } else { - $postheight = "100px"; - } - if ( - (!isset($_REQUEST["sort"]) && !$U["sortupdown"]) || - (isset($_REQUEST["sort"]) && $_REQUEST["sort"] == 0) - ) { - $styles[ - "frameset" - ] = "#frameset-mid{position:fixed;top:$postheight;bottom:45px;left:0;right:0;margin:0;padding:0;overflow:hidden}"; - $styles[ - "frameset" - ] .= "#frameset-top{position:fixed;top:0;left:0;right:0;height:$postheight;margin:0;padding:0;overflow:hidden;border-bottom: 1px solid}"; - $styles["frameset"] .= - "#frameset-bot{position:fixed;bottom:0;left:0;right:0;height:45px;margin:0;padding:0;overflow:hidden;border-top:1px solid}"; - } else { - $styles[ - "frameset" - ] = " #frameset-mid{position:fixed;top:45px;bottom:$postheight;left:0;right:0;margin:0;padding:0;overflow:hidden}"; - $styles["frameset"] .= - "#frameset-top{position:fixed;top:0;left:0;right:0;height:45px;margin:0;padding:0;overflow:hidden;border-bottom:1px solid}"; - $styles[ - "frameset" - ] .= "#frameset-bot{position:fixed;bottom:0;left:0;right:0;height:$postheight;margin:0;padding:0;overflow:hidden;border-top:1px solid}"; - } - } - if ($class === "filter") { - $styles["filter"] = "table table{width:100%} "; - $styles["filter"] .= - "table table td:nth-child(1){width:8em;font-weight:bold} "; - $styles["filter"] .= - "table table td:nth-child(2),table table td:nth-child(3){width:12em} "; - $styles["filter"] .= "table table td:nth-child(4){width:9em} "; - $styles["filter"] .= - "table table td:nth-child(5),table table td:nth-child(6),table table td:nth-child(7),table table td:nth-child(8){width:5em} "; - } - if ($class === "linkfilter") { - $styles["linkfilter"] = "table table{width:100%} "; - $styles["linkfilter"] .= - "table table td:nth-child(1){width:8em;font-weight:bold} "; - $styles["linkfilter"] .= - "table table td:nth-child(2),table table td:nth-child(3){width:12em} "; - $styles["linkfilter"] .= - "table table td:nth-child(4),table table td:nth-child(5){width:5em} "; - } - if ($class === "post") { - $styles["post"] = ".spacer{width:10px} #firstline{vertical-align:top}"; - } - if ($class === "messages") { - $styles["messages"] = - ".nicklink{text-decoration:none}.channellink{text-decoration:underline}"; - $styles["messages"] .= - "#chatters{max-height:100px;overflow-y:auto} #chatters, #chatters table{border-spacing:0px} "; - $styles["messages"] .= - "#manualrefresh{display:block;position:fixed;text-align:center;left:25%;width:50%;top:-200%;animation:timeout_messages "; - $styles["messages"] .= $U["refresh"] + 20; - $styles["messages"] .= - "s forwards;z-index:2;background-color:#500000;border:2px solid #ff0000} "; - $styles["messages"] .= - "@keyframes timeout_messages{0%{top:-200%} 99%{top:-200%} 100%{top:0%}} "; - $styles["messages"] .= - ".msg{max-height:180px;overflow-y:auto} #bottom_link{position:fixed;top:0.5em;right:0.5em} #top_link{position:fixed;bottom:0.5em;right:0.5em} "; - $styles["messages"] .= - "#chatters th,#chatters td{vertical-align:top} a img{width:15%} a:hover img{width:35%}"; - $styles["messages"] .= "#messages{word-wrap:break-word}"; - } - $css = get_setting("css"); - $coltxt = get_setting("coltxt"); - if (!empty($U["bgcolour"])) { - $colbg = $U["bgcolour"]; - } else { - $colbg = get_setting("colbg"); - } - $styles["custom"] = preg_replace( - "/(\r?\n|\r\n?)/u", - "", - "body,iframe{background-color:#$colbg;color:#$coltxt} $css", - ); - $allow_js = (bool) get_setting("allow_js"); - if ($allow_js) { - $scripts["default"] = - 'if(window.history.replaceState){window.history.replaceState(null,"");}'; - if ($class === "frameset") { - $scripts["frameset"] = 'window.addEventListener("message", (e)=>{ - if(e.data === "post_box_loaded"){ - let autofocus = document.querySelector("iframe[name=post").contentDocument.querySelector("input[autofocus]"); - if(autofocus){ - autofocus.focus(); - } - } - });'; - } - if ($class === "post") { - $scripts["post"] = 'window.addEventListener("load", _=>{ - window.top.postMessage("post_box_loaded", window.location.origin); - })'; - } - } -} - -function print_stylesheet(string $class): void -{ - global $scripts, $styles; - //default css - echo "<style>$styles[default]</style>"; - if ($class === "init") { - return; - } - if (isset($styles[$class])) { - echo "<style>$styles[$class]</style>"; - } - //overwrite with custom css - echo "<style>$styles[custom]</style>"; - $allow_js = (bool) get_setting("allow_js"); - if ($allow_js) { - echo "<script>$scripts[default]</script>"; - if (isset($scripts[$class])) { - echo "<script>$scripts[$class]</script>"; - } - } -} - -function print_end(): void -{ - echo "</body></html>"; - exit(); -} - -function credit(): string -{ - return '<small><br><br><a target="_blank" href="https://github.com/DanWin/le-chat-php" rel="noreferrer noopener">LE CHAT-PHP - ' . - VERSION . - "</a></small>"; -} - -function meta_html(): string -{ - global $U, $db; - $colbg = "000000"; - $description = ""; - if (!empty($U["bgcolour"])) { - $colbg = $U["bgcolour"]; - } else { - if ($db instanceof PDO) { - $colbg = get_setting("colbg"); - $description = - '<meta name="description" content="' . - htmlspecialchars(get_setting("metadescription")) . - '">'; - } - } - return '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="referrer" content="no-referrer"><meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes"><meta name="theme-color" content="#' . - $colbg . - '"><meta name="msapplication-TileColor" content="#' . - $colbg . - '">' . - $description; -} - -function form(string $action, string $do = ""): string -{ - global $language, $session; - $form = - "<form action=\"$_SERVER[SCRIPT_NAME]\" enctype=\"multipart/form-data\" method=\"post\">" . - hidden("lang", $language) . - hidden("nc", substr(time(), -6)) . - hidden("action", $action); - if (!empty($session)) { - $form .= hidden("session", $session); - } - if ($do !== "") { - $form .= hidden("do", $do); - } - return $form; -} - -function form_target(string $target, string $action, string $do = ""): string -{ - global $language, $session; - $form = - "<form action=\"$_SERVER[SCRIPT_NAME]\" enctype=\"multipart/form-data\" method=\"post\" target=\"$target\">" . - hidden("lang", $language) . - hidden("nc", substr(time(), -6)) . - hidden("action", $action); - if (!empty($session)) { - $form .= hidden("session", $session); - } - if ($do !== "") { - $form .= hidden("do", $do); - } - return $form; -} - -function hidden(string $name = "", string $value = ""): string -{ - return "<input type=\"hidden\" name=\"$name\" value=\"$value\">"; -} - -function submit(string $value = "", string $extra_attribute = ""): string -{ - return "<input type=\"submit\" value=\"$value\" $extra_attribute>"; -} - -function thr(): void -{ - echo "<tr><td><hr></td></tr>"; -} - -function print_start(string $class = "", int $ref = 0, string $url = ""): void -{ - global $language, $dir; - prepare_stylesheets($class); - send_headers(); - if (!empty($url)) { - $url = str_replace("&amp;", "&", $url); // Don't escape "&" in URLs here, it breaks some (older) browsers and js refresh! - header("Refresh: $ref; URL=$url"); - } - echo '<!DOCTYPE html><html lang="' . - $language . - '" dir="' . - $dir . - '"><head>' . - meta_html(); - if (!empty($url)) { - echo "<meta http-equiv=\"Refresh\" content=\"$ref; URL=$url\">"; - } - if ($class === "init") { - echo "<title>" . _("Initial Setup") . "</title>"; - } else { - echo "<title>" . get_setting("chatname") . "</title>"; - } - print_stylesheet($class); - echo "</head><body class=\"$class\">"; - if ( - $class !== "init" && - ($externalcss = get_setting("externalcss")) != "" - ) { - //external css - in body to make it non-renderblocking - echo "<link rel=\"stylesheet\" type=\"text/css\" href=\"$externalcss\">"; - } -} - -function send_redirect(string $url): void -{ - $url = trim(htmlspecialchars_decode(rawurldecode($url))); - preg_match("~^(.*)://~u", $url, $match); - $url = preg_replace("~^(.*)://~u", "", $url); - $escaped = htmlspecialchars($url); - if (isset($match[1]) && ($match[1] === "http" || $match[1] === "https")) { - print_start("redirect", 0, $match[0] . $escaped); - echo "<p>" . - sprintf( - _("Redirecting to: %s"), - "<a href=\"$match[0]$escaped\">$match[0]$escaped</a>", - ) . - "</p>"; - } else { - print_start("redirect"); - if (!isset($match[0])) { - $match[0] = ""; - } - if (preg_match("~^(javascript|blob|data):~", $url)) { - echo "<p>" . - sprintf( - _( - "Dangerous non-http link requested, copy paste this link if you are really sure: %s", - ), - "$match[0]$escaped", - ) . - "</p>"; - } else { - echo "<p>" . - sprintf( - _("Non-http link requested: %s"), - "<a href=\"$match[0]$escaped\">$match[0]$escaped</a>", - ) . - "</p>"; - } - echo "<p>" . - sprintf( - _("If it's not working, try this one: %s"), - "<a href=\"http://$escaped\">http://$escaped</a>", - ) . - "</p>"; - } - print_end(); -} - -function send_access_denied(): void -{ - global $U; - http_response_code(403); - print_start("access_denied"); - echo "<h1>" . - _("Access denied") . - "</h1>" . - sprintf( - _("You are logged in as %s and don't have access to this section."), - style_this(htmlspecialchars($U["nickname"]), $U["style"]), - ) . - "<br>"; - echo form("logout"); - echo submit(_("Logout"), 'id="exitbutton"') . "</form>"; - print_end(); -} - -function send_captcha(): void -{ - global $db, $memcached; - $difficulty = (int) get_setting("captcha"); - if ($difficulty === 0 || !extension_loaded("gd")) { - return; - } - if (function_exists("putenv")) { - // from https://www.php.net/manual/en/function.imagefttext.php - // enables fonts to be loaded from the directory the script is in - putenv("GDFONTPATH=" . realpath(".")); - } - $captchachars = get_setting("captchachars"); - $length = strlen($captchachars) - 1; - $code = ""; - for ($i = 0; $i < 5; ++$i) { - $code .= $captchachars[mt_rand(0, $length)]; - } - $randid = mt_rand(); - $time = time(); - if (MEMCACHED) { - $memcached->set( - DBNAME . "-" . PREFIX . "captcha-$randid", - $code, - get_setting("captchatime"), - ); - } else { - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "captcha (id, time, code) VALUES (?, ?, ?);", - ); - $stmt->execute([$randid, $time, $code]); - } - echo '<tr id="captcha"><td>'; - if ($difficulty === 4) { - echo _("Type the characters connected by dotted lines:"); - } elseif ($difficulty === 7 || $difficulty === 8 || $difficulty === 10) { - echo _("Type the five largest characters:"); - } else { - echo _("Type the characters in the image:"); - } - if ($difficulty === 1 || $difficulty === 2) { - $fontwidth = imagefontwidth(5); - $fontheight = imagefontheight(5); - $CAPTCHAWIDTH = $fontwidth * 5 * 3; - $CAPTCHAHEIGHT = $fontheight * 3; - $im = imagecreatetruecolor($CAPTCHAWIDTH, $CAPTCHAHEIGHT); - $bg = imagecolorallocate( - $im, - mt_rand(0, 255), - mt_rand(0, 255), - mt_rand(0, 255), - ); - imagefill($im, 0, 0, $bg); - $margins = 0; - if ($difficulty === 2) { - for ($i = 0; $i < 5; $i++) { - for ($j = 0; $j < 2; $j++) { - imagefilledrectangle( - $im, - mt_rand( - ($i * $CAPTCHAWIDTH) / 5, - (($i + 1) * $CAPTCHAWIDTH) / 5, - ), - mt_rand(0, $CAPTCHAHEIGHT), - mt_rand( - (($i + 0.5) * $CAPTCHAWIDTH) / 5, - (($i + 1.5) * $CAPTCHAWIDTH) / 5, - ), - mt_rand(0, $CAPTCHAHEIGHT), - imagecolorallocate( - $im, - mt_rand(128, 255), - mt_rand(128, 255), - mt_rand(128, 255), - ), - ); - } - } - } - imagefilter($im, IMG_FILTER_SMOOTH, 5); - $xoffset = 0; - for ($i = 0; $i < 5; $i++) { - $xoffset += mt_rand(0, $CAPTCHAWIDTH - 5 * $fontwidth - $xoffset); // randomly shift characters to the right - imagechar( - $im, - 5, - $xoffset + $i * $fontwidth, - mt_rand(0, $CAPTCHAHEIGHT - $fontheight), - $code[$i], - imagecolorallocate( - $im, - mt_rand(224, 255), - mt_rand(224, 255), - mt_rand(224, 255), - ), - ); - if ($difficulty === 2) { - imagelayereffect($im, IMG_EFFECT_OVERLAY); - } - } - } elseif ($difficulty === 3) { - // hard - $CAPTCHAWIDTH = 120; - $CAPTCHAHEIGHT = 80; - $im = imagecreatetruecolor(55, 24); - $bg = imagecolorallocatealpha($im, 0, 0, 0, 127); - $fg = imagecolorallocate($im, 255, 255, 255); - $cc = imagecolorallocate($im, 200, 200, 200); - $cb = imagecolorallocatealpha($im, 0, 0, 0, 127); - imagefill($im, 0, 0, $bg); - $line = imagecolorallocate($im, 255, 255, 255); - $deg = (mt_rand(0, 1) * 2 - 1) * mt_rand(10, 20); - - $background = imagecreatetruecolor(120, 80); - imagefill($background, 0, 0, $cb); - - for ($i = 0; $i < 20; $i++) { - $char = imagecreatetruecolor(12, 16); - imagestring( - $char, - 5, - 2, - 2, - $captchachars[mt_rand(0, $length)], - $cc, - ); - $char = imagerotate( - $char, - (mt_rand(0, 1) * 2 - 1) * mt_rand(10, 20), - $cb, - ); - $char = imagescale($char, 24, 32); - imagefilter($char, IMG_FILTER_SMOOTH, 0.6); - imagecopy( - $background, - $char, - rand(0, 100), - rand(0, 60), - 0, - 0, - 24, - 32, - ); - } - - imagestring($im, 5, 5, 5, $code, $fg); - $im = imagescale($im, 110, 48); - imagefilter($im, IMG_FILTER_SMOOTH, 0.5); - imagefilter($im, IMG_FILTER_GAUSSIAN_BLUR); - $im = imagerotate($im, $deg, $bg); - $im = imagecrop($im, [ - "x" => 0, - "y" => 0, - "width" => 120, - "height" => 80, - ]); - imagecopy($background, $im, 0, 0, 0, 0, 110, 80); - imagedestroy($im); - $im = $background; - - for ($i = 0; $i < 1000; ++$i) { - $c = mt_rand(100, 230); - $dots = imagecolorallocate($im, $c, $c, $c); - imagesetpixel($im, mt_rand(0, 120), mt_rand(0, 80), $dots); - } - imagedestroy($char); - } elseif ($difficulty === 4) { - // extreme - $CAPTCHAWIDTH = 300; - $CAPTCHAHEIGHT = 300; - $im = imagecreatetruecolor($CAPTCHAWIDTH, $CAPTCHAHEIGHT); - $bg = imagecolorallocate( - $im, - mt_rand(0, 255), - mt_rand(0, 255), - mt_rand(0, 255), - ); - $fg = imagecolorallocate( - $im, - mt_rand(0, 255), - mt_rand(0, 255), - mt_rand(0, 255), - ); - imagefill($im, 0, 0, $bg); - imagelayereffect($im, IMG_EFFECT_NORMAL); - $fontwidth = imagefontwidth(5); // 9 pixels wide - $fontheight = imagefontheight(5); // 10 pixels high - for ($i = 0; $i < 20; ++$i) { - $leftx = mt_rand(0, $CAPTCHAWIDTH - $fontwidth); - $topy = mt_rand(0, $CAPTCHAHEIGHT - $fontheight); - for ($j = 0; $j < $numpoints * 2; $j += 2) { - $points[$j] = $leftx + mt_rand(0, $fontwidth); - $points[$j + 1] = $topy + mt_rand(0, $fontheight); - } - $numpoints = (int) mt_rand(3, 6); - for ($k = 0; $k < $numpoints * 2; $k += 2) { - $points[$k] = mt_rand(0, $CAPTCHAWIDTH); - $points[$k + 1] = mt_rand(0, $CAPTCHAHEIGHT); - } - imagefilledpolygon( - $im, - $points, - $numpoints, - imagecolorallocate( - $im, - mt_rand(0, 255), - mt_rand(0, 255), - mt_rand(0, 255), - ), - ); - } - $chars = []; - $x = $y = 0; - imagelayereffect($im, IMG_EFFECT_NORMAL); - for ($i = 0; $i < 20; ++$i) { - $found = false; - while (!$found) { - $x = mt_rand(0, $CAPTCHAWIDTH - $fontwidth); - $y = mt_rand(0, $CAPTCHAHEIGHT - $fontheight); - $found = true; - foreach ($chars as $char) { - if ($char["x"] >= $x && $char["x"] - $x < 25) { - $found = false; - } elseif ($char["x"] < $x && $x - $char["x"] < 25) { - $found = false; - } - if (!$found) { - if ($char["y"] >= $y && $char["y"] - $y < 25) { - break; - } elseif ($char["y"] < $y && $y - $char["y"] < 25) { - break; - } else { - $found = true; - } - } - } - } - $chars[] = ["x", "y"]; - $chars[$i]["x"] = $x; - $chars[$i]["y"] = $y; - imagelayereffect($im, IMG_EFFECT_OVERLAY); - $fg = imagecolorallocate($im, 255, 255, 255); - if ($i < 5) { - // characters in solution - imagechar( - $im, - 5, - $chars[$i]["x"], - $chars[$i]["y"], - $code[$i], - $fg, - ); - } else { - // spurious characters - imagechar( - $im, - 5, - $chars[$i]["x"], - $chars[$i]["y"], - $captchachars[mt_rand(0, $length)], - $fg, - ); - } - } - imagelayereffect($im, IMG_EFFECT_OVERLAY); - for ($i = 5; $i < 19; ++$i) { - // solid lines between spurious characters - imageline( - $im, - $chars[$i]["x"] + $fontwidth / 2, - $chars[$i]["y"] + $fontheight / 2, - $chars[$i + 1]["x"] + $fontwidth / 2, - $chars[$i + 1]["y"] + $fontheight / 2, - imagecolorallocate( - $im, - mt_rand(0, 255), - mt_rand(0, 255), - mt_rand(0, 255), - ), - ); - } - // dashed lines between characters in solution - imagelayereffect($im, IMG_EFFECT_OVERLAY); - for ($i = 0; $i < 4; ++$i) { - $dottedlinecolor = imagecolorallocate($im, 255, 255, 255); - $dottedlinestyle = [ - $dottedlinecolor, - $dottedlinecolor, - $dottedlinecolor, - IMG_COLOR_TRANSPARENT, - IMG_COLOR_TRANSPARENT, - IMG_COLOR_TRANSPARENT, - ]; - imagesetstyle($im, $dottedlinestyle); - $follow = imagecolorallocate( - $im, - mt_rand(10, 255), - mt_rand(10, 255), - mt_rand(10, 255), - ); - imageline( - $im, - $chars[$i]["x"] + $fontwidth / 2, - $chars[$i]["y"] + $fontheight / 2, - $chars[$i + 1]["x"] + 4, - $chars[$i + 1]["y"] + 8, - IMG_COLOR_STYLED, - ); - } - } elseif ( - $difficulty === 5 || - $difficulty === 6 || - $difficulty === 7 || - $difficulty === 8 || - $difficulty === 9 || - $difficulty === 10 - ) { - // TrueType - $CAPTCHAWIDTH = 620; - $CAPTCHAHEIGHT = 177; - $im = imagecreatetruecolor($CAPTCHAWIDTH, $CAPTCHAHEIGHT); - if ($difficulty === 5 || $difficulty === 6) { - imagelayereffect($im, IMG_EFFECT_MULTIPLY); - } else { - imagelayereffect($im, IMG_EFFECT_OVERLAY); - } - if ($difficulty === 8) { - imagefill($im, 0, 0, imagecolorallocate($im, 0, 0, 0)); - } else { - imagefill( - $im, - 0, - 0, - imagecolorallocate( - $im, - mt_rand(0, 255), - mt_rand(0, 255), - mt_rand(0, 255), - ), - ); - } - if ($difficulty === 7 || $difficulty === 8 || $difficulty === 10) { - for ($i = 0; $i < 50; ++$i) { - // small spurious characters - if ($difficulty === 7 || $difficulty === 10) { - $color = imagecolorallocate( - $im, - mt_rand(0, 255), - mt_rand(0, 255), - mt_rand(0, 255), - ); - } else { - $lightness = mt_rand(127, 255); - $color = imagecolorallocate( - $im, - $lightness, - $lightness, - $lightness, - ); - } - $charsize = mt_rand( - $CAPTCHAHEIGHT * 0.18, - $CAPTCHAHEIGHT * 0.25, - ); - // $color = imagecolorallocate($im, mt_rand(0, 255), mt_rand(0, 255), mt_rand(0, 255)); - imagefttext( - $im, - $charsize, - mt_rand(0, 90) - 45, - mt_rand(0, $CAPTCHAWIDTH), - mt_rand(0, $CAPTCHAHEIGHT), - $color, - get_setting("captchattfont"), - $captchachars[mt_rand(0, strlen($captchachars) - 1)], - ); - } - } - // characters in solution - for ($i = 0; $i < 5; ++$i) { - if ($difficulty === 8) { - $lightness = mt_rand(127, 255); - $color = imagecolorallocate( - $im, - $lightness, - $lightness, - $lightness, - ); - } else { - $color = imagecolorallocate( - $im, - mt_rand(0, 255), - mt_rand(0, 255), - mt_rand(0, 255), - ); - } - $charsize = mt_rand($CAPTCHAHEIGHT * 0.5, $CAPTCHAHEIGHT * 0.7); - imagefttext( - $im, - $charsize, - mt_rand(0, 90) - 45, - ($CAPTCHAWIDTH / 8) * ($i * 1.5 + 0.5), - $CAPTCHAHEIGHT - $charsize / 2, - $color, - get_setting("captchattfont"), - $code[$i - 5], - ); - } - if ($difficulty === 7 || $difficulty === 8 || $difficulty === 10) { - for ($i = 0; $i < 50; ++$i) { - // more small spurious characters - if ($difficulty === 7) { - $color = imagecolorallocate( - $im, - mt_rand(0, 255), - mt_rand(0, 255), - mt_rand(0, 255), - ); - } else { - // monochrome - $lightness = mt_rand(127, 255); - $color = imagecolorallocate( - $im, - $lightness, - $lightness, - $lightness, - ); - } - $charsize = mt_rand( - $CAPTCHAHEIGHT * 0.18, - $CAPTCHAHEIGHT * 0.25, - ); - imagefttext( - $im, - $charsize, - mt_rand(0, 90) - 45, - mt_rand(0, $CAPTCHAWIDTH), - mt_rand(0, $CAPTCHAHEIGHT), - $color, - get_setting("captchattfont"), - $captchachars[mt_rand(0, strlen($captchachars) - 1)], - ); - } - } - if ($difficulty === 9 || $difficulty === 10) { - imagelayereffect($im, IMG_EFFECT_MULTIPLY); - $interval = mt_rand(3, $CAPTCHAWIDTH / 32); - $x = 0; - while ($x < $CAPTCHAWIDTH) { - $interval = mt_rand(2, $CAPTCHAWIDTH / 32); - $line = imagecolorallocate( - $im, - mt_rand(0, 255), - mt_rand(0, 255), - mt_rand(0, 255), - ); - imagefilledrectangle( - $im, - $x, - 0, - $x + $interval, - $CAPTCHAHEIGHT, - imagecolorallocate( - $im, - mt_rand(128, 255), - mt_rand(128, 255), - mt_rand(128, 255), - ), - ); - $x += $interval; - } - $y = 0; - while ($y < $CAPTCHAHEIGHT) { - $interval = mt_rand(3, $CAPTCHAHEIGHT / 16); - imagefilledrectangle( - $im, - 0, - $y, - $CAPTCHAWIDTH, - $y + $interval, - imagecolorallocate( - $im, - mt_rand(128, 255), - mt_rand(128, 255), - mt_rand(128, 255), - ), - ); - $y += $interval; - } - } - if ($difficulty === 5 || $difficulty === 6) { - if ($difficulty === 6) { - $iterations = 10; // hollow - } else { - $iterations = 1; // solid - } - for ($i = 0; $i < $iterations; ++$i) { - // apply layers of hollow or solid shapes - for ($j = 0; $j < 5; ++$j) { - $width = mt_rand(1, $CAPTCHAWIDTH / 5 - 1); - $height = mt_rand(1, $CAPTCHAHEIGHT - 1); - $center_x = - ($j * $CAPTCHAWIDTH) / 5 + - mt_rand(0, $CAPTCHAWIDTH / 5 - $width / 2); - $center_y = - $height / 2 + mt_rand(0, $CAPTCHAHEIGHT - $height); - if ($difficulty === 6) { - imagelayereffect($im, IMG_EFFECT_OVERLAY); - imageellipse( - $im, - $center_x, - $center_y, - $width, - $height, - imagecolorallocate( - $im, - mt_rand(127, 255), - mt_rand(127, 255), - mt_rand(127, 255), - ), - ); - imagelayereffect($im, IMG_EFFECT_MULTIPLY); - imageellipse( - $im, - $center_x, - $center_y, - $width, - $height, - imagecolorallocate( - $im, - mt_rand(127, 255), - mt_rand(127, 255), - mt_rand(127, 255), - ), - ); - } else { - imagefilledellipse( - $im, - $center_x, - $center_y, - $width, - $height, - imagecolorallocate( - $im, - mt_rand(127, 255), - mt_rand(127, 255), - mt_rand(127, 255), - ), - ); - } - } - for ($j = 0; $j < 5; ++$j) { - $width = mt_rand(1, $CAPTCHAWIDTH / 5 - 1); - $height = mt_rand(1, $CAPTCHAHEIGHT - 1); - $center_x = - ($j * $CAPTCHAWIDTH) / 5 + - $width / 2 + - mt_rand(0, $CAPTCHAWIDTH / 5 - $width / 2); - $center_y = - $height / 2 + mt_rand(0, $CAPTCHAHEIGHT - $height); - if ($difficulty === 6) { - // https://www.php.net/manual/en/function.imagearc.php - imagelayereffect($im, IMG_EFFECT_OVERLAY); - imagearc( - $im, - $center_x, - $center_y, - $width, - $height, - mt_rand(0, 360), - mt_rand(0, 360), - imagecolorallocate( - $im, - mt_rand(127, 255), - mt_rand(127, 255), - mt_rand(127, 255), - ), - ); - imagelayereffect($im, IMG_EFFECT_MULTIPLY); - imagearc( - $im, - $center_x, - $center_y, - $width, - $height, - mt_rand(0, 360), - mt_rand(0, 360), - imagecolorallocate( - $im, - mt_rand(127, 255), - mt_rand(127, 255), - mt_rand(127, 255), - ), - ); - } else { - imagefilledarc( - $im, - $center_x, - $center_y, - $width, - $height, - mt_rand(0, 360), - mt_rand(0, 360), - imagecolorallocate( - $im, - mt_rand(127, 255), - mt_rand(127, 255), - mt_rand(127, 255), - ), - IMG_ARC_PIE, - ); - } - } - for ($j = 0; $j < 5; ++$j) { - $numpoints = (int) mt_rand(3, 6); - for ($k = 0; $k < $numpoints * 2; $k += 2) { - $points[$k] = mt_rand( - ($j * $CAPTCHAWIDTH) / 5, - (($j + 1) * $CAPTCHAWIDTH) / 5, - ); - $points[$k + 1] = mt_rand(0, $CAPTCHAHEIGHT); - } - if ($difficulty === 6) { - imagelayereffect($im, IMG_EFFECT_OVERLAY); - imagepolygon( - $im, - $points, - $numpoints, - imagecolorallocate( - $im, - mt_rand(127, 255), - mt_rand(127, 255), - mt_rand(127, 255), - ), - ); - imagelayereffect($im, IMG_EFFECT_MULTIPLY); - imagepolygon( - $im, - $points, - $numpoints, - imagecolorallocate( - $im, - mt_rand(127, 255), - mt_rand(127, 255), - mt_rand(127, 255), - ), - ); - } else { - imagefilledpolygon( - $im, - $points, - $numpoints, - imagecolorallocate( - $im, - mt_rand(127, 255), - mt_rand(127, 255), - mt_rand(127, 255), - ), - ); - } - } - for ($j = 0; $j < 5; $j++) { - if ($difficulty === 6) { - imagelayereffect($im, IMG_EFFECT_OVERLAY); - imagerectangle( - $im, - mt_rand( - ($j * $CAPTCHAWIDTH) / 5, - (($j + 1) * $CAPTCHAWIDTH) / 5, - ), - mt_rand(0, $CAPTCHAHEIGHT), - mt_rand( - ($j * $CAPTCHAWIDTH) / 5, - (($j + 1) * $CAPTCHAWIDTH) / 5, - ), - mt_rand(0, $CAPTCHAHEIGHT), - imagecolorallocate( - $im, - mt_rand(127, 255), - mt_rand(127, 255), - mt_rand(127, 255), - ), - ); - imagelayereffect($im, IMG_EFFECT_MULTIPLY); - imagerectangle( - $im, - mt_rand( - ($j * $CAPTCHAWIDTH) / 5, - (($j + 1) * $CAPTCHAWIDTH) / 5, - ), - mt_rand(0, $CAPTCHAHEIGHT), - mt_rand( - ($j * $CAPTCHAWIDTH) / 5, - (($j + 1) * $CAPTCHAWIDTH) / 5, - ), - mt_rand(0, $CAPTCHAHEIGHT), - imagecolorallocate( - $im, - mt_rand(127, 255), - mt_rand(127, 255), - mt_rand(127, 255), - ), - ); - } else { - imagefilledrectangle( - $im, - mt_rand( - ($j * $CAPTCHAWIDTH) / 5, - (($j + 1) * $CAPTCHAWIDTH) / 5, - ), - mt_rand(0, $CAPTCHAHEIGHT), - mt_rand( - ($j * $CAPTCHAWIDTH) / 5, - (($j + 1) * $CAPTCHAWIDTH) / 5, - ), - mt_rand(0, $CAPTCHAHEIGHT), - imagecolorallocate( - $im, - mt_rand(127, 255), - mt_rand(127, 255), - mt_rand(127, 255), - ), - ); - } - } - } - } - } - echo '<img alt="CAPTCHA" width="' . - $CAPTCHAWIDTH . - '" height="' . - $CAPTCHAHEIGHT . - '" src="data:image/png;base64,'; - ob_start(); - imagepng($im); - imagedestroy($im); - echo base64_encode(ob_get_clean()) . '">'; - echo "</td></tr><tr><td>" . - hidden("challenge", $randid) . - '<input type="text" name="captcha" size="15" autocomplete="off" required></td></tr>'; -} - -function send_setup(array $C): void -{ - global $U; - print_start("setup"); - echo "<h2>" . _("Chat Setup") . "</h2>" . form("setup", "save"); - echo '<table id="guestaccess">'; - thr(); - $ga = (int) get_setting("guestaccess"); - echo "<tr><td><table><tr><th>" . _("Change Guestaccess") . "</th><td>"; - echo '<select name="guestaccess">'; - echo '<option value="1"'; - if ($ga === 1) { - echo " selected"; - } - echo ">" . _("Allow") . "</option>"; - echo '<option value="2"'; - if ($ga === 2) { - echo " selected"; - } - echo ">" . _("Allow with waitingroom") . "</option>"; - echo '<option value="3"'; - if ($ga === 3) { - echo " selected"; - } - echo ">" . _("Require moderator approval") . "</option>"; - echo '<option value="0"'; - if ($ga === 0) { - echo " selected"; - } - echo ">" . _("Only members") . "</option>"; - echo '<option value="4"'; - if ($ga === 4) { - echo " selected"; - } - echo ">" . _("Disable chat") . "</option>"; - echo "</select></td></tr></table></td></tr>"; - thr(); - $englobal = (int) get_setting("englobalpass"); - echo '<tr><td><table id="globalpass"><tr><th>' . - _("Global Password:") . - "</th><td>"; - echo "<table>"; - echo '<tr><td><select name="englobalpass">'; - echo '<option value="0"'; - if ($englobal === 0) { - echo " selected"; - } - echo ">" . _("Disabled") . "</option>"; - echo '<option value="1"'; - if ($englobal === 1) { - echo " selected"; - } - echo ">" . _("Enabled") . "</option>"; - echo '<option value="2"'; - if ($englobal === 2) { - echo " selected"; - } - echo ">" . _("Only for guests") . "</option>"; - echo "</select></td><td>&nbsp;</td>"; - echo '<td><input type="text" name="globalpass" value="' . - htmlspecialchars(get_setting("globalpass")) . - '"></td></tr>'; - echo "</table></td></tr></table></td></tr>"; - thr(); - $ga = (int) get_setting("guestreg"); - echo '<tr><td><table id="guestreg"><tr><th>' . - _("Let guests register themselves") . - "</th><td>"; - echo '<select name="guestreg">'; - echo '<option value="0"'; - if ($ga === 0) { - echo " selected"; - } - echo ">" . _("Disabled") . "</option>"; - echo '<option value="1"'; - if ($ga === 1) { - echo " selected"; - } - echo ">" . _("As applicant") . "</option>"; - echo '<option value="2"'; - if ($ga === 2) { - echo " selected"; - } - echo ">" . _("As member") . "</option>"; - echo "</select></td></tr></table></td></tr>"; - thr(); - echo '<tr><td><table id="sysmessages"><tr><th>' . - _("System messages") . - "</th><td>"; - echo "<table>"; - foreach ($C["msg_settings"] as $setting => $title) { - echo "<tr><td>&nbsp;$title</td><td>&nbsp;<input type=\"text\" name=\"$setting\" value=\"" . - get_setting($setting) . - '"></td></tr>'; - } - echo "</table></td></tr></table></td></tr>"; - foreach ($C["text_settings"] as $setting => $title) { - thr(); - echo "<tr><td><table id=\"$setting\"><tr><th>" . $title . "</th><td>"; - echo "<input type=\"text\" name=\"$setting\" value=\"" . - htmlspecialchars(get_setting($setting)) . - '">'; - echo "</td></tr></table></td></tr>"; - } - foreach ($C["colour_settings"] as $setting => $title) { - thr(); - echo "<tr><td><table id=\"$setting\"><tr><th>" . $title . "</th><td>"; - echo "<input type=\"color\" name=\"$setting\" value=\"#" . - htmlspecialchars(get_setting($setting)) . - '">'; - echo "</td></tr></table></td></tr>"; - } - thr(); - echo '<tr><td><table id="captcha"><tr><th>' . _("Captcha") . "</th><td>"; - echo "<table>"; - if (!extension_loaded("gd")) { - echo "<tr><td>" . - sprintf( - _( - "The %s extension of PHP is required for this feature. Please install it first.", - ), - "gd", - ) . - "</td></tr>"; - } else { - echo '<tr><td><select name="dismemcaptcha">'; - $dismemcaptcha = (bool) get_setting("dismemcaptcha"); - echo '<option value="0"'; - if (!$dismemcaptcha) { - echo " selected"; - } - echo ">" . _("Enabled") . "</option>"; - echo '<option value="1"'; - if ($dismemcaptcha) { - echo " selected"; - } - echo ">" . _("Only for guests") . "</option>"; - echo '</select></td><td><select name="captcha">'; - $captcha = (int) get_setting("captcha"); - echo '<option value="0"'; - if ($captcha === 0) { - echo " selected"; - } - echo ">" . _("Disabled") . "</option>"; - echo '<option value="1"'; - if ($captcha === 1) { - echo " selected"; - } - echo ">" . _("Simple") . "</option>"; - echo '<option value="2"'; - if ($captcha === 2) { - echo " selected"; - } - echo ">" . _("Moderate") . "</option>"; - echo '<option value="3"'; - if ($captcha === 3) { - echo " selected"; - } - echo ">" . _("Hard") . "</option>"; - echo '<option value="4"'; - if ($captcha === 4) { - echo " selected"; - } - echo ">" . _("Extreme") . "</option>"; - echo '<option value="5"'; - if ($captcha === 5) { - echo " selected"; - } - echo ">" . _("TrueType with solid shapes") . "</option>"; - echo '<option value="6"'; - if ($captcha === 6) { - echo " selected"; - } - echo ">" . _("TrueType with hollow shapes") . "</option>"; - echo '<option value="7"'; - if ($captcha === 7) { - echo " selected"; - } - echo ">" . _("TrueType with spurious characters") . "</option>"; - echo '<option value="8"'; - if ($captcha === 8) { - echo " selected"; - } - echo ">" . - _("TrueType with spurious characters, monochrome") . - "</option>"; - echo '<option value="9"'; - if ($captcha === 9) { - echo " selected"; - } - echo ">" . _("TrueType on plaid background") . "</option>"; - echo '<option value="10"'; - if ($captcha === 10) { - echo " selected"; - } - echo ">" . - _("TrueType on plaid background with spurious characters") . - "</option>"; - echo "</select></td></tr>"; - } - echo "</table></td></tr></table></td></tr>"; - thr(); - echo '<tr><td><table id="defaulttz"><tr><th>' . - _("Default time zone") . - "</th><td>"; - echo '<select name="defaulttz">'; - $tzs = timezone_identifiers_list(); - $defaulttz = get_setting("defaulttz"); - foreach ($tzs as $tz) { - echo "<option value=\"$tz\""; - if ($defaulttz == $tz) { - echo " selected"; - } - echo ">$tz</option>"; - } - echo "</select>"; - echo "</td></tr></table></td></tr>"; - foreach ($C["textarea_settings"] as $setting => $title) { - thr(); - echo "<tr><td><table id=\"$setting\"><tr><th>" . $title . "</th><td>"; - echo "<textarea name=\"$setting\" rows=\"4\" cols=\"60\">" . - htmlspecialchars(get_setting($setting)) . - "</textarea>"; - echo "</td></tr></table></td></tr>"; - } - foreach ($C["number_settings"] as $setting => $title) { - thr(); - echo "<tr><td><table id=\"$setting\"><tr><th>" . $title . "</th><td>"; - echo "<input type=\"number\" name=\"$setting\" value=\"" . - htmlspecialchars(get_setting($setting)) . - '">'; - echo "</td></tr></table></td></tr>"; - } - foreach ($C["bool_settings"] as $setting => $title) { - thr(); - echo "<tr><td><table id=\"$setting\"><tr><th>" . $title . "</th><td>"; - echo "<select name=\"$setting\">"; - $value = (bool) get_setting($setting); - echo '<option value="0"'; - if (!$value) { - echo " selected"; - } - echo ">" . _("Disabled") . "</option>"; - echo '<option value="1"'; - if ($value) { - echo " selected"; - } - echo ">" . _("Enabled") . "</option>"; - echo "</select></td></tr>"; - echo "</table></td></tr>"; - } - thr(); - echo "<tr><td>" . submit(_("Apply")) . "</td></tr></table></form><br>"; - if ($U["status"] == 8) { - echo '<table id="actions"><tr><td>'; - echo form("setup", "backup"); - echo submit(_("Backup and restore")) . "</form></td><td>"; - echo form("setup", "destroy"); - echo submit(_("Destroy chat"), 'class="delbutton"') . - "</form></td></tr></table><br>"; - } - echo form_target("_parent", "logout"); - echo submit(_("Logout"), 'id="exitbutton"') . "</form>" . credit(); - print_end(); -} - -function restore_backup(array $C): void -{ - global $db, $memcached; - if (!extension_loaded("json")) { - return; - } - $code = json_decode($_POST["restore"], true); - if (isset($_POST["settings"])) { - foreach ($C["settings"] as $setting) { - if (isset($code["settings"][$setting])) { - update_setting($setting, $code["settings"][$setting]); - } - } - } - if ( - isset($_POST["filter"]) && - (isset($code["filters"]) || isset($code["linkfilters"])) - ) { - $db->exec("DELETE FROM " . PREFIX . "filter;"); - $db->exec("DELETE FROM " . PREFIX . "linkfilter;"); - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "filter (filtermatch, filterreplace, allowinpm, regex, kick, cs) VALUES (?, ?, ?, ?, ?, ?);", - ); - foreach ($code["filters"] as $filter) { - if (!isset($filter["cs"])) { - $filter["cs"] = 0; - } - $stmt->execute([ - $filter["match"], - $filter["replace"], - $filter["allowinpm"], - $filter["regex"], - $filter["kick"], - $filter["cs"], - ]); - } - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "linkfilter (filtermatch, filterreplace, regex) VALUES (?, ?, ?);", - ); - foreach ($code["linkfilters"] as $filter) { - $stmt->execute([ - $filter["match"], - $filter["replace"], - $filter["regex"], - ]); - } - if (MEMCACHED) { - $memcached->delete(DBNAME . "-" . PREFIX . "filter"); - $memcached->delete(DBNAME . "-" . PREFIX . "linkfilter"); - } - } - if (isset($_POST["members"]) && isset($code["members"])) { - $db->exec("DELETE FROM " . PREFIX . "inbox;"); - $db->exec("DELETE FROM " . PREFIX . "members;"); - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "members (nickname, passhash, status, refresh, bgcolour, regedby, lastlogin, loginfails, timestamps, embed, incognito, style, nocache, tz, eninbox, sortupdown, hidechatters, nocache_old) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", - ); - foreach ($code["members"] as $member) { - $new_settings = [ - "nocache", - "tz", - "eninbox", - "sortupdown", - "hidechatters", - "nocache_old", - ]; - foreach ($new_settings as $setting) { - if (!isset($member[$setting])) { - $member[$setting] = 0; - } - } - $stmt->execute([ - $member["nickname"], - $member["passhash"], - $member["status"], - $member["refresh"], - $member["bgcolour"], - $member["regedby"], - $member["lastlogin"], - $member["loginfails"], - $member["timestamps"], - $member["embed"], - $member["incognito"], - $member["style"], - $member["nocache"], - $member["tz"], - $member["eninbox"], - $member["sortupdown"], - $member["hidechatters"], - $member["nocache_old"], - ]); - } - } - if (isset($_POST["notes"]) && isset($code["notes"])) { - $db->exec("DELETE FROM " . PREFIX . "notes;"); - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "notes (type, lastedited, editedby, text) VALUES (?, ?, ?, ?);", - ); - foreach ($code["notes"] as $note) { - if ($note["type"] === "admin") { - $note["type"] = 0; - } elseif ($note["type"] === "staff") { - $note["type"] = 1; - } elseif ($note["type"] === "public") { - $note["type"] = 3; - } - if (MSGENCRYPTED) { - try { - $note["text"] = base64_encode( - sodium_crypto_aead_aes256gcm_encrypt( - $note["text"], - "", - AES_IV, - ENCRYPTKEY, - ), - ); - } catch (SodiumException $e) { - send_error($e->getMessage()); - } - } - $stmt->execute([ - $note["type"], - $note["lastedited"], - $note["editedby"], - $note["text"], - ]); - } - } -} - -function send_backup(array $C): void -{ - global $db; - $code = []; - if ($_POST["do"] === "backup") { - if (isset($_POST["settings"])) { - foreach ($C["settings"] as $setting) { - $code["settings"][$setting] = get_setting($setting); - } - } - if (isset($_POST["filter"])) { - $result = $db->query("SELECT * FROM " . PREFIX . "filter;"); - while ($filter = $result->fetch(PDO::FETCH_ASSOC)) { - $code["filters"][] = [ - "match" => $filter["filtermatch"], - "replace" => $filter["filterreplace"], - "allowinpm" => $filter["allowinpm"], - "regex" => $filter["regex"], - "kick" => $filter["kick"], - "cs" => $filter["cs"], - ]; - } - $result = $db->query("SELECT * FROM " . PREFIX . "linkfilter;"); - while ($filter = $result->fetch(PDO::FETCH_ASSOC)) { - $code["linkfilters"][] = [ - "match" => $filter["filtermatch"], - "replace" => $filter["filterreplace"], - "regex" => $filter["regex"], - ]; - } - } - if (isset($_POST["members"])) { - $result = $db->query("SELECT * FROM " . PREFIX . "members;"); - while ($member = $result->fetch(PDO::FETCH_ASSOC)) { - $code["members"][] = $member; - } - } - if (isset($_POST["notes"])) { - $result = $db->query("SELECT * FROM " . PREFIX . "notes;"); - while ($note = $result->fetch(PDO::FETCH_ASSOC)) { - if (MSGENCRYPTED) { - try { - $note["text"] = sodium_crypto_aead_aes256gcm_decrypt( - base64_decode($note["text"]), - null, - AES_IV, - ENCRYPTKEY, - ); - } catch (SodiumException $e) { - send_error($e->getMessage()); - } - } - $code["notes"][] = $note; - } - } - } - if (isset($_POST["settings"])) { - $chksettings = " checked"; - } else { - $chksettings = ""; - } - if (isset($_POST["filter"])) { - $chkfilters = " checked"; - } else { - $chkfilters = ""; - } - if (isset($_POST["members"])) { - $chkmembers = " checked"; - } else { - $chkmembers = ""; - } - if (isset($_POST["notes"])) { - $chknotes = " checked"; - } else { - $chknotes = ""; - } - print_start("backup"); - echo "<h2>" . _("Backup and restore") . "</h2><table>"; - thr(); - if (!extension_loaded("json")) { - echo "<tr><td>" . - sprintf( - _( - "The %s extension of PHP is required for this feature. Please install it first.", - ), - "json", - ) . - "</td></tr>"; - } else { - echo "<tr><td>" . form("setup", "backup"); - echo '<table id="backup"><tr><td id="backupcheck">'; - echo '<label><input type="checkbox" name="settings" id="backupsettings" value="1"' . - $chksettings . - ">" . - _("Settings") . - "</label>"; - echo '<label><input type="checkbox" name="filter" id="backupfilter" value="1"' . - $chkfilters . - ">" . - _("Filter") . - "</label>"; - echo '<label><input type="checkbox" name="members" id="backupmembers" value="1"' . - $chkmembers . - ">" . - _("Members") . - "</label>"; - echo '<label><input type="checkbox" name="notes" id="backupnotes" value="1"' . - $chknotes . - ">" . - _("Notes") . - "</label>"; - echo '</td><td id="backupsubmit">' . - submit(_("Backup")) . - "</td></tr></table></form></td></tr>"; - thr(); - echo "<tr><td>" . form("setup", "restore"); - echo '<table id="restore">'; - echo '<tr><td colspan="2"><textarea name="restore" rows="4" cols="60">' . - htmlspecialchars(json_encode($code)) . - "</textarea></td></tr>"; - echo '<tr><td id=\"restorecheck\"><label><input type="checkbox" name="settings" id="restoresettings" value="1"' . - $chksettings . - ">" . - _("Settings") . - "</label>"; - echo '<label><input type="checkbox" name="filter" id="restorefilter" value="1"' . - $chkfilters . - ">" . - _("Filter") . - "</label>"; - echo '<label><input type="checkbox" name="members" id="restoremembers" value="1"' . - $chkmembers . - ">" . - _("Members") . - "</label>"; - echo '<label><input type="checkbox" name="notes" id="restorenotes" value="1"' . - $chknotes . - ">" . - _("Notes") . - "</label>"; - echo '</td><td id="restoresubmit">' . - submit(_("Restore")) . - "</td></tr></table>"; - echo "</form></td></tr>"; - } - thr(); - echo "<tr><td>" . - form("setup") . - submit(_("Go to the Setup-Page"), 'class="backbutton"') . - "</form></tr></td>"; - echo "</table>"; - print_end(); -} - -function send_destroy_chat(): void -{ - print_start("destroy_chat"); - echo '<table><tr><td colspan="2">' . - _("Are you sure?") . - "</td></tr><tr><td>"; - echo form_target("_parent", "setup", "destroy") . - hidden("confirm", "yes") . - submit(_("Yes"), 'class="delbutton"') . - "</form></td><td>"; - echo form("setup") . - submit(_("No"), 'class="backbutton"') . - "</form></td><tr></table>"; - print_end(); -} - -function send_delete_account(): void -{ - print_start("delete_account"); - echo '<table><tr><td colspan="2">' . - _("Are you sure?") . - "</td></tr><tr><td>"; - echo form("profile", "delete") . - hidden("confirm", "yes") . - submit(_("Yes"), 'class="delbutton"') . - "</form></td><td>"; - echo form("profile") . - submit(_("No"), 'class="backbutton"') . - "</form></td><tr></table>"; - print_end(); -} - -function send_init(): void -{ - print_start("init"); - echo "<h2>" . _("Initial Setup") . "</h2>"; - echo form("init") . - "<table><tr><td><h3>" . - _("Superadmin Login") . - "</h3><table>"; - echo "<tr><td>" . - _("Superadmin Nickname:") . - '</td><td><input type="text" name="sunick" size="15" autocomplete="username"></td></tr>'; - echo "<tr><td>" . - _("Superadmin Password:") . - '</td><td><input type="password" name="supass" size="15" autocomplete="new-password"></td></tr>'; - echo "<tr><td>" . - _("Confirm Password:") . - '</td><td><input type="password" name="supassc" size="15" autocomplete="new-password"></td></tr>'; - echo "</table></td></tr><tr><td><br>" . - submit(_("Initialise Chat")) . - "</td></tr></table></form>"; - echo '<p id="changelang">' . _("Change language:"); - foreach (LANGUAGES as $lang => $data) { - echo " <a href=\"$_SERVER[SCRIPT_NAME]?action=setup&amp;lang=$lang\">$data[name]</a>"; - } - echo "</p>" . credit(); - print_end(); -} - -function send_update(string $msg): void -{ - print_start("update"); - echo "<h2>" . - _("Database successfully updated!") . - "</h2><br>" . - form("setup") . - submit(_("Go to the Setup-Page")) . - "</form>$msg<br>" . - credit(); - print_end(); -} - -function send_alogin(): void -{ - print_start("alogin"); - echo form("setup") . "<table>"; - echo "<tr><td>" . - _("Nickname:") . - '</td><td><input type="text" name="nick" size="15" autocomplete="username" autofocus></td></tr>'; - echo "<tr><td>" . - _("Password:") . - '</td><td><input type="password" name="pass" size="15" autocomplete="current-password"></td></tr>'; - send_captcha(); - echo '<tr><td colspan="2">' . - submit(_("Login")) . - "</td></tr></table></form>"; - echo '<br><a href="?action=sa_password_reset">' . - _("Forgot login?") . - "</a><br>"; - echo '<p id="changelang">' . _("Change language:"); - foreach (LANGUAGES as $lang => $data) { - echo " <a href=\"?action=setup&amp;lang=$lang\" hreflang=\"$lang\">$data[name]</a>"; - } - echo "</p>" . credit(); - print_end(); -} - -function send_sa_password_reset(): void -{ - global $db; - print_start("sa_password_reset"); - echo "<h1>" . _("Reset password") . "</h1>"; - if ( - defined("RESET_SUPERADMIN_PASSWORD") && - !empty(RESET_SUPERADMIN_PASSWORD) - ) { - $stmt = $db->query( - "SELECT nickname FROM " . - PREFIX . - "members WHERE status = 8 LIMIT 1;", - ); - if ($user = $stmt->fetch(PDO::FETCH_ASSOC)) { - $mem_update = $db->prepare( - "UPDATE " . - PREFIX . - "members SET passhash = ? WHERE nickname = ? LIMIT 1;", - ); - $mem_update->execute([ - password_hash(RESET_SUPERADMIN_PASSWORD, PASSWORD_DEFAULT), - $user["nickname"], - ]); - $sess_delete = $db->prepare( - "DELETE FROM " . PREFIX . "sessions WHERE nickname = ?;", - ); - $sess_delete->execute([$user["nickname"]]); - printf( - "<p>" . - _( - "Successfully reset password for username %s. Please remove the password reset define from the script again.", - ) . - "</p>", - $user["nickname"], - ); - } - } else { - echo "<p>" . - _( - "Please modify the script and put the following at the bottom of it (change the password). Then refresh this page: define('RESET_SUPERADMIN_PASSWORD', 'changeme');", - ) . - "</p>"; - } - echo '<a href="?action=setup">' . _("Go to the Setup-Page") . "</a>"; - echo '<p id="changelang">' . _("Change language:"); - foreach (LANGUAGES as $lang => $data) { - echo " <a href=\"?action=sa_password_reset&amp;lang=$lang\" hreflang=\"$lang\">$data[name]</a>"; - } - echo "</p>" . credit(); - print_end(); -} - -function send_admin(string $arg): void -{ - global $U, $db; - $ga = (int) get_setting("guestaccess"); - print_start("admin"); - $chlist = - '<select name="name[]" size="5" multiple><option value="">' . - _("(choose)") . - "</option>"; - $chlist .= '<option value="s &#42;">' . _("All guests") . "</option>"; - $users = []; - $stmt = $db->query( - "SELECT nickname, style, status FROM " . - PREFIX . - "sessions WHERE entry!=0 AND status>0 ORDER BY LOWER(nickname);", - ); - while ($user = $stmt->fetch(PDO::FETCH_NUM)) { - $users[] = [htmlspecialchars($user[0]), $user[1], $user[2]]; - } - foreach ($users as $user) { - if ($user[2] < $U["status"]) { - $chlist .= "<option value=\"$user[0]\" style=\"$user[1]\">$user[0]</option>"; - } - } - $chlist .= "</select>"; - echo "<h2>" . _("Administrative functions") . "</h2><i>$arg</i><table>"; - if ($U["status"] >= 7) { - thr(); - echo "<tr><td>" . - form_target("view", "setup") . - submit(_("Go to the Setup-Page")) . - "</form></td></tr>"; - } - thr(); - echo '<tr><td><table id="clean"><tr><th>' . - _("Clean messages") . - "</th><td>"; - echo form("admin", "clean"); - echo '<table><tr><td><label><input type="radio" name="what" id="room" value="room">'; - echo _("Whole room") . - '</label></td><td>&nbsp;</td><td><label><input type="radio" name="what" id="choose" value="choose" checked>'; - echo _("Selection") . - '</label></td><td>&nbsp;</td></tr><tr><td colspan="3"><label><input type="radio" name="what" id="nick" value="nick">'; - echo _("Following nickname:") . - '</label> <select name="nickname" size="1"><option value="">' . - _("(choose)") . - "</option>"; - $stmt = $db->prepare( - "SELECT DISTINCT poster FROM " . - PREFIX . - "messages WHERE delstatus<? AND poster!='';", - ); - $stmt->execute([$U["status"]]); - while ($nick = $stmt->fetch(PDO::FETCH_NUM)) { - echo '<option value="' . - htmlspecialchars($nick[0]) . - '">' . - htmlspecialchars($nick[0]) . - "</option>"; - } - echo "</select></td><td>"; - echo submit(_("Clean"), 'class="delbutton"') . - "</td></tr></table></form></td></tr></table></td></tr>"; - thr(); - echo '<tr><td><table id="kick"><tr><th>' . - sprintf(_("Kick Chatter (%d minutes)"), get_setting("kickpenalty")) . - "</th></tr><tr><td>"; - echo form("admin", "kick"); - echo "<table><tr><td>" . - _("Kickmessage:") . - '</td><td><input type="text" name="kickmessage" size="30"></td><td>&nbsp;</td></tr>'; - echo '<tr><td><label><input type="checkbox" name="what" value="purge" id="purge">' . - _("Purge messages") . - "</label></td><td>" . - $chlist . - "</td><td>"; - echo submit(_("Kick")) . - "</td></tr></table></form></td></tr></table></td></tr>"; - thr(); - echo '<tr><td><table id="logout"><tr><th>' . - _("Logout inactive Chatter") . - "</th><td>"; - echo form("admin", "logout"); - echo "<table><tr><td>$chlist</td><td>"; - echo submit(_("Logout")) . - "</td></tr></table></form></td></tr></table></td></tr>"; - $views = [ - "sessions" => _("View active sessions"), - "filter" => _("Filter"), - "linkfilter" => _("Linkfilter"), - ]; - foreach ($views as $view => $title) { - thr(); - echo "<tr><td><table id=\"$view\"><tr><th>" . $title . "</th><td>"; - echo form("admin", $view); - echo submit(_("View")) . "</form></td></tr></table></td></tr>"; - } - thr(); - echo '<tr><td><table id="topic"><tr><th>' . _("Topic") . "</th><td>"; - echo form("admin", "topic"); - echo '<table><tr><td><input type="text" name="topic" size="20" value="' . - get_setting("topic") . - '"></td><td>'; - echo submit(_("Change")) . - "</td></tr></table></form></td></tr></table></td></tr>"; - thr(); - echo '<tr><td><table id="guestaccess"><tr><th>' . - _("Change Guestaccess") . - "</th><td>"; - echo form("admin", "guestaccess"); - echo "<table>"; - echo '<tr><td><select name="guestaccess">'; - echo '<option value="1"'; - if ($ga === 1) { - echo " selected"; - } - echo ">" . _("Allow") . "</option>"; - echo '<option value="2"'; - if ($ga === 2) { - echo " selected"; - } - echo ">" . _("Allow with waitingroom") . "</option>"; - echo '<option value="3"'; - if ($ga === 3) { - echo " selected"; - } - echo ">" . _("Require moderator approval") . "</option>"; - echo '<option value="0"'; - if ($ga === 0) { - echo " selected"; - } - echo ">" . _("Only members") . "</option>"; - if ($ga === 4) { - echo '<option value="4" selected'; - echo ">" . _("Disable chat") . "</option>"; - } - echo "</select></td><td>" . - submit(_("Change")) . - "</td></tr></table></form></td></tr></table></td></tr>"; - thr(); - if (get_setting("suguests")) { - echo '<tr><td><table id="suguests"><tr><th>' . - _("Register applicant") . - "</th><td>"; - echo form("admin", "superguest"); - echo '<table><tr><td><select name="name" size="1"><option value="">' . - _("(choose)") . - "</option>"; - foreach ($users as $user) { - if ($user[2] == 1) { - echo "<option value=\"$user[0]\" style=\"$user[1]\">$user[0]</option>"; - } - } - echo "</select></td><td>" . - submit(_("Register")) . - "</td></tr></table></form></td></tr></table></td></tr>"; - thr(); - } - if ($U["status"] >= 7) { - echo '<tr><td><table id="status"><tr><th>' . _("Members") . "</th><td>"; - echo form("admin", "status"); - echo '<table><tr><td><select name="name" size="1"><option value="">' . - _("(choose)") . - "</option>"; - $members = []; - $result = $db->query( - "SELECT nickname, style, status FROM " . - PREFIX . - "members ORDER BY LOWER(nickname);", - ); - while ($temp = $result->fetch(PDO::FETCH_NUM)) { - $members[] = [htmlspecialchars($temp[0]), $temp[1], $temp[2]]; - } - foreach ($members as $member) { - echo "<option value=\"$member[0]\" style=\"$member[1]\">$member[0]"; - if ($member[2] == 0) { - echo " (!)"; - } elseif ($member[2] == 2) { - echo " (SG)"; - } elseif ($member[2] == 3) { - } elseif ($member[2] == 5) { - echo " (M)"; - } elseif ($member[2] == 6) { - echo " (SM)"; - } elseif ($member[2] == 7) { - echo " (A)"; - } else { - echo " (SA)"; - } - echo "</option>"; - } - echo '</select><select name="set" size="1"><option value="">' . - _("(choose)") . - '</option><option value="-">' . - _("Delete from database") . - '</option><option value="0">' . - _("Deny access (!)") . - "</option>"; - if (get_setting("suguests")) { - echo '<option value="2">' . - _("Set to applicant (SG)") . - "</option>"; - } - echo '<option value="3">' . _("Set to regular member") . "</option>"; - echo '<option value="5">' . _("Set to moderator (M)") . "</option>"; - echo '<option value="6">' . _("Set to supermod (SM)") . "</option>"; - if ($U["status"] >= 8) { - echo '<option value="7">' . _("Set to admin (A)") . "</option>"; - } - echo "</select></td><td>" . - submit(_("Change")) . - "</td></tr></table></form></td></tr></table></td></tr>"; - thr(); - echo '<tr><td><table id="passreset"><tr><th>' . - _("Reset password") . - "</th><td>"; - echo form("admin", "passreset"); - echo '<table><tr><td><select name="name" size="1"><option value="">' . - _("(choose)") . - "</option>"; - foreach ($members as $member) { - echo "<option value=\"$member[0]\" style=\"$member[1]\">$member[0]</option>"; - } - echo '</select></td><td><input type="password" name="pass" autocomplete="off"></td><td>' . - submit(_("Change")) . - "</td></tr></table></form></td></tr></table></td></tr>"; - thr(); - echo '<tr><td><table id="register"><tr><th>' . - _("Register Guest") . - "</th><td>"; - echo form("admin", "register"); - echo '<table><tr><td><select name="name" size="1"><option value="">' . - _("(choose)") . - "</option>"; - foreach ($users as $user) { - if ($user[2] == 1) { - echo "<option value=\"$user[0]\" style=\"$user[1]\">$user[0]</option>"; - } - } - echo "</select></td><td>" . - submit(_("Register")) . - "</td></tr></table></form></td></tr></table></td></tr>"; - thr(); - echo '<tr><td><table id="regnew"><tr><th>' . - _("Register new Member") . - "</th></tr><tr><td>"; - echo form("admin", "regnew"); - echo "<table><tr><td>" . - _("Nickname:") . - '</td><td>&nbsp;</td><td><input type="text" name="name" size="20"></td><td>&nbsp;</td></tr>'; - echo "<tr><td>" . - _("Password:") . - '</td><td>&nbsp;</td><td><input type="password" name="pass" size="20" autocomplete="off"></td><td>'; - echo submit(_("Register")) . - "</td></tr></table></form></td></tr></table></td></tr>"; - thr(); - } - echo "</table><br>"; - echo form("admin") . submit(_("Reload")) . "</form>"; - print_end(); -} - -function send_sessions(): void -{ - global $U, $db; - $stmt = $db->prepare( - "SELECT nickname, style, lastpost, status, useragent, ip FROM " . - PREFIX . - "sessions WHERE entry!=0 AND (incognito=0 OR status<? OR nickname=?) ORDER BY status DESC, lastpost DESC;", - ); - $stmt->execute([$U["status"], $U["nickname"]]); - if (!($lines = $stmt->fetchAll(PDO::FETCH_ASSOC))) { - $lines = []; - } - print_start("sessions"); - echo "<h1>" . _("Active Sessions") . "</h1><table>"; - echo "<tr><th>" . - _("Nickname") . - "</th><th>" . - _("Timeout in") . - "</th><th>" . - _("User-Agent") . - "</th>"; - $trackip = (bool) get_setting("trackip"); - $memexpire = (int) get_setting("memberexpire"); - $guestexpire = (int) get_setting("guestexpire"); - if ($trackip) { - echo "<th>" . _("IP-Address") . "</th>"; - } - echo "<th>" . _("Actions") . "</th></tr>"; - foreach ($lines as $temp) { - if ($temp["status"] == 0) { - $s = " (K)"; - } elseif ($temp["status"] <= 1) { - $s = " (G)"; - } elseif ($temp["status"] == 2) { - $s = " (SG)"; - } elseif ($temp["status"] == 3) { - $s = ""; - } elseif ($temp["status"] == 5) { - $s = " (M)"; - } elseif ($temp["status"] == 6) { - $s = " (SM)"; - } elseif ($temp["status"] == 7) { - $s = " (A)"; - } else { - $s = " (SA)"; - } - echo '<tr><td class="nickname">' . - style_this( - htmlspecialchars($temp["nickname"]) . $s, - $temp["style"], - ) . - '</td><td class="timeout">'; - if ($temp["status"] > 2) { - get_timeout((int) $temp["lastpost"], $memexpire); - } else { - get_timeout((int) $temp["lastpost"], $guestexpire); - } - echo "</td>"; - if ( - $U["status"] > $temp["status"] || - $U["nickname"] === $temp["nickname"] - ) { - echo "<td class=\"ua\">$temp[useragent]</td>"; - if ($trackip) { - echo "<td class=\"ip\">$temp[ip]</td>"; - } - echo '<td class="action">'; - if ($temp["nickname"] !== $U["nickname"]) { - echo "<table><tr>"; - if ($temp["status"] != 0) { - echo "<td>"; - echo form("admin", "sessions"); - echo hidden("kick", "1") . - hidden("nick", htmlspecialchars($temp["nickname"])) . - submit(_("Kick")) . - "</form>"; - echo "</td>"; - } - echo "<td>"; - echo form("admin", "sessions"); - echo hidden("logout", "1") . - hidden("nick", htmlspecialchars($temp["nickname"])) . - submit($temp["status"] == 0 ? _("Unban") : _("Logout")) . - "</form>"; - echo "</td></tr></table>"; - } else { - echo "-"; - } - echo "</td></tr>"; - } else { - echo '<td class="ua">-</td>'; - if ($trackip) { - echo '<td class="ip">-</td>'; - } - echo '<td class="action">-</td></tr>'; - } - } - echo "</table><br>"; - echo form("admin", "sessions") . submit(_("Reload")) . "</form>"; - print_end(); -} - -function check_filter_match(int &$reg): string -{ - $_POST["match"] = htmlspecialchars($_POST["match"]); - if (isset($_POST["regex"]) && $_POST["regex"] == 1) { - if (!valid_regex($_POST["match"])) { - return _("Incorrect regular expression!") . - "<br>" . - sprintf( - _("Your match was as follows: %s"), - htmlspecialchars($_POST["match"]), - ); - } - $reg = 1; - } else { - $_POST["match"] = preg_replace( - "/([^\w\d])/u", - "\\\\$1", - $_POST["match"], - ); - $reg = 0; - } - if (mb_strlen($_POST["match"]) > 255) { - return _( - "Your match was too long. You can use max. 255 characters. Try splitting it up.", - ) . - "<br>" . - sprintf( - _("Your match was as follows: %s"), - htmlspecialchars($_POST["match"]), - ); - } - return ""; -} - -function manage_filter(): string -{ - global $db, $memcached; - if (isset($_POST["id"])) { - $reg = 0; - if (($tmp = check_filter_match($reg)) !== "") { - return $tmp; - } - if (isset($_POST["allowinpm"]) && $_POST["allowinpm"] == 1) { - $pm = 1; - } else { - $pm = 0; - } - if (isset($_POST["kick"]) && $_POST["kick"] == 1) { - $kick = 1; - } else { - $kick = 0; - } - if (isset($_POST["cs"]) && $_POST["cs"] == 1) { - $cs = 1; - } else { - $cs = 0; - } - if (preg_match('/^[0-9]+$/', $_POST["id"])) { - if (empty($_POST["match"])) { - $stmt = $db->prepare( - "DELETE FROM " . PREFIX . "filter WHERE id=?;", - ); - $stmt->execute([$_POST["id"]]); - } else { - $stmt = $db->prepare( - "UPDATE " . - PREFIX . - "filter SET filtermatch=?, filterreplace=?, allowinpm=?, regex=?, kick=?, cs=? WHERE id=?;", - ); - $stmt->execute([ - $_POST["match"], - $_POST["replace"], - $pm, - $reg, - $kick, - $cs, - $_POST["id"], - ]); - } - } elseif ($_POST["id"] === "+") { - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "filter (filtermatch, filterreplace, allowinpm, regex, kick, cs) VALUES (?, ?, ?, ?, ?, ?);", - ); - $stmt->execute([ - $_POST["match"], - $_POST["replace"], - $pm, - $reg, - $kick, - $cs, - ]); - } - if (MEMCACHED) { - $memcached->delete(DBNAME . "-" . PREFIX . "filter"); - } - } - return ""; -} - -function manage_linkfilter(): string -{ - global $db, $memcached; - if (isset($_POST["id"])) { - $reg = 0; - if (($tmp = check_filter_match($reg)) !== "") { - return $tmp; - } - if (preg_match('/^[0-9]+$/', $_POST["id"])) { - if (empty($_POST["match"])) { - $stmt = $db->prepare( - "DELETE FROM " . PREFIX . "linkfilter WHERE id=?;", - ); - $stmt->execute([$_POST["id"]]); - } else { - $stmt = $db->prepare( - "UPDATE " . - PREFIX . - "linkfilter SET filtermatch=?, filterreplace=?, regex=? WHERE id=?;", - ); - $stmt->execute([ - $_POST["match"], - $_POST["replace"], - $reg, - $_POST["id"], - ]); - } - } elseif ($_POST["id"] === "+") { - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "linkfilter (filtermatch, filterreplace, regex) VALUES (?, ?, ?);", - ); - $stmt->execute([$_POST["match"], $_POST["replace"], $reg]); - } - if (MEMCACHED) { - $memcached->delete(DBNAME . "-" . PREFIX . "linkfilter"); - } - } - return ""; -} - -function get_filters(): array -{ - global $db, $memcached; - $filters = []; - if (MEMCACHED) { - $filters = $memcached->get(DBNAME . "-" . PREFIX . "filter"); - } - if (!MEMCACHED || $memcached->getResultCode() !== Memcached::RES_SUCCESS) { - $filters = []; - $result = $db->query( - "SELECT id, filtermatch, filterreplace, allowinpm, regex, kick, cs FROM " . - PREFIX . - "filter;", - ); - while ($filter = $result->fetch(PDO::FETCH_ASSOC)) { - $filters[] = [ - "id" => $filter["id"], - "match" => $filter["filtermatch"], - "replace" => $filter["filterreplace"], - "allowinpm" => $filter["allowinpm"], - "regex" => $filter["regex"], - "kick" => $filter["kick"], - "cs" => $filter["cs"], - ]; - } - if (MEMCACHED) { - $memcached->set(DBNAME . "-" . PREFIX . "filter", $filters); - } - } - return $filters; -} - -function get_linkfilters(): array -{ - global $db, $memcached; - $filters = []; - if (MEMCACHED) { - $filters = $memcached->get(DBNAME . "-" . PREFIX . "linkfilter"); - } - if (!MEMCACHED || $memcached->getResultCode() !== Memcached::RES_SUCCESS) { - $filters = []; - $result = $db->query( - "SELECT id, filtermatch, filterreplace, regex FROM " . - PREFIX . - "linkfilter;", - ); - while ($filter = $result->fetch(PDO::FETCH_ASSOC)) { - $filters[] = [ - "id" => $filter["id"], - "match" => $filter["filtermatch"], - "replace" => $filter["filterreplace"], - "regex" => $filter["regex"], - ]; - } - if (MEMCACHED) { - $memcached->set(DBNAME . "-" . PREFIX . "linkfilter", $filters); - } - } - return $filters; -} - -function send_filter(string $arg = ""): void -{ - global $U; - print_start("filter"); - echo "<h2>" . _("Filter") . "</h2><i>$arg</i><table>"; - thr(); - echo "<tr><th><table><tr>"; - echo "<td>" . _("Filter ID:") . "</td>"; - echo "<td>" . _("Match") . "</td>"; - echo "<td>" . _("Replace") . "</td>"; - echo "<td>" . _("Allow in PM") . "</td>"; - echo "<td>" . _("Regex") . "</td>"; - echo "<td>" . _("Kick") . "</td>"; - echo "<td>" . _("Case sensitive") . "</td>"; - echo "<td>" . _("Apply") . "</td>"; - echo "</tr></table></th></tr>"; - $filters = get_filters(); - foreach ($filters as $filter) { - if ($filter["allowinpm"] == 1) { - $check = " checked"; - } else { - $check = ""; - } - if ($filter["regex"] == 1) { - $checked = " checked"; - } else { - $checked = ""; - $filter["match"] = preg_replace( - "/(\\\\(.))/u", - "$2", - $filter["match"], - ); - } - if ($filter["kick"] == 1) { - $checkedk = " checked"; - } else { - $checkedk = ""; - } - if ($filter["cs"] == 1) { - $checkedcs = " checked"; - } else { - $checkedcs = ""; - } - echo "<tr><td>"; - echo form("admin", "filter") . hidden("id", $filter["id"]); - echo "<table><tr><td>" . _("Filter") . " $filter[id]:</td>"; - echo '<td><input type="text" name="match" value="' . - $filter["match"] . - '" size="20" style="' . - $U["style"] . - '"></td>'; - echo '<td><input type="text" name="replace" value="' . - htmlspecialchars($filter["replace"]) . - '" size="20" style="' . - $U["style"] . - '"></td>'; - echo '<td><label><input type="checkbox" name="allowinpm" value="1"' . - $check . - ">" . - _("Allow in PM") . - "</label></td>"; - echo '<td><label><input type="checkbox" name="regex" value="1"' . - $checked . - ">" . - _("Regex") . - "</label></td>"; - echo '<td><label><input type="checkbox" name="kick" value="1"' . - $checkedk . - ">" . - _("Kick") . - "</label></td>"; - echo '<td><label><input type="checkbox" name="cs" value="1"' . - $checkedcs . - ">" . - _("Case sensitive") . - "</label></td>"; - echo '<td class="filtersubmit">' . - submit(_("Change")) . - "</td></tr></table></form></td></tr>"; - } - echo "<tr><td>"; - echo form("admin", "filter") . hidden("id", "+"); - echo "<table><tr><td>" . _("New filter:") . "</td>"; - echo '<td><input type="text" name="match" value="" size="20" style="' . - $U["style"] . - '"></td>'; - echo '<td><input type="text" name="replace" value="" size="20" style="' . - $U["style"] . - '"></td>'; - echo '<td><label><input type="checkbox" name="allowinpm" id="allowinpm" value="1">' . - _("Allow in PM") . - "</label></td>"; - echo '<td><label><input type="checkbox" name="regex" id="regex" value="1">' . - _("Regex") . - "</label></td>"; - echo '<td><label><input type="checkbox" name="kick" id="kick" value="1">' . - _("Kick") . - "</label></td>"; - echo '<td><label><input type="checkbox" name="cs" id="cs" value="1">' . - _("Case sensitive") . - "</label></td>"; - echo '<td class="filtersubmit">' . - submit(_("Add")) . - "</td></tr></table></form></td></tr>"; - echo "</table><br>"; - echo form("admin", "filter") . submit(_("Reload")) . "</form>"; - print_end(); -} - -function send_linkfilter(string $arg = ""): void -{ - global $U; - print_start("linkfilter"); - echo "<h2>" . _("Linkfilter") . "</h2><i>$arg</i><table>"; - thr(); - echo "<tr><th><table><tr>"; - echo "<td>" . _("Filter ID:") . "</td>"; - echo "<td>" . _("Match") . "</td>"; - echo "<td>" . _("Replace") . "</td>"; - echo "<td>" . _("Regex") . "</td>"; - echo "<td>" . _("Apply") . "</td>"; - echo "</tr></table></th></tr>"; - $filters = get_linkfilters(); - foreach ($filters as $filter) { - if ($filter["regex"] == 1) { - $checked = " checked"; - } else { - $checked = ""; - $filter["match"] = preg_replace( - "/(\\\\(.))/u", - "$2", - $filter["match"], - ); - } - echo "<tr><td>"; - echo form("admin", "linkfilter") . hidden("id", $filter["id"]); - echo "<table><tr><td>" . _("Filter") . " $filter[id]:</td>"; - echo '<td><input type="text" name="match" value="' . - $filter["match"] . - '" size="20" style="' . - $U["style"] . - '"></td>'; - echo '<td><input type="text" name="replace" value="' . - htmlspecialchars($filter["replace"]) . - '" size="20" style="' . - $U["style"] . - '"></td>'; - echo '<td><label><input type="checkbox" name="regex" value="1"' . - $checked . - ">" . - _("Regex") . - "</label></td>"; - echo '<td class="filtersubmit">' . - submit(_("Change")) . - "</td></tr></table></form></td></tr>"; - } - echo "<tr><td>"; - echo form("admin", "linkfilter") . hidden("id", "+"); - echo "<table><tr><td>" . _("New filter:") . "</td>"; - echo '<td><input type="text" name="match" value="" size="20" style="' . - $U["style"] . - '"></td>'; - echo '<td><input type="text" name="replace" value="" size="20" style="' . - $U["style"] . - '"></td>'; - echo '<td><label><input type="checkbox" name="regex" value="1">' . - _("Regex") . - "</label></td>"; - echo '<td class="filtersubmit">' . - submit(_("Add")) . - "</td></tr></table></form></td></tr>"; - echo "</table><br>"; - echo form("admin", "linkfilter") . submit(_("Reload")) . "</form>"; - print_end(); -} - -function send_frameset(): void -{ - global $U, $db, $language, $dir; - prepare_stylesheets("frameset"); - send_headers(); - echo '<!DOCTYPE html><html lang="' . - $language . - '" dir="' . - $dir . - '"><head>' . - meta_html(); - echo "<title>" . get_setting("chatname") . "</title>"; - print_stylesheet("frameset"); - echo "</head><body>"; - if (isset($_POST["sort"])) { - if ($_POST["sort"] == 1) { - $U["sortupdown"] = 1; - } else { - $U["sortupdown"] = 0; - } - $tmp = $U["nocache"]; - $U["nocache"] = $U["nocache_old"]; - $U["nocache_old"] = $tmp; - $stmt = $db->prepare( - "UPDATE " . - PREFIX . - "sessions SET sortupdown=?, nocache=?, nocache_old=? WHERE nickname=?;", - ); - $stmt->execute([ - $U["sortupdown"], - $U["nocache"], - $U["nocache_old"], - $U["nickname"], - ]); - if ($U["status"] > 1) { - $stmt = $db->prepare( - "UPDATE " . - PREFIX . - "members SET sortupdown=?, nocache=?, nocache_old=? WHERE nickname=?;", - ); - $stmt->execute([ - $U["sortupdown"], - $U["nocache"], - $U["nocache_old"], - $U["nickname"], - ]); - } - } - $bottom = ""; - if (get_setting("enablegreeting")) { - $action_mid = "greeting"; - } else { - if ($U["sortupdown"]) { - $bottom = "#bottom"; - } - $action_mid = "view"; - } - if ( - (!isset($_REQUEST["sort"]) && !$U["sortupdown"]) || - (isset($_REQUEST["sort"]) && $_REQUEST["sort"] == 0) - ) { - $action_top = "post"; - $action_bot = "controls"; - $sort_bot = "&sort=1"; - } else { - $action_top = "controls"; - $action_bot = "post"; - $sort_bot = ""; - } - echo "<div id=\"frameset-mid\"><iframe name=\"view\" src=\"$_SERVER[SCRIPT_NAME]?action=$action_mid&session=$U[session]&lang=$language$bottom\">" . - noframe_html() . - "</iframe></div>"; - echo "<div id=\"frameset-top\"><iframe name=\"$action_top\" src=\"$_SERVER[SCRIPT_NAME]?action=$action_top&session=$U[session]&lang=$language\">" . - noframe_html() . - "</iframe></div>"; - echo "<div id=\"frameset-bot\"><iframe name=\"$action_bot\" src=\"$_SERVER[SCRIPT_NAME]?action=$action_bot&session=$U[session]&lang=$language$sort_bot\">" . - noframe_html() . - "</iframe></div>"; - echo "</body></html>"; - exit(); -} - -function noframe_html(): string -{ - return _( - "This chat uses <b>frames</b>. Please enable frames in your browser or use a suitable one!", - ) . - form_target("_parent", "") . - submit(_("Back to the login page."), 'class="backbutton"') . - "</form>"; -} - -function send_messages(): void -{ - global $U, $language; - if ($U["nocache"]) { - $nocache = "&nc=" . substr(time(), -6); - } else { - $nocache = ""; - } - if ($U["sortupdown"]) { - $sort = "#bottom"; - } else { - $sort = ""; - } - print_start( - "messages", - (int) $U["refresh"], - "$_SERVER[SCRIPT_NAME]?action=view&session=$U[session]&lang=$language$nocache$sort", - ); - echo '<a id="top"></a>'; - echo '<a id="bottom_link" href="#bottom">' . _("Bottom") . "</a>"; - echo '<div id="manualrefresh"><br>' . - _("Manual refresh required") . - "<br>" . - form("view") . - submit(_("Reload")) . - "</form><br></div>"; - if (!$U["sortupdown"]) { - echo '<div id="topic">'; - echo get_setting("topic"); - echo "</div>"; - print_chatters(); - print_notifications(); - print_messages(); - } else { - print_messages(); - print_notifications(); - print_chatters(); - echo '<div id="topic">'; - echo get_setting("topic"); - echo "</div>"; - } - echo '<a id="bottom"></a><a id="top_link" href="#top">' . _("Top") . "</a>"; - print_end(); -} - -function send_inbox(): void -{ - global $U, $db; - print_start("inbox"); - echo form("inbox", "clean") . - submit(_("Delete selected messages"), 'class="delbutton"') . - "<br><br>"; - $dateformat = get_setting("dateformat"); - if (!$U["embed"] && get_setting("imgembed")) { - $removeEmbed = true; - } else { - $removeEmbed = false; - } - if ($U["timestamps"] && !empty($dateformat)) { - $timestamps = true; - } else { - $timestamps = false; - } - if ($U["sortupdown"]) { - $direction = "ASC"; - } else { - $direction = "DESC"; - } - $stmt = $db->prepare( - "SELECT id, postdate, text FROM " . - PREFIX . - "inbox WHERE recipient=? ORDER BY id $direction;", - ); - $stmt->execute([$U["nickname"]]); - while ($message = $stmt->fetch(PDO::FETCH_ASSOC)) { - prepare_message_print($message, $removeEmbed); - echo "<div class=\"msg\"><label><input type=\"checkbox\" name=\"mid[]\" value=\"$message[id]\">"; - if ($timestamps) { - echo " <small>" . - date($dateformat, $message["postdate"]) . - " - </small>"; - } - echo " $message[text]</label></div>"; - } - echo "</form><br>" . - form("view") . - submit(_("Back to the chat."), 'class="backbutton"') . - "</form>"; - print_end(); -} - -function send_notes(int $type): void -{ - global $U, $db; - print_start("notes"); - $personalnotes = (bool) get_setting("personalnotes"); - $publicnotes = (bool) get_setting("publicnotes"); - if ($U["status"] >= 3 && ($personalnotes || $publicnotes)) { - echo "<table><tr>"; - if ($U["status"] > 6) { - echo "<td>" . - form_target("view", "notes", "admin") . - submit(_("Admin notes")) . - "</form></td>"; - } - if ($U["status"] >= 5) { - echo "<td>" . - form_target("view", "notes", "staff") . - submit(_("Staff notes")) . - "</form></td>"; - } - if ($personalnotes) { - echo "<td>" . - form_target("view", "notes") . - submit(_("Personal notes")) . - "</form></td>"; - } - if ($publicnotes) { - echo "<td>" . - form_target("view", "notes", "public") . - submit(_("Public notes")) . - "</form></td>"; - } - echo "</tr></table>"; - } - if ($type === 1) { - echo "<h2>" . _("Staff notes") . "</h2><p>"; - $hiddendo = hidden("do", "staff"); - } elseif ($type === 0) { - echo "<h2>" . _("Admin notes") . "</h2><p>"; - $hiddendo = hidden("do", "admin"); - } elseif ($type === 2) { - echo "<h2>" . _("Personal notes") . "</h2><p>"; - $hiddendo = ""; - } elseif ($type === 3) { - echo "<h2>" . _("Public notes") . "</h2><p>"; - $hiddendo = hidden("do", "public"); - } - if (isset($_POST["text"])) { - if (MSGENCRYPTED) { - try { - $_POST["text"] = base64_encode( - sodium_crypto_aead_aes256gcm_encrypt( - $_POST["text"], - "", - AES_IV, - ENCRYPTKEY, - ), - ); - } catch (SodiumException $e) { - send_error($e->getMessage()); - } - } - $time = time(); - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "notes (type, lastedited, editedby, text) VALUES (?, ?, ?, ?);", - ); - $stmt->execute([$type, $time, $U["nickname"], $_POST["text"]]); - echo "<b>" . _("Notes saved!") . "</b> "; - } - $dateformat = get_setting("dateformat"); - if ($type !== 2 && $type !== 3) { - $stmt = $db->prepare( - "SELECT COUNT(*) FROM " . PREFIX . "notes WHERE type=?;", - ); - $stmt->execute([$type]); - } else { - $stmt = $db->prepare( - "SELECT COUNT(*) FROM " . - PREFIX . - "notes WHERE type=? AND editedby=?;", - ); - $stmt->execute([$type, $U["nickname"]]); - } - $num = $stmt->fetch(PDO::FETCH_NUM); - if (!empty($_POST["revision"])) { - $revision = intval($_POST["revision"]); - } else { - $revision = 0; - } - if ($type !== 2 && $type !== 3) { - $stmt = $db->prepare( - "SELECT * FROM " . - PREFIX . - "notes WHERE type=? ORDER BY id DESC LIMIT 1 OFFSET $revision;", - ); - $stmt->execute([$type]); - } else { - $stmt = $db->prepare( - "SELECT * FROM " . - PREFIX . - "notes WHERE type=? AND editedby=? ORDER BY id DESC LIMIT 1 OFFSET $revision;", - ); - $stmt->execute([$type, $U["nickname"]]); - } - if ($note = $stmt->fetch(PDO::FETCH_ASSOC)) { - printf( - _('Last edited by %1$s at %2$s'), - htmlspecialchars($note["editedby"]), - date($dateformat, $note["lastedited"]), - ); - } else { - $note["text"] = ""; - } - if (MSGENCRYPTED) { - try { - $note["text"] = sodium_crypto_aead_aes256gcm_decrypt( - base64_decode($note["text"]), - null, - AES_IV, - ENCRYPTKEY, - ); - } catch (SodiumException $e) { - send_error($e->getMessage()); - } - } - echo "</p>" . form("notes"); - echo "$hiddendo<textarea name=\"text\">" . - htmlspecialchars($note["text"]) . - "</textarea><br>"; - echo submit(_("Save notes")) . "</form><br>"; - if ($num[0] > 1) { - echo "<br><table><tr><td>" . _("Revisions:") . "</td>"; - if ($revision < $num[0] - 1) { - echo "<td>" . form("notes") . hidden("revision", $revision + 1); - echo $hiddendo . submit(_("Older")) . "</form></td>"; - } - if ($revision > 0) { - echo "<td>" . form("notes") . hidden("revision", $revision - 1); - echo $hiddendo . submit(_("Newer")) . "</form></td>"; - } - echo "</tr></table>"; - } - print_end(); -} - -function send_approve_waiting(): void -{ - global $db; - print_start("approve_waiting"); - echo "<h2>" . _("Waiting room") . "</h2>"; - $result = $db->query( - "SELECT * FROM " . - PREFIX . - "sessions WHERE entry=0 AND status=1 ORDER BY id LIMIT 100;", - ); - if ($tmp = $result->fetchAll(PDO::FETCH_ASSOC)) { - echo form("admin", "approve"); - echo "<table>"; - echo "<tr><th>" . - _("Nickname") . - "</th><th>" . - _("User-Agent") . - "</th></tr>"; - foreach ($tmp as $temp) { - echo "<tr>" . hidden("alls[]", htmlspecialchars($temp["nickname"])); - echo '<td><label><input type="checkbox" name="csid[]" value="' . - htmlspecialchars($temp["nickname"]) . - '">'; - echo style_this( - htmlspecialchars($temp["nickname"]), - $temp["style"], - ) . "</label></td>"; - echo "<td>$temp[useragent]</td></tr>"; - } - echo '</table><br><table id="action"><tr><td><label><input type="radio" name="what" value="allowchecked" id="allowchecked" checked>' . - _("Allow checked") . - "</label></td>"; - echo '<td><label><input type="radio" name="what" value="allowall" id="allowall">' . - _("Allow all") . - "</label></td>"; - echo '<td><label><input type="radio" name="what" value="denychecked" id="denychecked">' . - _("Deny checked") . - "</label></td>"; - echo '<td><label><input type="radio" name="what" value="denyall" id="denyall">' . - _("Deny all") . - '</label></td></tr><tr><td colspan="8">' . - _("Send message to denied:") . - ' <input type="text" name="kickmessage" size="45"></td>'; - echo '</tr><tr><td colspan="8">' . - submit(_("Submit")) . - "</td></tr></table></form>"; - } else { - echo _("No more entry requests to approve.") . "<br>"; - } - echo "<br>" . - form("view") . - submit(_("Back to the chat."), 'class="backbutton"') . - "</form>"; - print_end(); -} - -function send_waiting_room(): void -{ - global $U, $db, $language; - $ga = (int) get_setting("guestaccess"); - if ($ga === 3 && (get_count_mods() > 0 || !get_setting("modfallback"))) { - $wait = false; - } else { - $wait = true; - } - check_expired(); - check_kicked(); - $timeleft = get_setting("entrywait") - (time() - $U["lastpost"]); - if ($wait && ($timeleft <= 0 || $ga === 1)) { - $U["entry"] = $U["lastpost"]; - $stmt = $db->prepare( - "UPDATE " . PREFIX . "sessions SET entry=lastpost WHERE session=?;", - ); - $stmt->execute([$U["session"]]); - send_frameset(); - } elseif (!$wait && $U["entry"] != 0) { - send_frameset(); - } else { - $refresh = (int) get_setting("defaultrefresh"); - print_start( - "waitingroom", - $refresh, - "$_SERVER[SCRIPT_NAME]?action=wait&session=$U[session]&lang=$language&nc=" . - substr(time(), -6), - ); - echo "<h2>" . _("Waiting room") . "</h2><p>"; - if ($wait) { - printf( - _( - 'Welcome %1$s, your login has been delayed, you can access the chat in %2$d seconds.', - ), - style_this(htmlspecialchars($U["nickname"]), $U["style"]), - $timeleft, - ); - } else { - printf( - _( - 'Welcome %1$s, your login has been delayed, you can access the chat as soon, as a moderator lets you in.', - ), - style_this(htmlspecialchars($U["nickname"]), $U["style"]), - ); - } - echo "</p><br><p>"; - printf( - _( - "If this page doesn't refresh every %d seconds, use the button below to reload it manually!", - ), - $refresh, - ); - echo "</p><br><br>"; - echo "<hr>" . form("wait"); - echo submit(_("Reload")) . "</form><br>"; - echo form("logout"); - echo submit(_("Exit Chat"), 'id="exitbutton"') . "</form>"; - $rulestxt = get_setting("rulestxt"); - if (!empty($rulestxt)) { - echo '<div id="rules"><h2>' . - _("Rules") . - "</h2><b>$rulestxt</b></div>"; - } - print_end(); - } -} - -function send_choose_messages(): void -{ - global $U; - print_start("choose_messages"); - echo form("admin", "clean"); - echo hidden("what", "selected") . - submit(_("Delete selected messages"), 'class="delbutton"') . - "<br><br>"; - print_messages((int) $U["status"]); - echo "<br>" . - submit(_("Delete selected messages"), 'class="delbutton"') . - "</form>"; - print_end(); -} - -function send_del_confirm(): void -{ - print_start("del_confirm"); - echo '<table><tr><td colspan="2">' . - _("Are you sure?") . - "</td></tr><tr><td>" . - form("delete"); - if (isset($_POST["multi"])) { - echo hidden("multi", "on"); - } - if (isset($_POST["sendto"])) { - echo hidden("sendto", $_POST["sendto"]); - } - echo hidden("confirm", "yes") . - hidden("what", $_POST["what"]) . - submit(_("Yes"), 'class="delbutton"') . - "</form></td><td>" . - form("post"); - if (isset($_POST["multi"])) { - echo hidden("multi", "on"); - } - if (isset($_POST["sendto"])) { - echo hidden("sendto", $_POST["sendto"]); - } - echo submit(_("No"), 'class="backbutton"') . "</form></td><tr></table>"; - print_end(); -} - -function send_post(string $rejected = ""): void -{ - global $U, $db; - print_start("post"); - if (!isset($_REQUEST["sendto"])) { - $_REQUEST["sendto"] = ""; - } - echo "<table><tr><td>" . form("post"); - echo hidden("postid", $U["postid"]); - if (isset($_POST["multi"])) { - echo hidden("multi", "on"); - } - echo '<table><tr><td><table><tr id="firstline"><td> ' . - style_this(htmlspecialchars($U["nickname"]), $U["style"]) . - "</td><td>:</td>"; - if (isset($_POST["multi"])) { - echo "<td><textarea name=\"message\" rows=\"3\" cols=\"40\" style=\"$U[style]\" autofocus>$rejected</textarea></td>"; - } else { - echo "<td><input type=\"text\" name=\"message\" value=\"$rejected\" size=\"40\" style=\"$U[style]\" autofocus></td>"; - } - echo "<td>" . - submit(_("Send to")) . - '</td><td><select name="sendto" size="1">'; - echo "<option "; - if ($_REQUEST["sendto"] === "s *") { - echo "selected "; - } - echo 'value="s *">-' . _("All chatters") . "-</option>"; - if ($U["status"] >= 3) { - echo "<option "; - if ($_REQUEST["sendto"] === "s ?") { - echo "selected "; - } - echo 'value="s ?">-' . _("Members only") . "-</option>"; - } - if ($U["status"] >= 5) { - echo "<option "; - if ($_REQUEST["sendto"] === "s %") { - echo "selected "; - } - echo 'value="s %">-' . _("Staff only") . "-</option>"; - } - if ($U["status"] >= 6) { - echo "<option "; - if ($_REQUEST["sendto"] === "s _") { - echo "selected "; - } - echo 'value="s _">-' . _("Admin only") . "-</option>"; - } - $disablepm = (bool) get_setting("disablepm"); - if (!$disablepm && !($U["status"] == 1 && get_setting("noguestpm"))) { - $users = []; - $stmt = $db->prepare( - "SELECT * FROM (SELECT nickname, style, exiting, 0 AS offline FROM " . - PREFIX . - "sessions WHERE entry!=0 AND status>0 AND incognito=0 UNION SELECT nickname, style, 0, 1 AS offline FROM " . - PREFIX . - "members WHERE eninbox!=0 AND eninbox<=? AND nickname NOT IN (SELECT nickname FROM " . - PREFIX . - "sessions WHERE incognito=0)) AS t WHERE nickname NOT IN (SELECT ign FROM " . - PREFIX . - "ignored WHERE ignby=? UNION SELECT ignby FROM " . - PREFIX . - "ignored WHERE ign=?) ORDER BY LOWER(nickname);", - ); - $stmt->execute([$U["status"], $U["nickname"], $U["nickname"]]); - while ($tmp = $stmt->fetch(PDO::FETCH_ASSOC)) { - if ($tmp["offline"]) { - $users[] = [ - "$tmp[nickname] " . _("(offline)"), - $tmp["style"], - $tmp["nickname"], - ]; - } elseif ($tmp["exiting"]) { - $users[] = [ - "$tmp[nickname] " . _("(logging out)"), - $tmp["style"], - $tmp["nickname"], - ]; - } else { - $users[] = [$tmp["nickname"], $tmp["style"], $tmp["nickname"]]; - } - } - foreach ($users as $user) { - if ($U["nickname"] !== $user[2]) { - echo "<option "; - if ($_REQUEST["sendto"] == $user[2]) { - echo "selected "; - } - echo 'value="' . - htmlspecialchars($user[2]) . - "\" style=\"$user[1]\">" . - htmlspecialchars($user[0]) . - "</option>"; - } - } - } - echo "</select></td>"; - if ( - get_setting("enfileupload") > 0 && - get_setting("enfileupload") <= $U["status"] - ) { - if ( - !$disablepm && - ($U["status"] >= 5 || - ($U["status"] >= 3 && - (get_setting("memkickalways") || - (get_count_mods() == 0 && get_setting("memkick"))))) - ) { - echo '</tr></table><table><tr id="secondline">'; - } - printf( - '<td><input type="file" name="file"><small>' . - "Max %d KB" . - "</small></td>", - get_setting("maxuploadsize"), - ); - } - if ( - !$disablepm && - ($U["status"] >= 5 || - ($U["status"] >= 3 && - (get_setting("memkickalways") || - (get_count_mods() == 0 && get_setting("memkick"))))) - ) { - echo '<td><label><input type="checkbox" name="kick" id="kick" value="kick">' . - _("Kick") . - "</label></td>"; - echo '<td><label><input type="checkbox" name="what" id="what" value="purge" checked>' . - _("Also purge messages") . - "</label></td>"; - } - echo '</tr></table></td></tr></table></form></td></tr><tr><td><table><tr id="thirdline"><td>' . - form("delete"); - if (isset($_POST["multi"])) { - echo hidden("multi", "on"); - } - echo hidden("sendto", htmlspecialchars($_REQUEST["sendto"])) . - hidden("what", "last"); - echo submit(_("Delete last message"), 'class="delbutton"') . - "</form></td><td>" . - form("delete"); - if (isset($_POST["multi"])) { - echo hidden("multi", "on"); - } - echo hidden("sendto", htmlspecialchars($_REQUEST["sendto"])) . - hidden("what", "all"); - echo submit(_("Delete all messages"), 'class="delbutton"') . - '</form></td><td class="spacer"></td><td>' . - form("post"); - if (isset($_POST["multi"])) { - echo submit(_("Switch to single-line")); - } else { - echo hidden("multi", "on") . submit(_("Switch to multi-line")); - } - echo hidden("sendto", htmlspecialchars($_REQUEST["sendto"])) . - "</form></td>"; - echo "</tr></table></td></tr></table>"; - print_end(); -} - -function send_greeting(): void -{ - global $U, $language; - print_start( - "greeting", - (int) $U["refresh"], - "$_SERVER[SCRIPT_NAME]?action=view&session=$U[session]&lang=$language", - ); - printf( - "<h1>" . _("Welcome %s!") . "</h1>", - style_this(htmlspecialchars($U["nickname"]), $U["style"]), - ); - printf( - "<hr><small>" . - _( - 'If this frame does not reload in %d seconds, you\'ll have to enable automatic redirection (meta refresh) in your browser. Also make sure no web filter, local proxy tool or browser plugin is preventing automatic refreshing! This could be for example "Polipo", "NoScript", etc.<br>As a workaround (or in case of server/proxy reload errors) you can always use the buttons at the bottom to refresh manually.', - ) . - "</small>", - $U["refresh"], - ); - $rulestxt = get_setting("rulestxt"); - if (!empty($rulestxt)) { - echo '<hr><div id="rules"><h2>' . _("Rules") . "</h2>$rulestxt</div>"; - } - print_end(); -} - -function send_help(): void -{ - global $U; - print_start("help"); - $rulestxt = get_setting("rulestxt"); - if (!empty($rulestxt)) { - echo '<div id="rules"><h2>' . - _("Rules") . - "</h2>$rulestxt<br></div><hr>"; - } - echo "<h2>" . _("Help") . "</h2>"; - echo _( - "All functions should be pretty much self-explaining, just use the buttons. In your profile you can adjust the refresh rate and font colour, as well as ignore users.<br><u>Note:</u> This is a chat, so if you don't keep talking, you will be automatically logged out after a while.", - ); - if (get_setting("imgembed")) { - echo "<br>" . - _( - "If you want to embed an image in your post, simply put [img] in front of your image URL. Example: [img]http://example.com/images/file.jpg will embed the image in your post.", - ); - } - if ($U["status"] >= 3) { - echo "<br>" . - _( - "Members: You'll have some more options in your profile. You can adjust your font face, change your password anytime and of course you can delete your account.", - ) . - "<br>"; - if ($U["status"] >= 5) { - echo "<br>" . - _( - "Moderators: Notice the Admin-button at the bottom. It'll bring up a page where you can clean the room, kick chatters, view all active sessions and disable guest access completely if needed.", - ) . - "<br>"; - if ($U["status"] >= 7) { - echo "<br>" . - _( - "Admins: You'll be furthermore able to register guests, edit members and register new nicknames.", - ) . - "<br>"; - } - } - } - echo '<br><hr><div id="backcredit">' . - form("view") . - submit(_("Back to the chat."), 'class="backbutton"') . - "</form>" . - credit() . - "</div>"; - print_end(); -} - -function view_publicnotes(): void -{ - global $db; - $dateformat = get_setting("dateformat"); - print_start("publicnotes"); - echo "<h2>" . _("Public notes") . "</h2><p>"; - $query = $db->query( - "SELECT lastedited, editedby, text FROM " . - PREFIX . - "notes INNER JOIN (SELECT MAX(id) AS latest FROM " . - PREFIX . - "notes WHERE type=3 GROUP BY editedby) AS t ON t.latest = id;", - ); - while ($result = $query->fetch(PDO::FETCH_OBJ)) { - if (!empty($result->text)) { - if (MSGENCRYPTED) { - try { - $result->text = sodium_crypto_aead_aes256gcm_decrypt( - base64_decode($result->text), - null, - AES_IV, - ENCRYPTKEY, - ); - } catch (SodiumException $e) { - send_error($e->getMessage()); - } - } - echo "<hr>"; - printf( - _('Last edited by %1$s at %2$s'), - htmlspecialchars($result->editedby), - date($dateformat, $result->lastedited), - ); - echo "<br>"; - echo '<textarea cols="80" rows="9" readonly="true">' . - htmlspecialchars($result->text) . - "</textarea>"; - echo "<br>"; - } - } - print_end(); -} - -function send_profile(string $arg = ""): void -{ - global $U, $db, $language; - print_start("profile"); - echo form("profile", "save") . - "<h2>" . - _("Your Profile") . - "</h2><i>$arg</i><table>"; - thr(); - $ignored = []; - $stmt = $db->prepare( - "SELECT ign FROM " . - PREFIX . - "ignored WHERE ignby=? ORDER BY LOWER(ign);", - ); - $stmt->execute([$U["nickname"]]); - while ($tmp = $stmt->fetch(PDO::FETCH_ASSOC)) { - $ignored[] = htmlspecialchars($tmp["ign"]); - } - if (count($ignored) > 0) { - echo '<tr><td><table id="unignore"><tr><th>' . - _("Don't ignore anymore") . - "</th><td>"; - echo '<select name="unignore" size="1"><option value="">' . - _("(choose)") . - "</option>"; - foreach ($ignored as $ign) { - echo "<option value=\"$ign\">$ign</option>"; - } - echo "</select></td></tr></table></td></tr>"; - thr(); - } - echo '<tr><td><table id="ignore"><tr><th>' . _("Ignore") . "</th><td>"; - echo '<select name="ignore" size="1"><option value="">' . - _("(choose)") . - "</option>"; - $stmt = $db->prepare( - "SELECT DISTINCT poster, style FROM " . - PREFIX . - "messages INNER JOIN (SELECT nickname, style FROM " . - PREFIX . - "sessions UNION SELECT nickname, style FROM " . - PREFIX . - "members) AS t ON (" . - PREFIX . - "messages.poster=t.nickname) WHERE poster!=? AND poster NOT IN (SELECT ign FROM " . - PREFIX . - "ignored WHERE ignby=?) ORDER BY LOWER(poster);", - ); - $stmt->execute([$U["nickname"], $U["nickname"]]); - while ($nick = $stmt->fetch(PDO::FETCH_NUM)) { - echo '<option value="' . - htmlspecialchars($nick[0]) . - "\" style=\"$nick[1]\">" . - htmlspecialchars($nick[0]) . - "</option>"; - } - echo "</select></td></tr></table></td></tr>"; - thr(); - $max_refresh_rate = get_setting("max_refresh_rate"); - $min_refresh_rate = get_setting("min_refresh_rate"); - echo '<tr><td><table id="refresh"><tr><th>' . - sprintf( - _('Refresh rate (%1$d-%2$d seconds)'), - $min_refresh_rate, - $max_refresh_rate, - ) . - "</th><td>"; - echo '<input type="number" name="refresh" size="3" min="' . - $min_refresh_rate . - '" max="' . - $max_refresh_rate . - '" value="' . - $U["refresh"] . - '"></td></tr></table></td></tr>'; - thr(); - preg_match("/#([0-9a-f]{6})/i", $U["style"], $matches); - echo '<tr><td><table id="colour"><tr><th>' . - _("Font colour") . - " (<a href=\"$_SERVER[SCRIPT_NAME]?action=colours&amp;session=$U[session]&amp;lang=$language\" target=\"view\">" . - _("View examples") . - "</a>)</th><td>"; - echo "<input type=\"color\" value=\"#$matches[1]\" name=\"colour\"></td></tr></table></td></tr>"; - thr(); - echo '<tr><td><table id="bgcolour"><tr><th>' . - _("Background colour") . - " (<a href=\"$_SERVER[SCRIPT_NAME]?action=colours&amp;session=$U[session]&amp;lang=$language\" target=\"view\">" . - _("View examples") . - "</a>)</th><td>"; - echo "<input type=\"color\" value=\"#$U[bgcolour]\" name=\"bgcolour\"></td></tr></table></td></tr>"; - thr(); - if ($U["status"] >= 3) { - echo '<tr><td><table id="font"><tr><th>' . - _("Fontface") . - "</th><td><table>"; - echo '<tr><td>&nbsp;</td><td><select name="font" size="1"><option value="">* ' . - _("Room Default") . - " *</option>"; - $F = load_fonts(); - foreach ($F as $name => $font) { - echo "<option style=\"$font\" "; - if (strpos($U["style"], $font) !== false) { - echo "selected "; - } - echo "value=\"$name\">$name</option>"; - } - echo '</select></td><td>&nbsp;</td><td><label><input type="checkbox" name="bold" id="bold" value="on"'; - if (strpos($U["style"], "font-weight:bold;") !== false) { - echo " checked"; - } - echo "><b>" . - _("Bold") . - '</b></label></td><td>&nbsp;</td><td><label><input type="checkbox" name="italic" id="italic" value="on"'; - if (strpos($U["style"], "font-style:italic;") !== false) { - echo " checked"; - } - echo "><i>" . - _("Italic") . - '</i></label></td><td>&nbsp;</td><td><label><input type="checkbox" name="small" id="small" value="on"'; - if (strpos($U["style"], "font-size:smaller;") !== false) { - echo " checked"; - } - echo "><small>" . - _("Small") . - "</small></label></td></tr></table></td></tr></table></td></tr>"; - thr(); - } - echo "<tr><td>" . - style_this( - htmlspecialchars($U["nickname"]) . - " : " . - _("Example for your chosen font"), - $U["style"], - ) . - "</td></tr>"; - thr(); - $bool_settings = [ - "timestamps" => _("Show Timestamps"), - "nocache" => _("Autoscroll (for old browsers or top-to-bottom sort)."), - "sortupdown" => _("Sort messages from top to bottom"), - "hidechatters" => _("Hide list of chatters"), - ]; - if (get_setting("imgembed")) { - $bool_settings["embed"] = _("Embed images"); - } - if ($U["status"] >= 5 && get_setting("incognito")) { - $bool_settings["incognito"] = _("Incognito mode"); - } - foreach ($bool_settings as $setting => $title) { - echo "<tr><td><table id=\"$setting\"><tr><th>" . $title . "</th><td>"; - echo "<label><input type=\"checkbox\" name=\"$setting\" value=\"on\""; - if ($U[$setting]) { - echo " checked"; - } - echo "><b>" . _("Enabled") . "</b></label></td></tr></table></td></tr>"; - thr(); - } - if ($U["status"] >= 2 && get_setting("eninbox")) { - echo '<tr><td><table id="eninbox"><tr><th>' . - _("Enable offline inbox") . - "</th><td>"; - echo '<select name="eninbox" id="eninbox">'; - echo '<option value="0"'; - if ($U["eninbox"] == 0) { - echo " selected"; - } - echo ">" . _("Disabled") . "</option>"; - echo '<option value="1"'; - if ($U["eninbox"] == 1) { - echo " selected"; - } - echo ">" . _("For everyone") . "</option>"; - echo '<option value="3"'; - if ($U["eninbox"] == 3) { - echo " selected"; - } - echo ">" . _("For members only") . "</option>"; - echo '<option value="5"'; - if ($U["eninbox"] == 5) { - echo " selected"; - } - echo ">" . _("For staff only") . "</option>"; - echo "</select></td></tr></table></td></tr>"; - thr(); - } - echo '<tr><td><table id="tz"><tr><th>' . _("Time zone") . "</th><td>"; - echo '<select name="tz">'; - $tzs = timezone_identifiers_list(); - foreach ($tzs as $tz) { - echo "<option value=\"$tz\""; - if ($U["tz"] == $tz) { - echo " selected"; - } - echo ">$tz</option>"; - } - echo "</select></td></tr></table></td></tr>"; - thr(); - if ($U["status"] >= 2) { - echo '<tr><td><table id="changepass"><tr><th>' . - _("Change Password") . - "</th></tr>"; - echo "<tr><td><table>"; - echo "<tr><td>&nbsp;</td><td>" . - _("Old password:") . - '</td><td><input type="password" name="oldpass" size="20" autocomplete="current-password"></td></tr>'; - echo "<tr><td>&nbsp;</td><td>" . - _("New password:") . - '</td><td><input type="password" name="newpass" size="20" autocomplete="new-password"></td></tr>'; - echo "<tr><td>&nbsp;</td><td>" . - _("Confirm new password:") . - '</td><td><input type="password" name="confirmpass" size="20" autocomplete="new-password"></td></tr>'; - echo "</table></td></tr></table></td></tr>"; - thr(); - echo '<tr><td><table id="changenick"><tr><th>' . - _("Change Nickname") . - "</th><td><table>"; - echo "<tr><td>&nbsp;</td><td>" . - _("New nickname:") . - '</td><td><input type="text" name="newnickname" size="20" autocomplete="username">'; - echo "</table></td></tr></table></td></tr>"; - thr(); - } - echo "<tr><td>" . submit(_("Save changes")) . "</td></tr></table></form>"; - if ($U["status"] > 1 && $U["status"] < 8) { - echo "<br>" . - form("profile", "delete") . - submit(_("Delete account"), 'class="delbutton"') . - "</form>"; - } - echo '<br><p id="changelang">' . _("Change language:"); - foreach (LANGUAGES as $lang => $data) { - echo " <a href=\"$_SERVER[SCRIPT_NAME]?lang=$lang&amp;session=$U[session]&amp;action=controls\" target=\"controls\">$data[name]</a>"; - } - echo "</p><br>" . - form("view") . - submit(_("Back to the chat."), 'class="backbutton"') . - "</form>"; - print_end(); -} - -function send_controls(): void -{ - global $U; - print_start("controls"); - $personalnotes = (bool) get_setting("personalnotes"); - $publicnotes = (bool) get_setting("publicnotes"); - $hide_reload_post_box = (bool) get_setting("hide_reload_post_box"); - $hide_reload_messages = (bool) get_setting("hide_reload_messages"); - $hide_profile = (bool) get_setting("hide_profile"); - $hide_admin = (bool) get_setting("hide_admin"); - $hide_notes = (bool) get_setting("hide_notes"); - $hide_clone = (bool) get_setting("hide_clone"); - $hide_rearrange = (bool) get_setting("hide_rearrange"); - $hide_help = (bool) get_setting("hide_help"); - echo "<table><tr>"; - if (!$hide_reload_post_box) { - echo "<td>" . - form_target("post", "post") . - submit(_("Reload Post Box")) . - "</form></td>"; - } - if (!$hide_reload_messages) { - echo "<td>" . - form_target("view", "view") . - submit(_("Reload Messages")) . - "</form></td>"; - } - if (!$hide_profile) { - echo "<td>" . - form_target("view", "profile") . - submit(_("Profile")) . - "</form></td>"; - } - if ($U["status"] >= 5) { - if (!$hide_admin) { - echo "<td>" . - form_target("view", "admin") . - submit(_("Admin")) . - "</form></td>"; - } - if (!$personalnotes && !$hide_notes) { - echo "<td>" . - form_target("view", "notes", "staff") . - submit(_("Notes")) . - "</form></td>"; - } - } - if ($publicnotes) { - echo "<td>" . - form_target("view", "viewpublicnotes") . - submit(_("View public notes")) . - "</form></td>"; - } - if ($U["status"] >= 3) { - if ($personalnotes || $publicnotes) { - echo "<td>" . - form_target("view", "notes") . - submit(_("Notes")) . - "</form></td>"; - } - if (!$hide_clone) { - echo "<td>" . - form_target("_blank", "login") . - submit(_("Clone")) . - "</form></td>"; - } - } - if (!isset($_GET["sort"])) { - $sort = 0; - } else { - $sort = 1; - } - if (!$hide_rearrange) { - echo "<td>" . - form_target("_parent", "login") . - hidden("sort", $sort) . - submit(_("Rearrange")) . - "</form></td>"; - } - if (!$hide_help) { - echo "<td>" . - form_target("view", "help") . - submit(_("Rules & Help")) . - "</form></td>"; - } - echo "<td>" . - form_target("_parent", "logout") . - submit(_("Exit Chat"), 'id="exitbutton"') . - "</form></td>"; - echo "</tr></table>"; - print_end(); -} - -function send_download(): void -{ - global $db; - if (isset($_GET["id"])) { - $stmt = $db->prepare( - "SELECT filename, type, data FROM " . - PREFIX . - "files WHERE hash=?;", - ); - $stmt->execute([$_GET["id"]]); - if ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { - send_headers(); - header("Content-Type: $data[type]"); - header("Content-Disposition: filename=\"$data[filename]\""); - header("Content-Security-Policy: default-src 'none'"); - echo base64_decode($data["data"]); - } else { - http_response_code(404); - send_error(_("File not found!")); - } - } else { - http_response_code(404); - send_error(_("File not found!")); - } -} - -function send_logout(): void -{ - global $U; - print_start("logout"); - echo "<h1>" . - sprintf( - _("Bye %s, visit again soon!"), - style_this(htmlspecialchars($U["nickname"]), $U["style"]), - ) . - "</h1>" . - form_target("_parent", "") . - submit(_("Back to the login page."), 'class="backbutton"') . - "</form>"; - print_end(); -} - -function send_colours(): void -{ - print_start("colours"); - echo "<h2>" . _("Colourtable") . "</h2><kbd><b>"; - for ($red = 0x00; $red <= 0xff; $red += 0x33) { - for ($green = 0x00; $green <= 0xff; $green += 0x33) { - for ($blue = 0x00; $blue <= 0xff; $blue += 0x33) { - $hcol = sprintf("%02X%02X%02X", $red, $green, $blue); - echo "<span style=\"color:#$hcol\">$hcol</span> "; - } - echo "<br>"; - } - echo "<br>"; - } - echo "</b></kbd>" . - form("profile") . - submit(_("Back to your Profile"), ' class="backbutton"') . - "</form>"; - print_end(); -} - -function send_login(): void -{ - $ga = (int) get_setting("guestaccess"); - if ($ga === 4) { - send_chat_disabled(); - } - print_start("login"); - $englobal = (int) get_setting("englobalpass"); - echo '<h1 id="chatname">' . get_setting("chatname") . "</h1>"; - echo form_target("_parent", "login"); - if ($englobal === 1 && isset($_POST["globalpass"])) { - echo hidden("globalpass", htmlspecialchars($_POST["globalpass"])); - } - echo "<table>"; - if ( - $englobal !== 1 || - (isset($_POST["globalpass"]) && - $_POST["globalpass"] == get_setting("globalpass")) - ) { - echo "<tr><td>" . - _("Nickname:") . - '</td><td><input type="text" name="nick" size="15" autocomplete="username" autofocus></td></tr>'; - echo "<tr><td>" . - _("Password:") . - '</td><td><input type="password" name="pass" size="15" autocomplete="current-password"></td></tr>'; - send_captcha(); - if ($ga !== 0) { - if (get_setting("guestreg") != 0) { - echo "<tr><td>" . - _("Repeat password<br>to register") . - '</td><td><input type="password" name="regpass" size="15" placeholder="' . - _("(optional)") . - '" autocomplete="new-password"></td></tr>'; - } - if ($englobal === 2) { - echo "<tr><td>" . - _("Global Password:") . - '</td><td><input type="password" name="globalpass" size="15"></td></tr>'; - } - echo '<tr><td colspan="2">' . - _("Guests, choose a colour:") . - '<br><select name="colour"><option value="">* ' . - _("Random Colour") . - " *</option>"; - print_colours(); - echo "</select></td></tr>"; - } else { - echo '<tr><td colspan="2">' . - _("Sorry, currently members only!") . - "</td></tr>"; - } - echo '<tr><td colspan="2">' . - submit(_("Enter Chat")) . - "</td></tr></table></form>"; - get_nowchatting(); - echo '<br><div id="topic">'; - echo get_setting("topic"); - echo "</div>"; - $rulestxt = get_setting("rulestxt"); - if (!empty($rulestxt)) { - echo '<div id="rules"><h2>' . - _("Rules") . - "</h2><b>$rulestxt</b></div>"; - } - } else { - echo "<tr><td>" . - _("Global Password:") . - '</td><td><input type="password" name="globalpass" size="15" autofocus></td></tr>'; - if ($ga === 0) { - echo '<tr><td colspan="2">' . - _("Sorry, currently members only!") . - "</td></tr>"; - } - echo '<tr><td colspan="2">' . - submit(_("Enter Chat")) . - "</td></tr></table></form>"; - } - echo '<p id="changelang">' . _("Change language:"); - foreach (LANGUAGES as $lang => $data) { - echo " <a href=\"$_SERVER[SCRIPT_NAME]?lang=$lang\">$data[name]</a>"; - } - echo "</p>" . credit(); - print_end(); -} - -function send_chat_disabled(): void -{ - print_start("disabled"); - echo get_setting("disabletext"); - print_end(); -} - -function send_error(string $err): void -{ - print_start("error"); - echo "<h2>" . - sprintf(_("Error: %s"), $err) . - "</h2>" . - form_target("_parent", "") . - submit(_("Back to the login page."), 'class="backbutton"') . - "</form>"; - print_end(); -} - -function send_fatal_error(string $err): void -{ - global $language, $styles, $dir; - prepare_stylesheets("fatal_error"); - send_headers(); - echo '<!DOCTYPE html><html lang="' . - $language . - '" dir="' . - $dir . - '"><head>' . - meta_html(); - echo "<title>" . _("Fatal error") . "</title>"; - echo "<style>$styles[fatal_error]</style>"; - echo "</head><body>"; - echo "<h2>" . sprintf(_("Fatal error: %s"), $err) . "</h2>"; - print_end(); -} - -function print_notifications(): void -{ - global $U, $db; - echo '<span id="notifications">'; - $stmt = $db->prepare( - "SELECT loginfails FROM " . PREFIX . "members WHERE nickname=?;", - ); - $stmt->execute([$U["nickname"]]); - $temp = $stmt->fetch(PDO::FETCH_NUM); - if ($temp && $temp[0] > 0) { - echo '<p align="middle">' . - $temp[0] . - "&nbsp;" . - _("Failed login attempt(s)") . - "</p>"; - } - if ($U["status"] >= 2 && $U["eninbox"] != 0) { - $stmt = $db->prepare( - "SELECT COUNT(*) FROM " . PREFIX . "inbox WHERE recipient=?;", - ); - $stmt->execute([$U["nickname"]]); - $tmp = $stmt->fetch(PDO::FETCH_NUM); - if ($tmp[0] > 0) { - echo "<p>" . - form("inbox") . - submit(sprintf(_("Read %d messages in your inbox"), $tmp[0])) . - "</form></p>"; - } - } - if ($U["status"] >= 5 && get_setting("guestaccess") == 3) { - $result = $db->query( - "SELECT COUNT(*) FROM " . - PREFIX . - "sessions WHERE entry=0 AND status=1;", - ); - $temp = $result->fetch(PDO::FETCH_NUM); - if ($temp[0] > 0) { - echo "<p>"; - echo form("admin", "approve"); - echo submit(sprintf(_("%d new guests to approve"), $temp[0])) . - "</form></p>"; - } - } - echo "</span>"; -} - -function print_chatters(): void -{ - global $U, $db, $language; - if (!$U["hidechatters"]) { - echo '<div id="chatters"><table><tr>'; - $stmt = $db->prepare( - "SELECT nickname, style, status, exiting FROM " . - PREFIX . - "sessions WHERE entry!=0 AND status>0 AND incognito=0 AND nickname NOT IN (SELECT ign FROM " . - PREFIX . - "ignored WHERE ignby=? UNION SELECT ignby FROM " . - PREFIX . - "ignored WHERE ign=?) ORDER BY status DESC, lastpost DESC;", - ); - $stmt->execute([$U["nickname"], $U["nickname"]]); - $nc = substr(time(), -6); - $G = $M = $S = $A = []; - $channellink = "<a class=\"channellink\" href=\"$_SERVER[SCRIPT_NAME]?action=post&amp;session=$U[session]&amp;lang=$language&amp;nc=$nc&amp;sendto="; - $nicklink = "<a class=\"nicklink\" href=\"$_SERVER[SCRIPT_NAME]?action=post&amp;session=$U[session]&amp;lang=$language&amp;nc=$nc&amp;sendto="; - while ($user = $stmt->fetch(PDO::FETCH_NUM)) { - $link = - $nicklink . - urlencode($user[0]) . - '" target="post">' . - style_this(htmlspecialchars($user[0]), $user[1]) . - "</a>"; - if ($user[3] > 0) { - $link .= - '<span class="sysmsg" title="' . - _("logging out") . - '">' . - get_setting("exitingtxt") . - "</span>"; - } - if ($user[2] < 3) { - // guest or superguest - $G[] = $link; - } elseif ($user[2] >= 7) { - // admin or superadmin - $A[] = $link; - } elseif ($user[2] >= 5 && $user[2] <= 6) { - // moderator or supermoderator - $S[] = $link; - } elseif ($user[2] = 3) { - // member - $M[] = $link; - } - } - if ($U["status"] > 5) { - // can chat in admin channel - echo "<th>" . - $channellink . - 's _" target="post">' . - _("Admin") . - ":</a></th><td>&nbsp;</td><td>" . - implode(" &nbsp; ", $A) . - "</td>"; - } else { - echo "<th>" . - _("Admin:") . - "</th><td>&nbsp;</td><td>" . - implode(" &nbsp; ", $A) . - "</td>"; - } - if ($U["status"] > 4) { - // can chat in staff channel - echo "<th>" . - $channellink . - 's &#37;" target="post">' . - _("Staff") . - ":</a></th><td>&nbsp;</td><td>" . - implode(" &nbsp; ", $S) . - "</td>"; - } else { - echo "<th>" . - _("Staff:") . - "</th><td>&nbsp;</td><td>" . - implode(" &nbsp; ", $S) . - "</td>"; - } - if ($U["status"] >= 3) { - // can chat in member channel - echo "<th>" . - $channellink . - 's ?" target="post">' . - _("Members") . - ':</a></th><td>&nbsp;</td><td class="chattername">' . - implode(" &nbsp; ", $M) . - "</td>"; - } else { - echo "<th>" . - _("Members:") . - "</th><td>&nbsp;</td><td>" . - implode(" &nbsp; ", $M) . - "</td>"; - } - echo "<th>" . - $channellink . - 's *" target="post">' . - _("Guests") . - ':</a></th><td>&nbsp;</td><td class="chattername">' . - implode(" &nbsp; ", $G) . - "</td>"; - echo "</tr></table></div>"; - } -} - -// session management - -function create_session(bool $setup, string $nickname, string $password): void -{ - global $U; - $U["nickname"] = preg_replace("/\s/", "", $nickname); - if (check_member($password)) { - if ($setup && $U["status"] >= 7) { - $U["incognito"] = 1; - } - $U["entry"] = $U["lastpost"] = time(); - } else { - add_user_defaults($password); - check_captcha($_POST["challenge"] ?? "", $_POST["captcha"] ?? ""); - $ga = (int) get_setting("guestaccess"); - if (!valid_nick($U["nickname"])) { - send_error( - sprintf( - _( - 'Invalid nickname (%1$d characters maximum and has to match the regular expression "%2$s")', - ), - get_setting("maxname"), - get_setting("nickregex"), - ), - ); - } - if (!valid_pass($password)) { - send_error( - sprintf( - _( - 'Invalid password (At least %1$d characters and has to match the regular expression "%2$s")', - ), - get_setting("minpass"), - get_setting("passregex"), - ), - ); - } - if ($ga === 0) { - send_error(_("Sorry, currently members only!")); - } elseif (in_array($ga, [2, 3], true)) { - $U["entry"] = 0; - } - if ( - get_setting("englobalpass") != 0 && - isset($_POST["globalpass"]) && - $_POST["globalpass"] != get_setting("globalpass") - ) { - send_error(_("Wrong global Password!")); - } - } - $U["exiting"] = 0; - try { - $U["postid"] = bin2hex(random_bytes(3)); - } catch (Exception $e) { - send_error($e->getMessage()); - } - write_new_session($password); -} - -function check_captcha(string $challenge, string $captcha_code): void -{ - global $db, $memcached; - $captcha = (int) get_setting("captcha"); - if ($captcha !== 0) { - if (empty($challenge)) { - send_error(_("Wrong Captcha")); - } - $code = ""; - if (MEMCACHED) { - if ( - !($code = $memcached->get( - DBNAME . "-" . PREFIX . "captcha-$_POST[challenge]", - )) - ) { - send_error(_("Captcha already used or timed out.")); - } - $memcached->delete( - DBNAME . "-" . PREFIX . "captcha-$_POST[challenge]", - ); - } else { - $stmt = $db->prepare( - "SELECT code FROM " . PREFIX . "captcha WHERE id=?;", - ); - $stmt->execute([$challenge]); - $stmt->bindColumn(1, $code); - if (!$stmt->fetch(PDO::FETCH_BOUND)) { - send_error(_("Captcha already used or timed out.")); - } - $time = time(); - $stmt = $db->prepare( - "DELETE FROM " . - PREFIX . - "captcha WHERE id=? OR time<(?-(SELECT value FROM " . - PREFIX . - "settings WHERE setting='captchatime'));", - ); - $stmt->execute([$challenge, $time]); - } - if ($captcha_code !== $code) { - if ($captcha !== 3 || strrev($captcha_code) !== $code) { - send_error(_("Wrong Captcha")); - } - } - } -} - -function is_definitely_ssl(): bool -{ - if (!empty($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] !== "off") { - return true; - } - if (isset($_SERVER["SERVER_PORT"]) && "443" == $_SERVER["SERVER_PORT"]) { - return true; - } - if ( - isset($_SERVER["HTTP_X_FORWARDED_PROTO"]) && - "https" === $_SERVER["HTTP_X_FORWARDED_PROTO"] - ) { - return true; - } - return false; -} - -function set_secure_cookie(string $name, string $value): void -{ - if (version_compare(PHP_VERSION, "7.3.0") >= 0) { - setcookie($name, $value, [ - "expires" => 0, - "path" => "/", - "domain" => "", - "secure" => is_definitely_ssl(), - "httponly" => true, - "samesite" => "Strict", - ]); - } else { - setcookie($name, $value, 0, "/", "", is_definitely_ssl(), true); - } -} - -function write_new_session(string $password): void -{ - global $U, $db, $session; - $stmt = $db->prepare( - "SELECT * FROM " . PREFIX . "sessions WHERE nickname=?;", - ); - $stmt->execute([$U["nickname"]]); - if ($temp = $stmt->fetch(PDO::FETCH_ASSOC)) { - // check whether alrady logged in - if (password_verify($password, $temp["passhash"])) { - $U = $temp; - check_kicked(); - set_secure_cookie(COOKIENAME, $U["session"]); - } else { - send_error( - _("A user with this nickname is already logged in.") . - "<br>" . - _("Wrong Password!"), - ); - } - } else { - // create new session - $stmt = $db->prepare( - "SELECT null FROM " . PREFIX . "sessions WHERE session=?;", - ); - do { - try { - $U["session"] = bin2hex(random_bytes(16)); - } catch (Exception $e) { - send_error($e->getMessage()); - } - $stmt->execute([$U["session"]]); - } while ($stmt->fetch(PDO::FETCH_NUM)); // check for hash collision - if (isset($_SERVER["HTTP_USER_AGENT"])) { - $useragent = htmlspecialchars($_SERVER["HTTP_USER_AGENT"]); - } else { - $useragent = ""; - } - if (get_setting("trackip")) { - $ip = $_SERVER["REMOTE_ADDR"]; - } else { - $ip = ""; - } - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "sessions (session, nickname, status, refresh, style, lastpost, passhash, useragent, bgcolour, entry, exiting, timestamps, embed, incognito, ip, nocache, tz, eninbox, sortupdown, hidechatters, nocache_old, postid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", - ); - $stmt->execute([ - $U["session"], - $U["nickname"], - $U["status"], - $U["refresh"], - $U["style"], - $U["lastpost"], - $U["passhash"], - $useragent, - $U["bgcolour"], - $U["entry"], - $U["exiting"], - $U["timestamps"], - $U["embed"], - $U["incognito"], - $ip, - $U["nocache"], - $U["tz"], - $U["eninbox"], - $U["sortupdown"], - $U["hidechatters"], - $U["nocache_old"], - $U["postid"], - ]); - $session = $U["session"]; - set_secure_cookie(COOKIENAME, $U["session"]); - if ($U["status"] >= 3 && !$U["incognito"]) { - add_system_message( - sprintf( - get_setting("msgenter"), - style_this(htmlspecialchars($U["nickname"]), $U["style"]), - ), - "", - ); - } - } -} - -function show_fails(): void -{ - global $db, $U; - $stmt = $db->prepare( - "SELECT loginfails FROM " . PREFIX . "members WHERE nickname=?;", - ); - $stmt->execute([$U["nickname"]]); - $temp = $stmt->fetch(PDO::FETCH_NUM); - if ($temp && $temp[0] > 0) { - print_start("failednotice"); - echo $temp[0] . "&nbsp;" . _("Failed login attempt(s)") . "<br>"; - $stmt = $db->prepare( - "UPDATE " . PREFIX . "members SET loginfails=? WHERE nickname=?;", - ); - $stmt->execute([0, $U["nickname"]]); - echo form_target("_self", "login") . - submit(_("Dismiss")) . - "</form></td>"; - print_end(); - } -} - -function approve_session(): void -{ - global $db; - if (isset($_POST["what"])) { - if ($_POST["what"] === "allowchecked" && isset($_POST["csid"])) { - $stmt = $db->prepare( - "UPDATE " . - PREFIX . - "sessions SET entry=lastpost WHERE nickname=?;", - ); - foreach ($_POST["csid"] as $nick) { - $stmt->execute([$nick]); - } - } elseif ($_POST["what"] === "allowall" && isset($_POST["alls"])) { - $stmt = $db->prepare( - "UPDATE " . - PREFIX . - "sessions SET entry=lastpost WHERE nickname=?;", - ); - foreach ($_POST["alls"] as $nick) { - $stmt->execute([$nick]); - } - } elseif ($_POST["what"] === "denychecked" && isset($_POST["csid"])) { - $time = - 60 * (get_setting("kickpenalty") - get_setting("guestexpire")) + - time(); - $stmt = $db->prepare( - "UPDATE " . - PREFIX . - "sessions SET lastpost=?, status=0, kickmessage=? WHERE nickname=? AND status=1;", - ); - foreach ($_POST["csid"] as $nick) { - $stmt->execute([$time, $_POST["kickmessage"], $nick]); - } - } elseif ($_POST["what"] === "denyall" && isset($_POST["alls"])) { - $time = - 60 * (get_setting("kickpenalty") - get_setting("guestexpire")) + - time(); - $stmt = $db->prepare( - "UPDATE " . - PREFIX . - "sessions SET lastpost=?, status=0, kickmessage=? WHERE nickname=? AND status=1;", - ); - foreach ($_POST["alls"] as $nick) { - $stmt->execute([$time, $_POST["kickmessage"], $nick]); - } - } - } -} - -function check_login(): void -{ - global $U, $db; - $ga = (int) get_setting("guestaccess"); - parse_sessions(); - if (isset($U["session"])) { - if ($U["exiting"] == 1) { - $stmt = $db->prepare( - "UPDATE " . - PREFIX . - "sessions SET exiting=0 WHERE session=? LIMIT 1;", - ); - $stmt->execute([$U["session"]]); - } - check_kicked(); - } elseif ( - get_setting("englobalpass") == 1 && - (!isset($_POST["globalpass"]) || - $_POST["globalpass"] != get_setting("globalpass")) - ) { - send_error(_("Wrong global Password!")); - } elseif (!isset($_POST["nick"]) || !isset($_POST["pass"])) { - send_login(); - } else { - if ($ga === 4) { - send_chat_disabled(); - } - if (!empty($_POST["regpass"]) && $_POST["regpass"] !== $_POST["pass"]) { - send_error(_("Password confirmation does not match!")); - } - create_session(false, $_POST["nick"], $_POST["pass"]); - if (!empty($_POST["regpass"])) { - $guestreg = (int) get_setting("guestreg"); - if ($guestreg === 1) { - register_guest(2, $_POST["nick"]); - $U["status"] = 2; - } elseif ($guestreg === 2) { - register_guest(3, $_POST["nick"]); - $U["status"] = 3; - } - } - } - if ($U["status"] == 1) { - if (in_array($ga, [2, 3], true)) { - send_waiting_room(); - } - } -} - -function kill_session(): void -{ - global $U, $db, $session; - parse_sessions(); - check_expired(); - check_kicked(); - setcookie(COOKIENAME, false); - $session = ""; - $stmt = $db->prepare("DELETE FROM " . PREFIX . "sessions WHERE session=?;"); - $stmt->execute([$U["session"]]); - if ($U["status"] >= 3 && !$U["incognito"]) { - add_system_message( - sprintf( - get_setting("msgexit"), - style_this(htmlspecialchars($U["nickname"]), $U["style"]), - ), - "", - ); - } -} - -function kick_chatter(array $names, string $mes, bool $purge): bool -{ - global $U, $db; - $lonick = ""; - if (strlen($mes) < 1) { - $mes = _("no kick message"); - } - $time = - 60 * (get_setting("kickpenalty") - get_setting("guestexpire")) + time(); - $check = $db->prepare( - "SELECT style, entry FROM " . - PREFIX . - "sessions WHERE nickname=? AND status!=0 AND (status<? OR nickname=?);", - ); - $stmt = $db->prepare( - "UPDATE " . - PREFIX . - "sessions SET lastpost=?, status=0, kickmessage=? WHERE nickname=?;", - ); - $all = false; - if ($names[0] === "s *") { - $tmp = $db->query( - "SELECT nickname FROM " . PREFIX . "sessions WHERE status=1;", - ); - $names = []; - while ($name = $tmp->fetch(PDO::FETCH_NUM)) { - $names[] = $name[0]; - } - $all = true; - } - $i = 0; - foreach ($names as $name) { - $check->execute([$name, $U["status"], $U["nickname"]]); - if ($temp = $check->fetch(PDO::FETCH_ASSOC)) { - $stmt->execute([$time, $mes, $name]); - if ($purge) { - del_all_messages($name, (int) $temp["entry"]); - } - $lonick .= - style_this(htmlspecialchars($name), $temp["style"]) . ", "; - ++$i; - } - } - if ($i > 0) { - if ($all) { - add_system_message( - sprintf(get_setting("msgallkick"), $mes), - $U["nickname"], - ); - } else { - $lonick = substr($lonick, 0, -2); - if ($i > 1) { - add_system_message( - sprintf(get_setting("msgmultikick"), $lonick, $mes), - $U["nickname"], - ); - } else { - add_system_message( - sprintf(get_setting("msgkick"), $lonick, $mes), - $U["nickname"], - ); - } - } - return true; - } - return false; -} - -function logout_chatter(array $names): void -{ - global $U, $db; - $stmt = $db->prepare( - "DELETE FROM " . PREFIX . "sessions WHERE nickname=? AND status<?;", - ); - if ($names[0] === "s *") { - $tmp = $db->query( - "SELECT nickname FROM " . PREFIX . "sessions WHERE status=1;", - ); - $names = []; - while ($name = $tmp->fetch(PDO::FETCH_NUM)) { - $names[] = $name[0]; - } - } - foreach ($names as $name) { - $stmt->execute([$name, $U["status"]]); - } -} - -function check_session(): void -{ - global $U; - parse_sessions(); - check_expired(); - check_kicked(); - if ($U["entry"] == 0) { - send_waiting_room(); - } -} - -function check_expired(): void -{ - global $U, $session; - if (!isset($U["session"])) { - setcookie(COOKIENAME, false); - $session = ""; - send_error(_("Invalid/expired session")); - } -} - -function get_count_mods(): int -{ - global $db; - $c = $db - ->query("SELECT COUNT(*) FROM " . PREFIX . "sessions WHERE status>=5") - ->fetch(PDO::FETCH_NUM); - return (int) $c[0]; -} - -function check_kicked(): void -{ - global $U, $session; - if ($U["status"] == 0) { - setcookie(COOKIENAME, false); - $session = ""; - send_error(_("You have been kicked!") . "<br>$U[kickmessage]"); - } -} - -function get_nowchatting(): void -{ - global $db; - parse_sessions(); - $stmt = $db->query( - "SELECT COUNT(*) FROM " . - PREFIX . - "sessions WHERE entry!=0 AND status>0 AND incognito=0;", - ); - $count = $stmt->fetch(PDO::FETCH_NUM); - echo '<div id="chatters">' . - sprintf(_("Currently %d chatter(s) in room:"), $count[0]) . - "<br>"; - if (!get_setting("hidechatters")) { - $stmt = $db->query( - "SELECT nickname, style FROM " . - PREFIX . - "sessions WHERE entry!=0 AND status>0 AND incognito=0 ORDER BY status DESC, lastpost DESC;", - ); - while ($user = $stmt->fetch(PDO::FETCH_NUM)) { - echo style_this(htmlspecialchars($user[0]), $user[1]) . " &nbsp; "; - } - } - echo "</div>"; -} - -function parse_sessions(): void -{ - global $U, $db, $session; - // look for our session - if (!empty($session)) { - $stmt = $db->prepare( - "SELECT * FROM " . PREFIX . "sessions WHERE session=?;", - ); - $stmt->execute([$session]); - if ($tmp = $stmt->fetch(PDO::FETCH_ASSOC)) { - $U = $tmp; - } - } - set_default_tz(); -} - -// member handling - -function check_member(string $password): bool -{ - global $U, $db; - $stmt = $db->prepare( - "SELECT * FROM " . PREFIX . "members WHERE nickname=?;", - ); - $stmt->execute([$U["nickname"]]); - if ($temp = $stmt->fetch(PDO::FETCH_ASSOC)) { - if (get_setting("dismemcaptcha") == 0) { - check_captcha($_POST["challenge"] ?? "", $_POST["captcha"] ?? ""); - } - if ($temp["passhash"] === md5(sha1(md5($U["nickname"] . $password)))) { - // old hashing method, update on the fly - $temp["passhash"] = password_hash($password, PASSWORD_DEFAULT); - $stmt = $db->prepare( - "UPDATE " . PREFIX . "members SET passhash=? WHERE nickname=?;", - ); - $stmt->execute([$temp["passhash"], $U["nickname"]]); - } - if (password_verify($password, $temp["passhash"])) { - $U = $temp; - $stmt = $db->prepare( - "UPDATE " . - PREFIX . - "members SET lastlogin=? WHERE nickname=?;", - ); - $stmt->execute([time(), $U["nickname"]]); - return true; - } else { - $stmt = $db->prepare( - "UPDATE " . - PREFIX . - "members SET loginfails=? WHERE nickname=?;", - ); - $stmt->execute([$temp["loginfails"] + 1, $temp["nickname"]]); - send_error( - _("This nickname is a registered member.") . - "<br>" . - _("Wrong Password!"), - ); - } - } - return false; -} - -function delete_account(): void -{ - global $U, $db; - if ($U["status"] < 8) { - $stmt = $db->prepare( - "UPDATE " . - PREFIX . - "sessions SET status=1, incognito=0 WHERE nickname=?;", - ); - $stmt->execute([$U["nickname"]]); - $stmt = $db->prepare( - "DELETE FROM " . PREFIX . "members WHERE nickname=?;", - ); - $stmt->execute([$U["nickname"]]); - $stmt = $db->prepare( - "DELETE FROM " . PREFIX . "inbox WHERE recipient=?;", - ); - $stmt->execute([$U["nickname"]]); - $stmt = $db->prepare( - "DELETE FROM " . - PREFIX . - "notes WHERE (type=2 OR type=3) AND editedby=?;", - ); - $stmt->execute([$U["nickname"]]); - $U["status"] = 1; - } -} - -function register_guest(int $status, string $nick): string -{ - global $U, $db; - $stmt = $db->prepare( - "SELECT style FROM " . PREFIX . "members WHERE nickname=?", - ); - $stmt->execute([$nick]); - if ($tmp = $stmt->fetch(PDO::FETCH_NUM)) { - return sprintf( - _("%s is already registered."), - style_this(htmlspecialchars($nick), $tmp[0]), - ); - } - $stmt = $db->prepare( - "SELECT * FROM " . PREFIX . "sessions WHERE nickname=? AND status=1;", - ); - $stmt->execute([$nick]); - if ($reg = $stmt->fetch(PDO::FETCH_ASSOC)) { - $reg["status"] = $status; - $stmt = $db->prepare( - "UPDATE " . PREFIX . "sessions SET status=? WHERE session=?;", - ); - $stmt->execute([$reg["status"], $reg["session"]]); - } else { - return sprintf(_("Can't register %s"), htmlspecialchars($nick)); - } - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "members (nickname, passhash, status, refresh, bgcolour, regedby, timestamps, embed, style, incognito, nocache, tz, eninbox, sortupdown, hidechatters, nocache_old) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", - ); - $stmt->execute([ - $reg["nickname"], - $reg["passhash"], - $reg["status"], - $reg["refresh"], - $reg["bgcolour"], - $U["nickname"], - $reg["timestamps"], - $reg["embed"], - $reg["style"], - $reg["incognito"], - $reg["nocache"], - $reg["tz"], - $reg["eninbox"], - $reg["sortupdown"], - $reg["hidechatters"], - $reg["nocache_old"], - ]); - if ($reg["status"] == 3) { - add_system_message( - sprintf( - get_setting("msgmemreg"), - style_this(htmlspecialchars($reg["nickname"]), $reg["style"]), - ), - $U["nickname"], - ); - } else { - add_system_message( - sprintf( - get_setting("msgsureg"), - style_this(htmlspecialchars($reg["nickname"]), $reg["style"]), - ), - $U["nickname"], - ); - } - return sprintf( - _("%s successfully registered."), - style_this(htmlspecialchars($reg["nickname"]), $reg["style"]), - ); -} - -function register_new(string $nick, string $pass): string -{ - global $U, $db; - $nick = preg_replace("/\s/", "", $nick); - if (empty($nick)) { - return ""; - } - $stmt = $db->prepare( - "SELECT null FROM " . PREFIX . "sessions WHERE nickname=?", - ); - $stmt->execute([$nick]); - if ($stmt->fetch(PDO::FETCH_NUM)) { - return sprintf(_("Can't register %s"), htmlspecialchars($nick)); - } - if (!valid_nick($nick)) { - return sprintf( - _( - 'Invalid nickname (%1$d characters maximum and has to match the regular expression "%2$s")', - ), - get_setting("maxname"), - get_setting("nickregex"), - ); - } - if (!valid_pass($pass)) { - return sprintf( - _( - 'Invalid password (At least %1$d characters and has to match the regular expression "%2$s")', - ), - get_setting("minpass"), - get_setting("passregex"), - ); - } - $stmt = $db->prepare( - "SELECT null FROM " . PREFIX . "members WHERE nickname=?", - ); - $stmt->execute([$nick]); - if ($stmt->fetch(PDO::FETCH_NUM)) { - return sprintf(_("%s is already registered."), htmlspecialchars($nick)); - } - $reg = [ - "nickname" => $nick, - "passhash" => password_hash($pass, PASSWORD_DEFAULT), - "status" => 3, - "refresh" => get_setting("defaultrefresh"), - "bgcolour" => get_setting("colbg"), - "regedby" => $U["nickname"], - "timestamps" => get_setting("timestamps"), - "style" => "color:#" . get_setting("coltxt") . ";", - "embed" => 1, - "incognito" => 0, - "nocache" => 0, - "nocache_old" => 1, - "tz" => get_setting("defaulttz"), - "eninbox" => 0, - "sortupdown" => get_setting("sortupdown"), - "hidechatters" => get_setting("hidechatters"), - ]; - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "members (nickname, passhash, status, refresh, bgcolour, regedby, timestamps, style, embed, incognito, nocache, tz, eninbox, sortupdown, hidechatters, nocache_old) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", - ); - $stmt->execute([ - $reg["nickname"], - $reg["passhash"], - $reg["status"], - $reg["refresh"], - $reg["bgcolour"], - $reg["regedby"], - $reg["timestamps"], - $reg["style"], - $reg["embed"], - $reg["incognito"], - $reg["nocache"], - $reg["tz"], - $reg["eninbox"], - $reg["sortupdown"], - $reg["hidechatters"], - $reg["nocache_old"], - ]); - return sprintf( - _("%s successfully registered."), - htmlspecialchars($reg["nickname"]), - ); -} - -function change_status(string $nick, string $status): string -{ - global $U, $db; - if (empty($nick)) { - return ""; - } elseif ( - $U["status"] <= $status || - !preg_match('/^[023567\-]$/', $status) - ) { - return sprintf(_("Can't change status of %s"), htmlspecialchars($nick)); - } - $stmt = $db->prepare( - "SELECT incognito, style FROM " . - PREFIX . - "members WHERE nickname=? AND status<?;", - ); - $stmt->execute([$nick, $U["status"]]); - if (!($old = $stmt->fetch(PDO::FETCH_NUM))) { - return sprintf(_("Can't change status of %s"), htmlspecialchars($nick)); - } - if ($status === "-") { - $stmt = $db->prepare( - "DELETE FROM " . PREFIX . "members WHERE nickname=?;", - ); - $stmt->execute([$nick]); - $stmt = $db->prepare( - "UPDATE " . - PREFIX . - "sessions SET status=1, incognito=0 WHERE nickname=?;", - ); - $stmt->execute([$nick]); - $stmt = $db->prepare( - "DELETE FROM " . PREFIX . "inbox WHERE recipient=?;", - ); - $stmt->execute([$nick]); - $stmt = $db->prepare( - "DELETE FROM " . - PREFIX . - "notes WHERE (type=2 OR type=3) AND editedby=?;", - ); - $stmt->execute([$nick]); - return sprintf( - _("%s successfully deleted from database."), - style_this(htmlspecialchars($nick), $old[1]), - ); - } else { - if ($status < 5) { - $old[0] = 0; - } - $stmt = $db->prepare( - "UPDATE " . - PREFIX . - "members SET status=?, incognito=? WHERE nickname=?;", - ); - $stmt->execute([$status, $old[0], $nick]); - $stmt = $db->prepare( - "UPDATE " . - PREFIX . - "sessions SET status=?, incognito=? WHERE nickname=?;", - ); - $stmt->execute([$status, $old[0], $nick]); - return sprintf( - _("Status of %s successfully changed."), - style_this(htmlspecialchars($nick), $old[1]), - ); - } -} - -function passreset(string $nick, string $pass): string -{ - global $U, $db; - if (empty($nick)) { - return ""; - } - $stmt = $db->prepare( - "SELECT null FROM " . PREFIX . "members WHERE nickname=? AND status<?;", - ); - $stmt->execute([$nick, $U["status"]]); - if ($stmt->fetch(PDO::FETCH_ASSOC)) { - $passhash = password_hash($pass, PASSWORD_DEFAULT); - $stmt = $db->prepare( - "UPDATE " . PREFIX . "members SET passhash=? WHERE nickname=?;", - ); - $stmt->execute([$passhash, $nick]); - $stmt = $db->prepare( - "UPDATE " . PREFIX . "sessions SET passhash=? WHERE nickname=?;", - ); - $stmt->execute([$passhash, $nick]); - return sprintf( - _("Successfully reset password for %s"), - htmlspecialchars($nick), - ); - } else { - return sprintf( - _("Can't reset password for %s"), - htmlspecialchars($nick), - ); - } -} - -function amend_profile(): void -{ - global $U; - if (isset($_POST["refresh"])) { - $U["refresh"] = $_POST["refresh"]; - } - if ($U["refresh"] < 5) { - $U["refresh"] = 5; - } elseif ($U["refresh"] > 150) { - $U["refresh"] = 150; - } - if (preg_match('/^#([a-f0-9]{6})$/i', $_POST["colour"], $match)) { - $colour = $match[1]; - } else { - preg_match("/#([0-9a-f]{6})/i", $U["style"], $matches); - $colour = $matches[1]; - } - if (preg_match('/^#([a-f0-9]{6})$/i', $_POST["bgcolour"], $match)) { - $U["bgcolour"] = $match[1]; - } - $U["style"] = "color:#$colour;"; - if ($U["status"] >= 3) { - $F = load_fonts(); - if (isset($F[$_POST["font"]])) { - $U["style"] .= $F[$_POST["font"]]; - } - if (isset($_POST["small"])) { - $U["style"] .= "font-size:smaller;"; - } - if (isset($_POST["italic"])) { - $U["style"] .= "font-style:italic;"; - } - if (isset($_POST["bold"])) { - $U["style"] .= "font-weight:bold;"; - } - } - if ( - $U["status"] >= 5 && - isset($_POST["incognito"]) && - get_setting("incognito") - ) { - $U["incognito"] = 1; - } else { - $U["incognito"] = 0; - } - if (isset($_POST["tz"])) { - $tzs = timezone_identifiers_list(); - if (in_array($_POST["tz"], $tzs)) { - $U["tz"] = $_POST["tz"]; - } - } - if ( - isset($_POST["eninbox"]) && - $_POST["eninbox"] >= 0 && - $_POST["eninbox"] <= 5 - ) { - $U["eninbox"] = $_POST["eninbox"]; - } - $bool_settings = [ - "timestamps", - "embed", - "nocache", - "sortupdown", - "hidechatters", - ]; - foreach ($bool_settings as $setting) { - if (isset($_POST[$setting])) { - $U[$setting] = 1; - } else { - $U[$setting] = 0; - } - } -} - -function save_profile(): string -{ - global $U, $db; - amend_profile(); - $stmt = $db->prepare( - "UPDATE " . - PREFIX . - "sessions SET refresh=?, style=?, bgcolour=?, timestamps=?, embed=?, incognito=?, nocache=?, tz=?, eninbox=?, sortupdown=?, hidechatters=? WHERE session=?;", - ); - $stmt->execute([ - $U["refresh"], - $U["style"], - $U["bgcolour"], - $U["timestamps"], - $U["embed"], - $U["incognito"], - $U["nocache"], - $U["tz"], - $U["eninbox"], - $U["sortupdown"], - $U["hidechatters"], - $U["session"], - ]); - if ($U["status"] >= 2) { - $stmt = $db->prepare( - "UPDATE " . - PREFIX . - "members SET refresh=?, bgcolour=?, timestamps=?, embed=?, incognito=?, style=?, nocache=?, tz=?, eninbox=?, sortupdown=?, hidechatters=? WHERE nickname=?;", - ); - $stmt->execute([ - $U["refresh"], - $U["bgcolour"], - $U["timestamps"], - $U["embed"], - $U["incognito"], - $U["style"], - $U["nocache"], - $U["tz"], - $U["eninbox"], - $U["sortupdown"], - $U["hidechatters"], - $U["nickname"], - ]); - } - if (!empty($_POST["unignore"])) { - $stmt = $db->prepare( - "DELETE FROM " . PREFIX . "ignored WHERE ign=? AND ignby=?;", - ); - $stmt->execute([$_POST["unignore"], $U["nickname"]]); - } - if (!empty($_POST["ignore"])) { - $stmt = $db->prepare( - "SELECT null FROM " . - PREFIX . - "messages WHERE poster=? AND poster NOT IN (SELECT ign FROM " . - PREFIX . - "ignored WHERE ignby=?);", - ); - $stmt->execute([$_POST["ignore"], $U["nickname"]]); - if ( - $U["nickname"] !== $_POST["ignore"] && - $stmt->fetch(PDO::FETCH_NUM) - ) { - $stmt = $db->prepare( - "INSERT INTO " . PREFIX . "ignored (ign, ignby) VALUES (?, ?);", - ); - $stmt->execute([$_POST["ignore"], $U["nickname"]]); - } - } - if ($U["status"] > 1 && !empty($_POST["newpass"])) { - if (!valid_pass($_POST["newpass"])) { - return sprintf( - _( - 'Invalid password (At least %1$d characters and has to match the regular expression "%2$s")', - ), - get_setting("minpass"), - get_setting("passregex"), - ); - } - if (!isset($_POST["oldpass"])) { - $_POST["oldpass"] = ""; - } - if (!isset($_POST["confirmpass"])) { - $_POST["confirmpass"] = ""; - } - if ($_POST["newpass"] !== $_POST["confirmpass"]) { - return _("Password confirmation does not match!"); - } else { - $U["newhash"] = password_hash($_POST["newpass"], PASSWORD_DEFAULT); - } - if (!password_verify($_POST["oldpass"], $U["passhash"])) { - return _("Wrong Password!"); - } - $U["passhash"] = $U["newhash"]; - $stmt = $db->prepare( - "UPDATE " . PREFIX . "sessions SET passhash=? WHERE session=?;", - ); - $stmt->execute([$U["passhash"], $U["session"]]); - $stmt = $db->prepare( - "UPDATE " . PREFIX . "members SET passhash=? WHERE nickname=?;", - ); - $stmt->execute([$U["passhash"], $U["nickname"]]); - } - if ($U["status"] > 1 && !empty($_POST["newnickname"])) { - $msg = set_new_nickname(); - if ($msg !== "") { - return $msg; - } - } - return _("Your profile has successfully been saved."); -} - -function set_new_nickname(): string -{ - global $U, $db; - $_POST["newnickname"] = preg_replace("/\s/", "", $_POST["newnickname"]); - if (!valid_nick($_POST["newnickname"])) { - return sprintf( - _( - 'Invalid nickname (%1$d characters maximum and has to match the regular expression "%2$s")', - ), - get_setting("maxname"), - get_setting("nickregex"), - ); - } - $stmt = $db->prepare( - "SELECT id FROM " . - PREFIX . - "sessions WHERE nickname=? UNION SELECT id FROM " . - PREFIX . - "members WHERE nickname=?;", - ); - $stmt->execute([$_POST["newnickname"], $_POST["newnickname"]]); - if ($stmt->fetch(PDO::FETCH_NUM)) { - return _("Nickname is already taken"); - } else { - // Make sure members can not read private messages of previous guests with the same name - $stmt = $db->prepare( - "UPDATE " . - PREFIX . - 'messages SET poster = "" WHERE poster = ? AND poststatus = 9;', - ); - $stmt->execute([$_POST["newnickname"]]); - $stmt = $db->prepare( - "UPDATE " . - PREFIX . - 'messages SET recipient = "" WHERE recipient = ? AND poststatus = 9;', - ); - $stmt->execute([$_POST["newnickname"]]); - // change names in all tables - $stmt = $db->prepare( - "UPDATE " . PREFIX . "members SET nickname=? WHERE nickname=?;", - ); - $stmt->execute([$_POST["newnickname"], $U["nickname"]]); - $stmt = $db->prepare( - "UPDATE " . PREFIX . "sessions SET nickname=? WHERE nickname=?;", - ); - $stmt->execute([$_POST["newnickname"], $U["nickname"]]); - $stmt = $db->prepare( - "UPDATE " . PREFIX . "messages SET poster=? WHERE poster=?;", - ); - $stmt->execute([$_POST["newnickname"], $U["nickname"]]); - $stmt = $db->prepare( - "UPDATE " . PREFIX . "messages SET recipient=? WHERE recipient=?;", - ); - $stmt->execute([$_POST["newnickname"], $U["nickname"]]); - $stmt = $db->prepare( - "UPDATE " . PREFIX . "ignored SET ignby=? WHERE ignby=?;", - ); - $stmt->execute([$_POST["newnickname"], $U["nickname"]]); - $stmt = $db->prepare( - "UPDATE " . PREFIX . "ignored SET ign=? WHERE ign=?;", - ); - $stmt->execute([$_POST["newnickname"], $U["nickname"]]); - $stmt = $db->prepare( - "UPDATE " . PREFIX . "inbox SET poster=? WHERE poster=?;", - ); - $stmt->execute([$_POST["newnickname"], $U["nickname"]]); - $stmt = $db->prepare( - "UPDATE " . PREFIX . "notes SET editedby=? WHERE editedby=?;", - ); - $stmt->execute([$_POST["newnickname"], $U["nickname"]]); - $U["nickname"] = $_POST["newnickname"]; - } - return ""; -} - -//sets default settings for guests -function add_user_defaults(string $password): void -{ - global $U; - $U["refresh"] = get_setting("defaultrefresh"); - $U["bgcolour"] = get_setting("colbg"); - if ( - !isset($_POST["colour"]) || - !preg_match('/^[a-f0-9]{6}$/i', $_POST["colour"]) || - abs(greyval($_POST["colour"]) - greyval(get_setting("colbg"))) < 75 - ) { - do { - $colour = sprintf("%06X", mt_rand(0, 16581375)); - } while (abs(greyval($colour) - greyval(get_setting("colbg"))) < 75); - } else { - $colour = $_POST["colour"]; - } - $U["style"] = "color:#$colour;"; - $U["timestamps"] = get_setting("timestamps"); - $U["embed"] = 1; - $U["incognito"] = 0; - $U["status"] = 1; - $U["nocache"] = get_setting("sortupdown"); - if ($U["nocache"]) { - $U["nocache_old"] = 0; - } else { - $U["nocache_old"] = 1; - } - $U["loginfails"] = 0; - $U["tz"] = get_setting("defaulttz"); - $U["eninbox"] = 0; - $U["sortupdown"] = get_setting("sortupdown"); - $U["hidechatters"] = get_setting("hidechatters"); - $U["passhash"] = password_hash($password, PASSWORD_DEFAULT); - $U["entry"] = $U["lastpost"] = time(); - $U["exiting"] = 0; -} - -// message handling -function validate_input(): string -{ - global $U, $db; - $inbox = false; - $maxmessage = get_setting("maxmessage"); - $message = mb_substr($_POST["message"], 0, $maxmessage); - $rejected = mb_substr($_POST["message"], $maxmessage); - if (!isset($_POST["postid"])) { - // auto-kick spammers not setting a postid - kick_chatter([$U["nickname"]], "", false); - } - if ($U["postid"] !== $_POST["postid"] || time() - $U["lastpost"] <= 1) { - // reject bogus messages - $rejected = $_POST["message"]; - $message = ""; - } - if (!empty($rejected)) { - $rejected = trim($rejected); - $rejected = htmlspecialchars($rejected); - } - $message = htmlspecialchars($message); - $message = preg_replace("/(\r?\n|\r\n?)/u", "<br>", $message); - if (isset($_POST["multi"])) { - $message = preg_replace("/\s*<br>/u", "<br>", $message); - $message = preg_replace("/<br>(<br>)+/u", "<br><br>", $message); - $message = preg_replace('/<br><br>\s*$/u', "<br>", $message); - $message = preg_replace('/^<br>\s*$/u', "", $message); - } else { - $message = str_replace("<br>", " ", $message); - } - $message = trim($message); - $message = preg_replace("/\s+/u", " ", $message); - $recipient = ""; - if ($_POST["sendto"] === "s *") { - $poststatus = 1; - $displaysend = sprintf( - get_setting("msgsendall"), - style_this(htmlspecialchars($U["nickname"]), $U["style"]), - ); - } elseif ($_POST["sendto"] === "s ?" && $U["status"] >= 3) { - $poststatus = 3; - $displaysend = sprintf( - get_setting("msgsendmem"), - style_this(htmlspecialchars($U["nickname"]), $U["style"]), - ); - } elseif ($_POST["sendto"] === "s %" && $U["status"] >= 5) { - $poststatus = 5; - $displaysend = sprintf( - get_setting("msgsendmod"), - style_this(htmlspecialchars($U["nickname"]), $U["style"]), - ); - } elseif ($_POST["sendto"] === "s _" && $U["status"] >= 6) { - $poststatus = 6; - $displaysend = sprintf( - get_setting("msgsendadm"), - style_this(htmlspecialchars($U["nickname"]), $U["style"]), - ); - } elseif ($_POST["sendto"] === $U["nickname"]) { - // message to yourself? - return ""; - } else { - // known nick in room? - if (get_setting("disablepm")) { - //PMs disabled - return ""; - } - if ($U["status"] == 1 && get_setting("noguestpm")) { - // Guest user disabled from sending PMs - return ""; - } - $stmt = $db->prepare( - "SELECT null FROM " . - PREFIX . - "ignored WHERE (ignby=? AND ign=?) OR (ign=? AND ignby=?);", - ); - $stmt->execute([ - $_POST["sendto"], - $U["nickname"], - $_POST["sendto"], - $U["nickname"], - ]); - if ($stmt->fetch(PDO::FETCH_NUM)) { - //ignored - return ""; - } - $stmt = $db->prepare( - "SELECT s.style, 0 AS inbox FROM " . - PREFIX . - "sessions AS s LEFT JOIN " . - PREFIX . - "members AS m ON (m.nickname=s.nickname) WHERE s.nickname=? AND (s.incognito=0 OR (m.eninbox!=0 AND m.eninbox<=?));", - ); - $stmt->execute([$_POST["sendto"], $U["status"]]); - if (!($tmp = $stmt->fetch(PDO::FETCH_ASSOC))) { - $stmt = $db->prepare( - "SELECT style, 1 AS inbox FROM " . - PREFIX . - "members WHERE nickname=? AND eninbox!=0 AND eninbox<=?;", - ); - $stmt->execute([$_POST["sendto"], $U["status"]]); - if (!($tmp = $stmt->fetch(PDO::FETCH_ASSOC))) { - //nickname left or disabled offline inbox for us - return ""; - } - } - $recipient = $_POST["sendto"]; - $poststatus = 9; - $displaysend = sprintf( - get_setting("msgsendprv"), - style_this(htmlspecialchars($U["nickname"]), $U["style"]), - style_this(htmlspecialchars($recipient), $tmp["style"]), - ); - $inbox = $tmp["inbox"]; - } - if ($poststatus !== 9 && preg_match("~^/me~iu", $message)) { - $displaysend = style_this( - htmlspecialchars("$U[nickname] "), - $U["style"], - ); - $message = preg_replace("~^/me\s?~iu", "", $message); - } - $message = apply_filter($message, $poststatus, $U["nickname"]); - $message = create_hotlinks($message); - $message = apply_linkfilter($message); - if ( - isset($_FILES["file"]) && - get_setting("enfileupload") > 0 && - get_setting("enfileupload") <= $U["status"] - ) { - if ( - $_FILES["file"]["error"] === UPLOAD_ERR_OK && - $_FILES["file"]["size"] <= 1024 * get_setting("maxuploadsize") - ) { - $hash = sha1_file($_FILES["file"]["tmp_name"]); - $name = htmlspecialchars($_FILES["file"]["name"]); - $message = sprintf( - get_setting("msgattache"), - "<a class=\"attachement\" href=\"$_SERVER[SCRIPT_NAME]?action=download&amp;id=$hash\" target=\"_blank\">$name</a>", - $message, - ); - } - } - if ( - add_message( - $message, - $recipient, - $U["nickname"], - (int) $U["status"], - $poststatus, - $displaysend, - $U["style"], - ) - ) { - $U["lastpost"] = time(); - try { - $U["postid"] = bin2hex(random_bytes(3)); - } catch (Exception $e) { - $U["postid"] = substr(time(), -6); - } - $stmt = $db->prepare( - "UPDATE " . - PREFIX . - "sessions SET lastpost=?, postid=? WHERE session=?;", - ); - $stmt->execute([$U["lastpost"], $U["postid"], $U["session"]]); - $stmt = $db->prepare( - "SELECT id FROM " . - PREFIX . - "messages WHERE poster=? ORDER BY id DESC LIMIT 1;", - ); - $stmt->execute([$U["nickname"]]); - $id = $stmt->fetch(PDO::FETCH_NUM); - if ($inbox && $id) { - $newmessage = [ - "postdate" => time(), - "poster" => $U["nickname"], - "recipient" => $recipient, - "text" => - "<span class=\"usermsg\">$displaysend" . - style_this($message, $U["style"]) . - "</span>", - ]; - if (MSGENCRYPTED) { - try { - $newmessage["text"] = base64_encode( - sodium_crypto_aead_aes256gcm_encrypt( - $newmessage["text"], - "", - AES_IV, - ENCRYPTKEY, - ), - ); - } catch (SodiumException $e) { - send_error($e->getMessage()); - } - } - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "inbox (postdate, postid, poster, recipient, text) VALUES(?, ?, ?, ?, ?)", - ); - $stmt->execute([ - $newmessage["postdate"], - $id[0], - $newmessage["poster"], - $newmessage["recipient"], - $newmessage["text"], - ]); - } - if (isset($hash) && $id) { - if (function_exists("mime_content_type")) { - $type = mime_content_type($_FILES["file"]["tmp_name"]); - } elseif ( - !empty($_FILES["file"]["type"]) && - preg_match('~^[a-z0-9/\-.+]*$~i', $_FILES["file"]["type"]) - ) { - $type = $_FILES["file"]["type"]; - } else { - $type = "application/octet-stream"; - } - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "files (postid, hash, filename, type, data) VALUES (?, ?, ?, ?, ?);", - ); - $stmt->execute([ - $id[0], - $hash, - str_replace('"', '\"', $_FILES["file"]["name"]), - $type, - base64_encode(file_get_contents($_FILES["file"]["tmp_name"])), - ]); - unlink($_FILES["file"]["tmp_name"]); - } - } - return $rejected; -} - -function apply_filter( - string $message, - int $poststatus, - string $nickname, -): string { - global $U, $session; - $message = str_replace("<br>", "\n", $message); - $message = apply_mention($message); - $filters = get_filters(); - foreach ($filters as $filter) { - if ($poststatus !== 9 || !$filter["allowinpm"]) { - if ($filter["cs"]) { - $message = preg_replace( - "/$filter[match]/u", - $filter["replace"], - $message, - -1, - $count, - ); - } else { - $message = preg_replace( - "/$filter[match]/iu", - $filter["replace"], - $message, - -1, - $count, - ); - } - } - if ( - isset($count) && - $count > 0 && - $filter["kick"] && - ($U["status"] < 5 || get_setting("filtermodkick")) - ) { - kick_chatter([$nickname], $filter["replace"], false); - setcookie(COOKIENAME, false); - $session = ""; - send_error(_("You have been kicked!") . "<br>$filter[replace]"); - } - } - $message = str_replace("\n", "<br>", $message); - return $message; -} - -function apply_linkfilter(string $message): string -{ - $filters = get_linkfilters(); - foreach ($filters as $filter) { - $message = preg_replace_callback( - "/<a href=\"([^\"]+)\" target=\"_blank\" rel=\"noreferrer noopener\">([^<]*)<\/a>/iu", - function ($matched) use (&$filter) { - return "<a href=\"$matched[1]\" target=\"_blank\" rel=\"noreferrer noopener\">" . - preg_replace( - "/$filter[match]/iu", - $filter["replace"], - $matched[2], - ) . - "</a>"; - }, - $message, - ); - } - $redirect = get_setting("redirect"); - if (get_setting("imgembed")) { - $message = preg_replace_callback( - '/\[img]\s?<a href="([^"]+)" target="_blank" rel="noreferrer noopener">([^<]*)<\/a>/iu', - function ($matched) { - return str_ireplace( - "[/img]", - "", - "<br><a href=\"$matched[1]\" target=\"_blank\" rel=\"noreferrer noopener\"><img src=\"$matched[1]\" rel=\"noreferrer\" loading=\"lazy\"></a><br>", - ); - }, - $message, - ); - } - if (empty($redirect)) { - $redirect = "$_SERVER[SCRIPT_NAME]?action=redirect&amp;url="; - } - if (get_setting("forceredirect")) { - $message = preg_replace_callback( - '/<a href="([^"]+)" target="_blank" rel="noreferrer noopener">([^<]*)<\/a>/u', - function ($matched) use ($redirect) { - return "<a href=\"$redirect" . - rawurlencode($matched[1]) . - "\" target=\"_blank\" rel=\"noreferrer noopener\">$matched[2]</a>"; - }, - $message, - ); - } elseif ( - preg_match_all( - '/<a href="([^"]+)" target="_blank" rel="noreferrer noopener">([^<]*)<\/a>/u', - $message, - $matches, - ) - ) { - foreach ($matches[1] as $match) { - if (!preg_match("~^http(s)?://~u", $match)) { - $message = preg_replace_callback( - '/<a href="(' . - preg_quote($match, "/") . - ')\" target=\"_blank\" rel=\"noreferrer noopener\">([^<]*)<\/a>/u', - function ($matched) use ($redirect) { - return "<a href=\"$redirect" . - rawurlencode($matched[1]) . - "\" target=\"_blank\" rel=\"noreferrer noopener\">$matched[2]</a>"; - }, - $message, - ); - } - } - } - return $message; -} - -function create_hotlinks(string $message): string -{ - //Make hotlinks for URLs, redirect through dereferrer script to prevent session leakage - // 1. all explicit schemes with whatever xxx://yyyyyyy - $message = preg_replace( - '~(^|[^\w"])(\w+://[^\s<>]+)~iu', - "$1<<$2>>", - $message, - ); - // 2. valid URLs without scheme: - $message = preg_replace( - "~((?:[^\s<>]*:[^\s<>]*@)?[a-z0-9\-]+(?:\.[a-z0-9\-]+)+(?::\d*)?/[^\s<>]*)(?![^<>]*>)~iu", - "<<$1>>", - $message, - ); // server/path given - $message = preg_replace( - "~((?:[^\s<>]*:[^\s<>]*@)?[a-z0-9\-]+(?:\.[a-z0-9\-]+)+:\d+)(?![^<>]*>)~iu", - "<<$1>>", - $message, - ); // server:port given - $message = preg_replace( - "~([^\s<>]*:[^\s<>]*@[a-z0-9\-]+(?:\.[a-z0-9\-]+)+(?::\d+)?)(?![^<>]*>)~iu", - "<<$1>>", - $message, - ); // au:th@server given - // 3. likely servers without any hints but not filenames like *.rar zip exe etc. - $message = preg_replace( - "~((?:[a-z0-9\-]+\.)*(?:[a-z2-7]{55}d|[a-z2-7]{16})\.onion)(?![^<>]*>)~iu", - "<<$1>>", - $message, - ); // *.onion - $message = preg_replace( - '~([a-z0-9\-]+(?:\.[a-z0-9\-]+)+(?:\.(?!rar|zip|exe|gz|7z|bat|doc)[a-z]{2,}))(?=[^a-z0-9\-.]|$)(?![^<>]*>)~iu', - "<<$1>>", - $message, - ); // xxx.yyy.zzz - // Convert every <<....>> into proper links: - $message = preg_replace_callback( - "/<<([^<>]+)>>/u", - function ($matches) { - if (strpos($matches[1], "://") === false) { - return "<a href=\"http://$matches[1]\" target=\"_blank\" rel=\"noreferrer noopener\">$matches[1]</a>"; - } else { - return "<a href=\"$matches[1]\" target=\"_blank\" rel=\"noreferrer noopener\">$matches[1]</a>"; - } - }, - $message, - ); - return $message; -} - -function apply_mention(string $message): string -{ - return preg_replace_callback( - "/@([^\s]+)/iu", - function ($matched) { - global $db; - $nick = htmlspecialchars_decode($matched[1]); - $rest = ""; - for ($i = 0; $i <= 3; ++$i) { - //match case-sensitive present nicknames - $stmt = $db->prepare( - "SELECT style FROM " . - PREFIX . - "sessions WHERE nickname=?;", - ); - $stmt->execute([$nick]); - if ($tmp = $stmt->fetch(PDO::FETCH_NUM)) { - return style_this(htmlspecialchars("@$nick"), $tmp[0]) . - $rest; - } - //match case-insensitive present nicknames - $stmt = $db->prepare( - "SELECT style FROM " . - PREFIX . - "sessions WHERE LOWER(nickname)=LOWER(?);", - ); - $stmt->execute([$nick]); - if ($tmp = $stmt->fetch(PDO::FETCH_NUM)) { - return style_this(htmlspecialchars("@$nick"), $tmp[0]) . - $rest; - } - //match case-sensitive members - $stmt = $db->prepare( - "SELECT style FROM " . PREFIX . "members WHERE nickname=?;", - ); - $stmt->execute([$nick]); - if ($tmp = $stmt->fetch(PDO::FETCH_NUM)) { - return style_this(htmlspecialchars("@$nick"), $tmp[0]) . - $rest; - } - //match case-insensitive members - $stmt = $db->prepare( - "SELECT style FROM " . - PREFIX . - "members WHERE LOWER(nickname)=LOWER(?);", - ); - $stmt->execute([$nick]); - if ($tmp = $stmt->fetch(PDO::FETCH_NUM)) { - return style_this(htmlspecialchars("@$nick"), $tmp[0]) . - $rest; - } - if (strlen($nick) === 1) { - break; - } - $rest = mb_substr($nick, -1) . $rest; - $nick = mb_substr($nick, 0, -1); - } - return $matched[0]; - }, - $message, - ); -} - -function add_message( - string $message, - string $recipient, - string $poster, - int $delstatus, - int $poststatus, - string $displaysend, - string $style, -): bool { - global $db; - if ($message === "") { - return false; - } - $newmessage = [ - "postdate" => time(), - "poststatus" => $poststatus, - "poster" => $poster, - "recipient" => $recipient, - "text" => - "<span class=\"usermsg\">$displaysend" . - style_this($message, $style) . - "</span>", - "delstatus" => $delstatus, - ]; - //prevent posting the same message twice, if no other message was posted in-between. - $stmt = $db->prepare( - "SELECT id FROM " . - PREFIX . - "messages WHERE poststatus=? AND poster=? AND recipient=? AND text=? AND id IN (SELECT * FROM (SELECT id FROM " . - PREFIX . - "messages ORDER BY id DESC LIMIT 1) AS t);", - ); - $stmt->execute([ - $newmessage["poststatus"], - $newmessage["poster"], - $newmessage["recipient"], - $newmessage["text"], - ]); - if ($stmt->fetch(PDO::FETCH_NUM)) { - return false; - } - write_message($newmessage); - return true; -} - -function add_system_message(string $mes, string $doer): void -{ - if ($mes === "") { - return; - } - if ($doer === "" || !get_setting("namedoers")) { - $sysmessage = [ - "postdate" => time(), - "poststatus" => 4, - "poster" => "", - "recipient" => "", - "text" => "$mes", - "delstatus" => 4, - ]; - } else { - $sysmessage = [ - "postdate" => time(), - "poststatus" => 4, - "poster" => "", - "recipient" => "", - "text" => "$mes ($doer)", - "delstatus" => 4, - ]; - } - write_message($sysmessage); -} -function add_system_pm_message( - string $mes, - string $recipient, - string $doer, -): void { - if ($mes === "") { - return; - } - if ($doer === "" || !get_setting("namedoers")) { - $sysmessage = [ - "postdate" => time(), - "poststatus" => 9, - "poster" => "System", - "recipient" => $recipient, - "text" => "$mes", - "delstatus" => 4, - ]; - } else { - $sysmessage = [ - "postdate" => time(), - "poststatus" => 9, - "poster" => "System", - "recipient" => $recipient, - "text" => "$mes ($doer)", - "delstatus" => 4, - ]; - } - write_message($sysmessage); -} -function write_message(array $message): void -{ - global $db; - if (MSGENCRYPTED) { - try { - $message["text"] = base64_encode( - sodium_crypto_aead_aes256gcm_encrypt( - $message["text"], - "", - AES_IV, - ENCRYPTKEY, - ), - ); - } catch (SodiumException $e) { - send_error($e->getMessage()); - } - } - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "messages (postdate, poststatus, poster, recipient, text, delstatus) VALUES (?, ?, ?, ?, ?, ?);", - ); - $stmt->execute([ - $message["postdate"], - $message["poststatus"], - $message["poster"], - $message["recipient"], - $message["text"], - $message["delstatus"], - ]); - if ($message["poststatus"] < 9 && get_setting("sendmail")) { - $subject = "New Chat message"; - $headers = - "From: " . - get_setting("mailsender") . - "\r\nX-Mailer: PHP/" . - phpversion() . - "\r\nContent-Type: text/html; charset=UTF-8\r\n"; - $body = - '<html><body style="background-color:#' . - get_setting("colbg") . - ";color:#" . - get_setting("coltxt") . - ";\">$message[text]</body></html>"; - mail(get_setting("mailreceiver"), $subject, $body, $headers); - } -} - -function clean_room(): void -{ - global $U, $db; - $db->query("DELETE FROM " . PREFIX . "messages;"); - add_system_message( - sprintf(get_setting("msgclean"), get_setting("chatname")), - $U["nickname"], - ); -} - -function clean_selected(int $status, string $nick): void -{ - global $db; - if (isset($_POST["mid"])) { - $stmt = $db->prepare( - "DELETE FROM " . - PREFIX . - "messages WHERE id=? AND (poster=? OR recipient=? OR (poststatus<? AND delstatus<?));", - ); - foreach ($_POST["mid"] as $mid) { - $stmt->execute([$mid, $nick, $nick, $status, $status]); - } - } -} - -function clean_inbox_selected(): void -{ - global $U, $db; - if (isset($_POST["mid"])) { - $stmt = $db->prepare( - "DELETE FROM " . PREFIX . "inbox WHERE id=? AND recipient=?;", - ); - foreach ($_POST["mid"] as $mid) { - $stmt->execute([$mid, $U["nickname"]]); - } - } -} - -function del_all_messages(string $nick, int $entry): void -{ - global $db, $U; - $globally = (bool) get_setting("postbox_delete_globally"); - if ($globally && $U["status"] > 4) { - $stmt = $db->prepare("DELETE FROM " . PREFIX . "messages;"); - $stmt->execute(); - } else { - if ($nick === "") { - $nick = $U["nickname"]; - } - $stmt = $db->prepare( - "DELETE FROM " . - PREFIX . - "messages WHERE poster=? AND postdate>=?;", - ); - $stmt->execute([$nick, $entry]); - $stmt = $db->prepare( - "DELETE FROM " . PREFIX . "inbox WHERE poster=? AND postdate>=?;", - ); - $stmt->execute([$nick, $entry]); - } -} - -function del_last_message(): void -{ - global $U, $db; - if ($U["status"] > 1) { - $entry = 0; - } else { - $entry = $U["entry"]; - } - $globally = (bool) get_setting("postbox_delete_globally"); - if ($globally && $U["status"] > 4) { - $stmt = $db->prepare( - "SELECT id FROM " . - PREFIX . - "messages WHERE postdate>=? ORDER BY id DESC LIMIT 1;", - ); - $stmt->execute([$entry]); - } else { - $stmt = $db->prepare( - "SELECT id FROM " . - PREFIX . - "messages WHERE poster=? AND postdate>=? ORDER BY id DESC LIMIT 1;", - ); - $stmt->execute([$U["nickname"], $entry]); - } - if ($id = $stmt->fetch(PDO::FETCH_NUM)) { - $stmt = $db->prepare("DELETE FROM " . PREFIX . "messages WHERE id=?;"); - $stmt->execute($id); - $stmt = $db->prepare("DELETE FROM " . PREFIX . "inbox WHERE postid=?;"); - $stmt->execute($id); - } -} - -function print_messages(int $delstatus = 0): void -{ - global $U, $db; - $dateformat = get_setting("dateformat"); - if (!$U["embed"] && get_setting("imgembed")) { - $removeEmbed = true; - } else { - $removeEmbed = false; - } - if ($U["timestamps"] && !empty($dateformat)) { - $timestamps = true; - } else { - $timestamps = false; - } - if ($U["sortupdown"]) { - $direction = "ASC"; - } else { - $direction = "DESC"; - } - if ($U["status"] > 1) { - $entry = 0; - } else { - $entry = $U["entry"]; - } - echo '<div id="messages">'; - if ($delstatus > 0) { - $stmt = $db->prepare( - "SELECT postdate, id, text FROM " . - PREFIX . - "messages WHERE " . - "(poststatus<? AND delstatus<?) OR ((poster=? OR recipient=?) AND postdate>=?) ORDER BY id $direction;", - ); - $stmt->execute([ - $U["status"], - $delstatus, - $U["nickname"], - $U["nickname"], - $entry, - ]); - while ($message = $stmt->fetch(PDO::FETCH_ASSOC)) { - prepare_message_print($message, $removeEmbed); - echo "<div class=\"msg\"><label><input type=\"checkbox\" name=\"mid[]\" value=\"$message[id]\">"; - if ($timestamps) { - echo " <small>" . - date($dateformat, $message["postdate"]) . - " - </small>"; - } - echo " $message[text]</label></div>"; - } - } else { - $stmt = $db->prepare( - "SELECT id, postdate, poststatus, text FROM " . - PREFIX . - "messages WHERE (poststatus<=? OR poststatus=4 OR " . - "(poststatus=9 AND ( (poster=? AND recipient NOT IN (SELECT ign FROM " . - PREFIX . - "ignored WHERE ignby=?) ) OR recipient=?) AND postdate>=?)" . - ") AND poster NOT IN (SELECT ign FROM " . - PREFIX . - "ignored WHERE ignby=?) ORDER BY id $direction;", - ); - $stmt->execute([ - $U["status"], - $U["nickname"], - $U["nickname"], - $U["nickname"], - $entry, - $U["nickname"], - ]); - while ($message = $stmt->fetch(PDO::FETCH_ASSOC)) { - prepare_message_print($message, $removeEmbed); - echo '<div class="msg">'; - if ($timestamps) { - echo "<small>" . - date($dateformat, $message["postdate"]) . - " - </small>"; - } - if ($message["poststatus"] == 4) { - echo '<span class="sysmsg" title="' . - _("system message") . - '">' . - get_setting("sysmessagetxt") . - "$message[text]</span></div>"; - } else { - echo "$message[text]</div>"; - } - } - } - echo "</div>"; -} - -function prepare_message_print(array &$message, bool $removeEmbed): void -{ - if (MSGENCRYPTED) { - try { - $message["text"] = sodium_crypto_aead_aes256gcm_decrypt( - base64_decode($message["text"]), - null, - AES_IV, - ENCRYPTKEY, - ); - } catch (SodiumException $e) { - send_error($e->getMessage()); - } - } - if ($removeEmbed) { - $message["text"] = preg_replace_callback( - '/<img src="([^"]+)" rel="noreferrer" loading="lazy"><\/a>/u', - function ($matched) { - return "$matched[1]</a>"; - }, - $message["text"], - ); - } -} - -// this and that - -function send_headers(): void -{ - global $U, $scripts, $styles; - header("Content-Type: text/html; charset=UTF-8"); - header("Pragma: no-cache"); - header( - "Cache-Control: no-cache, no-store, must-revalidate, max-age=0, private", - ); - header("Expires: 0"); - header("Referrer-Policy: no-referrer"); - header( - "Permissions-Policy: accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), speaker-selection=(), conversion-measurement=(), focus-without-user-activation=(), hid=(), idle-detection=(), sync-script=(), vertical-scroll=(), serial=(), trust-token-redemption=(), interest-cohort=(), otp-credentials=()", - ); - if (!get_setting("imgembed") || !($U["embed"] ?? false)) { - header("Cross-Origin-Embedder-Policy: require-corp"); - } - header("Cross-Origin-Opener-Policy: same-origin"); - header("Cross-Origin-Resource-Policy: same-origin"); - $style_hashes = ""; - foreach ($styles as $style) { - $style_hashes .= - " 'sha256-" . base64_encode(hash("sha256", $style, true)) . "'"; - } - $script_hashes = ""; - foreach ($scripts as $script) { - $script_hashes .= - " 'sha256-" . base64_encode(hash("sha256", $script, true)) . "'"; - } - header( - "Content-Security-Policy: base-uri 'self'; default-src 'none'; font-src 'self'; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src * data:; media-src * data:; style-src 'self' 'unsafe-inline';" . - (empty($script_hashes) ? "" : " script-src $script_hashes;"), - ); // $style_hashes"); //we can add computed hashes as soon as all inline css is moved to default css - header("X-Content-Type-Options: nosniff"); - header("X-Frame-Options: sameorigin"); - header("X-XSS-Protection: 1; mode=block"); - if ($_SERVER["REQUEST_METHOD"] === "HEAD") { - exit(); // headers sent, no further processing needed - } -} - -function save_setup(array $C): void -{ - global $db; - //sanity checks and escaping - foreach ($C["msg_settings"] as $setting => $title) { - $_POST[$setting] = htmlspecialchars($_POST[$setting]); - } - foreach ($C["number_settings"] as $setting => $title) { - settype($_POST[$setting], "int"); - } - foreach ($C["colour_settings"] as $setting => $title) { - if (preg_match('/^#([a-f0-9]{6})$/i', $_POST[$setting], $match)) { - $_POST[$setting] = $match[1]; - } else { - unset($_POST[$setting]); - } - } - settype($_POST["guestaccess"], "int"); - if (!preg_match('/^[01234]$/', $_POST["guestaccess"])) { - unset($_POST["guestaccess"]); - } else { - change_guest_access(intval($_POST["guestaccess"])); - } - settype($_POST["englobalpass"], "int"); - settype($_POST["captcha"], "int"); - settype($_POST["dismemcaptcha"], "int"); - settype($_POST["guestreg"], "int"); - if (isset($_POST["defaulttz"])) { - $tzs = timezone_identifiers_list(); - if (!in_array($_POST["defaulttz"], $tzs)) { - unset($_POST["defualttz"]); - } - } - $_POST["rulestxt"] = preg_replace( - "/(\r?\n|\r\n?)/u", - "<br>", - $_POST["rulestxt"], - ); - $_POST["chatname"] = htmlspecialchars($_POST["chatname"]); - $_POST["redirect"] = htmlspecialchars($_POST["redirect"]); - if ($_POST["memberexpire"] < 5) { - $_POST["memberexpire"] = 5; - } - if ($_POST["captchatime"] < 30) { - $_POST["memberexpire"] = 30; - } - $max_refresh_rate = (int) get_setting("max_refresh_rate"); - $min_refresh_rate = (int) get_setting("min_refresh_rate"); - if ($_POST["defaultrefresh"] < $min_refresh_rate) { - $_POST["defaultrefresh"] = $min_refresh_rate; - } elseif ($_POST["defaultrefresh"] > $max_refresh_rate) { - $_POST["defaultrefresh"] = $max_refresh_rate; - } - if ($_POST["maxname"] < 1) { - $_POST["maxname"] = 1; - } elseif ($_POST["maxname"] > 50) { - $_POST["maxname"] = 50; - } - if ($_POST["maxmessage"] < 1) { - $_POST["maxmessage"] = 1; - } elseif ($_POST["maxmessage"] > 16000) { - $_POST["maxmessage"] = 16000; - } - if ($_POST["numnotes"] < 1) { - $_POST["numnotes"] = 1; - } - if (!valid_regex($_POST["nickregex"])) { - unset($_POST["nickregex"]); - } - if (!valid_regex($_POST["passregex"])) { - unset($_POST["passregex"]); - } - //save values - foreach ($C["settings"] as $setting) { - if (isset($_POST[$setting])) { - update_setting($setting, $_POST[$setting]); - } - } -} - -function change_guest_access(int $guest_access): void -{ - global $db; - if ($guest_access === 4) { - $db->exec("DELETE FROM " . PREFIX . "sessions WHERE status<7;"); - } elseif ($guest_access === 0) { - $db->exec("DELETE FROM " . PREFIX . "sessions WHERE status<3;"); - } -} - -function set_default_tz(): void -{ - global $U; - if (isset($U["tz"])) { - date_default_timezone_set($U["tz"]); - } else { - date_default_timezone_set(get_setting("defaulttz")); - } -} - -function valid_admin(): bool -{ - global $U; - parse_sessions(); - if ( - !isset($U["session"]) && - isset($_POST["nick"]) && - isset($_POST["pass"]) - ) { - create_session(true, $_POST["nick"], $_POST["pass"]); - } - if (isset($U["status"])) { - if ($U["status"] >= 7) { - return true; - } - send_access_denied(); - } - return false; -} - -function valid_nick(string $nick): bool -{ - $len = mb_strlen($nick); - if ($len < 1 || $len > get_setting("maxname")) { - return false; - } - return preg_match("/" . get_setting("nickregex") . "/u", $nick); -} - -function valid_pass(string $pass): bool -{ - if (mb_strlen($pass) < get_setting("minpass")) { - return false; - } - return preg_match("/" . get_setting("passregex") . "/u", $pass); -} - -function valid_regex(string &$regex): bool -{ - $regex = preg_replace("~(^|[^\\\\])/~", "$1\/u", $regex); // Escape "/" if not yet escaped - return @preg_match("/$_POST[match]/u", "") !== false; -} - -function get_timeout(int $lastpost, int $expire): void -{ - $s = $lastpost + 60 * $expire - time(); - $m = floor($s / 60); - $s %= 60; - if ($s < 10) { - $s = "0$s"; - } - if ($m > 60) { - $h = floor($m / 60); - $m %= 60; - if ($m < 10) { - $m = "0$m"; - } - echo "$h:$m:$s"; - } else { - echo "$m:$s"; - } -} - -function print_colours(): void -{ - // Prints a short list with selected named HTML colours and filters out illegible text colours for the given background. - // It's a simple comparison of weighted grey values. This is not very accurate but gets the job done well enough. - // name=>[colour, greyval(colour), translated name] - $colours = [ - "Beige" => ["F5F5DC", 242.25, _("Beige")], - "Black" => ["000000", 0, _("Black")], - "Blue" => ["0000FF", 28.05, _("Blue")], - "BlueViolet" => ["8A2BE2", 91.63, _("Blue violet")], - "Brown" => ["A52A2A", 78.9, _("Brown")], - "Cyan" => ["00FFFF", 178.5, _("Cyan")], - "DarkBlue" => ["00008B", 15.29, _("Dark blue")], - "DarkGreen" => ["006400", 59, _("Dark green")], - "DarkRed" => ["8B0000", 41.7, _("Dark red")], - "DarkViolet" => ["9400D3", 67.61, _("Dark violet")], - "DeepSkyBlue" => ["00BFFF", 140.74, _("Sky blue")], - "Gold" => ["FFD700", 203.35, _("Gold")], - "Grey" => ["808080", 128, _("Grey")], - "Green" => ["008000", 75.52, _("Green")], - "HotPink" => ["FF69B4", 158.25, _("Hot pink")], - "Indigo" => ["4B0082", 36.8, _("Indigo")], - "LightBlue" => ["ADD8E6", 204.64, _("Light blue")], - "LightGreen" => ["90EE90", 199.46, _("Light green")], - "LimeGreen" => ["32CD32", 141.45, _("Lime green")], - "Magenta" => ["FF00FF", 104.55, _("Magenta")], - "Olive" => ["808000", 113.92, _("Olive")], - "Orange" => ["FFA500", 173.85, _("Orange")], - "OrangeRed" => ["FF4500", 117.21, _("Orange red")], - "Purple" => ["800080", 52.48, _("Purple")], - "Red" => ["FF0000", 76.5, _("Red")], - "RoyalBlue" => ["4169E1", 106.2, _("Royal blue")], - "SeaGreen" => ["2E8B57", 105.38, _("Sea green")], - "Sienna" => ["A0522D", 101.33, _("Sienna")], - "Silver" => ["C0C0C0", 192, _("Silver")], - "Tan" => ["D2B48C", 184.6, _("Tan")], - "Teal" => ["008080", 89.6, _("Teal")], - "Violet" => ["EE82EE", 174.28, _("Violet")], - "White" => ["FFFFFF", 255, _("White")], - "Yellow" => ["FFFF00", 226.95, _("Yellow")], - "YellowGreen" => ["9ACD32", 172.65, _("Yellow green")], - ]; - $greybg = greyval(get_setting("colbg")); - foreach ($colours as $name => $colour) { - if (abs($greybg - $colour[1]) > 75) { - echo "<option value=\"$colour[0]\" style=\"color:#$colour[0];\">$colour[2]</option>"; - } - } -} - -function greyval(string $colour): string -{ - return hexdec(substr($colour, 0, 2)) * 0.3 + - hexdec(substr($colour, 2, 2)) * 0.59 + - hexdec(substr($colour, 4, 2)) * 0.11; -} - -function style_this(string $text, string $styleinfo): string -{ - return "<span style=\"$styleinfo\">$text</span>"; -} - -function check_init(): bool -{ - global $db; - try { - $db->query("SELECT null FROM " . PREFIX . "settings LIMIT 1;"); - } catch (Exception $e) { - return false; - } - return true; -} - -// run every minute doing various database cleanup task -function cron(): void -{ - global $db; - $time = time(); - if (get_setting("nextcron") > $time) { - return; - } - update_setting("nextcron", $time + 10); - // delete old sessions - $stmt = $db->prepare( - "DELETE FROM " . - PREFIX . - "sessions WHERE (status<=2 AND lastpost<(?-60*(SELECT value FROM " . - PREFIX . - "settings WHERE setting='guestexpire'))) OR (status>2 AND lastpost<(?-60*(SELECT value FROM " . - PREFIX . - "settings WHERE setting='memberexpire'))) OR (status<3 AND exiting>0 AND lastpost<(?-(SELECT value FROM " . - PREFIX . - "settings WHERE setting='exitwait')));", - ); - $stmt->execute([$time, $time, $time]); - // delete old messages - $limit = get_setting("messagelimit"); - $stmt = $db->query( - "SELECT id FROM " . - PREFIX . - "messages WHERE poststatus=1 OR poststatus=4 ORDER BY id DESC LIMIT 1 OFFSET $limit;", - ); - if ($id = $stmt->fetch(PDO::FETCH_NUM)) { - $stmt = $db->prepare("DELETE FROM " . PREFIX . "messages WHERE id<=?;"); - $stmt->execute($id); - } - $stmt = $db->prepare( - "DELETE FROM " . - PREFIX . - "messages WHERE id IN (SELECT * FROM (SELECT id FROM " . - PREFIX . - "messages WHERE postdate<(?-60*(SELECT value FROM " . - PREFIX . - "settings WHERE setting='messageexpire'))) AS t);", - ); - $stmt->execute([$time]); - // delete expired ignored people - $result = $db->query( - "SELECT id FROM " . - PREFIX . - "ignored WHERE ign NOT IN (SELECT nickname FROM " . - PREFIX . - "sessions UNION SELECT nickname FROM " . - PREFIX . - "members UNION SELECT poster FROM " . - PREFIX . - "messages) OR ignby NOT IN (SELECT nickname FROM " . - PREFIX . - "sessions UNION SELECT nickname FROM " . - PREFIX . - "members UNION SELECT poster FROM " . - PREFIX . - "messages);", - ); - $stmt = $db->prepare("DELETE FROM " . PREFIX . "ignored WHERE id=?;"); - while ($tmp = $result->fetch(PDO::FETCH_NUM)) { - $stmt->execute($tmp); - } - // delete files that do not belong to any message - $result = $db->query( - "SELECT id FROM " . - PREFIX . - "files WHERE postid NOT IN (SELECT id FROM " . - PREFIX . - "messages UNION SELECT postid FROM " . - PREFIX . - "inbox);", - ); - $stmt = $db->prepare("DELETE FROM " . PREFIX . "files WHERE id=?;"); - while ($tmp = $result->fetch(PDO::FETCH_NUM)) { - $stmt->execute($tmp); - } - // delete old notes - $limit = get_setting("numnotes"); - $to_keep = []; - $stmt = $db->query( - "SELECT id FROM " . - PREFIX . - "notes WHERE type=0 ORDER BY id DESC LIMIT $limit;", - ); - while ($tmp = $stmt->fetch(PDO::FETCH_ASSOC)) { - $to_keep[] = $tmp["id"]; - } - $stmt = $db->query( - "SELECT id FROM " . - PREFIX . - "notes WHERE type=1 ORDER BY id DESC LIMIT $limit;", - ); - while ($tmp = $stmt->fetch(PDO::FETCH_ASSOC)) { - $to_keep[] = $tmp["id"]; - } - $query = "DELETE FROM " . PREFIX . "notes WHERE type!=2 AND type!=3"; - if (!empty($to_keep)) { - $query .= " AND id NOT IN ("; - for ($i = count($to_keep); $i > 1; --$i) { - $query .= "?, "; - } - $query .= "?)"; - } - $stmt = $db->prepare($query); - $stmt->execute($to_keep); - $result = $db->query( - "SELECT editedby, COUNT(*) AS cnt FROM " . - PREFIX . - "notes WHERE type=2 GROUP BY editedby HAVING cnt>$limit;", - ); - $stmt = $db->prepare( - "DELETE FROM " . - PREFIX . - "notes WHERE (type=2 OR type=3) AND editedby=? AND id NOT IN (SELECT * FROM (SELECT id FROM " . - PREFIX . - "notes WHERE (type=2 OR type=3) AND editedby=? ORDER BY id DESC LIMIT $limit) AS t);", - ); - while ($tmp = $result->fetch(PDO::FETCH_NUM)) { - $stmt->execute([$tmp[0], $tmp[0]]); - } - // delete old captchas - $stmt = $db->prepare( - "DELETE FROM " . - PREFIX . - "captcha WHERE time<(?-(SELECT value FROM " . - PREFIX . - "settings WHERE setting='captchatime'));", - ); - $stmt->execute([$time]); - // delete member associated data of deleted accounts - $db->query( - "DELETE FROM " . - PREFIX . - "inbox WHERE recipient NOT IN (SELECT nickname FROM " . - PREFIX . - "members);", - ); - $db->query( - "DELETE FROM " . - PREFIX . - "notes WHERE (type=2 OR type=3) AND editedby NOT IN (SELECT nickname FROM " . - PREFIX . - "members);", - ); -} - -function destroy_chat(array $C): void -{ - global $db, $memcached, $session; - setcookie(COOKIENAME, false); - $session = ""; - print_start("destroy"); - $db->exec("DROP TABLE " . PREFIX . "captcha;"); - $db->exec("DROP TABLE " . PREFIX . "files;"); - $db->exec("DROP TABLE " . PREFIX . "filter;"); - $db->exec("DROP TABLE " . PREFIX . "ignored;"); - $db->exec("DROP TABLE " . PREFIX . "inbox;"); - $db->exec("DROP TABLE " . PREFIX . "linkfilter;"); - $db->exec("DROP TABLE " . PREFIX . "members;"); - $db->exec("DROP TABLE " . PREFIX . "messages;"); - $db->exec("DROP TABLE " . PREFIX . "notes;"); - $db->exec("DROP TABLE " . PREFIX . "sessions;"); - $db->exec("DROP TABLE " . PREFIX . "settings;"); - if (MEMCACHED) { - $memcached->delete(DBNAME . "-" . PREFIX . "filter"); - $memcached->delete(DBNAME . "-" . PREFIX . "linkfilter"); - foreach ($C["settings"] as $setting) { - $memcached->delete(DBNAME . "-" . PREFIX . "settings-$setting"); - } - $memcached->delete(DBNAME . "-" . PREFIX . "settings-dbversion"); - $memcached->delete(DBNAME . "-" . PREFIX . "settings-msgencrypted"); - $memcached->delete(DBNAME . "-" . PREFIX . "settings-nextcron"); - } - echo "<h2>" . _("Successfully destroyed chat") . "</h2><br><br><br>"; - echo form("setup") . submit(_("Initial Setup")) . "</form>" . credit(); - print_end(); -} - -function init_chat(): void -{ - global $db; - if (check_init()) { - $suwrite = _( - "Database tables already exist! To continue, you have to delete these tables manually first.", - ); - $result = $db->query( - "SELECT null FROM " . PREFIX . "members WHERE status=8;", - ); - if ($result->fetch(PDO::FETCH_NUM)) { - $suwrite = _("A Superadmin already exists!"); - } - } elseif (!preg_match('/^[a-z0-9]{1,20}$/i', $_POST["sunick"])) { - $suwrite = sprintf( - _( - 'Invalid nickname (%1$d characters maximum and has to match the regular expression "%2$s")', - ), - 20, - '^[A-Za-z1-9]*$', - ); - } elseif (mb_strlen($_POST["supass"]) < 5) { - $suwrite = sprintf( - _( - 'Invalid password (At least %1$d characters and has to match the regular expression "%2$s")', - ), - 5, - ".*", - ); - } elseif ($_POST["supass"] !== $_POST["supassc"]) { - $suwrite = _("Password confirmation does not match!"); - } else { - ignore_user_abort(true); - set_time_limit(0); - if (DBDRIVER === 0) { - //MySQL - $memengine = " ENGINE=MEMORY"; - $diskengine = " ENGINE=InnoDB"; - $charset = " DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin"; - $primary = "integer PRIMARY KEY AUTO_INCREMENT"; - $longtext = "longtext"; - } elseif (DBDRIVER === 1) { - //PostgreSQL - $memengine = ""; - $diskengine = ""; - $charset = ""; - $primary = "serial PRIMARY KEY"; - $longtext = "text"; - } else { - //SQLite - $memengine = ""; - $diskengine = ""; - $charset = ""; - $primary = "integer PRIMARY KEY"; - $longtext = "text"; - } - $db->exec( - "CREATE TABLE " . - PREFIX . - "captcha (id $primary, time integer NOT NULL, code char(5) NOT NULL)$memengine$charset;", - ); - $db->exec( - "CREATE TABLE " . - PREFIX . - "files (id $primary, postid integer NOT NULL UNIQUE, filename varchar(255) NOT NULL, hash char(40) NOT NULL, type varchar(255) NOT NULL, data $longtext NOT NULL)$diskengine$charset;", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "files_hash ON " . - PREFIX . - "files(hash);", - ); - $db->exec( - "CREATE TABLE " . - PREFIX . - "filter (id $primary, filtermatch varchar(255) NOT NULL, filterreplace text NOT NULL, allowinpm smallint NOT NULL, regex smallint NOT NULL, kick smallint NOT NULL, cs smallint NOT NULL)$diskengine$charset;", - ); - $db->exec( - "CREATE TABLE " . - PREFIX . - "ignored (id $primary, ign varchar(50) NOT NULL, ignby varchar(50) NOT NULL)$diskengine$charset;", - ); - $db->exec( - "CREATE INDEX " . PREFIX . "ign ON " . PREFIX . "ignored(ign);", - ); - $db->exec( - "CREATE INDEX " . PREFIX . "ignby ON " . PREFIX . "ignored(ignby);", - ); - $db->exec( - "CREATE TABLE " . - PREFIX . - "members (id $primary, nickname varchar(50) NOT NULL UNIQUE, passhash varchar(255) NOT NULL, status smallint NOT NULL, refresh smallint NOT NULL, bgcolour char(6) NOT NULL, regedby varchar(50) DEFAULT '', lastlogin integer DEFAULT 0, loginfails integer unsigned NOT NULL DEFAULT 0, timestamps smallint NOT NULL, embed smallint NOT NULL, incognito smallint NOT NULL, style varchar(255) NOT NULL, nocache smallint NOT NULL, tz varchar(255) NOT NULL, eninbox smallint NOT NULL, sortupdown smallint NOT NULL, hidechatters smallint NOT NULL, nocache_old smallint NOT NULL)$diskengine$charset;", - ); - $db->exec( - "CREATE TABLE " . - PREFIX . - "inbox (id $primary, postdate integer NOT NULL, postid integer NOT NULL UNIQUE, poster varchar(50) NOT NULL, recipient varchar(50) NOT NULL, text text NOT NULL, FOREIGN KEY (recipient) REFERENCES " . - PREFIX . - "members(nickname) ON DELETE CASCADE ON UPDATE CASCADE)$diskengine$charset;", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "inbox_poster ON " . - PREFIX . - "inbox(poster);", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "inbox_recipient ON " . - PREFIX . - "inbox(recipient);", - ); - $db->exec( - "CREATE TABLE " . - PREFIX . - "linkfilter (id $primary, filtermatch varchar(255) NOT NULL, filterreplace varchar(255) NOT NULL, regex smallint NOT NULL)$diskengine$charset;", - ); - $db->exec( - "CREATE TABLE " . - PREFIX . - "messages (id $primary, postdate integer NOT NULL, poststatus smallint NOT NULL, poster varchar(50) NOT NULL, recipient varchar(50) NOT NULL, text text NOT NULL, delstatus smallint NOT NULL)$diskengine$charset;", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "poster ON " . - PREFIX . - "messages (poster);", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "recipient ON " . - PREFIX . - "messages(recipient);", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "postdate ON " . - PREFIX . - "messages(postdate);", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "poststatus ON " . - PREFIX . - "messages(poststatus);", - ); - $db->exec( - "CREATE TABLE " . - PREFIX . - "notes (id $primary, type smallint NOT NULL, lastedited integer NOT NULL, editedby varchar(50) NOT NULL, text text NOT NULL)$diskengine$charset;", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "notes_type ON " . - PREFIX . - "notes(type);", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "notes_editedby ON " . - PREFIX . - "notes(editedby);", - ); - $db->exec( - "CREATE TABLE " . - PREFIX . - "sessions (id $primary, session char(32) NOT NULL UNIQUE, nickname varchar(50) NOT NULL UNIQUE, status smallint NOT NULL, refresh smallint NOT NULL, style varchar(255) NOT NULL, lastpost integer NOT NULL, passhash varchar(255) NOT NULL, postid char(6) NOT NULL DEFAULT '000000', useragent varchar(255) NOT NULL, kickmessage varchar(255) DEFAULT '', bgcolour char(6) NOT NULL, entry integer NOT NULL, exiting smallint NOT NULL, timestamps smallint NOT NULL, embed smallint NOT NULL, incognito smallint NOT NULL, ip varchar(45) NOT NULL, nocache smallint NOT NULL, tz varchar(255) NOT NULL, eninbox smallint NOT NULL, sortupdown smallint NOT NULL, hidechatters smallint NOT NULL, nocache_old smallint NOT NULL)$memengine$charset;", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "status ON " . - PREFIX . - "sessions(status);", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "lastpost ON " . - PREFIX . - "sessions(lastpost);", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "incognito ON " . - PREFIX . - "sessions(incognito);", - ); - $db->exec( - "CREATE TABLE " . - PREFIX . - "settings (setting varchar(50) NOT NULL PRIMARY KEY, value text NOT NULL)$diskengine$charset;", - ); - - $settings = [ - ["guestaccess", "0"], - ["globalpass", ""], - ["englobalpass", "0"], - ["captcha", "0"], - ["dateformat", "m-d H:i:s"], - ["rulestxt", ""], - ["msgencrypted", "0"], - ["dbversion", DBVERSION], - ["css", ""], - ["memberexpire", "60"], - ["guestexpire", "15"], - ["kickpenalty", "10"], - ["entrywait", "120"], - ["exitwait", "180"], - ["messageexpire", "14400"], - ["messagelimit", "150"], - ["maxmessage", 2000], - [ - "captchattfont", - "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", - ], - ["captchatime", "600"], - ["colbg", "000000"], - ["coltxt", "FFFFFF"], - ["maxname", "20"], - ["minpass", "5"], - ["defaultrefresh", "20"], - ["dismemcaptcha", "0"], - ["suguests", "0"], - ["noguestpm", "0"], - ["imgembed", "1"], - ["timestamps", "1"], - ["trackip", "0"], - [ - "captchachars", - "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", - ], - ["memkick", "1"], - ["memkickalways", "0"], - ["namedoers", "1"], - ["forceredirect", "0"], - ["redirect", ""], - ["incognito", "1"], - ["chatname", "My Chat"], - ["topic", ""], - ["msgsendall", _("%s - ")], - ["msgsendmem", _("[M] %s - ")], - ["msgsendmod", _("[Staff] %s - ")], - ["msgsendadm", _("[Admin] %s - ")], - ["msgsendprv", _('[%1$s to %2$s] - ')], - ["msgenter", _("%s entered the chat.")], - ["msgexit", _("%s left the chat.")], - ["msgmemreg", _("%s is now a registered member.")], - ["msgsureg", _("%s is now a registered applicant.")], - ["msgkick", _('%1$s has been kicked: %2$s')], - ["msgmultikick", _('%1$s have been kicked: %2$s')], - ["msgallkick", _('All guests have been kicked: %1$s')], - ["msgclean", _("%s has been cleaned.")], - ["numnotes", "3"], - ["mailsender", "www-data <www-data@localhost>"], - ["mailreceiver", "Webmaster <webmaster@localhost>"], - ["sendmail", "0"], - ["modfallback", "1"], - ["guestreg", "0"], - ["disablepm", "0"], - ["disabletext", "<h1>" . _("Temporarily disabled") . "</h1>"], - ["defaulttz", "UTC"], - ["eninbox", "0"], - ["passregex", ".*"], - ["nickregex", '^[A-Za-z0-9]*$'], - ["externalcss", ""], - ["enablegreeting", "0"], - ["sortupdown", "0"], - ["hidechatters", "0"], - ["enfileupload", "0"], - ["msgattache", '%2$s [%1$s]'], - ["maxuploadsize", "1024"], - ["nextcron", "0"], - ["personalnotes", "1"], - ["publicnotes", "1"], - ["filtermodkick", "0"], - ["metadescription", _("A chat community")], - ["exitingtxt", "&#128682;"], // door emoji - ["sysmessagetxt", "ā„¹ļø &nbsp;"], - ["hide_reload_post_box", "0"], - ["hide_reload_messages", "0"], - ["hide_profile", "0"], - ["hide_admin", "0"], - ["hide_notes", "0"], - ["hide_clone", "0"], - ["hide_rearrange", "0"], - ["hide_help", "0"], - ["max_refresh_rate", "150"], - ["min_refresh_rate", "5"], - ["postbox_delete_globally", "0"], - ["allow_js", "1"], - ]; - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "settings (setting, value) VALUES (?, ?);", - ); - foreach ($settings as $pair) { - $stmt->execute($pair); - } - $reg = [ - "nickname" => $_POST["sunick"], - "passhash" => password_hash($_POST["supass"], PASSWORD_DEFAULT), - "status" => 8, - "refresh" => 20, - "bgcolour" => "000000", - "timestamps" => 1, - "style" => "color:#FFFFFF;", - "embed" => 1, - "incognito" => 0, - "nocache" => 0, - "nocache_old" => 1, - "tz" => "UTC", - "eninbox" => 0, - "sortupdown" => 0, - "hidechatters" => 0, - ]; - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "members (nickname, passhash, status, refresh, bgcolour, timestamps, style, embed, incognito, nocache, tz, eninbox, sortupdown, hidechatters, nocache_old) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", - ); - $stmt->execute([ - $reg["nickname"], - $reg["passhash"], - $reg["status"], - $reg["refresh"], - $reg["bgcolour"], - $reg["timestamps"], - $reg["style"], - $reg["embed"], - $reg["incognito"], - $reg["nocache"], - $reg["tz"], - $reg["eninbox"], - $reg["sortupdown"], - $reg["hidechatters"], - $reg["nocache_old"], - ]); - $suwrite = _("Successfully registered!"); - } - print_start("init"); - echo "<h2>" . - _("Initial Setup") . - "</h2><br><h3>" . - _("Superadmin Login") . - "</h3>$suwrite<br><br><br>"; - echo form("setup") . - submit(_("Go to the Setup-Page")) . - "</form>" . - credit(); - print_end(); -} - -function update_db(): void -{ - global $db, $memcached; - $dbversion = (int) get_setting("dbversion"); - $msgencrypted = (bool) get_setting("msgencrypted"); - if ($dbversion >= DBVERSION && $msgencrypted === MSGENCRYPTED) { - return; - } - ignore_user_abort(true); - set_time_limit(0); - if (DBDRIVER === 0) { - //MySQL - $memengine = " ENGINE=MEMORY"; - $diskengine = " ENGINE=InnoDB"; - $charset = " DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin"; - $primary = "integer PRIMARY KEY AUTO_INCREMENT"; - $longtext = "longtext"; - } elseif (DBDRIVER === 1) { - //PostgreSQL - $memengine = ""; - $diskengine = ""; - $charset = ""; - $primary = "serial PRIMARY KEY"; - $longtext = "text"; - } else { - //SQLite - $memengine = ""; - $diskengine = ""; - $charset = ""; - $primary = "integer PRIMARY KEY"; - $longtext = "text"; - } - $msg = ""; - if ($dbversion < 2) { - $db->exec( - "CREATE TABLE IF NOT EXISTS " . - PREFIX . - "ignored (id integer unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, ignored varchar(50) NOT NULL, `by` varchar(50) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8;", - ); - } - if ($dbversion < 3) { - $db->exec( - "INSERT INTO " . - PREFIX . - "settings (setting, value) VALUES ('rulestxt', '');", - ); - } - if ($dbversion < 4) { - $db->exec( - "ALTER TABLE " . - PREFIX . - "members ADD incognito smallint NOT NULL;", - ); - } - if ($dbversion < 5) { - $db->exec( - "INSERT INTO " . - PREFIX . - "settings (setting, value) VALUES ('globalpass', '');", - ); - } - if ($dbversion < 6) { - $db->exec( - "INSERT INTO " . - PREFIX . - "settings (setting, value) VALUES ('dateformat', 'm-d H:i:s');", - ); - } - if ($dbversion < 7) { - $db->exec( - "ALTER TABLE " . - PREFIX . - "captcha ADD code char(5) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;", - ); - } - if ($dbversion < 8) { - $db->exec( - "INSERT INTO " . - PREFIX . - "settings (setting, value) VALUES ('captcha', '0'), ('englobalpass', '0');", - ); - $ga = (int) get_setting("guestaccess"); - if ($ga === -1) { - update_setting("guestaccess", 0); - update_setting("englobalpass", 1); - } elseif ($ga === 4) { - update_setting("guestaccess", 1); - update_setting("englobalpass", 2); - } - } - if ($dbversion < 9) { - $db->exec( - "INSERT INTO " . - PREFIX . - "settings (setting,value) VALUES ('msgencrypted', '0');", - ); - $db->exec( - "ALTER TABLE " . - PREFIX . - "settings MODIFY value varchar(20000) NOT NULL;", - ); - $db->exec("ALTER TABLE " . PREFIX . "messages DROP postid;"); - } - if ($dbversion < 10) { - $db->exec( - "INSERT INTO " . - PREFIX . - "settings (setting, value) VALUES ('css', ''), ('memberexpire', '60'), ('guestexpire', '15'), ('kickpenalty', '10'), ('entrywait', '120'), ('messageexpire', '14400'), ('messagelimit', '150'), ('maxmessage', 2000), ('captchatime', '600');", - ); - } - if ($dbversion < 11) { - $db->exec( - "ALTER TABLE " . - PREFIX . - "captcha CHARACTER SET utf8 COLLATE utf8_bin;", - ); - $db->exec( - "ALTER TABLE " . - PREFIX . - "filter CHARACTER SET utf8 COLLATE utf8_bin;", - ); - $db->exec( - "ALTER TABLE " . - PREFIX . - "ignored CHARACTER SET utf8 COLLATE utf8_bin;", - ); - $db->exec( - "ALTER TABLE " . - PREFIX . - "messages CHARACTER SET utf8 COLLATE utf8_bin;", - ); - $db->exec( - "ALTER TABLE " . - PREFIX . - "notes CHARACTER SET utf8 COLLATE utf8_bin;", - ); - $db->exec( - "ALTER TABLE " . - PREFIX . - "settings CHARACTER SET utf8 COLLATE utf8_bin;", - ); - $db->exec( - "CREATE TABLE " . - PREFIX . - "linkfilter (id integer unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, `match` varchar(255) NOT NULL, `replace` varchar(255) NOT NULL, regex smallint NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_bin;", - ); - $db->exec( - "ALTER TABLE " . - PREFIX . - "members ADD style varchar(255) NOT NULL;", - ); - $result = $db->query("SELECT * FROM " . PREFIX . "members;"); - $stmt = $db->prepare( - "UPDATE " . PREFIX . "members SET style=? WHERE id=?;", - ); - $F = load_fonts(); - while ($temp = $result->fetch(PDO::FETCH_ASSOC)) { - $style = "color:#$temp[colour];"; - if (isset($F[$temp["fontface"]])) { - $style .= $F[$temp["fontface"]]; - } - if (strpos($temp["fonttags"], "i") !== false) { - $style .= "font-style:italic;"; - } - if (strpos($temp["fonttags"], "b") !== false) { - $style .= "font-weight:bold;"; - } - $stmt->execute([$style, $temp["id"]]); - } - $db->exec( - "INSERT INTO " . - PREFIX . - "settings (setting, value) VALUES ('colbg', '000000'), ('coltxt', 'FFFFFF'), ('maxname', '20'), ('minpass', '5'), ('defaultrefresh', '20'), ('dismemcaptcha', '0'), ('suguests', '0'), ('imgembed', '1'), ('timestamps', '1'), ('trackip', '0'), ('captchachars', '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), ('memkick', '1'), ('forceredirect', '0'), ('redirect', ''), ('incognito', '1');", - ); - } - if ($dbversion < 12) { - $db->exec( - "ALTER TABLE " . - PREFIX . - "captcha MODIFY code char(5) NOT NULL, DROP INDEX id, ADD PRIMARY KEY (id) USING BTREE;", - ); - $db->exec("ALTER TABLE " . PREFIX . "captcha ENGINE=MEMORY;"); - $db->exec( - "ALTER TABLE " . - PREFIX . - "filter MODIFY id integer unsigned NOT NULL AUTO_INCREMENT, MODIFY `match` varchar(255) NOT NULL, MODIFY replace varchar(20000) NOT NULL;", - ); - $db->exec( - "ALTER TABLE " . - PREFIX . - "ignored MODIFY ignored varchar(50) NOT NULL, MODIFY `by` varchar(50) NOT NULL, ADD INDEX(ignored), ADD INDEX(`by`);", - ); - $db->exec( - "ALTER TABLE " . - PREFIX . - "linkfilter MODIFY match varchar(255) NOT NULL, MODIFY replace varchar(255) NOT NULL;", - ); - $db->exec( - "ALTER TABLE " . - PREFIX . - "messages MODIFY poster varchar(50) NOT NULL, MODIFY recipient varchar(50) NOT NULL, MODIFY text varchar(20000) NOT NULL, ADD INDEX(poster), ADD INDEX(recipient), ADD INDEX(postdate), ADD INDEX(poststatus);", - ); - $db->exec( - "ALTER TABLE " . - PREFIX . - "notes MODIFY type char(5) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, MODIFY editedby varchar(50) NOT NULL, MODIFY text varchar(20000) NOT NULL;", - ); - $db->exec( - "ALTER TABLE " . - PREFIX . - "settings MODIFY id integer unsigned NOT NULL, MODIFY setting varchar(50) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, MODIFY value varchar(20000) NOT NULL;", - ); - $db->exec( - "ALTER TABLE " . - PREFIX . - "settings DROP PRIMARY KEY, DROP id, ADD PRIMARY KEY(setting);", - ); - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "settings (setting, value) VALUES ('chatname', 'My Chat'), ('topic', ''), ('msgsendall', ?), ('msgsendmem', ?), ('msgsendmod', ?), ('msgsendadm', ?), ('msgsendprv', ?), ('numnotes', '3');", - ); - $stmt->execute([ - _("%s - "), - _("[M] %s - "), - _("[Staff] %s - "), - _("[Admin] %s - "), - _('[%1$s to %2$s] - '), - ]); - } - if ($dbversion < 13) { - $db->exec( - "ALTER TABLE " . - PREFIX . - "filter CHANGE `match` filtermatch varchar(255) NOT NULL, CHANGE `replace` filterreplace varchar(20000) NOT NULL;", - ); - $db->exec( - "ALTER TABLE " . - PREFIX . - "ignored CHANGE ignored ign varchar(50) NOT NULL, CHANGE `by` ignby varchar(50) NOT NULL;", - ); - $db->exec( - "ALTER TABLE " . - PREFIX . - "linkfilter CHANGE `match` filtermatch varchar(255) NOT NULL, CHANGE `replace` filterreplace varchar(255) NOT NULL;", - ); - } - if ($dbversion < 14) { - if (MEMCACHED) { - $memcached->delete(DBNAME . "-" . PREFIX . "members"); - $memcached->delete(DBNAME . "-" . PREFIX . "ignored"); - } - if (DBDRIVER === 0) { - //MySQL - previously had a wrong SQL syntax and the captcha table was not created. - $db->exec( - "CREATE TABLE IF NOT EXISTS " . - PREFIX . - "captcha (id integer unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, time integer unsigned NOT NULL, code char(5) NOT NULL) ENGINE=MEMORY DEFAULT CHARSET=utf8 COLLATE=utf8_bin;", - ); - } - } - if ($dbversion < 15) { - $db->exec( - "INSERT INTO " . - PREFIX . - "settings (setting, value) VALUES ('mailsender', 'www-data <www-data@localhost>'), ('mailreceiver', 'Webmaster <webmaster@localhost>'), ('sendmail', '0'), ('modfallback', '1'), ('guestreg', '0');", - ); - } - if ($dbversion < 17) { - $db->exec( - "ALTER TABLE " . - PREFIX . - "members ADD COLUMN nocache smallint NOT NULL DEFAULT 0;", - ); - } - if ($dbversion < 18) { - $db->exec( - "INSERT INTO " . - PREFIX . - "settings (setting, value) VALUES ('disablepm', '0');", - ); - } - if ($dbversion < 19) { - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "settings (setting, value) VALUES ('disabletext', ?);", - ); - $stmt->execute(["<h1>" . _("Temporarily disabled") . "</h1>"]); - } - if ($dbversion < 20) { - $db->exec( - "ALTER TABLE " . - PREFIX . - "members ADD COLUMN tz smallint NOT NULL DEFAULT 0;", - ); - $db->exec( - "INSERT INTO " . - PREFIX . - "settings (setting, value) VALUES ('defaulttz', 'UTC');", - ); - } - if ($dbversion < 21) { - $db->exec( - "ALTER TABLE " . - PREFIX . - "members ADD COLUMN eninbox smallint NOT NULL DEFAULT 0;", - ); - $db->exec( - "INSERT INTO " . - PREFIX . - "settings (setting, value) VALUES ('eninbox', '0');", - ); - if (DBDRIVER === 0) { - $db->exec( - "CREATE TABLE " . - PREFIX . - "inbox (id integer unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT, postid integer unsigned NOT NULL, postdate integer unsigned NOT NULL, poster varchar(50) NOT NULL, recipient varchar(50) NOT NULL, text varchar(20000) NOT NULL, INDEX(postid), INDEX(poster), INDEX(recipient)) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;", - ); - } else { - $db->exec( - "CREATE TABLE " . - PREFIX . - "inbox (id $primary, postdate integer NOT NULL, postid integer NOT NULL, poster varchar(50) NOT NULL, recipient varchar(50) NOT NULL, text varchar(20000) NOT NULL);", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "inbox_postid ON " . - PREFIX . - "inbox(postid);", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "inbox_poster ON " . - PREFIX . - "inbox(poster);", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "inbox_recipient ON " . - PREFIX . - "inbox(recipient);", - ); - } - } - if ($dbversion < 23) { - $db->exec( - "DELETE FROM " . PREFIX . "settings WHERE setting='enablejs';", - ); - } - if ($dbversion < 25) { - $db->exec( - "DELETE FROM " . PREFIX . "settings WHERE setting='keeplimit';", - ); - } - if ($dbversion < 26) { - $db->exec( - "INSERT INTO " . - PREFIX . - 'settings (setting, value) VALUES (\'passregex\', \'.*\'), (\'nickregex\', \'^[A-Za-z0-9]*$\');', - ); - } - if ($dbversion < 27) { - $db->exec( - "INSERT INTO " . - PREFIX . - "settings (setting, value) VALUES ('externalcss', '');", - ); - } - if ($dbversion < 28) { - $db->exec( - "INSERT INTO " . - PREFIX . - "settings (setting, value) VALUES ('enablegreeting', '0');", - ); - } - if ($dbversion < 29) { - $db->exec( - "INSERT INTO " . - PREFIX . - "settings (setting, value) VALUES ('sortupdown', '0');", - ); - $db->exec( - "ALTER TABLE " . - PREFIX . - "members ADD COLUMN sortupdown smallint NOT NULL DEFAULT 0;", - ); - } - if ($dbversion < 30) { - $db->exec( - "ALTER TABLE " . - PREFIX . - "filter ADD COLUMN cs smallint NOT NULL DEFAULT 0;", - ); - if (MEMCACHED) { - $memcached->delete(DBNAME . "-" . PREFIX . "filter"); - } - } - if ($dbversion < 31) { - $db->exec( - "INSERT INTO " . - PREFIX . - "settings (setting, value) VALUES ('hidechatters', '0');", - ); - $db->exec( - "ALTER TABLE " . - PREFIX . - "members ADD COLUMN hidechatters smallint NOT NULL DEFAULT 0;", - ); - } - if ($dbversion < 32 && DBDRIVER === 0) { - //recreate db in utf8mb4 - try { - $olddb = new PDO( - "mysql:host=" . DBHOST . ";dbname=" . DBNAME, - DBUSER, - DBPASS, - [ - PDO::ATTR_ERRMODE => PDO::ERRMODE_WARNING, - PDO::ATTR_PERSISTENT => PERSISTENT, - ], - ); - $db->exec("DROP TABLE " . PREFIX . "captcha;"); - $db->exec( - "CREATE TABLE " . - PREFIX . - "captcha (id integer PRIMARY KEY AUTO_INCREMENT, time integer NOT NULL, code char(5) NOT NULL) ENGINE=MEMORY DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;", - ); - $result = $olddb->query( - "SELECT filtermatch, filterreplace, allowinpm, regex, kick, cs FROM " . - PREFIX . - "filter;", - ); - $data = $result->fetchAll(PDO::FETCH_NUM); - $db->exec("DROP TABLE " . PREFIX . "filter;"); - $db->exec( - "CREATE TABLE " . - PREFIX . - "filter (id integer PRIMARY KEY AUTO_INCREMENT, filtermatch varchar(255) NOT NULL, filterreplace text NOT NULL, allowinpm smallint NOT NULL, regex smallint NOT NULL, kick smallint NOT NULL, cs smallint NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;", - ); - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "filter (filtermatch, filterreplace, allowinpm, regex, kick, cs) VALUES(?, ?, ?, ?, ?, ?);", - ); - foreach ($data as $tmp) { - $stmt->execute($tmp); - } - $result = $olddb->query( - "SELECT ign, ignby FROM " . PREFIX . "ignored;", - ); - $data = $result->fetchAll(PDO::FETCH_NUM); - $db->exec("DROP TABLE " . PREFIX . "ignored;"); - $db->exec( - "CREATE TABLE " . - PREFIX . - "ignored (id integer PRIMARY KEY AUTO_INCREMENT, ign varchar(50) NOT NULL, ignby varchar(50) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;", - ); - $stmt = $db->prepare( - "INSERT INTO " . PREFIX . "ignored (ign, ignby) VALUES(?, ?);", - ); - foreach ($data as $tmp) { - $stmt->execute($tmp); - } - $db->exec( - "CREATE INDEX " . PREFIX . "ign ON " . PREFIX . "ignored(ign);", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "ignby ON " . - PREFIX . - "ignored(ignby);", - ); - $result = $olddb->query( - "SELECT postdate, postid, poster, recipient, text FROM " . - PREFIX . - "inbox;", - ); - $data = $result->fetchAll(PDO::FETCH_NUM); - $db->exec("DROP TABLE " . PREFIX . "inbox;"); - $db->exec( - "CREATE TABLE " . - PREFIX . - "inbox (id integer PRIMARY KEY AUTO_INCREMENT, postdate integer NOT NULL, postid integer NOT NULL UNIQUE, poster varchar(50) NOT NULL, recipient varchar(50) NOT NULL, text text NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;", - ); - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "inbox (postdate, postid, poster, recipient, text) VALUES(?, ?, ?, ?, ?);", - ); - foreach ($data as $tmp) { - $stmt->execute($tmp); - } - $db->exec( - "CREATE INDEX " . - PREFIX . - "inbox_poster ON " . - PREFIX . - "inbox(poster);", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "inbox_recipient ON " . - PREFIX . - "inbox(recipient);", - ); - $result = $olddb->query( - "SELECT filtermatch, filterreplace, regex FROM " . - PREFIX . - "linkfilter;", - ); - $data = $result->fetchAll(PDO::FETCH_NUM); - $db->exec("DROP TABLE " . PREFIX . "linkfilter;"); - $db->exec( - "CREATE TABLE " . - PREFIX . - "linkfilter (id integer PRIMARY KEY AUTO_INCREMENT, filtermatch varchar(255) NOT NULL, filterreplace varchar(255) NOT NULL, regex smallint NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;", - ); - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "linkfilter (filtermatch, filterreplace, regex) VALUES(?, ?, ?);", - ); - foreach ($data as $tmp) { - $stmt->execute($tmp); - } - $result = $olddb->query( - "SELECT nickname, passhash, status, refresh, bgcolour, regedby, lastlogin, timestamps, embed, incognito, style, nocache, tz, eninbox, sortupdown, hidechatters FROM " . - PREFIX . - "members;", - ); - $data = $result->fetchAll(PDO::FETCH_NUM); - $db->exec("DROP TABLE " . PREFIX . "members;"); - $db->exec( - "CREATE TABLE " . - PREFIX . - "members (id integer PRIMARY KEY AUTO_INCREMENT, nickname varchar(50) NOT NULL UNIQUE, passhash char(32) NOT NULL, status smallint NOT NULL, refresh smallint NOT NULL, bgcolour char(6) NOT NULL, regedby varchar(50) DEFAULT '', lastlogin integer DEFAULT 0, timestamps smallint NOT NULL, embed smallint NOT NULL, incognito smallint NOT NULL, style varchar(255) NOT NULL, nocache smallint NOT NULL, tz smallint NOT NULL, eninbox smallint NOT NULL, sortupdown smallint NOT NULL, hidechatters smallint NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;", - ); - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "members (nickname, passhash, status, refresh, bgcolour, regedby, lastlogin, timestamps, embed, incognito, style, nocache, tz, eninbox, sortupdown, hidechatters) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", - ); - foreach ($data as $tmp) { - $stmt->execute($tmp); - } - $result = $olddb->query( - "SELECT postdate, poststatus, poster, recipient, text, delstatus FROM " . - PREFIX . - "messages;", - ); - $data = $result->fetchAll(PDO::FETCH_NUM); - $db->exec("DROP TABLE " . PREFIX . "messages;"); - $db->exec( - "CREATE TABLE " . - PREFIX . - "messages (id integer PRIMARY KEY AUTO_INCREMENT, postdate integer NOT NULL, poststatus smallint NOT NULL, poster varchar(50) NOT NULL, recipient varchar(50) NOT NULL, text text NOT NULL, delstatus smallint NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;", - ); - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "messages (postdate, poststatus, poster, recipient, text, delstatus) VALUES(?, ?, ?, ?, ?, ?);", - ); - foreach ($data as $tmp) { - $stmt->execute($tmp); - } - $db->exec( - "CREATE INDEX " . - PREFIX . - "poster ON " . - PREFIX . - "messages (poster);", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "recipient ON " . - PREFIX . - "messages(recipient);", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "postdate ON " . - PREFIX . - "messages(postdate);", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "poststatus ON " . - PREFIX . - "messages(poststatus);", - ); - $result = $olddb->query( - "SELECT type, lastedited, editedby, text FROM " . - PREFIX . - "notes;", - ); - $data = $result->fetchAll(PDO::FETCH_NUM); - $db->exec("DROP TABLE " . PREFIX . "notes;"); - $db->exec( - "CREATE TABLE " . - PREFIX . - "notes (id integer PRIMARY KEY AUTO_INCREMENT, type char(5) NOT NULL, lastedited integer NOT NULL, editedby varchar(50) NOT NULL, text text NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;", - ); - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "notes (type, lastedited, editedby, text) VALUES(?, ?, ?, ?);", - ); - foreach ($data as $tmp) { - $stmt->execute($tmp); - } - $result = $olddb->query( - "SELECT setting, value FROM " . PREFIX . "settings;", - ); - $data = $result->fetchAll(PDO::FETCH_NUM); - $db->exec("DROP TABLE " . PREFIX . "settings;"); - $db->exec( - "CREATE TABLE " . - PREFIX . - "settings (setting varchar(50) NOT NULL PRIMARY KEY, value text NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;", - ); - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "settings (setting, value) VALUES(?, ?);", - ); - foreach ($data as $tmp) { - $stmt->execute($tmp); - } - } catch (PDOException $e) { - send_fatal_error(_("No connection to database!")); - } - } - if ($dbversion < 33) { - $db->exec( - "CREATE TABLE " . - PREFIX . - "files (id $primary, postid integer NOT NULL UNIQUE, filename varchar(255) NOT NULL, hash char(40) NOT NULL, type varchar(255) NOT NULL, data $longtext NOT NULL)$diskengine$charset;", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "files_hash ON " . - PREFIX . - "files(hash);", - ); - $db->exec( - "INSERT INTO " . - PREFIX . - "settings (setting, value) VALUES ('enfileupload', '0'), ('msgattache', '%2\$s [%1\$s]'), ('maxuploadsize', '1024');", - ); - } - if ($dbversion < 34) { - $msg .= - "<br>" . - _( - "Note: Default CSS is now hardcoded and can be removed from the CSS setting", - ); - $db->exec( - "ALTER TABLE " . - PREFIX . - "members ADD COLUMN nocache_old smallint NOT NULL DEFAULT 0;", - ); - } - if ($dbversion < 37) { - $db->exec( - "ALTER TABLE " . - PREFIX . - "members MODIFY tz varchar(255) NOT NULL;", - ); - $db->exec("UPDATE " . PREFIX . "members SET tz='UTC';"); - $db->exec( - "UPDATE " . - PREFIX . - "settings SET value='UTC' WHERE setting='defaulttz';", - ); - } - if ($dbversion < 38) { - $db->exec( - "INSERT INTO " . - PREFIX . - "settings (setting, value) VALUES ('nextcron', '0');", - ); - $db->exec( - "DELETE FROM " . - PREFIX . - "inbox WHERE recipient NOT IN (SELECT nickname FROM " . - PREFIX . - "members);", - ); // delete inbox of members who deleted themselves - } - if ($dbversion < 39) { - $db->exec( - "INSERT INTO " . - PREFIX . - "settings (setting, value) VALUES ('personalnotes', '1');", - ); - $result = $db->query("SELECT type, id FROM " . PREFIX . "notes;"); - $data = []; - while ($tmp = $result->fetch(PDO::FETCH_NUM)) { - if ($tmp[0] === "admin") { - $tmp[0] = 0; - } else { - $tmp[0] = 1; - } - $data[] = $tmp; - } - $db->exec( - "ALTER TABLE " . PREFIX . "notes MODIFY type smallint NOT NULL;", - ); - $stmt = $db->prepare( - "UPDATE " . PREFIX . "notes SET type=? WHERE id=?;", - ); - foreach ($data as $tmp) { - $stmt->execute($tmp); - } - $db->exec( - "CREATE INDEX " . - PREFIX . - "notes_type ON " . - PREFIX . - "notes(type);", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "notes_editedby ON " . - PREFIX . - "notes(editedby);", - ); - } - if ($dbversion < 41) { - $db->exec("DROP TABLE " . PREFIX . "sessions;"); - $db->exec( - "CREATE TABLE " . - PREFIX . - "sessions (id $primary, session char(32) NOT NULL UNIQUE, nickname varchar(50) NOT NULL UNIQUE, status smallint NOT NULL, refresh smallint NOT NULL, style varchar(255) NOT NULL, lastpost integer NOT NULL, passhash varchar(255) NOT NULL, postid char(6) NOT NULL DEFAULT '000000', useragent varchar(255) NOT NULL, kickmessage varchar(255) DEFAULT '', bgcolour char(6) NOT NULL, entry integer NOT NULL, timestamps smallint NOT NULL, embed smallint NOT NULL, incognito smallint NOT NULL, ip varchar(45) NOT NULL, nocache smallint NOT NULL, tz varchar(255) NOT NULL, eninbox smallint NOT NULL, sortupdown smallint NOT NULL, hidechatters smallint NOT NULL, nocache_old smallint NOT NULL)$memengine$charset;", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "status ON " . - PREFIX . - "sessions(status);", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "lastpost ON " . - PREFIX . - "sessions(lastpost);", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "incognito ON " . - PREFIX . - "sessions(incognito);", - ); - $result = $db->query( - "SELECT nickname, passhash, status, refresh, bgcolour, regedby, lastlogin, timestamps, embed, incognito, style, nocache, nocache_old, tz, eninbox, sortupdown, hidechatters FROM " . - PREFIX . - "members;", - ); - $members = $result->fetchAll(PDO::FETCH_NUM); - $result = $db->query( - "SELECT postdate, postid, poster, recipient, text FROM " . - PREFIX . - "inbox;", - ); - $inbox = $result->fetchAll(PDO::FETCH_NUM); - $db->exec("DROP TABLE " . PREFIX . "inbox;"); - $db->exec("DROP TABLE " . PREFIX . "members;"); - $db->exec( - "CREATE TABLE " . - PREFIX . - "members (id $primary, nickname varchar(50) NOT NULL UNIQUE, passhash varchar(255) NOT NULL, status smallint NOT NULL, refresh smallint NOT NULL, bgcolour char(6) NOT NULL, regedby varchar(50) DEFAULT '', lastlogin integer DEFAULT 0, timestamps smallint NOT NULL, embed smallint NOT NULL, incognito smallint NOT NULL, style varchar(255) NOT NULL, nocache smallint NOT NULL, nocache_old smallint NOT NULL, tz varchar(255) NOT NULL, eninbox smallint NOT NULL, sortupdown smallint NOT NULL, hidechatters smallint NOT NULL)$diskengine$charset", - ); - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "members (nickname, passhash, status, refresh, bgcolour, regedby, lastlogin, timestamps, embed, incognito, style, nocache, nocache_old, tz, eninbox, sortupdown, hidechatters) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", - ); - foreach ($members as $tmp) { - $stmt->execute($tmp); - } - $db->exec( - "CREATE TABLE " . - PREFIX . - "inbox (id $primary, postdate integer NOT NULL, postid integer NOT NULL UNIQUE, poster varchar(50) NOT NULL, recipient varchar(50) NOT NULL, text text NOT NULL)$diskengine$charset;", - ); - $stmt = $db->prepare( - "INSERT INTO " . - PREFIX . - "inbox (postdate, postid, poster, recipient, text) VALUES(?, ?, ?, ?, ?);", - ); - foreach ($inbox as $tmp) { - $stmt->execute($tmp); - } - $db->exec( - "CREATE INDEX " . - PREFIX . - "inbox_poster ON " . - PREFIX . - "inbox(poster);", - ); - $db->exec( - "CREATE INDEX " . - PREFIX . - "inbox_recipient ON " . - PREFIX . - "inbox(recipient);", - ); - $db->exec( - "ALTER TABLE " . - PREFIX . - "inbox ADD FOREIGN KEY (recipient) REFERENCES " . - PREFIX . - "members(nickname) ON DELETE CASCADE ON UPDATE CASCADE;", - ); - } - if ($dbversion < 42) { - $db->exec( - "INSERT IGNORE INTO " . - PREFIX . - "settings (setting, value) VALUES ('filtermodkick', '1');", - ); - } - if ($dbversion < 43) { - $stmt = $db->prepare( - "INSERT IGNORE INTO " . - PREFIX . - "settings (setting, value) VALUES ('metadescription', ?);", - ); - $stmt->execute([_("A chat community")]); - } - if ($dbversion < 44) { - $db->exec( - "INSERT INTO " . - PREFIX . - "settings (setting,value) VALUES ('publicnotes', '0');", - ); - } - if ($dbversion < 45) { - $db->exec( - "INSERT INTO " . - PREFIX . - "settings (setting,value) VALUES ('memkickalways', '0'), ('sysmessagetxt', 'ā„¹ļø &nbsp;'),('namedoers', '1');", - ); - } - if ($dbversion < 46) { - $db->exec( - "ALTER TABLE " . - PREFIX . - "members ADD COLUMN loginfails integer unsigned NOT NULL DEFAULT 0;", - ); - } - if ($dbversion < 47) { - $db->exec( - "INSERT INTO " . - PREFIX . - "settings (setting,value) VALUES ('hide_reload_post_box', '0'), ('hide_reload_messages', '0'),('hide_profile', '0'),('hide_admin', '0'),('hide_notes', '0'),('hide_clone', '0'),('hide_rearrange', '0'),('hide_help', '0'),('max_refresh_rate', '150'),('min_refresh_rate', '5'),('postbox_delete_globally', '0'),('allow_js', '1');", - ); - } - if ($dbversion < 48) { - $db->exec( - "INSERT INTO " . - PREFIX . - "settings (setting, value) VALUES ('exitwait', '180'), ('exitingtxt', ' &#128682;", - ); // door emoji - $db->exec( - "ALTER TABLE " . - PREFIX . - "sessions ADD COLUMN exiting smallint NOT NULL DEFAULT 0;", - ); - } - if ($dbversion < 49) { - $db->exec( - "INSERT INTO " . - PREFIX . - "settings (setting, value) VALUES ('captchattfont', '/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf');", - ); - } - if ($dbversion < 50) { - $db->exec( - "INSERT INTO " . - PREFIX . - "settings (setting, value) VALUES ('noguestpm', '0');", - ); - } - update_setting("dbversion", DBVERSION); - if ($msgencrypted !== MSGENCRYPTED) { - if (!extension_loaded("sodium")) { - send_fatal_error( - sprintf( - _( - "The %s extension of PHP is required for the encryption feature. Please install it first or set the encrypted setting back to false.", - ), - "sodium", - ), - ); - } - $result = $db->query("SELECT id, text FROM " . PREFIX . "messages;"); - $stmt = $db->prepare( - "UPDATE " . PREFIX . "messages SET text=? WHERE id=?;", - ); - while ($message = $result->fetch(PDO::FETCH_ASSOC)) { - try { - if (MSGENCRYPTED) { - $message["text"] = base64_encode( - sodium_crypto_aead_aes256gcm_encrypt( - $message["text"], - "", - AES_IV, - ENCRYPTKEY, - ), - ); - } else { - $message["text"] = sodium_crypto_aead_aes256gcm_decrypt( - base64_decode($message["text"]), - null, - AES_IV, - ENCRYPTKEY, - ); - } - } catch (SodiumException $e) { - send_error($e->getMessage()); - } - $stmt->execute([$message["text"], $message["id"]]); - } - $result = $db->query("SELECT id, text FROM " . PREFIX . "notes;"); - $stmt = $db->prepare( - "UPDATE " . PREFIX . "notes SET text=? WHERE id=?;", - ); - while ($message = $result->fetch(PDO::FETCH_ASSOC)) { - try { - if (MSGENCRYPTED) { - $message["text"] = base64_encode( - sodium_crypto_aead_aes256gcm_encrypt( - $message["text"], - "", - AES_IV, - ENCRYPTKEY, - ), - ); - } else { - $message["text"] = sodium_crypto_aead_aes256gcm_decrypt( - base64_decode($message["text"]), - null, - AES_IV, - ENCRYPTKEY, - ); - } - } catch (SodiumException $e) { - send_error($e->getMessage()); - } - $stmt->execute([$message["text"], $message["id"]]); - } - update_setting("msgencrypted", (int) MSGENCRYPTED); - } - send_update($msg); -} - -function get_setting(string $setting): string -{ - global $db, $memcached; - $value = ""; - if ( - $db instanceof PDO && - (!MEMCACHED || - !($value = $memcached->get( - DBNAME . "-" . PREFIX . "settings-$setting", - ))) - ) { - try { - $stmt = $db->prepare( - "SELECT value FROM " . PREFIX . "settings WHERE setting=?;", - ); - $stmt->execute([$setting]); - $stmt->bindColumn(1, $value); - $stmt->fetch(PDO::FETCH_BOUND); - if (MEMCACHED) { - $memcached->set( - DBNAME . "-" . PREFIX . "settings-$setting", - $value, - ); - } - } catch (Exception $e) { - return ""; - } - } - return $value; -} - -function update_setting(string $setting, $value): void -{ - global $db, $memcached; - $stmt = $db->prepare( - "UPDATE " . PREFIX . "settings SET value=? WHERE setting=?;", - ); - $stmt->execute([$value, $setting]); - if (MEMCACHED) { - $memcached->set(DBNAME . "-" . PREFIX . "settings-$setting", $value); - } -} - -// configuration, defaults and internals - -function check_db(): void -{ - global $db, $memcached; - $options = [ - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_PERSISTENT => PERSISTENT, - ]; - try { - if (DBDRIVER === 0) { - if (!extension_loaded("pdo_mysql")) { - send_fatal_error( - sprintf( - _( - "The %s extension of PHP is required for the selected database driver. Please install it first.", - ), - "pdo_mysql", - ), - ); - } - $db = new PDO( - "mysql:host=" . - DBHOST . - ";dbname=" . - DBNAME . - ";charset=utf8mb4", - DBUSER, - DBPASS, - $options, - ); - } elseif (DBDRIVER === 1) { - if (!extension_loaded("pdo_pgsql")) { - send_fatal_error( - sprintf( - _( - "The %s extension of PHP is required for the selected database driver. Please install it first.", - ), - "pdo_pgsql", - ), - ); - } - $db = new PDO( - "pgsql:host=" . DBHOST . ";dbname=" . DBNAME, - DBUSER, - DBPASS, - $options, - ); - } else { - if (!extension_loaded("pdo_sqlite")) { - send_fatal_error( - sprintf( - _( - "The %s extension of PHP is required for the selected database driver. Please install it first.", - ), - "pdo_sqlite", - ), - ); - } - $db = new PDO("sqlite:" . SQLITEDBFILE, null, null, $options); - $db->exec("PRAGMA foreign_keys = ON;"); - } - } catch (PDOException $e) { - try { - //Attempt to create database - if (DBDRIVER === 0) { - $db = new PDO("mysql:host=" . DBHOST, DBUSER, DBPASS, $options); - if (false !== $db->exec("CREATE DATABASE " . DBNAME)) { - $db = new PDO( - "mysql:host=" . - DBHOST . - ";dbname=" . - DBNAME . - ";charset=utf8mb4", - DBUSER, - DBPASS, - $options, - ); - } else { - send_fatal_error( - _( - "No connection to database, please create a database and edit the script to use the correct database with given username and password!", - ), - ); - } - } elseif (DBDRIVER === 1) { - $db = new PDO("pgsql:host=" . DBHOST, DBUSER, DBPASS, $options); - if (false !== $db->exec("CREATE DATABASE " . DBNAME)) { - $db = new PDO( - "pgsql:host=" . DBHOST . ";dbname=" . DBNAME, - DBUSER, - DBPASS, - $options, - ); - } else { - send_fatal_error( - _( - "No connection to database, please create a database and edit the script to use the correct database with given username and password!", - ), - ); - } - } else { - if ( - isset($_REQUEST["action"]) && - $_REQUEST["action"] === "setup" - ) { - send_fatal_error( - _( - "No connection to database, please create a database and edit the script to use the correct database with given username and password!", - ), - ); - } else { - send_fatal_error(_("No connection to database!")); - } - } - } catch (PDOException $e) { - if (isset($_REQUEST["action"]) && $_REQUEST["action"] === "setup") { - send_fatal_error( - _( - "No connection to database, please create a database and edit the script to use the correct database with given username and password!", - ), - ); - } else { - send_fatal_error(_("No connection to database!")); - } - } - } - if (MEMCACHED) { - if (!extension_loaded("memcached")) { - send_fatal_error( - _( - "The memcached extension of PHP is required for the caching feature. Please install it first or set the memcached setting back to false.", - ), - ); - } - $memcached = new Memcached(); - $memcached->addServer(MEMCACHEDHOST, MEMCACHEDPORT); - } - if (!isset($_REQUEST["action"]) || $_REQUEST["action"] === "setup") { - if (!check_init()) { - send_init(); - } - update_db(); - } elseif ($_REQUEST["action"] === "init") { - init_chat(); - } -} - -function load_fonts(): array -{ - return [ - "Arial" => "font-family:Arial,Helvetica,sans-serif;", - "Book Antiqua" => "font-family:'Book Antiqua','MS Gothic',serif;", - "Comic" => "font-family:'Comic Sans MS',Papyrus,sans-serif;", - "Courier" => "font-family:'Courier New',Courier,monospace;", - "Cursive" => "font-family:Cursive,Papyrus,sans-serif;", - "Fantasy" => "font-family:Fantasy,Futura,Papyrus,sans;", - "Garamond" => "font-family:Garamond,Palatino,serif;", - "Georgia" => "font-family:Georgia,'Times New Roman',Times,serif;", - "Serif" => "font-family:'MS Serif','New York',serif;", - "System" => "font-family:System,Chicago,sans-serif;", - "Times New Roman" => "font-family:'Times New Roman',Times,serif;", - "Verdana" => "font-family:Verdana,Geneva,Arial,Helvetica,sans-serif;", - ]; -} - -function load_lang(): void -{ - global $language, $locale, $dir; - if (isset($_REQUEST["lang"]) && isset(LANGUAGES[$_REQUEST["lang"]])) { - $locale = LANGUAGES[$_REQUEST["lang"]]["locale"]; - $language = $_REQUEST["lang"]; - $dir = LANGUAGES[$_REQUEST["lang"]]["dir"]; - set_secure_cookie("language", $language); - } elseif ( - isset($_COOKIE["language"]) && - isset(LANGUAGES[$_COOKIE["language"]]) - ) { - $locale = LANGUAGES[$_COOKIE["language"]]["locale"]; - $language = $_COOKIE["language"]; - $dir = LANGUAGES[$_COOKIE["language"]]["dir"]; - } elseif ( - !empty($_SERVER["HTTP_ACCEPT_LANGUAGE"]) && - extension_loaded("intl") - ) { - $prefLocales = array_reduce( - explode(",", $_SERVER["HTTP_ACCEPT_LANGUAGE"]), - function (array $res, string $el) { - [$l, $q] = array_merge(explode(";q=", $el), [1]); - $res[$l] = (float) $q; - return $res; - }, - [], - ); - arsort($prefLocales); - foreach ($prefLocales as $l => $q) { - $lang = locale_lookup(array_keys(LANGUAGES), $l); - if (!empty($lang)) { - $locale = LANGUAGES[$lang]["locale"]; - $language = $lang; - $dir = LANGUAGES[$lang]["dir"]; - set_secure_cookie("language", $language); - break; - } - } - } - if (function_exists("putenv")) { - putenv("LC_ALL=" . $locale); - } - setlocale(LC_ALL, $locale); - bindtextdomain("le-chat-php", __DIR__ . "/locale"); - bind_textdomain_codeset("le-chat-php", "UTF-8"); - textdomain("le-chat-php"); -} - -function load_config(): void -{ - define("VERSION", "1.24.1"); // Script version - define("DBVERSION", 50); // Database layout version - define("MSGENCRYPTED", false); // Store messages encrypted in the database to prevent other database users from reading them - true/false - visit the setup page after editing! - define("ENCRYPTKEY_PASS", "MY_SECRET_KEY"); // Recommended length: 32. Encryption key for messages - define("AES_IV_PASS", "012345678912"); // Recommended length: 12. AES Encryption IV - define("DBHOST", "localhost"); // Database host - define("DBUSER", "www-data"); // Database user - define("DBPASS", "YOUR_DB_PASS"); // Database password - define("DBNAME", "public_chat"); // Database - define("PERSISTENT", true); // Use persistent database conection true/false - define("PREFIX", ""); // Prefix - Set this to a unique value for every chat, if you have more than 1 chats on the same database or domain - use only alpha-numeric values (A-Z, a-z, 0-9, or _) other symbols might break the queries - define("MEMCACHED", false); // Enable/disable memcached caching true/false - needs memcached extension and a memcached server. - if (MEMCACHED) { - define("MEMCACHEDHOST", "localhost"); // Memcached host - define("MEMCACHEDPORT", "11211"); // Memcached port - } - define("DBDRIVER", 0); // Selects the database driver to use - 0=MySQL, 1=PostgreSQL, 2=sqlite - if (DBDRIVER === 2) { - define("SQLITEDBFILE", "public_chat.sqlite"); // Filepath of the sqlite database, if sqlite is used - make sure it is writable for the webserver user - } - define("COOKIENAME", PREFIX . "chat_session"); // Cookie name storing the session information - define("LANG", "en"); // Default language - if (MSGENCRYPTED) { - if (version_compare(PHP_VERSION, "7.2.0") < 0) { - die("You need at least PHP >= 7.2.x"); - } - //Do not touch: Compute real keys needed by encryption functions - if (strlen(ENCRYPTKEY_PASS) !== SODIUM_CRYPTO_AEAD_AES256GCM_KEYBYTES) { - define( - "ENCRYPTKEY", - substr( - hash("sha512/256", ENCRYPTKEY_PASS), - 0, - SODIUM_CRYPTO_AEAD_AES256GCM_KEYBYTES, - ), - ); - } else { - define("ENCRYPTKEY", ENCRYPTKEY_PASS); - } - if (strlen(AES_IV_PASS) !== SODIUM_CRYPTO_AEAD_AES256GCM_NPUBBYTES) { - define( - "AES_IV", - substr( - hash("sha512/256", AES_IV_PASS), - 0, - SODIUM_CRYPTO_AEAD_AES256GCM_NPUBBYTES, - ), - ); - } else { - define("AES_IV", AES_IV_PASS); - } - } - //define('RESET_SUPERADMIN_PASSWORD', 'changeme'); //Use this to reset your superadmin password in case you forgot it -} diff --git a/demo.png b/demo.png Binary files differ. diff --git a/manpage/bhcli.1 b/manpage/bhcli.1 @@ -0,0 +1,488 @@ +.TH BHCLI 1 "January 2026" "BHCLI 1.0" "User Commands" +.SH NAME +bhcli \- terminal client for le-chat-php chat systems +.SH SYNOPSIS +.B bhcli +[\fIOPTIONS\fR] +.SH DESCRIPTION +.B bhcli +is a sophisticated terminal-based client for anonymous communication over Tor. It connects to any le-chat-php chat server with full feature parity plus advanced capabilities like AI integration, bot automation, and developer tools. +.PP +Built for the darknet, optimized for Tor, works everywhere. +.SH OPTIONS +.SS "Authentication" +.TP +.BR \-u ", " \-\-username " \fIUSERNAME\fR" +Set username (env: BHC_USERNAME) +.TP +.BR \-p ", " \-\-password " \fIPASSWORD\fR" +Set password (env: BHC_PASSWORD) +.TP +.BR \-c ", " \-\-profile " \fIPROFILE\fR" +Select configuration profile (default: "default") +.TP +.BR \-\-session " \fISESSION\fR" +Use existing session ID to skip login +.SS "Connection" +.TP +.BR \-\-url " \fIURL\fR" +Override chat server URL +.TP +.BR \-\-page\-php " \fIPAGE\fR" +Override chat page filename (default: chat.php) +.TP +.BR \-s ", " \-\-socks\-proxy\-url " \fIURL\fR" +SOCKS proxy URL (default: socks5h://127.0.0.1:9050, env: BHC_PROXY_URL) +.TP +.BR \-\-no\-proxy +Disable proxy usage +.TP +.BR \-r ", " \-\-refresh\-rate " \fISECONDS\fR" +Message refresh rate (default: 5, env: BHC_REFRESH_RATE) +.SS "Bot System" +.TP +.BR \-\-bot " \fINAME\fR" +Enable background bot with specified name +.TP +.BR \-\-bot\-admins " \fIUSERS\fR" +Comma-separated list of bot administrators +.TP +.BR \-\-bot\-data\-dir " \fIPATH\fR" +Custom directory for bot data storage +.SS "Display" +.TP +.BR \-g ", " \-\-guest\-color " \fICOLOR\fR" +Set guest color theme +.TP +.BR \-m ", " \-\-manual\-captcha +Enable manual captcha solving (env: BHC_MANUAL_CAPTCHA) +.TP +.BR \-\-sxiv +Enable sxiv image viewer integration +.SS "Integrations" +.TP +.BR \-\-dkf\-api\-key " \fIKEY\fR" +DKF API key for notifications (env: DKF_API_KEY) +.TP +.BR \-\-dnmx\-username " \fIUSERNAME\fR" +DNMX email username (env: DNMX_USERNAME) +.TP +.BR \-\-dnmx\-password " \fIPASSWORD\fR" +DNMX email password (env: DNMX_PASSWORD) +.SH KEYBOARD SHORTCUTS +.SS "Navigation" +.TP +.BR j ", " "down arrow" +Move down one message +.TP +.BR k ", " "up arrow" +Move up one message +.TP +.BR J +Jump down 5 messages +.TP +.BR K +Jump up 5 messages +.TP +.BR gg +Jump to top message +.TP +.BR "ctrl+d" +Page down +.TP +.BR "ctrl+u" +Page up +.SS "Quick Actions" +.TP +.BR t +Tag author of selected message +.TP +.BR p +Start PM to author +.TP +.BR y ", " "ctrl+c" +Copy selected message +.TP +.BR "shift+Y" +Copy first link in message +.TP +.BR d +Download embedded file +.TP +.BR D +Download and open file +.SS "View Toggles" +.TP +.BR m +Toggle sound notifications +.TP +.BR "shift+G" +Toggle guest view +.TP +.BR "shift+M" +Toggle members view +.TP +.BR "ctrl+h" +Toggle hidden messages +.TP +.BR backspace +Hide selected message +.SS "Input Editing" +.TP +.BR "ctrl+a" +Move to start of line +.TP +.BR "ctrl+e" +Move to end of line +.TP +.BR "ctrl+f" +Move forward one word +.TP +.BR "ctrl+b" +Move backward one word +.TP +.BR "ctrl+l" +Toggle multiline mode +.TP +.BR "ctrl+." ", " "ctrl+x" ", " "ctrl+o" +Open external editor +.TP +.BR "tab" +Autocomplete username +.SS "Moderation (Members+)" +.TP +.BR "ctrl+k" +Prefill kick command +.TP +.BR "ctrl+b" +Prefill ban command +.TP +.BR "ctrl+a" +Prefill members message +.TP +.BR "ctrl+w" +Send warning message +.SH CHAT COMMANDS +.SS "Basic" +.TP +.BR "/pm \fIUSER\fR \fIMESSAGE\fR" +Send private message +.TP +.BR "/ignore \fIUSER\fR" +Ignore user's messages +.TP +.BR "/unignore \fIUSER\fR" +Unignore user +.TP +.BR "/f \fITERMS\fR" +Filter messages by content +.TP +.BR "/u \fIPATH\fR [@\fIUSER\fR] [\fIMESSAGE\fR]" +Upload file +.SS "Moderation (Members+)" +.TP +.BR "/kick \fIUSER\fR [\fIREASON\fR]" +Kick user from chat +.TP +.BR "/ban \fIUSER\fR" +Ban user (fuzzy match) +.TP +.BR "/ban \(dq\fIUSER\fR\(dq" +Ban exact username +.TP +.BR "/filter \fITEXT\fR" +Add message filter +.TP +.BR "/unban \fIUSER\fR" +Remove ban +.TP +.BR "/unfilter \fITEXT\fR" +Remove filter +.TP +.BR "/banlist" +Show banned users +.TP +.BR "/filterlist" +Show filtered terms +.TP +.BR "/dl[\fIN\fR]" +Delete last N messages (default: 1) +.TP +.BR "/dall" +Delete all your messages +.SS "ChatOps (30+ commands)" +.TP +.BR "/help [\fICOMMAND\fR]" +List commands or get detailed help +.TP +.BR "/man \fICOMMAND\fR" +System manual pages +.TP +.BR "/doc \fILANG\fR \fITERM\fR" +Language documentation +.TP +.BR "/hash \fIALGO\fR \fITEXT\fR" +Generate hash (md5/sha1/sha256/sha512) +.TP +.BR "/uuid" +Generate UUID +.TP +.BR "/base64 \fIencode|decode\fR \fITEXT\fR" +Base64 encoding/decoding +.TP +.BR "/regex \fIPATTERN\fR \fITEXT\fR" +Test regex pattern +.TP +.BR "/github \fIUSER/REPO\fR" +Repository information +.TP +.BR "/crates \fINAME\fR" +Rust crate info +.TP +.BR "/npm \fINAME\fR" +NPM package info +.TP +.BR "/pip \fINAME\fR" +Python package info +.TP +.BR "/ping \fIHOST\fR" +Test connectivity +.TP +.BR "/dig \fIDOMAIN\fR" +DNS lookup +.TP +.BR "/whois \fIDOMAIN\fR" +Domain registration info +.SS "AI Features (requires OPENAI_API_KEY)" +.TP +.BR "/ai \fIoff|mod|reply\fR" +Control AI mode +.TP +.BR "/ai \fIstrict|balanced|lenient\fR" +Set moderation strictness +.TP +.BR "/translate \fILANG\fR \fITEXT\fR" +Translate text +.TP +.BR "/detect \fITEXT\fR" +Detect language +.TP +.BR "/sentiment \fITEXT\fR" +Analyze sentiment +.TP +.BR "/summarize \fITEXT\fR" +Summarize text +.TP +.BR "/fix \fICODE\fR" +Get code fix suggestions +.TP +.BR "/review \fICODE\fR" +Code quality review +.SH BOT COMMANDS +.PP +Interact with bots by mentioning them in chat. Admin-only unless noted. +.TP +.BR "@\fIBOT\fR help" +List bot commands (anyone) +.TP +.BR "@\fIBOT\fR stats \fIUSER\fR" +View user statistics +.TP +.BR "@\fIBOT\fR recall \fITIME\fR" +Find messages from specific time +.TP +.BR "@\fIBOT\fR search \fIQUERY\fR" +Search message history +.TP +.BR "@\fIBOT\fR export \fIUSER\fR \fIDAYS\fR" +Export user messages +.TP +.BR "@\fIBOT\fR restore \fIMSG_ID\fR" +Restore deleted message +.TP +.BR "@\fIBOT\fR summary \fIHOURS\fR" +Chat activity summary +.TP +.BR "@\fIBOT\fR users" +List online users +.SH CONFIGURATION +.PP +Configuration file: \fB~/.config/bhcli/bhcli.toml\fR +.PP +Example profile configuration: +.PP +.nf +.RS +[profiles] + +[profiles.default] +username = "yourusername" +password = "yourpassword" + +[commands] +hello = "hey everyone!" +afk = "away for a bit" +.RE +.fi +.SH CUSTOM COMMANDS +.PP +Create shortcuts in config file under [commands] section. Use them with ! prefix: +.PP +.nf +.RS +!hello +!afk +!rules +.RE +.fi +.SH ENVIRONMENT VARIABLES +.TP +.BR BHC_USERNAME +Default username +.TP +.BR BHC_PASSWORD +Default password +.TP +.BR BHC_PROXY_URL +SOCKS proxy URL +.TP +.BR BHC_REFRESH_RATE +Message refresh rate in seconds +.TP +.BR BHC_MANUAL_CAPTCHA +Enable manual captcha mode +.TP +.BR OPENAI_API_KEY +OpenAI API key for AI features +.TP +.BR DKF_API_KEY +DKF API key for notifications +.TP +.BR DNMX_USERNAME +DNMX email username +.TP +.BR DNMX_PASSWORD +DNMX email password +.SH FILES +.TP +.BR ~/.config/bhcli/bhcli.toml +Main configuration file +.TP +.BR ./bhcli.log +Log file (created in current directory) +.TP +.BR ./bot_data/ +Bot data storage directory +.SH EXAMPLES +.PP +Basic usage with credentials: +.PP +.RS +.nf +bhcli -u myusername -p mypassword +.fi +.RE +.PP +Use specific profile: +.PP +.RS +.nf +bhcli -c myprofile +.fi +.RE +.PP +Connect through custom Tor port: +.PP +.RS +.nf +bhcli -s socks5h://127.0.0.1:9150 +.fi +.RE +.PP +Direct connection (not recommended for darknet): +.PP +.RS +.nf +bhcli --no-proxy +.fi +.RE +.PP +Run with bot and admins: +.PP +.RS +.nf +bhcli --bot Assistant --bot-admins alice,bob +.fi +.RE +.PP +Using environment variables: +.PP +.RS +.nf +export BHC_USERNAME="myuser" +export BHC_PASSWORD="mypass" +export OPENAI_API_KEY="sk-..." +bhcli +.fi +.RE +.SH SECURITY +.PP +BHCLI is designed for anonymous communication: +.IP \[bu] 2 +Always use Tor for darknet chats +.IP \[bu] +Never use --no-proxy with .onion addresses +.IP \[bu] +No telemetry or phone-home +.IP \[bu] +Local storage only +.IP \[bu] +Optional features minimize attack surface +.PP +For maximum security, route through Tor and use unique credentials per chat. +.SH BUILDING +.PP +Build from source: +.PP +.RS +.nf +cargo build --release +make release +.fi +.RE +.PP +Build without audio for headless servers: +.PP +.RS +.nf +cargo build --release --no-default-features +make build-no-audio +.fi +.RE +.PP +Cross-platform builds: +.PP +.RS +.nf +make build-linux-musl +make build-macos +make build-windows +make build-all +.fi +.RE +.SH SEE ALSO +.PP +Full documentation: \fBMANUAL.md\fR +.PP +Optimization report: \fBOPTIMIZATION_REPORT.md\fR +.PP +Official repo: https://github.com/d-a-s-h-o/bhcli +.SH BUGS +.PP +Report bugs at official mirror or GitHub issues. +.SH AUTHOR +.PP +Built with ā¤ļø by Dasho. +.SH COPYRIGHT +.PP +MIT License. See LICENSE file for details. diff --git a/screenshot.png b/screenshot.png Binary files differ. diff --git a/src/account_management.rs b/src/account_management.rs @@ -76,15 +76,6 @@ impl AccountManager { self.setup_default_delegated_commands(); } - /// Clear account relationships - pub fn clear_relationships(&mut self) { - self.master_account = None; - self.alt_account = None; - self.is_master = false; - *self.last_verified_together.borrow_mut() = None; - self.delegated_commands.clear(); - } - /// Check the current relationship status pub fn get_relationship_status(&self, users: &Arc<Mutex<Users>>) -> AccountRelationshipStatus { if self.master_account.is_none() && self.alt_account.is_none() { @@ -194,7 +185,7 @@ impl AccountManager { if args.len() >= 2 { let target = args[0]; let message = args[1..].join(" "); - if let Some(master) = &self.master_account { + if self.master_account.is_some() { Some(format!("/pm {} [via {}] {}", target, self.current_user, message)) } else { None @@ -266,16 +257,6 @@ impl AccountManager { } } - /// Add a custom delegated command - pub fn add_delegated_command(&mut self, alias: String, template: String) { - self.delegated_commands.insert(alias, template); - } - - /// Remove a custom delegated command - pub fn remove_delegated_command(&mut self, alias: &str) -> bool { - self.delegated_commands.remove(alias).is_some() - } - /// Set up default delegated commands fn setup_default_delegated_commands(&mut self) { self.delegated_commands.clear(); @@ -352,22 +333,6 @@ impl AccountManager { } } -/// Helper function to parse forwarded commands from alt accounts -pub fn parse_alt_forwarded_command(message: &str, alt_account: &str) -> Option<(String, Vec<String>)> { - // Look for patterns like "[via altname] /command args" - let prefix = format!("[via {}] /", alt_account); - if message.starts_with(&prefix) { - let command_part = &message[prefix.len()..]; - let parts: Vec<&str> = command_part.split_whitespace().collect(); - if !parts.is_empty() { - let command = parts[0].to_string(); - let args: Vec<String> = parts[1..].iter().map(|s| s.to_string()).collect(); - return Some((command, args)); - } - } - None -} - /// Enhanced command parsing that handles master/alt delegation pub fn parse_enhanced_command( input: &str, diff --git a/src/main.rs b/src/main.rs @@ -55,6 +55,7 @@ use regex::Regex; use reqwest::blocking::multipart; use reqwest::blocking::Client; use reqwest::redirect::Policy; +#[cfg(feature = "audio")] use rodio::{source::Source, Decoder, OutputStream}; use select::document::Document; use select::predicate::{Attr, Name}; @@ -259,6 +260,10 @@ struct Opts { bot_admins: Vec<String>, #[arg(long)] bot_data_dir: Option<String>, + + // Use 404 chatroom profile + #[arg(long = "404")] + use_404: bool, } struct LeChatPHPConfig { @@ -281,6 +286,17 @@ impl LeChatPHPConfig { staffs_tag: "[Staff] ".to_owned(), } } + + fn new_404_chatroom_not_found_config() -> Self { + Self { + url: "http://4o4o4hn4hsujpnbsso7tqigujuokafxys62thulbk2k3mf46vq22qfqd.onion/chat/min".to_owned(), + datetime_fmt: "%Y-%m-%d %H:%M:%S".to_owned(), + page_php: "index.php".to_owned(), + keepalive_send_to: "0".to_owned(), + members_tag: "[M] ".to_owned(), + staffs_tag: "[Staff] ".to_owned(), + } + } } struct BaseClient { @@ -553,7 +569,11 @@ impl LeChatPHPClient { let ai_service = Arc::clone(&self.ai_service); let bot_manager = self.bot_manager.clone(); thread::spawn(move || { - let (_stream, stream_handle) = OutputStream::try_default().unwrap(); + #[cfg(feature = "audio")] + let audio_output = OutputStream::try_default().ok(); + #[cfg(feature = "audio")] + let stream_handle = audio_output.as_ref().map(|(_, handle)| handle); + loop { let mut should_notify = false; @@ -595,9 +615,13 @@ impl LeChatPHPClient { let muted = { *is_muted.lock().unwrap() }; if should_notify && !muted { - let source = Decoder::new_mp3(Cursor::new(SOUND1)).unwrap(); - if let Err(err) = stream_handle.play_raw(source.convert_samples()) { - log::error!("{}", err); + #[cfg(feature = "audio")] + if let Some(handle) = &stream_handle { + if let Ok(source) = Decoder::new_mp3(Cursor::new(SOUND1)) { + if let Err(err) = handle.play_raw(source.convert_samples()) { + log::error!("Audio playback error: {}", err); + } + } } } @@ -647,7 +671,8 @@ impl LeChatPHPClient { let (events, h4) = Events::with_config(Config { messages_updated_rx, exit_rx: sig.lock().unwrap().clone(), - tick_rate: Duration::from_millis(250), + // Increased from 250ms to 500ms to reduce CPU usage significantly + tick_rate: Duration::from_millis(500), }); loop { @@ -1864,7 +1889,7 @@ impl LeChatPHPClient { format!("@{}", msg) }; let end_msg = format!( - "This is your warning - {}, will be kicked next. Please read the !-rules / https://4-0-4.io/bhc-rules", + "This is your warning - {}, will be kicked next. Please read the !-rules.", msg ); self.post_msg(PostType::Post(end_msg, None)).unwrap(); @@ -3583,295 +3608,6 @@ Connection: } } - fn handle_editing_mode_key_event_external_editor( - &mut self, - app: &mut App, - users: &Arc<Mutex<Users>>, - ) -> Result<(), ExitSignal> { - use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; - use crossterm::{ - execute, - terminal::{Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}, - }; - use std::fs; - use std::io::{stdout, Write}; - use std::process::{Command, Stdio}; - use tempfile::NamedTempFile; - - // Create a temporary file with .txt extension for better editor recognition - let mut temp_file = match NamedTempFile::with_suffix(".txt") { - Ok(file) => file, - Err(e) => { - log::error!("Failed to create temp file: {}", e); - return Ok(()); - } - }; - - // Write current input content to the temp file - if !app.input.is_empty() { - if let Err(e) = temp_file.write_all(app.input.as_bytes()) { - log::error!("Failed to write to temp file: {}", e); - return Ok(()); - } - if let Err(e) = temp_file.flush() { - log::error!("Failed to flush temp file: {}", e); - return Ok(()); - } - } - - // Get the temp file path and keep temp_file alive - let temp_path = match temp_file.path().to_str() { - Some(path) => path.to_string(), - None => { - log::error!("Failed to get temp file path"); - return Ok(()); - } - }; - - // Save the current input to restore if editor fails - let original_input = app.input.clone(); - let original_input_idx = app.input_idx; - - // Completely shut down the TUI application first - let _ = disable_raw_mode(); - let _ = execute!(stdout(), LeaveAlternateScreen, Clear(ClearType::All)); - let _ = stdout().flush(); - - // Print a clear message to the terminal - println!("\nšŸš€ Launching external editor...\n"); - - // Determine which editor to use - let editor = std::env::var("EDITOR").unwrap_or_else(|_| { - for editor in &["nvim", "vim", "nano", "vi"] { - if Command::new("which") - .arg(editor) - .output() - .map_or(false, |o| o.status.success()) - { - return editor.to_string(); - } - } - "vi".to_string() - }); - - // Launch the editor as a completely independent process - // Give the editor complete control of the terminal - let status = Command::new(&editor) - .arg(&temp_path) - .stdin(Stdio::inherit()) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .status(); - - // Editor has finished - immediately restart TUI without waiting for input - // Editor has finished - immediately restart TUI without waiting for input - println!("šŸ“ Editor closed. Returning to chat...\n"); - - // Small delay to let user see the message - std::thread::sleep(std::time::Duration::from_millis(500)); - - // Immediately restart the TUI - no user input required - if let Err(e) = enable_raw_mode() { - log::error!("Failed to re-enable raw mode: {}", e); - } - if let Err(e) = execute!(stdout(), EnterAlternateScreen) { - log::error!("Failed to enter alternate screen: {}", e); - } - - // Force a complete screen refresh - if let Err(e) = execute!(stdout(), Clear(ClearType::All)) { - log::error!("Failed to clear screen: {}", e); - } - if let Err(e) = stdout().flush() { - log::error!("Failed to flush stdout: {}", e); - } - - // Process the editor results - match status { - Ok(exit_status) if exit_status.success() => { - // Read and process the edited content - match fs::read_to_string(&temp_path) { - Ok(content) => { - let content = content.trim_end().to_string(); - - if !content.is_empty() { - // Add to history if not empty - app.add_to_history(content.clone()); - - // Process and send the message directly - let mut processed_content = replace_newline_escape(&content); - - // Check for commands and execute them - for (command, action) in &app.commands.commands { - let expected_input = format!("!{}", command); - if processed_content == expected_input { - if let Err(e) = - self.post_msg(PostType::Post(action.clone(), None)) - { - log::error!("Failed to send command from editor: {}", e); - } - app.input.clear(); - app.input_idx = 0; - app.input_mode = InputMode::Normal; - return Ok(()); - } - } - - // Handle prefixes and process commands - let mut members_prefix = false; - let mut staffs_prefix = false; - let mut admin_prefix = false; - let mut pm_target: Option<String> = None; - - // Check for /pm prefix first - if let Some(captures) = PM_RGX.captures(&processed_content) { - pm_target = Some(captures[1].to_string()); - processed_content = captures[2].to_string(); - } else if processed_content.starts_with("/m ") { - members_prefix = true; - processed_content = - processed_content.strip_prefix("/m ").unwrap().to_string(); - } else if processed_content.starts_with("/s ") { - staffs_prefix = true; - processed_content = - processed_content.strip_prefix("/s ").unwrap().to_string(); - } else if processed_content.starts_with("/a ") { - admin_prefix = true; - processed_content = - processed_content.strip_prefix("/a ").unwrap().to_string(); - } - - // Determine target for ChatOps commands - let chatops_target = if let Some(user) = pm_target.clone() { - Some(user) - } else if members_prefix { - Some(SEND_TO_MEMBERS.to_owned()) - } else if staffs_prefix { - Some(SEND_TO_STAFFS.to_owned()) - } else if admin_prefix { - Some(SEND_TO_ADMINS.to_owned()) - } else { - None - }; - - // Try to process as ChatOps command - if self.process_command_with_target( - &processed_content, - app, - users, - chatops_target.clone(), - ) { - // Command was processed successfully - if let Some(user) = pm_target { - app.input = format!("/pm {} ", user); - app.input_idx = app.input.width(); - } else if members_prefix { - app.input = "/m ".to_owned(); - app.input_idx = app.input.width(); - } else if staffs_prefix { - app.input = "/s ".to_owned(); - app.input_idx = app.input.width(); - } else if admin_prefix { - app.input = "/a ".to_owned(); - app.input_idx = app.input.width(); - } else { - app.input.clear(); - app.input_idx = 0; - app.input_mode = InputMode::Normal; - } - return Ok(()); - } - - // Send as regular message with appropriate target - if let Some(user) = pm_target { - if let Err(e) = self - .post_msg(PostType::Post(processed_content, Some(user.clone()))) - { - log::error!("Failed to send PM from editor: {}", e); - } - app.input = format!("/pm {} ", user); - app.input_idx = app.input.width(); - } else if members_prefix { - if let Err(e) = self.post_msg(PostType::Post( - processed_content, - Some(SEND_TO_MEMBERS.to_owned()), - )) { - log::error!("Failed to send message to members: {}", e); - } - app.input = "/m ".to_owned(); - app.input_idx = app.input.width(); - } else if staffs_prefix { - if let Err(e) = self.post_msg(PostType::Post( - processed_content, - Some(SEND_TO_STAFFS.to_owned()), - )) { - log::error!("Failed to send message to staffs: {}", e); - } - app.input = "/s ".to_owned(); - app.input_idx = app.input.width(); - } else if admin_prefix { - if let Err(e) = self.post_msg(PostType::Post( - processed_content, - Some(SEND_TO_ADMINS.to_owned()), - )) { - log::error!("Failed to send message to admins: {}", e); - } - app.input = "/a ".to_owned(); - app.input_idx = app.input.width(); - } else { - if processed_content.starts_with("/") - && !processed_content.starts_with("/me ") - { - // Invalid command - put it back in input with error state - app.input = processed_content; - app.input_idx = app.input.chars().count(); - app.input_mode = InputMode::EditingErr; - } else { - // Send as regular message - if let Err(e) = - self.post_msg(PostType::Post(processed_content, None)) - { - log::error!("Failed to send message from editor: {}", e); - } - app.input.clear(); - app.input_idx = 0; - app.input_mode = InputMode::Normal; - } - } - } else { - // Empty content - just go back to normal mode - app.input.clear(); - app.input_idx = 0; - app.input_mode = InputMode::Normal; - } - } - Err(e) => { - log::error!("Failed to read edited file: {}", e); - // Restore original input on read error - app.input = original_input; - app.input_idx = original_input_idx; - } - } - } - Ok(_) => { - // Editor was cancelled/failed - restore original input - app.input = original_input; - app.input_idx = original_input_idx; - } - Err(e) => { - log::error!("Failed to launch editor {}: {}", editor, e); - // Restore original input on launch error - app.input = original_input; - app.input_idx = original_input_idx; - } - } - - // Ensure we're back in the correct state - app.input_mode = InputMode::Editing; - - Ok(()) - } - fn handle_editing_mode_key_event_newline(&mut self, app: &mut App) { let byte_position = byte_pos(&app.input, app.input_idx).unwrap(); app.input.insert(byte_position, '\n'); @@ -6437,7 +6173,11 @@ fn new_default_le_chat_php_client(params: Params) -> LeChatPHPClient { manual_captcha: params.manual_captcha, sxiv: params.sxiv, refresh_rate: params.refresh_rate, - config: LeChatPHPConfig::new_black_hat_chat_config(), + config: if params.profile == "404_chatroom" { + LeChatPHPConfig::new_404_chatroom_not_found_config() + } else { + LeChatPHPConfig::new_black_hat_chat_config() + }, is_muted: Arc::new(Mutex::new(false)), show_sys: false, display_guest_view: false, @@ -6611,7 +6351,7 @@ fn get_guest_color(wanted: Option<String>) -> String { } fn get_tor_client(socks_proxy_url: &str, no_proxy: bool) -> Client { - let ua = "Dasho's Black Hat Chat Client v0.2-Epic"; + let ua = "Dasho's Black Hat Chat Client v1.0-Epic"; let mut builder = reqwest::blocking::ClientBuilder::new() .redirect(Policy::none()) .cookie_store(true) @@ -6658,7 +6398,11 @@ fn start_dkf_notifier(client: &Client, dkf_api_key: &str) { let dkf_api_key = dkf_api_key.to_owned(); let mut last_known_date = Utc::now(); thread::spawn(move || { - let (_stream, stream_handle) = OutputStream::try_default().unwrap(); + #[cfg(feature = "audio")] + let audio_output = OutputStream::try_default().ok(); + #[cfg(feature = "audio")] + let stream_handle = audio_output.as_ref().map(|(_, handle)| handle); + loop { let params: Vec<(&str, String)> = vec![( "last_known_date", @@ -6674,8 +6418,12 @@ fn start_dkf_notifier(client: &Client, dkf_api_key: &str) { if let Ok(txt) = resp.text() { if let Ok(v) = serde_json::from_str::<DkfNotifierResp>(&txt) { if v.pm_sound || v.tagged_sound { - let source = Decoder::new_mp3(Cursor::new(SOUND1)).unwrap(); - stream_handle.play_raw(source.convert_samples()).unwrap(); + #[cfg(feature = "audio")] + if let Some(handle) = &stream_handle { + if let Ok(source) = Decoder::new_mp3(Cursor::new(SOUND1)) { + let _ = handle.play_raw(source.convert_samples()); + } + } } last_known_date = DateTime::parse_from_rfc3339(&v.last_message_created_at) .unwrap() @@ -6696,7 +6444,11 @@ fn start_dnmx_mail_notifier(client: &Client, username: &str, password: &str) { let client_clone = client.clone(); thread::spawn(move || { - let (_stream, stream_handle) = OutputStream::try_default().unwrap(); + #[cfg(feature = "audio")] + let audio_output = OutputStream::try_default().ok(); + #[cfg(feature = "audio")] + let stream_handle = audio_output.as_ref().map(|(_, handle)| handle); + loop { let right_url = format!("{}/src/right_main.php", DNMX_URL); if let Ok(resp) = client_clone.get(right_url).send() { @@ -6713,8 +6465,12 @@ fn start_dnmx_mail_notifier(client: &Client, username: &str, password: &str) { } if nb_mails > 0 { log::error!("{} new mails", nb_mails); - let source = Decoder::new_mp3(Cursor::new(SOUND1)).unwrap(); - stream_handle.play_raw(source.convert_samples()).unwrap(); + #[cfg(feature = "audio")] + if let Some(handle) = &stream_handle { + if let Ok(source) = Decoder::new_mp3(Cursor::new(SOUND1)) { + let _ = handle.play_raw(source.convert_samples()); + } + } } } thread::sleep(Duration::from_secs(60)); @@ -6752,8 +6508,50 @@ fn read_commands_file(file_path: &str) -> Result<Commands, Box<dyn std::error::E Ok(commands) } +// Install man page on first run +fn install_manpage() -> anyhow::Result<()> { + const MANPAGE_CONTENT: &str = include_str!("../manpage/bhcli.1"); + + let home = std::env::var("HOME")?; + let man_dir = format!("{}/.local/share/man/man1", home); + let man_path = format!("{}/bhcli.1", man_dir); + + // Check if man page already exists + if std::path::Path::new(&man_path).exists() { + return Ok(()); + } + + // Create directory if it doesn't exist + std::fs::create_dir_all(&man_dir)?; + + // Write man page + std::fs::write(&man_path, MANPAGE_CONTENT)?; + + // Update man database (try both user and system mandb commands) + // Ignore errors if mandb fails (it's not critical) + let _ = Command::new("mandb") + .arg("-u") + .arg(&format!("{}/.local/share/man", home)) + .output(); + + println!("Man page installed to {}", man_path); + println!("Access it anytime with: man bhcli"); + println!(); + + Ok(()) +} + fn main() -> anyhow::Result<()> { + // Install man page on first run + let _ = install_manpage(); + let mut opts: Opts = Opts::parse(); + + // If --404 flag is set, use the 404_chatroom profile + if opts.use_404 { + opts.profile = "404_chatroom".to_string(); + } + // println!("Parsed Session: {:?}", opts.session); // Configs file @@ -9078,33 +8876,6 @@ impl App { pos } - - // Helper function to search for text in content - fn search_in_content(content: &[String], query: &str, start_line: usize, start_col: usize) -> Option<(usize, usize)> { - if query.is_empty() { - return None; - } - - // Search from current position forward - for (line_idx, line) in content.iter().enumerate().skip(start_line) { - let search_start = if line_idx == start_line { start_col } else { 0 }; - - if let Some(col_idx) = line[search_start..].find(query) { - return Some((line_idx, search_start + col_idx)); - } - } - - // Wrap around to beginning if not found - for (line_idx, line) in content.iter().enumerate().take(start_line + 1) { - let search_end = if line_idx == start_line { start_col } else { line.len() }; - - if let Some(col_idx) = line[..search_end].find(query) { - return Some((line_idx, col_idx)); - } - } - - None - } // Helper function to find all matches in content fn find_all_matches(content: &[String], query: &str) -> Vec<(usize, usize)> { diff --git a/strange_bhcli.jpg b/strange_bhcli.jpg Binary files differ.