mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-04 16:46:08 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a247f458b | ||
|
|
fba8fbe49b | ||
|
|
ac067b2c0b | ||
|
|
d1d16f6f0e | ||
|
|
338946382f | ||
|
|
5d1934e2c6 | ||
|
|
441a97e8c9 | ||
|
|
11ce586f29 | ||
|
|
b28b98e336 | ||
|
|
87f2cbb011 | ||
|
|
65766eab1f | ||
|
|
675a39cb15 | ||
|
|
cda890de60 | ||
|
|
7eced63608 | ||
|
|
07996a78ed | ||
|
|
329dfe422b | ||
|
|
b1aeb8cbb2 | ||
|
|
7964355e08 | ||
|
|
1e877cf886 | ||
|
|
0105f798e0 | ||
|
|
607217b314 | ||
|
|
40aea9a04b | ||
|
|
f8f4e03ddd | ||
|
|
4dcb480d1d | ||
|
|
45dc0f9d0f | ||
|
|
fbeb546ac5 | ||
|
|
e3654c637c | ||
|
|
2297b58c74 | ||
|
|
6e57247a45 | ||
|
|
fd61d9cb9c | ||
|
|
79e5a1191f | ||
|
|
7525f8b1d8 | ||
|
|
0c2e8eae87 | ||
|
|
75ff10c5fd | ||
|
|
46afd2a195 | ||
|
|
38eca99fb1 | ||
|
|
df1bd0119c | ||
|
|
71d82b45fb | ||
|
|
53d0d326cb | ||
|
|
3281064720 | ||
|
|
bb987dda82 | ||
|
|
e71575d920 | ||
|
|
96736d69df | ||
|
|
953e081123 | ||
|
|
b9d099dd73 | ||
|
|
a9519a52cc | ||
|
|
4d25458717 | ||
|
|
584ce45979 | ||
|
|
53927b130c | ||
|
|
ca3c1ec2b5 | ||
|
|
35a4eec749 | ||
|
|
78f18a36ba | ||
|
|
2157884e61 | ||
|
|
4f2cffa372 | ||
|
|
31a0bb8c9f | ||
|
|
e439c8ae72 | ||
|
|
b6c01dab15 | ||
|
|
1f4af9a543 | ||
|
|
b62729dc95 | ||
|
|
aae788028e | ||
|
|
c048b39e42 | ||
|
|
3886e7148c | ||
|
|
b9a80806e1 | ||
|
|
e797ec2542 | ||
|
|
dbf6565d18 | ||
|
|
494dcb46cf | ||
|
|
ce41ef9c0a | ||
|
|
9aea6ecfc3 | ||
|
|
5040c5c201 | ||
|
|
a3a413f0b1 | ||
|
|
6292e82cec | ||
|
|
2d33773c24 | ||
|
|
e05b420ce9 | ||
|
|
ea8e583ee5 | ||
|
|
3ca9dbfd91 | ||
|
|
15187c4665 | ||
|
|
64bca51619 | ||
|
|
3538ec8005 | ||
|
|
1b234e39e1 | ||
|
|
19bf123c8c | ||
|
|
6cf284a475 | ||
|
|
3b45bb5661 | ||
|
|
6235565480 | ||
|
|
ecb20c4c52 | ||
|
|
a85d16fff8 | ||
|
|
6db93ecfea | ||
|
|
67587dafc2 | ||
|
|
1bbe099835 | ||
|
|
769aa5b468 | ||
|
|
6660944588 | ||
|
|
4b41438af0 | ||
|
|
a4fe432992 | ||
|
|
9279e8be06 | ||
|
|
7f9fcfe3cc | ||
|
|
62c5ab27c5 | ||
|
|
240892495b | ||
|
|
6e7cbf39f0 | ||
|
|
83d257591e | ||
|
|
e42f1a0965 | ||
|
|
fe6dfd3d4d | ||
|
|
d6282885e3 | ||
|
|
bd8b88add2 | ||
|
|
ff9a9e3dd0 | ||
|
|
aa6599a4e0 | ||
|
|
3091a146eb | ||
|
|
000764fdb4 | ||
|
|
56fccf5cfb | ||
|
|
b98673f8c5 | ||
|
|
335cfcc28a | ||
|
|
6aef6890cf | ||
|
|
5b8df9d505 | ||
|
|
6dbc29818c | ||
|
|
6758aceb5d | ||
|
|
06aba16303 | ||
|
|
89b028cf06 | ||
|
|
eb4aac9861 | ||
|
|
38d63d844d | ||
|
|
3f2211f42a | ||
|
|
0606394af2 | ||
|
|
ee18743721 | ||
|
|
0aaa371488 | ||
|
|
8689dc2a01 | ||
|
|
5d61266fd7 | ||
|
|
ae342381e0 | ||
|
|
ba536c6f9c | ||
|
|
92b89fa8a0 | ||
|
|
9029d63e54 | ||
|
|
023e62dea6 | ||
|
|
36e1ea0dfa | ||
|
|
be18cf88df | ||
|
|
595475c436 | ||
|
|
1b6c27247f | ||
|
|
4a598aa8a8 | ||
|
|
ed7d0a73a3 | ||
|
|
6af85559cb | ||
|
|
43f1b97244 | ||
|
|
9baa3ff566 | ||
|
|
0330f24e45 | ||
|
|
ac98845c69 | ||
|
|
d4f540285b | ||
|
|
3896640ac7 | ||
|
|
29bfd87d8b | ||
|
|
b0adb55b8f | ||
|
|
24a5bdb9a1 | ||
|
|
5391d2cb1c | ||
|
|
62996f85e9 | ||
|
|
ac5348dbb0 | ||
|
|
ad5627a4bb | ||
|
|
c7a5829fac | ||
|
|
a0d9d86956 | ||
|
|
0c16f44db6 | ||
|
|
fb4f6c34ed | ||
|
|
8647b05c12 | ||
|
|
e2bab3d0d9 | ||
|
|
6295831045 | ||
|
|
045515369c | ||
|
|
fe0e31f9a0 | ||
|
|
3273e62b37 | ||
|
|
cf5fca5772 | ||
|
|
38df05df41 | ||
|
|
760693befe | ||
|
|
241fee67e8 | ||
|
|
0e3dd932af | ||
|
|
81b09b1af7 | ||
|
|
94b31b6056 | ||
|
|
6e76065dcb | ||
|
|
bdcff74ad5 | ||
|
|
27a25b1b43 | ||
|
|
28c4b2993f | ||
|
|
3c760b0bd8 | ||
|
|
c2a8e9cfc8 | ||
|
|
45fd382b13 | ||
|
|
039390c8b0 | ||
|
|
c5be63db25 | ||
|
|
993bb1f973 | ||
|
|
94f968ccf9 | ||
|
|
4f96b8d986 | ||
|
|
fbb2958fdd | ||
|
|
62e9d7fff5 | ||
|
|
be4d686d4d | ||
|
|
c8ea124198 | ||
|
|
c5d8e25a86 | ||
|
|
03f4685ae6 | ||
|
|
17e64293b5 | ||
|
|
ce375c7619 | ||
|
|
d12f249222 | ||
|
|
bc8e2fd7da | ||
|
|
35a162d013 | ||
|
|
de34fb43b9 | ||
|
|
3e03a77aeb | ||
|
|
5ef338bbf7 | ||
|
|
f96f70f09a | ||
|
|
b7d58dc10d | ||
|
|
7fadd08702 | ||
|
|
576dd4bc94 | ||
|
|
d4cf35e04e | ||
|
|
23abb9b0bb | ||
|
|
dcb8bbc05f | ||
|
|
4930aee615 | ||
|
|
2cd3bbb570 | ||
|
|
0d959e317e | ||
|
|
75491d65e2 | ||
|
|
1861fd00f9 | ||
|
|
9996a6be97 | ||
|
|
cf9c647c03 | ||
|
|
1fa4348700 | ||
|
|
0c7c620f24 | ||
|
|
56a962c1af | ||
|
|
a65b41fb7f | ||
|
|
ab9e09a609 | ||
|
|
1bdc6ec99e | ||
|
|
fb12848e9a | ||
|
|
7e78c57665 | ||
|
|
9d4979d4a7 | ||
|
|
c0d38015ff | ||
|
|
3bc1446d1c | ||
|
|
411042c397 | ||
|
|
50817ce6e9 | ||
|
|
9265a9f312 | ||
|
|
a860a8fbc3 |
@@ -0,0 +1,93 @@
|
|||||||
|
# EchoHub Server Configuration
|
||||||
|
# Copy this file to .env and customize as needed: cp .env.example .env
|
||||||
|
# These override appsettings.json via ASP.NET Core's configuration hierarchy.
|
||||||
|
|
||||||
|
# ── Server ───────────────────────────────────────────────────────────
|
||||||
|
Server__Name=My EchoHub Server
|
||||||
|
Server__Description=A self-hosted EchoHub chat server
|
||||||
|
Server__PublicServer=false
|
||||||
|
# Registration mode: open (default), invite (codes via /invite, Admin+), or closed.
|
||||||
|
# Server__Registration=open
|
||||||
|
# Hostnames advertised to the EchoHubSpace directory. Index per entry.
|
||||||
|
# Server__PublicHosts__0=echohub.example.com
|
||||||
|
# Server__PublicHosts__1=alias.example.com
|
||||||
|
# Topic tags surfaced in the EchoHubSpace browser. Index per entry.
|
||||||
|
# Server__Tags__0=community
|
||||||
|
# Server__Tags__1=gaming
|
||||||
|
# Server__Admins__0=adminUsername
|
||||||
|
# Override where the EchoHubSpace claim token file is stored (defaults next to the DB).
|
||||||
|
# Server__DirectoryClaimPath=
|
||||||
|
|
||||||
|
# ── JWT ──────────────────────────────────────────────────────────────
|
||||||
|
# Auto-generated on first run if left empty. Only set if you need a stable secret across containers.
|
||||||
|
# Jwt__Secret=
|
||||||
|
# Jwt__Issuer=EchoHub.Server
|
||||||
|
# Jwt__Audience=EchoHub.Client
|
||||||
|
|
||||||
|
# ── Encryption ───────────────────────────────────────────────────────
|
||||||
|
# Auto-generated on first run if left empty.
|
||||||
|
# Encryption__Key=
|
||||||
|
# Encryption__EncryptDatabase=false
|
||||||
|
|
||||||
|
# ── Storage ──────────────────────────────────────────────────────────
|
||||||
|
# Defaults are set in the Dockerfile to use /app/data for persistence.
|
||||||
|
# Storage__Path=
|
||||||
|
# Storage__CleanupIntervalHours=1
|
||||||
|
# Storage__RetentionDays=30
|
||||||
|
|
||||||
|
# ── Uploads ──────────────────────────────────────────────────────────
|
||||||
|
# Uploads__MaxFileSizeMB=100
|
||||||
|
# Uploads__MaxImageSizeMB=10
|
||||||
|
# Uploads__MaxAudioSizeMB=10
|
||||||
|
# Uploads__MaxAvatarSizeMB=2
|
||||||
|
# Uploads__MaxAttachmentsPerMessage=10
|
||||||
|
|
||||||
|
# ── Spam protection ──────────────────────────────────────────────────
|
||||||
|
# Per-user flood/duplicate limits with auto-mute escalation; Mods and above are exempt.
|
||||||
|
# Spam__Enabled=true
|
||||||
|
# Spam__MaxMessagesPerWindow=8
|
||||||
|
# Spam__WindowSeconds=5
|
||||||
|
# Spam__MaxDuplicateMessages=3
|
||||||
|
# Spam__AutoMuteMinutes=5
|
||||||
|
# Spam__ViolationThreshold=5
|
||||||
|
# Spam__ViolationWindowMinutes=5
|
||||||
|
# Only first-time channel joins count — keep this above your public channel count.
|
||||||
|
# Spam__MaxJoinsPerWindow=25
|
||||||
|
# Spam__JoinWindowSeconds=30
|
||||||
|
# Spam__MaxChannelCreatesPerWindow=3
|
||||||
|
# Spam__ChannelCreateWindowMinutes=10
|
||||||
|
|
||||||
|
# ── IRC Gateway ──────────────────────────────────────────────────────
|
||||||
|
Irc__Enabled=false
|
||||||
|
# Irc__Port=6667
|
||||||
|
# Irc__TlsEnabled=false
|
||||||
|
# Irc__TlsPort=6697
|
||||||
|
# Irc__TlsCertPath=
|
||||||
|
# Irc__TlsCertPassword=
|
||||||
|
# Irc__ServerName=echohub
|
||||||
|
# Irc__Motd=Welcome to EchoHub IRC Gateway!
|
||||||
|
# Public base URL used to make attachment links absolute for IRC clients.
|
||||||
|
# Irc__PublicBaseUrl=https://chat.example.com
|
||||||
|
|
||||||
|
# ── CORS ─────────────────────────────────────────────────────────────
|
||||||
|
# Leave unset to allow all origins. Index per entry to whitelist.
|
||||||
|
# Cors__AllowedOrigins__0=https://chat.example.com
|
||||||
|
|
||||||
|
# ── Logging ──────────────────────────────────────────────────────────
|
||||||
|
# Serilog__MinimumLevel__Default=Information
|
||||||
|
|
||||||
|
# ── Server logs room ─────────────────────────────────────────────────
|
||||||
|
# Read-only system channel that live-streams Serilog events to Mod+ users.
|
||||||
|
# ServerLogs__Enabled=true
|
||||||
|
# ServerLogs__RoomName=server-logs
|
||||||
|
# ServerLogs__MinRole=Mod
|
||||||
|
# ServerLogs__MinLevel=Information
|
||||||
|
# ServerLogs__BacklogLines=100
|
||||||
|
# ServerLogs__LogDirectory=logs
|
||||||
|
# ServerLogs__LogFilePattern=echohub-server-*.log
|
||||||
|
|
||||||
|
# ── Periodic stats report ────────────────────────────────────────────
|
||||||
|
# Aggregate activity snapshot logged as JSON and persisted to the DB.
|
||||||
|
# Stats__Enabled=true
|
||||||
|
# Stats__IntervalHours=6
|
||||||
|
# Stats__RetentionDays=90
|
||||||
@@ -14,6 +14,15 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install pinned linter
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
- name: Run markdownlint
|
- name: Run markdownlint
|
||||||
run: bash scripts/lint-markdown.sh
|
run: bash scripts/lint-markdown.sh
|
||||||
|
|
||||||
@@ -33,6 +42,8 @@ jobs:
|
|||||||
|
|
||||||
build-and-test:
|
build-and-test:
|
||||||
name: Build & Test
|
name: Build & Test
|
||||||
|
# Run the cheap, fast checks first; only spend build/test compute if they pass.
|
||||||
|
needs: [format-check, lint-markdown]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
name: Docker
|
||||||
|
|
||||||
|
# Runs only after the CI workflow completes, so an image is never pushed for a commit
|
||||||
|
# whose formatting, lint, build, or tests failed.
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["CI"]
|
||||||
|
types: [completed]
|
||||||
|
branches: [master]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
IMAGE: ghcr.io/huebyte/echohub-server
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker:
|
||||||
|
name: Build & Push Docker Image
|
||||||
|
# Proceed only for a successful CI run on a master push, or a manual dispatch.
|
||||||
|
if: >-
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
(github.event.workflow_run.conclusion == 'success' &&
|
||||||
|
github.event.workflow_run.event == 'push' &&
|
||||||
|
github.event.workflow_run.head_branch == 'master')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
# The exact commit CI tested (workflow_run), or the current tip (manual dispatch).
|
||||||
|
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||||
|
|
||||||
|
- name: Check for src/ changes
|
||||||
|
id: changes
|
||||||
|
run: |
|
||||||
|
if git rev-parse HEAD~1 >/dev/null 2>&1; then
|
||||||
|
CHANGED=$(git diff --name-only HEAD~1 HEAD -- 'src/' | wc -l)
|
||||||
|
else
|
||||||
|
CHANGED=1
|
||||||
|
fi
|
||||||
|
[ "$CHANGED" -gt 0 ] && echo "src_changed=true" >> "$GITHUB_OUTPUT" || echo "src_changed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Read version
|
||||||
|
if: steps.changes.outputs.src_changed == 'true'
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION=$(grep -oP '(?<=<Version>)[^<]+' src/Directory.Build.props)
|
||||||
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Check if image tag exists
|
||||||
|
if: steps.changes.outputs.src_changed == 'true'
|
||||||
|
id: check_image
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.version.outputs.tag }}"
|
||||||
|
if docker manifest inspect "${{ env.IMAGE }}:${TAG}" &>/dev/null; then
|
||||||
|
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
if: steps.changes.outputs.src_changed == 'true' && steps.check_image.outputs.exists == 'false'
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
if: steps.changes.outputs.src_changed == 'true' && steps.check_image.outputs.exists == 'false'
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
if: steps.changes.outputs.src_changed == 'true' && steps.check_image.outputs.exists == 'false'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
if: steps.changes.outputs.src_changed == 'true' && steps.check_image.outputs.exists == 'false'
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: ./src
|
||||||
|
file: ./src/EchoHub.Server/Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.IMAGE }}:latest
|
||||||
|
${{ env.IMAGE }}:${{ steps.version.outputs.tag }}
|
||||||
|
labels: |
|
||||||
|
org.opencontainers.image.title=EchoHub Server
|
||||||
|
org.opencontainers.image.description=Self-hosted IRC-style chat server
|
||||||
|
org.opencontainers.image.version=${{ steps.version.outputs.version }}
|
||||||
|
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
||||||
|
|
||||||
|
- name: Skip notice
|
||||||
|
if: steps.changes.outputs.src_changed != 'true' || steps.check_image.outputs.exists == 'true'
|
||||||
|
run: echo "⏭️ Skipped — no src/ changes or image tag already exists."
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
name: Release Checklist
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release-checklist:
|
||||||
|
name: Release Checklist
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Extract version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION=$(grep -oP '(?<=<Version>)[^<]+' src/Directory.Build.props)
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
echo "::error file=src/Directory.Build.props::Could not read version from Directory.Build.props"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Version: $VERSION"
|
||||||
|
|
||||||
|
- name: Check version was bumped from master
|
||||||
|
run: |
|
||||||
|
BRANCH_VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
git fetch origin master --depth=1
|
||||||
|
MASTER_VERSION=$(git show origin/master:src/Directory.Build.props | grep -oP '(?<=<Version>)[^<]+')
|
||||||
|
echo "Branch: $BRANCH_VERSION | Master: $MASTER_VERSION"
|
||||||
|
if [ "$BRANCH_VERSION" = "$MASTER_VERSION" ]; then
|
||||||
|
echo "::error file=src/Directory.Build.props::Version $BRANCH_VERSION was not bumped from master. Update <Version> in src/Directory.Build.props."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check changelog file exists
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
FILE="docs/changelog/v${VERSION}.md"
|
||||||
|
if [ ! -f "$FILE" ]; then
|
||||||
|
echo "::error::Missing changelog file: $FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Found: $FILE"
|
||||||
|
|
||||||
|
- name: Check changelog TOC
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
if ! grep -q "v${VERSION}.md" docs/changelog/toc.yml; then
|
||||||
|
echo "::error file=docs/changelog/toc.yml::v${VERSION} not found in changelog TOC. Add it to docs/changelog/toc.yml."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! grep -q "v${VERSION}" docs/changelog/index.md; then
|
||||||
|
echo "::error file=docs/changelog/index.md::v${VERSION} not found in changelog index. Add it to docs/changelog/index.md."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "toc.yml and index.md: OK"
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
name: Release
|
name: Release
|
||||||
|
|
||||||
|
# Runs only after the CI workflow completes, so a release is never published on a commit
|
||||||
|
# whose formatting, lint, build, or tests failed.
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_run:
|
||||||
|
workflows: ["CI"]
|
||||||
|
types: [completed]
|
||||||
branches: [master]
|
branches: [master]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
@@ -11,26 +15,36 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
name: Create Release
|
name: Create Release
|
||||||
|
# Proceed only for a successful CI run on a master push, or a manual dispatch.
|
||||||
|
if: >-
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
(github.event.workflow_run.conclusion == 'success' &&
|
||||||
|
github.event.workflow_run.event == 'push' &&
|
||||||
|
github.event.workflow_run.head_branch == 'master')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
should_release: ${{ steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false' }}
|
||||||
|
release_exists: ${{ steps.check_release.outputs.exists }}
|
||||||
|
version: ${{ steps.version.outputs.version }}
|
||||||
|
tag: ${{ steps.version.outputs.tag }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
# The exact commit CI tested (workflow_run), or the current tip (manual dispatch).
|
||||||
|
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||||
|
|
||||||
- name: Check for src/ changes
|
- name: Check for src/ changes
|
||||||
id: changes
|
id: changes
|
||||||
env:
|
|
||||||
BEFORE: ${{ github.event.before }}
|
|
||||||
run: |
|
run: |
|
||||||
if [ -z "$BEFORE" ] || [ "$BEFORE" = "0000000000000000000000000000000000000000" ]; then
|
if git rev-parse HEAD~1 >/dev/null 2>&1; then
|
||||||
echo "src_changed=true" >> "$GITHUB_OUTPUT"
|
CHANGED=$(git diff --name-only HEAD~1 HEAD -- 'src/' | wc -l)
|
||||||
else
|
else
|
||||||
CHANGED=$(git diff --name-only "$BEFORE" HEAD -- 'src/' | wc -l)
|
CHANGED=1
|
||||||
[ "$CHANGED" -gt 0 ] && echo "src_changed=true" >> "$GITHUB_OUTPUT" || echo "src_changed=false" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
fi
|
||||||
|
[ "$CHANGED" -gt 0 ] && echo "src_changed=true" >> "$GITHUB_OUTPUT" || echo "src_changed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Read version
|
- name: Read version
|
||||||
if: steps.changes.outputs.src_changed == 'true'
|
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: |
|
||||||
VERSION=$(grep -oP '(?<=<Version>)[^<]+' src/Directory.Build.props)
|
VERSION=$(grep -oP '(?<=<Version>)[^<]+' src/Directory.Build.props)
|
||||||
@@ -38,7 +52,6 @@ jobs:
|
|||||||
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
|
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Check if release exists
|
- name: Check if release exists
|
||||||
if: steps.changes.outputs.src_changed == 'true'
|
|
||||||
id: check_release
|
id: check_release
|
||||||
run: |
|
run: |
|
||||||
if gh release view "${{ steps.version.outputs.tag }}" &>/dev/null; then
|
if gh release view "${{ steps.version.outputs.tag }}" &>/dev/null; then
|
||||||
@@ -53,7 +66,7 @@ jobs:
|
|||||||
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: '10.0.x'
|
dotnet-version: "10.0.x"
|
||||||
|
|
||||||
- name: Publish Server win-x64
|
- name: Publish Server win-x64
|
||||||
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
||||||
@@ -71,21 +84,29 @@ jobs:
|
|||||||
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
||||||
run: dotnet publish src/EchoHub.Server/EchoHub.Server.csproj -c Release -r osx-arm64 --self-contained true -o publish/server-osx-arm64
|
run: dotnet publish src/EchoHub.Server/EchoHub.Server.csproj -c Release -r osx-arm64 --self-contained true -o publish/server-osx-arm64
|
||||||
|
|
||||||
|
- name: Publish Server linux-arm64
|
||||||
|
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
||||||
|
run: dotnet publish src/EchoHub.Server/EchoHub.Server.csproj -c Release -r linux-arm64 --self-contained true -o publish/server-linux-arm64
|
||||||
|
|
||||||
- name: Publish Client win-x64
|
- name: Publish Client win-x64
|
||||||
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
||||||
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r win-x64 --self-contained true -o publish/client-win-x64
|
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o publish/client-win-x64
|
||||||
|
|
||||||
- name: Publish Client linux-x64
|
- name: Publish Client linux-x64
|
||||||
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
||||||
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r linux-x64 --self-contained true -o publish/client-linux-x64
|
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o publish/client-linux-x64
|
||||||
|
|
||||||
- name: Publish Client osx-x64
|
- name: Publish Client osx-x64
|
||||||
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
||||||
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r osx-x64 --self-contained true -o publish/client-osx-x64
|
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r osx-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o publish/client-osx-x64
|
||||||
|
|
||||||
- name: Publish Client osx-arm64
|
- name: Publish Client osx-arm64
|
||||||
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
||||||
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r osx-arm64 --self-contained true -o publish/client-osx-arm64
|
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r osx-arm64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o publish/client-osx-arm64
|
||||||
|
|
||||||
|
- name: Publish Client linux-arm64
|
||||||
|
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
||||||
|
run: dotnet publish src/EchoHub.Client/EchoHub.Client.csproj -c Release -r linux-arm64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o publish/client-linux-arm64
|
||||||
|
|
||||||
- name: Zip artifacts
|
- name: Zip artifacts
|
||||||
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
||||||
@@ -95,14 +116,15 @@ jobs:
|
|||||||
zip -r ../EchoHub-Server-linux-x64.zip server-linux-x64/
|
zip -r ../EchoHub-Server-linux-x64.zip server-linux-x64/
|
||||||
zip -r ../EchoHub-Server-osx-x64.zip server-osx-x64/
|
zip -r ../EchoHub-Server-osx-x64.zip server-osx-x64/
|
||||||
zip -r ../EchoHub-Server-osx-arm64.zip server-osx-arm64/
|
zip -r ../EchoHub-Server-osx-arm64.zip server-osx-arm64/
|
||||||
|
zip -r ../EchoHub-Server-linux-arm64.zip server-linux-arm64/
|
||||||
zip -r ../EchoHub-Client-win-x64.zip client-win-x64/
|
zip -r ../EchoHub-Client-win-x64.zip client-win-x64/
|
||||||
zip -r ../EchoHub-Client-linux-x64.zip client-linux-x64/
|
zip -r ../EchoHub-Client-linux-x64.zip client-linux-x64/
|
||||||
zip -r ../EchoHub-Client-osx-x64.zip client-osx-x64/
|
zip -r ../EchoHub-Client-osx-x64.zip client-osx-x64/
|
||||||
zip -r ../EchoHub-Client-osx-arm64.zip client-osx-arm64/
|
zip -r ../EchoHub-Client-osx-arm64.zip client-osx-arm64/
|
||||||
|
zip -r ../EchoHub-Client-linux-arm64.zip client-linux-arm64/
|
||||||
|
|
||||||
- name: Build release notes
|
- name: Build release notes
|
||||||
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
||||||
id: notes
|
|
||||||
run: |
|
run: |
|
||||||
TAG="${{ steps.version.outputs.tag }}"
|
TAG="${{ steps.version.outputs.tag }}"
|
||||||
CHANGELOG_URL="https://huebyte.github.io/EchoHub/changelog/${TAG}.html"
|
CHANGELOG_URL="https://huebyte.github.io/EchoHub/changelog/${TAG}.html"
|
||||||
@@ -110,7 +132,6 @@ jobs:
|
|||||||
PREV_TAG=$(git tag --sort=-v:refname | grep -v "^${TAG}$" | head -n 1)
|
PREV_TAG=$(git tag --sort=-v:refname | grep -v "^${TAG}$" | head -n 1)
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "body<<RELEASE_EOF"
|
|
||||||
echo "📋 **[Full Changelog](${CHANGELOG_URL})**"
|
echo "📋 **[Full Changelog](${CHANGELOG_URL})**"
|
||||||
echo ""
|
echo ""
|
||||||
echo "---"
|
echo "---"
|
||||||
@@ -129,22 +150,69 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo "*Version diff: [${TAG}](${REPO}/commits/${TAG})*"
|
echo "*Version diff: [${TAG}](${REPO}/commits/${TAG})*"
|
||||||
fi
|
fi
|
||||||
echo "RELEASE_EOF"
|
} > release-notes.md
|
||||||
} >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
if: steps.changes.outputs.src_changed == 'true' && steps.check_release.outputs.exists == 'false'
|
||||||
run: |
|
run: |
|
||||||
gh release create "${{ steps.version.outputs.tag }}" \
|
gh release create "${{ steps.version.outputs.tag }}" \
|
||||||
--title "EchoHub ${{ steps.version.outputs.tag }}" \
|
--title "EchoHub ${{ steps.version.outputs.tag }}" \
|
||||||
--notes "${{ steps.notes.outputs.body }}" \
|
--notes-file release-notes.md \
|
||||||
EchoHub-Server-win-x64.zip \
|
EchoHub-Server-win-x64.zip \
|
||||||
EchoHub-Server-linux-x64.zip \
|
EchoHub-Server-linux-x64.zip \
|
||||||
EchoHub-Server-osx-x64.zip \
|
EchoHub-Server-osx-x64.zip \
|
||||||
EchoHub-Server-osx-arm64.zip \
|
EchoHub-Server-osx-arm64.zip \
|
||||||
|
EchoHub-Server-linux-arm64.zip \
|
||||||
EchoHub-Client-win-x64.zip \
|
EchoHub-Client-win-x64.zip \
|
||||||
EchoHub-Client-linux-x64.zip \
|
EchoHub-Client-linux-x64.zip \
|
||||||
EchoHub-Client-osx-x64.zip \
|
EchoHub-Client-osx-x64.zip \
|
||||||
EchoHub-Client-osx-arm64.zip
|
EchoHub-Client-osx-arm64.zip \
|
||||||
|
EchoHub-Client-linux-arm64.zip
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
choco:
|
||||||
|
name: Publish to Chocolatey
|
||||||
|
needs: release
|
||||||
|
if: needs.release.outputs.release_exists == 'true' || needs.release.outputs.should_release == 'true'
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check if version already published
|
||||||
|
id: choco_check
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$version = "${{ needs.release.outputs.version }}"
|
||||||
|
$result = choco search echohub --version $version --exact --source https://community.chocolatey.org/api/v2/ 2>&1
|
||||||
|
if ($result -match "echohub $version") {
|
||||||
|
echo "exists=true" >> $env:GITHUB_OUTPUT
|
||||||
|
Write-Host "Chocolatey package echohub $version already published — skipping."
|
||||||
|
} else {
|
||||||
|
echo "exists=false" >> $env:GITHUB_OUTPUT
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Download release asset
|
||||||
|
if: steps.choco_check.outputs.exists == 'false'
|
||||||
|
run: gh release download "v${{ needs.release.outputs.version }}" --pattern "EchoHub-Client-win-x64.zip"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Pack and push
|
||||||
|
if: steps.choco_check.outputs.exists == 'false'
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$version = "${{ needs.release.outputs.version }}"
|
||||||
|
$checksum = (Get-FileHash EchoHub-Client-win-x64.zip -Algorithm SHA256).Hash.ToLower()
|
||||||
|
|
||||||
|
# Stamp version and checksum into package templates
|
||||||
|
(Get-Content packaging/choco/echohub.nuspec) -replace '__VERSION__', $version | Set-Content packaging/choco/echohub.nuspec
|
||||||
|
(Get-Content packaging/choco/tools/chocolateyInstall.ps1) -replace '__VERSION__', $version | Set-Content packaging/choco/tools/chocolateyInstall.ps1
|
||||||
|
(Get-Content packaging/choco/tools/chocolateyInstall.ps1) -replace '__CHECKSUM64__', $checksum | Set-Content packaging/choco/tools/chocolateyInstall.ps1
|
||||||
|
|
||||||
|
cd packaging/choco
|
||||||
|
choco pack echohub.nuspec --output-directory $env:TEMP
|
||||||
|
$pkg = Get-ChildItem "$env:TEMP\echohub.*.nupkg" | Select-Object -First 1
|
||||||
|
choco push $pkg.FullName --source https://push.chocolatey.org/ --api-key $env:CHOCO_API_KEY
|
||||||
|
env:
|
||||||
|
CHOCO_API_KEY: ${{ secrets.CHOCOLATEY_API_KEY }}
|
||||||
|
|||||||
@@ -433,3 +433,6 @@ src/EchoHub.Server/uploads/*
|
|||||||
# DocFx generated output
|
# DocFx generated output
|
||||||
docs/_site/
|
docs/_site/
|
||||||
docs/_api_meta/
|
docs/_api_meta/
|
||||||
|
|
||||||
|
# Diff files
|
||||||
|
*.diff
|
||||||
|
|||||||
@@ -6,10 +6,14 @@
|
|||||||
"MD013": false,
|
"MD013": false,
|
||||||
// Allow duplicate headings in different sections
|
// Allow duplicate headings in different sections
|
||||||
"MD024": { "siblings_only": true },
|
"MD024": { "siblings_only": true },
|
||||||
|
// Allow files to open with a centered HTML header block instead of an H1 (README)
|
||||||
|
"MD041": false,
|
||||||
// Allow inline HTML (docfx uses it)
|
// Allow inline HTML (docfx uses it)
|
||||||
"MD033": false,
|
"MD033": false,
|
||||||
// Allow bare URLs
|
// Allow bare URLs
|
||||||
"MD034": false
|
"MD034": false,
|
||||||
|
// Allow compact table pipe style (flow docs use compact tables)
|
||||||
|
"MD060": false
|
||||||
},
|
},
|
||||||
|
|
||||||
"globs": ["**/*.md"],
|
"globs": ["**/*.md"],
|
||||||
@@ -18,6 +22,7 @@
|
|||||||
".dev/**",
|
".dev/**",
|
||||||
"docs/_site/**",
|
"docs/_site/**",
|
||||||
"docs/api/**",
|
"docs/api/**",
|
||||||
|
"docs/auriondocs/**",
|
||||||
"node_modules/**",
|
"node_modules/**",
|
||||||
"**/bin/**",
|
"**/bin/**",
|
||||||
"**/obj/**",
|
"**/obj/**",
|
||||||
|
|||||||
@@ -1,39 +1,23 @@
|
|||||||
# EchoHub
|
<div align="center">
|
||||||
|
|
||||||
<p align="center">
|
<img src="https://cdn.voidcube.cloud/assets/hue_icon.svg" alt="EchoHub" width="112" />
|
||||||
<img src="https://cdn.voidcube.cloud/assets/hue_icon.svg" alt="EchoHub Logo" width="120" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h1 align="center">EchoHub</h1>
|
<h1>EchoHub</h1>
|
||||||
|
|
||||||
<p align="center">
|
**No tracking. No subscriptions. No "enhanced AI features". Just chat.**
|
||||||
No tracking. No subscriptions. No "enhanced AI features". Just chat.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
Self-hosted, IRC-inspired chat with an open API. Use the terminal, a desktop app, IRC, or a client you build yourself.
|
||||||
<a href="#what-is-this">What</a> •
|
|
||||||
<a href="#getting-started">Setup</a> •
|
|
||||||
<a href="#irc-gateway">IRC</a> •
|
|
||||||
<a href="#deployment-with-nginx">Deploy</a> •
|
|
||||||
<a href="#client-commands">Commands</a> •
|
|
||||||
<a href="#configuration">Config</a> •
|
|
||||||
<a href="#license">License</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
[Website](https://echohub.voidcube.cloud/) · [Public servers](https://echohub.voidcube.cloud/servers) · [Documentation](https://huebyte.github.io/EchoHub/) · [Changelog](docs/changelog/index.md)
|
||||||
<a href="https://echohub.voidcube.cloud/">Website</a> •
|
|
||||||
<a href="https://echohub.voidcube.cloud/servers">Public Servers</a> •
|
|
||||||
<a href="https://huebyte.github.io/EchoHub/">Documentation</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
<a href="https://github.com/HueByte/EchoHub/actions/workflows/ci.yml"><img alt="Build" src="https://img.shields.io/github/actions/workflow/status/HueByte/EchoHub/ci.yml?branch=master&style=flat-square&logo=github&label=Build" /></a>
|
||||||
|
<a href="https://github.com/HueByte/EchoHub/releases/latest"><img alt="Release" src="https://img.shields.io/github/v/release/HueByte/EchoHub?style=flat-square&logo=github&label=Release" /></a>
|
||||||
|
<a href="https://community.chocolatey.org/packages/echohub"><img alt="Chocolatey" src="https://img.shields.io/chocolatey/v/echohub?style=flat-square&logo=chocolatey&label=Chocolatey" /></a>
|
||||||
|
<a href="https://github.com/HueByte/EchoHub/pkgs/container/echohub-server"><img alt="Docker" src="https://img.shields.io/badge/Docker-GHCR-2496ED?style=flat-square&logo=docker&logoColor=white" /></a>
|
||||||
<img alt=".NET 10" src="https://img.shields.io/badge/.NET-10-512BD4?style=flat-square&logo=dotnet&logoColor=white" />
|
<img alt=".NET 10" src="https://img.shields.io/badge/.NET-10-512BD4?style=flat-square&logo=dotnet&logoColor=white" />
|
||||||
<img alt="SignalR" src="https://img.shields.io/badge/SignalR-Real--time-0078D4?style=flat-square" />
|
<a href="LICENSE"><img alt="License" src="https://img.shields.io/github/license/HueByte/EchoHub?style=flat-square" /></a>
|
||||||
<img alt="SQLite" src="https://img.shields.io/badge/SQLite-EF%20Core-003B57?style=flat-square&logo=sqlite&logoColor=white" />
|
|
||||||
<img alt="License" src="https://img.shields.io/badge/License-MIT-green?style=flat-square" />
|
</div>
|
||||||
<img alt="Terminal.Gui" src="https://img.shields.io/badge/TUI-Terminal.Gui%20v2-yellow?style=flat-square" />
|
|
||||||
<img alt="Electron" src="https://img.shields.io/badge/Electron-None-red?style=flat-square" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -41,313 +25,153 @@
|
|||||||
|
|
||||||
Chat apps used to be simple. You connected to a server, joined a channel, and talked to people. No one was mining your messages for ad targeting, no one was selling your "engagement metrics", and the app didn't need 2GB of RAM to display text.
|
Chat apps used to be simple. You connected to a server, joined a channel, and talked to people. No one was mining your messages for ad targeting, no one was selling your "engagement metrics", and the app didn't need 2GB of RAM to display text.
|
||||||
|
|
||||||
EchoHub is a return to that. Self-hosted, IRC-inspired chat that runs in your terminal. You own the server, you own the data, and the client won't try to upsell you on a premium tier.
|
EchoHub is a return to that. Self-hosted, IRC-inspired chat where you own the server and the data, and the client won't try to upsell you on a premium tier.
|
||||||
|
|
||||||
Each server is fully independent — no central authority, no account federation, no corporate overlord. Just spin one up and go.
|
Each server is fully independent. No central authority, no account federation, no corporate overlord. Just spin one up and go.
|
||||||
|
|
||||||
|
The terminal client in this repo is the main way in: a proper native app that runs right in your terminal, no browser or Electron in sight. But you're not locked to it. The server speaks an open API (REST and SignalR) plus native IRC, so you can reach it however you like. There's already [**decho**](https://github.com/Stone-Red-Code/Decho), any IRC client works fine, and if none of those suit you, writing your own is fair game.
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Screenshots / demo GIF go here once ready. Drop them in docs/images/ and reference like:
|
||||||
|
<p align="center"><img src="docs/images/demo.gif" alt="EchoHub in action" width="820" /></p>
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
One server process speaks two protocols: SignalR for native clients and raw IRC for everything else. Both land on the same `ChatService`, so a message from irssi and one from the terminal client look identical by the time they hit the database.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
graph LR
|
||||||
subgraph Server["Server"]
|
subgraph Clients
|
||||||
ChatSvc["ChatService"]
|
direction TB
|
||||||
Hub["SignalR ChatHub"]
|
TUI["Terminal client<br/>Terminal.Gui v2"]
|
||||||
IRC["IRC Gateway :6667"]
|
Desk["decho · your app<br/>REST + SignalR"]
|
||||||
|
IRCC["IRC clients<br/>irssi · WeeChat · …"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Server["EchoHub Server · ASP.NET Core"]
|
||||||
|
direction TB
|
||||||
|
Hub["SignalR Hub"]
|
||||||
|
GW["IRC Gateway<br/>:6667"]
|
||||||
|
Chat["ChatService"]
|
||||||
Auth["JWT Auth"]
|
Auth["JWT Auth"]
|
||||||
DB["SQLite DB (EF Core)"]
|
DB[("SQLite · EF Core")]
|
||||||
Files["File Storage"]
|
Files[("File store")]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Clients["Clients"]
|
TUI == "WebSocket" ==> Hub
|
||||||
TUI["Terminal GUI (TUI)"]
|
TUI -. "REST" .-> Auth
|
||||||
IRCClient["IRC Client (irssi, WeeChat, ...)"]
|
Desk == "WebSocket" ==> Hub
|
||||||
end
|
Desk -. "REST" .-> Auth
|
||||||
|
IRCC == "TCP" ==> GW
|
||||||
|
Hub --> Chat
|
||||||
|
GW --> Chat
|
||||||
|
Auth --> Chat
|
||||||
|
Chat --> DB
|
||||||
|
Chat --> Files
|
||||||
|
|
||||||
TUI -- "WebSocket" --> Hub
|
classDef core fill:#512BD4,stroke:#c3b5ff,color:#ffffff,stroke-width:1.5px;
|
||||||
TUI -- "REST" --> Auth
|
classDef io fill:#1f6feb,stroke:#9dc1ff,color:#ffffff;
|
||||||
IRCClient -- "TCP" --> IRC
|
classDef data fill:#0f7b8a,stroke:#7fd3de,color:#ffffff;
|
||||||
Hub --> ChatSvc
|
classDef client fill:#22272e,stroke:#768390,color:#e6edf3;
|
||||||
IRC --> ChatSvc
|
|
||||||
ChatSvc --> DB
|
class Chat core
|
||||||
Auth --> DB
|
class Hub,GW,Auth io
|
||||||
Files --> DB
|
class DB,Files data
|
||||||
|
class TUI,Desk,IRCC client
|
||||||
```
|
```
|
||||||
|
|
||||||
## What you get
|
## Highlights
|
||||||
|
|
||||||
### Server
|
| | |
|
||||||
|
| --- | --- |
|
||||||
|
| 🔒 **Actually private** | Self-hosted, no telemetry. Password rooms are end-to-end encrypted with a key derived from the passphrase on your own machine, so not even the server owner can read them. |
|
||||||
|
| 🖥️ **Native client, open API** | A real terminal client ships in the box. The REST + SignalR API is open too, so you're never stuck with it: there's the [**decho**](https://github.com/Stone-Red-Code/Decho) desktop app, any IRC client, or roll your own. |
|
||||||
|
| 💬 **IRC still works** | A built-in gateway drops irssi and WeeChat users into the same rooms as everyone else, live. Actions, replies and presence all carry across. |
|
||||||
|
| 🛡️ **Moderation, built in** | Four roles with ban, kick, mute and invite-only signup. Spam and flooding earn an automatic timed mute, no plugins to install. |
|
||||||
|
| 📎 **Files, images, audio** | Uploads are checked by their real bytes, not the file extension. Images even render as ASCII in the terminal, because why not. |
|
||||||
|
| 📤 **Take your data and go** | One command exports everything the server knows about you. Another deletes the account for good. |
|
||||||
|
|
||||||
- **Self-hostable** — your server, your rules, your data
|
Full feature tour in the [documentation](https://huebyte.github.io/EchoHub/articles/getting-started.html).
|
||||||
- **Real-time messaging** via SignalR WebSockets
|
|
||||||
- **IRC gateway** — native IRC clients connect alongside TUI users, full cross-protocol messaging
|
|
||||||
- **JWT auth** with short-lived access tokens and 30-day refresh tokens
|
|
||||||
- **Channels** — create, set topics, delete (no 47-step permission wizard required)
|
|
||||||
- **File & image uploads** with actual validation (magic bytes, not just trusting the extension)
|
|
||||||
- **Image-to-ASCII** — because images in a terminal is objectively cool
|
|
||||||
- **Presence tracking** — online/away/DND/invisible with custom status messages
|
|
||||||
- **Rate limiting** — in case someone gets too excited
|
|
||||||
- **Auto-restart** on crash with exponential backoff — it picks itself back up
|
|
||||||
- **Serilog logging** — console + rolling file, because `Console.WriteLine` isn't a logging strategy
|
|
||||||
- **Zero config first run** — generates its own JWT secret and config on launch
|
|
||||||
|
|
||||||
### Client
|
## Clients
|
||||||
|
|
||||||
- **Runs in your terminal** — no browser, no Electron, no 500MB of bundled Chromium
|
The terminal client is the main, native interface. Beyond that the API and the IRC gateway are open, so you can bring whatever you like.
|
||||||
- **13 built-in themes** — including `hacker` for when you want to feel like you're in a movie
|
|
||||||
- **Slash commands** — `/join`, `/send`, `/status`, `/theme`, etc.
|
|
||||||
- **Colored nicknames** — pick your hex color, express yourself
|
|
||||||
- **File/image sharing** — local files or URLs
|
|
||||||
- **Multi-server** — save and switch between servers
|
|
||||||
- **Auto-reconnect** — drops happen, it rejoins your channels automatically
|
|
||||||
- **Message history** on join — you won't miss context
|
|
||||||
|
|
||||||
## Getting Started
|
| Client | Platform | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **[Terminal client](src/EchoHub.Client)** | Windows · macOS · Linux | The main, native client (Terminal.Gui v2), shipped as a self-contained binary |
|
||||||
|
| **[decho](https://github.com/Stone-Red-Code/Decho)** | Desktop | A community desktop client |
|
||||||
|
| **Any IRC client** | Everywhere | irssi, WeeChat, TheLounge, and friends, through the built-in gateway |
|
||||||
|
| **Your own** | anything | Build against the open REST + SignalR API. See the [documentation](https://huebyte.github.io/EchoHub/) |
|
||||||
|
|
||||||
### Download
|
## Quick start
|
||||||
|
|
||||||
Grab a self-contained binary from [Releases](../../releases) — no runtime needed, just run it.
|
### Install the client
|
||||||
|
|
||||||
### Prerequisites (for development)
|
|
||||||
|
|
||||||
- [.NET 10 SDK](https://dotnet.microsoft.com/download)
|
|
||||||
|
|
||||||
### Run the Server
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet run --project src/EchoHub.Server
|
# Windows (Chocolatey)
|
||||||
|
choco install echohub
|
||||||
|
|
||||||
|
# Linux / macOS
|
||||||
|
curl -sSfL https://raw.githubusercontent.com/HueByte/EchoHub/master/scripts/install.sh | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
First run does everything for you:
|
Or grab a self-contained binary from [Releases](../../releases). No runtime required.
|
||||||
|
|
||||||
1. Creates `appsettings.json` from the example config
|
### Host a server
|
||||||
2. Generates a secure JWT secret
|
|
||||||
3. Creates the database with a `#general` channel
|
|
||||||
|
|
||||||
### Run the Client
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet run --project src/EchoHub.Client
|
cp .env.example .env
|
||||||
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Connect, register, chat. That's the whole onboarding flow.
|
Pre-built multi-arch images live on [GHCR](https://github.com/HueByte/EchoHub/pkgs/container/echohub-server). Prefer running from source? `dotnet run --project src/EchoHub.Server` (needs the [.NET 10 SDK](https://dotnet.microsoft.com/download)). The first launch writes its config, generates a JWT secret, and creates the database with a `#general` channel. No manual setup.
|
||||||
|
|
||||||
### Build from Source
|
→ **[Getting started](https://huebyte.github.io/EchoHub/articles/getting-started.html)** · **[Docker & deployment](https://huebyte.github.io/EchoHub/articles/docker.html)** · **[Configuration](https://huebyte.github.io/EchoHub/articles/configuration.html)**
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Everything lives at **[huebyte.github.io/EchoHub](https://huebyte.github.io/EchoHub/)**:
|
||||||
|
|
||||||
|
| Guide | What's inside |
|
||||||
|
| --- | --- |
|
||||||
|
| [Getting started](https://huebyte.github.io/EchoHub/articles/getting-started.html) | Install, first connection, the onboarding flow |
|
||||||
|
| [TUI guide](https://huebyte.github.io/EchoHub/articles/tui-guide.html) | Slash commands, themes, keybindings, message actions |
|
||||||
|
| [Configuration](https://huebyte.github.io/EchoHub/articles/configuration.html) | Every `appsettings.json` / env key explained |
|
||||||
|
| [IRC gateway](https://huebyte.github.io/EchoHub/articles/irc-gateway.html) | Connecting IRC clients, what maps to what, TLS |
|
||||||
|
| [Encrypted rooms](https://huebyte.github.io/EchoHub/articles/encrypted-rooms.html) | How zero-knowledge password channels work |
|
||||||
|
| [Moderation](https://huebyte.github.io/EchoHub/articles/moderation.html) | Roles, bans, mutes, invite codes |
|
||||||
|
| [Docker & deployment](https://huebyte.github.io/EchoHub/articles/docker.html) | Compose, reverse proxy, TLS (see also [`examples/nginx.conf`](examples/nginx.conf)) |
|
||||||
|
| [Architecture](https://huebyte.github.io/EchoHub/articles/architecture.html) | How the pieces above actually fit together |
|
||||||
|
|
||||||
|
## Building from source
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet build src/EchoHub.slnx
|
dotnet build src/EchoHub.slnx # build everything
|
||||||
|
dotnet test src/EchoHub.Tests # run the test suite
|
||||||
```
|
```
|
||||||
|
|
||||||
## IRC Gateway
|
Contributions are welcome. Open an issue to discuss larger changes first.
|
||||||
|
|
||||||
EchoHub includes a built-in IRC protocol gateway. Any standard IRC client can connect to the same server and chat alongside TUI users — messages flow both ways in real time.
|
## Contributors
|
||||||
|
|
||||||
### Enable It
|
<div align="center">
|
||||||
|
|
||||||
In the server's `appsettings.json`:
|
<sub>The people building EchoHub.</sub>
|
||||||
|
|
||||||
```json
|
<p align="center">
|
||||||
{
|
<a href="https://github.com/HueByte"><img src="https://wsrv.nl/?url=github.com/HueByte.png&w=220&h=220&fit=cover&r=32" width="116" height="116" alt="HueByte" /></a>
|
||||||
"Irc": {
|
|
||||||
"Enabled": true,
|
<a href="https://github.com/Stone-Red-Code"><img src="https://wsrv.nl/?url=github.com/Stone-Red-Code.png&w=220&h=220&fit=cover&r=32" width="116" height="116" alt="Stone_Red" /></a>
|
||||||
"Port": 6667,
|
</p>
|
||||||
"ServerName": "echohub",
|
|
||||||
"Motd": "Welcome to EchoHub IRC Gateway!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Connect
|
<p align="center">
|
||||||
|
<a href="https://github.com/HueByte"><b>HueByte</b></a>
|
||||||
|
·
|
||||||
|
<a href="https://github.com/Stone-Red-Code"><b>Stone_Red</b></a>
|
||||||
|
<br />
|
||||||
|
<sub>Creators & maintainers</sub>
|
||||||
|
</p>
|
||||||
|
|
||||||
```bash
|
</div>
|
||||||
# irssi
|
|
||||||
irssi -c your-server.com -p 6667 -w <password> -n <username>
|
|
||||||
|
|
||||||
# WeeChat
|
|
||||||
/server add echohub your-server.com/6667 -password=<password> -nicks=<username>
|
|
||||||
/connect echohub
|
|
||||||
```
|
|
||||||
|
|
||||||
IRC users must have an existing EchoHub account (no registration via IRC). Auth works via `PASS`/`NICK`/`USER` or SASL PLAIN.
|
|
||||||
|
|
||||||
### What Works
|
|
||||||
|
|
||||||
| Feature | How it maps to IRC |
|
|
||||||
| ------- | ------------------ |
|
|
||||||
| Text messages | Standard `PRIVMSG` (long messages split at ~400 byte chunks) |
|
|
||||||
| Images | `[Image: filename]` + download URL + ASCII art line-by-line |
|
|
||||||
| File uploads | `[File: filename] /api/files/{id}` |
|
|
||||||
| Channels | `JOIN`, `PART`, `NAMES`, `TOPIC`, `LIST` |
|
|
||||||
| Presence | `AWAY`, `WHO`, `WHOIS` |
|
|
||||||
| Status | Maps to IRC away/here |
|
|
||||||
|
|
||||||
### TLS
|
|
||||||
|
|
||||||
If running behind **nginx** (recommended), let nginx handle TLS -- see [Deployment with nginx](#deployment-with-nginx) below.
|
|
||||||
|
|
||||||
For direct TLS without a reverse proxy, the IRC gateway can terminate TLS itself:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"Irc": {
|
|
||||||
"TlsEnabled": true,
|
|
||||||
"TlsPort": 6697,
|
|
||||||
"TlsCertPath": "/path/to/cert.pfx",
|
|
||||||
"TlsCertPassword": "your-password"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Client Commands
|
|
||||||
|
|
||||||
| Command | Description |
|
|
||||||
| ------- | ----------- |
|
|
||||||
| `/join <channel>` | Join a channel |
|
|
||||||
| `/leave` | Leave current channel |
|
|
||||||
| `/topic <text>` | Set channel topic (creator only) |
|
|
||||||
| `/send <file or URL>` | Upload a file or image |
|
|
||||||
| `/status <online\|away\|dnd\|invisible>` | Set your status |
|
|
||||||
| `/status <message>` | Set a status message |
|
|
||||||
| `/nick <name>` | Set display name |
|
|
||||||
| `/color <#hex>` | Set nickname color |
|
|
||||||
| `/theme <name>` | Switch theme |
|
|
||||||
| `/profile` | Open profile editor |
|
|
||||||
| `/users` | List online users in channel |
|
|
||||||
| `/servers` | Manage saved servers |
|
|
||||||
| `/help` | Show help |
|
|
||||||
| `/quit` | Exit |
|
|
||||||
|
|
||||||
## Themes
|
|
||||||
|
|
||||||
`/theme <name>` to switch:
|
|
||||||
|
|
||||||
| Theme | Vibe |
|
|
||||||
| ----- | ---- |
|
|
||||||
| `default` | Gray on black — clean and quiet |
|
|
||||||
| `transparent` | White on black — for fancy transparent terminals |
|
|
||||||
| `classic` | White on blue — IRC nostalgia |
|
|
||||||
| `light` | Black on white — for the brave |
|
|
||||||
| `hacker` | Green on black — *I'm in* |
|
|
||||||
| `solarized` | Cyan/yellow on dark gray — for the refined |
|
|
||||||
| `dracula` | Purple accents on black — the classic dark theme |
|
|
||||||
| `monokai` | Yellow highlights on black — warm and familiar |
|
|
||||||
| `nord` | Cool blues — arctic vibes |
|
|
||||||
| `gruvbox` | Earthy yellows on black — retro warmth |
|
|
||||||
| `ocean` | Cyan on deep blue — underwater aesthetics |
|
|
||||||
| `highcontrast` | Bright yellow on black — maximum readability |
|
|
||||||
| `rosepine` | Muted pinks on black — cozy and soft |
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
`appsettings.json` is auto-generated on first run. Tweak what you need:
|
|
||||||
|
|
||||||
| Key | Default | Description |
|
|
||||||
| --- | ------- | ----------- |
|
|
||||||
| `Urls` | `http://0.0.0.0:5000` | Listen address and port |
|
|
||||||
| `ConnectionStrings:DefaultConnection` | *(empty — app directory)* | SQLite connection string |
|
|
||||||
| `Jwt:Secret` | *(auto-generated)* | JWT signing key |
|
|
||||||
| `Server:Name` | `My EchoHub Server` | Server display name |
|
|
||||||
| `Server:Description` | `A self-hosted EchoHub chat server` | Server description |
|
|
||||||
| `Server:PublicServer` | `false` | List on the [public directory](https://echohub.voidcube.cloud/servers) |
|
|
||||||
| `Server:PublicHost` | *(empty)* | Public hostname for directory listing |
|
|
||||||
| `Irc:Enabled` | `false` | Enable the IRC gateway |
|
|
||||||
| `Irc:Port` | `6667` | IRC listen port |
|
|
||||||
| `Irc:TlsEnabled` | `false` | Enable TLS for IRC |
|
|
||||||
| `Irc:TlsPort` | `6697` | IRC TLS port |
|
|
||||||
| `Irc:ServerName` | `echohub` | IRC server name in protocol messages |
|
|
||||||
| `Irc:Motd` | `Welcome to EchoHub IRC Gateway!` | Message of the day |
|
|
||||||
| `Cors:AllowedOrigins` | *(all origins)* | CORS whitelist |
|
|
||||||
|
|
||||||
Logging uses Serilog — console + daily rolling files with 14-day retention. Configure in the `Serilog` section.
|
|
||||||
|
|
||||||
## Deployment with nginx
|
|
||||||
|
|
||||||
Most production deployments run behind nginx. Here's a config that handles both the HTTP/WebSocket server and the IRC gateway:
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
# HTTP + WebSocket (EchoHub Server API + SignalR)
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name echohub.example.com;
|
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/echohub.example.com/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/echohub.example.com/privkey.pem;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://127.0.0.1:5000;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
|
|
||||||
# Required for SignalR WebSocket
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection $http_connection;
|
|
||||||
|
|
||||||
proxy_read_timeout 86400s;
|
|
||||||
proxy_send_timeout 86400s;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Increase max upload size for file sharing
|
|
||||||
client_max_body_size 10m;
|
|
||||||
}
|
|
||||||
|
|
||||||
# HTTP → HTTPS redirect
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name echohub.example.com;
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
# IRC TLS (port 6697 → plain IRC on 6667)
|
|
||||||
stream {
|
|
||||||
upstream irc_backend {
|
|
||||||
server 127.0.0.1:6667;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 6697 ssl;
|
|
||||||
proxy_pass irc_backend;
|
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/echohub.example.com/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/echohub.example.com/privkey.pem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
With this setup, keep the EchoHub IRC gateway's `TlsEnabled` set to `false` — nginx terminates TLS. See the full example at [`examples/nginx.conf`](examples/nginx.conf).
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```text
|
|
||||||
src/
|
|
||||||
├── EchoHub.Core/ # Shared models, DTOs, contracts, validation
|
|
||||||
│ ├── Constants/ # ValidationConstants, HubConstants
|
|
||||||
│ ├── Contracts/ # IChatService, IChatBroadcaster, IEchoHubClient
|
|
||||||
│ ├── DTOs/ # Record DTOs
|
|
||||||
│ └── Models/ # Entity models
|
|
||||||
│
|
|
||||||
├── EchoHub.Server/ # ASP.NET Core server
|
|
||||||
│ ├── Auth/ # JWT token service
|
|
||||||
│ ├── Controllers/ # REST API endpoints
|
|
||||||
│ ├── Data/ # EF Core DbContext + migrations
|
|
||||||
│ ├── Hubs/ # SignalR ChatHub
|
|
||||||
│ ├── Services/ # ChatService, presence, file storage, image processing
|
|
||||||
│ └── Setup/ # First-run setup, DB initialization
|
|
||||||
│
|
|
||||||
├── EchoHub.Server.Irc/ # IRC protocol gateway
|
|
||||||
│ ├── IrcGatewayService # TCP listener (BackgroundService)
|
|
||||||
│ ├── IrcCommandHandler # IRC command dispatch (JOIN, PRIVMSG, etc.)
|
|
||||||
│ ├── IrcBroadcaster # Fans chat events to IRC connections
|
|
||||||
│ └── IrcMessageFormatter # MessageDto → IRC PRIVMSG lines
|
|
||||||
│
|
|
||||||
├── EchoHub.Client/ # Terminal.Gui TUI client
|
|
||||||
│ ├── Config/ # Client configuration
|
|
||||||
│ ├── Services/ # API client, SignalR connection
|
|
||||||
│ ├── Themes/ # 13 built-in themes
|
|
||||||
│ └── UI/ # MainWindow, dialogs, chat renderer
|
|
||||||
│
|
|
||||||
└── EchoHub.slnx # Solution file
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
[MIT](LICENSE) — do whatever you want with it.
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
@@ -0,0 +1,11 @@
|
|||||||
|
<svg width="32" height="32" viewBox="0 0 1000 1000" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g stroke="#E6C06E" stroke-width="28" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="500" cy="500" r="420" />
|
||||||
|
<circle cx="500" cy="500" r="180" />
|
||||||
|
<polygon points="500,440 550,532 450,532" fill="#E6C06E" />
|
||||||
|
<line x1="500" y1="80" x2="500" y2="320" />
|
||||||
|
<circle cx="285" cy="390" r="8" fill="#E6C06E" />
|
||||||
|
<circle cx="715" cy="390" r="8" fill="#E6C06E" />
|
||||||
|
<circle cx="500" cy="750" r="8" fill="#E6C06E" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 590 B |
@@ -0,0 +1,19 @@
|
|||||||
|
services:
|
||||||
|
echohub-server:
|
||||||
|
build:
|
||||||
|
context: ./src
|
||||||
|
dockerfile: EchoHub.Server/Dockerfile
|
||||||
|
# image: ghcr.io/huebyte/echohub-server:latest # use this instead of build for pre-built images
|
||||||
|
container_name: echohub-server
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
# - "6667:6667" # IRC (plain, no encryption)
|
||||||
|
# - "6697:6697" # IRC (TLS encrypted, preferred)
|
||||||
|
volumes:
|
||||||
|
- echohub-data:/app/data
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
echohub-data:
|
||||||
@@ -5,7 +5,9 @@ Articles related to the EchoHub TUI client built with Terminal.Gui v2.
|
|||||||
## Topics
|
## Topics
|
||||||
|
|
||||||
- Terminal.Gui v2 patterns and conventions
|
- Terminal.Gui v2 patterns and conventions
|
||||||
- Theme system and customization
|
- Theme system and customization (including transparent theme)
|
||||||
- Command system reference
|
- Command system reference
|
||||||
- Configuration management
|
- Configuration and session persistence
|
||||||
|
- Audio playback and file downloads
|
||||||
|
- Automatic update checking
|
||||||
- [Notification sounds](../../articles/notification-sounds.md)
|
- [Notification sounds](../../articles/notification-sounds.md)
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ Articles related to the EchoHub.Core shared library.
|
|||||||
## Topics
|
## Topics
|
||||||
|
|
||||||
- Data models and DTOs
|
- Data models and DTOs
|
||||||
- Contract interfaces (IChatService, IChatBroadcaster, IEchoHubClient)
|
- Contract interfaces (IChatService, IChannelService, IChatBroadcaster, IEchoHubClient)
|
||||||
- Validation constants and shared rules
|
- Validation constants and shared rules
|
||||||
|
|||||||
+2
-2
@@ -10,11 +10,11 @@ Terminal.Gui v2 TUI application -- UI components, services, themes, and configur
|
|||||||
|
|
||||||
### Core
|
### Core
|
||||||
|
|
||||||
Shared library -- DTOs, models, constants, and contracts (`IChatService`, `IChatBroadcaster`, `IEchoHubClient`).
|
Shared library -- DTOs, models, constants, and contracts (`IChatService`, `IChannelService`, `IChatBroadcaster`, `IEchoHubClient`).
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
|
|
||||||
ASP.NET Core server -- controllers, hubs, ChatService, SignalRBroadcaster, authentication, and data access.
|
ASP.NET Core server -- controllers, hubs, ChatService, ChannelService, SignalRBroadcaster, authentication, file cleanup, and data access.
|
||||||
|
|
||||||
### Server.Irc
|
### Server.Irc
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Articles related to the EchoHub server built with ASP.NET Core.
|
|||||||
- SignalR hub and real-time messaging
|
- SignalR hub and real-time messaging
|
||||||
- IRC gateway and protocol bridging
|
- IRC gateway and protocol bridging
|
||||||
- ChatService and broadcaster pattern
|
- ChatService and broadcaster pattern
|
||||||
- File upload and validation
|
- ChannelService and channel CRUD
|
||||||
|
- File upload, validation, and cleanup
|
||||||
- Rate limiting configuration
|
- Rate limiting configuration
|
||||||
- Database schema and migrations
|
- Database schema, migrations, and DataMigrationService
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Shared library containing:
|
|||||||
|
|
||||||
- **Models**: `User`, `Channel`, `Message`, `RefreshToken`
|
- **Models**: `User`, `Channel`, `Message`, `RefreshToken`
|
||||||
- **DTOs**: Record types for API requests/responses
|
- **DTOs**: Record types for API requests/responses
|
||||||
- **Contracts**: `IChatService` (protocol-agnostic chat operations), `IChatBroadcaster` (event fan-out interface), `IEchoHubClient` (SignalR client interface)
|
- **Contracts**: `IChatService` (protocol-agnostic chat operations), `IChannelService` (channel CRUD and membership), `IChatBroadcaster` (event fan-out interface), `IEchoHubClient` (SignalR client interface)
|
||||||
- **Constants**: `ValidationConstants` (shared regex patterns), `HubConstants`
|
- **Constants**: `ValidationConstants` (shared regex patterns), `HubConstants`
|
||||||
|
|
||||||
### EchoHub.Server
|
### EchoHub.Server
|
||||||
@@ -33,7 +33,7 @@ ASP.NET Core web application:
|
|||||||
- **Hubs**: SignalR `ChatHub` -- thin adapter delegating to `IChatService`
|
- **Hubs**: SignalR `ChatHub` -- thin adapter delegating to `IChatService`
|
||||||
- **Auth**: JWT token service (15-min access tokens, 30-day refresh tokens)
|
- **Auth**: JWT token service (15-min access tokens, 30-day refresh tokens)
|
||||||
- **Data**: EF Core with SQLite
|
- **Data**: EF Core with SQLite
|
||||||
- **Services**: `ChatService` (core business logic), `SignalRBroadcaster`, presence tracking, file storage, image-to-ASCII conversion
|
- **Services**: `ChatService` (core business logic), `ChannelService` (channel CRUD and membership), `SignalRBroadcaster`, presence tracking, file storage, image-to-ASCII conversion, `FileCleanupService` (periodic removal of expired uploads), `DataMigrationService` (startup schema/data evolution)
|
||||||
|
|
||||||
### EchoHub.Server.Irc
|
### EchoHub.Server.Irc
|
||||||
|
|
||||||
@@ -51,9 +51,9 @@ IRC users authenticate with existing EchoHub accounts via `PASS`/`NICK`/`USER` o
|
|||||||
Terminal.Gui v2 TUI application:
|
Terminal.Gui v2 TUI application:
|
||||||
|
|
||||||
- **UI**: Main window, dialogs, chat renderer with ANSI color support
|
- **UI**: Main window, dialogs, chat renderer with ANSI color support
|
||||||
- **Services**: API client with automatic token refresh, SignalR connection wrapper
|
- **Services**: API client with automatic token refresh, SignalR connection wrapper, audio playback (NetCoreAudio), automatic update checker (AlwaysUpToDate)
|
||||||
- **Themes**: 13 built-in color themes
|
- **Themes**: 14 built-in color themes (including transparent dark/light themes with true terminal transparency)
|
||||||
- **Config**: Client configuration management
|
- **Config**: Client configuration management with session persistence ("Remember Me" refresh tokens)
|
||||||
|
|
||||||
## Communication
|
## Communication
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
# Configuration
|
||||||
|
|
||||||
|
EchoHub Server generates an `appsettings.json` with sensible defaults on first run (including a random JWT secret), so you can launch and start chatting immediately. Tweak things later when you feel like it.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Under the hood, EchoHub Server is built on ASP.NET Core, so it inherits the standard .NET configuration system. If you're familiar with that, everything works exactly as you'd expect. If not — no worries, this page covers everything you need.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
EchoHub Server loads settings from multiple sources. Each source **overrides** the previous one, so you can layer defaults with environment-specific values:
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. appsettings.json (base defaults)
|
||||||
|
2. appsettings.{Environment}.json (e.g. appsettings.Production.json)
|
||||||
|
3. Environment variables (great for Docker / CI)
|
||||||
|
4. Command-line arguments (highest priority)
|
||||||
|
```
|
||||||
|
|
||||||
|
The last one wins. If `appsettings.json` says `"Irc:Port": 6667` but you pass `--Irc:Port=7000` on the command line, port 7000 is what you get.
|
||||||
|
|
||||||
|
In practice this means you can leave `appsettings.json` alone and override just the settings you care about using environment variables or CLI flags — no need to edit JSON files if that's not your thing.
|
||||||
|
|
||||||
|
### Environment Variable Mapping
|
||||||
|
|
||||||
|
Environment variables use **double underscores** (`__`) in place of the JSON nesting. The rule is simple — replace every `:` (or each level of JSON nesting) with `__`:
|
||||||
|
|
||||||
|
| appsettings.json path | Environment variable |
|
||||||
|
| --- | --- |
|
||||||
|
| `Server:Name` | `Server__Name` |
|
||||||
|
| `Irc:Enabled` | `Irc__Enabled` |
|
||||||
|
| `Jwt:Secret` | `Jwt__Secret` |
|
||||||
|
| `Serilog:MinimumLevel:Default` | `Serilog__MinimumLevel__Default` |
|
||||||
|
| `ConnectionStrings:DefaultConnection` | `ConnectionStrings__DefaultConnection` |
|
||||||
|
|
||||||
|
Arrays use numeric indices: `Server:Admins:0` becomes `Server__Admins__0`, `Server:Admins:1` becomes `Server__Admins__1`, and so on.
|
||||||
|
|
||||||
|
This is why the Docker `.env` file uses `Server__Name=My Server` instead of JSON — Docker passes these as environment variables, and the server picks them up automatically.
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
All three of these achieve the same thing — use whichever fits your setup.
|
||||||
|
|
||||||
|
**appsettings.json** (direct editing):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Server": {
|
||||||
|
"Name": "My EchoHub Server",
|
||||||
|
"PublicServer": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Environment variables** (Docker, systemd, shell export):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export Server__Name="My EchoHub Server"
|
||||||
|
export Server__PublicServer=true
|
||||||
|
```
|
||||||
|
|
||||||
|
**Command-line arguments** (quick overrides, highest priority):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./EchoHub.Server --Server:Name="My EchoHub Server" --Irc:Enabled=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Reference
|
||||||
|
|
||||||
|
The full `appsettings.json` is auto-generated on first run from the [example config](https://github.com/HueByte/EchoHub/blob/master/src/EchoHub.Server/appsettings.example.json). Here's every option:
|
||||||
|
|
||||||
|
### General
|
||||||
|
|
||||||
|
| Key | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Urls` | `http://0.0.0.0:5000` | Listen address and port |
|
||||||
|
| `AllowedHosts` | `*` | Allowed host headers (leave `*` unless you need host filtering) |
|
||||||
|
|
||||||
|
### Database
|
||||||
|
|
||||||
|
| Key | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `ConnectionStrings:DefaultConnection` | *(empty)* | SQLite connection string. Empty = `echohub.db` in the app directory |
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
| Key | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Jwt:Secret` | *(auto-generated)* | Signing key (min 32 chars). Auto-generated on first run |
|
||||||
|
| `Jwt:Issuer` | `EchoHub.Server` | JWT issuer claim |
|
||||||
|
| `Jwt:Audience` | `EchoHub.Client` | JWT audience claim |
|
||||||
|
|
||||||
|
Access tokens expire after 15 minutes, refresh tokens after 30 days with rotation on each use.
|
||||||
|
|
||||||
|
### Server Identity
|
||||||
|
|
||||||
|
| Key | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Server:Name` | `My EchoHub Server` | Display name shown to clients |
|
||||||
|
| `Server:Description` | `A self-hosted EchoHub chat server` | Server description |
|
||||||
|
| `Server:PublicServer` | `false` | Register on the [public directory](https://echohub.voidcube.cloud/servers) |
|
||||||
|
| `Server:PublicHosts` | `[]` | Hostnames advertised to the directory (array, e.g. `["chat.example.com"]`). Required when `PublicServer` is `true` |
|
||||||
|
| `Server:Tags` | `[]` | Topic tags surfaced in the directory browser (array, e.g. `["community", "gaming"]`) |
|
||||||
|
| `Server:Admins` | `[]` | Array of admin usernames (e.g. `["alice", "bob"]`) |
|
||||||
|
| `Server:Registration` | `open` | Registration mode: `open`, `invite` (codes via `/invite`, Admin+), or `closed` |
|
||||||
|
| `Server:DirectoryClaimPath` | *(empty)* | Overrides where the directory claim token file is stored. Empty = `directory-claim.json` next to the SQLite database. Treat it as a secret and back it up with the database |
|
||||||
|
|
||||||
|
### Uploads
|
||||||
|
|
||||||
|
Per-attachment size limits by kind (in megabytes) and the per-message attachment cap. An
|
||||||
|
absent or partial `Uploads` section keeps the built-in defaults. See
|
||||||
|
[Messages & Attachments](messages-and-attachments.md) for how kinds are detected.
|
||||||
|
|
||||||
|
| Key | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Uploads:MaxImageSizeMB` | `10` | Max size for one image attachment |
|
||||||
|
| `Uploads:MaxAudioSizeMB` | `10` | Max size for one audio attachment |
|
||||||
|
| `Uploads:MaxFileSizeMB` | `100` | Max size for any other attachment |
|
||||||
|
| `Uploads:MaxAvatarSizeMB` | `2` | Max avatar upload size |
|
||||||
|
| `Uploads:MaxAttachmentsPerMessage` | `10` | Attachments allowed on a single message |
|
||||||
|
|
||||||
|
The server sizes its request-body limits from these values, so raising a limit here is all
|
||||||
|
that's needed — no separate Kestrel tuning.
|
||||||
|
|
||||||
|
### Spam Protection
|
||||||
|
|
||||||
|
Per-user flood, duplicate, join, and channel-create limits with auto-mute escalation. Mods and
|
||||||
|
above are always exempt, and the defaults are lenient enough that a fast typist never trips them.
|
||||||
|
An absent or partial `Spam` section keeps these defaults.
|
||||||
|
|
||||||
|
| Key | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Spam:Enabled` | `true` | Master switch for all spam protection |
|
||||||
|
| `Spam:MaxMessagesPerWindow` | `8` | Max messages per `WindowSeconds` before a send is rejected |
|
||||||
|
| `Spam:WindowSeconds` | `5` | Sliding window (seconds) for the message-rate check |
|
||||||
|
| `Spam:MaxDuplicateMessages` | `3` | Identical messages in a row tolerated before rejection (E2E rooms are exempt — ciphertext differs each time) |
|
||||||
|
| `Spam:AutoMuteMinutes` | `5` | Auto-mute duration once a user hits the violation threshold. `0` disables auto-mute (rejections still apply) |
|
||||||
|
| `Spam:ViolationThreshold` | `5` | Rejected sends within `ViolationWindowMinutes` that trigger an auto-mute |
|
||||||
|
| `Spam:ViolationWindowMinutes` | `5` | Window (minutes) over which violations accumulate |
|
||||||
|
| `Spam:MaxJoinsPerWindow` | `25` | Max first-time channel joins per `JoinWindowSeconds` — keep this above your public channel count |
|
||||||
|
| `Spam:JoinWindowSeconds` | `30` | Sliding window (seconds) for the join-rate check |
|
||||||
|
| `Spam:MaxChannelCreatesPerWindow` | `3` | Max channel creations per `ChannelCreateWindowMinutes` |
|
||||||
|
| `Spam:ChannelCreateWindowMinutes` | `10` | Window (minutes) for the channel-create check |
|
||||||
|
|
||||||
|
### Encryption
|
||||||
|
|
||||||
|
| Key | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Encryption:Key` | *(auto-generated)* | AES key for message encryption in transit |
|
||||||
|
| `Encryption:EncryptDatabase` | `false` | Also encrypt message content at rest in SQLite |
|
||||||
|
|
||||||
|
### Storage
|
||||||
|
|
||||||
|
| Key | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Storage:Path` | *(empty)* | Directory for uploaded file blobs (attachments, avatars). Empty = `uploads/` in the app directory; the Docker image sets it to `/app/data/uploads` on the persistent volume |
|
||||||
|
| `Storage:CleanupIntervalHours` | `1` | How often the cleanup job runs (hours) |
|
||||||
|
| `Storage:RetentionDays` | `30` | Days to keep uploaded files before cleanup |
|
||||||
|
|
||||||
|
### IRC Gateway
|
||||||
|
|
||||||
|
| Key | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Irc:Enabled` | `false` | Enable the IRC protocol gateway |
|
||||||
|
| `Irc:Port` | `6667` | IRC plain-text listen port |
|
||||||
|
| `Irc:TlsEnabled` | `false` | Enable TLS termination for IRC |
|
||||||
|
| `Irc:TlsPort` | `6697` | IRC TLS listen port |
|
||||||
|
| `Irc:TlsCertPath` | *(empty)* | Path to a PKCS#12 (`.pfx`) certificate |
|
||||||
|
| `Irc:TlsCertPassword` | *(empty)* | Password for the certificate file |
|
||||||
|
| `Irc:ServerName` | `echohub` | IRC server name in protocol messages |
|
||||||
|
| `Irc:Motd` | `Welcome to EchoHub IRC Gateway!` | Message of the day |
|
||||||
|
|
||||||
|
### Server Logs Room
|
||||||
|
|
||||||
|
When enabled, EchoHub auto-creates a read-only system channel and streams log events to it live,
|
||||||
|
so operators can watch the server from inside the app. Log lines are **never stored as messages** —
|
||||||
|
the rolling Serilog files remain the only persistence, and the room replays recent lines from those
|
||||||
|
files when someone opens it.
|
||||||
|
|
||||||
|
| Key | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `ServerLogs:Enabled` | `true` | Master switch for the live log room |
|
||||||
|
| `ServerLogs:RoomName` | `server-logs` | Name of the auto-created channel (reserved — users can't create a channel with this name) |
|
||||||
|
| `ServerLogs:MinRole` | `Mod` | Minimum server role that can see and join the room (`Member`, `Mod`, `Admin`, `Owner`) |
|
||||||
|
| `ServerLogs:MinLevel` | `Information` | Minimum log level streamed to the room (`Verbose`, `Debug`, `Information`, `Warning`, `Error`, `Fatal`) — affects only the room, not the file/console sinks |
|
||||||
|
| `ServerLogs:BacklogLines` | `100` | Recent log lines replayed from file when someone opens the room |
|
||||||
|
| `ServerLogs:LogDirectory` | `logs` | Directory holding the rolling log files (must match the Serilog file sink path below) |
|
||||||
|
| `ServerLogs:LogFilePattern` | `echohub-server-*.log` | Filename glob for the rolling log files inside `LogDirectory` |
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
EchoHub uses [Serilog](https://serilog.net/) for structured logging — console output + daily rolling files with 14-day retention by default.
|
||||||
|
|
||||||
|
| Key | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Serilog:MinimumLevel:Default` | `Information` | Global log level (`Debug`, `Information`, `Warning`, `Error`) |
|
||||||
|
| `Serilog:MinimumLevel:Override:Microsoft` | `Warning` | Suppress noisy framework logs |
|
||||||
|
| `Serilog:MinimumLevel:Override:Microsoft.AspNetCore` | `Warning` | Suppress request pipeline logs |
|
||||||
|
| `Serilog:MinimumLevel:Override:Microsoft.EntityFrameworkCore` | `Warning` | Suppress database query logs |
|
||||||
|
|
||||||
|
Log files are written to `logs/echohub-server-YYYY-MM-DD.log`. To change the path or retention, edit the `Serilog:WriteTo` section in `appsettings.json`.
|
||||||
|
|
||||||
|
Want more verbose output for debugging? Set the minimum level to `Debug`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# via environment variable
|
||||||
|
export Serilog__MinimumLevel__Default=Debug
|
||||||
|
|
||||||
|
# or command line
|
||||||
|
./EchoHub.Server --Serilog:MinimumLevel:Default=Debug
|
||||||
|
```
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# Docker
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env # create your config
|
||||||
|
docker compose up -d # start the server
|
||||||
|
```
|
||||||
|
|
||||||
|
On first run the server automatically generates JWT and encryption keys, creates the database, and seeds a `#general` channel. Connect with the EchoHub client to `http://localhost:5000`.
|
||||||
|
|
||||||
|
### Using a Pre-built Image
|
||||||
|
|
||||||
|
Instead of building locally, you can pull from GHCR. In `docker-compose.yml`, replace the `build` block:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
echohub-server:
|
||||||
|
image: ghcr.io/huebyte/echohub-server:latest
|
||||||
|
# build:
|
||||||
|
# context: ./src
|
||||||
|
# dockerfile: EchoHub.Server/Dockerfile
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
All settings are configured through the `.env` file. These are environment variables that override `appsettings.json` — the `__` (double underscore) maps to JSON nesting levels. For example, `Server__Name` overrides the `Server:Name` key in `appsettings.json`.
|
||||||
|
|
||||||
|
See the [Configuration](configuration.md) guide for the full reference of all available settings and how the override hierarchy works.
|
||||||
|
|
||||||
|
Common Docker-relevant variables:
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Server__Name` | My EchoHub Server | Display name for your server |
|
||||||
|
| `Server__Admins__0` | *(empty)* | Admin username (use `__1`, `__2` for more) |
|
||||||
|
| `Irc__Enabled` | `false` | Enable the IRC gateway |
|
||||||
|
| `Serilog__MinimumLevel__Default` | `Information` | Log level (`Debug`, `Warning`, etc.) |
|
||||||
|
|
||||||
|
## Persistent Data
|
||||||
|
|
||||||
|
All server state lives in a single Docker volume mounted at `/app/data`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/app/data/
|
||||||
|
├── appsettings.json # generated config with JWT/encryption keys
|
||||||
|
├── echohub.db # SQLite database
|
||||||
|
├── uploads/ # uploaded files and avatars
|
||||||
|
└── logs/ # rolling log files (14-day retention)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# stop the server to ensure a consistent snapshot
|
||||||
|
docker compose stop
|
||||||
|
# copy the data volume to a local directory
|
||||||
|
docker cp echohub-server:/app/data ./backup
|
||||||
|
docker compose start
|
||||||
|
```
|
||||||
|
|
||||||
|
## IRC Gateway
|
||||||
|
|
||||||
|
To enable IRC, set these in your `.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
Irc__Enabled=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Then uncomment the port in `docker-compose.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
- "6697:6697" # IRC (TLS encrypted, preferred)
|
||||||
|
```
|
||||||
|
|
||||||
|
For TLS, also set:
|
||||||
|
|
||||||
|
```env
|
||||||
|
Irc__TlsEnabled=true
|
||||||
|
Irc__TlsCertPath=/app/data/cert.pfx
|
||||||
|
Irc__TlsCertPassword=your_password
|
||||||
|
```
|
||||||
|
|
||||||
|
Mount your certificate into the data volume or bind-mount it directly.
|
||||||
|
|
||||||
|
IRC users must have an existing EchoHub account. See [Getting Started](getting-started.md#connect-via-irc) for client connection examples.
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# if using pre-built images
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# if building locally
|
||||||
|
docker compose build
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Data persists across updates since it lives in the named volume.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Port already in use** -- Another process is using port 5000. Change the host port in `docker-compose.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ports:
|
||||||
|
- "8080:5000" # access via http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
**Permission denied on volume** -- The container runs as a non-root `echohub` user (UID 999). If using bind mounts instead of named volumes, ensure the directory is writable.
|
||||||
|
|
||||||
|
**View logs** -- Check the container output:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose logs -f echohub-server
|
||||||
|
```
|
||||||
|
|
||||||
|
File-based logs are also available inside the volume at `/app/data/logs/`.
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
# Encrypted Rooms (Password-Protected Channels)
|
||||||
|
|
||||||
|
An **encrypted room** is a channel whose entire content — every message and every file — is
|
||||||
|
end-to-end encrypted with a key derived from a shared passphrase. Only people who know the
|
||||||
|
passphrase can read the room. **Not even the server owner can read the content**, yet the server
|
||||||
|
can still gate who joins, and it can count and measure what's stored (message count, file sizes,
|
||||||
|
timestamps) without ever seeing the plaintext.
|
||||||
|
|
||||||
|
This is a stronger guarantee than the [transport and at-rest encryption](encryption.md) described
|
||||||
|
elsewhere, where the server decrypts every message to process it. Here the server is treated as
|
||||||
|
*untrusted* for content: it holds only ciphertext and wrapped keys.
|
||||||
|
|
||||||
|
> **The passphrase is the only key.** There is no recovery. If everyone who knows a room's
|
||||||
|
> passphrase forgets it, that room's history is permanently unreadable — by design.
|
||||||
|
|
||||||
|
## What the server can and cannot see
|
||||||
|
|
||||||
|
| The server **can** see | The server **cannot** see |
|
||||||
|
| --- | --- |
|
||||||
|
| That the channel is encrypted | Message text |
|
||||||
|
| Message count and timestamps | File contents |
|
||||||
|
| Who sent each message (sender identity) | Image previews (ASCII art) |
|
||||||
|
| Each attachment's **file name** and byte size | The passphrase, the room key, or the key-encryption key |
|
||||||
|
| The estimated total size (via `/meta`) | Anything that would let it decrypt the above |
|
||||||
|
|
||||||
|
File **names are stored in plaintext** so the file list stays usable — treat a file name itself as
|
||||||
|
non-secret. Everything *inside* the file is encrypted.
|
||||||
|
|
||||||
|
## Key hierarchy
|
||||||
|
|
||||||
|
Three keys are derived from one passphrase. The passphrase, the key-encryption key, and the room
|
||||||
|
content key **never leave the client**.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
P[Passphrase] -->|PBKDF2-SHA256, 210k iterations, per-room salt| OKM[64-byte output]
|
||||||
|
OKM --> AK["Auth key (first 32 bytes)"]
|
||||||
|
OKM --> KEK["Key-encryption key / KEK (last 32 bytes)"]
|
||||||
|
AK -->|sent as lowercase hex| SRV1["Server: BCrypt-hash as the join gate"]
|
||||||
|
RCK["Room Content Key (random 256-bit)"] -->|encrypts all content| CONTENT[Messages + files + previews]
|
||||||
|
RCK -->|AES-256-GCM wrap under KEK| WRAP["Wrapped room key"]
|
||||||
|
WRAP -->|stored| SRV2["Server: stores wrapped key + salt only"]
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Auth key** — the join credential. Derived from the passphrase, sent to the server as hex, and
|
||||||
|
stored only as a **BCrypt hash**. Proving knowledge of it is what lets you join; it reveals
|
||||||
|
nothing about the content key.
|
||||||
|
- **Key-encryption key (KEK)** — never sent. Used locally to *wrap* (encrypt) and *unwrap* the room
|
||||||
|
content key.
|
||||||
|
- **Room Content Key (RCK)** — a random 256-bit key generated once, at room creation. It encrypts
|
||||||
|
every message and file. The server stores it only in wrapped form, so it can hand the wrapped key
|
||||||
|
to a joiner but can never unwrap it itself.
|
||||||
|
|
||||||
|
All content encryption is **AES-256-GCM** with a random 12-byte nonce and a 16-byte authentication
|
||||||
|
tag per item, so identical inputs never produce identical ciphertext, and any tampering is detected.
|
||||||
|
|
||||||
|
Room-encrypted text carries a self-describing prefix so clients and the server can tell it apart
|
||||||
|
from plaintext:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$RC1$base64(nonce || tag || ciphertext)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating a room
|
||||||
|
|
||||||
|
The client does all the cryptography locally, then hands the server only what it needs to gate joins
|
||||||
|
and store (but not read) the content.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Client
|
||||||
|
participant Server
|
||||||
|
|
||||||
|
Client->>Client: generate salt + random Room Content Key (RCK)
|
||||||
|
Client->>Client: DeriveKeys(passphrase, salt) → auth key + KEK
|
||||||
|
Client->>Client: wrap RCK under KEK
|
||||||
|
Client->>Server: create channel { authKey(hex), salt, wrappedRoomKey }
|
||||||
|
Server->>Server: BCrypt-hash auth key, store salt + wrapped key
|
||||||
|
Note over Server: Server never receives passphrase, KEK, or RCK
|
||||||
|
```
|
||||||
|
|
||||||
|
## Joining a room
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Client
|
||||||
|
participant Server
|
||||||
|
|
||||||
|
Client->>Server: GET /crypto → { isEncrypted, salt }
|
||||||
|
Client->>Client: DeriveKeys(passphrase, salt) → auth key + KEK
|
||||||
|
Client->>Server: join { authKey(hex) }
|
||||||
|
Server->>Server: BCrypt-verify against stored hash
|
||||||
|
alt correct passphrase
|
||||||
|
Server->>Client: history (ciphertext) + wrapped room key
|
||||||
|
Client->>Client: unwrap RCK with KEK, then decrypt everything locally
|
||||||
|
else wrong passphrase
|
||||||
|
Server->>Client: rejected (join gate fails)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
A wrong passphrase fails the BCrypt gate, so the server never even hands out the wrapped key. Even if
|
||||||
|
it did, an attacker without the KEK cannot unwrap it.
|
||||||
|
|
||||||
|
## What gets encrypted
|
||||||
|
|
||||||
|
When you send a message or attach files to an encrypted room, the client encrypts each part with the
|
||||||
|
room content key **before** uploading:
|
||||||
|
|
||||||
|
- **Message text** → `$RC1$…` ciphertext.
|
||||||
|
- **Files** (any kind) → the whole blob is AES-256-GCM encrypted client-side; the server stores an
|
||||||
|
opaque ciphertext blob.
|
||||||
|
- **Image ASCII previews** → rendered on the client, then room-encrypted. The server never sees the
|
||||||
|
rendered art.
|
||||||
|
|
||||||
|
The server records each attachment's **kind**, **file name**, and **byte size** (of the ciphertext
|
||||||
|
blob) as metadata, and broadcasts the ciphertext to other members, who decrypt locally.
|
||||||
|
|
||||||
|
## Changing the passphrase
|
||||||
|
|
||||||
|
`/passwd <old> <new>` rotates the passphrase. Because only the *wrapping* of the room content key
|
||||||
|
changes — not the RCK itself — **all existing history stays readable**:
|
||||||
|
|
||||||
|
1. The client proves knowledge of the old passphrase (old auth key).
|
||||||
|
2. It unwraps the RCK with the old KEK, then re-wraps it under the new KEK (new salt).
|
||||||
|
3. It uploads the new auth key + salt + re-wrapped key. The content is never re-encrypted.
|
||||||
|
|
||||||
|
## Inspecting a room
|
||||||
|
|
||||||
|
Use `/meta` in any channel to see what the server knows about it, including encrypted rooms:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Room info for #private-room:
|
||||||
|
Room ID 3f2a…-…-…
|
||||||
|
Created 7/16/2026 2:31 PM
|
||||||
|
Messages 128
|
||||||
|
Unique users 4
|
||||||
|
Est. size 42.5 MB
|
||||||
|
Protection end-to-end encrypted
|
||||||
|
```
|
||||||
|
|
||||||
|
`Est. size` is the sum of stored attachment blob sizes plus message text length — an estimate of the
|
||||||
|
room's footprint, computed entirely from metadata the server holds without reading any content.
|
||||||
|
|
||||||
|
## Limitations & security notes
|
||||||
|
|
||||||
|
- **No recovery.** A lost passphrase means unrecoverable history. Keep it safe; there is no reset.
|
||||||
|
- **File names are plaintext.** They stay readable so the file list works — don't put secrets in a
|
||||||
|
file name.
|
||||||
|
- **IRC is disabled for encrypted rooms.** The IRC gateway forwards plaintext and cannot participate
|
||||||
|
in the room's key scheme, so encrypted channels are not bridged to IRC.
|
||||||
|
- **Metadata is visible.** Message counts, timestamps, sender identities, file names, and sizes are
|
||||||
|
intentionally readable so the server can moderate at the metadata level and report `/meta`.
|
||||||
|
- **Endpoint trust.** End-to-end encryption protects content from the server and the network, not
|
||||||
|
from a compromised client device that already holds the passphrase.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Message Encryption](encryption.md) — transport (`$ENC$v1$`) and optional at-rest database
|
||||||
|
encryption, where the server *does* decrypt content for processing. Encrypted rooms are a separate,
|
||||||
|
stronger layer that sits on top.
|
||||||
@@ -118,7 +118,7 @@ If you need to recover old encrypted messages, restore the original key from a b
|
|||||||
### Limitations
|
### Limitations
|
||||||
|
|
||||||
- **TLS-inspecting proxies** — if a corporate proxy terminates TLS with a trusted root CA, it can intercept the key exchange (`GET /api/server/encryption-key`) and read all traffic. A future upgrade to ECDH key exchange would address this.
|
- **TLS-inspecting proxies** — if a corporate proxy terminates TLS with a trusted root CA, it can intercept the key exchange (`GET /api/server/encryption-key`) and read all traffic. A future upgrade to ECDH key exchange would address this.
|
||||||
- **Server has full access** — the server decrypts all messages for processing. This is not end-to-end encryption between users; it's transport encryption between client and server.
|
- **Server has full access** — the server decrypts all messages for processing. This is not end-to-end encryption between users; it's transport encryption between client and server. For true end-to-end encryption where the server cannot read content, use [encrypted rooms](encrypted-rooms.md).
|
||||||
- **IRC clients receive plaintext** — IRC is an open protocol and third-party clients cannot participate in the encryption scheme.
|
- **IRC clients receive plaintext** — IRC is an open protocol and third-party clients cannot participate in the encryption scheme.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|||||||
@@ -1,29 +1,60 @@
|
|||||||
# Getting Started
|
# Getting Started
|
||||||
|
|
||||||
## Prerequisites
|
## Install the Client
|
||||||
|
|
||||||
- [.NET 10 SDK](https://dotnet.microsoft.com/download)
|
### Windows (Chocolatey)
|
||||||
|
|
||||||
Or grab a self-contained binary from [Releases](https://github.com/HueByte/EchoHub/releases) -- no runtime needed.
|
```bash
|
||||||
|
choco install echohub
|
||||||
|
```
|
||||||
|
|
||||||
## Run the Server
|
### Linux / macOS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sSfL https://raw.githubusercontent.com/HueByte/EchoHub/master/scripts/install.sh | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
To install a specific version or to a custom directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sSfL .../install.sh | sh -s -- --version 0.2.14
|
||||||
|
curl -sSfL .../install.sh | sh -s -- --install-dir /opt/echohub
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Download
|
||||||
|
|
||||||
|
Grab a self-contained binary from [Releases](https://github.com/HueByte/EchoHub/releases) -- no runtime needed.
|
||||||
|
|
||||||
|
## Host a Server
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
The quickest way to host a server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [Docker guide](docker.md) for configuration, pre-built images, and more.
|
||||||
|
|
||||||
|
### From Source
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet run --project src/EchoHub.Server
|
dotnet run --project src/EchoHub.Server
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Requires [.NET 10 SDK](https://dotnet.microsoft.com/download).
|
||||||
|
|
||||||
On first run, the server automatically:
|
On first run, the server automatically:
|
||||||
|
|
||||||
1. Creates `appsettings.json` from the example config
|
1. Creates `appsettings.json` from the example config
|
||||||
2. Generates a secure JWT secret
|
2. Generates a secure JWT secret
|
||||||
3. Creates the SQLite database with a `#general` channel
|
3. Creates the SQLite database with a `#general` channel
|
||||||
|
|
||||||
## Run the Client
|
## Usage
|
||||||
|
|
||||||
```bash
|
|
||||||
dotnet run --project src/EchoHub.Client
|
|
||||||
```
|
|
||||||
|
|
||||||
|
After installing the client, run `echohub` (or `dotnet run --project src/EchoHub.Client` from source).
|
||||||
Connect to a server, register an account, and start chatting.
|
Connect to a server, register an account, and start chatting.
|
||||||
|
|
||||||
## Connect via IRC
|
## Connect via IRC
|
||||||
@@ -45,17 +76,17 @@ Then connect with any standard IRC client:
|
|||||||
irssi -c localhost -p 6667 -w <password> -n <username>
|
irssi -c localhost -p 6667 -w <password> -n <username>
|
||||||
```
|
```
|
||||||
|
|
||||||
IRC users must have an existing EchoHub account. Authentication works via `PASS`/`NICK`/`USER` or SASL PLAIN. Messages flow bidirectionally between IRC and TUI clients.
|
Your nick is your EchoHub username and the server password is your account password (`PASS`/`NICK`/`USER` or SASL PLAIN). Connecting with a new username registers the account. Messages flow bidirectionally between IRC and TUI clients.
|
||||||
|
|
||||||
For TLS, set `TlsEnabled: true`, `TlsPort: 6697`, and provide a PKCS#12 certificate path.
|
For TLS, set `TlsEnabled: true`, `TlsPort: 6697`, and provide a PKCS#12 certificate path.
|
||||||
|
|
||||||
See the [Architecture](architecture.md) page for details on how the IRC gateway integrates with the chat service.
|
See the [IRC Gateway guide](irc-gateway.md) for command mapping, attachment rendering, and limitations, or [Architecture](architecture.md) for how the gateway integrates with the chat service.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Server configuration is in `appsettings.json` (auto-generated on first run). See the [example config](https://github.com/HueByte/EchoHub/blob/master/src/EchoHub.Server/appsettings.example.json) for all available options.
|
Server configuration is in `appsettings.json` (auto-generated on first run). You can also use environment variables or command-line arguments to override settings.
|
||||||
|
|
||||||
To list your server on the [public directory](https://echohub.voidcube.cloud/servers), set `Server:PublicServer` to `true` and `Server:PublicHost` to your server's public address.
|
See the [Configuration](configuration.md) guide for the full reference and how it all works.
|
||||||
|
|
||||||
## Build from Source
|
## Build from Source
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# IRC Gateway
|
||||||
|
|
||||||
|
Every EchoHub server can expose a second door: a built-in **IRC gateway** that speaks the
|
||||||
|
classic IRC protocol on port 6667. Any standard IRC client — irssi, WeeChat, HexChat,
|
||||||
|
Halloy — can join the same channels as TUI users, see the same messages, and chat with the
|
||||||
|
same accounts. Under the hood both protocols call the same chat service, so a message sent
|
||||||
|
from IRC appears instantly in the TUI and vice versa (see [Architecture](architecture.md)).
|
||||||
|
|
||||||
|
## Enabling the gateway
|
||||||
|
|
||||||
|
The gateway is off by default. Enable it in `appsettings.json` (or `Irc__Enabled=true` as an
|
||||||
|
environment variable):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Irc": {
|
||||||
|
"Enabled": true,
|
||||||
|
"Port": 6667,
|
||||||
|
"TlsEnabled": false,
|
||||||
|
"TlsPort": 6697,
|
||||||
|
"TlsCertPath": "",
|
||||||
|
"ServerName": "echohub",
|
||||||
|
"Motd": "Welcome to EchoHub IRC Gateway!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The plaintext listener always starts on `Port`. The TLS listener on `TlsPort` starts only
|
||||||
|
when `TlsEnabled` is `true` **and** `TlsCertPath` points to a PKCS#12 (`.pfx`) certificate.
|
||||||
|
See the [configuration reference](configuration.md#irc-gateway) for every option.
|
||||||
|
|
||||||
|
## Connecting & authentication
|
||||||
|
|
||||||
|
Your IRC **nick is your EchoHub username** and your server password is your **account
|
||||||
|
password**. Two flows are supported:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# classic PASS/NICK/USER — most clients call this the "server password"
|
||||||
|
irssi -c chat.example.com -p 6667 -w <password> -n <username>
|
||||||
|
```
|
||||||
|
|
||||||
|
or **SASL PLAIN** (advertised via `CAP LS`), where the SASL username/password are the account
|
||||||
|
credentials.
|
||||||
|
|
||||||
|
A few things worth knowing:
|
||||||
|
|
||||||
|
- **Connecting auto-registers.** If the username doesn't exist yet, the gateway creates the
|
||||||
|
account with that password (usernames: 3–50 chars of `a-z 0-9 _ -`; passwords: 6+ chars).
|
||||||
|
The very first account ever created on a server becomes the **Owner**.
|
||||||
|
- Because of that, a typo'd password for an *existing* account fails with
|
||||||
|
`Username is already taken` — the gateway tried to log in, couldn't, then tried to register
|
||||||
|
the name. If you see that error, re-check your password.
|
||||||
|
- Connecting without a password is rejected: `Password required. Use PASS command or SASL PLAIN.`
|
||||||
|
|
||||||
|
## What maps to what
|
||||||
|
|
||||||
|
| IRC | EchoHub |
|
||||||
|
| --- | --- |
|
||||||
|
| `JOIN #room` | Join a channel (history is replayed on join) |
|
||||||
|
| `JOIN #room <key>` | Join a password-protected (`+k`) channel |
|
||||||
|
| `PART` / `QUIT` | Leave channel / disconnect |
|
||||||
|
| `LIST` | Public channels only (password-protected ones show a `[+k]` hint) |
|
||||||
|
| `TOPIC` | Read or set the channel topic (permission-checked) |
|
||||||
|
| `NAMES` / `WHO` | Online users in the channel |
|
||||||
|
| `WHOIS` | Profile: display name, channels, idle time, away status |
|
||||||
|
| `AWAY [message]` | Sets your EchoHub status to Away / back to Online |
|
||||||
|
| `MODE #room +k <key>` / `-k` | Set / clear the channel password |
|
||||||
|
|
||||||
|
Private (unlisted) channels don't appear in `LIST`, but members who know the exact name can
|
||||||
|
still `JOIN` them. Channels are not auto-created from IRC — create them from the TUI first.
|
||||||
|
|
||||||
|
## How messages look
|
||||||
|
|
||||||
|
- **Attachments** arrive as labeled link lines — `[Image: photo.png] https://…`,
|
||||||
|
`♪ [Audio: song.mp3] https://…`, `[File: report.pdf] https://…` — and image attachments
|
||||||
|
additionally render their **ASCII-art preview** using truecolor ANSI escapes, so a modern
|
||||||
|
terminal IRC client shows actual picture previews.
|
||||||
|
- **Link embeds** are appended as `│`-prefixed text lines.
|
||||||
|
- Long messages are split at word boundaries into IRC-safe lines (~400 bytes each);
|
||||||
|
incoming messages may be up to 2,000 characters like any EchoHub message.
|
||||||
|
- Your own messages aren't echoed back (standard IRC convention).
|
||||||
|
- Moderation actions surface natively: kicks arrive as `KICK`, bans and channel nukes as
|
||||||
|
server `NOTICE`s.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
The gateway bridges what IRC can express — and deliberately refuses what it can't:
|
||||||
|
|
||||||
|
- **No end-to-end encrypted rooms.** Joining an [encrypted room](encrypted-rooms.md) fails
|
||||||
|
with *"Cannot join channel — end-to-end encrypted, use the EchoHub client."* Bridging one
|
||||||
|
would require the server to hold the room key, breaking the zero-knowledge design.
|
||||||
|
- **No private messages.** `PRIVMSG` to a nick is rejected; EchoHub is channel-based.
|
||||||
|
- **Usernames, not display names.** Messages are attributed to the account username;
|
||||||
|
a user's display name is visible via `WHOIS`/`WHO` (realname field).
|
||||||
|
- **No client features.** Uploading attachments, profiles, themes, and reactions to status
|
||||||
|
changes are TUI-client features. Other users' status changes aren't pushed to IRC —
|
||||||
|
discover them with `WHOIS`/`WHO`.
|
||||||
|
|
||||||
|
## How IRC users appear to TUI users
|
||||||
|
|
||||||
|
Users connected *only* through the gateway are tagged `[irc]` in the users panel — a hint
|
||||||
|
that they can't receive encrypted content or use client-side features. Someone connected
|
||||||
|
with both an IRC client and the TUI shows untagged.
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# Messages & Attachments
|
||||||
|
|
||||||
|
An EchoHub message is **text content plus up to 10 attachments**, Discord-style. A plain chat
|
||||||
|
line is just a message with no attachments; a photo dump is one message with several files and
|
||||||
|
an optional caption. This page explains how to attach files, what happens to them on the way to
|
||||||
|
the server, and how other clients receive them.
|
||||||
|
|
||||||
|
## Message basics
|
||||||
|
|
||||||
|
| Limit | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| Max message length | 2,000 characters |
|
||||||
|
| Max newlines per message | 30 (no blank-line runs) |
|
||||||
|
| Max attachments per message | 10 |
|
||||||
|
| Link embeds per message | first 3 URLs |
|
||||||
|
|
||||||
|
Multiline messages are written with `Ctrl+N` for a newline; `Enter` sends. URLs in a message
|
||||||
|
get link embeds (title, description, theme color) fetched by the server.
|
||||||
|
|
||||||
|
## Attaching files
|
||||||
|
|
||||||
|
All of these end up in the same place — the **staging tray** — and are sent together as one
|
||||||
|
message the next time you press `Enter`, with whatever you've typed as the caption:
|
||||||
|
|
||||||
|
- **Paste a copied file** — copy one *or several* files in your file manager and press
|
||||||
|
`Ctrl+V` in the input. All of them are staged at once.
|
||||||
|
- **Paste an image from the clipboard** — copy an image in a browser (right-click → *Copy
|
||||||
|
image*), take a screenshot (`Win+Shift+S`), or copy from an image editor, then `Ctrl+V`.
|
||||||
|
The image is attached directly as a PNG named `image.png` — no saving to disk first.
|
||||||
|
On Linux this uses `wl-paste` or `xclip`; on macOS it requires
|
||||||
|
[`pngpaste`](https://github.com/jcsalterego/pngpaste) (`brew install pngpaste`).
|
||||||
|
- **Drag & drop** — drop a file onto the terminal window; the client recognizes the dropped
|
||||||
|
path and stages the file.
|
||||||
|
- **`/send <filepath>`** — stage a file by path (quote paths containing spaces).
|
||||||
|
|
||||||
|
The input frame's title shows what's currently staged. `/clear` drops all staged attachments
|
||||||
|
without sending. Sending with an empty input is fine — the message is just the attachments.
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌ Message (2 attached: report.pdf, image.png) ──────────────┐
|
||||||
|
│ here's the summary and a screenshot_ │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**URL sends are different:** `/send <https://…>` sends an image URL immediately as its own
|
||||||
|
message — nothing is staged, and it isn't available in end-to-end encrypted rooms (the server
|
||||||
|
would have to fetch the image, which would defeat the encryption).
|
||||||
|
|
||||||
|
## Attachment kinds
|
||||||
|
|
||||||
|
The kind is detected per attachment, not per message:
|
||||||
|
|
||||||
|
| Kind | Detected by | Renders as | Default size limit |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| **Image** | Magic bytes: JPEG, PNG, GIF, WebP | ASCII-art preview in chat | 10 MB |
|
||||||
|
| **Audio** | Extension: `.mp3` `.wav` `.ogg` `.flac` `.aac` `.m4a` `.wma` | Playable row (▶) | 10 MB |
|
||||||
|
| **File** | Everything else | Downloadable row | 100 MB |
|
||||||
|
|
||||||
|
Limits are per file and server-configurable — see the `Uploads` section in the
|
||||||
|
[configuration guide](configuration.md) (`MaxImageSizeMB`, `MaxAudioSizeMB`, `MaxFileSizeMB`,
|
||||||
|
`MaxAttachmentsPerMessage`).
|
||||||
|
|
||||||
|
## Image previews (ASCII art)
|
||||||
|
|
||||||
|
Images are rendered in chat as half-block ASCII art. You pick the rendering size:
|
||||||
|
|
||||||
|
| Flag | Size | Feel |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `-s` / `/size s` | 40 × 40 | compact |
|
||||||
|
| `-m` / `/size m` | 80 × 80 | default |
|
||||||
|
| `-l` / `/size l` | 120 × 120 | detailed |
|
||||||
|
|
||||||
|
`/size` with no argument opens a picker; the choice persists as your default. A one-off
|
||||||
|
`-s|-m|-l` flag on `/send` applies to that message.
|
||||||
|
|
||||||
|
## Receiving attachments
|
||||||
|
|
||||||
|
Right-click a message (or press `F6` to select one with the arrow keys) for actions:
|
||||||
|
|
||||||
|
- **Images** → save to disk
|
||||||
|
- **Audio** → play (in-client playback)
|
||||||
|
- **Files** → download
|
||||||
|
|
||||||
|
Downloads go to your configured download folder — set it with `/downloadpath` (no argument
|
||||||
|
opens a native folder picker, or pass a path directly).
|
||||||
|
|
||||||
|
## Attachments in encrypted rooms
|
||||||
|
|
||||||
|
In an [end-to-end encrypted room](encrypted-rooms.md) every attachment is encrypted
|
||||||
|
client-side **before** upload:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
F[File bytes] -->|AES-256-GCM with room key| B[Ciphertext blob]
|
||||||
|
F -->|if image: render ASCII locally| A[ASCII preview]
|
||||||
|
A -->|room-encrypt| AP["$RC1$… preview"]
|
||||||
|
B --> S[Server stores blob + name + size]
|
||||||
|
AP --> S
|
||||||
|
```
|
||||||
|
|
||||||
|
The server never sees the file contents or the rendered preview — it stores an opaque blob and
|
||||||
|
broadcasts it to members, who decrypt locally. File **names and sizes remain visible** to the
|
||||||
|
server so the file list stays usable; don't put secrets in a file name. Pasted clipboard
|
||||||
|
images go through exactly the same pipeline.
|
||||||
|
|
||||||
|
## Deleting messages with attachments
|
||||||
|
|
||||||
|
Deleting a message also removes its uploaded attachment files from the server. You can always
|
||||||
|
delete your own messages; moderators can delete others' — see [Moderation & Roles](moderation.md).
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# Moderation & Roles
|
||||||
|
|
||||||
|
Every EchoHub server has a four-tier role hierarchy. Moderation is **strictly hierarchical**:
|
||||||
|
acting on another user requires outranking them — equal rank is never enough — and a few
|
||||||
|
invariants protect the server owner from lockouts.
|
||||||
|
|
||||||
|
## Roles
|
||||||
|
|
||||||
|
| Role | Rank | Users panel glyph | How it's granted |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| **Owner** | 3 | ★ | The first account ever registered on the server |
|
||||||
|
| **Admin** | 2 | ♦ | Assigned by the Owner |
|
||||||
|
| **Mod** | 1 | ❀ | Assigned by an Admin or the Owner |
|
||||||
|
| **Member** | 0 | — | Everyone else |
|
||||||
|
|
||||||
|
Assign roles with `/role <user> <admin|mod|member>`. Two rules apply:
|
||||||
|
|
||||||
|
- You can only assign roles **strictly below your own** — an Admin can promote to Mod but
|
||||||
|
cannot create another Admin; only the Owner can.
|
||||||
|
- **Owner is not assignable and not demotable.** There is exactly one Owner (the first
|
||||||
|
account), nobody can be promoted to it, and the Owner's role can't be changed.
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
| Command | Minimum role | Effect |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `/kick <user> [reason]` | Mod | Disconnects the user. Not persistent — they can reconnect immediately. |
|
||||||
|
| `/ban <user> [reason]` | Admin | Persistent: flags the account banned and disconnects it. Banned accounts are rejected at login. |
|
||||||
|
| `/unban <user>` | Admin | Lifts a ban. |
|
||||||
|
| `/mute <user> [minutes]` | Mod | Blocks the user from sending messages or uploading files. Without a duration the mute is **indefinite**; with one it auto-expires (checked every ~15 seconds). |
|
||||||
|
| `/unmute <user>` | Mod | Lifts a mute early. |
|
||||||
|
| `/role <user> <role>` | Admin | Assign a role (see rules above). |
|
||||||
|
| `/nuke` | Mod | Deletes the **entire history of the current channel**, including all attachment files on disk. Channel-wide — no per-user check. |
|
||||||
|
|
||||||
|
Kick, ban, and mute all enforce the hierarchy: the target's role must be **strictly lower**
|
||||||
|
than yours. A Mod cannot kick another Mod; nobody can kick, ban, mute, or demote the Owner.
|
||||||
|
|
||||||
|
## Deleting messages
|
||||||
|
|
||||||
|
Deletion has its own, slightly different rule set:
|
||||||
|
|
||||||
|
- **Your own messages** — always deletable, whatever your role. Right-click a message →
|
||||||
|
*Delete message*, or press `F6`, pick the message, and hit `Delete`.
|
||||||
|
- **Someone else's messages** — requires **Mod or higher** *and* strictly outranking the
|
||||||
|
author. A Mod can delete a Member's message, but not another Mod's.
|
||||||
|
|
||||||
|
Deleting a message also purges its uploaded attachment blobs from the server's disk, and the
|
||||||
|
removal is broadcast live — the message disappears from everyone's chat immediately.
|
||||||
|
|
||||||
|
## How actions surface
|
||||||
|
|
||||||
|
Everyone in the channel sees moderation happen:
|
||||||
|
|
||||||
|
- **TUI clients** show system messages — *"alice was kicked (reason)"*, *"bob was banned"*,
|
||||||
|
*"Channel history has been cleared by a moderator."* The kicked or banned user themselves
|
||||||
|
gets a dialog with the reason, then the client disconnects.
|
||||||
|
- **IRC clients** get native protocol events: kicks arrive as a real `KICK` command, bans as
|
||||||
|
a server `NOTICE`. (See the [IRC Gateway guide](irc-gateway.md).)
|
||||||
|
|
||||||
|
Muted users aren't announced; they simply receive *"You are muted and cannot send messages."*
|
||||||
|
when they try to speak.
|
||||||
|
|
||||||
|
## Design notes
|
||||||
|
|
||||||
|
- All checks run server-side in the moderation API — the client commands are conveniences,
|
||||||
|
and the same rules bind IRC users and any direct API caller.
|
||||||
|
- Bans are account-level, not IP-level. A banned person can register a fresh account; pair
|
||||||
|
bans with registration hygiene on public servers.
|
||||||
|
- In [end-to-end encrypted rooms](encrypted-rooms.md) moderation still works at the metadata
|
||||||
|
level — messages can be deleted and users muted/kicked by identity — but no moderator can
|
||||||
|
*read* the content, including the Owner.
|
||||||
@@ -1,8 +1,26 @@
|
|||||||
|
- name: Guides
|
||||||
|
items:
|
||||||
- name: Getting Started
|
- name: Getting Started
|
||||||
href: getting-started.md
|
href: getting-started.md
|
||||||
|
- name: TUI Guide
|
||||||
|
href: tui-guide.md
|
||||||
|
- name: Messages & Attachments
|
||||||
|
href: messages-and-attachments.md
|
||||||
|
- name: Moderation & Roles
|
||||||
|
href: moderation.md
|
||||||
|
- name: IRC Gateway
|
||||||
|
href: irc-gateway.md
|
||||||
|
- name: Docker
|
||||||
|
href: docker.md
|
||||||
- name: Architecture
|
- name: Architecture
|
||||||
href: architecture.md
|
href: architecture.md
|
||||||
|
- name: Configuration
|
||||||
|
href: configuration.md
|
||||||
- name: Encryption
|
- name: Encryption
|
||||||
href: encryption.md
|
href: encryption.md
|
||||||
|
- name: Encrypted Rooms
|
||||||
|
href: encrypted-rooms.md
|
||||||
- name: Notification Sounds
|
- name: Notification Sounds
|
||||||
href: notification-sounds.md
|
href: notification-sounds.md
|
||||||
|
- name: Flows
|
||||||
|
href: ../flows/toc.yml
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# TUI Guide
|
||||||
|
|
||||||
|
Everything you can do in the EchoHub terminal client: keyboard shortcuts, mouse actions,
|
||||||
|
slash commands, themes, and the everyday behaviors (unread markers, auto-join, scrollback)
|
||||||
|
that make it feel like a proper IRC-era client with modern comforts.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌ Menu bar ──────────────────────────────────────────────────┐
|
||||||
|
│ ┌ Channels ─┐ ┌ Messages ────────────────────┐ ┌ Users ──┐ │
|
||||||
|
│ │ #general 3│ │ 12:01 <alice> hi │ │ ★ alice │ │
|
||||||
|
│ │ #dev │ │ ── new messages ── │ │ ❀ bob │ │
|
||||||
|
│ │ #random*~ │ │ 12:04 <bob> anyone around? │ │ carol │ │
|
||||||
|
│ └───────────┘ └──────────────────────────────┘ │ d [irc] │ │
|
||||||
|
│ ┌ Message │ Enter=send │ Tab=complete │ … ────┐ └─────────┘ │
|
||||||
|
│ │ _ │ │
|
||||||
|
│ └─────────────────────────────────────────────┘ │
|
||||||
|
│ Status: Connected │ v0.2.14 │ alice │ Act: #dev │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Channel list markers: `*` = password-protected, `~` = private (unlisted), plus unread counts
|
||||||
|
(orange when you were @mentioned). Users panel glyphs: `★` Owner, `♦` Admin, `❀` Mod,
|
||||||
|
`[irc]` for IRC-gateway-only users; status icons `●`/`○`/`◐`/`◌` for online/offline/away/dnd.
|
||||||
|
|
||||||
|
## Keyboard shortcuts
|
||||||
|
|
||||||
|
### In the message input
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
| --- | --- |
|
||||||
|
| `Enter` | Send the message (also sends staged attachments with the text as caption) |
|
||||||
|
| `Ctrl+N` | Insert a newline (multiline message) |
|
||||||
|
| `Tab` | Autocomplete a slash command (`/th` → `/theme`) |
|
||||||
|
| `Ctrl+V` (or `Ctrl+Y`) | Paste — copied files and images become attachments, text pastes normally ([details](messages-and-attachments.md)) |
|
||||||
|
| `Ctrl+C` / `Ctrl+X` | Copy / cut in the input |
|
||||||
|
| `Ctrl+W` | Delete the word left of the cursor |
|
||||||
|
| `Ctrl+K` | Open the search palette |
|
||||||
|
| `F6` | Move focus into the message list |
|
||||||
|
|
||||||
|
### In the message list (after `F6`)
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
| --- | --- |
|
||||||
|
| `↑` / `↓` | Select a message |
|
||||||
|
| `Enter` | Activate: play/download/save an attachment, open an `@mention`'s profile, join a `#channel`, or open the sender's profile |
|
||||||
|
| `Delete` / `Backspace` | Delete the selected message (with confirmation; [permission rules](moderation.md)) |
|
||||||
|
| `F6` | Return focus to the input |
|
||||||
|
|
||||||
|
### Anywhere
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
| --- | --- |
|
||||||
|
| `F2` | Toggle the users panel |
|
||||||
|
| `Ctrl+K` | Search palette |
|
||||||
|
| `Alt+Q` | Quit |
|
||||||
|
|
||||||
|
## The search palette (`Ctrl+K`)
|
||||||
|
|
||||||
|
A command-palette that searches **channels and app actions** — type to filter, `↓` to
|
||||||
|
navigate, `Enter` to jump. Actions include Connect, Disconnect, Logout, My Profile,
|
||||||
|
Set Status, Create/Delete Channel, Saved Servers, Toggle Users Panel, Check for Updates,
|
||||||
|
and Quit. `Ctrl+K` again closes it.
|
||||||
|
|
||||||
|
## Mouse
|
||||||
|
|
||||||
|
- **Right-click a message** for the context menu: save image / play audio / download file
|
||||||
|
(depending on the attachment), *Mention @user*, *View profile*, *Copy text*,
|
||||||
|
*Copy message ID*, *Delete message*.
|
||||||
|
- **Left-click a message** does the most useful thing for that line: attachments
|
||||||
|
play/download/save, `@mentions` and the sender open profiles, `#channel` references join
|
||||||
|
that channel.
|
||||||
|
- **Click a user** in the users panel to open their profile; **click a channel** to switch.
|
||||||
|
|
||||||
|
## Slash commands
|
||||||
|
|
||||||
|
Type `/help` in any channel for the full list. The highlights:
|
||||||
|
|
||||||
|
| Command | What it does |
|
||||||
|
| --- | --- |
|
||||||
|
| `/status <online\|away\|dnd\|invisible>` or `/status <message>` | Presence / status message |
|
||||||
|
| `/nick <name>`, `/color <#hex>`, `/avatar <url or path>` | Display name, nick color, avatar |
|
||||||
|
| `/theme <name>` | Switch theme |
|
||||||
|
| `/send`, `/clear`, `/size`, `/downloadpath` | Attachments — see [Messages & Attachments](messages-and-attachments.md) |
|
||||||
|
| `/join <channel> [password]`, `/leave`, `/topic <text>` | Channel membership and topic |
|
||||||
|
| `/passwd <old> <new>` | Rotate an encrypted room's passphrase |
|
||||||
|
| `/profile [user]`, `/users`, `/meta` | Profiles, online users, room info |
|
||||||
|
| `/kick`, `/ban`, `/mute`, `/role`, `/nuke`, … | [Moderation](moderation.md) |
|
||||||
|
| `/servers`, `/quit` | Saved servers, exit |
|
||||||
|
|
||||||
|
Emoji shortcodes (`:smile:` style) are replaced live as you type.
|
||||||
|
|
||||||
|
## Themes
|
||||||
|
|
||||||
|
14 built-in themes: **Default, Transparent, TransparentLight, Classic, Light, Hacker,
|
||||||
|
Solarized, Dracula, Monokai, Nord, Gruvbox, Ocean, HighContrast, RosePine** — switch from
|
||||||
|
the User menu or `/theme <name>`. The two *Transparent* themes use no background color at
|
||||||
|
all, so your terminal's own background (and any blur/acrylic) shows through.
|
||||||
|
|
||||||
|
You can add your own: drop a theme JSON into `~/.echohub/themes/` and it appears in the list
|
||||||
|
(names that collide with a built-in are skipped).
|
||||||
|
|
||||||
|
## Everyday behaviors
|
||||||
|
|
||||||
|
- **Unread markers** — a `── new messages ──` rule marks where you left off in each channel,
|
||||||
|
irssi-style. Read positions are **persisted per server**, so the marker survives
|
||||||
|
reconnects and restarts. The status bar's `Act:` segment lists channels with activity
|
||||||
|
(orange when you were @mentioned), and day boundaries draw a date rule.
|
||||||
|
- **Auto-join** — connecting joins `#general` plus every channel you're a member of, so
|
||||||
|
unread counts and mentions accumulate everywhere. Channels you `/leave` stay left, and
|
||||||
|
password-protected or [encrypted rooms](encrypted-rooms.md) are never auto-prompted —
|
||||||
|
join those explicitly. `#general` is the home channel and can't be left or deleted.
|
||||||
|
- **Scrollback** — history loads 100 messages at a time; scrolling to the top of a channel
|
||||||
|
fetches the next page and keeps your position (no jump).
|
||||||
|
- **Drag & drop** — dropping a file onto the window stages it as an attachment.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Agent Context Pack — HueByte/EchoHub
|
||||||
|
|
||||||
|
Machine-oriented context for AI coding agents, generated deterministically from
|
||||||
|
the repository's symbol graph (no LLM involved). Reading this pack instead of
|
||||||
|
scanning the source tree saves tokens: one map read plus targeted doc lookups
|
||||||
|
replaces repeated file reads and greps.
|
||||||
|
|
||||||
|
| File | What it is | When to read it |
|
||||||
|
|---|---|---|
|
||||||
|
| [repo-map](repo-map.md) | Subsystem map + most-connected symbols | First — orientation |
|
||||||
|
| [symbol-graph.json](symbol-graph.json) | Full queryable symbol graph (kind, file, subsystem, complexity, dependencies, doc path) | To trace exact dependencies or find a symbol |
|
||||||
|
| `../Code/**` | Per-file generated documentation | For the behaviour of a specific file |
|
||||||
|
| `../Synthesis/Architecture.md` | Cross-cutting architecture narrative + diagram | For the big picture |
|
||||||
|
|
||||||
|
Suggested workflow: read `repo-map.md`; locate the symbols relevant to your task in
|
||||||
|
`symbol-graph.json` (each carries a `doc` path and `dependsOn` edges); open only
|
||||||
|
those docs/sources. The pack is regenerated on every full documentation run, so it
|
||||||
|
matches the commit recorded in the graph's `commit` field.
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
# Repo Map — HueByte/EchoHub
|
||||||
|
|
||||||
|
> Deterministic structural map generated for AI agents. Read this file for
|
||||||
|
> orientation, query [`symbol-graph.json`](symbol-graph.json) for exact
|
||||||
|
> dependencies, and open the linked per-file docs for behaviour — instead of
|
||||||
|
> scanning the source tree.
|
||||||
|
|
||||||
|
Commit `40aea9a04b2b4bd3a2e431cdd1cf4e1bfa11c343` · 201 symbols · 128 files · 655 dependency edges
|
||||||
|
|
||||||
|
## Subsystems
|
||||||
|
|
||||||
|
*Structural clusters detected from the dependency graph — groups of symbols more densely wired to each other than to the rest of the codebase.*
|
||||||
|
|
||||||
|
### src/EchoHub.Client/Services · AppOrchestrator
|
||||||
|
|
||||||
|
37 symbols across 23 files. Key symbols (by connectivity):
|
||||||
|
|
||||||
|
- [`AppOrchestrator`](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) (class) — `src/EchoHub.Client/AppOrchestrator.cs`
|
||||||
|
- [`ConnectionManager`](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md) (class) — `src/EchoHub.Client/Services/ConnectionManager.cs`
|
||||||
|
- [`MessageDto`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
- [`EchoHubConnection`](../Code/src/EchoHub.Client/Services/EchoHubConnection.cs.md) (class) — `src/EchoHub.Client/Services/EchoHubConnection.cs`
|
||||||
|
- [`RefreshToken`](../Code/src/EchoHub.Core/Models/RefreshToken.cs.md) (class) — `src/EchoHub.Core/Models/RefreshToken.cs`
|
||||||
|
- [`SavedServer`](../Code/src/EchoHub.Client/Config/ClientConfig.cs.md) (class) — `src/EchoHub.Client/Config/ClientConfig.cs`
|
||||||
|
- [`RoomKeyStore`](../Code/src/EchoHub.Client/Services/RoomKeyStore.cs.md) (class) — `src/EchoHub.Client/Services/RoomKeyStore.cs`
|
||||||
|
- [`IrcMessageFormatter`](../Code/src/EchoHub.Server.Irc/IrcMessageFormatter.cs.md) (class) — `src/EchoHub.Server.Irc/IrcMessageFormatter.cs`
|
||||||
|
- *…and 29 more (see symbol-graph.json)*
|
||||||
|
|
||||||
|
### src/EchoHub.Client/UI · MainWindow
|
||||||
|
|
||||||
|
22 symbols across 19 files. Key symbols (by connectivity):
|
||||||
|
|
||||||
|
- [`MainWindow`](../Code/src/EchoHub.Client/UI/MainWindow.cs.md) (class) — `src/EchoHub.Client/UI/MainWindow.cs`
|
||||||
|
- [`ChatMessageManager`](../Code/src/EchoHub.Client/UI/Chat/ChatMessageManager.cs.md) (class) — `src/EchoHub.Client/UI/Chat/ChatMessageManager.cs`
|
||||||
|
- [`ChatLine`](../Code/src/EchoHub.Client/UI/Chat/ChatLine.cs.md) (class) — `src/EchoHub.Client/UI/Chat/ChatLine.cs`
|
||||||
|
- [`ProfileViewDialog`](../Code/src/EchoHub.Client/UI/Dialogs/ProfileViewDialog.cs.md) (class) — `src/EchoHub.Client/UI/Dialogs/ProfileViewDialog.cs`
|
||||||
|
- [`AttachmentKind`](../Code/src/EchoHub.Core/Models/AttachmentKind.cs.md) (enum) — `src/EchoHub.Core/Models/AttachmentKind.cs`
|
||||||
|
- [`AttachmentDto`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
- [`ChatColors`](../Code/src/EchoHub.Client/UI/Chat/ChatColors.cs.md) (class) — `src/EchoHub.Client/UI/Chat/ChatColors.cs`
|
||||||
|
- [`ChatSegment`](../Code/src/EchoHub.Client/UI/Chat/ChatSegment.cs.md) (record) — `src/EchoHub.Client/UI/Chat/ChatSegment.cs`
|
||||||
|
- *…and 14 more (see symbol-graph.json)*
|
||||||
|
|
||||||
|
### src/EchoHub.Core/DTOs · ApiClient
|
||||||
|
|
||||||
|
19 symbols across 11 files. Key symbols (by connectivity):
|
||||||
|
|
||||||
|
- [`ApiClient`](../Code/src/EchoHub.Client/Services/ApiClient.cs.md) (class) — `src/EchoHub.Client/Services/ApiClient.cs`
|
||||||
|
- [`ModerationController`](../Code/src/EchoHub.Server/Controllers/ModerationController.cs.md) (class) — `src/EchoHub.Server/Controllers/ModerationController.cs`
|
||||||
|
- [`ServerStatsCollector`](../Code/src/EchoHub.Server/Services/Stats/ServerStatsCollector.cs.md) (class) — `src/EchoHub.Server/Services/Stats/ServerStatsCollector.cs`
|
||||||
|
- [`RefreshRequest`](../Code/src/EchoHub.Core/DTOs/AuthDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/AuthDtos.cs`
|
||||||
|
- [`AssignRoleRequest`](../Code/src/EchoHub.Core/DTOs/ModerationDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ModerationDtos.cs`
|
||||||
|
- [`UpdateProfileRequest`](../Code/src/EchoHub.Core/DTOs/ProfileDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ProfileDtos.cs`
|
||||||
|
- [`AvatarHelper`](../Code/src/EchoHub.Client/Services/AvatarHelper.cs.md) (class) — `src/EchoHub.Client/Services/AvatarHelper.cs`
|
||||||
|
- [`OutgoingAttachment`](../Code/src/EchoHub.Client/Services/OutgoingAttachment.cs.md) (record) — `src/EchoHub.Client/Services/OutgoingAttachment.cs`
|
||||||
|
- *…and 11 more (see symbol-graph.json)*
|
||||||
|
|
||||||
|
### src/EchoHub.Server · User
|
||||||
|
|
||||||
|
19 symbols across 17 files. Key symbols (by connectivity):
|
||||||
|
|
||||||
|
- [`User`](../Code/src/EchoHub.Core/Models/User.cs.md) (class) — `src/EchoHub.Core/Models/User.cs`
|
||||||
|
- [`UsersController`](../Code/src/EchoHub.Server/Controllers/UsersController.cs.md) (class) — `src/EchoHub.Server/Controllers/UsersController.cs`
|
||||||
|
- [`EchoHubDbContext`](../Code/src/EchoHub.Server/Data/EchoHubDbContext.cs.md) (class) — `src/EchoHub.Server/Data/EchoHubDbContext.cs`
|
||||||
|
- [`ServerRole`](../Code/src/EchoHub.Core/Models/ServerRole.cs.md) (enum) — `src/EchoHub.Core/Models/ServerRole.cs`
|
||||||
|
- [`AuthController`](../Code/src/EchoHub.Server/Controllers/AuthController.cs.md) (class) — `src/EchoHub.Server/Controllers/AuthController.cs`
|
||||||
|
- [`UserProfileDto`](../Code/src/EchoHub.Core/DTOs/ProfileDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ProfileDtos.cs`
|
||||||
|
- [`UserService`](../Code/src/EchoHub.Server/Services/UserService.cs.md) (class) — `src/EchoHub.Server/Services/UserService.cs`
|
||||||
|
- [`IUserService`](../Code/src/EchoHub.Core/Contracts/IUserService.cs.md) (interface) — `src/EchoHub.Core/Contracts/IUserService.cs`
|
||||||
|
- *…and 11 more (see symbol-graph.json)*
|
||||||
|
|
||||||
|
### src/EchoHub.Core/DTOs · ChannelService
|
||||||
|
|
||||||
|
13 symbols across 9 files. Key symbols (by connectivity):
|
||||||
|
|
||||||
|
- [`ChannelService`](../Code/src/EchoHub.Server/Services/ChannelService.cs.md) (class) — `src/EchoHub.Server/Services/ChannelService.cs`
|
||||||
|
- [`IrcCommandHandler`](../Code/src/EchoHub.Server.Irc/IrcCommandHandler.cs.md) (class) — `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
|
||||||
|
- [`IChannelService`](../Code/src/EchoHub.Core/Contracts/IChannelService.cs.md) (interface) — `src/EchoHub.Core/Contracts/IChannelService.cs`
|
||||||
|
- [`ChannelOperationResult`](../Code/src/EchoHub.Core/DTOs/CommonDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/CommonDtos.cs`
|
||||||
|
- [`FakeChannelService`](../Code/src/EchoHub.Tests/Irc/TestHelpers.cs.md) (class) — `src/EchoHub.Tests/Irc/TestHelpers.cs`
|
||||||
|
- [`ValidationConstants`](../Code/src/EchoHub.Core/Constants/ValidationConstants.cs.md) (class) — `src/EchoHub.Core/Constants/ValidationConstants.cs`
|
||||||
|
- [`ChannelCryptoDto`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
- [`ChannelError`](../Code/src/EchoHub.Core/DTOs/CommonDtos.cs.md) (enum) — `src/EchoHub.Core/DTOs/CommonDtos.cs`
|
||||||
|
- *…and 5 more (see symbol-graph.json)*
|
||||||
|
|
||||||
|
### src/EchoHub.Server · Program
|
||||||
|
|
||||||
|
13 symbols across 12 files. Key symbols (by connectivity):
|
||||||
|
|
||||||
|
- [`Program`](../Code/src/EchoHub.Server/Program.cs.md) (file) — `src/EchoHub.Server/Program.cs`
|
||||||
|
- [`HubConstants`](../Code/src/EchoHub.Core/Constants/HubConstants.cs.md) (class) — `src/EchoHub.Core/Constants/HubConstants.cs`
|
||||||
|
- [`PresenceTracker`](../Code/src/EchoHub.Server/Services/PresenceTracker.cs.md) (class) — `src/EchoHub.Server/Services/PresenceTracker.cs`
|
||||||
|
- [`ImageToAsciiService`](../Code/src/EchoHub.Core/Services/ImageToAsciiService.cs.md) (class) — `src/EchoHub.Core/Services/ImageToAsciiService.cs`
|
||||||
|
- [`UploadLimits`](../Code/src/EchoHub.Server/Config/UploadLimits.cs.md) (class) — `src/EchoHub.Server/Config/UploadLimits.cs`
|
||||||
|
- [`DatabaseSetup`](../Code/src/EchoHub.Server/Setup/DatabaseSetup.cs.md) (class) — `src/EchoHub.Server/Setup/DatabaseSetup.cs`
|
||||||
|
- [`DirectoryClaimStore`](../Code/src/EchoHub.Server/Services/DirectoryClaimStore.cs.md) (class) — `src/EchoHub.Server/Services/DirectoryClaimStore.cs`
|
||||||
|
- [`LinkEmbedService`](../Code/src/EchoHub.Server/Services/LinkEmbedService.cs.md) (class) — `src/EchoHub.Server/Services/LinkEmbedService.cs`
|
||||||
|
- *…and 5 more (see symbol-graph.json)*
|
||||||
|
|
||||||
|
### src/EchoHub.Core/Contracts · ChannelDto
|
||||||
|
|
||||||
|
9 symbols across 9 files. Key symbols (by connectivity):
|
||||||
|
|
||||||
|
- [`ChannelDto`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
- [`UserPresenceDto`](../Code/src/EchoHub.Core/DTOs/ProfileDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ProfileDtos.cs`
|
||||||
|
- [`ChatHub`](../Code/src/EchoHub.Server/Hubs/ChatHub.cs.md) (class) — `src/EchoHub.Server/Hubs/ChatHub.cs`
|
||||||
|
- [`IChatService`](../Code/src/EchoHub.Core/Contracts/IChatService.cs.md) (interface) — `src/EchoHub.Core/Contracts/IChatService.cs`
|
||||||
|
- [`IChatBroadcaster`](../Code/src/EchoHub.Core/Contracts/IChatBroadcaster.cs.md) (interface) — `src/EchoHub.Core/Contracts/IChatBroadcaster.cs`
|
||||||
|
- [`IrcBroadcaster`](../Code/src/EchoHub.Server.Irc/IrcBroadcaster.cs.md) (class) — `src/EchoHub.Server.Irc/IrcBroadcaster.cs`
|
||||||
|
- [`SignalRBroadcaster`](../Code/src/EchoHub.Server/Services/SignalRBroadcaster.cs.md) (class) — `src/EchoHub.Server/Services/SignalRBroadcaster.cs`
|
||||||
|
- [`IEchoHubClient`](../Code/src/EchoHub.Core/Contracts/IEchoHubClient.cs.md) (interface) — `src/EchoHub.Core/Contracts/IEchoHubClient.cs`
|
||||||
|
- *…and 1 more (see symbol-graph.json)*
|
||||||
|
|
||||||
|
### src/EchoHub.Server · ServerDirectoryService
|
||||||
|
|
||||||
|
7 symbols across 1 files. Key symbols (by connectivity):
|
||||||
|
|
||||||
|
- [`ServerDirectoryService`](../Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md) (class) — `src/EchoHub.Server/Services/ServerDirectoryService.cs`
|
||||||
|
- [`ErrorDetail`](../Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md) (record) — `src/EchoHub.Server/Services/ServerDirectoryService.cs`
|
||||||
|
- [`Response`](../Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md) (record) — `src/EchoHub.Server/Services/ServerDirectoryService.cs`
|
||||||
|
- [`DirectoryProtocol`](../Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md) (class) — `src/EchoHub.Server/Services/ServerDirectoryService.cs`
|
||||||
|
- [`DirectoryRegistrationErrors`](../Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md) (class) — `src/EchoHub.Server/Services/ServerDirectoryService.cs`
|
||||||
|
- [`RegisterServerDto`](../Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md) (record) — `src/EchoHub.Server/Services/ServerDirectoryService.cs`
|
||||||
|
- [`RegisterServerResult`](../Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md) (record) — `src/EchoHub.Server/Services/ServerDirectoryService.cs`
|
||||||
|
|
||||||
|
### src/EchoHub.Client/UI · Channel
|
||||||
|
|
||||||
|
6 symbols across 4 files. Key symbols (by connectivity):
|
||||||
|
|
||||||
|
- [`Channel`](../Code/src/EchoHub.Core/Models/Channel.cs.md) (class) — `src/EchoHub.Core/Models/Channel.cs`
|
||||||
|
- [`SearchDialog`](../Code/src/EchoHub.Client/UI/Dialogs/SearchDialog.cs.md) (class) — `src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`
|
||||||
|
- [`SearchResultType`](../Code/src/EchoHub.Client/UI/Dialogs/SearchDialog.cs.md) (enum) — `src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`
|
||||||
|
- [`SearchListSource`](../Code/src/EchoHub.Client/UI/ListSources/SearchListSource.cs.md) (class) — `src/EchoHub.Client/UI/ListSources/SearchListSource.cs`
|
||||||
|
- [`SearchResult`](../Code/src/EchoHub.Client/UI/Dialogs/SearchDialog.cs.md) (record) — `src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`
|
||||||
|
- [`IrcNumericReply`](../Code/src/EchoHub.Server.Irc/IrcNumericReply.cs.md) (class) — `src/EchoHub.Server.Irc/IrcNumericReply.cs`
|
||||||
|
|
||||||
|
### src/EchoHub.Client/UI · UserStatus
|
||||||
|
|
||||||
|
6 symbols across 5 files. Key symbols (by connectivity):
|
||||||
|
|
||||||
|
- [`UserStatus`](../Code/src/EchoHub.Core/Models/UserStatus.cs.md) (enum) — `src/EchoHub.Core/Models/UserStatus.cs`
|
||||||
|
- [`StatusDialog`](../Code/src/EchoHub.Client/UI/Dialogs/StatusDialog.cs.md) (class) — `src/EchoHub.Client/UI/Dialogs/StatusDialog.cs`
|
||||||
|
- [`UserSession`](../Code/src/EchoHub.Client/Services/UserSession.cs.md) (class) — `src/EchoHub.Client/Services/UserSession.cs`
|
||||||
|
- [`StatusDialogResult`](../Code/src/EchoHub.Client/UI/Dialogs/StatusDialog.cs.md) (record) — `src/EchoHub.Client/UI/Dialogs/StatusDialog.cs`
|
||||||
|
- [`UserDto`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
- [`UpdateStatusRequest`](../Code/src/EchoHub.Core/DTOs/ProfileDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ProfileDtos.cs`
|
||||||
|
|
||||||
|
### src/EchoHub.Core/DTOs · ChannelsController
|
||||||
|
|
||||||
|
6 symbols across 3 files. Key symbols (by connectivity):
|
||||||
|
|
||||||
|
- [`ChannelsController`](../Code/src/EchoHub.Server/Controllers/ChannelsController.cs.md) (class) — `src/EchoHub.Server/Controllers/ChannelsController.cs`
|
||||||
|
- [`RekeyChannelRequest`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
- [`FileValidationHelper`](../Code/src/EchoHub.Core/Services/FileValidationHelper.cs.md) (class) — `src/EchoHub.Core/Services/FileValidationHelper.cs`
|
||||||
|
- [`CreateChannelRequest`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
- [`SendUrlRequest`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
- [`UpdateTopicRequest`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
|
||||||
|
### src/EchoHub.Core/Models
|
||||||
|
|
||||||
|
6 symbols across 6 files. Key symbols (by connectivity):
|
||||||
|
|
||||||
|
- [`Message`](../Code/src/EchoHub.Core/Models/Message.cs.md) (class) — `src/EchoHub.Core/Models/Message.cs`
|
||||||
|
- [`DataMigrationService`](../Code/src/EchoHub.Server/Setup/DataMigrationService.cs.md) (class) — `src/EchoHub.Server/Setup/DataMigrationService.cs`
|
||||||
|
- [`Attachment`](../Code/src/EchoHub.Core/Models/Attachment.cs.md) (class) — `src/EchoHub.Core/Models/Attachment.cs`
|
||||||
|
- [`AsyncRunner`](../Code/src/EchoHub.Client/Services/AsyncRunner.cs.md) (class) — `src/EchoHub.Client/Services/AsyncRunner.cs`
|
||||||
|
- [`MessageType`](../Code/src/EchoHub.Core/Models/MessageType.cs.md) (enum) — `src/EchoHub.Core/Models/MessageType.cs`
|
||||||
|
- [`ApiResponse`](../Code/src/EchoHub.Core/DTOs/CommonDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/CommonDtos.cs`
|
||||||
|
|
||||||
|
*…and 11 smaller subsystems (see symbol-graph.json).*
|
||||||
|
|
||||||
|
## Most connected symbols
|
||||||
|
|
||||||
|
The load-bearing symbols — changes here have the widest blast radius.
|
||||||
|
|
||||||
|
| Symbol | Kind | Used by | Uses | File |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| [`AppOrchestrator`](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) | class | 3 | 54 | `src/EchoHub.Client/AppOrchestrator.cs` |
|
||||||
|
| [`Program`](../Code/src/EchoHub.Server/Program.cs.md) | file | 0 | 34 | `src/EchoHub.Server/Program.cs` |
|
||||||
|
| [`ApiClient`](../Code/src/EchoHub.Client/Services/ApiClient.cs.md) | class | 3 | 29 | `src/EchoHub.Client/Services/ApiClient.cs` |
|
||||||
|
| [`ChatService`](../Code/src/EchoHub.Server/Services/ChatService.cs.md) | class | 2 | 29 | `src/EchoHub.Server/Services/ChatService.cs` |
|
||||||
|
| [`Channel`](../Code/src/EchoHub.Core/Models/Channel.cs.md) | class | 29 | 1 | `src/EchoHub.Core/Models/Channel.cs` |
|
||||||
|
| [`MainWindow`](../Code/src/EchoHub.Client/UI/MainWindow.cs.md) | class | 2 | 25 | `src/EchoHub.Client/UI/MainWindow.cs` |
|
||||||
|
| [`Message`](../Code/src/EchoHub.Core/Models/Message.cs.md) | class | 21 | 4 | `src/EchoHub.Core/Models/Message.cs` |
|
||||||
|
| [`ChannelsController`](../Code/src/EchoHub.Server/Controllers/ChannelsController.cs.md) | class | 0 | 25 | `src/EchoHub.Server/Controllers/ChannelsController.cs` |
|
||||||
|
| [`ConnectionManager`](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md) | class | 1 | 22 | `src/EchoHub.Client/Services/ConnectionManager.cs` |
|
||||||
|
| [`MessageDto`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) | record | 20 | 3 | `src/EchoHub.Core/DTOs/ChatDtos.cs` |
|
||||||
|
|
||||||
|
*Regenerated on every full documentation run; see [README](README.md) for how to use this pack.*
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,229 @@
|
|||||||
|
# HueByte/EchoHub — Documentation
|
||||||
|
|
||||||
|
*Generated from branch `master`*
|
||||||
|
*Commit: `40aea9a0`*
|
||||||
|
|
||||||
|
## Languages
|
||||||
|
|
||||||
|
- **C#**: 128 files
|
||||||
|
|
||||||
|
## Statistics
|
||||||
|
|
||||||
|
- **Files processed:** 128
|
||||||
|
- **Lines of code:** 22,133
|
||||||
|
- **Symbols documented:** 617
|
||||||
|
- **Validation retries:** 69
|
||||||
|
|
||||||
|
## Documentation Index
|
||||||
|
|
||||||
|
### src/EchoHub.Client
|
||||||
|
|
||||||
|
- [AppOrchestrator](src/EchoHub.Client/AppOrchestrator.cs.md)
|
||||||
|
- [Program](src/EchoHub.Client/Program.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Client/Commands
|
||||||
|
|
||||||
|
- [CommandHandler](src/EchoHub.Client/Commands/CommandHandler.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Client/Config
|
||||||
|
|
||||||
|
- [ClientConfig](src/EchoHub.Client/Config/ClientConfig.cs.md)
|
||||||
|
- [ConfigManager](src/EchoHub.Client/Config/ConfigManager.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Client/Services
|
||||||
|
|
||||||
|
- [ApiClient](src/EchoHub.Client/Services/ApiClient.cs.md)
|
||||||
|
- [AsyncRunner](src/EchoHub.Client/Services/AsyncRunner.cs.md)
|
||||||
|
- [AudioPlaybackService](src/EchoHub.Client/Services/AudioPlaybackService.cs.md)
|
||||||
|
- [AvatarHelper](src/EchoHub.Client/Services/AvatarHelper.cs.md)
|
||||||
|
- [ClientEncryptionService](src/EchoHub.Client/Services/ClientEncryptionService.cs.md)
|
||||||
|
- [ClipboardFiles](src/EchoHub.Client/Services/ClipboardFiles.cs.md)
|
||||||
|
- [ClipboardImage](src/EchoHub.Client/Services/ClipboardImage.cs.md)
|
||||||
|
- [ConnectionManager](src/EchoHub.Client/Services/ConnectionManager.cs.md)
|
||||||
|
- [EchoHubConnection](src/EchoHub.Client/Services/EchoHubConnection.cs.md)
|
||||||
|
- [NativeFolderPicker](src/EchoHub.Client/Services/NativeFolderPicker.cs.md)
|
||||||
|
- [NotificationSoundService](src/EchoHub.Client/Services/NotificationSoundService.cs.md)
|
||||||
|
- [OutgoingAttachment](src/EchoHub.Client/Services/OutgoingAttachment.cs.md)
|
||||||
|
- [PathSetup](src/EchoHub.Client/Services/PathSetup.cs.md)
|
||||||
|
- [RoomKeyProtector](src/EchoHub.Client/Services/RoomKeyProtector.cs.md)
|
||||||
|
- [RoomKeyStore](src/EchoHub.Client/Services/RoomKeyStore.cs.md)
|
||||||
|
- [UpdateBackupService](src/EchoHub.Client/Services/UpdateBackupService.cs.md)
|
||||||
|
- [UpdateChecker](src/EchoHub.Client/Services/UpdateChecker.cs.md)
|
||||||
|
- [UserSession](src/EchoHub.Client/Services/UserSession.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Client/Themes
|
||||||
|
|
||||||
|
- [Theme](src/EchoHub.Client/Themes/Theme.cs.md)
|
||||||
|
- [ThemeManager](src/EchoHub.Client/Themes/ThemeManager.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Client/UI
|
||||||
|
|
||||||
|
- [MainWindow](src/EchoHub.Client/UI/MainWindow.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Client/UI/Chat
|
||||||
|
|
||||||
|
- [ChatColors](src/EchoHub.Client/UI/Chat/ChatColors.cs.md)
|
||||||
|
- [ChatLine](src/EchoHub.Client/UI/Chat/ChatLine.cs.md)
|
||||||
|
- [ChatListSource](src/EchoHub.Client/UI/Chat/ChatListSource.cs.md)
|
||||||
|
- [ChatMessageManager](src/EchoHub.Client/UI/Chat/ChatMessageManager.cs.md)
|
||||||
|
- [ChatSegment](src/EchoHub.Client/UI/Chat/ChatSegment.cs.md)
|
||||||
|
- [RenderHelpers](src/EchoHub.Client/UI/Chat/RenderHelpers.cs.md)
|
||||||
|
- [WelcomeBanner](src/EchoHub.Client/UI/Chat/WelcomeBanner.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Client/UI/Dialogs
|
||||||
|
|
||||||
|
- [AudioPlayerDialog](src/EchoHub.Client/UI/Dialogs/AudioPlayerDialog.cs.md)
|
||||||
|
- [ChannelPasswordDialog](src/EchoHub.Client/UI/Dialogs/ChannelPasswordDialog.cs.md)
|
||||||
|
- [ConnectDialog](src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs.md)
|
||||||
|
- [CreateChannelDialog](src/EchoHub.Client/UI/Dialogs/CreateChannelDialog.cs.md)
|
||||||
|
- [ProfileEditDialog](src/EchoHub.Client/UI/Dialogs/ProfileEditDialog.cs.md)
|
||||||
|
- [ProfileViewDialog](src/EchoHub.Client/UI/Dialogs/ProfileViewDialog.cs.md)
|
||||||
|
- [SearchDialog](src/EchoHub.Client/UI/Dialogs/SearchDialog.cs.md)
|
||||||
|
- [StatusDialog](src/EchoHub.Client/UI/Dialogs/StatusDialog.cs.md)
|
||||||
|
- [UpdateConfirmDialog](src/EchoHub.Client/UI/Dialogs/UpdateConfirmDialog.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Client/UI/Helpers
|
||||||
|
|
||||||
|
- [DroppedFileParser](src/EchoHub.Client/UI/Helpers/DroppedFileParser.cs.md)
|
||||||
|
- [EmojiHelper](src/EchoHub.Client/UI/Helpers/EmojiHelper.cs.md)
|
||||||
|
- [HexColorHelper](src/EchoHub.Client/UI/Helpers/HexColorHelper.cs.md)
|
||||||
|
- [NickColorHelper](src/EchoHub.Client/UI/Helpers/NickColorHelper.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Client/UI/ListSources
|
||||||
|
|
||||||
|
- [ChannelListSource](src/EchoHub.Client/UI/ListSources/ChannelListSource.cs.md)
|
||||||
|
- [SearchListSource](src/EchoHub.Client/UI/ListSources/SearchListSource.cs.md)
|
||||||
|
- [UserListSource](src/EchoHub.Client/UI/ListSources/UserListSource.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Core/Constants
|
||||||
|
|
||||||
|
- [HubConstants](src/EchoHub.Core/Constants/HubConstants.cs.md)
|
||||||
|
- [MessageConventions](src/EchoHub.Core/Constants/MessageConventions.cs.md)
|
||||||
|
- [ValidationConstants](src/EchoHub.Core/Constants/ValidationConstants.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Core/Contracts
|
||||||
|
|
||||||
|
- [IChannelService](src/EchoHub.Core/Contracts/IChannelService.cs.md)
|
||||||
|
- [IChatBroadcaster](src/EchoHub.Core/Contracts/IChatBroadcaster.cs.md)
|
||||||
|
- [IChatService](src/EchoHub.Core/Contracts/IChatService.cs.md)
|
||||||
|
- [IEchoHubClient](src/EchoHub.Core/Contracts/IEchoHubClient.cs.md)
|
||||||
|
- [IMessageEncryptionService](src/EchoHub.Core/Contracts/IMessageEncryptionService.cs.md)
|
||||||
|
- [IUserService](src/EchoHub.Core/Contracts/IUserService.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Core/DTOs
|
||||||
|
|
||||||
|
- [AccountDtos](src/EchoHub.Core/DTOs/AccountDtos.cs.md)
|
||||||
|
- [AuthDtos](src/EchoHub.Core/DTOs/AuthDtos.cs.md)
|
||||||
|
- [ChatDtos](src/EchoHub.Core/DTOs/ChatDtos.cs.md)
|
||||||
|
- [CommonDtos](src/EchoHub.Core/DTOs/CommonDtos.cs.md)
|
||||||
|
- [InviteDtos](src/EchoHub.Core/DTOs/InviteDtos.cs.md)
|
||||||
|
- [ModerationDtos](src/EchoHub.Core/DTOs/ModerationDtos.cs.md)
|
||||||
|
- [ProfileDtos](src/EchoHub.Core/DTOs/ProfileDtos.cs.md)
|
||||||
|
- [ServerDtos](src/EchoHub.Core/DTOs/ServerDtos.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Core/Models
|
||||||
|
|
||||||
|
- [Attachment](src/EchoHub.Core/Models/Attachment.cs.md)
|
||||||
|
- [AttachmentKind](src/EchoHub.Core/Models/AttachmentKind.cs.md)
|
||||||
|
- [Channel](src/EchoHub.Core/Models/Channel.cs.md)
|
||||||
|
- [ChannelMembership](src/EchoHub.Core/Models/ChannelMembership.cs.md)
|
||||||
|
- [InviteCode](src/EchoHub.Core/Models/InviteCode.cs.md)
|
||||||
|
- [Message](src/EchoHub.Core/Models/Message.cs.md)
|
||||||
|
- [MessageType](src/EchoHub.Core/Models/MessageType.cs.md)
|
||||||
|
- [RefreshToken](src/EchoHub.Core/Models/RefreshToken.cs.md)
|
||||||
|
- [ServerRole](src/EchoHub.Core/Models/ServerRole.cs.md)
|
||||||
|
- [ServerStatsReport](src/EchoHub.Core/Models/ServerStatsReport.cs.md)
|
||||||
|
- [User](src/EchoHub.Core/Models/User.cs.md)
|
||||||
|
- [UserStatus](src/EchoHub.Core/Models/UserStatus.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Core/Security
|
||||||
|
|
||||||
|
- [RoomCrypto](src/EchoHub.Core/Security/RoomCrypto.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Core/Services
|
||||||
|
|
||||||
|
- [AsciiBannerService](src/EchoHub.Core/Services/AsciiBannerService.cs.md)
|
||||||
|
- [FileValidationHelper](src/EchoHub.Core/Services/FileValidationHelper.cs.md)
|
||||||
|
- [ImageToAsciiService](src/EchoHub.Core/Services/ImageToAsciiService.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Server
|
||||||
|
|
||||||
|
- [Program](src/EchoHub.Server/Program.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Server.Irc
|
||||||
|
|
||||||
|
- [IrcBroadcaster](src/EchoHub.Server.Irc/IrcBroadcaster.cs.md)
|
||||||
|
- [IrcClientConnection](src/EchoHub.Server.Irc/IrcClientConnection.cs.md)
|
||||||
|
- [IrcCommandHandler](src/EchoHub.Server.Irc/IrcCommandHandler.cs.md)
|
||||||
|
- [IrcGatewayService](src/EchoHub.Server.Irc/IrcGatewayService.cs.md)
|
||||||
|
- [IrcMessage](src/EchoHub.Server.Irc/IrcMessage.cs.md)
|
||||||
|
- [IrcMessageFormatter](src/EchoHub.Server.Irc/IrcMessageFormatter.cs.md)
|
||||||
|
- [IrcNumericReply](src/EchoHub.Server.Irc/IrcNumericReply.cs.md)
|
||||||
|
- [IrcOptions](src/EchoHub.Server.Irc/IrcOptions.cs.md)
|
||||||
|
- [IrcServiceExtensions](src/EchoHub.Server.Irc/IrcServiceExtensions.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Server/Auth
|
||||||
|
|
||||||
|
- [JwtTokenService](src/EchoHub.Server/Auth/JwtTokenService.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Server/Config
|
||||||
|
|
||||||
|
- [ServerLogsOptions](src/EchoHub.Server/Config/ServerLogsOptions.cs.md)
|
||||||
|
- [SpamOptions](src/EchoHub.Server/Config/SpamOptions.cs.md)
|
||||||
|
- [StatsOptions](src/EchoHub.Server/Config/StatsOptions.cs.md)
|
||||||
|
- [UploadLimits](src/EchoHub.Server/Config/UploadLimits.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Server/Controllers
|
||||||
|
|
||||||
|
- [AuthController](src/EchoHub.Server/Controllers/AuthController.cs.md)
|
||||||
|
- [ChannelsController](src/EchoHub.Server/Controllers/ChannelsController.cs.md)
|
||||||
|
- [FilesController](src/EchoHub.Server/Controllers/FilesController.cs.md)
|
||||||
|
- [InvitesController](src/EchoHub.Server/Controllers/InvitesController.cs.md)
|
||||||
|
- [ModerationController](src/EchoHub.Server/Controllers/ModerationController.cs.md)
|
||||||
|
- [ServerController](src/EchoHub.Server/Controllers/ServerController.cs.md)
|
||||||
|
- [UsersController](src/EchoHub.Server/Controllers/UsersController.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Server/Data
|
||||||
|
|
||||||
|
- [EchoHubDbContext](src/EchoHub.Server/Data/EchoHubDbContext.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Server/Hubs
|
||||||
|
|
||||||
|
- [ChatHub](src/EchoHub.Server/Hubs/ChatHub.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Server/Services
|
||||||
|
|
||||||
|
- [ChannelService](src/EchoHub.Server/Services/ChannelService.cs.md)
|
||||||
|
- [ChatService](src/EchoHub.Server/Services/ChatService.cs.md)
|
||||||
|
- [DirectoryClaimStore](src/EchoHub.Server/Services/DirectoryClaimStore.cs.md)
|
||||||
|
- [FileCleanupService](src/EchoHub.Server/Services/FileCleanupService.cs.md)
|
||||||
|
- [FileStorageService](src/EchoHub.Server/Services/FileStorageService.cs.md)
|
||||||
|
- [LinkEmbedService](src/EchoHub.Server/Services/LinkEmbedService.cs.md)
|
||||||
|
- [MessageEncryptionService](src/EchoHub.Server/Services/MessageEncryptionService.cs.md)
|
||||||
|
- [MuteExpirationService](src/EchoHub.Server/Services/MuteExpirationService.cs.md)
|
||||||
|
- [PresenceTracker](src/EchoHub.Server/Services/PresenceTracker.cs.md)
|
||||||
|
- [ServerDirectoryService](src/EchoHub.Server/Services/ServerDirectoryService.cs.md)
|
||||||
|
- [SignalRBroadcaster](src/EchoHub.Server/Services/SignalRBroadcaster.cs.md)
|
||||||
|
- [SpamGuard](src/EchoHub.Server/Services/SpamGuard.cs.md)
|
||||||
|
- [UserService](src/EchoHub.Server/Services/UserService.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Server/Services/ServerLogs
|
||||||
|
|
||||||
|
- [ServerLogsService](src/EchoHub.Server/Services/ServerLogs/ServerLogsService.cs.md)
|
||||||
|
- [ServerLogsSink](src/EchoHub.Server/Services/ServerLogs/ServerLogsSink.cs.md)
|
||||||
|
- [ServerLogsStreamService](src/EchoHub.Server/Services/ServerLogs/ServerLogsStreamService.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Server/Services/Stats
|
||||||
|
|
||||||
|
- [ServerStatsCollector](src/EchoHub.Server/Services/Stats/ServerStatsCollector.cs.md)
|
||||||
|
- [ServerStatsReportService](src/EchoHub.Server/Services/Stats/ServerStatsReportService.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Server/Setup
|
||||||
|
|
||||||
|
- [DataMigrationService](src/EchoHub.Server/Setup/DataMigrationService.cs.md)
|
||||||
|
- [DatabaseSetup](src/EchoHub.Server/Setup/DatabaseSetup.cs.md)
|
||||||
|
- [FirstRunSetup](src/EchoHub.Server/Setup/FirstRunSetup.cs.md)
|
||||||
|
|
||||||
|
### src/EchoHub.Tests/Irc
|
||||||
|
|
||||||
|
- [TestHelpers](src/EchoHub.Tests/Irc/TestHelpers.cs.md)
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,115 @@
|
|||||||
|
# ClientConfig.cs
|
||||||
|
|
||||||
|
> **Source:** `src/EchoHub.Client/Config/ClientConfig.cs`
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [AccountPreset](#accountpreset)
|
||||||
|
- [ClientConfig](#clientconfig)
|
||||||
|
- [NotificationConfig](#notificationconfig)
|
||||||
|
- [SavedServer](#savedserver)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AccountPreset
|
||||||
|
> **File:** `src/EchoHub.Client/Config/ClientConfig.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class AccountPreset
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
AccountPreset is a lightweight data container that groups optional account presentation attributes used by client configuration. It encapsulates a DisplayName, Bio, and NicknameColor so a named preset can be stored, transferred, or reapplied as a unit to influence how an account is presented in the UI.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This type exists to package related display properties together, enabling reuse and persistence of account presentation presets. Since all properties are nullable, consumers can merge a preset with existing data and only override the attributes that are explicitly set.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var preset = new AccountPreset
|
||||||
|
{
|
||||||
|
DisplayName = "Nova",
|
||||||
|
Bio = "Exploring the stars of code",
|
||||||
|
NicknameColor = "#1E90FF"
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Null properties indicate that the corresponding attribute should not override any existing value when applying the preset to an existing account.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ClientConfig
|
||||||
|
> **File:** `src/EchoHub.Client/Config/ClientConfig.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ClientConfig
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ClientConfig is a simple data container that groups the client’s preferences and runtime settings into a single object. It includes the list of configured servers (`SavedServers`), the default account preset (`DefaultPreset`), the currently selected theme (`ActiveTheme`), and the notification configuration (`Notifications`). It also carries optional application paths and rendering settings: `DownloadPath` specifies where attachments are saved (null means use the OS Downloads folder), and `DefaultAsciiSize` selects the ASCII-art rendering size for attached images (values 's', 'm', or 'l', defaulting to 'm').
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
ClientConfig centralizes user preferences and runtime settings, so components can rely on a single source of truth for initialization, persistence, and UI decisions. It folds server configuration (`SavedServers`) together with user-facing settings like the default preset (`DefaultPreset`), the active theme (`ActiveTheme`), and notification behavior (`Notifications`), reducing coupling between subsystems. By exposing `DownloadPath` and `DefaultAsciiSize`, it also captures file-management and rendering preferences that affect attachments across the app.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## NotificationConfig
|
||||||
|
> **File:** `src/EchoHub.Client/Config/ClientConfig.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class NotificationConfig
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The `NotificationConfig` class is a small, strongly-typed container for notification playback settings used by the client. It exposes `Enabled`, `Volume`, and an optional `SoundFile` to customize sound behavior. By default, `Enabled` is `true`, `Volume` is `30`, and `SoundFile` is unset, making it ready to bind from configuration sources.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This is a lightweight configuration object that decouples notification behavior from business logic and supports binding from JSON or other configuration providers. It keeps the surface minimal while making it easy to override defaults without code changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SavedServer
|
||||||
|
> **File:** `src/EchoHub.Client/Config/ClientConfig.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class SavedServer
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
SavedServer is a client-side representation of a configured server for the EchoHub client. It aggregates the server identity (Name and Url), optional user credentials (Username and RefreshToken), user preferences (RememberMe), and per-server state needed to restore a session across restarts. Notably, it includes per-channel encryption state (ChannelKeys), channel-level navigation state (LeftChannels), and per-channel read-tracking (LastReadMessages). These members are stored locally and are not exposed to the server; the server never sees the encryption keys, which are encrypted at rest and scoped to the local machine (see [`RoomKeyProtector`](../Services/RoomKeyProtector.cs.md)). At startup, the client can deserialize this object to rehydrate connections, rejoin channels (excluding those the user explicitly left), and persist unread counts and mentions across restarts.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
The `SavedServer` acts as a simple data container that binds together server identity, user identity (when supplied), and user-driven state that enhances the reconnect experience. It sits at the boundary between the persistence layer and the networking layer: serialization of this object enables quick restoration of a user session without re-issuing authentication or resynchronizing channel state. The `ChannelKeys` field, in particular, represents sensitive data tied to end-to-end encrypted channels and is kept on the client; its lifecycle is intentionally scoped to the user’s device and is managed with the same care prescribed for the `RefreshToken`.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var server = new SavedServer
|
||||||
|
{
|
||||||
|
Name = "EchoHub",
|
||||||
|
Url = "https://echo.example",
|
||||||
|
Username = "alice",
|
||||||
|
RememberMe = true,
|
||||||
|
LastConnected = DateTimeOffset.UtcNow,
|
||||||
|
ChannelKeys = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "general", "base64encryptedKeyHere" }
|
||||||
|
},
|
||||||
|
LeftChannels = new List<string> { "old-channel" },
|
||||||
|
LastReadMessages = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "general", "12345" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Treat `ChannelKeys` as sensitive data: avoid logging them or exposing them to the UI; ensure at-rest encryption via the client’s security model. The keys are stored only on the client device and are not sent to `server` endpoints.
|
||||||
|
- This class is intended as a plain data carrier (DTO) used by the persistence and connection layers; do not embed domain logic here. When upgrading or migrating fields, consider versioning in the surrounding storage layer to preserve compatibility.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# ConfigManager
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Config/ConfigManager.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class ConfigManager
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ConfigManager is a static helper that persists the client configuration to a JSON file under the user's profile directory and provides focused APIs for loading, saving, and managing saved servers. It centralizes file I/O behind a private lock to serialize access from UI actions and background tasks (token refresh, room keys, last-read checkpoints), helping prevent race conditions that could corrupt the config.
|
||||||
|
|
||||||
|
Use `ConfigManager.Load()` to obtain the current configuration (or a default [`ClientConfig`](ClientConfig.cs.md) when the file is missing or unreadable), modify the returned object, and persist changes with `ConfigManager.Save(config)`.
|
||||||
|
|
||||||
|
To manage saved servers, use `ConfigManager.SaveServer(...)` to upsert by `Url` and `ConfigManager.RemoveServer(string url)` to delete by `Url` (case-insensitive).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
All file I/O performed by `ConfigManager` is guarded by a single static lock (the private `Lock` named `FileLock`), ensuring reads and writes do not interleave across threads. The design favors resilience: a missing or unreadable config yields a fresh [`ClientConfig`](ClientConfig.cs.md), and save errors are swallowed to avoid crashing the host process. When upserting or removing saved servers, the code compares the server URLs using a case-insensitive match (`StringComparison.OrdinalIgnoreCase`), so entries differing only by casing do not duplicate and removals reliably locate targets.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Saves are best-effort; any exception during persistence is swallowed so callers should not depend on hard failures for user feedback.
|
||||||
|
- If the config file is absent, the directory is created and a default [`ClientConfig`](ClientConfig.cs.md) is used when loading.
|
||||||
|
- URL-based operations for saved servers use case-insensitive matching to maintain a consistent, deduplicated set.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Program
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Program.cs`
|
||||||
|
> **Kind:** file
|
||||||
|
|
||||||
|
|
||||||
|
The `Program` file serves as the entry point for the EchoHub client. It bootstraps startup by handling a potential CLI rollback (`--rollback`), performing a best-effort Unix execute-permission check, provisioning configuration (loading from `appsettings.json` with a fallback embedded resource at `EchoHub.Client.appsettings.example.json`), and configuring `Serilog` from the configuration before loading the runtime settings via [`ConfigManager`](Config/ConfigManager.cs.md) and initializing the Terminal.Gui UI with `Application.Create().Init()`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This file centralizes environment preparation and startup orchestration, encapsulating cross-platform concerns (rollback handling, permission checks, path setup, and post-update housekeeping) so the rest of the application can assume a ready, consistent runtime context. It also exposes a clear, testable bootstrap path that wires configuration, logging, and the UI startup in a single phase, reducing duplication across modules.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Rolling back can terminate startup early because `UpdateBackupService.RestoreBackup()` or subsequent error paths invoke `Environment.Exit`.
|
||||||
|
- Unix permission checks are best-effort and any failures are swallowed to avoid blocking startup on platform quirks.
|
||||||
|
- The initial configuration may be sourced from an embedded resource (`EchoHub.Client.appsettings.example.json`) if `appsettings.json` is absent, providing a safe fallback during first-run scenarios.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
|||||||
|
# AsyncRunner
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Services/AsyncRunner.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class AsyncRunner
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Runs the provided asynchronous work on a background thread and routes exceptions to the UI, eliminating boilerplate like `Task.Run`/try/catch/`app.Invoke(ShowError)`.
|
||||||
|
|
||||||
|
`AsyncRunner.Run` takes an `IApplication` (`app`), a `Func<Task>` representing the work, an `Action<string>` (`showError`), a string (`errorPrefix`) used in the user-facing error, and an optional `string? logContext` to enrich logs; if an exception occurs, it logs with `Log.Error` and invokes the UI thread to display the error via `showError`.
|
||||||
|
|
||||||
|
This pattern centralizes background execution and UI-error reporting, so callers need only supply the work and error message components and can rely on consistent logging and user feedback.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This abstraction isolates the cross-cutting concerns of background execution and UI error presentation. By encapsulating this pattern, it avoids duplicating boilerplate across call sites and ensures errors are logged with contextual information and surfaced on the UI thread via `IApplication.Invoke`.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- This method is fire-and-forget: it launches the work and does not return a `Task`; callers cannot await completion or observe exceptions from the caller's context. If you need completion signaling, consider returning a `Task` or providing a completion callback.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# AudioPlaybackService
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Services/AudioPlaybackService.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class AudioPlaybackService
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
AudioPlaybackService provides a thread-safe, asynchronous facade for audio playback using a private `Player` instance. It exposes the playback state via `IsPlaying` and `IsPaused`, and it forwards a `PlaybackFinished` event when the underlying `Player` completes playback. All public operations are serialized with a private `SemaphoreSlim` named `_lock` to prevent concurrent access to the player. When you call `PlayAsync`, if something is already playing it stops it before starting the new file; `PauseAsync`, `ResumeAsync`, and [`StopAsync`](../../EchoHub.Server/Services/ServerDirectoryService.cs.md) similarly acquire the lock, perform the appropriate operation if possible, and log any exceptions with `Log.Warning`. Volume is controlled via `SetVolumeAsync`, which clamps the requested volume to a maximum of 100 using `Math.Min`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This abstraction centralizes concurrency concerns and error handling around audio playback. By bridging the `Player` with a single, serialized surface, it reduces race conditions when multiple callers request playback from different parts of the application. The `PlaybackFinished` event provides a clean notification channel to consumers without exposing the internal player, enabling a decoupled UI or service layer to react to completion.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Exceptions during playback operations are swallowed after being logged with `Log.Warning`, so callers do not observe crashes but must rely on the logs to diagnose issues.
|
||||||
|
- All playback-related methods acquire the `_lock` semaphore, meaning long-running operations inside any call can block other playback requests and should be kept短-lived to avoid contention.
|
||||||
|
- `SetVolumeAsync` caps the volume at 100 via `Math.Min`, ensuring the underlying player never receives an out-of-range value.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# AvatarHelper
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Services/AvatarHelper.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
internal static class AvatarHelper
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
AvatarHelper provides a single entry point to upload an avatar from either a local file path or a remote URL by converting the target into a `Stream`, then delegating the actual upload to `ApiClient.UploadAvatarAsync`. It abstracts away the file I/O and HTTP fetch logic, ensuring callers don't need to manage streams or HTTP requests themselves. It returns the server's ASCII art response as a `string?` and guarantees the `Stream` is disposed after the upload.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
AvatarHelper isolates avatar uploading behind a focused API, so higher-level code doesn't need to know whether the source is a local file or a URL. It accepts either a local path or an HTTP(S) URL, resolves a valid `fileName` (defaulting to `avatar.png` when the URL doesn't supply one), and streams the content to `ApiClient.UploadAvatarAsync`. The helper ensures proper resource management by disposing the `Stream` after the upload, and it centralizes the cross-cutting concern of avatar uploads to a single place.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# ClientEncryptionService
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Services/ClientEncryptionService.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class ClientEncryptionService : IMessageEncryptionService
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ClientEncryptionService implements client-side encryption using AES-256-GCM to protect messages before sending them to the server, aligning with the server's ciphertext format so decryption occurs only with the shared key. After you provide a base64-encoded key via `SetKey`, it encrypts plaintext by generating a fresh 12-byte nonce and a 16-byte authentication tag, returning a string that starts with the `EncryptionPrefix` and includes base64-encoded nonce and payload; if no key has been set (`_key` is null), `Encrypt` returns the plaintext unchanged.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This class hides cryptography behind the [`IMessageEncryptionService`](../../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md) contract, offering a simple, predictable API for encryption and decryption while keeping key material private. It ensures that only a server-provisioned key enables encryption, and it produces self-contained ciphertext that carries its nonce and tag so the server can decrypt it reliably. The design also provides nullable-friendly helpers (`EncryptNullable`, `DecryptNullable`) to gracefully handle missing values.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
// Example usage of client-side encryption
|
||||||
|
var encryption = new ClientEncryptionService();
|
||||||
|
string base64Key = "<32-byte-base64-key>";
|
||||||
|
encryption.SetKey(base64Key);
|
||||||
|
string plaintext = "Secret message";
|
||||||
|
string ciphertext = encryption.Encrypt(plaintext);
|
||||||
|
string decrypted = encryption.Decrypt(ciphertext);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Encrypt before calling `SetKey` is a no-op: the input plaintext is returned unchanged when `_key` is null.
|
||||||
|
- Decrypt returns the original content if `_key` is null or the input does not start with the expected `EncryptionPrefix`.
|
||||||
|
- `SetKey` enforces a 32-byte (256-bit) key length and throws `InvalidOperationException` if the length is not exactly 32 bytes.
|
||||||
|
- Decryption errors are handled gracefully; if decryption fails for any reason, a sentinel message is returned: "[encrypted message — decryption failed, try re-logging to fetch the latest key]".
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# ClipboardFiles
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Services/ClipboardFiles.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class ClipboardFiles
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ClipboardFiles reads the OS clipboard to obtain a list of files when the clipboard holds a file-list (such as after copying files in a file manager). This enables scenarios where a copied set of files can be pasted or attached directly, without requiring the user to paste raw text paths. Call `TryGetFiles` to retrieve existing file paths from the clipboard; the method returns true when one or more valid paths are found, and false otherwise (including on platforms without file-list clipboard support).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This helper abstracts away platform differences in clipboard formats and presents a single, cohesive API for retrieving file lists from the clipboard. On Windows it enumerates files via the CF_HDROP channel and returns the paths that point to existing files. On Linux it reads a `text/uri-list` from the clipboard (via `wl-paste` or `xclip`), converts `file://` URLs to local paths, and keeps only paths that exist. The implementation favors a graceful failure path: any read-time exception is logged and the caller simply receives a non-success result, allowing callers to degrade gracefully without crashing. The API design emphasizes a simple success/failure boolean along with a concrete list of files, enabling straightforward integration into UX flows that want to treat copied files as attachable entities rather than plain text.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
if (ClipboardFiles.TryGetFiles(out var files))
|
||||||
|
{
|
||||||
|
foreach (var path in files)
|
||||||
|
Console.WriteLine(path);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- macOS and other non-supported platforms do not provide a file-list clipboard, so `TryGetFiles` returns false there.
|
||||||
|
- The method only returns paths that actually exist on disk; non-existent or malformed clipboard entries are ignored, and an empty result yields false.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# ClipboardImage
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Services/ClipboardImage.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class ClipboardImage
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Reads raw image bytes from the platform clipboard and returns them as a PNG byte array when available. Use `ClipboardImage.TryGetPng` when you need a canonical, pasteable PNG representation of whatever image the user has on the clipboard (for example, when accepting pasted screenshots or images in a terminal or chat input that cannot accept raw bitmap data).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`ClipboardImage` centralizes platform-specific clipboard handling: `TryGetPng` dispatches to `TryGetWindows`, `TryGetLinux`, or `TryGetMacOS` depending on `OperatingSystem` checks, and normalizes all outputs to PNG. When the clipboard format already contains PNG bytes (detected using the `PngMagic` signature or platform-registered PNG formats such as those discovered via `RegisterClipboardFormatW` on Windows), the bytes are passed through to preserve fidelity and transparency. When the clipboard exposes a DIB/bitmap (`CfDib` on Windows), the `DibToPng` helper builds a minimal BMP wrapper around the DIB bytes, decodes it with `Image.Load`, and re-encodes the result as PNG; this covers screenshots and editors that expose only device-independent bitmaps.
|
||||||
|
|
||||||
|
The class intentionally swallows and logs exceptions (via `Log.Warning`) from clipboard access and image decoding so callers get a simple success/failure result from `TryGetPng` instead of propagating clipboard or image-library exceptions.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Clipboard APIs are platform and threading sensitive. On Windows the OS clipboard typically requires running on an STA thread; calling `TryGetPng` from a non-STA thread may fail or return false. Ensure clipboard access is performed on an appropriate thread context for the platform.
|
||||||
|
- `DibToPng` validates the DIB header (minimum 40 bytes, header size bounds) and returns null for malformed input. Decoding can still fail at `Image.Load` for unsupported or corrupted bitmaps; such failures are logged and surface as a failure to `TryGetPng`.
|
||||||
|
- Re-encoding a DIB to PNG may not preserve alpha/transparency if the original bitmap format lacks alpha channels (DIB/CF_DIB often does not include alpha). If preserving exact alpha semantics is required, prefer sources that supply native PNG clipboard formats when possible.
|
||||||
|
- Converting clipboard data allocates buffers (the BMP wrapper and the resulting PNG byte array) and performs image decode/encode work; callers should expect a non-trivial CPU and memory cost for large images.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# ConnectionManager.cs
|
||||||
|
|
||||||
|
> **Source:** `src/EchoHub.Client/Services/ConnectionManager.cs`
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [ConnectionManager](#connectionmanager)
|
||||||
|
- [ConnectResult](#connectresult)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ConnectionManager
|
||||||
|
> **File:** `src/EchoHub.Client/Services/ConnectionManager.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
internal sealed class ConnectionManager : IAsyncDisposable
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Manages a server connection end-to-end: handles authentication via [`ApiClient`](ApiClient.cs.md), establishes end-to-end encryption, creates and wires an [`EchoHubConnection`](EchoHubConnection.cs.md), tracks joined channels, and exposes SignalR events so higher-level orchestrators can react without touching connection internals. Reach for `ConnectionManager` when you want UI code (for example an [`AppOrchestrator`](../AppOrchestrator.cs.md)) to observe connection and chat events through simple events rather than managing [`ApiClient`](ApiClient.cs.md) and [`EchoHubConnection`](EchoHubConnection.cs.md) yourself.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`ConnectionManager` centralizes lifecycle concerns: it authenticates (login/registration/refresh), subscribes to token rotation, attempts to fetch and apply the E2E encryption key, constructs and registers handlers on the [`EchoHubConnection`](EchoHubConnection.cs.md), and ensures channel membership state is tracked. It forwards the hub's runtime events (for example `MessageReceived`, `UserJoined`, `ChannelUpdated`) so callers receive high-level notifications and do not need to bind SignalR handlers directly. The class is intended as the single place that composes [`ApiClient`](ApiClient.cs.md), [`ClientEncryptionService`](ClientEncryptionService.cs.md)/[`RoomKeyStore`](RoomKeyStore.cs.md), and [`EchoHubConnection`](EchoHubConnection.cs.md) into a usable connection for the UI.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `ConnectAsync` reports progress via the `onStatus` callback and will throw on authentication failure — callers are expected to handle saved-session expiry and similar error flows.
|
||||||
|
- Event handlers (for example `MessageReceived`, `UserJoined`, `ConnectionStatusChanged`) may be invoked from signalr/connection threads; subscribers should not assume they run on the UI thread and must marshal to the UI thread when necessary.
|
||||||
|
- Always `await` disposing the manager (it implements `IAsyncDisposable`) so underlying resources such as the [`EchoHubConnection`](EchoHubConnection.cs.md) and [`ApiClient`](ApiClient.cs.md) are cleanly released; failing to do so can leave connections or background work active.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ConnectResult
|
||||||
|
> **File:** `src/EchoHub.Client/Services/ConnectionManager.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
internal record ConnectResult(
|
||||||
|
LoginResponse Login,
|
||||||
|
List<ChannelDto> Channels,
|
||||||
|
Dictionary<string, List<MessageDto>> Histories)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Login` | [`LoginResponse`](../../EchoHub.Core/DTOs/AuthDtos.cs.md) | — |
|
||||||
|
| `Channels` | `List<ChannelDto>` | — |
|
||||||
|
| `Histories` | `Dictionary<string, List<MessageDto>>` | — |
|
||||||
|
|
||||||
|
|
||||||
|
ConnectResult represents the payload returned after a successful connection, carrying everything the [`AppOrchestrator`](../AppOrchestrator.cs.md) needs to update the UI. It includes the authenticated login information (`Login`), the collection of available channels (`Channels`), and the initial per-channel histories (`Histories`), where each channel name maps to its starting list of messages, always including the default channel.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
ConnectResult is a `record`, so it participates in value-based equality and can be treated as a single unit when comparing connection outcomes. Note that its `Channels` and `Histories` collections are mutable (`List<ChannelDto>` and `Dictionary<string, List<MessageDto>>`); if you need true immutability, expose read-only wrappers or clone the collections when passing them onward.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The contained `List<ChannelDto>` and `Dictionary<string, List<MessageDto>>` are mutable; avoid mutating them in place and consider treating the `ConnectResult` as a snapshot that should be cloned if you require immutability downstream.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# EchoHubConnection.cs
|
||||||
|
|
||||||
|
> **Source:** `src/EchoHub.Client/Services/EchoHubConnection.cs`
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [ChannelPasswordRequiredException](#channelpasswordrequiredexception)
|
||||||
|
- [EchoHubConnection](#echohubconnection)
|
||||||
|
- [RoomLockedException](#roomlockedexception)
|
||||||
|
- [JoinOutcome](#joinoutcome)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ChannelPasswordRequiredException
|
||||||
|
> **File:** `src/EchoHub.Client/Services/EchoHubConnection.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class ChannelPasswordRequiredException : Exception
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ChannelPasswordRequiredException represents the domain condition that a join operation on a channel cannot proceed because a password is required or the provided password was invalid. It is intended to be caught by the UI layer, which then prompts the user for the correct password and retries the join operation. The exception carries the channel name via the `ChannelName` property to identify which channel needs authentication.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Using a distinct exception type to signal password-related authentication flows keeps the connection logic decoupled from the UI. The `ChannelName` property provides channel-specific context for prompts, enabling precise feedback such as prompting for the password of the channel identified by `ChannelName` when retrying.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Use a specific catch for `ChannelPasswordRequiredException` rather than a broad catch of `Exception`, to avoid handling unrelated failures; access the `ChannelName` to present a contextual, channel-specific prompt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## EchoHubConnection
|
||||||
|
> **File:** `src/EchoHub.Client/Services/EchoHubConnection.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class EchoHubConnection : IAsyncDisposable
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
A lightweight, event-driven wrapper around a SignalR `HubConnection` that manages authentication, reconnection and client-side handlers for the chat protocol. Use `EchoHubConnection` when you need a high-level, strongly-typed bridge between the server's [`IEchoHubClient`](../../EchoHub.Core/Contracts/IEchoHubClient.cs.md) callbacks and your UI or application logic — it registers the server method handlers, decrypts incoming content, exposes simple events (for messages, presence, channel updates, errors, etc.), and surfaces connection state changes.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`EchoHubConnection` centralizes SignalR integration concerns: it creates and configures the underlying `HubConnection` (including token provisioning via the provided [`ApiClient`](ApiClient.cs.md)), wires up automatic reconnect behavior, and maps server-invoked methods to public events such as `OnMessageReceived`, `OnUserJoined`, `OnChannelUpdated`, and others. Incoming [`MessageDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) instances are run through the connection's decryption path (see `DecryptMessage`/`DecryptField`) before being forwarded, and encrypted content that cannot be unlocked is replaced by the `LockedMessagePlaceholder`. The class implements `IAsyncDisposable` so consumers should `await DisposeAsync()` to cleanly stop the connection.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
// Assume these are already created: serverUrl (string), apiClient (ApiClient),
|
||||||
|
// encryption (ClientEncryptionService), roomKeys (RoomKeyStore).
|
||||||
|
var connection = new EchoHubConnection(serverUrl, apiClient, encryption, roomKeys);
|
||||||
|
|
||||||
|
connection.OnConnectionStateChanged += state => Console.WriteLine($"State: {state}");
|
||||||
|
connection.OnMessageReceived += message => Console.WriteLine($"Message from {message.From}: {message.Content}");
|
||||||
|
connection.OnReconnected += () => Console.WriteLine("Reconnected to hub");
|
||||||
|
|
||||||
|
// When finished with the connection:
|
||||||
|
await connection.DisposeAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Event handlers are invoked from the SignalR callbacks — subscribers should ensure any UI updates or shared-state mutations are marshalled to the correct synchronization context or made thread-safe.
|
||||||
|
- Encrypted message content is represented by the `LockedMessagePlaceholder` when the client lacks the room key; rejoining the channel with the passphrase (and so populating [`RoomKeyStore`](RoomKeyStore.cs.md)) is required to decrypt those contents.
|
||||||
|
- `IsConnected` reflects the underlying `HubConnection.State` at the moment of access and may change shortly after; use `OnConnectionStateChanged` and `OnReconnected` for lifecycle-driven logic.
|
||||||
|
- Attempting to join a password-protected channel can surface a `ChannelPasswordRequiredException` — callers that perform join flows should handle that explicitly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RoomLockedException
|
||||||
|
> **File:** `src/EchoHub.Client/Services/EchoHubConnection.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class RoomLockedException : Exception
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
`RoomLockedException` is thrown when attempting to send into an end-to-end encrypted channel whose room key isn’t cached. Without the key, the operation would emit plaintext, which must never happen, so the exception blocks the send. The `ChannelName` property exposes which channel is locked, and the constructor formats the failure message to include `#{channelName}` to guide unlocking.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This exception acts as a boundary between encryption state and message-sending logic. It is a domain-level signal distinct from other transport or I/O failures, enabling callers to trigger a user prompt to unlock the channel and retry the operation once unlocked. The `ChannelName` property ties the failure to a specific channel, enabling precise remediation flows.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## JoinOutcome
|
||||||
|
> **File:** `src/EchoHub.Client/Services/EchoHubConnection.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed record JoinOutcome(List<MessageDto> History, string? EncryptionSalt, string? WrappedRoomKey)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `History` | `List<MessageDto>` | — |
|
||||||
|
| `EncryptionSalt` | `string?` | — |
|
||||||
|
| `WrappedRoomKey` | `string?` | — |
|
||||||
|
|
||||||
|
|
||||||
|
Represents the result of joining a channel: the decrypted message history and, for end-to-end encrypted rooms, the key envelope needed to unlock the room content key. `History` is a `List<MessageDto>` containing the decrypted messages, and `WrappedRoomKey` (with optional `EncryptionSalt`) provides the cryptographic envelope when encryption is in play.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By encapsulating the join outcome in a single type, the caller can render history and prepare for decryption in one step. The nullable `WrappedRoomKey` and `EncryptionSalt` signal whether encryption is active for the channel; callers not using end-to-end encryption can ignore them. This keeps the join path concise while preserving a clear contract about what data is available after join.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `EncryptionSalt` and `WrappedRoomKey` are nullable; guard for nulls and only attempt decryption when these values are provided.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# NativeFolderPicker.cs
|
||||||
|
|
||||||
|
> **Source:** `src/EchoHub.Client/Services/NativeFolderPicker.cs`
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [NativeFolderPicker](#nativefolderpicker)
|
||||||
|
- [FolderPickResult](#folderpickresult)
|
||||||
|
- [PickerOutcome](#pickeroutcome)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## NativeFolderPicker
|
||||||
|
> **File:** `src/EchoHub.Client/Services/NativeFolderPicker.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class NativeFolderPicker
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Opens the OS-native folder chooser by shelling out to platform-specific dialogs (Windows Explorer, macOS Finder, Linux GTK/KDE), allowing the TUI to remain GUI-toolkit agnostic. It dispatches to the appropriate platform helper at runtime and returns a `FolderPickResult` with a `PickerOutcome` of `Unavailable` when no native dialog can run, so callers can fall back to a configured path. Failures are caught and logged to avoid crashing the UI, and the dialog title is a fixed prompt guiding the user to select EchoHub’s download folder.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
By shielding native dialogs behind `NativeFolderPicker`, the rest of the application stays decoupled from platform GUI toolkits, improving portability and testability. The abstraction also centralizes cross‑platform quirks (Windows PowerShell quoting, AppleScript invocation, and GTK/KDialog fallbacks) in one place, reducing duplication and ensuring a consistent user experience across environments.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Headless Linux environments (no `DISPLAY` or `WAYLAND_DISPLAY`) cause the picker to return `PickerOutcome.Unavailable`.
|
||||||
|
- Windows path handling escapes apostrophes in the initial directory to survive the embedded PowerShell script.
|
||||||
|
- If the user cancels the dialog or no path is selected, the result is `PickerOutcome.Cancelled` rather than an error; callers should handle this as a user action.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FolderPickResult
|
||||||
|
> **File:** `src/EchoHub.Client/Services/NativeFolderPicker.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed record FolderPickResult(PickerOutcome Outcome, string? Path)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Outcome` | `PickerOutcome` | — |
|
||||||
|
| `Path` | `string?` | — |
|
||||||
|
|
||||||
|
|
||||||
|
FolderPickResult is an immutable data container that captures the result of a native folder picker operation. It pairs the `PickerOutcome` with an optional `Path`, letting callers distinguish between a successful selection and cancellation while carrying the selected folder path only when available.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
As a `record`, `FolderPickResult` benefits from value-based equality and supports deconstruction, enabling concise comparisons and pattern matching when consuming results from the native folder picker. It encapsulates the outcome and potential path in a single, strongly-typed value, simplifying higher-level handling and reducing the need for multiple disparate return values.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- `Path` is nullable; validate before use and prefer accessing `Path` only when `Outcome` indicates a successful result.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PickerOutcome
|
||||||
|
> **File:** `src/EchoHub.Client/Services/NativeFolderPicker.cs`
|
||||||
|
> **Kind:** enum
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public enum PickerOutcome
|
||||||
|
{
|
||||||
|
Chosen,
|
||||||
|
|
||||||
|
Cancelled,
|
||||||
|
|
||||||
|
Unavailable,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Represents the outcome of prompting the user to pick a folder via the native picker. Use it to branch logic based on whether the user selected a folder, cancelled the dialog, or the environment doesn't provide a picker.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By isolating the three possible results into a single enum, callers can write concise, robust code without tying their logic to UI details. The Cancelled and Unavailable outcomes allow you to differentiate between a user-initiated abort and a runtime environment where the picker isn't present, enabling graceful fallbacks. Tie the Chosen outcome to a corresponding `FolderPickResult` instance that carries the selected path in its `Path` property.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
// Example: respond to folder-picking outcomes
|
||||||
|
public void HandleOutcome(PickerOutcome outcome, FolderPickResult folderPath)
|
||||||
|
{
|
||||||
|
switch (outcome)
|
||||||
|
{
|
||||||
|
case PickerOutcome.Chosen:
|
||||||
|
Console.WriteLine($"Selected folder: {folderPath.Path}");
|
||||||
|
break;
|
||||||
|
case PickerOutcome.Cancelled:
|
||||||
|
// User cancelled the dialog; no folder selected.
|
||||||
|
break;
|
||||||
|
case PickerOutcome.Unavailable:
|
||||||
|
// Fall back to a non-UI flow
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Do not access `FolderPickResult.Path` when outcome is not `PickerOutcome.Chosen`.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# NotificationSoundService
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Services/NotificationSoundService.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class NotificationSoundService
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
NotificationSoundService coordinates playback of the application's notification sound using a configurable file path and volume. It exposes `PlayAsync` for normal operation (respecting the `Enabled` setting) and `PlayTestAsync` to audition the sound regardless of that setting; internally it resolves the sound path, applies the configured volume, and uses a `SemaphoreSlim` lock plus a timeout (`PlaybackTimeout`) to avoid blocking future notifications.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
Architecturally, this class centralizes notification sound behavior so callers don't need to touch the `_player` or handle `PlaybackFinished` events directly. It encapsulates path resolution: first a user-configured path (`_config.SoundFile`), if present and exists, else a bundled default at `Path.Combine(AppContext.BaseDirectory, "Assets", "Notification.mp3")`. The combination of a serializing lock (`_lock`) and a guarded finish path ensures only one sound plays at a time and that resources are released promptly even if playback misbehaves.
|
||||||
|
|
||||||
|
The playback flow subscribes to `_player.PlaybackFinished` and uses a `TaskCompletionSource` to await either completion or the timeout; this design guarantees the lock is released even if playback misfires or completes synchronously.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- If no valid sound file is found, notifications will be silent (log: "No notification sound file found — notifications will be silent").
|
||||||
|
- `PlayAsync` will early-return if `_config.Enabled` is false or `_resolvedSoundPath` is null; `PlayTestAsync` will still return early if `_resolvedSoundPath` is null. Both rely on a correctly resolved path to function.
|
||||||
|
- The `_lock` is released in a `finally` block to guarantee progress even when exceptions occur.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# OutgoingAttachment
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Services/OutgoingAttachment.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed record OutgoingAttachment(
|
||||||
|
Stream Stream,
|
||||||
|
string FileName,
|
||||||
|
string? DeclaredKind = null,
|
||||||
|
string? EncryptedPreview = null)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Stream` | `Stream` | — |
|
||||||
|
| `FileName` | `string` | — |
|
||||||
|
| `DeclaredKind` | `string?` | `null` |
|
||||||
|
| `EncryptedPreview` | `string?` | `null` |
|
||||||
|
|
||||||
|
|
||||||
|
OutgoingAttachment is a compact, immutable data carrier that bundles the pieces needed to upload a file as part of a message: the content as a `Stream` and the original `FileName`. When using end-to-end encrypted channels, `DeclaredKind` signals the attachment type (image, audio, or file) and `EncryptedPreview` holds the room-encrypted ASCII preview for images; on normal channels, only `Stream` and `FileName` are populated.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
As a `record`, `OutgoingAttachment` provides value-based equality, making attachments easy to compare, cache, or deduplicate as they traverse the messaging pipeline. The optional `DeclaredKind` and `EncryptedPreview` fields separate transport payload from encryption/presentation concerns, keeping encoding logic out of the transport object.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- If `DeclaredKind` is provided for an encrypted attachment, ensure `EncryptedPreview` is also supplied to avoid inconsistent previews.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# PathSetup
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Services/PathSetup.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class PathSetup
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
PathSetup is a small helper that ensures the application's directory is present on the system PATH so users can run the `echohub` CLI from any terminal without specifying the full path. The public entry point, `EnsureOnPath`, checks the current PATH and, if the app directory isn't already included, updates PATH in a platform-appropriate way: Windows adds the directory to the user-level PATH, while Unix-like systems append an export line to common shell profile files. The implementation derives the target directory from `AppContext.BaseDirectory`, normalizes path separators, and gracefully handles failures by logging at the debug level if PATH modification cannot be completed.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
PathSetup centralizes platform-specific PATH augmentation behind a simple, testable API. It makes the side-effect of PATH modification explicit and isolated from business logic, reducing duplication and potential inconsistencies across the codebase. The class uses an idempotent approach: it first checks whether the directory is already on PATH and only proceeds if needed. On Unix-like systems, it uses a persistent marker (`# Added by EchoHub`) to identify its export line in shell profiles, and it guards against duplicating entries. The combination of platform-specific handling, guarded writes, and informative logging ensures predictable behavior during installation and first-run setup while minimizing surprises for end users.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
// Typical usage during installation or first-run setup
|
||||||
|
PathSetup.EnsureOnPath();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- On Windows, the path update affects only the current user by modifying the user PATH environment variable, avoiding system-wide changes.
|
||||||
|
- On Unix-like systems, the code appends a PATH export line to common shell profiles (``.profile``, ``.bashrc``, ``.zshrc``); it skips profiles that already contain the app directory and creates ``~/.profile`` as a fallback when no profiles exist.
|
||||||
|
- A persistent marker (``# Added by EchoHub``) helps avoid duplicating the export line on repeated runs.
|
||||||
|
- The operation is best observed after restarting terminals or re-sourcing profiles; until that point, newly opened sessions may not reflect the updated PATH.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# RoomKeyProtector
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Services/RoomKeyProtector.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class RoomKeyProtector
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Encrypts cached room content keys at rest so the client config never holds them as plain base64. Windows uses DPAPI (current-user scope, format prefix `dp1:`). On other platforms the keys are AES-GCM encrypted with a per-user master key file stored next to the config with permissions 0600 (prefix `k1:`) — without an OS keychain that is file-permission-level protection, not zero-knowledge: anyone who can read both the config and the key file can recover the room keys. Values with no recognized prefix are legacy plain-base64 keys from older clients; they load once and are re-encrypted. The room passphrase itself is never stored in any form.
|
||||||
|
|
||||||
|
The primary public surface consists of:
|
||||||
|
- `Protect(byte[] roomKey)`: encrypts a room key for storage in the config.
|
||||||
|
- `TryUnprotect(string stored, out byte[] roomKey, out bool wasLegacy)`: decrypts a stored value back into a room key.
|
||||||
|
The class caches the per-user master key and selects the protection mechanism based on the platform (DPAPI on Windows when enabled, otherwise the per-user master-key path). It also handles migration of legacy entries by re-encrypting them using the active scheme on subsequent saves. The constants `DpapiPrefix` and `KeyFilePrefix` label the on-disk formats, ensuring callers remain agnostic to the underlying storage strategy.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# RoomKeyStore
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Services/RoomKeyStore.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class RoomKeyStore
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Holds and manages room content keys for end-to-end encrypted channels for the active session and the persisted per-server client configuration. Use `RoomKeyStore` when you need a single place to cache decrypted room keys in memory, persist them encrypted to the local config (so users don't retype passphrases on each launch), and track which channels are known to be end-to-end encrypted.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`RoomKeyStore` is the in-process authority for room keys: it keeps a memory cache (`_keys`) for the running session and a set (`_encryptedChannels`) to mark channels that are treated as encrypted. It delegates on-disk protection to [`RoomKeyProtector`](RoomKeyProtector.cs.md) so keys never leave the machine in plaintext. Calling `LoadForServer` binds the store to a specific server URL, loads that server's `SavedServer.ChannelKeys` via `ConfigManager.Load()`, and hydates the in-memory cache (skipping unreadable entries). Legacy plaintext/legacy-storage entries detected by `RoomKeyProtector.TryUnprotect` are re-encrypted and re-persisted as a one-way upgrade. All public mutation and lookup methods synchronize on the internal `Lock` (`_lock`) to provide basic thread-safety for concurrent callers.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `TryGetKey` returns the stored byte array reference from the internal `_keys` map (no defensive copy). Callers must not mutate the returned `byte[]` in-place — clone it first if modification is required.
|
||||||
|
- Channel name lookup is case-insensitive because the internal collections use `StringComparer.OrdinalIgnoreCase`. Treat channel names consistently to avoid duplicate/lookup surprises.
|
||||||
|
- Loading ignores unreadable cached entries and will re-persist only entries that [`RoomKeyProtector`](RoomKeyProtector.cs.md) could successfully unprotect; `TryStoreFromEnvelope` returns false when the provided KEK fails to unwrap the envelope and will leave the cache unchanged. Storing or removing a key persists the corresponding `SavedServer.ChannelKeys` entry immediately (via the store's persistence path).
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# UpdateBackupService.cs
|
||||||
|
|
||||||
|
> **Source:** `src/EchoHub.Client/Services/UpdateBackupService.cs`
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [BackupJsonContext](#backupjsoncontext)
|
||||||
|
- [UpdateBackupService](#updatebackupservice)
|
||||||
|
- [BackupInfo](#backupinfo)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## BackupJsonContext
|
||||||
|
> **File:** `src/EchoHub.Client/Services/UpdateBackupService.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[System.Text.Json.Serialization.JsonSerializable(typeof(BackupInfo))]
|
||||||
|
internal partial class BackupJsonContext : System.Text.Json.Serialization.JsonSerializerContext
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Defines a source-generated JSON serialization context for `BackupInfo` by annotating the internal partial class ``BackupJsonContext`` with ``JsonSerializable(typeof(BackupInfo))``. This enables high-performance, reflection-free JSON serialization and deserialization via System.Text.Json's source generator when working with ``BackupInfo``.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By centralizing the JSON metadata in ``BackupJsonContext``, the codebase gains a single, version-stable contract for serializing ``BackupInfo``. The generated ```JsonTypeInfo<BackupInfo>``` exposed as ``BackupJsonContext.Default.BackupInfo`` is consumed by ``JsonSerializer`` overloads that accept type metadata, reducing runtime reflection and enabling better inlining and optimization. This scope-limited context also makes it straightforward to extend serialization support to additional related types by extending the same context without changing call-sites.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UpdateBackupService
|
||||||
|
> **File:** `src/EchoHub.Client/Services/UpdateBackupService.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class UpdateBackupService
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
UpdateBackupService is a centralized helper that manages pre-update backups and rollback restoration for the auto-updater. It stores backups under the user profile in `~/.echohub/update-backup/` and exposes operations to create a snapshot, verify an existing backup, and read its metadata. Before applying an update, `CreateBackup()` snapshots the current application directory (via `AppContext.BaseDirectory`) into a ZIP named `backup.zip` and writes a `backup-info.json` containing the version, app directory, and timestamp. It skips log files to avoid locking issues, uses `CompressionLevel.Fastest` for speed, and annotates the backup with the current version from `UpdateChecker.CurrentVersion`. `BackupExists()` checks for the presence of both `backup.zip` and `backup-info.json`, while `GetBackupInfo()` reads and deserializes the metadata using `BackupJsonContext.Default.BackupInfo`. The `IsPostUpdate` flag signals that a post-update backup was produced and may influence rollback or recovery flow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## BackupInfo
|
||||||
|
> **File:** `src/EchoHub.Client/Services/UpdateBackupService.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record BackupInfo(
|
||||||
|
string Version,
|
||||||
|
string AppDirectory,
|
||||||
|
DateTimeOffset CreatedAt)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Version` | `string` | — |
|
||||||
|
| `AppDirectory` | `string` | — |
|
||||||
|
| `CreatedAt` | `DateTimeOffset` | — |
|
||||||
|
|
||||||
|
|
||||||
|
BackupInfo is a `record` that encapsulates the metadata for a backup produced by the application. It aggregates the `Version` string, the `AppDirectory` path where the backup resides, and the creation timestamp `CreatedAt` as a `DateTimeOffset`, providing a single, immutable value that callers can transport, compare, or display without reconstructing individual fields. Use this type whenever you need to pass around a complete snapshot of backup identity and location rather than scattering primitive values.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Because `BackupInfo` is a `record`, it provides value-based equality and immutability, so two backups with the same `Version`, `AppDirectory`, and `CreatedAt` compare as equal. This makes it ideal as a transport object across service boundaries and as a stable key or result in collections. It also supports deconstruction, enabling concise extraction of its three fields when needed.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- This object is immutable; its properties are set at construction time and cannot be changed afterward.
|
||||||
|
- The `CreatedAt` value uses `DateTimeOffset` to preserve the exact point in time including offset, which is important for cross-system backups and logs.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# UpdateChecker
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Services/UpdateChecker.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class UpdateChecker : IDisposable
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Checks for application updates on a background schedule and coordinates a safe, post-TUI update process. Use `UpdateChecker` when you want automatic or on-demand update checks inside a Terminal.Gui-based host but need the actual download/extract/restart work to run after the UI main loop has exited.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`UpdateChecker` encapsulates the interaction between the UI, a periodic `Updater` and the host process that must perform the actual update. It listens for `Updater` events and, when the user confirms an update via `UpdateConfirmDialog.Show`, sets the public [`PendingUpdate`](../AppOrchestrator.cs.md) delegate and requests the UI to stop so the host can perform the heavy work on a plain console. This design avoids the console deadlock that would occur if the updating process tried to restart while the Terminal.Gui main loop still owned the console. `CurrentVersion` exposes the assembly version used in the confirmation UI.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
// During application startup
|
||||||
|
var checker = new UpdateChecker(app);
|
||||||
|
checker.Start(); // starts periodic checks in RELEASE builds
|
||||||
|
|
||||||
|
// Trigger a manual check from UI or command handler
|
||||||
|
await checker.CheckNowAsync();
|
||||||
|
|
||||||
|
// After the Terminal.Gui main loop exits, the host should run any pending update
|
||||||
|
if (checker.PendingUpdate != null)
|
||||||
|
{
|
||||||
|
await checker.PendingUpdate(); // will download/extract and may restart the process
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- [`PendingUpdate`](../AppOrchestrator.cs.md) is only set when the user confirms an available update via `UpdateConfirmDialog.Show`; the host must check and invoke [`PendingUpdate`](../AppOrchestrator.cs.md) after the TUI main loop exits.
|
||||||
|
- `Start()` is conditional on the `RELEASE` build symbol — in non-RELEASE builds the periodic checker does not run.
|
||||||
|
- Invoking the [`PendingUpdate`](../AppOrchestrator.cs.md) delegate runs the updater on a plain console and may end by restarting the app (the code calls into the `Updater` which performs download/extract/restart). The host should not expect normal process continuation after the update completes.
|
||||||
|
- `CurrentVersion` reads the assembly version and will return `"0.0.0"` if the assembly version cannot be determined.
|
||||||
|
- Backup creation is attempted via `UpdateBackupService.CreateBackup()` before applying an update; failures are logged and the update continues without a backup.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# UserSession
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Services/UserSession.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
internal sealed class UserSession
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Represents the current user\'s session state within the client, encapsulating the `Username`, the presence `Status` from [`UserStatus`](../../EchoHub.Core/Models/UserStatus.cs.md), and an optional `StatusMessage`. It is a lightweight in-memory container used by UI and networking layers to track who is logged in and how they present themselves. The `Reset` method reinitializes all fields to their defaults: `Username` to empty, `Status` to `UserStatus.Online`, and `StatusMessage` to `null`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This small class centralizes session-related data so multiple components can read and update the user\'s identity and presence from a single source of truth. By being `internal` and `sealed`, it communicates that this is an implementation detail of the client assembly and should not be extended or exposed publicly.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Theme.cs
|
||||||
|
|
||||||
|
> **Source:** `src/EchoHub.Client/Themes/Theme.cs`
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [Theme](#theme)
|
||||||
|
- [ThemeColors](#themecolors)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Theme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/Theme.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class Theme
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The `Theme` class encapsulates the color palette used by the UI. It groups per-surface color sets for the main surfaces (`Base`, `Menu`, `Dialog`, `Status`) and exposes an optional `Border` color that can override the window frame independently of text. By providing a name and a complete set of colors, a developer can switch or define visual styles at runtime and apply them to the UI. If you do not need a separate border color, leave `Border` as null to fall back to `Base`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
The `Theme` object acts as a central theme descriptor that isolates surface-specific colors from the core palette, making it easy to create variants (e.g., light, dark, or glassy appearances) without scattering color values through the code. The optional `Border` enables stylistic nuances for window chrome without altering text or control coloring, helping to achieve subtler, themed aesthetics while preserving readability.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var theme = new Theme
|
||||||
|
{
|
||||||
|
Name = "Glass",
|
||||||
|
Base = new ThemeColors(), // default color family for surfaces
|
||||||
|
Border = null // explicit fallback to Base colors for borders
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ThemeColors
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/Theme.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ThemeColors
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ThemeColors is a small data container that groups the color tokens used by the UI: `Foreground`, `Background`, `FocusForeground`, and `FocusBackground`. Create and pass a single `ThemeColors` instance to ensure consistent theming across components rather than scattering color literals throughout the code.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By centralizing color choices in `ThemeColors`, the UI can swap themes or provide variations without touching individual controls. The default initializers encode a high-contrast dark theme (white text on black, focus highlight in blue), but you can override any property to tailor a theme for a particular context.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Mutability: the properties have public setters, so the color values can be changed after construction; if a `ThemeColors` instance is shared, mutations will affect all dependents.
|
||||||
|
- Defaults are defined via property initializers; override them on construction if you want a different baseline.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,559 @@
|
|||||||
|
# ThemeManager.cs
|
||||||
|
|
||||||
|
> **Source:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [ThemeManager](#thememanager)
|
||||||
|
- [ApplyTheme](#applytheme)
|
||||||
|
- [BuildColorScheme](#buildcolorscheme)
|
||||||
|
- [GetAvailableThemes](#getavailablethemes)
|
||||||
|
- [GetTheme](#gettheme)
|
||||||
|
- [ParseColor](#parsecolor)
|
||||||
|
- [SaveTheme](#savetheme)
|
||||||
|
- [BuiltInThemes](#builtinthemes)
|
||||||
|
- [ClassicTheme](#classictheme)
|
||||||
|
- [DefaultTheme](#defaulttheme)
|
||||||
|
- [DraculaTheme](#draculatheme)
|
||||||
|
- [GruvboxTheme](#gruvboxtheme)
|
||||||
|
- [HackerTheme](#hackertheme)
|
||||||
|
- [HighContrastTheme](#highcontrasttheme)
|
||||||
|
- [JsonOptions](#jsonoptions)
|
||||||
|
- [LightTheme](#lighttheme)
|
||||||
|
- [MonokaiTheme](#monokaitheme)
|
||||||
|
- [NordTheme](#nordtheme)
|
||||||
|
- [OceanTheme](#oceantheme)
|
||||||
|
- [RosePineTheme](#rosepinetheme)
|
||||||
|
- [SolarizedTheme](#solarizedtheme)
|
||||||
|
- [ThemeDir](#themedir)
|
||||||
|
- [TransparentLightTheme](#transparentlighttheme)
|
||||||
|
- [TransparentTheme](#transparenttheme)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ThemeManager
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class ThemeManager
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ThemeManager is a static helper that centralizes theming for the client UI. It defines built-in themes, reads user-defined themes from the user's theme directory, and exposes methods to enumerate available themes, fetch a theme by name, apply a theme at runtime, and persist theme definitions to disk. Developers reach for it when they need to present theme choices to users, switch the active look, or save a customized theme for future sessions.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
Theme definitions live as [`Theme`](Theme.cs.md) instances inside the manager, with a fixed set of built-ins (e.g. `DefaultTheme`, `TransparentTheme`, `TransparentLightTheme`, `ClassicTheme`, `LightTheme`, `HackerTheme`, `SolarizedTheme`, `DraculaTheme`, `MonokaiTheme`, `NordTheme`, `GruvboxTheme`, `OceanTheme`, `HighContrastTheme`, `RosePineTheme`) and a mechanism to discover additional user themes from the directory located at `ThemeDir`. `GetAvailableThemes()` merges these sources while skipping duplicates by name and ignoring malformed theme files; if the theme directory cannot be read, it gracefully falls back to the built-ins. The color wiring happens in `BuildColorScheme(ThemeColors colors)` to ensure the editor surfaces—such as `TextView` and `TextField`—are pinned to the theme’s colors so transparency is preserved (e.g. transparent themes do not render an opaque input background). `ApplyTheme(Theme theme)` applies the chosen look to UI chrome like frame borders and titles, while `SaveTheme(Theme theme)` persists changes to disk as a best-effort operation.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Reading themes from disk is guarded with a fallback to built-ins; IO failures result in a safe degradation rather than a crash.
|
||||||
|
- Saving themes is a best-effort operation and may fail silently to avoid impacting startup or runtime stability.
|
||||||
|
- Color parsing relies on `ParseColor(string colorName)`; ensure color names in themes map to known colors to avoid rendering surprises.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ApplyTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** method
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static void ApplyTheme(Theme theme)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `theme` | [`Theme`](Theme.cs.md) | — |
|
||||||
|
|
||||||
|
**Returns:** `void`
|
||||||
|
|
||||||
|
|
||||||
|
ApplyTheme translates a [`Theme`](Theme.cs.md) into runtime color schemes and registers them with the central scheme registry (`SchemeManager`). For each area (`Base`, `Menu`, `Dialog`) it calls `BuildColorScheme` and registers the result via `SchemeManager.AddScheme`. The `Border` area is populated as well, using `theme.Border` when provided or falling back to `theme.Base` when it is not, ensuring frame decorations always have a defined appearance.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
By encapsulating the mapping from a [`Theme`](Theme.cs.md) to per-area color schemes, `ApplyTheme` centralizes theming logic and reduces boilerplate across the UI. It also encodes the intended fallback for borders: if a `Border` scheme isn't specified, the `Base` scheme is reused so borders and title bars stay consistent with the rest of the theme.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- If `theme.Base` is null and no explicit `theme.Border` is provided, `BuildColorScheme` will receive null, which could lead to an exception at runtime. Ensure `theme.Base` is non-null when a border theme isn't supplied.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BuildColorScheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** method
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static Scheme BuildColorScheme(ThemeColors colors)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `colors` | [`ThemeColors`](Theme.cs.md) | — |
|
||||||
|
|
||||||
|
**Returns:** `Scheme`
|
||||||
|
|
||||||
|
|
||||||
|
BuildColorScheme is an internal helper that converts a [`ThemeColors`](Theme.cs.md) instance into a complete `Scheme` by translating the theme's foreground/background for normal and focused states into two `Attribute`s and applying them across the scheme's state properties (`Normal`, `Focus`, `HotNormal`, `HotFocus`, `Disabled`, `Editable`, `ReadOnly`). It ensures the editable areas reflect the same colors as the surrounding background, which matters for transparent themes.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Conceptually, this centralizes the translation from [`ThemeColors`](Theme.cs.md) to a `Scheme`, guaranteeing consistent color usage across `Normal`/`Focus` and their hot variants. By reusing the same color attributes for `Normal`, `Disabled`, and the editable states, it reduces drift when themes change and keeps UI elements visually cohesive. The inline comment explains the rationale: binding `Editable` and `ReadOnly` to the theme's `Normal` colors ensures the input areas don't render an opaque box behind transparent themes.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Disabled uses the same color as `Normal`; if you need a distinct disabled appearance, this method would need to be extended.
|
||||||
|
- Editable and ReadOnly are pinned to `Normal` to preserve background transparency; changing this could cause mismatches with the theme's background in transparent themes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GetAvailableThemes
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** method
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static List<Theme> GetAvailableThemes()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:** `List<Theme>`
|
||||||
|
|
||||||
|
|
||||||
|
GetAvailableThemes collects the available themes by starting with the built-in set (`BuiltInThemes`), then augmenting it with user-provided themes discovered as JSON files in `ThemeDir`. It reads each `*.json` file, deserializes the content into a [`Theme`](Theme.cs.md) using `JsonSerializer` with `JsonOptions`, and, if the resulting theme has a non-empty `Name` and isn't already present (checked by name using `StringComparison.OrdinalIgnoreCase`), adds it to the list. If the theme directory can't be read or a file is malformed, those items are skipped and the method returns the built-in themes as a fallback. The result is a `List<Theme>` that callers can present to the user.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
The `GetAvailableThemes` abstraction centralizes theme discovery, ensuring that built-in themes serve as a baseline while allowing runtime customization through JSON files in `ThemeDir`. It performs simple de-duplication by `Theme.Name` in a case-insensitive manner, so user-provided themes do not create duplicates of built-ins. The design favors resilience: IO or deserialization failures are swallowed so startup remains stable, and valid themes are still returned. This function depends on the shape of the [`Theme`](Theme.cs.md) model (e.g., `Name`, `Base`/`Menu`/`Dialog` color sets) to render themes in the UI.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var themes = ThemeManager.GetAvailableThemes();
|
||||||
|
foreach (var t in themes)
|
||||||
|
{
|
||||||
|
Console.WriteLine(t.Name);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- IO or JSON parsing errors for individual files are ignored; only valid themes are included in the result.
|
||||||
|
- If `ThemeDir` does not exist or cannot be read, the method falls back to returning only the built-in themes.
|
||||||
|
- A runtime-provided theme with a name equal (ignoring case) to an existing built-in theme will be skipped to avoid duplicates.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GetTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** method
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static Theme GetTheme(string name)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `name` | `string` | — |
|
||||||
|
|
||||||
|
**Returns:** [`Theme`](Theme.cs.md)
|
||||||
|
|
||||||
|
|
||||||
|
Returns the [`Theme`](Theme.cs.md) whose `Name` matches the provided `name` using a case-insensitive comparison (`StringComparison.OrdinalIgnoreCase`), sourcing candidates from `GetAvailableThemes()`. If no match is found, it returns `DefaultTheme` as a safe fallback. This encapsulates the pattern of resolving a theme by name and protects callers from handling nulls or missing themes themselves.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ParseColor
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** method
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static Color ParseColor(string colorName)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `colorName` | `string` | — |
|
||||||
|
|
||||||
|
**Returns:** `Color`
|
||||||
|
|
||||||
|
|
||||||
|
Parses a color name into a `Color` value by delegating to `Color.TryParse`. If the parse succeeds, it returns the resulting color (or `Color.White` if the parsed value is null). If parsing fails, it falls back to `Color.White`. This provides a safe, centralized way to convert string-based color specifications (for example, theme or config values) into a concrete `Color` without forcing callers to handle parsing errors themselves.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This method encapsulates the color-name resolution logic so the rest of the theming code does not need to repeat `TryParse` calls or null checks. It guarantees a non-null `Color` return value by defaulting to `Color.White`, thereby defining a system-wide fallback policy for theme colors. Being a private helper, it represents an internal implementation detail of the theme system rather than a public API, which keeps the surface area clean for consumers.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Invalid or unrecognized color names map to `Color.White`, which can mask configuration errors; consider validating color names if distinguishing between an explicit white and a default fallback is important.
|
||||||
|
- If `colorName` is null or empty, the method still returns `Color.White` via the parse/fallback path, ensuring callers always receive a concrete `Color` without exceptions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### SaveTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** method
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static void SaveTheme(Theme theme)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `theme` | [`Theme`](Theme.cs.md) | — |
|
||||||
|
|
||||||
|
**Returns:** `void`
|
||||||
|
|
||||||
|
|
||||||
|
Saves a [`Theme`](Theme.cs.md) to disk as a JSON file under `ThemeDir`. It ensures `ThemeDir` exists, constructs the file path using the theme's name (the value of `theme.Name`) with a `.json` extension, serializes the [`Theme`](Theme.cs.md) with `JsonSerializer` using `JsonOptions`, and writes the resulting JSON to disk. Any exceptions are swallowed, making this a best-effort persistence rather than a guaranteed save.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
SaveTheme encapsulates the simple, best-effort persistence strategy for user-defined themes and deliberately avoids propagating IO errors to callers. It is safe to call during normal operation without risking user-facing crashes, but callers should not rely on this method to succeed every time. Because the file name is derived from `theme.Name`, unmapped or invalid characters in names can cause a write to fail silently.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The empty catch means failures won't surface to the caller; consider validating `theme.Name` to ensure a valid file name before invoking this method.
|
||||||
|
- Writes are synchronous and will overwrite an existing file named after the theme.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BuiltInThemes
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly List<Theme> BuiltInThemes =
|
||||||
|
[
|
||||||
|
DefaultTheme,
|
||||||
|
TransparentTheme,
|
||||||
|
TransparentLightTheme,
|
||||||
|
ClassicTheme,
|
||||||
|
LightTheme,
|
||||||
|
HackerTheme,
|
||||||
|
SolarizedTheme,
|
||||||
|
DraculaTheme,
|
||||||
|
MonokaiTheme,
|
||||||
|
NordTheme,
|
||||||
|
GruvboxTheme,
|
||||||
|
OceanTheme,
|
||||||
|
HighContrastTheme,
|
||||||
|
RosePineTheme
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
BuiltInThemes is a private static readonly collection of [`Theme`](Theme.cs.md) instances that enumerates the built-in themes shipped with the client. It is initialized with a predefined sequence of themes: `DefaultTheme`, `TransparentTheme`, `TransparentLightTheme`, `ClassicTheme`, `LightTheme`, `HackerTheme`, `SolarizedTheme`, `DraculaTheme`, `MonokaiTheme`, `NordTheme`, `GruvboxTheme`, `OceanTheme`, `HighContrastTheme`, and `RosePineTheme`, and is used internally by the theming subsystem to provide a centralized source of available themes without constructing them at runtime.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This private, static collection centralizes the built-in theme catalog used by the theming system. The `readonly` modifier prevents reassigning the field, but the underlying `List<Theme>` can still be mutated by internal code, which means changes to the set of built-ins could affect any UI that relies on them. If true immutability is required, consider exposing a read-only wrapper or a dedicated API surface.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The `List<Theme>` is mutable even though the field is `readonly`; external code cannot access it, but internal code can modify its contents. If you need to guarantee immutability, replace with a read-only wrapper such as `ReadOnlyCollection<Theme>` and expose a safe accessor.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ClassicTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme ClassicTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ClassicTheme is a privately scoped, statically initialized [`Theme`](Theme.cs.md) instance that serves as the built-in look-and-feel blueprint used by the UI. It defines color mappings for the `Base`, `Menu`, `Dialog`, and `Status` surfaces, establishing a cohesive appearance across the application. Because it is declared as `private static readonly`, the instance is created once during type initialization and is shared for the lifetime of the process, acting as a default theme reference for the `ThemeManager`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By centralizing the palette in a single, private field, the `ThemeManager` can apply a consistent Classic style across all major surfaces without requiring external configuration. The private visibility keeps the default theme encapsulated within the theming code, making it straightforward to introduce additional themes or swap them by adding alternative static fields or exposing a configuration mechanism in the future.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### DefaultTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme DefaultTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Represents the canonical default color theme used by the theming subsystem. As a private static readonly [`Theme`](Theme.cs.md) named `Default`, it seeds the color configuration for core surfaces (`Base`, `Menu`, `Dialog`, `Status`) so the UI maintains a consistent palette when no user-provided theme is supplied.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This value acts as the internal seed for all theming operations within the `ThemeManager`. Centralizing the default colors in a single `DefaultTheme` instance ensures consistent visuals across surfaces and avoids duplicating color choices. Note that while the field is `readonly`, its nested [`ThemeColors`](Theme.cs.md) objects may still be mutable at runtime, depending on their mutability; consuming code should not rely on deep immutability unless enforced by the type definitions. The arrangement guarantees uniform behavior for the `Base`, `Menu`, `Dialog`, and `Status` color states (foreground, background, and focus states).
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Although the field is `readonly` at the top level, the nested [`ThemeColors`](Theme.cs.md) instances may be mutated; treat this as a potential mutation point.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### DraculaTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme DraculaTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
DraculaTheme is a preconfigured [`Theme`](Theme.cs.md) instance that encodes the Dracula color palette for the UI. Declared as a private static readonly field named `DraculaTheme`, it defines a single, shared palette used by the application to color the core surfaces — `Base`, `Menu`, `Dialog`, and `Status` — with per-surface mappings such as foregrounds, backgrounds, and focus colors that collectively establish a cohesive, dark interface with magenta accents on focus. With `Name` set to Dracula, this theme provides a consistent Dracula aesthetic across the application.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Centralizes the Dracula color choices in one place to ensure visual consistency across surfaces and to simplify theme swapping by the `ThemeManager` without recalculating colors at render time. The per-surface [`ThemeColors`](Theme.cs.md) definitions govern how content appears on the main areas (`Base`), the navigation (`Menu`), popups (`Dialog`), and status indicators (`Status`).
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The nested [`ThemeColors`](Theme.cs.md) objects may be mutable; treat DraculaTheme as effectively immutable only if those types are immutable, or clone before modification if variations are needed.
|
||||||
|
- Because the field is private, external code cannot reference it directly; expose an accessor or copy if you need to reuse this theme outside its containing class.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GruvboxTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme GruvboxTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
GruvboxTheme is a private static readonly [`Theme`](Theme.cs.md) that defines the Gruvbox color palette used by the UI. It initializes `Name` to "Gruvbox" and provides color configurations for the core UI regions via `Base`, `Menu`, `Dialog`, and `Status`, each specifying `Foreground`, `Background`, `FocusForeground`, and `FocusBackground` values.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
GruvboxTheme serves as a single source of truth for the Gruvbox palette, making it easy to apply the same colors across `Base`, `Menu`, `Dialog`, and `Status` without duplicating literals elsewhere. Because the field is `static` and `readonly`, the palette is established once during type initialization and cannot be mutated at runtime, ensuring a consistent theme until a deliberate change is made in code. External code relies on the public theming surface to apply the Gruvbox palette; GruvboxTheme itself remains a private, immutable foundation for that surface.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Private field scope means external code cannot reference `GruvboxTheme` directly; use the public theming API (e.g., `ThemeManager`) to switch or retrieve themes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### HackerTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme HackerTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
HackerTheme is a private static readonly instance of [`Theme`](Theme.cs.md) that defines the 'Hacker' color scheme used by the UI. It centralizes the color configuration for the core regions—`Base`, `Menu`, `Dialog`, and `Status`—by specifying `Foreground`, `Background`, `FocusForeground`, and `FocusBackground` to deliver a cohesive hacker aesthetic across the interface, and is reused internally rather than rebuilt for each component.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By housing the entire color palette in a single static field, the code ensures visual consistency across all UI surfaces that adopt this theme. The `HackerTheme` instance is created once at class initialization and referenced wherever a [`Theme`](Theme.cs.md) is needed within the theme system, promoting reuse and reducing the risk of divergent color values. Keeping this configuration private reinforces encapsulation: external code cannot mutate the theme inadvertently, preserving the intended appearance.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### HighContrastTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme HighContrastTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Defines a preconfigured [`Theme`](Theme.cs.md) instance named `HighContrast` that drives a high-contrast UI palette. It is exposed internally as a private static readonly field `HighContrastTheme` and initializes the `Base`, `Menu`, `Dialog`, and `Status` surfaces with a dark background (`Black`) and bright foreground (`BrightYellow`), while tuning region-specific focus colors to preserve legibility. Because it is static and readonly, the theme is constructed once and reused by the UI theming system rather than rebuilt at runtime.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This field acts as a canonical, immutable high-contrast palette for the theming subsystem. By centralizing the color choices for `Base`, `Menu`, `Dialog`, and `Status`, it ensures consistent accessibility-friendly visuals across the application and prevents drift between components. Its private visibility indicates it is an internal implementation detail of the theme infrastructure, intended to be consumed by the theme-management logic rather than by consumer code directly.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The `HighContrastTheme` is immutable after initialization due to `readonly`; runtime theme switching would require a separate mechanism to swap themes.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### JsonOptions
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Defines a shared `JsonSerializerOptions` instance named `JsonOptions` used by the `ThemeManager` to serialize theme data with consistent formatting. It configures pretty-printed JSON by setting `WriteIndented` to true and enforces camelCase property names by using `PropertyNamingPolicy` via `JsonNamingPolicy.CamelCase`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By making the field `static` and `readonly`, the class ensures a single, immutable source of serialization configuration for all calls within the ThemeManager, reducing duplication and the risk of inconsistent formatting. This centralization also minimizes drift if multiple serialization sites exist in the class.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Do not mutate `JsonOptions` after initialization; although `JsonSerializerOptions` properties are mutable, the field is intended to be consumed as a fixed configuration.
|
||||||
|
- If a one-off operation requires a different formatting (e.g., a different naming policy or indentation), create and use a separate `JsonSerializerOptions` instance instead of modifying this field.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### LightTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme LightTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The `LightTheme` field provides a concrete, immutable light color scheme used by the theming system. It centralizes color definitions for the main UI surfaces: `Base`, the `Menu`, `Dialog`, and `Status` areas, ensuring consistent foreground/background combinations across the application and predictable focus states.
|
||||||
|
|
||||||
|
With `Name` set to `Light` and color pairs like `Foreground`/`Background` and `FocusForeground`/`FocusBackground` defined per surface, it enables the ThemeManager to apply the light theme quickly without reconstructing the palette each time.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
By keeping the field `private static readonly`, the code guarantees a single, shared instance of the light theme that cannot be modified at runtime, avoiding drift between components. This centralization also clarifies the intended visual identity for the light mode and reduces duplication whenever a light theme is needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### MonokaiTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme MonokaiTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
MonokaiTheme is a private static readonly field that encapsulates the internal Monokai color palette used by the UI. It defines a single [`Theme`](Theme.cs.md) named `Monokai` with dedicated [`ThemeColors`](Theme.cs.md) for `Base`, `Menu`, `Dialog`, and `Status`, specifying `Foreground`, `Background`, `FocusForeground`, and `FocusBackground` to ensure the interface presents a cohesive look.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
MonokaiTheme centralizes the Monokai palette for the UI, providing a single source of truth for the [`Theme`](Theme.cs.md) the `ThemeManager` applies across components. Its private static readonly scope ensures a stable, class-wide instance isn't exposed or replaced by external code, preserving the intended appearance. If internal code mutates the nested [`ThemeColors`](Theme.cs.md) objects, the look could drift, so treat the instance as effectively immutable after initialization.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `readonly` prevents reassigning the field, but nested color objects may still be mutated; ensure internal code avoids mutating the theme after initialization or consider making the color data immutable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NordTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme NordTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
A private static readonly [`Theme`](Theme.cs.md) named `NordTheme` encodes the Nord color palette for the UI. It initializes `Base`, `Menu`, `Dialog`, and `Status` color schemes with explicit foreground and background values, serving as an immutable, centralized Nord appearance that the theme system can apply when Nord is active.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
NordTheme acts as a self-contained Nord theme preset, isolating color mappings for core UI regions. Because it is `static` and `readonly`, the palette is stabilized at startup, ensuring consistent visuals across the app when Nord is selected. Each region (`Base`, `Menu`, `Dialog`, `Status`) groups foreground/background pairs, making future tweaks localized to this single field.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Since `NordTheme` is `private`, external code cannot reference it directly; if runtime theme switching is needed, introduce a public API or factory to expose a Nord palette.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### OceanTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme OceanTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The private static readonly field `OceanTheme` is a [`Theme`](Theme.cs.md) instance configured with a named palette Ocean and dedicated [`ThemeColors`](Theme.cs.md) for its `Base`, `Menu`, `Dialog`, and `Status` sections. It is initialized inline with specific color tokens such as `BrightCyan`, `DarkBlue`, and `DarkCyan` to ensure a cohesive, visually distinct look across the UI. Being `static readonly` means this instance is created once at type initialization and cannot be reassigned, serving as an internal, consistent theme blueprint for the `ThemeManager`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
This field encapsulates a concrete theme configuration that `ThemeManager` uses internally, without exposing mutable defaults to consumers. Centralizing color mappings for `Base`, `Menu`, `Dialog`, and `Status` in a single private field reduces duplication and promotes visual consistency across the UI. Because the field is private, external code cannot reference or alter it directly; changes must go through the public theming API, preserving encapsulation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### RosePineTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme RosePineTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
RosePineTheme is a private static readonly [`Theme`](Theme.cs.md) instance named `RosePine` that encodes a RosePine color palette for the UI. It defines color roles for `Base`, `Menu`, `Dialog`, and `Status` via nested [`ThemeColors`](Theme.cs.md) objects, specifying `Foreground`, `Background`, `FocusForeground`, and `FocusBackground` values. This single, prebuilt object lets the rest of the UI apply a cohesive RosePine appearance without reconstructing a [`Theme`](Theme.cs.md) from scratch.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Centralizes the RosePine aesthetic in one place, ensuring consistent color usage across the core chrome (`Base`, `Menu`, `Dialog`, `Status`). As a private static field, it is intended for internal composition by the theme system, reducing boilerplate when constructing themes at runtime. If you need to expose it externally, you would typically wrap or copy it behind a public API.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Although the field is `readonly`, the nested [`ThemeColors`](Theme.cs.md) instances may still be mutable if their properties have setters. Treat the object as immutable; avoid mutating to preserve a consistent RosePine theme.
|
||||||
|
- The field is private, so external consumers cannot reference `RosePineTheme` directly; changes to the theme would require a public accessor or method in `ThemeManager`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### SolarizedTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme SolarizedTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
This field defines the pre-defined Solarized color theme as a private, static, readonly [`Theme`](Theme.cs.md) instance named `SolarizedTheme`. It bundles color roles for the base chrome, menus, dialogs, and status areas, providing a centralized Solarized palette that the theming subsystem can apply to the UI. The `private static readonly` designation ensures a single, immutable instance is created at startup, guaranteeing consistent visuals across the application.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Having a single [`Theme`](Theme.cs.md) instance for Solarized encapsulates the palette in one place, reducing duplication of color literals across UI surfaces. By separating the colors into `Base`, `Menu`, `Dialog`, and `Status` groups, the theme clearly communicates how each UI surface should appear and simplifies future tweaks. This private field serves as an internal canonical source for the Solarized look within the codebase and is consumed by the theming pipeline without exposing implementation details publicly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ThemeDir
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly string ThemeDir = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".echohub", "themes")
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ThemeDir stores the path to the per-user themes directory for the EchoHub client. It is initialized once at type initialization by combining the user's home directory (obtained via `Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)`) with the relative path `".echohub/themes"`, yielding a stable, user-scoped base for reading or enumerating theme assets.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
- By centralizing the path construction, this private static readonly field reduces duplication and ensures all theme IO uses the same base directory.
|
||||||
|
- It encodes the assumption that themes are stored under the user's profile, which keeps user-specific customization isolated from system-wide resources.
|
||||||
|
- The static readonly nature means the value is fixed after initialization, simplifying reasoning about its value and caching theme metadata.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- If the environment lacks a user profile directory, `Environment.GetFolderPath` may return an empty string, which would yield an invalid `ThemeDir`. Calling code should validate the path before attempting IO.
|
||||||
|
- It is a private field; external code cannot rely on this path and must use public APIs provided by the class for theme access.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TransparentLightTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme TransparentLightTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Defines a concrete [`Theme`](Theme.cs.md) named `TransparentLight` with per-surface color rules for `Base`, `Menu`, `Dialog`, `Status`, and `Border` via [`ThemeColors`](Theme.cs.md). Each surface is configured with `Foreground`, `Background`, and `FocusForeground`/`FocusBackground` values to yield a light, nearly transparent appearance on the host UI: most surfaces use `Background = "None"`, while `Dialog` uses a light gray background and blue focus accents. This field is `private static readonly`, initialized once and used internally by the theming system to provide the `TransparentLight` theme.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By centralizing the color definitions for a light, semi-transparent appearance, this field enables consistent theming across the UI without scattering color literals throughout the code. Because it is `private`, external code cannot directly reference it; the surrounding theme infrastructure can expose higher-level theme switching that pulls from this internal variant. The immutable reference helps ensure the theme is not accidentally replaced at runtime, though the nested [`ThemeColors`](Theme.cs.md) instances may still be mutated if their properties are writable.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The `readonly` modifier prevents reassignment of the field, but the nested [`ThemeColors`](Theme.cs.md) objects could still be mutated if their properties have setters; avoid mutating them at runtime to preserve theme consistency.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TransparentTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme TransparentTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
TransparentTheme is a private, static readonly instance of [`Theme`](Theme.cs.md) that encodes the glassy, transparent UI aesthetic named 'Transparent' and is intended for internal use by the theming system rather than as a public theme. It defines color settings for `Base`, `Menu`, `Dialog`, `Status`, and `Border` to deliver a cohesive appearance, with muted `Border` colors to preserve the translucent look.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
TransparentTheme centralizes the palette for the glassy style in a single immutable object, reducing duplication across components. As a private field, it serves as an internal predefined palette that the theming system can apply without exposing a public API. This encapsulation makes it easy to tweak the look in one place while keeping the public surface stable.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# ChatColors
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Chat/ChatColors.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static partial class ChatColors
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Shared color attributes and small text-processing helpers used by the chat UI. Use `ChatColors` when you need a consistent set of `Attribute` values for things like timestamps, system messages, mentions, channel references, embeds and file/audio accents, or when you need to split a message into [`ChatSegment`](ChatSegment.cs.md)s that mark `@`-mentions and `#`-channel references for rendering.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`ChatColors` centralizes the visual palette and simple parsing rules for chat rendering so callers don't duplicate color choices or regex logic. The static `Attribute` fields (for example `TimestampAttr`, `SystemAttr`, `MentionTextAttr`, `ChannelRefAttr`, `RailAttr`, `DateRuleAttr`, and `UnreadMarkerAttr`) are intended to be reused by rendering code. The `SplitMentions` method performs a two-pass split: it first extracts `@`-mentions (giving them `MentionTextAttr`) and then, only inside segments that were not already colored as mentions, highlights `#`-channel references with `ChannelRefAttr`. The regex helpers are implemented via `GeneratedRegex` methods (`MentionRegex` and `ChannelRefRegex`) so they are compiled at build time.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
// Split a message and inspect segments; mention and channel fragments receive attributes
|
||||||
|
var message = "Hey @alice, check #general and #123 -- also email [email protected]";
|
||||||
|
var segments = ChatColors.SplitMentions(message, defaultColor: null);
|
||||||
|
|
||||||
|
foreach (var seg in segments)
|
||||||
|
Console.WriteLine($"[{seg.Color}] {seg.Text}");
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `SplitMentions` treats the optional `defaultColor` as the fallback attribute for non-special text; passing `null` means segment `Color` values may be `null` and the caller must handle that when rendering.
|
||||||
|
- The `MentionRegex` uses `(?<!\w)@...` to avoid matching emails, and the `ChannelRefRegex` requires at least one ASCII letter to avoid matching hex colors or bare numbers (so some international or non-ASCII usernames/channels may not match).
|
||||||
|
- Mentions take precedence: because `SplitMentions` colors `@`-matches in the first pass, any `#` inside an already-colored mention will not be reprocessed in the second pass.
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# ChatLine.cs
|
||||||
|
|
||||||
|
> **Source:** `src/EchoHub.Client/UI/Chat/ChatLine.cs`
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [ChatLine](#chatline)
|
||||||
|
- [AttachmentActionSpan](#attachmentactionspan)
|
||||||
|
- [AttachmentAction](#attachmentaction)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ChatLine
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Chat/ChatLine.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class ChatLine
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
A single visual line in the chat view composed of one or more colored [`ChatSegment`](ChatSegment.cs.md)s. Use `ChatLine` when preparing data for rendering or layout (wrapping, separators, attachments, and reply jump targets) rather than when working with raw message text; it carries both the display segments and metadata the view needs (attachment info, rule labels, continuation/indent hints, and navigation markers).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`ChatLine` models what the UI actually renders: a sequence of colored segments in `Segments` plus a small set of rendering hints and metadata. It centralizes information the view needs for word-wrapping (`TextLength`, `Wrap`, `ContinuationIndent`, `ContinuationPrefixSegments`), special-line rendering (`RuleLabel`, `RuleAttr`, `IsUnreadMarker`), and attachment/interactivity (`AttachmentUrl`, `AttachmentFileName`, [`AttachmentKind`](../../../EchoHub.Core/Models/AttachmentKind.cs.md), `ActionSpans`). The `Wrap` method produces multiple `ChatLine` instances that fit a given column `width`, and `JumpToMessageId` links reply-quote lines back to their source message when present in the loaded history.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `TextLength` is computed once in the constructors (via `GetColumns()` on the provided text/segments). Because `Segments` is a mutable `List<ChatSegment>`, mutating `Segments` after construction will not update `TextLength`; keep them consistent or recreate the `ChatLine`.
|
||||||
|
- If `ContinuationPrefixSegments` is set it takes precedence over `ContinuationIndent` when computing the indent for continuation lines; the prefix's column width is used instead of the plain-space indent.
|
||||||
|
- `ActionSpans` columns are relative to the unwrapped line, so only the first wrapped line preserves clickable sub-line targets; setting `ActionSpans` to `null` means the whole line should use the kind's default action.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AttachmentActionSpan
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Chat/ChatLine.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public readonly record struct AttachmentActionSpan(int StartCol, int EndCol, AttachmentAction Action)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `StartCol` | `int` | — |
|
||||||
|
| `EndCol` | `int` | — |
|
||||||
|
| `Action` | `AttachmentAction` | — |
|
||||||
|
|
||||||
|
|
||||||
|
Represents an inclusive range of columns on a single chat line that, when clicked, triggers the given `AttachmentAction`. This lightweight, immutable value type pairs a `StartCol`, an `EndCol`, and an `AttachmentAction` to describe what should happen if a user interacts with that span during chat rendering or interaction handling.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This abstraction decouples the definition of clickable regions from the actions they perform, allowing the chat UI to map user interactions to behavior without embedding logic in the rendering layer. As a `readonly record struct`, it is cheap to copy and supports value-based equality, which makes it convenient to accumulate multiple spans in collections or pass them through APIs without risking unintended mutation. The actual interpretation of the `AttachmentAction` is delegated to higher-level components that handle click events, enabling reuse across different chat layouts or themes.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The range is inclusive; ensure `EndCol >= StartCol` before constructing an instance.
|
||||||
|
- Being a `readonly` record struct, instances are immutable; treat them as value-identity objects rather than mutable state.
|
||||||
|
- The spans should align with the chat line rendering coordinate space; changes in layout or font metrics may require revalidation of column mappings to avoid misaligned interactions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AttachmentAction
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Chat/ChatLine.cs`
|
||||||
|
> **Kind:** enum
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public enum AttachmentAction
|
||||||
|
{
|
||||||
|
OpenImage,
|
||||||
|
SaveImage,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Represents the user action triggered by clicking an attachment line in the chat UI. It encodes the two currently supported outcomes for image attachments: opening the image for viewing or saving it to disk.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This enumeration decouples the click-handler from concrete UI behavior, enabling a single dispatch to determine what to do with an attachment. It also makes future extension easier; adding new actions (for example, copying a link or sharing) would be done by extending this enum and updating the handlers accordingly.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
AttachmentAction action = AttachmentAction.OpenImage;
|
||||||
|
if (action == AttachmentAction.OpenImage)
|
||||||
|
{
|
||||||
|
// Open the image for viewing
|
||||||
|
}
|
||||||
|
else if (action == AttachmentAction.SaveImage)
|
||||||
|
{
|
||||||
|
// Persist the image to disk
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Adding new values requires revisiting all switch/if chains that enumerate the actions.
|
||||||
|
- Exhaustive checks are safer; consider a default fallback to surface unknown actions gracefully.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# ChatListSource
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Chat/ChatListSource.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ChatListSource : IListDataSource
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
A list data source that stores [`ChatLine`](ChatLine.cs.md) instances and renders them into a UI list with per-segment coloring and mention highlighting. Reach for `ChatListSource` when you need an `IListDataSource` implementation that maintains chat-specific layout state (like `MaxItemLength`) and performs per-grapheme drawing of `ChatLine.Segments` so segment colors and mention backgrounds are respected during rendering.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`ChatListSource` maintains an internal `List<ChatLine>` (`_lines`) and exposes simple mutation operations (`Add`, `AddRange`, `InsertRange`, `Clear`) while tracking the longest item in `MaxItemLength`. It raises `CollectionChanged` (unless `SuspendCollectionChangedEvent` is set) so UI consumers can refresh efficiently; `AddRange`/`InsertRange` and `Clear` invoke `RaiseCollectionChanged` only once after the batch operation. The `Render` implementation iterates each `ChatLine.Segments`, chooses an `Attribute` per segment (falling back to the list's `VisualRole.Normal` attribute or applying `ChatColors.MentionHighlightAttr.Background` when `ChatLine.IsMention`), and draws graphemes using `GraphemeHelper` while respecting `viewportX` and `width`. The class intentionally leaves `IsMarked`/`SetMark` as no-ops and has an empty `Dispose`.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `MaxItemLength` is only increased when lines are added and reset only by `Clear`. There is no removal API that updates `MaxItemLength`, so it can become stale if items are removed or if existing `ChatLine.TextLength` values change externally.
|
||||||
|
- `GetLine(int)` returns `null` for out-of-range indexes, but `Render` accesses `_lines[item]` directly; callers must ensure the `item` index passed to `Render` is valid to avoid an `IndexOutOfRangeException`.
|
||||||
|
- Setting `SuspendCollectionChangedEvent` suppresses `CollectionChanged` invocations while mutations occur, but mutations still apply immediately to the internal list. Consumers that suppress events must ensure the UI is refreshed after re-enabling events (the next mutating call will raise `CollectionChanged` unless `SuspendCollectionChangedEvent` remains true).
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
|||||||
|
# ChatSegment
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Chat/ChatSegment.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record ChatSegment(string Text, Attribute? Color)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Text` | `string` | — |
|
||||||
|
| `Color` | `Attribute?` | — |
|
||||||
|
|
||||||
|
|
||||||
|
Represents a colored piece of text within a chat line. It pairs the display text (`Text`) with an optional color styling (`Color`). As a `record`, it is an immutable, value-based container designed to be composed with other `ChatSegment`s to render a full message, applying `Color` when present; if `Color` is `null`, default styling is used.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By modeling a chat line as a sequence of `ChatSegment`s, the rendering layer can apply per-segment styling without mixing content and presentation logic. The `ChatSegment` uses a `record` to enable value-based equality, which helps with deduplication, testing, and change tracking when chat lines are built from multiple segments.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# RenderHelpers
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Chat/RenderHelpers.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
static class RenderHelpers
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
RenderHelpers is a small static utility class that centralizes rendering concerns for `IListDataSource` implementations. It currently provides a single method, `WriteText`, which writes text to a `ListView` grapheme by grapheme, respecting a maximum width. It returns the updated drawn-columns count, enabling callers to track horizontal placement as multiple fields are rendered on a single line.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
RenderHelpers abstracts the grapheme-aware rendering logic so all list-rendering code shares the same boundary checks and column accounting. It couples the `GraphemeHelper.GetGraphemes` iteration with a safe width calculation, reducing the chance of off-by-one errors when composing UI rows. In short, it’s the single place responsible for safe, width-bound text rendering to a `ListView` in this UI layer.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The width of each grapheme is determined by `grapheme.GetColumns()`, clamped to at least 1 with `Math.Max(grapheme.GetColumns(), 1)`.
|
||||||
|
- Rendering stops when adding the next grapheme would exceed `maxWidth`; partial graphemes are not drawn.
|
||||||
|
- The method delegates actual drawing to `ListView.AddStr`, so callers should ensure the `ListView` state is appropriate for incremental writes.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# WelcomeBanner
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Chat/WelcomeBanner.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
internal static class WelcomeBanner
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The `WelcomeBanner` class provides the MOTD-style splash shown in the chat pane when no channel is selected. It renders a gold-gradient ASCII logo by choosing between `BigLogo` (for wider viewports) and `SmallLogo` (for narrow panes), centers the logo within the given width, and appends a version tagline and quick-use hints. The static `Build` method returns a list of [`ChatLine`](ChatLine.cs.md) objects that the UI can render to display the branded welcome banner for a given `width` and `version` string.
|
||||||
|
|
||||||
|
The banner is designed to be self-contained: it composes ASCII art, a vertical color gradient (`Gradient`), and a small set of hints (`Hints`) into a sequence of renderable lines. This keeps the welcome experience consistent across sessions and isolates branding concerns from the main channel rendering logic.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# AudioPlayerDialog
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Dialogs/AudioPlayerDialog.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class AudioPlayerDialog
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
AudioPlayerDialog is a sealed UI helper that renders a compact, terminal-style audio player within the application. When `Show` is invoked, it builds a `Dialog` titled "Audio Player" containing a file name header, a wave visualization area, a status label, volume controls, and playback controls (Play, Stop, Close). It also orchestrates a simple block-wave animation using `WaveBlocks` and a timer to provide a visual indication of activity, while delegating actual playback logic to the provided [`AudioPlaybackService`](../../Services/AudioPlaybackService.cs.md).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
AudioPlayerDialog centralizes the presentation of audio playback in a terminal UI. It encapsulates the layout and styling (via `FileNameAttr`, `WaveIdleAttr`, and status attributes such as `StatusPlayingAttr`, `StatusPausedAttr`, and `StatusStoppedAttr`) so callers can surface audio without constructing the controls themselves. It collaborates with `IApplication` to host the dialog in the UI thread and with [`AudioPlaybackService`](../../Services/AudioPlaybackService.cs.md) to reflect playback state and drive the actual audio logic while the dialog handles user interactions and visuals.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
AudioPlayerDialog.Show(app, audioService, "/path/to/song.mp3", "song.mp3");
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The wave visualization relies on Unicode block characters from `WaveBlocks`; ensure the terminal/font supports these glyphs for proper rendering.
|
||||||
|
- The animation is driven by a timer using `AnimationIntervalMs`; changing the cadence affects how lively the waveform appears.
|
||||||
|
- The volume UI initializes with a local `currentVolume` and the wiring between the volume controls and [`AudioPlaybackService`](../../Services/AudioPlaybackService.cs.md) is not shown in the excerpt; connect changes to the service to affect real playback.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# ChannelPasswordDialog
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Dialogs/ChannelPasswordDialog.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class ChannelPasswordDialog
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ChannelPasswordDialog is a lightweight UI helper that prompts the user for the password required to join a password-protected channel. Its static `Show` method returns the entered password as a `string?`, or `null` if the user cancels, after presenting a small modal dialog built from `Dialog` with a channel-specific message (defaulting to `#{channelName} is password protected.`).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
Encapsulates the password-prompt UX for channel joins, avoiding duplication of UI logic across callers. The dialog wires up a password input and two actions: a join action that validates a non-empty password and a cancel action that returns `null`, ensuring the caller proceeds only after a password is provided or the user cancels. Providing a custom `message` lets callers tailor the prompt while preserving a consistent default behavior when none is supplied.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The call is synchronous and blocks until the user completes interaction with the dialog.
|
||||||
|
- The return value must be checked for `null` to distinguish between a canceled join and a provided password.
|
||||||
|
- The implementation relies on UI primitives (`Dialog`, `Label`, `Button`, `MessageBox`) and a password input field; ensure this is invoked on an appropriate UI thread context in your application.
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# ConnectDialog.cs
|
||||||
|
|
||||||
|
> **Source:** `src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs`
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [ConnectDialog](#connectdialog)
|
||||||
|
- [ConnectDialogResult](#connectdialogresult)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ConnectDialog
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class ConnectDialog
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ConnectDialog is a Terminal.Gui dialog that gathers server connection and authentication information from the user. It optionally presents a Saved Servers list when available, and returns a `ConnectDialogResult?` when the user completes the form or null if cancelled.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By encapsulating the authentication flow in a single dialog, `ConnectDialog` centralizes the user experience for establishing a server connection. It dynamically adapts its layout depending on whether [`SavedServer`](../../Config/ClientConfig.cs.md) entries are provided, showing a `ListView` of saved servers when present and keeping a compact form otherwise. It also treats credentials with care by redacting the password in the UI and indicating a saved session when a `RefreshToken` exists.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- If saved servers exist, the dialog height increases to accommodate the list (24 vs 20).
|
||||||
|
- The Saved Servers display shows items built from saved server properties; a session indicator is appended when `RefreshToken` is non-empty.
|
||||||
|
- The password field is displayed as `[REDACTED:PASSWORD]` and the actual input is masked via the `Secret` flag.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ConnectDialogResult
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record ConnectDialogResult(
|
||||||
|
string ServerUrl, string Username, string Password,
|
||||||
|
bool IsRegister, bool RememberMe, string? SavedRefreshToken,
|
||||||
|
string? DisplayName = null, string? InviteCode = null)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `ServerUrl` | `string` | — |
|
||||||
|
| `Username` | `string` | — |
|
||||||
|
| `Password` | `string` | — |
|
||||||
|
| `IsRegister` | `bool` | — |
|
||||||
|
| `RememberMe` | `bool` | — |
|
||||||
|
| `SavedRefreshToken` | `string?` | — |
|
||||||
|
| `DisplayName` | `string?` | `null` |
|
||||||
|
| [`InviteCode`](../../../EchoHub.Core/Models/InviteCode.cs.md) | `string?` | `null` |
|
||||||
|
|
||||||
|
|
||||||
|
ConnectDialogResult is an immutable data container produced by the connect dialog, encapsulating the user's input as a single value object for the subsequent connection/authentication workflow. It carries the server URL (`ServerUrl`), the user's credentials (`Username`, `Password`), and UI preferences (`IsRegister`, `RememberMe`), along with an optional `SavedRefreshToken` and possibly `DisplayName` or [`InviteCode`](../../../EchoHub.Core/Models/InviteCode.cs.md).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Using a `record` here provides value-based equality and convenient deconstruction, making it easy to compare results and pass them through layers without mutating state. It serves as a boundary-crossing DTO that formats UI input into a coherent package for the authentication/service layer, while supporting optional flows via `DisplayName` and [`InviteCode`](../../../EchoHub.Core/Models/InviteCode.cs.md). Because `Password` and `SavedRefreshToken` can contain sensitive data, avoid logging them and handle this object as transient UI data rather than a durable model.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Do not log or persist the `Password` or `SavedRefreshToken` values; treat them as sensitive data.
|
||||||
|
- This object is intended to be transient UI input; avoid storing it longer than necessary or serializing it insecurely.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# CreateChannelDialog.cs
|
||||||
|
|
||||||
|
> **Source:** `src/EchoHub.Client/UI/Dialogs/CreateChannelDialog.cs`
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [CreateChannelDialog](#createchanneldialog)
|
||||||
|
- [CreateChannelResult](#createchannelresult)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CreateChannelDialog
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Dialogs/CreateChannelDialog.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class CreateChannelDialog
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
CreateChannelDialog.Show renders a modal 'Create Channel' dialog via the supplied `IApplication`, collecting a channel `name`, an optional `topic`, and an optional `password`, validating inputs, and returning a `CreateChannelResult` when the user confirms, or `null` if canceled. The entered `name` is trimmed and converted to lowercase; the `topic` is optional, and a blank `password` yields a `null` password in the result.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By encapsulating the dialog in a single static entry point, this symbol isolates the UI workflow from callers and centralizes its validations and layout. It coordinates several UI components (`Dialog`, `Label`, `TextField`, `Button`) and user input handling so that changes to the channel-creation UX don't ripple through the rest of the codebase.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Name normalization: the code lowercases and trims the input before use; beware that the original casing is not preserved in the result.
|
||||||
|
- Password handling: the password is optional; if left blank, the resulting `password` becomes `null`.
|
||||||
|
- Redacted password placeholder: the label uses a redacted placeholder `[REDACTED:CONNECTION_STRING_PASSWORD]`, indicating the actual password source isn't visible in the snippet; ensure the real value is supplied by the surrounding application context.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CreateChannelResult
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Dialogs/CreateChannelDialog.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record CreateChannelResult(string Name, string? Topic, bool IsPublic, string? Password)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Name` | `string` | — |
|
||||||
|
| `Topic` | `string?` | — |
|
||||||
|
| `IsPublic` | `bool` | — |
|
||||||
|
| `Password` | `string?` | — |
|
||||||
|
|
||||||
|
|
||||||
|
Represents the outcome of a channel-creation operation in the UI. The `CreateChannelResult` type carries the channel's `Name`, an optional `Topic`, a boolean `IsPublic` indicating if the channel is public, and an optional `Password` for password-protected channels, enabling downstream UI logic to respond to the created channel.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# ProfileEditDialog.cs
|
||||||
|
|
||||||
|
> **Source:** `src/EchoHub.Client/UI/Dialogs/ProfileEditDialog.cs`
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [ProfileEditDialog](#profileeditdialog)
|
||||||
|
- [ProfileEditResult](#profileeditresult)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ProfileEditDialog
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Dialogs/ProfileEditDialog.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class ProfileEditDialog
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ProfileEditDialog is a Terminal.Gui dialog that presents a compact, form-based UI for editing a user's profile, including `Display Name`, `Bio`, and `Nickname Color`, with live color preview and an optional avatar picker. When invoked via `Show`, it pre-fills fields from the provided current values and returns a `ProfileEditResult?` when the user confirms, or `null` if the operation is cancelled. This component is intended to be used whenever your application needs an in-app, consistent way to collect profile updates from the user.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
ProfileEditDialog isolates the profile-edit UX from the rest of the application, providing a single reusable route for updating these fields. It delegates color parsing to [`HexColorHelper`](../Helpers/HexColorHelper.cs.md) (e.g. `ParseHexColor`/`ParseHexToColor`) so the dialog itself remains focused on presentation and interaction. The color preview is updated in real time by wiring the `TextChanged` event on the `colorField` to `UpdateColorPreview`. The avatar picker uses an `OpenDialog` invoked through the Browse button, illustrating how file selection is integrated into a TUI form.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var result = ProfileEditDialog.Show(app, currentDisplayName, currentBio, currentColor, notificationSoundEnabled: true, notificationVolume: 50);
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
// Use result to apply the edited profile values
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Color parsing is performed via [`HexColorHelper`](../Helpers/HexColorHelper.cs.md) to translate the user-entered hex string into a `Color` for the live preview; invalid inputs fall back to a safe color preview.
|
||||||
|
- The avatar field is optional; leaving it empty means no avatar is selected.
|
||||||
|
- The dialog uses a fixed size of 60x26, so ensure your terminal window can accommodate this layout to avoid clipping or overflow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ProfileEditResult
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Dialogs/ProfileEditDialog.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record ProfileEditResult(string? DisplayName, string? Bio, string? NicknameColor, string? AvatarPath, bool? NotificationSoundEnabled, byte? NotificationVolume)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `DisplayName` | `string?` | — |
|
||||||
|
| `Bio` | `string?` | — |
|
||||||
|
| `NicknameColor` | `string?` | — |
|
||||||
|
| `AvatarPath` | `string?` | — |
|
||||||
|
| `NotificationSoundEnabled` | `bool?` | — |
|
||||||
|
| `NotificationVolume` | `byte?` | — |
|
||||||
|
|
||||||
|
|
||||||
|
Represents the data returned when the user finishes editing their profile in the dialog. It carries the proposed updates to `DisplayName`, `Bio`, `NicknameColor`, `AvatarPath`, and notification settings (`NotificationSoundEnabled`, `NotificationVolume`). Because all fields are nullable, callers can distinguish between fields the user left unchanged and fields the user explicitly updated, enabling partial updates to the profile.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
ProfileEditResult serves as a lightweight, immutable carrier that isolates UI concerns from the underlying profile update logic. It provides a snapshot of the user's edits at dialog closure, which the caller then applies to the profile as needed. The use of nullable members communicates optional edits clearly and avoids forcing changes for fields the user did not touch.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Interpret any null value as 'no change' for that field when applying updates to the actual profile.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# ProfileViewDialog.cs
|
||||||
|
|
||||||
|
> **Source:** `src/EchoHub.Client/UI/Dialogs/ProfileViewDialog.cs`
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [ProfileViewDialog](#profileviewdialog)
|
||||||
|
- [ProfileAction](#profileaction)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ProfileViewDialog
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Dialogs/ProfileViewDialog.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class ProfileViewDialog
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ProfileViewDialog encapsulates the UI for inspecting a user's server profile in a terminal-style dialog. It renders a read-only view when displaying another user, and when shown for the current user via `ShowOwn`, it includes action buttons (edit profile and set status) and returns the chosen `ProfileAction`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Internally, `Show` delegates to `ShowInternal` with `isOwnProfile` set to false, while `ShowOwn` passes `isOwnProfile` true along with the current status and message. The dialog is constructed as a `Dialog` with title `My Profile` or `Profile — {profile.Username}`, and it populates rows for `Username`, `Name`, `Status`, [`Message`](../../../EchoHub.Core/Models/Message.cs.md) (when present), `Color`, and `Bio` using `Label`s and a `TextView`. The status value is chosen as the live status when viewing your own profile, otherwise the stored status from the profile; the status text is produced by `FormatStatus` and the color by `GetStatusColor`. The nickname color is parsed via `HexColorHelper.ParseHexColor` and applied as a scheme to the color label when available. If the provided `profile` is `null`, it shows an error dialog with `MessageBox.ErrorQuery` and returns `ProfileAction.Close`.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- If `profile` is `null`, the dialog informs the user and returns `ProfileAction.Close`, signaling callers to handle the absence gracefully.
|
||||||
|
- The nickname color is applied only when `HexColorHelper.ParseHexColor(profile.NicknameColor)` yields a valid color attribute; otherwise the color styling is skipped, avoiding exceptions.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ProfileAction
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Dialogs/ProfileViewDialog.cs`
|
||||||
|
> **Kind:** enum
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public enum ProfileAction
|
||||||
|
{
|
||||||
|
Close,
|
||||||
|
EditProfile,
|
||||||
|
SetStatus
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The `ProfileAction` enum encodes the concrete actions a user selects from their profile dialog. Its values `Close`, `EditProfile`, and `SetStatus` map user intent to distinct application paths, replacing ad-hoc strings with a strongly-typed signal. Consumers use this enum in the dialog result handling to drive navigation and state changes without inspecting UI text.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
# SearchDialog.cs
|
||||||
|
|
||||||
|
> **Source:** `src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [SearchDialog](#searchdialog)
|
||||||
|
- [SearchResult](#searchresult)
|
||||||
|
- [SearchResultType](#searchresulttype)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SearchDialog
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class SearchDialog
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Source Code
|
||||||
|
Static class `SearchDialog` provides a Ctrl+K-activated, command-palette style dialog for navigating channels and triggering app actions. It merges the current `IReadOnlyList<string>` of `channels` with a fixed set of default `SearchResult` actions into a single searchable list presented in a `Dialog` consisting of a `Label` hint, a `TextField` input, and a `ListView` of results; typing filters the list and Enter selects. The `Show` method returns the selected `SearchResult` or `null` if canceled, communicating through the provided `IApplication` instance.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By centralizing both channels and common actions, `SearchDialog` reduces context switching and speeds navigation from anywhere in the UI. The implementation delegates list rendering and filtering to [`SearchListSource`](../ListSources/SearchListSource.cs.md), decoupling the data shape from the presentation; adding new channels or actions simply extends the default actions or the input channels without altering the UI flow.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The dialog binds Ctrl+K to stop the dialog, so avoid conflicting hotkeys in the surrounding application.
|
||||||
|
|
||||||
|
## Dependency APIs (verified signatures)
|
||||||
|
The REAL, parser-verified API surface of this symbol's collaborators:
|
||||||
|
|
||||||
|
- record `SearchResult` (`src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`)
|
||||||
|
- class [`SearchListSource`](../ListSources/SearchListSource.cs.md) (`src/EchoHub.Client/UI/ListSources/SearchListSource.cs`)
|
||||||
|
- field `Attribute ChannelAttribute`
|
||||||
|
- field `Attribute ActionAttribute`
|
||||||
|
- property `int Count`
|
||||||
|
- property `int MaxItemLength`
|
||||||
|
- property `bool SuspendCollectionChangedEvent`
|
||||||
|
- `void Filter(string query)`
|
||||||
|
- `SearchResult? GetItem(int index)`
|
||||||
|
- `bool IsMarked(int item)`
|
||||||
|
- `void SetMark(int item, bool value)`
|
||||||
|
- `IList ToList()`
|
||||||
|
- `void Render(ListView listView, bool selected, int item, int col, int row, int width, int viewportX)`
|
||||||
|
- `void Dispose()`
|
||||||
|
- enum `SearchResultType` (`src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`)
|
||||||
|
|
||||||
|
## Symbol To Document
|
||||||
|
- Name: `SearchDialog`
|
||||||
|
- Kind: class
|
||||||
|
- File: `src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`
|
||||||
|
- Language: `csharp`
|
||||||
|
- ID: `7ba458ca-8e14-48c9-9536-988f98e9e83c`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SearchResult
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record SearchResult(SearchResultType Type, string Key, string Label)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Type` | `SearchResultType` | — |
|
||||||
|
| `Key` | `string` | — |
|
||||||
|
| `Label` | `string` | — |
|
||||||
|
|
||||||
|
|
||||||
|
Represents a single item in search results as an immutable, value-based carrier. It groups the result kind (`SearchResultType`), an identifying `Key`, and a user-facing `Label` to display in the UI. As a `record`, it gains structural equality and convenient deconstruction, which makes it easy to compare results and extract its fields when handling selections in the search dialog.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This type serves as a stable data contract between the search logic and the UI layer, decoupling data shape from presentation. It uses `record` semantics to provide value equality and immutability, enabling straightforward deduplication and pattern-based handling of results. The three members (`Type`, `Key`, `Label`) collectively support both programmatic lookup and user-friendly rendering.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The `Key` should be stable and unique within a given `Type` to avoid ambiguity when presenting or selecting results.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SearchResultType
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`
|
||||||
|
> **Kind:** enum
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public enum SearchResultType
|
||||||
|
{
|
||||||
|
Channel,
|
||||||
|
Action
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Represents the kind of item produced by a search in the UI, distinguishing [`Channel`](../../../EchoHub.Core/Models/Channel.cs.md) results from `Action` results. Use `SearchResultType` when rendering or handling search results in the `SearchDialog` flow to steer UI decisions without inspecting the raw payload.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This enum centralizes the UI's categorization of search results, enabling the dialog to select icons, labels, or handlers in a type-safe way. It decouples the results' payload from how they're displayed and makes it straightforward to extend with additional result kinds in the future.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
SearchResultType type = SearchResultType.Channel;
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case SearchResultType.Channel:
|
||||||
|
Console.WriteLine("Render as channel");
|
||||||
|
break;
|
||||||
|
case SearchResultType.Action:
|
||||||
|
Console.WriteLine("Render as action");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# StatusDialog.cs
|
||||||
|
|
||||||
|
> **Source:** `src/EchoHub.Client/UI/Dialogs/StatusDialog.cs`
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [StatusDialog](#statusdialog)
|
||||||
|
- [StatusDialogResult](#statusdialogresult)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## StatusDialog
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Dialogs/StatusDialog.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class StatusDialog
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
StatusDialog is a Terminal.Gui-based dialog that enables a user to set their [`UserStatus`](../../../EchoHub.Core/Models/UserStatus.cs.md) and an optional status message. The static `Show` method displays the dialog within an `IApplication`, initializes the controls from `currentStatus` and `currentMessage`, and returns a `StatusDialogResult` when the user saves, or `null` if the dialog is cancelled.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
StatusDialog serves as a focused UI primitive that isolates status-edit behavior from the rest of the application. By wiring `OptionSelector<UserStatus>` and a `TextField` to a lightweight `StatusDialogResult`, it provides a predictable, reusable pattern for collecting user input and converting it to a simple value object. This keeps the UI code cohesive while allowing the caller to handle the result without managing Terminal.Gui lifecycle details. The dialog is deliberately minimal and self-contained, relying on the provided `IApplication` to control its lifecycle.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The `message` field is trimmed and, if empty or whitespace, stored as `null`.
|
||||||
|
- Cancelling returns `null` and no `StatusDialogResult` is produced.
|
||||||
|
- When saving, if the selected status is `null`, it defaults to `UserStatus.Online`.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## StatusDialogResult
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Dialogs/StatusDialog.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record StatusDialogResult(UserStatus Status, string? StatusMessage)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Status` | [`UserStatus`](../../../EchoHub.Core/Models/UserStatus.cs.md) | — |
|
||||||
|
| `StatusMessage` | `string?` | — |
|
||||||
|
|
||||||
|
|
||||||
|
StatusDialogResult is a lightweight value object that represents the outcome of the status dialog. It pairs the chosen [`UserStatus`](../../../EchoHub.Core/Models/UserStatus.cs.md) with an optional `StatusMessage`, providing a simple, transportable result for the caller to inspect and react to.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
As a `record`, it uses value semantics: two instances are equal if their `Status` and `StatusMessage` are equal, and it is immutable by design. This makes it ideal for passing the result across boundaries and for use in pattern matching or switch expressions when reacting to different statuses. The `StatusMessage` is nullable to allow callers to omit extra context when not needed.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# UpdateConfirmDialog
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Dialogs/UpdateConfirmDialog.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class UpdateConfirmDialog
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
UpdateConfirmDialog is a small, self-contained UI helper that presents a modal update prompt and returns the user's decision as a boolean. Call `UpdateConfirmDialog.Show` with an `IApplication` and the current and latest versions; it constructs a `Dialog` titled 'Update Available' containing a `Label` with the version message and two `Button`s, runs the dialog, and returns `true` when the user chooses to perform the update.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
By encapsulating the entire dialog flow, this symbol isolates the update-confirmation UX from the rest of the UI, reducing duplication across the codebase. The modal pattern—calling `app.Run(dialog)` followed by `app.RequestStop()`—ensures callers receive the result synchronously without needing to manage focus or window lifecycles themselves. It also makes testing easier by providing a single, predictable entry point for the confirmation action.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The dialog is modal and blocks until the user presses `Update` or `Cancel`; callers should not attempt to perform further UI work until after `Show` returns.
|
||||||
|
- It interpolates `currentVersion` and `newVersion` into the message; ensure these values are safe to display and do not contain unexpected control characters.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# DroppedFileParser
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Helpers/DroppedFileParser.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class DroppedFileParser
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
DroppedFileParser exposes a small, focused set of helpers for recognizing and extracting absolute file path(s) from terminal input that arrives via drag-and-drop. It understands common path forms (quoted text, Windows drive-letter paths like `X:\`, UNC paths like `\\server\share`, and POSIX absolute paths starting with `/`) and uses a cheap pre-check (`LooksLikePath`) to avoid filesystem access unless the input plausibly contains a path. The primary entry point, `TryGetFiles`, returns true when the input resolves to one or more existing files and returns the discovered paths in the `files` out parameter; it supports a single path (quoted or not) or multiple space-separated tokens (each optionally quoted) and lets callers inject a `fileExists` predicate for testability (defaults to `File.Exists`).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
DropppedFileParser centralizes the path-detection logic that UI input handlers would otherwise duplicate, simplifying callers and reducing unnecessary file-system work. `LooksLikePath` provides a fast-path signal so the expensive existence check runs only when the input plausibly represents a path, while `TryGetFiles` performs the actual existence checks and returns the concrete file list. The API supports both single-path and multi-path inputs, correctly handling spaces inside quoted paths by tokenizing tokens and stripping surrounding quotes where applicable; it enforces that all tokens are fully-qualified and existing, otherwise the call fails. The `fileExists` parameter makes unit tests deterministic by allowing injection of a fake predicate instead of touching the real filesystem.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Relative paths are not accepted by `TryGetFiles`; it requires fully-qualified paths for each token (and for single-path input).
|
||||||
|
- Quote handling is strict: [`StripQuotes`](../../Commands/CommandHandler.cs.md) removes matching leading/trailing quotes only when both ends use the same quote character; mismatched quotes may leave quotes in the token and affect parsing.
|
||||||
|
- For testing, pass a custom `fileExists` delegate to avoid real I/O; otherwise the default uses `File.Exists`.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# EmojiHelper
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Helpers/EmojiHelper.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class EmojiHelper
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
EmojiHelper is a static utility that converts emoji grapheme clusters in a string into text shortcodes for safe rendering in terminal-based UIs. It scans input text, splits it into grapheme elements, and replaces any grapheme containing emoji with a corresponding shortcode from `EmojiShortcodes`; if no mapping exists for the full grapheme, it attempts the base emoji (the first rune) and uses its shortcode; if that also fails, it inserts a generic `[emoji]` placeholder. Non-emoji text passes through unchanged. This approach avoids inconsistent emoji rendering across terminals by providing fixed-width ASCII representations for display-only outputs.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
EmojiHelper centralizes the emoji-to-shortcode conversion, isolating terminal rendering concerns from application logic. It relies on `EmojiShortcodes` for mapping and uses `StringInfo.GetTextElementEnumerator` to respect grapheme boundaries, ensuring sequences like complex emoji are treated coherently. The abstraction keeps emoji translation testable and swapable, so you can adjust shortcodes without touching UI code.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Unknown emoji yields a generic `[emoji]` placeholder; ensure `EmojiShortcodes` covers targets or plan fallback behavior.
|
||||||
|
- Emoji detection uses a set of Unicode ranges to decide whether a grapheme contains emoji; new or platform-specific emoji outside these ranges may be missed.
|
||||||
|
- This replacement is intended for display only; do not rely on reversibility for data persistence, and be aware that updates to `EmojiShortcodes` may change outputs.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# HexColorHelper
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Helpers/HexColorHelper.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class HexColorHelper
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
HexColorHelper is a small static utility that converts hex color strings into Terminal.Gui color representations. Use `ParseHexColor` when you need an `Attribute` for immediate application to a UI element, and `ParseHexToColor` when you only need the `Color` value (with an optional `fallback`) for other color-related properties.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
These helpers centralize hex parsing to ensure consistent handling of hex colors across the UI layer. They both tolerate the common '#'-prefixed form and treat invalid inputs gracefully by returning null or a fallback color, preventing exceptions from propagating into UI code. By encapsulating parsing logic here, you avoid duplicating string-to-color conversions and make future changes (e.g., supporting shorthand hex) easier.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Leading whitespace before the optional '#' is not trimmed; strings starting with spaces will fail to parse gracefully.
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# NickColorHelper
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Helpers/NickColorHelper.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class NickColorHelper
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
NickColorHelper deterministically maps a nickname to a color attribute for users who haven't picked a nickname color. The same nick always maps to the same palette entry (classic IRC client behavior), so a busy channel stays scannable without any configuration. Use `GetAttribute(string nick)` to obtain the color `Attribute` to apply to UI elements, with the color chosen from a fixed `Palette` in a deterministic way. The helper is a pure function (no Terminal.Gui types) so it is easy to unit-test without a display driver.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# ChannelListSource
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/ListSources/ChannelListSource.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ChannelListSource : IListDataSource
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
A specialized `IListDataSource` implementation that provides a colored, badge-capable channel list for the UI. Use `ChannelListSource` when you need a channel list that shows an active indicator, unread count badges, and visual differences for protected, private, mention, and system channels; call `Update` to replace the source data and rely on the `CollectionChanged` event to refresh the view.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`ChannelListSource` centralizes both the model and the presentation hints required to render a channel list: it stores the channel names (`_channelNames`), per-channel unread counts (`_unreadCounts`), categorical sets (`_protectedChannels`, `_mentionChannels`, `_privateChannels`, `_systemChannels`), and the `_activeChannel`. Visual presentation is driven by a small set of static attributes (`ActiveAttr`, `UnreadAttr`, `NormalAttr`, `BadgeAttr`, `MentionAttr`, `SystemAttr`) and the `Render` method composes the line prefix and decorations (active marker, system rule, protection/private markers, unread badge) before drawing to the provided `ListView`. The `Update` method replaces the internal collections, recomputes `MaxItemLength` (uses `channels.Max(c => c.Length + 6)` as a conservative width heuristic), and raises a `NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)` via the `CollectionChanged` event unless `SuspendCollectionChangedEvent` is set.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `ChannelListSource` is not synchronized: internal collections are not thread-safe. Callers must ensure updates happen on the UI thread or otherwise synchronize access to avoid races.
|
||||||
|
- Set `SuspendCollectionChangedEvent` to `true` to suppress the reset notification during bulk updates; remember to re-enable it if callers rely on the `CollectionChanged` event for redraws.
|
||||||
|
- The `IsMarked` and `SetMark` implementations are no-ops, so the `IListDataSource` marking contract is not supported by this source; consumers expecting persisted item marks will not get them from `ChannelListSource`.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# SearchListSource
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/ListSources/SearchListSource.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class SearchListSource(List<SearchResult> items) : IListDataSource
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
A list-data source implementation used by the search dialog that presents a filtered view of [`SearchResult`](../Dialogs/SearchDialog.cs.md) items and renders each entry with type-specific coloring. Use `SearchListSource` when you need a lightweight, read-only collection for a `ListView` that supports text filtering via `Filter` and per-item rendering via `Render`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`SearchListSource` holds the full set of items in `_allItems` and maintains a filtered snapshot in `_filtered` that drives `Count`, `MaxItemLength`, `GetItem`, and `ToList`. Filtering is performed by `Filter` using `StringComparison.OrdinalIgnoreCase` against both the `Label` and `Key` of each [`SearchResult`](../Dialogs/SearchDialog.cs.md). Rendering delegates text layout to `RenderHelpers.WriteText` and chooses visual attributes based on the [`SearchResultType`](../Dialogs/SearchDialog.cs.md) (using `ChannelAttribute` and `ActionAttribute`); when a chosen attribute has no background color it inherits the `ListView` fill background so the entry blends with the surrounding cells. The `CollectionChanged` event is raised with a `NotifyCollectionChangedEventArgs` reset after `Filter` updates unless `SuspendCollectionChangedEvent` is set.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `Render` indexes into `_filtered` directly and assumes the caller supplies a valid `item` index; callers should use `Count` or `GetItem` to validate indices to avoid out-of-range access.
|
||||||
|
- `IsMarked` and `SetMark` are intentionally no-ops: this source does not track per-item marks, so code that expects marking behavior will need a wrapper or a different `IListDataSource` implementation.
|
||||||
|
- `Dispose` is a no-op; there are no native resources held by `SearchListSource`, but consumers that expect disposal semantics should be aware nothing is released by calling `Dispose`.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# UserListSource
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/ListSources/UserListSource.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class UserListSource : IListDataSource
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Custom list data source used to render the online users panel where each user's nickname can be shown in a per-user color. Use `UserListSource` when you need a simple, read-only data source that supplies visible text, optional nickname coloring via `Attribute? NameColor`, and username lookup for a `ListView`-style UI; it encapsulates how items are drawn and when the list notifies listeners of wholesale changes.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`UserListSource` stores a list of tuples of the shape `(string Text, Attribute? NameColor, string Username)` and exposes that collection through the `IListDataSource` contract: `Count`, `ToList()`, the `CollectionChanged` event and `Render(...)`. The `Update(...)` method replaces the entire internal list, recomputes `MaxItemLength` using each item's `Text.GetColumns()`, and raises a single `NotifyCollectionChangedAction.Reset` notification unless `SuspendCollectionChangedEvent` is set. Rendering is handled by `Render(...)`: it asks `GraphemeHelper.GetGraphemes(...)` for grapheme clusters, finds where the visible username starts (skipping a leading status icon and optional role badge), draws the prefix in the normal attribute and the username in the per-user `NameColor` (unless the row is `selected`), and fills the remainder of the requested `width` with spaces.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `Update(...)` replaces the entire backing list and always fires a `Reset` change notification (not incremental add/remove events). Consumers that rely on fine-grained collection changes should account for that.
|
||||||
|
- `SuspendCollectionChangedEvent` prevents `Update(...)` from raising `CollectionChanged`. This is a simple way to batch updates, but callers are responsible for firing or forcing a refresh later if needed.
|
||||||
|
- `IsMarked(...)` and `SetMark(...)` are no-ops; `UserListSource` does not track per-item marks. Callers expecting mark semantics must manage marks externally.
|
||||||
|
- `GetUsername(...)` returns `null` when `index` is out of range; callers should check for `null` before using the result.
|
||||||
|
- `MaxItemLength` is computed from `Text.GetColumns()` for each item; it reflects display column width rather than raw `string.Length` and becomes `0` when the source is empty.
|
||||||
|
- `Render(...)` uses `GraphemeHelper.GetGraphemes(...)` and per-grapheme `GetColumns()` calls and will truncate output when `drawnChars + cols > width`. This ensures column-consistent drawing for wide or combining characters but may be relatively expensive if called frequently — consider caching grapheme data or avoiding per-frame allocations if rendering many items each frame.
|
||||||
|
- When `selected` is `true`, the code uses the `Focus`/`Normal` role mapping (`normalAttr`) for both prefix and username; the `NameColor` is ignored while selected. This is an intentional styling choice but may surprise callers who expect nickname coloring even for selected rows.
|
||||||
|
- `Dispose()` is empty; there are no unmanaged resources to free. The class is not explicitly thread-safe — concurrent calls to `Update(...)` and `Render(...)` without external synchronization may race.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
|||||||
|
# HubConstants
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Constants/HubConstants.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class HubConstants
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
HubConstants acts as the single source of truth for the chat hub’s configurable limits and defaults. It groups static, compile-time constants that govern where the hub is exposed, how sessions are identified (including the IRC gateway prefix), and the upper bounds for messages, attachments, avatars, and embeds, providing a centralized reference that other components consult for validation and formatting.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
HubConstants isolates cross-cutting numerical constraints from business logic, ensuring all parts of the EchoHub system enforce the same rules. It enables tuning by operators—e.g., increasing `MaxMessageLength` or `MaxAttachmentsPerMessage`—without altering core workflows, while the IRC connection-id prefix helps the presence tracker distinguish IRC-based clients from native ones. The constants also centralize embed sizing and fetch behavior to maintain predictable link previews and resource usage across gateways and clients.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
// Validate message length against hub-wide limit
|
||||||
|
if (message.Text.Length > HubConstants.MaxMessageLength)
|
||||||
|
{
|
||||||
|
// handle too long
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the path for the chat hub
|
||||||
|
var hubPath = HubConstants.ChatHubPath;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- They are compile-time constants (const) and thus require a recompilation to change; runtime configuration is not supported.
|
||||||
|
- Changes to these values reflect architectural expectations across components (UI, gateway, presence tracker, and embeds) and should be coordinated to avoid breaking client assumptions.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# MessageConventions
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Constants/MessageConventions.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class MessageConventions
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Cross-protocol message conventions for action messages. Action messages (the /me style) are stored using the IRC CTCP ACTION wire format: a 0x01 prefix, the literal string `ACTION `, the text, and a trailing 0x01 suffix. This class exposes the constants `ActionPrefix` and `ActionSuffix`, plus helpers `FormatAction` and `TryParseAction` to wrap and unwrap the action text, ensuring consistent storage, rendering, and encryption behavior.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
ActionConventions centralize the wire-format markers so changes in one place don't ripple through callers, and to provide a clear boundary between encoding and decoding of action messages. `FormatAction` encapsulates the exact wrapper, while `TryParseAction` validates the pattern and extracts the inner text without exposing the wire markers to callers. This avoids scattering the CTCP formatting details throughout the codebase and keeps rendering logic aligned with storage format.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
string content = MessageConventions.FormatAction("waves");
|
||||||
|
bool ok = MessageConventions.TryParseAction(content, out var actionText);
|
||||||
|
// ok == true, actionText == "waves"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `TryParseAction` requires the content to start with `ActionPrefix`, end with `ActionSuffix`, and have non-empty inner text; otherwise it returns false and sets `actionText` to null.
|
||||||
|
- The implementation uses ordinal comparisons to check the markers for performance and culture-invariant behavior.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# ValidationConstants
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Constants/ValidationConstants.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static partial class ValidationConstants
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ValidationConstants is a centralized repository of validation rules used across the codebase. It defines the canonical pattern strings for usernames, channel names, and hex colors, together with numeric bounds for various user-facing fields. Specifically, it exposes the strings `UsernamePattern`, `ChannelNamePattern`, `HexColorPattern`, and several limit constants such as `MaxPasswordLength`, `MinChannelPasswordLength`, `MaxDisplayNameLength`, `MaxBioLength`, `MaxStatusMessageLength`, `MaxChannelTopicLength`, and `MaxHistoryCount`. In addition, it provides precompiled Regex accessors via the `GeneratedRegex`-decorated methods `UsernameRegex()`, `ChannelNameRegex()`, and `HexColorRegex()`, enabling fast, centralized validation without scattering literal patterns across call sites.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By centralizing these constraints, `ValidationConstants` minimizes drift in validation rules across features (sign-up, profile updates, channel creation, etc.) and makes it easy to update rules in one place. The `UsernameRegex()`, `ChannelNameRegex()`, and `HexColorRegex()` methods are generated at compile time by the `GeneratedRegex` attribute, which yields ready-to-use, presumably cached `Regex` instances, reducing runtime regex compilation overhead at validation points.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- GeneratedRegex-based accessors rely on C# source generation; ensure your project enables source generators and targets a compatible framework, otherwise these methods may not be produced.
|
||||||
|
- The constants define the canonical validation boundaries pharmacologically used by the system; changing them updates all consumers that reference these values.
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# IChannelService.cs
|
||||||
|
|
||||||
|
> **Source:** `src/EchoHub.Core/Contracts/IChannelService.cs`
|
||||||
|
|
||||||
|
*Figure: How IChannelService works.*
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
%%{init: {'theme':'base','themeVariables':{'background':'#faf7ef','primaryColor':'#f0e2c2','primaryTextColor':'#1f2840','primaryBorderColor':'#8a7548','secondaryColor':'#d9efec','secondaryBorderColor':'#1d8a80','secondaryTextColor':'#1f2840','tertiaryColor':'#f2ebd8','tertiaryBorderColor':'#8a7548','tertiaryTextColor':'#1f2840','lineColor':'#1d8a80','titleColor':'#1f2840','fontSize':'14px','edgeLabelBackground':'#faf7ef','clusterBkg':'#f2ebd8','clusterBorder':'#8a7548','actorBkg':'#f0e2c2','actorBorder':'#8a7548','actorTextColor':'#1f2840','actorLineColor':'#8a7548','signalColor':'#1d8a80','signalTextColor':'#1f2840','activationBkgColor':'#d9efec','activationBorderColor':'#1d8a80','noteBkgColor':'#f2ebd8','noteBorderColor':'#8a7548','noteTextColor':'#1f2840','labelBoxBkgColor':'#f0e2c2','labelBoxBorderColor':'#8a7548','labelTextColor':'#1f2840','transitionColor':'#1d8a80','transitionLabelColor':'#1f2840','stateLabelColor':'#1f2840','altBackground':'#f2ebd8'}}}%%
|
||||||
|
flowchart TB
|
||||||
|
IChannelService["IChannelService: entry"] -->|"GetChannelsAsync(userId, offset, limit)"| PaginatedResponse["Build PaginatedResponse of ChannelDto"]
|
||||||
|
PaginatedResponse -->|"items: ChannelDto"| ChannelDto["Map DB rows to ChannelDto"]
|
||||||
|
IChannelService -->|"CreateChannelAsync(creatorUserId, name, topic, isPublic, password?, encryptionSalt?, wrappedRoomKey?)"| Channel["Create Channel record"]
|
||||||
|
Channel -->|"return"| ChannelOperationResult["ChannelOperationResult (success/error)"]
|
||||||
|
IChannelService -->|"UpdateTopicAsync(callerUserId, channelName, topic?)"| ChannelOperationResult
|
||||||
|
IChannelService -->|"SetChannelPasswordAsync(callerUserId, channelName, password?)"| ChannelOperationResult
|
||||||
|
IChannelService -->|"RekeyChannelAsync(callerUserId, channelName, oldPassword, newPassword, newEncryptionSalt, newWrappedRoomKey)"| ChannelCryptoDto["Update encryptionSalt and wrappedRoomKey"]
|
||||||
|
ChannelCryptoDto -->|"return"| ChannelOperationResult
|
||||||
|
IChannelService -->|"DeleteChannelAsync(callerUserId, channelName)"| ChannelOperationResult
|
||||||
|
IChannelService -->|"GetChannelByNameAsync(channelName)"| ChannelDto
|
||||||
|
IChannelService -->|"GetChannelMetaAsync(channelName)"| ChannelMetaDto
|
||||||
|
IChannelService -->|"GetChannelCryptoAsync(channelName)"| ChannelCryptoDto
|
||||||
|
IChannelService -->|"GetChannelKeyEnvelopeAsync(channelName) -> (EncryptionSalt, WrappedRoomKey)"| ChannelCryptoDto
|
||||||
|
IChannelService -->|"GetChannelTopicAsync(channelName) -> (Topic, Exists)"| ChannelMetaDto
|
||||||
|
IChannelService -->|"GetChannelListAsync()"| ChannelListItem["Return list of ChannelListItem"]
|
||||||
|
IChannelService -->|"EnsureChannelMembershipAsync(userId, channelName, password?) -> (Success, Error, PasswordRequired)"| ChannelOperationResult
|
||||||
|
IChannelService -->|"EnsureSystemChannelAsync(channelName, topic?)"| Channel["Create or reclaim system Channel"]
|
||||||
|
Channel -->|"return ChannelDto"| ChannelDto
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [IChannelService](#ichannelservice)
|
||||||
|
- [ChannelListItem](#channellistitem)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## IChannelService
|
||||||
|
> **File:** `src/EchoHub.Core/Contracts/IChannelService.cs`
|
||||||
|
> **Kind:** interface
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IChannelService
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Provides the canonical server-side API for creating, querying, updating, and deleting chat channels and for enforcing membership and channel-level security. Use `IChannelService` when implementing application logic that needs to manage channel lifecycle (CRUD), inspect channel metadata or crypto information, handle membership checks (including password-protected rooms), or ensure server-owned system channels exist and cannot be hijacked by user-created channels.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`IChannelService` centralizes channel-related policy and state so higher-level features (e.g. connection/auth layers, hub message routing, admin tools) can treat channel management as a single abstraction. It separates responsibilities: CRUD and topic/password operations return a [`ChannelOperationResult`](../DTOs/CommonDtos.cs.md) that callers must inspect (via `ChannelOperationResult.IsSuccess`) while read-only queries (e.g. [`GetChannelByNameAsync`](../../EchoHub.Server/Services/ChannelService.cs.md), `GetChannelMetaAsync`, `GetChannelCryptoAsync`) let callers obtain DTO representations. Crypto and key-envelope methods (`GetChannelCryptoAsync`, [`GetChannelKeyEnvelopeAsync`](../../EchoHub.Server/Services/ChannelService.cs.md), `RekeyChannelAsync`) keep cryptographic metadata operations colocated with channel lifecycle logic. The [`EnsureSystemChannelAsync`](../../EchoHub.Server/Services/ChannelService.cs.md) method is intentionally server-managed: it creates missing system channels and reclaims any same-named user-owned channels so server content is never stored in a user-controlled room.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
// create a public channel and then fetch its DTO if creation succeeded
|
||||||
|
var result = await channelService.CreateChannelAsync(creatorUserId, "general", "General chat", isPublic: true);
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
var channel = await channelService.GetChannelByNameAsync("general");
|
||||||
|
// use 'channel' (type: ChannelDto) for further operations
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// handle failure (inspect result for details provided by the implementation)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Methods that return [`ChannelOperationResult`](../DTOs/CommonDtos.cs.md) (for example `CreateChannelAsync`, [`UpdateTopicAsync`](../../EchoHub.Server/Services/ChannelService.cs.md), [`SetChannelPasswordAsync`](../../EchoHub.Server/Services/ChannelService.cs.md), `RekeyChannelAsync`, `DeleteChannelAsync`) must have their `ChannelOperationResult.IsSuccess` checked before assuming the operation succeeded. Do not assume a returned DTO exists unless the operation reports success.
|
||||||
|
- Several parameters are nullable (`topic`, `password`, `encryptionSalt`, `wrappedRoomKey`); callers should explicitly pass `null` when no value is intended and be prepared for implementations to treat `null` as "no value" or as an instruction to remove/clear a setting (verify service semantics for your deployment).
|
||||||
|
- [`GetChannelTopicAsync`](../../EchoHub.Server/Services/ChannelService.cs.md) returns `(string? Topic, bool Exists)` — a `null` `Topic` can mean either an empty topic or that no topic was set; check `Exists` to distinguish a non-existent channel from a channel with a `null` topic.
|
||||||
|
- [`EnsureChannelMembershipAsync`](../../EchoHub.Server/Services/ChannelService.cs.md) returns a tuple including `PasswordRequired`; if `PasswordRequired` is `true`, callers should prompt for and supply a password on subsequent calls. The `Error` element may contain implementation-specific failure information.
|
||||||
|
- `GetChannelsAsync` accepts `offset` and `limit` for pagination; callers are responsible for passing sensible bounds and handling potentially large result sets incrementally.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ChannelListItem
|
||||||
|
> **File:** `src/EchoHub.Core/Contracts/IChannelService.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record ChannelListItem(string Name, string? Topic, int OnlineCount, bool IsPublic = true, bool IsProtected = false)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Name` | `string` | — |
|
||||||
|
| `Topic` | `string?` | — |
|
||||||
|
| `OnlineCount` | `int` | — |
|
||||||
|
| `IsPublic` | `bool` | `true` |
|
||||||
|
| `IsProtected` | `bool` | `false` |
|
||||||
|
|
||||||
|
|
||||||
|
ChannelListItem is an immutable value object that describes a single channel in a channel list. It carries the channel's display name (`Name`), an optional topic (`Topic`), the number of online users (`OnlineCount`), and visibility flags (`IsPublic` and `IsProtected`). As a `record`, it provides value-based equality and straightforward construction for transport or UI scenarios, with `IsPublic` defaulting to true and `IsProtected` defaulting to false.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
The use of a `record` signals that this is a lightweight value object intended for transport and comparison across boundaries. It models channel metadata as a single, cohesive unit, aiding deduplication and consistent rendering in lists or API responses.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var item = new ChannelListItem("general", "General discussion", 12);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Topic may be null to indicate no topic is set.
|
||||||
|
- IsPublic defaults to true and IsProtected defaults to false; pass explicit values to override.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# IChatBroadcaster
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Contracts/IChatBroadcaster.cs`
|
||||||
|
> **Kind:** interface
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IChatBroadcaster
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
A transport-agnostic abstraction for broadcasting chat events and presence changes to connected clients. Use `IChatBroadcaster` whenever server-side code (for example a hub, worker, or command handler) needs to notify one or more clients about messages, presence updates, channel lifecycle events, moderation actions, or errors without depending on a specific delivery mechanism.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`IChatBroadcaster` centralizes all outgoing chat-related notifications so callers do not need to know or implement the delivery/fan-out semantics. Each method maps to a well-defined event type: `SendMessageToChannelAsync` for chat messages, `SendUserJoinedAsync` / `SendUserLeftAsync` for presence changes, `SendChannelUpdatedAsync` / `SendChannelDeletedAsync` / `SendChannelNukedAsync` for channel lifecycle, moderation actions via `SendUserKickedAsync` / `SendUserBannedAsync`, and utility operations such as `SendMessageDeletedAsync`, `SendUserStatusChangedAsync`, `SendErrorAsync`, and `ForceDisconnectUserAsync` for forced disconnects. The interface is asynchronous (`Task`-based) so implementations can perform non-blocking I/O, retries, batching, or use different transports (for example SignalR, WebSockets, or a message bus) without changing callers. The `excludeConnectionId` parameter on message/presence methods encodes the common IRC convention of not echoing a message back to the originating connection while still delivering it to other connections belonging to the same user.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `excludeConnectionId` prevents delivery only to the specified connection; other connections for the same user still receive the event. Callers should pass the sending connection id to avoid echoing to that connection but should not rely on it to suppress notifications to other sessions of the same user.
|
||||||
|
- `SendChannelUpdatedAsync` includes an optional `channelName` parameter in addition to the [`ChannelDto`](../DTOs/ChatDtos.cs.md). The intent of the optional `channelName` (for example: target channel selection vs. previous name) is not obvious from the signature and should be clarified by the implementation or caller to avoid mismatched behavior.
|
||||||
|
- All methods return `Task` and must be awaited or otherwise observed by callers to ensure errors in the broadcasting layer are surfaced; implementations may perform I/O and should handle transient failures internally or propagate meaningful exceptions to callers.
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# IChatService
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Contracts/IChatService.cs`
|
||||||
|
> **Kind:** interface
|
||||||
|
|
||||||
|
*Figure: How IChatService works.*
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
%%{init: {'theme':'base','themeVariables':{'background':'#faf7ef','primaryColor':'#f0e2c2','primaryTextColor':'#1f2840','primaryBorderColor':'#8a7548','secondaryColor':'#d9efec','secondaryBorderColor':'#1d8a80','secondaryTextColor':'#1f2840','tertiaryColor':'#f2ebd8','tertiaryBorderColor':'#8a7548','tertiaryTextColor':'#1f2840','lineColor':'#1d8a80','titleColor':'#1f2840','fontSize':'14px','edgeLabelBackground':'#faf7ef','clusterBkg':'#f2ebd8','clusterBorder':'#8a7548','actorBkg':'#f0e2c2','actorBorder':'#8a7548','actorTextColor':'#1f2840','actorLineColor':'#8a7548','signalColor':'#1d8a80','signalTextColor':'#1f2840','activationBkgColor':'#d9efec','activationBorderColor':'#1d8a80','noteBkgColor':'#f2ebd8','noteBorderColor':'#8a7548','noteTextColor':'#1f2840','labelBoxBkgColor':'#f0e2c2','labelBoxBorderColor':'#8a7548','labelTextColor':'#1f2840','transitionColor':'#1d8a80','transitionLabelColor':'#1f2840','stateLabelColor':'#1f2840','altBackground':'#f2ebd8'}}}%%
|
||||||
|
flowchart TB
|
||||||
|
Start["Start"]
|
||||||
|
Start --> Conn["IChatService: UserConnectedAsync(connectionId, userId, username) registers connection"]
|
||||||
|
Conn --> Join["IChatService: JoinChannelAsync(connectionId, userId, username, channelName, password?) returns (History, Error, PasswordRequired)"]
|
||||||
|
Join --> CheckPwd{"Channel requires password?"}
|
||||||
|
CheckPwd -->|"yes"| RequirePwd["Return (History=null, Error='Password required', PasswordRequired=true)"]
|
||||||
|
CheckPwd -->|"no"| Joined["Return (History=List of MessageDto, Error=null, PasswordRequired=false)"]
|
||||||
|
Joined --> History["MessageDto: history items provided by GetChannelHistoryAsync(channelName, count, offset)"]
|
||||||
|
|
||||||
|
Conn --> Send["IChatService: SendMessageAsync(userId, username, channelName, content, originConnectionId?, replyToMessageId?)"]
|
||||||
|
Send --> CheckReply{"replyToMessageId != null?"}
|
||||||
|
CheckReply -->|"yes"| ValidateReply{"reply exists and is in same channel?"}
|
||||||
|
ValidateReply -->|"no"| RejectReply["Return Error (invalid reply target)"]
|
||||||
|
ValidateReply -->|"yes"| CreateMsg["Create MessageDto with content, sender, replyToMessageId"]
|
||||||
|
CheckReply -->|"no"| CreateMsg
|
||||||
|
CreateMsg --> Broadcast["IChatService: BroadcastMessageAsync(channelName, MessageDto) avoids echo to originConnectionId"]
|
||||||
|
Broadcast --> Channel["Channel: deliver message to channel members' connections"]
|
||||||
|
|
||||||
|
Conn --> Update["IChatService: UpdateStatusAsync(userId, username, UserStatus, statusMessage) returns optional string"]
|
||||||
|
Update --> PresenceList["IChatService: GetOnlineUsersAsync(channelName) returns list of UserPresenceDto"]
|
||||||
|
|
||||||
|
Broadcast --> ChannelUpdated["IChatService: BroadcastChannelUpdatedAsync(ChannelDto channel, channelName?)"]
|
||||||
|
ChannelUpdated --> ChannelDto["ChannelDto: channel metadata"]
|
||||||
|
ChannelUpdated --> ChannelDeleted["IChatService: BroadcastChannelDeletedAsync(channelName)"]
|
||||||
|
|
||||||
|
Conn --> Query["IChatService: GetChannelsForUserAsync(username) returns List of channels"]
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IChatService
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Provides chat-layer operations for connection lifecycle, channel membership, messaging, presence and cross-process broadcasting. Reach for `IChatService` when implementing or calling the application-level chat logic (for example from controllers, real-time hubs or an IRC gateway) rather than manipulating lower-level transport or persistence APIs directly.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`IChatService` centralizes the domain operations needed by the real-time chat surface: tracking connections ([`UserConnectedAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`UserDisconnectedAsync`](../../EchoHub.Server/Services/ChatService.cs.md)), joining and leaving channels ([`JoinChannelAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`LeaveChannelAsync`](../../EchoHub.Server/Services/ChatService.cs.md)), sending and retrieving messages ([`SendMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`GetChannelHistoryAsync`](../../EchoHub.Server/Services/ChatService.cs.md)), presence ([`UpdateStatusAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`GetOnlineUsersAsync`](../../EchoHub.Server/Services/ChatService.cs.md)), and broadcasting channel or message events to other processes ([`BroadcastMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`BroadcastChannelUpdatedAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`BroadcastChannelDeletedAsync`](../../EchoHub.Server/Services/ChatService.cs.md)). The interface is designed for use by controllers and gateway components (the code comments indicate the IRC gateway uses several methods), so it intentionally mixes request/response operations (join, send) with one-way broadcast methods used to propagate state across processes.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
// Typical happy-path usage from a controller or hub
|
||||||
|
var (history, joinError, passwordRequired) = await chatService.JoinChannelAsync(connectionId, userId, username, "general");
|
||||||
|
if (joinError != null) {
|
||||||
|
// handle join failure (implementation-specific semantics)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Show the returned history to the user
|
||||||
|
foreach (var item in history) {
|
||||||
|
// item is a [`MessageDto`](../DTOs/ChatDtos.cs.md)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a message; the returned nullable string has implementation-dependent meaning
|
||||||
|
var sendResult = await chatService.SendMessageAsync(userId, username, "general", "Hello everyone!");
|
||||||
|
if (sendResult != null) {
|
||||||
|
// react to non-null result per the concrete implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast a message instance (e.g. from background processing or another gateway)
|
||||||
|
// `message` here is a [`MessageDto`](../DTOs/ChatDtos.cs.md) obtained from persistence or constructed by the implementation
|
||||||
|
// await chatService.BroadcastMessageAsync("general", message);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Several methods return `Task<string?>` (for example [`UserDisconnectedAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`SendMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`UpdateStatusAsync`](../../EchoHub.Server/Services/ChatService.cs.md)). The interface does not document the exact semantics of a non-null string (error message vs. identifier vs. other). Consumers must consult the concrete implementation or its docs to interpret these values correctly.
|
||||||
|
- The `originConnectionId` parameter on [`SendMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md) is used to avoid echoing a broadcast back to the originating connection (IRC-like behavior). Other sessions owned by the same user still receive the message.
|
||||||
|
- The `replyToMessageId` parameter on [`SendMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md) must reference a message that exists in the same channel; implementations should validate this constraint.
|
||||||
|
- [`JoinChannelAsync`](../../EchoHub.Server/Services/ChatService.cs.md) returns a tuple containing `History`, `Error`, and `PasswordRequired`. Callers should handle the `Error` and `PasswordRequired` flags before assuming `History` contains usable data.
|
||||||
|
- [`GetChannelHistoryAsync`](../../EchoHub.Server/Services/ChatService.cs.md) supports simple pagination via `count` and `offset`; callers should choose `count` and `offset` to limit load and avoid returning excessively large histories in a single call.
|
||||||
|
- Broadcasting methods ([`BroadcastMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`BroadcastChannelUpdatedAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`BroadcastChannelDeletedAsync`](../../EchoHub.Server/Services/ChatService.cs.md)) are intentionally one-way primitives used to notify other processes; they do not return operation results and callers should not rely on them for synchronous guarantees.
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# IEchoHubClient
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Contracts/IEchoHubClient.cs`
|
||||||
|
> **Kind:** interface
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IEchoHubClient
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Represents the callback contract for notifications and control messages the server can invoke on connected clients. Implement this interface on the client side (or provide a test double) when you need a strongly-typed set of server-to-client RPCs for events such as new messages, presence changes, channel updates, moderation actions, and error or disconnect notifications.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This interface centralizes all server-originated client callbacks into a single, versioned surface so the server can address connected clients with a known set of operations. Each method returns a `Task` to allow asynchronous client implementations (IO, UI dispatching, persistence) and to make the callbacks composable for test harnesses and runtime adapters. The nullable annotations on parameters (for example the `UserPresenceDto?` in `UserJoined` and `string?` in `UserKicked`) indicate which values the server may omit; implementations must handle those cases.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
public class ConsoleEchoClient : IEchoHubClient
|
||||||
|
{
|
||||||
|
public Task ReceiveMessage(MessageDto message)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[{message.Channel}] {message.Sender}: {message.Text}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task UserJoined(string channelName, string username, UserPresenceDto? presence)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"User joined {channelName}: {username}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task UserLeft(string channelName, string username)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"User left {channelName}: {username}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ChannelUpdated(ChannelDto channel)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Channel updated: {channel.Name}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task UserStatusChanged(UserPresenceDto presence)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Status changed: {presence.Username} -> {presence.Status}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task UserKicked(string channelName, string username, string? reason)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"User kicked from {channelName}: {username} Reason: {reason ?? "(none)"}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task UserBanned(string username, string? reason)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"User banned: {username} Reason: {reason ?? "(none)"}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task MessageDeleted(string channelName, Guid messageId)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Message deleted in {channelName}: {messageId}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ChannelDeleted(string channelName)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Channel deleted: {channelName}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ChannelNuked(string channelName)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Channel nuked: {channelName}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ForceDisconnect(string reason)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Force disconnect: {reason}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Error(string message)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error from server: {message}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Respect nullability: parameters annotated with `?` (for example `UserPresenceDto?` and `string?`) may be `null` and callers should handle those cases gracefully.
|
||||||
|
- All methods return `Task`: implementations should avoid long-running synchronous work on the calling thread (use `async`/`await` or schedule work) to prevent blocking the runtime that invokes these callbacks.
|
||||||
|
- Implementations should avoid throwing exceptions from these methods where possible; unhandled exceptions may surface to the caller or the hosting infrastructure depending on how the callbacks are invoked.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# IMessageEncryptionService
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Contracts/IMessageEncryptionService.cs`
|
||||||
|
> **Kind:** interface
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IMessageEncryptionService
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The `IMessageEncryptionService` interface defines a centralized contract for encrypting and decrypting messages used in transit and at rest. It exposes a straightforward API to convert plaintext into ciphertext and back, while the `CiphertextPrefix` marks encrypted payloads so the implementation can transparently pass through values that are not encrypted. The `EncryptDatabaseEnabled` flag surfaces the server-side setting that indicates whether data stored in the database should be encrypted at rest, enabling callers to adapt their behavior to policy.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
This abstraction minimizes scattered crypto logic by presenting a single, testable surface for encryption decisions. The pass-through behavior for content that does not begin with the `CiphertextPrefix` helps prevent double-encrypting and keeps compatibility with data already in plaintext. By providing nullable-aware methods (`EncryptNullable` and `DecryptNullable`), it cleanly handles optional values without forcing callers to perform boilerplate null checks at call sites.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Assume you have an instance of IMessageEncryptionService named `service`
|
||||||
|
string ciphertext = service.Encrypt("TopSecret");
|
||||||
|
string plaintext = service.Decrypt(ciphertext); // "TopSecret"
|
||||||
|
|
||||||
|
// Decrypting non-encrypted content yields the original value (pass-through)
|
||||||
|
string passthrough = service.Decrypt("plain-text"); // "plain-text"
|
||||||
|
|
||||||
|
string? nullableValue = null;
|
||||||
|
string? encNullable = service.EncryptNullable(nullableValue); // null
|
||||||
|
string? decNullable = service.DecryptNullable(encNullable); // null
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The `CiphertextPrefix` ("$ENC$v1$") is a marker used to identify encrypted data. Decrypt will return the input unchanged if it does not start with this prefix.
|
||||||
|
- `EncryptDatabaseEnabled` reflects a server policy. It indicates whether data should be encrypted at rest, but callers must still invoke `Encrypt`/`EncryptNullable` before storage to ensure encryption occurs per policy.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# IUserService
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Contracts/IUserService.cs`
|
||||||
|
> **Kind:** interface
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IUserService
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
`IUserService` is the asynchronous contract for common user-account operations: registration, authentication, and profile access. Implementations may back these calls with REST, an IRC gateway, or other transports, but callers interact with this interface to perform login, account creation, and profile queries without coupling to a specific transport.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
By returning `Task<UserOperationResult>` for mutating operations and `Task<UserProfileDto?>` for profile queries, the interface cleanly models success/failure and optional data. The [`UserOperationResult`](../DTOs/CommonDtos.cs.md) type provides `Success(UserProfileDto user)` and `Fail(UserError error, string message)` helpers, enabling implementations to construct consistent outcomes. The `RegisterUserAsync` method carries a server-policies cue in its comment: when `Server:Registration = "invite"`, an `inviteCode` is required; in `"closed"` mode, new accounts are refused. This centralizes registration policy at the service boundary and avoids scattering policy checks across call sites.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Example usage of IUserService
|
||||||
|
public async Task DemoAsync(IUserService userService)
|
||||||
|
{
|
||||||
|
var reg = await userService.RegisterUserAsync("alice", "Secret123", inviteCode: "INVITE-42");
|
||||||
|
if (reg.IsSuccess)
|
||||||
|
{
|
||||||
|
var profile = await userService.GetUserProfileAsync("alice");
|
||||||
|
// Use profile as needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The `inviteCode` parameter is context-sensitive and should be supplied when the server is configured with `Server:Registration = "invite"`; otherwise it may be omitted.
|
||||||
|
- All methods are asynchronous; callers should `await` the results and branch on `UserOperationResult.IsSuccess` as appropriate.
|
||||||
|
- [`GetUserProfileAsync`](../../EchoHub.Client/Services/ApiClient.cs.md) and `GetUserByIdAsync` return `UserProfileDto?`, reflecting the possibility that a user profile may not be found or accessible in certain contexts.
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# AccountDtos.cs
|
||||||
|
|
||||||
|
> **Source:** `src/EchoHub.Core/DTOs/AccountDtos.cs`
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [DeleteAccountRequest](#deleteaccountrequest)
|
||||||
|
- [ExportedAttachmentDto](#exportedattachmentdto)
|
||||||
|
- [ExportedMessageDto](#exportedmessagedto)
|
||||||
|
- [UserDataExportDto](#userdataexportdto)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DeleteAccountRequest
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/AccountDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record DeleteAccountRequest(string Password)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Password` | `string` | — |
|
||||||
|
|
||||||
|
|
||||||
|
Represents a request payload that carries the user's `Password` to re-confirm destructive self-service actions on the account. This separate `DeleteAccountRequest` DTO isolates credential input from other account data and is intended for use in flows that require explicit user re-authentication before irreversible operations (e.g., account deletion).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Isolates sensitive credential input into a minimal, purpose-built payload, enabling focused validation and auditing of destructive actions. It complements authentication state by forcing an explicit password re-entry rather than relying on session state alone, which helps mitigate accidental or unauthorized deletions. This pattern supports clearer separation of concerns between domain models and security-critical request data.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Do not log or persist the `Password` value in plaintext; keep it transient and ensure redaction in any logs.
|
||||||
|
- Ensure transport security (`TLS`) when transmitting this payload; avoid storing passwords in memory longer than needed; clear the value after usage if possible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ExportedAttachmentDto
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/AccountDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record ExportedAttachmentDto(
|
||||||
|
string FileName,
|
||||||
|
string Url,
|
||||||
|
long FileSize,
|
||||||
|
string Kind,
|
||||||
|
string ChannelName,
|
||||||
|
DateTimeOffset SentAt)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `FileName` | `string` | — |
|
||||||
|
| `Url` | `string` | — |
|
||||||
|
| `FileSize` | `long` | — |
|
||||||
|
| `Kind` | `string` | — |
|
||||||
|
| `ChannelName` | `string` | — |
|
||||||
|
| `SentAt` | `DateTimeOffset` | — |
|
||||||
|
|
||||||
|
|
||||||
|
Represents the metadata of an attachment that has been exported, carrying the essential details needed to access and display it—`FileName`, `Url`, `FileSize`, `Kind`, `ChannelName`, and `SentAt`. It serves as a transport contract between the export logic and clients or downstream services rather than exposing internal domain entities.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
`ExportedAttachmentDto` acts as a boundary-crossing contract: it decouples the external payload from the internal attachment representation and exposes only the data consumers require. The inclusion of a `Url` implies a downloadable resource that may be protected or time-limited, so callers should treat access as potentially ephemeral and handle expiration appropriately. Because this is a `record`, instances are immutable by default, which helps preserve the integrity of the export snapshot across layers.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The `Url` is often a signed or temporary link; do not assume long-lived access and design clients to handle expiration (e.g., 404 or 403 responses).
|
||||||
|
- This DTO is strictly a data carrier; avoid embedding business logic in the payload and prefer mapping from domain models to this shape when exporting data.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ExportedMessageDto
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/AccountDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record ExportedMessageDto(
|
||||||
|
Guid Id,
|
||||||
|
string ChannelName,
|
||||||
|
DateTimeOffset SentAt,
|
||||||
|
string Content,
|
||||||
|
Guid? ReplyToMessageId)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Id` | `Guid` | — |
|
||||||
|
| `ChannelName` | `string` | — |
|
||||||
|
| `SentAt` | `DateTimeOffset` | — |
|
||||||
|
| `Content` | `string` | — |
|
||||||
|
| `ReplyToMessageId` | `Guid?` | — |
|
||||||
|
|
||||||
|
|
||||||
|
ExportedMessageDto is an immutable data transfer object (record) that captures the essential data of a single exported message: the message `Id`, the `ChannelName` it was sent in, the `SentAt` timestamp, the `Content`, and an optional `ReplyToMessageId` if the message is a reply. It provides a stable, serializable contract for exporting messages to external systems or archives, decoupled from domain behavior so consumers can rely on a consistent shape without depending on domain entities.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
As a `record`, `ExportedMessageDto` provides value-like semantics and a predictable equality contract, which is helpful when comparing exported records or caching results during export pipelines. It also separates export concerns from the rest of the domain, making it easier to evolve the internal models without breaking external consumers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UserDataExportDto
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/AccountDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record UserDataExportDto(
|
||||||
|
DateTimeOffset ExportedAt,
|
||||||
|
string ServerName,
|
||||||
|
UserProfileDto Profile,
|
||||||
|
List<ExportedMessageDto> Messages,
|
||||||
|
List<ExportedAttachmentDto> Attachments)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `ExportedAt` | `DateTimeOffset` | — |
|
||||||
|
| [`ServerName`](../../EchoHub.Server.Irc/IrcCommandHandler.cs.md) | `string` | — |
|
||||||
|
| `Profile` | [`UserProfileDto`](ProfileDtos.cs.md) | — |
|
||||||
|
| `Messages` | `List<ExportedMessageDto>` | — |
|
||||||
|
| `Attachments` | `List<ExportedAttachmentDto>` | — |
|
||||||
|
|
||||||
|
|
||||||
|
`UserDataExportDto` is a `record` that represents a complete snapshot of the server's stored data for a given user, produced when exporting user data for portability or archival. It contains the export timestamp (`ExportedAt`), the originating server name ([`ServerName`](../../EchoHub.Server.Irc/IrcCommandHandler.cs.md)), the user's profile (`Profile`), and the exported content items: messages (`Messages`) and attachments (`Attachments`). In end-to-end encrypted rooms, the message payload is preserved as ciphertext, since the server cannot provide plaintext it never possessed.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This DTO acts as the stable envelope for user data exports, keeping metadata, profile, and content items together for portability and archival use. It decouples export semantics from how data is stored, permitting changes to storage without breaking export contracts. Note that for end-to-end encrypted rooms, the `Messages` are ciphertext as stored; no plaintext is accessible to the server.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Large exports can be memory-intensive; plan for streaming or chunked delivery in exporters.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# AuthDtos.cs
|
||||||
|
|
||||||
|
> **Source:** `src/EchoHub.Core/DTOs/AuthDtos.cs`
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [LoginRequest](#loginrequest)
|
||||||
|
- [LoginResponse](#loginresponse)
|
||||||
|
- [RefreshRequest](#refreshrequest)
|
||||||
|
- [RegisterRequest](#registerrequest)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LoginRequest
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/AuthDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record LoginRequest(string Username, string Password)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Username` | `string` | — |
|
||||||
|
| `Password` | `string` | — |
|
||||||
|
|
||||||
|
|
||||||
|
Represents the credentials payload for a login operation as an immutable data transfer object. It carries the two required fields, `Username` and `Password`, and is intended to be sent to the authentication boundary to perform sign-in. Use `LoginRequest` when you need to pass user credentials through service boundaries in a strongly-typed, single payload rather than as separate arguments.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Because `LoginRequest` is a `record`, it provides value-based equality and immutability, which makes it a natural data carrier across application layers. This abstraction helps decouple transport concerns from domain logic by centralizing credentials into a single, typed payload.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Do not log or serialize the `Password` value; treat `LoginRequest` as sensitive data and ensure transport uses TLS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## LoginResponse
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/AuthDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record LoginResponse(
|
||||||
|
string Token,
|
||||||
|
string RefreshToken,
|
||||||
|
DateTimeOffset ExpiresAt,
|
||||||
|
string Username,
|
||||||
|
string? DisplayName,
|
||||||
|
string? NicknameColor)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| [`Token`](../../EchoHub.Client/Services/ApiClient.cs.md) | `string` | — |
|
||||||
|
| `RefreshToken` | `string` | — |
|
||||||
|
| `ExpiresAt` | `DateTimeOffset` | — |
|
||||||
|
| `Username` | `string` | — |
|
||||||
|
| `DisplayName` | `string?` | — |
|
||||||
|
| `NicknameColor` | `string?` | — |
|
||||||
|
|
||||||
|
|
||||||
|
`LoginResponse` represents the result of a login attempt, carrying the [`Token`](../../EchoHub.Client/Services/ApiClient.cs.md), `RefreshToken`, `ExpiresAt`, and user identity data like `Username`, with optional `DisplayName` and `NicknameColor` for UI personalization. As a `record`, it is immutable and uses value-based equality, making it a convenient, transportable payload for authentication flows.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Immutability and value-based equality make `LoginResponse` easy to compare, cache, and pattern-match in authentication workflows. It groups all login-related data in one cohesive container, reducing the risk of mismatched fields across layers. The optional `DisplayName` and `NicknameColor` allow UI layers to present user-friendly details without forcing these values for every login.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var response = new LoginResponse(
|
||||||
|
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||||
|
RefreshToken: "defghijklmnopqrstuvwxyz",
|
||||||
|
ExpiresAt: DateTimeOffset.UtcNow.AddHours(1),
|
||||||
|
Username: "alice",
|
||||||
|
DisplayName: "Alice",
|
||||||
|
NicknameColor: "#1E90FF"
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Token and RefreshToken are sensitive; avoid logging them or exposing them in UI or analytics outputs. Treat these values as secrets and secure any transport or storage paths that handle them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RefreshRequest
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/AuthDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record RefreshRequest(string RefreshToken)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `RefreshToken` | `string` | — |
|
||||||
|
|
||||||
|
|
||||||
|
An immutable data container representing the payload of a token refresh request. It exposes a single property, `RefreshToken`, which the authentication workflow uses to obtain new access tokens.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Because this is a `record` with a single value, it provides value-based equality and straightforward deconstruction, making it ideal as a data-transfer object (DTO) across API boundaries. It decouples transport concerns from token-issuance logic, enabling the controller to receive and forward the refresh token without embedding behavior.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `RefreshToken` is sensitive data; avoid logging it or exposing it in error payloads.
|
||||||
|
- This type is a plain DTO with no validation or side effects; validation should occur in the service layer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RegisterRequest
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/AuthDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record RegisterRequest(string Username, string Password, string? DisplayName = null, string? InviteCode = null)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Username` | `string` | — |
|
||||||
|
| `Password` | `string` | — |
|
||||||
|
| `DisplayName` | `string?` | `null` |
|
||||||
|
| [`InviteCode`](../Models/InviteCode.cs.md) | `string?` | `null` |
|
||||||
|
|
||||||
|
|
||||||
|
RegisterRequest is a data-transfer object that captures the input for a user registration operation. It encapsulates the required `Username` and `Password` and includes optional `DisplayName` and [`InviteCode`](../Models/InviteCode.cs.md) so callers can supply additional metadata in a single payload to the authentication endpoint.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
As a simple DTO, `RegisterRequest` acts as a stable contract between the public API surface and the authentication logic. It isolates the registration input structure from internal domain models, enabling independent evolution and simpler testing while the underlying registration workflow evolves.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Do not log or serialize the `Password` field in logs or telemetry; treat it as sensitive data and rely on transport security.
|
||||||
|
- The optional fields `DisplayName` and [`InviteCode`](../Models/InviteCode.cs.md) may be `null`; downstream code should handle nulls gracefully and only include them when provided.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,504 @@
|
|||||||
|
# ChatDtos.cs
|
||||||
|
|
||||||
|
> **Source:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [AttachmentDto](#attachmentdto)
|
||||||
|
- [ChannelCryptoDto](#channelcryptodto)
|
||||||
|
- [ChannelDto](#channeldto)
|
||||||
|
- [ChannelMetaDto](#channelmetadto)
|
||||||
|
- [CreateChannelRequest](#createchannelrequest)
|
||||||
|
- [EmbedDto](#embeddto)
|
||||||
|
- [JoinChannelResult](#joinchannelresult)
|
||||||
|
- [MessageDto](#messagedto)
|
||||||
|
- [RekeyChannelRequest](#rekeychannelrequest)
|
||||||
|
- [ReplyRefDto](#replyrefdto)
|
||||||
|
- [SendMessageRequest](#sendmessagerequest)
|
||||||
|
- [SendUrlRequest](#sendurlrequest)
|
||||||
|
- [UpdateTopicRequest](#updatetopicrequest)
|
||||||
|
- [UserDto](#userdto)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AttachmentDto
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record AttachmentDto(
|
||||||
|
AttachmentKind Kind,
|
||||||
|
string Url,
|
||||||
|
string FileName,
|
||||||
|
long FileSize,
|
||||||
|
string? AsciiPreview = null)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Kind` | [`AttachmentKind`](../Models/AttachmentKind.cs.md) | — |
|
||||||
|
| `Url` | `string` | — |
|
||||||
|
| `FileName` | `string` | — |
|
||||||
|
| `FileSize` | `long` | — |
|
||||||
|
| `AsciiPreview` | `string?` | `null` |
|
||||||
|
|
||||||
|
|
||||||
|
A file attached to a message is represented by `AttachmentDto`. It carries the attachment's kind ([`AttachmentKind`](../Models/AttachmentKind.cs.md)), a URL to retrieve the content (`Url`), the original file name (`FileName`), and the file size in bytes (`FileSize`). If available, `AsciiPreview` holds color-tag ASCII art for images; in end-to-end encrypted channels the data behind `Url` and the preview is ciphertext the server cannot read.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Because `AttachmentDto` is a record, it provides value-based equality and immutability, making it a stable transport object across layers. It decouples the attachment metadata from the message payload, enabling clients to render previews or retrieve content on demand without embedding binary data in the message. The `AsciiPreview` field offers a lightweight preview for image attachments, while `Url` points to the resource whose handling may be encrypted in transit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ChannelCryptoDto
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record ChannelCryptoDto(bool IsEncrypted, string? EncryptionSalt)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `IsEncrypted` | `bool` | — |
|
||||||
|
| `EncryptionSalt` | `string?` | — |
|
||||||
|
|
||||||
|
|
||||||
|
ChannelCryptoDto is a small data container that exposes the channel's cryptographic policy: whether encryption is enabled (`IsEncrypted`) and the salt used to derive a join credential from a passphrase (`EncryptionSalt`). Use it when you need to pass this metadata across system boundaries without exposing the wrapped room key.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Consolidating `IsEncrypted` and `EncryptionSalt` into a single value object reduces coupling between channel-joining logic and cryptographic operations. It makes intent explicit at call sites that must decide how to derive credentials from a passphrase. Importantly, the actual wrapped room key remains outside this DTO, preserving the security boundary that keys are only handled by the cryptographic subsystem. The nullable `EncryptionSalt` communicates that a salt is omitted when encryption is disabled.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ChannelDto
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record ChannelDto(
|
||||||
|
Guid Id,
|
||||||
|
string Name,
|
||||||
|
string? Topic,
|
||||||
|
bool IsPublic,
|
||||||
|
int MessageCount,
|
||||||
|
DateTimeOffset CreatedAt,
|
||||||
|
bool IsProtected = false,
|
||||||
|
bool IsEncrypted = false,
|
||||||
|
bool IsSystem = false)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Id` | `Guid` | — |
|
||||||
|
| `Name` | `string` | — |
|
||||||
|
| `Topic` | `string?` | — |
|
||||||
|
| `IsPublic` | `bool` | — |
|
||||||
|
| `MessageCount` | `int` | — |
|
||||||
|
| `CreatedAt` | `DateTimeOffset` | — |
|
||||||
|
| `IsProtected` | `bool` | `false` |
|
||||||
|
| `IsEncrypted` | `bool` | `false` |
|
||||||
|
| `IsSystem` | `bool` | `false` |
|
||||||
|
|
||||||
|
|
||||||
|
ChannelDto is an immutable data transfer object that carries the essential metadata of a chat channel: `Id`, `Name`, `Topic`, `IsPublic`, `MessageCount`, `CreatedAt`, and the optional flags `IsProtected`, `IsEncrypted`, and `IsSystem`. As a `record`, it provides value-based equality and a straightforward bundle of properties suitable for transport across layers or API boundaries without exposing domain entities. Use it when returning channel summaries, listings, or lightweight channel representations to clients or other services, rather than leaking internal domain models.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
ChannelDto exists to decouple transport contracts from domain models; by consolidating channel metadata into a single, serializable shape, it enables stable APIs and easier versioning. The `IsSystem` flag allows distinguishing system channels (like announcements) from user-created ones, while `CreatedAt` helps clients sort or display recency.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var channel = new ChannelDto(
|
||||||
|
Guid.NewGuid(),
|
||||||
|
"general",
|
||||||
|
"General discussion",
|
||||||
|
true,
|
||||||
|
128,
|
||||||
|
DateTimeOffset.UtcNow
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Topic is nullable; consumers should handle `null` before displaying a topic, or provide a fallback.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ChannelMetaDto
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record ChannelMetaDto(
|
||||||
|
Guid Id,
|
||||||
|
string Name,
|
||||||
|
string? Topic,
|
||||||
|
bool IsEncrypted,
|
||||||
|
bool IsProtected,
|
||||||
|
int MessageCount,
|
||||||
|
int UniqueUserCount,
|
||||||
|
long EstimatedSizeBytes,
|
||||||
|
DateTimeOffset CreatedAt)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Id` | `Guid` | — |
|
||||||
|
| `Name` | `string` | — |
|
||||||
|
| `Topic` | `string?` | — |
|
||||||
|
| `IsEncrypted` | `bool` | — |
|
||||||
|
| `IsProtected` | `bool` | — |
|
||||||
|
| `MessageCount` | `int` | — |
|
||||||
|
| `UniqueUserCount` | `int` | — |
|
||||||
|
| `EstimatedSizeBytes` | `long` | — |
|
||||||
|
| `CreatedAt` | `DateTimeOffset` | — |
|
||||||
|
|
||||||
|
|
||||||
|
ChannelMetaDto is an immutable data transfer object that presents a concise, human-facing snapshot of a channel's metadata (the `/meta` command) to clients. It exposes the channel's identity (`Id`, `Name`), optional `Topic`, security/status flags (`IsEncrypted`, `IsProtected`), audience metrics (`MessageCount`, `UniqueUserCount`), and an estimated on-disk footprint (`EstimatedSizeBytes`), which is the sum of stored attachment blob sizes plus message text length and thus an estimate rather than an exact total. For encrypted channels the server still knows these figures — counts, timestamps, and stored blob sizes — even though it cannot read the content itself. The `CreatedAt` field records when the channel was created.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
ChannelMetaDto serves as a stable, read-only contract between server and clients for channel overviews. As an immutable `record`, it guarantees value-based equality and prevents accidental mutation, which simplifies caching and change detection in UI layers. The metadata it carries—identity, topic, security flags, counts, and size—supports efficient rendering of channel lists and summaries without exposing the channel contents.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The `EstimatedSizeBytes` is an estimate (sum of stored attachment blob sizes and message text length); it is not an exact on-disk size and can drift as content changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CreateChannelRequest
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record CreateChannelRequest(
|
||||||
|
string Name,
|
||||||
|
string? Topic = null,
|
||||||
|
bool IsPublic = true,
|
||||||
|
string? Password = null,
|
||||||
|
string? EncryptionSalt = null,
|
||||||
|
string? WrappedRoomKey = null)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Name` | `string` | — |
|
||||||
|
| `Topic` | `string?` | `null` |
|
||||||
|
| `IsPublic` | `bool` | `true` |
|
||||||
|
| `Password` | `string?` | `null` |
|
||||||
|
| `EncryptionSalt` | `string?` | `null` |
|
||||||
|
| `WrappedRoomKey` | `string?` | `null` |
|
||||||
|
|
||||||
|
|
||||||
|
The `CreateChannelRequest` is an immutable data transfer object that encapsulates all parameters needed to create a new chat channel. It requires a `Name` and exposes optional settings including `Topic`, whether the channel is public via `IsPublic` (default true), and optional security fields such as `Password`, `EncryptionSalt`, and `WrappedRoomKey` used for encrypted channel setup. Use this record when issuing a channel creation operation so that all related options are passed as a single, strongly-typed payload rather than a loose collection of parameters.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By collecting channel creation options into a single `CreateChannelRequest`, the boundary between API inputs and domain logic is cleanly expressed. The defaults on `IsPublic` and the optional nature of the other fields enable flexible requests while preserving a stable, serializable contract across process boundaries. This abstraction also makes future extension safer: new optional settings can be added without altering existing call sites.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Do not log sensitive fields: avoid writing `Password`, `EncryptionSalt`, or `WrappedRoomKey` to logs or telemetry.
|
||||||
|
- Nullable fields imply validation; ensure meaningful values before persisting or acting on them.
|
||||||
|
- If `IsPublic` is false, consider validating that a `Password` is provided for access control; enforce this at the API or domain layer if required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## EmbedDto
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record EmbedDto(
|
||||||
|
string? SiteName,
|
||||||
|
string? Title,
|
||||||
|
string? Description,
|
||||||
|
string? ImageAscii,
|
||||||
|
string Url,
|
||||||
|
string? ThemeColor = null)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `SiteName` | `string?` | — |
|
||||||
|
| `Title` | `string?` | — |
|
||||||
|
| `Description` | `string?` | — |
|
||||||
|
| `ImageAscii` | `string?` | — |
|
||||||
|
| `Url` | `string` | — |
|
||||||
|
| `ThemeColor` | `string?` | `null` |
|
||||||
|
|
||||||
|
|
||||||
|
EmbedDto is an immutable data container used to carry the metadata needed to render a rich embed, such as in chat messages or UI panels. It groups the surface data for an embed: `SiteName`, `Title`, `Description`, `ImageAscii`, `Url`, and an optional `ThemeColor`, so callers can supply a complete embed definition in a single object.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
As a `record`, `EmbedDto` provides value-based equality and supports deconstruction, making it straightforward to compare embeddings or pattern-match in rendering logic. It serves as a clean boundary between data authors and renderers: producers populate an `EmbedDto`, consumers render an embed from its fields without needing to understand surrounding domain.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var embed = new EmbedDto(
|
||||||
|
SiteName: "EchoHub",
|
||||||
|
Title: "Welcome",
|
||||||
|
Description: "A friendly hello from EchoHub.",
|
||||||
|
ImageAscii: " ___ \n (o o) \n \_/ ",
|
||||||
|
Url: "https://echohub.example",
|
||||||
|
ThemeColor: "#4B8BBE"
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `ThemeColor` is optional; omit it to use a default theming.
|
||||||
|
- `Url` is required; ensure it is a valid URL to enable link previews.
|
||||||
|
- Because `EmbedDto` is a `record`, two instances with identical field values compare equal.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## JoinChannelResult
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record JoinChannelResult(
|
||||||
|
bool Success,
|
||||||
|
List<MessageDto> History,
|
||||||
|
string? Error = null,
|
||||||
|
bool PasswordRequired = false,
|
||||||
|
string? EncryptionSalt = null,
|
||||||
|
string? WrappedRoomKey = null)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Success` | `bool` | — |
|
||||||
|
| `History` | `List<MessageDto>` | — |
|
||||||
|
| `Error` | `string?` | `null` |
|
||||||
|
| `PasswordRequired` | `bool` | `false` |
|
||||||
|
| `EncryptionSalt` | `string?` | `null` |
|
||||||
|
| `WrappedRoomKey` | `string?` | `null` |
|
||||||
|
|
||||||
|
|
||||||
|
Represents the outcome of a join-channel operation as a `JoinChannelResult` type. It exposes a `bool` `Success` flag, a `List<MessageDto>` `History` of messages retrieved for the channel, and optional metadata including a `string?` `Error`, a `bool` `PasswordRequired`, and optional encryption data (`string?` `EncryptionSalt`, `string?` `WrappedRoomKey`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MessageDto
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record MessageDto(
|
||||||
|
Guid Id,
|
||||||
|
string Content,
|
||||||
|
string SenderUsername,
|
||||||
|
string? SenderNicknameColor,
|
||||||
|
string ChannelName,
|
||||||
|
DateTimeOffset SentAt,
|
||||||
|
List<AttachmentDto>? Attachments = null,
|
||||||
|
List<EmbedDto>? Embeds = null,
|
||||||
|
string? SenderDisplayName = null,
|
||||||
|
ReplyRefDto? ReplyTo = null)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Id` | `Guid` | — |
|
||||||
|
| `Content` | `string` | — |
|
||||||
|
| `SenderUsername` | `string` | — |
|
||||||
|
| `SenderNicknameColor` | `string?` | — |
|
||||||
|
| `ChannelName` | `string` | — |
|
||||||
|
| `SentAt` | `DateTimeOffset` | — |
|
||||||
|
| `Attachments` | `List<AttachmentDto>?` | `null` |
|
||||||
|
| `Embeds` | `List<EmbedDto>?` | `null` |
|
||||||
|
| `SenderDisplayName` | `string?` | `null` |
|
||||||
|
| `ReplyTo` | `ReplyRefDto?` | `null` |
|
||||||
|
|
||||||
|
|
||||||
|
Represents a chat message as a data contract used by the chat API. It captures the message `Id`, the textual `Content`, and author info (`SenderUsername`, optional `SenderNicknameColor`, optional `SenderDisplayName`), the `ChannelName`, and the `SentAt` timestamp. Optional `Attachments` and `Embeds` support rich content, while `ReplyTo` references a prior message.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This DTO is designed as a transport-friendly aggregation of message data, suitable for serialization across clients and services. By referencing the dedicated `AttachmentDto` and `EmbedDto` types, it remains extensible for rich content, and its optional fields (`Attachments`, `Embeds`, `ReplyTo`, `SenderNicknameColor`, `SenderDisplayName`) allow the same shape to cover both simple and feature-rich messages.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `Attachments` and `Embeds` may be `null`; treat them as empty sequences when rendering or iterating.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RekeyChannelRequest
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record RekeyChannelRequest(
|
||||||
|
string OldPassword,
|
||||||
|
string NewPassword,
|
||||||
|
string NewEncryptionSalt,
|
||||||
|
string NewWrappedRoomKey)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `OldPassword` | `string` | — |
|
||||||
|
| `NewPassword` | `string` | — |
|
||||||
|
| `NewEncryptionSalt` | `string` | — |
|
||||||
|
| `NewWrappedRoomKey` | `string` | — |
|
||||||
|
|
||||||
|
|
||||||
|
The `RekeyChannelRequest` record represents the data the client sends to request a rekey of an encrypted channel. It conveys knowledge of the current passphrase (via `OldPassword`) and the new credentials and wrapped key to apply (via `NewPassword`, `NewEncryptionSalt`, and `NewWrappedRoomKey`).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This DTO enables the server to verify the client's possession of the existing auth key while atomically applying new encryption material in a single operation. It decouples the client's input from the rekeying logic, allowing validation, auditing, and rollback policies to be applied at the server boundary.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Do not log `OldPassword` or `NewPassword`; treat these values as ephemeral and ensure transport-layer secrecy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ReplyRefDto
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record ReplyRefDto(
|
||||||
|
Guid MessageId,
|
||||||
|
string SenderUsername,
|
||||||
|
string Content)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `MessageId` | `Guid` | — |
|
||||||
|
| `SenderUsername` | `string` | — |
|
||||||
|
| `Content` | `string` | — |
|
||||||
|
|
||||||
|
|
||||||
|
Represents a reference to the message that a reply targets. It carries the target message's identifier (`MessageId`), the original sender's username (`SenderUsername`), and the reply content (`Content`), which is treated exactly like message content on the wire: transport-encrypted, and for end-to-end encrypted rooms it is room ciphertext the client must decrypt (the server truncates only plaintext snippets). Null on a `MessageDto` when the original message no longer exists.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
ReplyRefDto acts as a compact pointer that preserves the link between a reply and its target message without duplicating payloads. It separates transport- and encryption-aware handling from display logic, enabling clients to decrypt or render the referenced content while the server retains plaintext-only signals. In threaded chat UX, this symbol supports rendering reply previews and context for the target message.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Be aware that `Content` might be ciphertext in encrypted rooms and may not be human-readable until decrypted; do not display it as plaintext without decryption.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SendMessageRequest
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record SendMessageRequest(string ChannelName, string Content)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `ChannelName` | `string` | — |
|
||||||
|
| `Content` | `string` | — |
|
||||||
|
|
||||||
|
|
||||||
|
SendMessageRequest is a simple, immutable data carrier (record) that encapsulates the channel to which a message should be sent and the message content itself. Use this `SendMessageRequest` when you need to issue a message to a specific chat channel, providing both the `ChannelName` and the `Content` in a single object rather than passing multiple parameters or ad-hoc structures.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SendUrlRequest
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record SendUrlRequest(string Url)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Url` | `string` | — |
|
||||||
|
|
||||||
|
|
||||||
|
SendUrlRequest is a minimal value object used to convey a URL as a request payload. As a `record` with a single `string Url` positional parameter, it provides value-based equality and immutability, making it ideal for passing URL data through layers or across API boundaries instead of threading raw `string` values.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`SendUrlRequest` serves as a precise contract for operations that require a URL. Its `record` semantics ensure structural equality and allow easy deconstruction; by encapsulating the `Url` property, it clarifies intent and supports serialization as a simple payload.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var req = new SendUrlRequest("https://example.com");
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- No URL validation is performed by this type; validate the URL in the caller or service layer before processing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UpdateTopicRequest
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record UpdateTopicRequest(string? Topic)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Topic` | `string?` | — |
|
||||||
|
|
||||||
|
|
||||||
|
Represents a request payload to update a topic, encapsulating an optional `Topic` value. As a positional-record, it provides an immutable, lightweight data carrier that callers populate with the new topic string when issuing an update to a chat's topic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UserDto
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record UserDto(
|
||||||
|
Guid Id,
|
||||||
|
string Username,
|
||||||
|
string? DisplayName,
|
||||||
|
string? NicknameColor,
|
||||||
|
UserStatus Status,
|
||||||
|
DateTimeOffset LastSeenAt)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Id` | `Guid` | — |
|
||||||
|
| `Username` | `string` | — |
|
||||||
|
| `DisplayName` | `string?` | — |
|
||||||
|
| `NicknameColor` | `string?` | — |
|
||||||
|
| `Status` | [`UserStatus`](../Models/UserStatus.cs.md) | — |
|
||||||
|
| `LastSeenAt` | `DateTimeOffset` | — |
|
||||||
|
|
||||||
|
|
||||||
|
`UserDto` is an immutable data transfer object that carries a concise snapshot of a user for chat workflows. It exposes the user’s `Id` (`Guid`), `Username`, optional `DisplayName` and `NicknameColor`, the current `Status` ([`UserStatus`](../Models/UserStatus.cs.md)), and the `LastSeenAt` timestamp (`DateTimeOffset`). Use this DTO when returning or transferring lightweight user data across API boundaries or UI layers instead of exposing full domain entities.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Being a `record` with positional parameters, `UserDto` benefits from value-based equality and convenient deconstruction, which is helpful for tests and payload comparisons. The nullable fields `DisplayName` and `NicknameColor` reflect optional user profile data; readers should handle the possibility of missing values gracefully.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Nullable fields require null checks during consumption.
|
||||||
|
- Being immutable, modifying a `UserDto` requires creating a new instance (e.g., via a `with` expression).
|
||||||
|
- The `LastSeenAt` is a `DateTimeOffset`; ensure consistent time zone handling across systems.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
# CommonDtos.cs
|
||||||
|
|
||||||
|
> **Source:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [ApiResponse](#apiresponse)
|
||||||
|
- [ApiResponse](#apiresponse-1)
|
||||||
|
- [ChannelOperationResult](#channeloperationresult)
|
||||||
|
- [ErrorResponse](#errorresponse)
|
||||||
|
- [PaginatedResponse](#paginatedresponse)
|
||||||
|
- [UserOperationResult](#useroperationresult)
|
||||||
|
- [ChannelError](#channelerror)
|
||||||
|
- [UserError](#usererror)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ApiResponse
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record ApiResponse(bool Success, string? Message = null, List<string>? Errors = null)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Success` | `bool` | — |
|
||||||
|
| [`Message`](../Models/Message.cs.md) | `string?` | `null` |
|
||||||
|
| `Errors` | `List<string>?` | `null` |
|
||||||
|
|
||||||
|
|
||||||
|
Represents a standard API outcome as a `record` with a `bool` `Success`, an optional `string?` [`Message`](../Models/Message.cs.md), and an optional `List<string>?` `Errors`. Use `ApiResponse` to package the result of API operations or service methods into a single, strongly-typed object for consistent client consumption instead of scattering boolean flags and messages across code.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By centralizing outcome data in `ApiResponse`, callers can handle success/failure logic in a uniform way and avoid ad-hoc boolean checks scattered through the code. The `Errors` collection is intended for granular, field-level validation messages that the client can display; the [`Message`](../Models/Message.cs.md) offers a concise summary, while `Success` drives flow control.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var success = new ApiResponse(true);
|
||||||
|
var failure = new ApiResponse(false, "Validation failed", new List<string> { "Name is required", "Email is invalid" });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The `Errors` property is a mutable `List<string>`; external mutation is possible. If you need true immutability, consider using `IReadOnlyList<string>` or an immutable collection.
|
||||||
|
- When `Success` is true, you may omit [`Message`](../Models/Message.cs.md) and `Errors` or set them as appropriate; when `Success` is false, provide a meaningful [`Message`](../Models/Message.cs.md) and optionally populate `Errors` to detail issues.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ApiResponse
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record ApiResponse<T>(bool Success, string? Message = null, List<string>? Errors = null, T? Data = default)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Success` | `bool` | — |
|
||||||
|
| [`Message`](../Models/Message.cs.md) | `string?` | `null` |
|
||||||
|
| `Errors` | `List<string>?` | `null` |
|
||||||
|
| `Data` | `T?` | `default` |
|
||||||
|
|
||||||
|
|
||||||
|
A generic wrapper for operation results that standardizes API responses. It indicates success with `Success` and carries an optional [`Message`](../Models/Message.cs.md), a `List<string>` named `Errors` for validation or processing issues, and an optional `Data` payload of type `T`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
This abstraction decouples the shape of a successful response from the actual data, enabling consistent error handling and client-side parsing across services. By returning `ApiResponse<T>` from operations, you centralize how success, messages, and validation details are conveyed, which simplifies cross-cutting concerns like localization and error translation.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Successful response with data
|
||||||
|
var success = new ApiResponse<string>(true, "Operation completed", null, "payload-data");
|
||||||
|
|
||||||
|
// Failed response with errors
|
||||||
|
var failure = new ApiResponse<string>(false, "Validation failed", new List<string> { "Name is required", "Email is invalid" }, null);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ChannelOperationResult
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record ChannelOperationResult(ChannelDto? Channel, ChannelError? Error, string? ErrorMessage)
|
||||||
|
{
|
||||||
|
public bool IsSuccess => Error is null;
|
||||||
|
|
||||||
|
public static ChannelOperationResult Success(ChannelDto channel) => new(channel, null, null);
|
||||||
|
public static ChannelOperationResult Fail(ChannelError error, string message) => new(null, error, message);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| [`Channel`](../Models/Channel.cs.md) | `ChannelDto?` | — |
|
||||||
|
| `Error` | `ChannelError?` | — |
|
||||||
|
| `ErrorMessage` | `string?` | — |
|
||||||
|
|
||||||
|
|
||||||
|
ChannelOperationResult is an immutable wrapper that conveys the outcome of a channel-related operation. It either carries a [`ChannelDto`](ChatDtos.cs.md) when the operation succeeds or a `ChannelError` with an `ErrorMessage` when it fails; the static helpers `Success` and `Fail` make the intent explicit when constructing results.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
ChannelOperationResult uses a C# `record` to express a simple, value-like outcome. It centralizes success/failure information for channel-oriented operations, enabling uniform error handling and reducing scattered null-checks. Consumers should inspect `IsSuccess` before accessing [`Channel`](../Models/Channel.cs.md); when `IsSuccess` is true, [`Channel`](../Models/Channel.cs.md) is non-null, and when false, `Error` and `ErrorMessage` describe the failure.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Prefer constructing via `ChannelOperationResult.Success(...)` or `ChannelOperationResult.Fail(...)` rather than the primary constructor to preserve the invariant that a successful result has a non-null [`Channel`](../Models/Channel.cs.md) and a failed result has non-null `Error`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ErrorResponse
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record ErrorResponse(string Error, string? Detail = null)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Error` | `string` | — |
|
||||||
|
| `Detail` | `string?` | `null` |
|
||||||
|
|
||||||
|
|
||||||
|
Encapsulates a standardized error payload with a mandatory `Error` code and an optional `Detail` string for extra context. As a `record`, it is immutable by design and supports value-based equality and deconstruction, which makes it ideal for returning a single, comparable error object from APIs or services. Use this type to produce consistent, serializable error information across the system.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Centralizes the error payload shape to ensure all error responses share a single contract. The optional `Detail` field provides human-friendly context without breaking clients that only inspect the `Error` code. Because it is a `record`, it naturally supports comparisons and pattern matching when handling error responses.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PaginatedResponse
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record PaginatedResponse<T>(List<T> Items, int Total, int Offset, int Limit)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Items` | `List<T>` | — |
|
||||||
|
| `Total` | `int` | — |
|
||||||
|
| `Offset` | `int` | — |
|
||||||
|
| `Limit` | `int` | — |
|
||||||
|
|
||||||
|
|
||||||
|
Represents a paged result set for a collection of items of type `T`. It bundles the current page of data (`Items`) with paging metadata: the total item count (`Total`), the starting offset (`Offset`), and the page size limit (`Limit`). This shape is used by APIs that support paging to convey both the data and how to fetch additional pages; the use of a `record` provides value-based equality and immutability for API responses.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Using a `record` for `PaginatedResponse<T>` gives value-based equality and an immutable data shape, which makes it natural for transporting paging results across boundaries. It centralizes both the data (`Items`) and its paging metadata (`Total`, `Offset`, `Limit`) in a single coherent DTO, reducing the risk of mismatch between data and paging state when consumed by clients or other services.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The `Items` collection is a `List<T>`, which is mutable. If you require immutability guarantees, wrap it in a read-only collection or clone the list before exposure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UserOperationResult
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record UserOperationResult(UserProfileDto? User, UserError? Error, string? ErrorMessage)
|
||||||
|
{
|
||||||
|
public bool IsSuccess => Error is null;
|
||||||
|
|
||||||
|
public static UserOperationResult Success(UserProfileDto user) => new(user, null, null);
|
||||||
|
public static UserOperationResult Fail(UserError error, string message) => new(null, error, message);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| [`User`](../Models/User.cs.md) | `UserProfileDto?` | — |
|
||||||
|
| `Error` | `UserError?` | — |
|
||||||
|
| `ErrorMessage` | `string?` | — |
|
||||||
|
|
||||||
|
|
||||||
|
An immutable result wrapper for user-related operations. It encapsulates either a [`UserProfileDto`](ProfileDtos.cs.md) payload via [`User`](../Models/User.cs.md) on success, or a `UserError` and a diagnostic `ErrorMessage` on failure. Use the static factories `Success` and `Fail` to construct consistent results, and check `IsSuccess` to decide how to proceed.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By encapsulating both success payload and failure details into a single value, this symbol standardizes how user-operation results are communicated. Callers check `IsSuccess` and then access either the [`User`](../Models/User.cs.md) payload or the `Error`/`ErrorMessage` to react. Because it is a `record`, equality is based on its contents, which helps tests and caching rely on value semantics.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Directly constructing with a mismatched state (for example, a non-null `Error` but a null or missing `ErrorMessage`) can create inconsistent results; prefer the provided factories to enforce the invariant that success results include a [`User`](../Models/User.cs.md) and no error, while failures include an `Error` and an `ErrorMessage`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ChannelError
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
|
||||||
|
> **Kind:** enum
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public enum ChannelError
|
||||||
|
{
|
||||||
|
ValidationFailed,
|
||||||
|
AlreadyExists,
|
||||||
|
NotFound,
|
||||||
|
Forbidden,
|
||||||
|
Protected
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ChannelError is an enum that enumerates the standard error conditions that may arise when working with channels in the `EchoHub` domain. It provides a typed set of failure reasons—`ValidationFailed`, `AlreadyExists`, `NotFound`, `Forbidden`, and `Protected`—to be returned by channel-related operations, enabling callers to branch on the specific cause and handle it uniformly rather than parsing strings.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
`ChannelError` centralizes the failure kinds that can occur during channel-related operations and is intended to be carried by DTOs that report operation results. It enables type-safe error handling, allowing callers to pattern-match on the exact failure (`ValidationFailed`, `AlreadyExists`, `NotFound`, `Forbidden`, `Protected`) and map them to appropriate responses without parsing human-generated messages. This separation of error kind from presentation keeps the API consistent as channel semantics evolve.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UserError
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
|
||||||
|
> **Kind:** enum
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public enum UserError
|
||||||
|
{
|
||||||
|
ValidationFailed,
|
||||||
|
AlreadyExists,
|
||||||
|
NotFound,
|
||||||
|
InvalidCredentials,
|
||||||
|
Banned
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The `UserError` enum defines the canonical set of failure conditions related to user accounts that may be surfaced by operations in the core DTO layer. Members include `ValidationFailed`, `AlreadyExists`, `NotFound`, `InvalidCredentials`, and `Banned`, each representing a distinct error scenario that downstream code can pattern-match to drive error responses and user messaging.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This enum centralizes common user-domain errors so that authentication, registration, and profile-management flows can share a consistent error-handling strategy. By codifying these cases in a single type, callers can translate domain failures into uniform API responses and UI messages without depending on implementation details.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- When mapping these errors to user-facing messages, avoid exposing sensitive internal details and rely on generic messaging driven by the enum value.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# InviteDtos.cs
|
||||||
|
|
||||||
|
> **Source:** `src/EchoHub.Core/DTOs/InviteDtos.cs`
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [CreateInviteRequest](#createinviterequest)
|
||||||
|
- [InviteDto](#invitedto)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CreateInviteRequest
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/InviteDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record CreateInviteRequest(int? MaxUses = null, int? ExpiresInHours = null)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `MaxUses` | `int?` | `null` |
|
||||||
|
| `ExpiresInHours` | `int?` | `null` |
|
||||||
|
|
||||||
|
|
||||||
|
Represents the request payload for creating an invite, carrying optional constraints for the invite. The nullable `MaxUses` and `ExpiresInHours` allow callers to omit constraints. As a `record`, it provides value-based equality and immutability, making it a convenient, typed carrier for API calls.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This type centralizes the concept of invite constraints and cleanly separates client request construction from business logic. It interoperates with the invite-creation pathway by encoding optional parameters as nullable properties, allowing the API to apply defaults when a field is null.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Null values indicate 'not specified' and will be treated as absent by the invite-creation endpoint; set only the fields you intend to constrain.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## InviteDto
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/InviteDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record InviteDto(
|
||||||
|
string Code,
|
||||||
|
string CreatedByUsername,
|
||||||
|
DateTimeOffset CreatedAt,
|
||||||
|
DateTimeOffset? ExpiresAt,
|
||||||
|
int MaxUses,
|
||||||
|
int UseCount)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Code` | `string` | — |
|
||||||
|
| `CreatedByUsername` | `string` | — |
|
||||||
|
| `CreatedAt` | `DateTimeOffset` | — |
|
||||||
|
| `ExpiresAt` | `DateTimeOffset?` | — |
|
||||||
|
| `MaxUses` | `int` | — |
|
||||||
|
| `UseCount` | `int` | — |
|
||||||
|
|
||||||
|
|
||||||
|
InviteDto is an immutable data transfer object that carries the metadata for an invitation: the `Code`, the creator's username (`CreatedByUsername`), the creation time (`CreatedAt`), an optional expiration (`ExpiresAt`), and usage counters (`MaxUses` and `UseCount`). It is designed for transporting invitation data across application boundaries without behavior, making it easy to serialize, deserialize, and compare by value.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Because it is defined as a `record`, `InviteDto` benefits from value-based equality and structural immutability, ensuring that two invitations with the same data compare equal and that the payload remains unchanged after construction. The nullable `ExpiresAt` conveys that an invitation might have no expiration; consumers must treat a null as no expiry. The `MaxUses` together with `UseCount` enables the system to enforce limits at the boundary without embedding logic here. This symbol sits at the boundary between persistence, API contracts, and business logic, keeping the shape of invitation data consistent across layers.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# ModerationDtos.cs
|
||||||
|
|
||||||
|
> **Source:** `src/EchoHub.Core/DTOs/ModerationDtos.cs`
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [AssignRoleRequest](#assignrolerequest)
|
||||||
|
- [BanRequest](#banrequest)
|
||||||
|
- [KickRequest](#kickrequest)
|
||||||
|
- [MuteRequest](#muterequest)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AssignRoleRequest
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ModerationDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record AssignRoleRequest(string Username, ServerRole Role)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Username` | `string` | — |
|
||||||
|
| `Role` | [`ServerRole`](../Models/ServerRole.cs.md) | — |
|
||||||
|
|
||||||
|
|
||||||
|
AssignRoleRequest is a lightweight, immutable data container (a positional `record`) that carries the target `Username` and the `Role` to be assigned. It serves as the payload for moderation workflows when granting a [`ServerRole`](../Models/ServerRole.cs.md) to a user, enabling consistent transport of this intent across API boundaries without embedding behavior. As a `record`, it uses value-based equality and can be copied with a `with` expression to create variations.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This symbol acts purely as a data carrier for the moderation flow, separating payload shape from the enforcement logic. It relies on the `Username` and `Role` values to identify the target user and the desired permission, enabling services to validate and enact the change consistently.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Ensure `Username` is a valid existing member; the DTO does not enforce existence.
|
||||||
|
- The `Role` must be a valid [`ServerRole`](../Models/ServerRole.cs.md) value; rely on server-side validation to handle invalid roles.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## BanRequest
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ModerationDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record BanRequest(string? Reason = null)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Reason` | `string?` | `null` |
|
||||||
|
|
||||||
|
|
||||||
|
BanRequest is a simple data carrier used to submit a moderation ban action, optionally including a rationale. Its only member, `Reason`, is nullable and defaults to null, so callers may omit a reason when none is provided.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## KickRequest
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ModerationDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record KickRequest(string? Reason = null)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Reason` | `string?` | `null` |
|
||||||
|
|
||||||
|
|
||||||
|
KickRequest is a minimal, immutable data carrier used to convey a moderation kick action. It carries an optional `Reason` explaining why the kick is issued. Callers instantiate a `KickRequest` when initiating a kick, providing a `Reason` if available; if no reason is supplied, the `Reason` property is `null`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
KickRequest being a `record` makes it a value object with structural equality and immutability, which is helpful when routing kick intents through handlers or messaging layers. It encapsulates the kick payload so that higher-level services can work with a single, consistent input type rather than ad-hoc parameters.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var req = new KickRequest("Spamming in chat");
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `Reason` is nullable; downstream code should handle `null` and decide whether a reason is required.
|
||||||
|
- Records provide value-based equality; two `KickRequest` instances with the same `Reason` compare equal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MuteRequest
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ModerationDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record MuteRequest(string? Reason = null, int? DurationMinutes = null)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Reason` | `string?` | `null` |
|
||||||
|
| `DurationMinutes` | `int?` | `null` |
|
||||||
|
|
||||||
|
|
||||||
|
MuteRequest is a lightweight data transfer object used to specify the parameters of a mute action in moderation flows. It includes two optional values: `Reason`, a `string?` describing why the mute is issued, and `DurationMinutes`, an `int?` indicating how long the mute should last; both default to `null` if not provided. This allows callers to mute with a default duration or provide additional context for auditing and user experience.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
# ProfileDtos.cs
|
||||||
|
|
||||||
|
> **Source:** `src/EchoHub.Core/DTOs/ProfileDtos.cs`
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [AvatarUploadResponse](#avataruploadresponse)
|
||||||
|
- [UpdateProfileRequest](#updateprofilerequest)
|
||||||
|
- [UpdateStatusRequest](#updatestatusrequest)
|
||||||
|
- [UserPresenceDto](#userpresencedto)
|
||||||
|
- [UserProfileDto](#userprofiledto)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AvatarUploadResponse
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ProfileDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record AvatarUploadResponse(string AvatarAscii)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `AvatarAscii` | `string` | — |
|
||||||
|
|
||||||
|
|
||||||
|
AvatarUploadResponse is a lightweight data container that carries the ASCII representation of a user-uploaded avatar. Its sole payload is the `AvatarAscii` string, which downstream clients can render to display the avatar in text form after an upload.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UpdateProfileRequest
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ProfileDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record UpdateProfileRequest(
|
||||||
|
string? DisplayName = null,
|
||||||
|
string? Bio = null,
|
||||||
|
string? NicknameColor = null)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `DisplayName` | `string?` | `null` |
|
||||||
|
| `Bio` | `string?` | `null` |
|
||||||
|
| `NicknameColor` | `string?` | `null` |
|
||||||
|
|
||||||
|
|
||||||
|
This `UpdateProfileRequest` is a `record` that carries a partial update payload for a user's profile. By supplying only non-null properties (e.g. `DisplayName`, `Bio`, or `NicknameColor`), callers express which fields should be updated; fields left as `null` indicate no change for that field.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
Using a `record` provides value-based equality and inherent immutability, which makes it ideal for data-carrying DTOs. The ability to set properties to `null` gives a clean contract for partial updates; consumers should treat nulls as 'do not modify' for that field and pass through only the intended changes to the update operation.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var request = new UpdateProfileRequest(DisplayName: "Nova", NicknameColor: "#FFAA00");
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Ensure the update handler interprets nulls as "no change" to avoid overwriting existing values.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UpdateStatusRequest
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ProfileDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record UpdateStatusRequest(
|
||||||
|
UserStatus Status,
|
||||||
|
string? StatusMessage = null)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Status` | [`UserStatus`](../Models/UserStatus.cs.md) | — |
|
||||||
|
| `StatusMessage` | `string?` | `null` |
|
||||||
|
|
||||||
|
|
||||||
|
This `UpdateStatusRequest` record encapsulates the payload required to update a user's profile status. It carries the new [`UserStatus`](../Models/UserStatus.cs.md) and an optional `StatusMessage`, and is intended to be used when issuing a status update to APIs or command handlers where a consistent update payload is expected.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
By modeling the input as a dedicated value object, this abstraction centralizes validation and transport concerns at the boundaries between the domain and application layers, ensuring a stable contract for status updates. It also isolates update-related concerns from the rest of the profile payload, making it easier to evolve serialization, auditing, or routing rules without touching domain entities.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The `StatusMessage` property is nullable. Callers must handle the possibility of a missing message when consuming this payload.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UserPresenceDto
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ProfileDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record UserPresenceDto(
|
||||||
|
string Username,
|
||||||
|
string? DisplayName,
|
||||||
|
string? NicknameColor,
|
||||||
|
UserStatus Status,
|
||||||
|
string? StatusMessage,
|
||||||
|
ServerRole Role,
|
||||||
|
bool IsIrc = false)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Username` | `string` | — |
|
||||||
|
| `DisplayName` | `string?` | — |
|
||||||
|
| `NicknameColor` | `string?` | — |
|
||||||
|
| `Status` | [`UserStatus`](../Models/UserStatus.cs.md) | — |
|
||||||
|
| `StatusMessage` | `string?` | — |
|
||||||
|
| `Role` | [`ServerRole`](../Models/ServerRole.cs.md) | — |
|
||||||
|
| `IsIrc` | `bool` | `false` |
|
||||||
|
|
||||||
|
|
||||||
|
Represents the presence-related data for a user in profile contexts, bundling the `Username`, optional `DisplayName`, optional `NicknameColor`, current `Status`, optional `StatusMessage`, `Role`, and the `IsIrc` flag into a single immutable DTO (with `IsIrc` defaulting to `false`). It is intended to be created and transported as a coherent unit when rendering user cards or updating presence in the UI or API responses, rather than scattering these fields across multiple structures.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Acts as a stable boundary for presence data used by profile-related UI and API surfaces, consolidating identity, status, and role information into one payload. The [`UserStatus`](../Models/UserStatus.cs.md) and [`ServerRole`](../Models/ServerRole.cs.md) collaborators encode the allowed presence states and roles, while `NicknameColor` provides a UI cue without forcing a separate domain type. Being a `record`, it relies on value equality to simplify change detection and caching as presence updates propagate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UserProfileDto
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ProfileDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record UserProfileDto(
|
||||||
|
Guid Id,
|
||||||
|
string Username,
|
||||||
|
string? DisplayName,
|
||||||
|
string? Bio,
|
||||||
|
string? NicknameColor,
|
||||||
|
string? AvatarAscii,
|
||||||
|
UserStatus Status,
|
||||||
|
string? StatusMessage,
|
||||||
|
ServerRole Role,
|
||||||
|
DateTimeOffset CreatedAt,
|
||||||
|
DateTimeOffset LastSeenAt)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Id` | `Guid` | — |
|
||||||
|
| `Username` | `string` | — |
|
||||||
|
| `DisplayName` | `string?` | — |
|
||||||
|
| `Bio` | `string?` | — |
|
||||||
|
| `NicknameColor` | `string?` | — |
|
||||||
|
| `AvatarAscii` | `string?` | — |
|
||||||
|
| `Status` | [`UserStatus`](../Models/UserStatus.cs.md) | — |
|
||||||
|
| `StatusMessage` | `string?` | — |
|
||||||
|
| `Role` | [`ServerRole`](../Models/ServerRole.cs.md) | — |
|
||||||
|
| `CreatedAt` | `DateTimeOffset` | — |
|
||||||
|
| `LastSeenAt` | `DateTimeOffset` | — |
|
||||||
|
|
||||||
|
|
||||||
|
UserProfileDto is an immutable data transfer object that represents a snapshot of a user's profile for API responses and inter-layer communication. Implemented as a `record`, it carries a stable payload including the user's identity (`Id` of type `Guid`, `Username`), optional display details (`DisplayName`, `Bio`, `NicknameColor`, `AvatarAscii`), presence (`Status` of type [`UserStatus`](../Models/UserStatus.cs.md), `StatusMessage`), role (`Role` of type [`ServerRole`](../Models/ServerRole.cs.md)), and timestamps (`CreatedAt`, `LastSeenAt` of type `DateTimeOffset`).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By modelling the payload as a `record`, `UserProfileDto` benefits from value-based equality and straightforward serialization for API clients. It serves as a transport contract that decouples external API surfaces from the internal domain model, allowing optional fields to convey partial profile information without mutating server state.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# ServerDtos.cs
|
||||||
|
|
||||||
|
> **Source:** `src/EchoHub.Core/DTOs/ServerDtos.cs`
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [EncryptionKeyResponse](#encryptionkeyresponse)
|
||||||
|
- [ServerStatusDto](#serverstatusdto)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## EncryptionKeyResponse
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ServerDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record EncryptionKeyResponse(string Key)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Key` | `string` | — |
|
||||||
|
|
||||||
|
|
||||||
|
EncryptionKeyResponse is a minimal, strongly-typed envelope used to return an encryption key from server-side DTOs. It is implemented as a C# `record` with a single property `string Key`, providing value-based equality and convenient deconstruction while keeping the surface area stable for serialization and future extension.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Using a one-property `record` as a DTO provides a stable, strongly-typed surface for returning the key, while enabling easy evolution (e.g., adding metadata like algorithm, expiration, or salt) without breaking client contracts. It also leverages `record` semantics to support value-based equality and clean deconstruction when used in responses.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ServerStatusDto
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ServerDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record ServerStatusDto(
|
||||||
|
string Name,
|
||||||
|
string? Description,
|
||||||
|
int OnlineUsers,
|
||||||
|
int TotalChannels,
|
||||||
|
string RegistrationMode = "open")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Name` | `string` | — |
|
||||||
|
| `Description` | `string?` | — |
|
||||||
|
| `OnlineUsers` | `int` | — |
|
||||||
|
| `TotalChannels` | `int` | — |
|
||||||
|
| `RegistrationMode` | `string` | `"open"` |
|
||||||
|
|
||||||
|
|
||||||
|
Represents a lightweight, immutable snapshot of a server's status for transport between layers or to clients. It exposes the server's `Name`, optional `Description`, current `OnlineUsers`, total `TotalChannels`, and the `RegistrationMode` (defaulting to `open` when not provided).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Because this is a `record`, it uses value-based equality and immutable properties, making it ideal as a DTO boundary between internal domain models and external consumers. Construct this type from your server state when returning status information to clients, rather than leaking domain entities.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `Description` is nullable (`string?`). Guard against null or provide a fallback when presenting it to callers.
|
||||||
|
- To derive a modified copy (e.g., update `OnlineUsers`), use the `with` expression since `ServerStatusDto` is immutable.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Attachment
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Models/Attachment.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class Attachment
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Represents a file attached to a message (such as an image, audio, or any file), enabling a message to carry zero or more attachments alongside its text content. The `Attachment` entity associates a downloadable resource with its parent [`Message`](Message.cs.md) via `MessageId` and, optionally, [`Message`](Message.cs.md), while storing the attachment's URL (`Url`), filename (`FileName`), size (`FileSize`), type (`Kind`), and an optional ASCII preview (`AsciiPreview`).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
Attachments decouple media from the textual content of a message, allowing the system to manage downloads, permissions, and encryption independently from the message body. The `AsciiPreview` provides a lightweight visual cue for image attachments, and its presence is influenced by how media is encrypted at rest or within channel scopes. The [`AttachmentKind`](AttachmentKind.cs.md) helps callers distinguish among images, audio, and other file types to apply appropriate handling.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The `Url` is a relative download path (for example, `/api/files/{fileId}`); clients should prefix it with the API base URL when constructing a full link.
|
||||||
|
- The `AsciiPreview` is null for non-image attachments and is stored encrypted at rest in encrypted channels.
|
||||||
|
- The `FileSize` is the number of bytes stored for the attachment and may reflect ciphertext size when encryption is enabled.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# AttachmentKind
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Models/AttachmentKind.cs`
|
||||||
|
> **Kind:** enum
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public enum AttachmentKind
|
||||||
|
{
|
||||||
|
Image,
|
||||||
|
Audio,
|
||||||
|
File
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
AttachmentKind is an enum that encodes how a message attachment should be rendered in the client. It enables rendering logic to pick the appropriate UI: for `Image` attachments, an ASCII image preview is shown; for `Audio`, a play control is exposed; and for `File`, a download line is presented. Use this enum when you need to branch rendering behavior based on the attachment's kind, instead of scattering rendering decisions across the codebase.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This enum centralizes how attachments are presented, decoupling the attachment data from UI rendering code. It helps the rendering layer evolve independently (e.g., swapping ASCII previews or adding new affordances) without changing attachment structures.
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Channel
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Models/Channel.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class Channel
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Channel models a chat channel within EchoHub's chat surface. It exposes an identifier `Id` (`Guid`), a required `Name` (`string`), an optional `Topic` (`string?`), and a flag `IsPublic` (`bool`) that defaults to `true`. The model also supports server-managed channels via `IsSystem` (`bool`), which are auto-created and read-only for all roles; users cannot create them. When a channel is password-protected, `PasswordHash` (`string?`) stores the hashed password. For end-to-end encryption, the envelope is represented by `EncryptionSalt` (`string?`) and `WrappedRoomKey` (`string?`), both client-generated so that the server never has access to the room content. Creation metadata is captured by `CreatedAt` (`DateTimeOffset`) and `CreatedByUserId` (`Guid`). The `Messages` collection (`List<Message>`) contains the related [`Message`](Message.cs.md) entities that belong to this channel.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user