Compare commits

..

118 Commits

Author SHA1 Message Date
timedout
9b4845bf8d feat(ci): Allow running manual workflows against specific commits 2025-12-22 13:29:40 +00:00
aviac
fb5b515f96 chore: update flake lock 2025-12-22 04:11:41 +00:00
Jade Ellis
e6336d694a chore: Fix escape 2025-12-22 02:42:21 +00:00
Jade Ellis
b7841280d9 chore: Security announcement 2025-12-22 02:36:31 +00:00
Jade Ellis
f4ccb81913 chore: Release 2025-12-22 00:23:20 +00:00
Jade Ellis
710cdfeadb chore: Update mailmap 2025-12-21 20:34:11 +00:00
Jade Ellis
666849ea87 chore(ci): Unify artifact versions 2025-12-21 19:11:12 +00:00
Jade Ellis
71094803f1 fix(ci): Try use path that exists 2025-12-21 18:50:48 +00:00
Jade Ellis
bf91ce5c7f feat: Mark v12 as stable 2025-12-21 17:15:16 +00:00
Jade Ellis
8fd15f26ce style: Fix clippy 2025-12-21 17:12:36 +00:00
Jade Ellis
705fa6c5c6 fix: Simplify visibility check code 2025-12-21 17:12:36 +00:00
Jade Ellis
6f67c27538 fix: Ensure that room ID is present on state events sent to client
routes

Mostly fixes !1094

The remaining issue is federation routes
2025-12-21 17:12:35 +00:00
Jade Ellis
8586d747d1 feat: Run visibility checks on bundled relations 2025-12-21 17:12:35 +00:00
Jade Ellis
11012a9ce1 fix: Always return the same 404 message in context 2025-12-21 17:12:35 +00:00
Jade Ellis
07be190507 fix: Return 404 when event is not accessible 2025-12-21 17:12:35 +00:00
Jade Ellis
ae4acc9568 fix: Don't incorrectly add thread root to relation response 2025-12-21 17:12:35 +00:00
Jade Ellis
f83ddecd8c refactor(perf): Push down visibility check after limit 2025-12-21 17:12:34 +00:00
Jade Ellis
dd87232f1f refactor: Reduce database lookups in some cases 2025-12-21 17:12:34 +00:00
Jade Ellis
8e33f9a7d0 refactor: Improve code style for bundled aggregations 2025-12-21 17:12:34 +00:00
Jade Ellis
8d3e4eba99 fix: Add aggregations to the search endpoint 2025-12-21 17:12:34 +00:00
Jade Ellis
96bfdb97da fix: Filter out invalid replacements from bundled aggregations 2025-12-21 17:12:34 +00:00
Jade Ellis
b61010da47 feat: Add bundled aggregations support
Add support for the m.replace and m.reference bundled
aggregations.
This should fix plenty of subtle client issues.
Threads are not included in the new code as they have
historically been written to the database. Replacing the
old system would result in issues when switching away from
continuwuity, so saved for later.
Some TODOs have been left re event visibility and ignored users.
These should be OK for now, though.
2025-12-21 17:12:34 +00:00
Jade Ellis
987c5eeb03 refactor: Promote handling unsigned data out of timeline
Also fixes:
- Transaction IDs leaking in event route
- Age not being set for event relations or threads
- Both of the above for search results

Notes down concern with relations table
2025-12-21 17:12:33 +00:00
timedout
7fa4fa9862 fix: Also check sender origin 2025-12-21 10:58:50 +00:00
timedout
b2bead67ac fix: Apply additional validation to invites 2025-12-21 10:10:54 +00:00
timedout
48a6a475ce fix: Omit children with invalid state from space summary 2025-12-18 19:48:58 +00:00
timedout
86450da705 style: Run clippy 2025-12-18 19:48:26 +00:00
timedout
8538b21860 feat: Check for incoming signatures 2025-12-18 19:03:32 +00:00
timedout
63e4aacd2b style: Reword TODO comment 2025-12-18 18:24:00 +00:00
timedout
72f0eb9493 feat: Fetch policy server signatures 2025-12-18 18:23:54 +00:00
Odd E. Ebbesen
867d0ab671 fix(reload): Store paths to config files for admin reload
Paths given via --config at startup are now stored inside the config
struct at runtime, to make it possible to reload config without setting
an env var for the config file location.
2025-12-16 14:58:33 +00:00
Ginger
64e187e5b4 fix: Update comment in src/core/config/mod.rs 2025-12-16 14:19:43 +00:00
aviac
5dc449a87a test: add test for config with default_room_version
This commit refactors the test a bit to run the basic test script with
different configs. Currently we have two configs we test:

- the bare minimum to make it run (base)
- base + default_room_version set to "12"
2025-12-16 14:19:43 +00:00
aviac
f5fda01013 docs: Add note about the type of the default_room_version option 2025-12-16 14:19:43 +00:00
Jade Ellis
cdc53b3421 fix: Allow using LDAP passwords in UIAA
Fixes #1131

Co-authored-by: Jade Ellis <jade@ellis.link>
2025-12-16 13:55:32 +00:00
Ginger
0b667ae4fd fix(ci): Try explicitly specifying the ref for debian/fedora workflows 2025-12-15 10:21:46 -05:00
unbeatable-101
83baf9b524 Keep location of Continuwuity configuration file consitant 2025-12-13 22:51:16 +00:00
timedout
4f198fb4ef fix: Enforce limits when joining rooms 2025-12-13 22:17:47 +00:00
timedout
1631c0afa4 fix: Perform additional validation on events 2025-12-13 21:36:20 +00:00
Charlotte Hartmann Paludo
862684af28 fix: remove trailing whitespace from secrets read from secrets file 2025-12-13 16:07:51 +00:00
Ginger
7345c241a9 fix: Don't halt and catch fire on deserialization errors in MSC4133 migration 2025-12-12 11:16:52 -05:00
Ginger
6a8b988b36 fix(ci): Downgrade upload-artifact actions again to v3 this time 2025-12-10 11:33:36 -05:00
Ginger
f1d6536793 fix(ci): Downgrade upload-artifact actions to v4 2025-12-10 11:33:36 -05:00
Ginger
cf8d8e4ea6 chore: Post-rebase cleanup 2025-12-09 03:25:04 +00:00
timedout
393d341f07 perf: Throttle frequent device metadata updates & centralise site 2025-12-09 03:25:03 +00:00
timedout
ba55dffa0e perf: Don't increment the device list version when updating local info 2025-12-09 03:25:03 +00:00
timedout
f3115e14ab feat: Update device metadata upon hitting hot endpoints 2025-12-09 03:25:03 +00:00
Ginger
b3fa4705ef chore: Fix line endings 2025-12-07 15:28:19 -05:00
Ginger
53b06a7918 chore(sync/v3): Remove unused imports 2025-12-07 19:58:24 +00:00
Ginger
fafc1d3fd1 fix(sync/v3): Don't send rejected invites on initial syncs 2025-12-07 19:58:24 +00:00
Ginger
dbc74272c3 refactor(sync/v3): Extract left room timeline logic into its own function 2025-12-07 19:58:24 +00:00
Ginger
f11caac05e fix(sync/v3): Don't send dummy leaves on an initial sync 2025-12-07 19:58:24 +00:00
Ginger
e581face44 chore: Formatting 2025-12-07 19:58:24 +00:00
ginger
037ba41adb fix: Nitpicky comment reword 2025-12-07 19:58:24 +00:00
Ginger
941c8f7d52 fix: Bump max startup time to ten minutes in the systemd unit 2025-12-07 19:58:24 +00:00
Ginger
7dae118af9 chore(sync/v3): More goat sacrifices 2025-12-07 19:58:24 +00:00
Ginger
07dfc5528d refactor(sync/v3): Split load_joined_room into smaller functions 2025-12-07 19:58:24 +00:00
ginger
3f4749a796 fix: Correct error message 2025-12-07 19:58:24 +00:00
Ginger
be8d72fafc fix(sync/v3): Add a workaround for matrix-js-sdk/5071 2025-12-07 19:58:24 +00:00
Ginger
0008709481 fix(sync/v3): Stop ignoring leave cache deserialization failures 2025-12-07 19:58:24 +00:00
Ginger
ee51d4357f fix(sync/v3): Do not include the last membership event when syncing left rooms 2025-12-07 19:58:24 +00:00
Ginger
8ffc6d4f15 chore(sync/v3): Sacrifice a goat to clippy 2025-12-07 19:58:24 +00:00
Ginger
93efe89a1f fix(sync/v3): Cache shortstatehashes to speed up migration 2025-12-07 19:58:24 +00:00
Ginger
16f37d21ff fix(sync/v3): Implement a migration for the userroomid_leftstate table 2025-12-07 19:58:24 +00:00
Ginger
800ac8d1f1 fix(sync/v3): Fix invite filtering for federated invites 2025-12-07 19:58:24 +00:00
Ginger
872f5bf077 feat(sync/v3): Remove TL size config option in favor of using the sync filter 2025-12-07 19:58:24 +00:00
Ginger
992217d644 chore(sync/v3): Fix clippy lints 2025-12-07 19:58:24 +00:00
Ginger
4fb4397a9f fix(sync/v3): Remove mysterious membership event manipulation code 2025-12-07 19:58:24 +00:00
Ginger
61b6947e88 fix(sync/v3): Properly sync room heroes 2025-12-07 19:58:24 +00:00
Ginger
876d3faec4 chore(sync/v3): Use "build_*" terminology instead of "calculate_*" 2025-12-07 19:58:24 +00:00
Ginger
9cc0cc69f7 chore(sync/v3): Use more descriptive names for SyncContext properties 2025-12-07 19:58:24 +00:00
Ginger
5513bb4dff chore: Remove unneeded comment 2025-12-07 19:58:24 +00:00
Ginger
693e327004 fix: Use prepare_lazily_loaded_members for joined rooms
Also, don't take read receipts into consideration for lazy loading.
Synapse doesn't do this and they're making initial syncs very large.
2025-12-07 19:58:24 +00:00
Ginger
3e6571a2b8 chore: Clippy fixes 2025-12-07 19:58:24 +00:00
Jade Ellis
f0f10f8f3e feat: Typing notifications in simplified sliding sync
What's missing? Being able to use separate rooms & lists for typing
indicators.
At the moment, we use the same ones as we use for the timeline, as
todo_rooms is quite intertwined. We need to disentangle this to get that
functionality, although I'm not sure if clients use it.
2025-12-07 19:58:24 +00:00
Ginger
a4f2b55a8a feat: Add a config option to change the max TL size for legacy sync 2025-12-07 19:58:24 +00:00
Ginger
213a361c53 fix: Set limited to true for newly joined rooms again 2025-12-07 19:58:24 +00:00
Ginger
1c21e4af6e fix: Properly sync left rooms
- Remove most usages of `update_membership` in favor
  of directly calling the `mark_as_*` functions
- Store the leave membership event as the value in the
  `userroomid_leftstate` table
- Use the `userroomid_leftstate` table to synchronize the
  timeline and state for left rooms if possible
2025-12-07 19:58:24 +00:00
Ginger
fceaaedc04 fix: Properly sync newly joined rooms 2025-12-07 19:58:24 +00:00
Ginger
0eff173c0b fix(sync/v3): Further cleanup + improve incremental sync consistency 2025-12-07 19:58:24 +00:00
Ginger
72bf8e5927 fix: Correctly send limited timelines again 2025-12-07 19:58:24 +00:00
Ginger
3491f653a5 refactor: Split sync v3 into multiple files 2025-12-07 19:58:24 +00:00
Ginger
e820dd7aed feat: Drop support for MSC3575 (legacy sliding sync) 2025-12-07 19:58:24 +00:00
Ginger
c92b7239a8 chore: Clippy fixes 2025-12-07 19:58:24 +00:00
Ginger
2940bc69c1 fix(sync/v3): Cleanup part 1: mostly fix redundant data in state 2025-12-07 19:58:24 +00:00
Jade
502919b248 chore: Tell continuwuity.org to use my livekit instance 2025-12-04 14:23:02 +00:00
Renovate Bot
33c3d23d60 chore(deps): update rust-patch-updates 2025-11-29 05:01:44 +00:00
Renovate Bot
ce318fe455 chore(deps): update pre-commit hook crate-ci/typos to v1.40.0 2025-11-28 20:19:35 +00:00
Renovate Bot
a729e1d63d chore(deps): update actions/upload-artifact action to v5 2025-11-28 20:19:10 +00:00
Ginger
956c3dfa62 chore: Fix deprecation warning 2025-11-28 15:08:20 -05:00
Renovate Bot
49e8f06559 chore(deps): update rust-patch-updates 2025-11-28 15:00:38 -05:00
rooot
c0f4424cb9 fix(docs): blurry small logo, scroll resizing top bar
Signed-off-by: rooot <hey@rooot.gay>
2025-11-27 13:53:12 +01:00
Tobias Fella
3eac985c5e fix(docs): Correct typo and outdated name 2025-11-26 21:13:43 +01:00
Jade
5fd341096d fix(docs): Dead link 2025-11-26 00:10:59 +00:00
Renovate Bot
a1b2d6ec46 chore(deps): update dependency @rspress/plugin-client-redirects to v2.0.0-rc.1 2025-11-25 21:50:27 +00:00
Renovate Bot
551563ce83 chore(deps): update dependency @rspress/plugin-preview to v2.0.0-rc.1 2025-11-25 18:38:26 +00:00
Ginger
9f133cf75b chore(deps): Update actions/checkout to v6 2025-11-25 18:26:28 +00:00
Ginger
23c398dc1e fix(ci): Remove explicit references to code.forgejo.org in action steps 2025-11-25 18:26:28 +00:00
Renovate Bot
fa73893179 chore(deps): update pre-commit hook crate-ci/committed to v1.1.8 2025-11-25 17:16:13 +00:00
Renovate Bot
57fec44ec7 chore(deps): update dependency cargo-bins/cargo-binstall to v1.16.2 2025-11-24 05:02:19 +00:00
timedout
bc8d304dbf style: Fix unnecessary qualification
Sounds like my university experience
2025-11-23 16:33:32 +00:00
timedout
7f4248a8c6 feat: Enhance remote room leave handling 2025-11-23 16:33:32 +00:00
aviac
430200b60e fix: add explicit fix of rpath
Without this the rpath is empty and the binary won't be able to load the librocksdb.so.10 shared lib
2025-11-23 16:26:27 +00:00
Jade Ellis
a573f1f502 docs: Disable incomplete homepage prose for now 2025-11-22 23:13:52 +00:00
Jade Ellis
3bf3c24d22 docs: Update homepage layout 2025-11-22 22:41:53 +00:00
Jade Ellis
c1dc336c65 docs: Fix a couple contrast issues 2025-11-22 20:58:18 +00:00
Jade Ellis
be3c6ebb58 docs: Update rspress, add redirects 2025-11-22 20:49:19 +00:00
Jade Ellis
bdf31fa92e docs: Remove exclude rules 2025-11-22 20:35:13 +00:00
Jade Ellis
1372f74812 docs: Capitalise page title 2025-11-22 20:35:13 +00:00
Jade Ellis
5935d99af0 feat(docs): Sitemap 2025-11-22 20:35:13 +00:00
Jade Ellis
6b11a65545 fix: Repair symlink 2025-11-22 20:35:13 +00:00
Jade Ellis
5ea1206739 docs: New site 🎉 2025-11-22 20:35:09 +00:00
Jade Ellis
d45c5a9f47 feat: Deal with low screen widths in CSS 2025-11-22 20:30:30 +00:00
Jade Ellis
9a1039b215 docs: Update logos 2025-11-22 20:30:29 +00:00
Renovate Bot
59f9b8bdb8 chore(deps): update rust crate bytes to v1.11.0 2025-11-22 16:13:45 +00:00
lily
3b76e9876d fix: revert to mdbook version 0.4.52 since >=0.5.0 broke rendering with our current setup completely 2025-11-22 17:05:14 +01:00
nex
64c059f82e fix(docs): Partially revert 4a9bea5764
Signed-off-by: nex <git@nexy7574.co.uk>
2025-11-22 14:15:26 +00:00
lily
76b90acea6 Remove rust flags since they're pretty much outdated and don't work anymore
Not much difference from what I've observed, plus it's as max-perf as it gets
2025-11-21 17:12:27 +00:00
159 changed files with 10540 additions and 4344 deletions

View File

@@ -32,11 +32,13 @@ outputs:
runs:
using: composite
steps:
- run: mkdir -p digests
shell: bash
- name: Download digests
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
uses: forgejo/download-artifact@v4
with:
path: /tmp/digests
path: digests
pattern: ${{ inputs.digest_pattern }}
merge-multiple: true
@@ -78,7 +80,7 @@ runs:
- name: Create manifest list and push
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
working-directory: /tmp/digests
working-directory: digests
shell: bash
env:
IMAGES: ${{ inputs.images }}

View File

@@ -32,12 +32,13 @@ jobs:
echo "Debian distribution: $DISTRIBUTION ($VERSION)"
- name: Checkout repository with full history
uses: https://code.forgejo.org/actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ github.ref_name }}
- name: Cache Cargo registry
uses: https://code.forgejo.org/actions/cache@v4
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
@@ -126,7 +127,7 @@ jobs:
[ -f /etc/conduwuit/conduwuit.toml ] && echo "✅ Config file installed"
- name: Upload deb artifact
uses: https://code.forgejo.org/actions/upload-artifact@v3
uses: forgejo/upload-artifact@v4
with:
name: continuwuity-${{ steps.debian-version.outputs.distribution }}
path: ${{ steps.cargo-deb.outputs.path }}

View File

@@ -30,13 +30,14 @@ jobs:
echo "Fedora version: $VERSION"
- name: Checkout repository with full history
uses: https://code.forgejo.org/actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ github.ref_name }}
- name: Cache DNF packages
uses: https://code.forgejo.org/actions/cache@v4
uses: actions/cache@v4
with:
path: |
/var/cache/dnf
@@ -46,7 +47,7 @@ jobs:
dnf-fedora${{ steps.fedora.outputs.version }}-
- name: Cache Cargo registry
uses: https://code.forgejo.org/actions/cache@v4
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
@@ -56,7 +57,7 @@ jobs:
cargo-fedora${{ steps.fedora.outputs.version }}-
- name: Cache Rust build dependencies
uses: https://code.forgejo.org/actions/cache@v4
uses: actions/cache@v4
with:
path: |
~/rpmbuild/BUILD/*/target/release/deps
@@ -238,13 +239,13 @@ jobs:
cp $BIN_RPM upload-bin/
- name: Upload binary RPM
uses: https://code.forgejo.org/actions/upload-artifact@v3
uses: forgejo/upload-artifact@v4
with:
name: continuwuity
path: upload-bin/
- name: Upload debug RPM artifact
uses: https://code.forgejo.org/actions/upload-artifact@v3
uses: forgejo/upload-artifact@v4
with:
name: continuwuity-debug
path: artifacts/*debuginfo*.rpm

View File

@@ -21,34 +21,11 @@ jobs:
steps:
- name: Sync repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
fetch-depth: 0
- name: Setup mdBook
uses: https://github.com/peaceiris/actions-mdbook@v2
with:
mdbook-version: "latest"
- name: Build mdbook
run: mdbook build
- name: Prepare static files for deployment
run: |
mkdir -p ./public/.well-known/matrix
mkdir -p ./public/.well-known/continuwuity
mkdir -p ./public/schema
# Copy the Matrix .well-known files
cp ./docs/static/server ./public/.well-known/matrix/server
cp ./docs/static/client ./public/.well-known/matrix/client
cp ./docs/static/client ./public/.well-known/matrix/support
cp ./docs/static/announcements.json ./public/.well-known/continuwuity/announcements
cp ./docs/static/announcements.schema.json ./public/schema/announcements.schema.json
# Copy the custom headers file
cp ./docs/static/_headers ./public/_headers
echo "Copied .well-known files and _headers to ./public"
- name: Detect runner environment
id: runner-env
uses: https://git.tomfos.tr/actions/detect-versions@v1
@@ -63,9 +40,18 @@ jobs:
uses: actions/cache@v3
with:
path: ~/.npm
key: continuwuity-${{ steps.runner-env.outputs.slug }}-${{ steps.runner-env.outputs.arch }}-node-${{ steps.runner-env.outputs.node_version }}
key: continuwuity-rspress-${{ steps.runner-env.outputs.slug }}-${{ steps.runner-env.outputs.arch }}-node-${{ steps.runner-env.outputs.node_version }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
continuwuity-rspress-${{ steps.runner-env.outputs.slug }}-${{ steps.runner-env.outputs.arch }}-node-${{ steps.runner-env.outputs.node_version }}-
continuwuity-rspress-${{ steps.runner-env.outputs.slug }}-${{ steps.runner-env.outputs.arch }}-node-
- name: Install dependencies
run: npm ci
- name: Build Rspress documentation
run: npm run docs:build
- name: Install Wrangler
run: npm install --save-dev wrangler@latest
- name: Deploy to Cloudflare Pages (Production)
@@ -74,7 +60,7 @@ jobs:
with:
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: pages deploy ./public --branch="main" --commit-dirty=true --project-name="${{ vars.CLOUDFLARE_PROJECT_NAME }}"
command: pages deploy ./doc_build --branch="main" --commit-dirty=true --project-name="${{ vars.CLOUDFLARE_PROJECT_NAME }}"
- name: Deploy to Cloudflare Pages (Preview)
if: github.ref != 'refs/heads/main' && vars.CLOUDFLARE_PROJECT_NAME != ''
@@ -82,4 +68,4 @@ jobs:
with:
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: pages deploy ./public --branch="${{ github.head_ref || github.ref_name }}" --commit-dirty=true --project-name="${{ vars.CLOUDFLARE_PROJECT_NAME }}"
command: pages deploy ./doc_build --branch="${{ github.head_ref || github.ref_name }}" --commit-dirty=true --project-name="${{ vars.CLOUDFLARE_PROJECT_NAME }}"

View File

@@ -38,7 +38,7 @@ jobs:
DOCKER_MIRROR_TOKEN: ${{ secrets.DOCKER_MIRROR_TOKEN }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
@@ -47,7 +47,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false

View File

@@ -18,6 +18,12 @@ on:
- "v*.*.*"
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
inputs:
commit:
description: 'Commit SHA to build (default: latest commit on the branch)'
required: false
default: ""
type: string
env:
BUILTIN_REGISTRY: forgejo.ellis.link
@@ -42,10 +48,14 @@ jobs:
slug: "linux-arm64"
steps:
- name: Pick checkout ref
run:
echo "COMMIT_REF=${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.commit != '' && github.event.inputs.commit || github.sha) || github.sha }}" >> $GITHUB_ENV
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ env.COMMIT_REF }}
- name: Prepare Docker build environment
id: prepare
uses: ./.forgejo/actions/prepare-docker-build
@@ -97,7 +107,7 @@ jobs:
needs: build-release
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Create multi-platform manifest
@@ -130,7 +140,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Prepare max-perf Docker build environment
@@ -184,7 +194,7 @@ jobs:
needs: build-maxperf
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Create max-perf manifest

View File

@@ -47,7 +47,7 @@ jobs:
options: --tmpfs /tmp:exec
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
show-progress: false

View File

@@ -14,7 +14,7 @@ jobs:
update-flake-hashes:
runs-on: ubuntu-latest
steps:
- uses: https://code.forgejo.org/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
fetch-tags: false

9
.gitignore vendored
View File

@@ -79,7 +79,7 @@ test-conduit.toml
/.gitlab-ci.d
# mdbook output
public/
/public/
# macOS
.DS_Store
@@ -95,3 +95,10 @@ rustc-ice-*
# complement test logs are huge
tests/test_results/complement/test_logs.jsonl
# Node
node_modules/
# Rspress
doc_build/
.rspress/

View File

@@ -2,6 +2,7 @@ AlexPewMaster <git@alex.unbox.at> <68469103+AlexPewMaster@users.noreply.github.c
Daniel Wiesenberg <weasy@hotmail.de> <weasy666@gmail.com>
Devin Ragotzy <devin.ragotzy@gmail.com> <d6ragotzy@wmich.edu>
Devin Ragotzy <devin.ragotzy@gmail.com> <dragotzy7460@mail.kvcc.edu>
Ginger <ginger@gingershaped.computer> <75683114+gingershaped@users.noreply.github.com>
Jonas Platte <jplatte+git@posteo.de> <jplatte+gitlab@posteo.de>
Jonas Zohren <git-pbkyr@jzohren.de> <gitlab-jfowl-0ux98@sh14.de>
Jonathan de Jong <jonathan@automatia.nl> <jonathandejong02@gmail.com>
@@ -12,5 +13,6 @@ Olivia Lee <olivia@computer.surgery> <benjamin@computer.surgery>
Rudi Floren <rudi.floren@gmail.com> <rudi.floren@googlemail.com>
Tamara Schmitz <tamara.zoe.schmitz@posteo.de> <15906939+tamara-schmitz@users.noreply.github.com>
Timo Kösters <timo@koesters.xyz>
nexy7574 <git@nexy7574.co.uk> <nex@noreply.forgejo.ellis.link>
nexy7574 <git@nexy7574.co.uk> <nex@noreply.localhost>
x4u <xi.zhu@protonmail.ch> <14617923-x4u@users.noreply.gitlab.com>
Ginger <ginger@gingershaped.computer> <75683114+gingershaped@users.noreply.github.com>

View File

@@ -23,7 +23,7 @@ repos:
- id: check-added-large-files
- repo: https://github.com/crate-ci/typos
rev: v1.39.2
rev: v1.40.0
hooks:
- id: typos
- id: typos
@@ -31,7 +31,7 @@ repos:
stages: [commit-msg]
- repo: https://github.com/crate-ci/committed
rev: v1.1.7
rev: v1.1.8
hooks:
- id: committed

View File

@@ -1,7 +1,7 @@
# Contributing guide
This page is about contributing to Continuwuity. The
[development](./development.md) and [code style guide](./development/code_style.md) pages may be of interest for you as well.
[development](/development/index.mdx) and [code style guide](/development/code_style.mdx) pages may be of interest for you as well.
If you would like to work on an [issue][issues] that is not assigned, preferably
ask in the Matrix room first at [#continuwuity:continuwuity.org][continuwuity-matrix],
@@ -9,7 +9,7 @@ # Contributing guide
### Code Style
Please review and follow the [code style guide](./development/code_style.md) for formatting, linting, naming conventions, and other code standards.
Please review and follow the [code style guide](/development/code_style.mdx) for formatting, linting, naming conventions, and other code standards.
### Pre-commit Checks
@@ -150,7 +150,7 @@ ### Creating pull requests
Before submitting a pull request, please ensure:
1. Your code passes all CI checks (formatting, linting, typo detection, etc.)
2. Your code follows the [code style guide](./development/code_style.md)
2. Your code follows the [code style guide](/development/code_style.md)
3. Your commit messages follow the conventional commits format
4. Tests are added for new functionality
5. Documentation is updated if needed

153
Cargo.lock generated
View File

@@ -737,9 +737,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.10.1"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
[[package]]
name = "bytesize"
@@ -831,9 +831,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.52"
version = "4.5.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa8120877db0e5c011242f96806ce3c94e0737ab8108532a76a3300a01db2ab8"
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
dependencies = [
"clap_builder",
"clap_derive",
@@ -850,9 +850,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.52"
version = "4.5.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02576b399397b659c26064fbc92a75fede9d18ffd5f80ca1cd74ddab167016e1"
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
dependencies = [
"anstream",
"anstyle",
@@ -940,7 +940,7 @@ dependencies = [
[[package]]
name = "conduwuit"
version = "0.5.0-rc.8.1"
version = "0.5.0"
dependencies = [
"clap",
"conduwuit_admin",
@@ -972,7 +972,7 @@ dependencies = [
[[package]]
name = "conduwuit_admin"
version = "0.5.0-rc.8.1"
version = "0.5.0"
dependencies = [
"clap",
"conduwuit_api",
@@ -994,7 +994,7 @@ dependencies = [
[[package]]
name = "conduwuit_api"
version = "0.5.0-rc.8.1"
version = "0.5.0"
dependencies = [
"async-trait",
"axum 0.7.9",
@@ -1027,14 +1027,14 @@ dependencies = [
[[package]]
name = "conduwuit_build_metadata"
version = "0.5.0-rc.8.1"
version = "0.5.0"
dependencies = [
"built",
]
[[package]]
name = "conduwuit_core"
version = "0.5.0-rc.8.1"
version = "0.5.0"
dependencies = [
"argon2",
"arrayvec",
@@ -1095,7 +1095,7 @@ dependencies = [
[[package]]
name = "conduwuit_database"
version = "0.5.0-rc.8.1"
version = "0.5.0"
dependencies = [
"async-channel",
"conduwuit_core",
@@ -1114,7 +1114,7 @@ dependencies = [
[[package]]
name = "conduwuit_macros"
version = "0.5.0-rc.8.1"
version = "0.5.0"
dependencies = [
"itertools 0.14.0",
"proc-macro2",
@@ -1124,7 +1124,7 @@ dependencies = [
[[package]]
name = "conduwuit_router"
version = "0.5.0-rc.8.1"
version = "0.5.0"
dependencies = [
"axum 0.7.9",
"axum-client-ip",
@@ -1159,7 +1159,7 @@ dependencies = [
[[package]]
name = "conduwuit_service"
version = "0.5.0-rc.8.1"
version = "0.5.0"
dependencies = [
"async-trait",
"base64 0.22.1",
@@ -1169,7 +1169,6 @@ dependencies = [
"conduwuit_database",
"const-str",
"ctor",
"dashmap",
"either",
"futures",
"hickory-resolver",
@@ -1201,7 +1200,7 @@ dependencies = [
[[package]]
name = "conduwuit_web"
version = "0.5.0-rc.8.1"
version = "0.5.0"
dependencies = [
"askama",
"axum 0.7.9",
@@ -1526,20 +1525,6 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "817fa642fb0ee7fe42e95783e00e0969927b96091bdd4b9b1af082acd943913b"
[[package]]
name = "dashmap"
version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
dependencies = [
"cfg-if",
"crossbeam-utils",
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]]
name = "data-encoding"
version = "2.9.0"
@@ -1765,7 +1750,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -2127,12 +2112,6 @@ version = "0.1.2+12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "647deb1583b14d160f85f3ff626f20b6edd366e3852c9843b06077388f794cb6"
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hashbrown"
version = "0.15.5"
@@ -2426,7 +2405,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
"socket2 0.6.1",
"socket2 0.5.10",
"tokio",
"tower-service",
"tracing",
@@ -3011,23 +2990,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minicbor"
version = "2.1.1"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f182275033b808ede9427884caa8e05fa7db930801759524ca7925bd8aa7a82"
dependencies = [
"minicbor-derive",
]
[[package]]
name = "minicbor-derive"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b17290c95158a760027059fe3f511970d6857e47ff5008f9e09bffe3d3e1c6af"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
checksum = "f9a1119e42fbacc2bb65d860de6eb7c930562bc71d42dca026d06b0228231f77"
[[package]]
name = "minicbor-serde"
@@ -3041,9 +3006,9 @@ dependencies = [
[[package]]
name = "minimad"
version = "0.13.1"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9c5d708226d186590a7b6d4a9780e2bdda5f689e0d58cd17012a298efd745d2"
checksum = "df8b688969b16915f3ecadc7829d5b7779dee4977e503f767f34136803d5c06f"
dependencies = [
"once_cell",
]
@@ -3156,7 +3121,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -3782,7 +3747,7 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls",
"socket2 0.6.1",
"socket2 0.5.10",
"thiserror 2.0.17",
"tokio",
"tracing",
@@ -3819,7 +3784,7 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2 0.6.1",
"socket2 0.5.10",
"tracing",
"windows-sys 0.52.0",
]
@@ -4065,9 +4030,9 @@ dependencies = [
[[package]]
name = "resolv-conf"
version = "0.7.5"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799"
checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7"
[[package]]
name = "rgb"
@@ -4098,7 +4063,7 @@ checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3"
[[package]]
name = "ruma"
version = "0.10.1"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
dependencies = [
"assign",
"js_int",
@@ -4118,7 +4083,7 @@ dependencies = [
[[package]]
name = "ruma-appservice-api"
version = "0.10.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
dependencies = [
"js_int",
"ruma-common",
@@ -4130,7 +4095,7 @@ dependencies = [
[[package]]
name = "ruma-client-api"
version = "0.18.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
dependencies = [
"as_variant",
"assign",
@@ -4153,7 +4118,7 @@ dependencies = [
[[package]]
name = "ruma-common"
version = "0.13.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
dependencies = [
"as_variant",
"base64 0.22.1",
@@ -4185,7 +4150,7 @@ dependencies = [
[[package]]
name = "ruma-events"
version = "0.28.1"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
dependencies = [
"as_variant",
"indexmap",
@@ -4210,7 +4175,7 @@ dependencies = [
[[package]]
name = "ruma-federation-api"
version = "0.9.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
dependencies = [
"bytes",
"headers",
@@ -4232,7 +4197,7 @@ dependencies = [
[[package]]
name = "ruma-identifiers-validation"
version = "0.9.5"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
dependencies = [
"js_int",
"thiserror 2.0.17",
@@ -4241,7 +4206,7 @@ dependencies = [
[[package]]
name = "ruma-identity-service-api"
version = "0.9.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
dependencies = [
"js_int",
"ruma-common",
@@ -4251,7 +4216,7 @@ dependencies = [
[[package]]
name = "ruma-macros"
version = "0.13.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
dependencies = [
"cfg-if",
"proc-macro-crate",
@@ -4266,7 +4231,7 @@ dependencies = [
[[package]]
name = "ruma-push-gateway-api"
version = "0.9.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
dependencies = [
"js_int",
"ruma-common",
@@ -4278,7 +4243,7 @@ dependencies = [
[[package]]
name = "ruma-signatures"
version = "0.15.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=50b2a91b2ab8f9830eea80b9911e11234e0eac66#50b2a91b2ab8f9830eea80b9911e11234e0eac66"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9"
dependencies = [
"base64 0.22.1",
"ed25519-dalek",
@@ -4358,7 +4323,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -4664,9 +4629,9 @@ dependencies = [
[[package]]
name = "serde-saphyr"
version = "0.0.8"
version = "0.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0916ccf524f1ccec1b3c02193c9e3d2e167aee9b6b294829dce6f4411332155"
checksum = "9b9e06cddad47cc6214c0c456cf209b99a58b54223e7af2f6d4b88a5a9968499"
dependencies = [
"ahash",
"base64 0.22.1",
@@ -4995,9 +4960,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.110"
version = "2.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
dependencies = [
"proc-macro2",
"quote",
@@ -5043,9 +5008,9 @@ dependencies = [
[[package]]
name = "termimad"
version = "0.34.0"
version = "0.34.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ff5ca043d65d4ea43b65cdb4e3aba119657d0d12caf44f93212ec3168a8e20"
checksum = "889a9370996b74cf46016ce35b96c248a9ac36d69aab1d112b3e09bc33affa49"
dependencies = [
"coolor",
"crokey",
@@ -5441,9 +5406,9 @@ dependencies = [
[[package]]
name = "tower-http"
version = "0.6.6"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456"
dependencies = [
"async-compression",
"bitflags",
@@ -5477,9 +5442,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.41"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647"
dependencies = [
"pin-project-lite",
"tracing-attributes",
@@ -5488,9 +5453,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
version = "0.1.30"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
@@ -5499,9 +5464,9 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.34"
version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c"
dependencies = [
"once_cell",
"valuable",
@@ -5520,9 +5485,9 @@ dependencies = [
[[package]]
name = "tracing-journald"
version = "0.3.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0b4143302cf1022dac868d521e36e8b27691f72c84b3311750d5188ebba657"
checksum = "2d3a81ed245bfb62592b1e2bc153e77656d94ee6a0497683a65a12ccaf2438d0"
dependencies = [
"libc",
"tracing-core",
@@ -5561,9 +5526,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.20"
version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
dependencies = [
"matchers",
"nu-ansi-term",
@@ -6239,7 +6204,7 @@ dependencies = [
[[package]]
name = "xtask"
version = "0.5.0-rc.8.1"
version = "0.5.0"
dependencies = [
"clap",
"serde",
@@ -6248,7 +6213,7 @@ dependencies = [
[[package]]
name = "xtask-generate-commands"
version = "0.5.0-rc.8.1"
version = "0.5.0"
dependencies = [
"clap-markdown",
"clap_builder",

View File

@@ -21,7 +21,7 @@ license = "Apache-2.0"
readme = "README.md"
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
rust-version = "1.86.0"
version = "0.5.0-rc.8.1"
version = "0.5.0"
[workspace.metadata.crane]
name = "conduwuit"
@@ -167,7 +167,7 @@ features = ["raw_value"]
# Used for appservice registration files
[workspace.dependencies.serde-saphyr]
version = "0.0.8"
version = "0.0.10"
# Used to load forbidden room/user regex from config
[workspace.dependencies.serde_regex]
@@ -351,7 +351,7 @@ version = "0.1.2"
# Used for matrix spec type definitions and helpers
[workspace.dependencies.ruma]
git = "https://forgejo.ellis.link/continuwuation/ruwuma"
rev = "50b2a91b2ab8f9830eea80b9911e11234e0eac66"
rev = "27abe0dcd33fd4056efc94bab3582646b31b6ce9"
features = [
"compat",
"rand",
@@ -554,6 +554,9 @@ version = "0.12.0"
default-features = false
features = ["sync", "tls-rustls", "rustls-provider"]
[workspace.dependencies.resolv-conf]
version = "0.7.5"
#
# Patches
#
@@ -667,24 +670,6 @@ panic = "abort"
inherits = "release"
strip = "symbols"
lto = "fat"
#rustflags = [
# '-Ctarget-cpu=native',
# '-Ztune-cpu=native',
# '-Ctarget-feature=+crt-static',
# '-Crelocation-model=static',
# '-Ztls-model=local-exec',
# '-Zinline-in-all-cgus=true',
# '-Zinline-mir=true',
# '-Zmir-opt-level=3',
# '-Clink-arg=-fuse-ld=gold',
# '-Clink-arg=-Wl,--threads',
# '-Clink-arg=-Wl,--gc-sections',
# '-Clink-arg=-luring',
# '-Clink-arg=-lstdc++',
# '-Clink-arg=-lc',
# '-Ztime-passes',
# '-Ztime-llvm-passes',
#]
[profile.release-max-perf.build-override]
inherits = "release-max-perf"

View File

@@ -22,7 +22,7 @@ ### Responsible Disclosure
1. **Contact members of the team directly** over E2EE private message.
- [@jade:ellis.link](https://matrix.to/#/@jade:ellis.link)
- [@nex:nexy7574.co.uk](https://matrix.to/#/@nex:nexy7574.co.uk) <!-- ? -->
- [@nex:nexy7574.co.uk](https://matrix.to/#/@nex:nexy7574.co.uk)
2. **Email the security team** at [security@continuwuity.org](mailto:security@continuwuity.org). This is not E2EE, so don't include sensitive details.
3. **Do not disclose the vulnerability publicly** until it has been addressed
4. **Provide detailed information** about the vulnerability, including:

View File

@@ -17,7 +17,7 @@ edition = "2024"
[output.html]
edit-url-template = "https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/{path}"
git-repository-url = "https://forgejo.ellis.link/continuwuation/continuwuity"
git-repository-icon = "fab-git-alt"
git-repository-icon = "fa-git-alt"
[output.html.search]
limit-results = 15

View File

@@ -586,10 +586,13 @@
#allow_unstable_room_versions = true
# Default room version continuwuity will create rooms with.
# Note that this has to be a string since the room version is a string
# rather than an integer. Forgetting the quotes will make the server fail
# to start!
#
# Per spec, room version 11 is the default.
# Per spec, room version "11" is the default.
#
#default_room_version = 11
#default_room_version = "11"
# Enable OpenTelemetry OTLP tracing export. This replaces the deprecated
# Jaeger exporter. Traces will be sent via OTLP to a collector (such as

View File

@@ -1 +1 @@
docs/development.md
docs/development/index.mdx

View File

@@ -48,7 +48,7 @@ EOF
# Developer tool versions
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
ENV BINSTALL_VERSION=1.16.0
ENV BINSTALL_VERSION=1.16.2
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree

View File

@@ -18,7 +18,7 @@ RUN --mount=type=cache,target=/etc/apk/cache apk add \
# Developer tool versions
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
ENV BINSTALL_VERSION=1.16.0
ENV BINSTALL_VERSION=1.16.2
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree

View File

@@ -1,26 +0,0 @@
# Summary
- [Introduction](introduction.md)
- [Configuration](configuration.md)
- [Examples](configuration/examples.md)
- [Deploying](deploying.md)
- [Generic](deploying/generic.md)
- [NixOS](deploying/nixos.md)
- [Docker](deploying/docker.md)
- [Kubernetes](deploying/kubernetes.md)
- [Arch Linux](deploying/arch-linux.md)
- [Debian](deploying/debian.md)
- [Fedora](deploying/fedora.md)
- [FreeBSD](deploying/freebsd.md)
- [TURN](turn.md)
- [Appservices](appservices.md)
- [Maintenance](maintenance.md)
- [Troubleshooting](troubleshooting.md)
- [Admin Command Reference](admin_reference.md)
- [Development](development.md)
- [Contributing](contributing.md)
- [Code Style Guide](development/code_style.md)
- [Testing](development/testing.md)
- [Hot Reloading ("Live" Development)](development/hot_reload.md)
- [Community (and Guidelines)](community.md)
- [Security](security.md)

74
docs/_meta.json Normal file
View File

@@ -0,0 +1,74 @@
[
{
"type": "file",
"name": "introduction",
"label": "Continuwuity"
},
{
"type": "file",
"name": "configuration",
"label": "Configuration"
},
{
"type": "dir",
"name": "deploying",
"label": "Deploying"
},
{
"type": "file",
"name": "turn",
"label": "TURN"
},
{
"type": "file",
"name": "appservices",
"label": "Appservices"
},
{
"type": "file",
"name": "maintenance",
"label": "Maintenance"
},
{
"type": "file",
"name": "troubleshooting",
"label": "Troubleshooting"
},
{
"type": "divider"
},
{
"type": "dir-section-header",
"name": "development",
"label": "Development",
"collapsible": true,
"collapsed": false
},
{
"type": "divider"
},
{
"type": "section-header",
"label": "Reference"
},
{
"type": "file",
"label": "Configuration Reference",
"name": "/reference/config"
},
{
"type": "file",
"label": "Admin Command Reference",
"name": "/reference/admin"
},
{
"type": "file",
"label": "Server Reference",
"name": "/reference/server"
},
{
"type": "divider"
},
"community",
"security"
]

37
docs/_nav.json Normal file
View File

@@ -0,0 +1,37 @@
[
{
"text": "Guide",
"link": "/introduction",
"activeMatch": "^/(introduction|configuration|deploying|turn|appservices|maintenance|troubleshooting)"
},
{
"text": "Development",
"link": "/development/index",
"activeMatch": "^/development/"
},
{
"text": "Reference",
"items": [
{
"text": "Configuration Reference",
"link": "/reference/config"
},
{
"text": "Admin Command Reference",
"link": "/reference/admin"
},
{
"text": "Server Reference",
"link": "/reference/server"
}
]
},
{
"text": "Community",
"link": "/community"
},
{
"text": "Security",
"link": "/security"
}
]

View File

@@ -1,36 +0,0 @@
<svg
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
width="100%"
viewBox="0 0 864 864"
enableBackground="new 0 0 864 864"
xmlSpace="preserve"
>
<path
fill="#EC008C"
opacity="1.000000"
stroke="none"
d="M0.999997,649.000000 C1.000000,433.052795 1.000000,217.105591 1.000000,1.079198 C288.876801,1.079198 576.753601,1.079198 865.000000,1.079198 C865.000000,73.025414 865.000000,145.051453 864.634888,217.500671 C852.362488,223.837280 840.447632,229.735275 828.549438,235.666794 C782.143677,258.801056 735.743225,281.945923 688.998657,304.980469 C688.122009,304.476532 687.580750,304.087708 687.053894,303.680206 C639.556946,266.944733 573.006775,291.446869 560.804199,350.179443 C560.141357,353.369446 559.717590,356.609131 559.195374,359.748962 C474.522705,359.748962 390.283478,359.748962 306.088135,359.748962 C298.804138,318.894806 265.253357,295.206024 231.834442,293.306793 C201.003021,291.554596 169.912033,310.230042 156.935104,338.792725 C149.905151,354.265930 147.884064,370.379944 151.151794,387.034515 C155.204453,407.689667 166.300507,423.954224 183.344437,436.516663 C181.938263,437.607025 180.887405,438.409576 179.849426,439.228516 C147.141953,465.032562 139.918045,510.888947 163.388611,545.322632 C167.274551,551.023804 172.285187,555.958313 176.587341,561.495728 C125.846893,587.012817 75.302292,612.295532 24.735992,637.534790 C16.874903,641.458496 8.914484,645.183228 0.999997,649.000000 z"
/>
<path
fill="#000000"
opacity="1.000000"
stroke="none"
d="M689.340759,305.086823 C735.743225,281.945923 782.143677,258.801056 828.549438,235.666794 C840.447632,229.735275 852.362488,223.837280 864.634888,217.961929 C865.000000,433.613190 865.000000,649.226379 865.000000,864.919800 C577.000000,864.919800 289.000000,864.919800 1.000000,864.919800 C1.000000,793.225708 1.000000,721.576721 0.999997,649.463867 C8.914484,645.183228 16.874903,641.458496 24.735992,637.534790 C75.302292,612.295532 125.846893,587.012817 176.939667,561.513062 C178.543060,562.085083 179.606812,562.886414 180.667526,563.691833 C225.656799,597.853394 291.232574,574.487244 304.462524,519.579773 C304.989105,517.394409 305.501068,515.205505 305.984619,513.166748 C391.466370,513.166748 476.422729,513.166748 561.331177,513.166748 C573.857727,555.764343 608.978149,572.880920 638.519897,572.672791 C671.048340,572.443665 700.623230,551.730408 711.658752,520.910583 C722.546875,490.502106 715.037842,453.265564 682.776733,429.447052 C683.966064,428.506866 685.119507,427.602356 686.265320,426.688232 C712.934143,405.412262 723.011475,370.684631 711.897339,338.686676 C707.312805,325.487671 699.185303,314.725128 689.340759,305.086823 z"
/>
<path
fill="#FEFBFC"
opacity="1.000000"
stroke="none"
d="M688.998657,304.980469 C699.185303,314.725128 707.312805,325.487671 711.897339,338.686676 C723.011475,370.684631 712.934143,405.412262 686.265320,426.688232 C685.119507,427.602356 683.966064,428.506866 682.776733,429.447052 C715.037842,453.265564 722.546875,490.502106 711.658752,520.910583 C700.623230,551.730408 671.048340,572.443665 638.519897,572.672791 C608.978149,572.880920 573.857727,555.764343 561.331177,513.166748 C476.422729,513.166748 391.466370,513.166748 305.984619,513.166748 C305.501068,515.205505 304.989105,517.394409 304.462524,519.579773 C291.232574,574.487244 225.656799,597.853394 180.667526,563.691833 C179.606812,562.886414 178.543060,562.085083 177.128418,561.264465 C172.285187,555.958313 167.274551,551.023804 163.388611,545.322632 C139.918045,510.888947 147.141953,465.032562 179.849426,439.228516 C180.887405,438.409576 181.938263,437.607025 183.344437,436.516663 C166.300507,423.954224 155.204453,407.689667 151.151794,387.034515 C147.884064,370.379944 149.905151,354.265930 156.935104,338.792725 C169.912033,310.230042 201.003021,291.554596 231.834442,293.306793 C265.253357,295.206024 298.804138,318.894806 306.088135,359.748962 C390.283478,359.748962 474.522705,359.748962 559.195374,359.748962 C559.717590,356.609131 560.141357,353.369446 560.804199,350.179443 C573.006775,291.446869 639.556946,266.944733 687.053894,303.680206 C687.580750,304.087708 688.122009,304.476532 688.998657,304.980469 M703.311279,484.370789 C698.954468,457.053253 681.951416,440.229645 656.413696,429.482330 C673.953552,421.977875 688.014709,412.074219 696.456482,395.642365 C704.862061,379.280853 706.487793,362.316345 700.947998,344.809204 C691.688965,315.548492 664.183716,296.954437 633.103516,298.838257 C618.467957,299.725372 605.538086,305.139557 594.588501,314.780121 C577.473999,329.848511 570.185486,349.121399 571.838501,371.750854 C479.166595,371.750854 387.082886,371.750854 294.582672,371.750854 C293.993011,354.662048 288.485260,339.622314 276.940491,327.118439 C265.392609,314.611176 251.082092,307.205322 234.093262,305.960541 C203.355347,303.708374 176.337585,320.898438 166.089890,348.816620 C159.557541,366.613007 160.527206,384.117401 168.756042,401.172516 C177.054779,418.372589 191.471954,428.832886 207.526581,435.632172 C198.407059,442.272583 188.815598,448.302246 180.383728,455.660675 C171.685028,463.251984 166.849655,473.658661 163.940216,484.838684 C161.021744,496.053375 161.212982,507.259705 164.178833,518.426208 C171.577927,546.284302 197.338104,566.588867 226.001465,567.336853 C240.828415,567.723816 254.357819,563.819092 266.385468,555.199646 C284.811554,541.994751 293.631104,523.530579 294.687347,501.238312 C387.354828,501.238312 479.461304,501.238312 571.531799,501.238312 C577.616638,543.189026 615.312866,566.342102 651.310059,559.044739 C684.973938,552.220398 708.263306,519.393127 703.311279,484.370789 z"
/>
<path
fill="#EC008C"
opacity="1.000000"
stroke="none"
d="M703.401855,484.804718 C708.263306,519.393127 684.973938,552.220398 651.310059,559.044739 C615.312866,566.342102 577.616638,543.189026 571.531799,501.238312 C479.461304,501.238312 387.354828,501.238312 294.687347,501.238312 C293.631104,523.530579 284.811554,541.994751 266.385468,555.199646 C254.357819,563.819092 240.828415,567.723816 226.001465,567.336853 C197.338104,566.588867 171.577927,546.284302 164.178833,518.426208 C161.212982,507.259705 161.021744,496.053375 163.940216,484.838684 C166.849655,473.658661 171.685028,463.251984 180.383728,455.660675 C188.815598,448.302246 198.407059,442.272583 207.526581,435.632172 C191.471954,428.832886 177.054779,418.372589 168.756042,401.172516 C160.527206,384.117401 159.557541,366.613007 166.089890,348.816620 C176.337585,320.898438 203.355347,303.708374 234.093262,305.960541 C251.082092,307.205322 265.392609,314.611176 276.940491,327.118439 C288.485260,339.622314 293.993011,354.662048 294.582672,371.750854 C387.082886,371.750854 479.166595,371.750854 571.838501,371.750854 C570.185486,349.121399 577.473999,329.848511 594.588501,314.780121 C605.538086,305.139557 618.467957,299.725372 633.103516,298.838257 C664.183716,296.954437 691.688965,315.548492 700.947998,344.809204 C706.487793,362.316345 704.862061,379.280853 696.456482,395.642365 C688.014709,412.074219 673.953552,421.977875 656.413696,429.482330 C681.951416,440.229645 698.954468,457.053253 703.401855,484.804718 z"
/>
</svg>

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -8,7 +8,7 @@ ## Basics
setting individual config options via commandline.
Please refer to the [example config
file](./configuration/examples.md#example-configuration) for all of those
file](./reference/config.mdx) for all of those
settings.
The config file to use can be specified on the commandline when running

View File

@@ -1,21 +0,0 @@
## Example configuration
<details>
<summary>Example configuration</summary>
```toml
{{#include ../../conduwuit-example.toml}}
```
</details>
## systemd unit file
<details>
<summary>systemd unit file</summary>
```
{{#include ../../pkg/conduwuit.service}}
```
</details>

42
docs/deploying/_meta.json Normal file
View File

@@ -0,0 +1,42 @@
[
{
"type": "file",
"name": "generic",
"label": "Generic"
},
{
"type": "file",
"name": "docker",
"label": "Docker"
},
{
"type": "file",
"name": "debian",
"label": "Debian"
},
{
"type": "file",
"name": "fedora",
"label": "Fedora"
},
{
"type": "file",
"name": "nixos",
"label": "NixOS"
},
{
"type": "file",
"name": "arch-linux",
"label": "Arch Linux"
},
{
"type": "file",
"name": "kubernetes",
"label": "Kubernetes"
},
{
"type": "file",
"name": "freebsd",
"label": "FreeBSD"
}
]

View File

@@ -1 +0,0 @@
{{#include ../../pkg/debian/README.md}}

1
docs/deploying/debian.mdx Symbolic link
View File

@@ -0,0 +1 @@
../../pkg/debian/README.md

View File

@@ -2,7 +2,7 @@
services:
homeserver:
### If you already built the conduduwit image with 'docker build' or want to use the Docker Hub image,
### If you already built the continuwuity image with 'docker build' or want to use the Docker Hub image,
### then you are ready to go.
image: forgejo.ellis.link/continuwuation/continuwuity:latest
restart: unless-stopped

View File

@@ -40,10 +40,10 @@ ### Run
The `-d` flag lets the container run in detached mode. You may supply an
optional `continuwuity.toml` config file, the example config can be found
[here](../configuration/examples.md). You can pass in different env vars to
[here](../reference/config.mdx). You can pass in different env vars to
change config values on the fly. You can even configure Continuwuity completely by
using env vars. For an overview of possible values, please take a look at the
[`docker-compose.yml`](docker-compose.yml) file.
<a href="/examples/docker-compose.yml" target="_blank">`docker-compose.yml`</a> file.
If you just want to test Continuwuity for a short time, you can use the `--rm`
flag, which cleans up everything related to your container after you stop
@@ -56,14 +56,62 @@ ### Docker-compose
Depending on your proxy setup, you can use one of the following files:
- If you already have a `traefik` instance set up, use
[`docker-compose.for-traefik.yml`](docker-compose.for-traefik.yml)
- If you don't have a `traefik` instance set up and would like to use it, use
[`docker-compose.with-traefik.yml`](docker-compose.with-traefik.yml)
- If you want a setup that works out of the box with `caddy-docker-proxy`, use
[`docker-compose.with-caddy.yml`](docker-compose.with-caddy.yml) and replace all
`example.com` placeholders with your own domain
- For any other reverse proxy, use [`docker-compose.yml`](docker-compose.yml)
### For existing Traefik setup
<details>
<summary>docker-compose.for-traefik.yml</summary>
```yaml file="./docker-compose.for-traefik.yml"
```
</details>
### With Traefik included
<details>
<summary>docker-compose.with-traefik.yml</summary>
```yaml file="./docker-compose.with-traefik.yml"
```
</details>
### With Caddy Docker Proxy
<details>
<summary>docker-compose.with-caddy.yml</summary>
Replace all `example.com` placeholders with your own domain.
```yaml file="./docker-compose.with-caddy.yml"
```
</details>
### For other reverse proxies
<details>
<summary>docker-compose.yml</summary>
```yaml file="./docker-compose.yml"
```
</details>
### Override file
<details>
<summary>docker-compose.override.yml</summary>
```yaml file="./docker-compose.override.yml"
```
</details>
When picking the Traefik-related compose file, rename it to
`docker-compose.yml`, and rename the override file to
@@ -80,7 +128,7 @@ ### Docker-compose
After that, you can rename it to `docker-compose.yml` and spin up the
containers!
Additional info about deploying Continuwuity can be found [here](generic.md).
Additional info about deploying Continuwuity can be found [here](generic.mdx).
### Build
@@ -88,7 +136,18 @@ ### Build
The resulting images are widely compatible with Docker and other container runtimes like Podman or containerd.
The images *do not contain a shell*. They contain only the Continuwuity binary, required libraries, TLS certificates, and metadata. Please refer to the [`docker/Dockerfile`][dockerfile-path] for the specific details of the image composition.
The images *do not contain a shell*. They contain only the Continuwuity binary, required libraries, TLS certificates, and metadata.
<details>
<summary>Click to view the Dockerfile</summary>
You can also <a href="https://forgejo.ellis.link/continuwuation/continuwuation/src/branch/main/docker/Dockerfile" target="_blank">view the Dockerfile on Forgejo</a>.
```dockerfile file="../../docker/Dockerfile"
```
</details>
To build an image locally using Docker Buildx, you can typically run a command like:
@@ -105,7 +164,7 @@ # docker buildx build --load --tag continuwuity:latest --build-arg TARGET_CPU=na
Refer to the Docker Buildx documentation for more advanced build options.
[dockerfile-path]: ../../docker/Dockerfile
[dockerfile-path]: https://forgejo.ellis.link/continuwuation/continuwuation/src/branch/main/docker/Dockerfile
### Run
@@ -123,10 +182,7 @@ ### Use Traefik as Proxy
As a container user, you probably know about Traefik. It is an easy-to-use
reverse proxy for making containerized apps and services available through the
web. With the two provided files,
[`docker-compose.for-traefik.yml`](docker-compose.for-traefik.yml) (or
[`docker-compose.with-traefik.yml`](docker-compose.with-traefik.yml)) and
[`docker-compose.override.yml`](docker-compose.override.yml), it is equally easy
web. With the Traefik-related docker-compose files provided above, it is equally easy
to deploy and use Continuwuity, with a small caveat. If you have already looked at
the files, you should have seen the `well-known` service, which is the
small caveat. Traefik is simply a proxy and load balancer and cannot

View File

@@ -97,8 +97,7 @@ ## Forwarding ports in the firewall or the router
## Setting up a systemd service
You can find two example systemd units for Continuwuity
[on the configuration page](../configuration/examples.md#debian-systemd-unit-file).
You can find an example unit for continuwuity below.
You may need to change the `ExecStart=` path to match where you placed the Continuwuity
binary if it is not in `/usr/bin/conduwuit`.
@@ -117,11 +116,26 @@ ## Setting up a systemd service
ReadWritePaths=/path/to/custom/database/path
```
### Example systemd Unit File
<details>
<summary>Click to expand systemd unit file (conduwuit.service)</summary>
```ini file="../../pkg/conduwuit.service"
```
</details>
You can also [view the file on Foregejo](https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/pkg/conduwuit.service).
## Creating the Continuwuity configuration file
Now you need to create the Continuwuity configuration file in
`/etc/continuwuity/continuwuity.toml`. You can find an example configuration at
[conduwuit-example.toml](../configuration/examples.md).
`/etc/conduwuit/conduwuit.toml`. You can find an example configuration at
[conduwuit-example.toml](../reference/config.mdx).
**Please take a moment to read the config. You need to change at least the
server name.**
@@ -156,7 +170,7 @@ ### Caddy
After installing Caddy via your preferred method, create `/etc/caddy/conf.d/conduwuit_caddyfile`
and enter the following (substitute your actual server name):
```caddyfile
```
your.server.name, your.server.name:8448 {
# TCP reverse_proxy
reverse_proxy 127.0.0.1:6167
@@ -193,8 +207,10 @@ ### Other Reverse Proxies
- [`/.well-known/matrix/support`](https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixsupport)
Examples of delegation:
- <https://puppygock.gay/.well-known/matrix/server>
- <https://puppygock.gay/.well-known/matrix/client>
- https://continuwuity.org/.well-known/matrix/server
- https://continuwuity.org/.well-known/matrix/client
- https://ellis.link/.well-known/matrix/server
- https://ellis.link/.well-known/matrix/client
For Apache and Nginx there are many examples available online.

View File

@@ -1,8 +1,8 @@
# Continuwuity for Kubernetes
Continuwuity doesn't support horizontal scalability or distributed loading
natively. However, a community-maintained Helm Chart is available here to run
conduwuit on Kubernetes: <https://gitlab.cronce.io/charts/conduwuit>
natively. However, [a community-maintained Helm Chart is available here to run
conduwuit on Kubernetes](https://gitlab.cronce.io/charts/conduwuit)
This should be compatible with Continuwuity, but you will need to change the image reference.

View File

@@ -48,7 +48,7 @@ ### Available options
- `package`: The Continuwuity package to use
- `settings`: The Continuwuity configuration (in TOML format)
Use the `settings` option to configure Continuwuity itself. See the [example configuration file](../configuration/examples.md#example-configuration) for all available options.
Use the `settings` option to configure Continuwuity itself. See the [example configuration file](../reference/config.mdx) for all available options.
### UNIX sockets

View File

@@ -0,0 +1,27 @@
[
{
"type": "file",
"name": "index",
"label": "Development Guide"
},
{
"type": "file",
"name": "contributing",
"label": "Contributing"
},
{
"type": "file",
"name": "code_style",
"label": "Code Style Guide"
},
{
"type": "file",
"name": "testing",
"label": "Testing"
},
{
"type": "file",
"name": "hot_reload",
"label": "Hot Reloading"
}
]

View File

@@ -0,0 +1,173 @@
# Contributing guide
This page is about contributing to Continuwuity. The
[development](./index.mdx) and [code style guide](./code_style.mdx) pages may be of interest for you as well.
If you would like to work on an [issue][issues] that is not assigned, preferably
ask in the Matrix room first at [#continuwuity:continuwuity.org][continuwuity-matrix],
and comment on it.
### Code Style
Please review and follow the [code style guide](./code_style) for formatting, linting, naming conventions, and other code standards.
### Pre-commit Checks
Continuwuity uses pre-commit hooks to enforce various coding standards and catch common issues before they're committed. These checks include:
- Code formatting and linting
- Typo detection (both in code and commit messages)
- Checking for large files
- Ensuring proper line endings and no trailing whitespace
- Validating YAML, JSON, and TOML files
- Checking for merge conflicts
You can run these checks locally by installing [prefligit](https://github.com/j178/prefligit):
```bash
# Requires UV: https://docs.astral.sh/uv/getting-started/installation/
# Mac/linux: curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows: powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
# Install prefligit using cargo-binstall
cargo binstall prefligit
# Install git hooks to run checks automatically
prefligit install
# Run all checks
prefligit --all-files
```
Alternatively, you can use [pre-commit](https://pre-commit.com/):
```bash
# Requires python
# Install pre-commit
pip install pre-commit
# Install the hooks
pre-commit install
# Run all checks manually
pre-commit run --all-files
```
These same checks are run in CI via the prefligit-checks workflow to ensure consistency. These must pass before the PR is merged.
### Running tests locally
Tests, compilation, and linting can be run with standard Cargo commands:
```bash
# Run tests
cargo test
# Check compilation
cargo check --workspace --features full
# Run lints
cargo clippy --workspace --features full
# Auto-fix: cargo clippy --workspace --features full --fix --allow-staged;
# Format code (must use nightly)
cargo +nightly fmt
```
### Matrix tests
Continuwuity uses [Complement][complement] for Matrix protocol compliance testing. Complement tests are run manually by developers, and documentation on how to run these tests locally is currently being developed.
If your changes are done to fix Matrix tests, please note that in your pull request. If more Complement tests start failing from your changes, please review the logs and determine if they're intended or not.
[Sytest][sytest] is currently unsupported.
### Writing documentation
Continuwuity's website uses [`mdbook`][mdbook] and is deployed via CI using Cloudflare Pages
in the [`documentation.yml`][documentation.yml] workflow file. All documentation is in the `docs/`
directory at the top level.
To build the documentation locally:
1. Install mdbook if you don't have it already:
```bash
cargo install mdbook # or cargo binstall, or another method
```
2. Build the documentation:
```bash
mdbook build
```
The output of the mdbook generation is in `public/`. You can open the HTML files directly in your browser without needing a web server.
### Commit Messages
Continuwuity follows the [Conventional Commits](https://www.conventionalcommits.org/) specification for commit messages. This provides a standardized format that makes the commit history more readable and enables automated tools to generate changelogs.
The basic structure is:
```
<type>[(optional scope)]: <description>
[optional body]
[optional footer(s)]
```
The allowed types for commits are:
- `fix`: Bug fixes
- `feat`: New features
- `docs`: Documentation changes
- `style`: Changes that don't affect the meaning of the code (formatting, etc.)
- `refactor`: Code changes that neither fix bugs nor add features
- `perf`: Performance improvements
- `test`: Adding or fixing tests
- `build`: Changes to the build system or dependencies
- `ci`: Changes to CI configuration
- `chore`: Other changes that don't modify source or test files
Examples:
```
feat: add user authentication
fix(database): resolve connection pooling issue
docs: update installation instructions
```
The project uses the `committed` hook to validate commit messages in pre-commit. This ensures all commits follow the conventional format.
### Creating pull requests
Please try to keep contributions to the Forgejo Instance. While the mirrors of continuwuity
allow for pull/merge requests, there is no guarantee the maintainers will see them in a timely
manner. Additionally, please mark WIP or unfinished or incomplete PRs as drafts.
This prevents us from having to ping once in a while to double check the status
of it, especially when the CI completed successfully and everything so it
*looks* done.
Before submitting a pull request, please ensure:
1. Your code passes all CI checks (formatting, linting, typo detection, etc.)
2. Your code follows the [code style guide](./code_style)
3. Your commit messages follow the conventional commits format
4. Tests are added for new functionality
5. Documentation is updated if needed
Direct all PRs/MRs to the `main` branch.
By sending a pull request or patch, you are agreeing that your changes are
allowed to be licenced under the Apache-2.0 licence and all of your conduct is
in line with the Contributor's Covenant, and continuwuity's Code of Conduct.
Contribution by users who violate either of these code of conducts may not have
their contributions accepted. This includes users who have been banned from
continuwuity Matrix rooms for Code of Conduct violations.
[issues]: https://forgejo.ellis.link/continuwuation/continuwuity/issues
[continuwuity-matrix]: https://matrix.to/#/#continuwuity:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org
[complement]: https://github.com/matrix-org/complement/
[sytest]: https://github.com/matrix-org/sytest/
[mdbook]: https://rust-lang.github.io/mdBook/
[documentation.yml]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/.forgejo/workflows/documentation.yml

View File

@@ -137,7 +137,7 @@ ### Addendum
it.**
![Continuwuity's dynamic library setup diagram - created by Jason
Volk](assets/libraries.png)
Volk](./assets/libraries.png)
When a symbol is referenced between crates they become bound: **crates cannot be
unloaded until their calling crates are first unloaded.** Thus we start the
@@ -148,7 +148,7 @@ ### Addendum
binding ever occurs between them.
![Continuwuity's reload and load order diagram - created by Jason
Volk](assets/reload_order.png)
Volk](./assets/reload_order.png)
Proper resource management is essential for reliable reloading to occur. This is
a very basic ask in RAII-idiomatic Rust and the exposure to reloading hazards is

View File

@@ -2,7 +2,7 @@ # Development
Information about developing the project. If you are only interested in using
it, you can safely ignore this page. If you plan on contributing, see the
[contributor's guide](./contributing.md) and [code style guide](./development/code_style.md).
[contributor's guide](./contributing.mdx) and [code style guide](./code_style.mdx).
## Continuwuity project layout

View File

@@ -24,7 +24,7 @@ ## Complement
If you're on macOS and need to build an image, run `nix build .#linux-complement`.
We have a Complement fork as some tests have needed to be fixed. This can be found
at: <https://forgejo.ellis.link/continuwuation/complement>
at [continuwuation/complement](https://forgejo.ellis.link/continuwuation/complement)
[ci-workflows]:
https://forgejo.ellis.link/continuwuation/continuwuity/actions/?workflow=ci.yml&actor=0&status=1

51
docs/index.mdx Normal file
View File

@@ -0,0 +1,51 @@
---
pageType: home
hero:
name: Continuwuity
text: A community-driven Matrix homeserver
tagline: Fast, lightweight and open
actions:
- theme: brand
text: Get Started
link: /introduction
- theme: alt
text: Contribute on Forgejo
link: https://forgejo.ellis.link/continuwuation/continuwuity
- theme: alt
text: Star on GitHub
link: https://github.com/continuwuity/continuwuity
image:
src: /assets/logo.svg
alt: continuwuity logo
features:
- title: 🚀 High Performance
details: Built with Rust for exceptional speed and efficiency. Designed to run smoothly even on modest hardware.
- title: 🔒 Secure by Default
details: Memory-safe Rust implementation with built-in security features to protect your communication.
- title: 🌐 Matrix Protocol
details: Fully compatible with the Matrix ecosystem. Connect with users across the federated network.
- title: 🛠️ Community Maintained
details: Actively developed by a dedicated community of Matrix enthusiasts and contributors.
- title: 📦 Easy to Deploy
details: Multiple deployment options including Docker, NixOS, and traditional package managers.
- title: 🔌 Appservice Support
details: Bridge to other platforms like Discord, Telegram, and more with Matrix appservices.
doc: false
---
## What is Continuwuity?
Continuwuity is a Matrix homeserver.
Matrix is an open chat network that lets anyone talk to anyone, no matter what server or address they use - sort of like email.
Continuwuity receives and keeps track of all your messages, and delivers what you send to the right people.
## Why is Continuwuity different?
Continuwuity is light and fast, using a fraction of the memory of other major homeservers. It's also simple to set up, and secure by default.
We are a community run project, filled with diverse and friendly people. Everything is built by people who care about the project volunteering their free time.

View File

@@ -1,18 +0,0 @@
# Continuwuity
{{#include ../README.md:catchphrase}}
{{#include ../README.md:body}}
#### How can I deploy my own?
- [Deployment options](deploying.md)
If you want to connect an appservice to Continuwuity, take a look at the
[appservices documentation](appservices.md).
#### How can I contribute?
See the [contributor's guide](contributing.md)
{{#include ../README.md:footer}}

92
docs/introduction.mdx Normal file
View File

@@ -0,0 +1,92 @@
# Continuwuity
## A community-driven [Matrix](https://matrix.org/) homeserver in Rust
[![Chat on Matrix](https://img.shields.io/matrix/continuwuity%3Acontinuwuity.org?server_fqdn=matrix.continuwuity.org&fetchMode=summary&logo=matrix)](https://matrix.to/#/#continuwuity:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org) [![Join the space](https://img.shields.io/matrix/space%3Acontinuwuity.org?server_fqdn=matrix.continuwuity.org&fetchMode=summary&logo=matrix&label=space)](https://matrix.to/#/#space:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org)
[continuwuity] is a Matrix homeserver written in Rust.
It's the official community continuation of the [conduwuit](https://github.com/girlbossceo/conduwuit) homeserver.
[![forgejo.ellis.link](https://img.shields.io/badge/Ellis%20Git-main+packages-green?style=flat&logo=forgejo&labelColor=fff)](https://forgejo.ellis.link/continuwuation/continuwuity) [![Stars](https://forgejo.ellis.link/continuwuation/continuwuity/badges/stars.svg?style=flat)](https://forgejo.ellis.link/continuwuation/continuwuity/stars) [![Issues](https://forgejo.ellis.link/continuwuation/continuwuity/badges/issues/open.svg?style=flat)](https://forgejo.ellis.link/continuwuation/continuwuity/issues?state=open) [![Pull Requests](https://forgejo.ellis.link/continuwuation/continuwuity/badges/pulls/open.svg?style=flat)](https://forgejo.ellis.link/continuwuation/continuwuity/pulls?state=open)
[![GitHub](https://img.shields.io/badge/GitHub-mirror-blue?style=flat&logo=github&labelColor=fff&logoColor=24292f)](https://github.com/continuwuity/continuwuity) [![Stars](https://img.shields.io/github/stars/continuwuity/continuwuity?style=flat)](https://github.com/continuwuity/continuwuity/stargazers)
[![GitLab](https://img.shields.io/badge/GitLab-mirror-blue?style=flat&logo=gitlab&labelColor=fff)](https://gitlab.com/continuwuity/continuwuity) [![Stars](https://img.shields.io/gitlab/stars/continuwuity/continuwuity?style=flat)](https://gitlab.com/continuwuity/continuwuity/-/starrers)
[![Codeberg](https://img.shields.io/badge/Codeberg-mirror-2185D0?style=flat&logo=codeberg&labelColor=fff)](https://codeberg.org/continuwuity/continuwuity) [![Stars](https://codeberg.org/continuwuity/continuwuity/badges/stars.svg?style=flat)](https://codeberg.org/continuwuity/continuwuity/stars)
## Why does this exist?
The original conduwuit project has been archived and is no longer maintained. Rather than letting this Rust-based Matrix homeserver disappear, a group of community contributors have forked the project to continue its development, fix outstanding issues, and add new features.
We aim to provide a stable, well-maintained alternative for current conduwuit users and welcome newcomers seeking a lightweight, efficient Matrix homeserver.
## Who are we?
We are a group of Matrix enthusiasts, developers and system administrators who have used conduwuit and believe in its potential. Our team includes both previous
contributors to the original project and new developers who want to help maintain and improve this important piece of Matrix infrastructure.
We operate as an open community project, welcoming contributions from anyone interested in improving continuwuity.
## What is Matrix?
[Matrix](https://matrix.org) is an open, federated, and extensible network for
decentralized communication. Users from any Matrix homeserver can chat with users from all
other homeservers over federation. Matrix is designed to be extensible and built on top of.
You can even use bridges such as Matrix Appservices to communicate with users outside of Matrix, like a community on Discord.
## What are the project's goals?
continuwuity aims to:
- Maintain a stable, reliable Matrix homeserver implementation in Rust
- Improve compatibility and specification compliance with the Matrix protocol
- Fix bugs and performance issues from the original conduwuit
- Add missing features needed by homeserver administrators
- Provide comprehensive documentation and easy deployment options
- Create a sustainable development model for long-term maintenance
- Keep a lightweight, efficient codebase that can run on modest hardware
## Can I try it out?
Check out the [documentation](https://continuwuity.org) for installation instructions.
There are currently no open registration continuwuity instances available.
## What are we working on?
We're working our way through all of the issues in the [Forgejo project](https://forgejo.ellis.link/continuwuation/continuwuity/issues).
- [Packaging & availability in more places](https://forgejo.ellis.link/continuwuation/continuwuity/issues/747)
- [Appservices bugs & features](https://forgejo.ellis.link/continuwuation/continuwuity/issues?q=&type=all&state=open&labels=178&milestone=0&assignee=0&poster=0)
- [Improving compatibility and spec compliance](https://forgejo.ellis.link/continuwuation/continuwuity/issues?labels=119)
- Automated testing
- [Admin API](https://forgejo.ellis.link/continuwuation/continuwuity/issues/748)
- [Policy-list controlled moderation](https://forgejo.ellis.link/continuwuation/continuwuity/issues/750)
## Can I migrate my data from x?
- **Conduwuit**: Yes
- **Conduit**: No, database is now incompatible
- **Grapevine**: No, database is now incompatible
- **Dendrite**: No
- **Synapse**: No
We haven't written up a guide on migrating from incompatible homeservers yet. Reach out to us if you need to do this!
## How can I deploy my own?
- [Deployment options](deploying)
If you want to connect an appservice to continuwuity, take a look at the
[appservices documentation](appservices).
## How can I contribute?
See the [contributor's guide](development/contributing)
## Contact
Join our [Matrix room](https://matrix.to/#/#continuwuity:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org) and [space](https://matrix.to/#/#space:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org) to chat with us about the project!
[continuwuity]: https://forgejo.ellis.link/continuwuation/continuwuity

View File

@@ -47,7 +47,7 @@ ## Database (RocksDB)
### Compression
Some RocksDB settings can be adjusted such as the compression method chosen. See
the RocksDB section in the [example config](configuration/examples.md).
the RocksDB section in the [example config](./reference/config.mdx).
btrfs users have reported that database compression does not need to be disabled
on Continuwuity as the filesystem already does not attempt to compress. This can be
@@ -55,7 +55,7 @@ ### Compression
the `physical_offset` matches (no filesystem compression). It is very important
to ensure no additional filesystem compression takes place as this can render
unbuffered Direct IO inoperable, significantly slowing down read and write
performance. See <https://btrfs.readthedocs.io/en/latest/Compression.html#compatibility>
performance. See [the Btrfs docs](https://btrfs.readthedocs.io/en/latest/Compression.html#compatibility).
> Compression is done using the COW mechanism so its incompatible with
> nodatacow. Direct IO read works on compressed files but will fall back to

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://continuwuity.org/schema/announcements.schema.json",
"announcements": [
{
"id": 1,
"message": "Welcome to Continuwuity! Important announcements about the project will appear here."
},
{
"id": 6,
"mention_room": true,
"date": "2025-12-22",
"message": "Continuwuity v0.5.0 has been released. **The release contains a fix for the critical vulnerability [GHSA-22fw-4jq7-g8r8](https://github.com/continuwuity/continuwuity/security/advisories/GHSA-22fw-4jq7-g8r8). Update as soon as possible.**\n\nThis has been *actively exploited* to create fake leave events in the Continuwuity rooms. Please leave and rejoin the rooms to fix any issues this may have caused. \n\n - [Continuwuity (space)](https://matrix.to/#/!PxtzompFuodlyzdCDtV5lzjXs10XIHeOOaq_FYodHyk?via=ellis.link&via=gingershaped.computer&via=continuwuity.org)\n - [Continuwuity](https://matrix.to/#/!kn3VQSLcgWGUFm0FFRid4MinJ_aeZPjHQ0irXbHa3bU?via=ellis.link&via=gingershaped.computer&via=continuwuity.org)\n - [Continuwuity Announcements](https://matrix.to/#/!d7zDZg1Vu5nhkCi50jNfOIObD5fpfGhfl48SZWZek7k?via=ellis.link)\n - [Continuwuity Offtopic](https://matrix.to/#/!QlOomq-suHC9rJHfDFVdbcGg4HS2ojSQ0bo4W2JOGMM?via=ellis.link&via=gingershaped.computer&via=continuwuity.org)\n - [Continuwuity Development](https://matrix.to/#/!aAvealFbgiKTJGzumNbjuwDgt1tOkBKwiyfYqE3ouk0?via=ellis.link&via=explodie.org&via=continuwuity.org)\n"
}
]
}

View File

@@ -1 +1 @@
{"m.homeserver":{"base_url": "https://matrix.continuwuity.org"},"org.matrix.msc3575.proxy":{"url": "https://matrix.continuwuity.org"}}
{"m.homeserver":{"base_url": "https://matrix.continuwuity.org"},"org.matrix.msc3575.proxy":{"url": "https://matrix.continuwuity.org"},"org.matrix.msc4143.rtc_foci":[{"type":"livekit","livekit_service_url":"https://livekit.ellis.link"}]}

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="447.99823"
height="447.99823"
viewBox="0 0 447.99823 447.99823"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><g
id="layer1"
transform="translate(-32.000893,-32.000893)"><circle
style="fill:#9b4bd4;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-dasharray:none;stroke-opacity:1"
id="path1"
cy="256"
cx="256"
r="176" /><path
style="fill:#de6cd3;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 41,174 69,36 C 135,126 175,102 226,94 l -12,31 62,-44 -69,-44 15,30 C 128,69 84,109 41,172 Z"
id="path7" /><path
style="fill:#de6cd3;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 338,41 -36,69 c 84,25 108,65 116,116 l -31,-12 44,62 44,-69 -30,15 C 443,128 403,84 340,41 Z"
id="path6" /><path
style="fill:#de6cd3;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 471,338 -69,-36 c -25,84 -65,108 -116,116 l 12,-31 -62,44 69,44 -15,-30 c 94,-2 138,-42 181,-105 z"
id="path8" /><path
style="fill:#de6cd3;fill-opacity:1;stroke:#000000;stroke-width:10;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 174,471 36,-69 C 126,377 102,337 94,286 l 31,12 -44,-62 -44,69 30,-15 c 2,94 42,138 105,181 z"
id="path9" /><g
id="g15"
transform="translate(-5.4157688e-4)"><path
style="fill:none;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
d="m 155.45977,224.65379 c -7.25909,13.49567 -7.25909,26.09161 -6.35171,39.58729 0.90737,11.69626 12.7034,24.29222 24.49943,26.09164 21.77727,3.59884 28.12898,-20.69338 28.12898,-20.69338 0,0 4.53693,-15.29508 5.4443,-40.48699"
id="path11" /><path
style="fill:none;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
d="m 218.96706,278.05399 c 3.00446,17.12023 7.52704,24.88918 19.22704,28.48918 9,2.7 22.5,-4.5 22.5,-16.2 0.9,21.6 17.1,17.1 19.8,17.1 11.7,-1.8 18.9,-14.4 16.2,-30.6"
id="path12" /><path
style="fill:none;stroke:#000000;stroke-width:10;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
d="m 305.6941,230.94317 c 1.8,27 6.3,40.5 6.3,40.5 8.1,27 28.8,19.8 28.8,19.8 18.9,-7.2 22.5,-24.3 22.5,-30.6 0,-25.2 -6.3,-35.1 -6.3,-35.1"
id="path13" /></g></g></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

17
docs/reference/_meta.json Normal file
View File

@@ -0,0 +1,17 @@
[
{
"type": "file",
"name": "config",
"label": "Configuration"
},
{
"type": "file",
"name": "admin",
"label": "Admin Commands"
},
{
"type": "file",
"name": "server",
"label": "Server command"
}
]

View File

@@ -0,0 +1,4 @@
```toml file="../../conduwuit-example.toml"
```

View File

@@ -1 +0,0 @@
{{#include ../SECURITY.md}}

1
docs/security.mdx Symbolic link
View File

@@ -0,0 +1 @@
../SECURITY.md

View File

@@ -1,17 +0,0 @@
{
"$schema": "https://continuwuity.org/schema/announcements.schema.json",
"announcements": [
{
"id": 1,
"message": "Welcome to Continuwuity! Important announcements about the project will appear here."
},
{
"id": 3,
"message": "_taps microphone_ The Continuwuity 0.5.0-rc.7 release is now available, and it's better than ever! **177 commits**, **35 pull requests**, **11 contributors,** and a lot of new stuff!\n\nFor highlights, we've got:\n\n* 🕵️ Full Policy Server support to fight spam!\n* 🚀 Smarter room & space upgrades.\n* 🚫 User suspension tools for better moderation.\n* 🤖 reCaptcha support for safer open registration.\n* 🔍 Ability to disable read receipts & typing indicators.\n* ⚡ Sweeping performance improvements!\n\nGet the [full changelog and downloads on our Forgejo](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v0.5.0-rc.7) - and make sure you're in the [Announcements room](https://matrix.to/#/!releases:continuwuity.org/$hN9z6L2_dTAlPxFLAoXVfo_g8DyYXu4cpvWsSrWhmB0) to get stuff like this sooner."
},
{
"id": 5,
"message": "It's a bird! It's a plane! No, it's 0.5.0-rc.8.1!\n\nThis is a minor bugfix update to the rc8 which backports some important fixes from the latest main branch. If you still haven't updated to rc8, you should skip to main. Otherwise, you should upgrade to this bugfix release as soon as possible.\n\nBugfixes backported to this version:\n\n- Resolved several issues with state resolution v2.1 (room version 12)\n- Fixed issues with the `restricted` and `knock_restricted` join rules that would sometimes incorrectly disallow a valid join\n- Fixed the automatic support contact listing being a no-op\n- Fixed upgrading pre-v12 rooms to v12 rooms\n- Fixed policy servers sending the incorrect JSON objects (resulted in false positives)\n- Fixed debug build panic during MSC4133 migration\n\nIt is recommended, if you can and are comfortable with doing so, following updates to the main branch - we're in the run up to the full 0.5.0 release, and more and more bugfixes and new features are being pushed constantly. Please don't forget to join [#announcements:continuwuity.org](https://matrix.to/#/#announcements:continuwuity.org) to receive this news faster and be alerted to other important updates!"
}
]
}

View File

@@ -128,7 +128,7 @@ ### Database corruption
With this in mind:
- First start Continuwuity with the `PointInTime` recovery method. See the [example
config](configuration/examples.md) for how to do this using
config](./reference/config.mdx) for how to do this using
`rocksdb_recovery_mode`
- If your database successfully opens, clients are recommended to clear their
client cache to account for the rollback

View File

@@ -8,7 +8,7 @@ ### Configuration
Create a configuration file called `coturn.conf` containing:
```conf
```
use-auth-secret
static-auth-secret=<a secret key>
realm=<your server domain>
@@ -18,7 +18,7 @@ ### Configuration
-s 64 1`.
These same values need to be set in Continuwuity. See the [example
config](configuration/examples.md) in the TURN section for configuring these and
config](./reference/config.mdx) in the TURN section for configuring these and
restart Continuwuity after.
`turn_secret` or a path to `turn_secret_file` must have a value of your

54
flake.lock generated
View File

@@ -3,11 +3,11 @@
"advisory-db": {
"flake": false,
"locked": {
"lastModified": 1761112158,
"narHash": "sha256-RIXu/7eyKpQHjsPuAUODO81I4ni8f+WYSb7K4mTG6+0=",
"lastModified": 1766324728,
"narHash": "sha256-9C+WyE5U3y5w4WQXxmb0ylRyMMsPyzxielWXSHrcDpE=",
"owner": "rustsec",
"repo": "advisory-db",
"rev": "58f3aaec0e1776f4a900737be8cd7cb00972210d",
"rev": "c88b88c62bda077be8aa621d4e89d8701e39cb5d",
"type": "github"
},
"original": {
@@ -18,11 +18,11 @@
},
"crane": {
"locked": {
"lastModified": 1760924934,
"narHash": "sha256-tuuqY5aU7cUkR71sO2TraVKK2boYrdW3gCSXUkF4i44=",
"lastModified": 1766194365,
"narHash": "sha256-4AFsUZ0kl6MXSm4BaQgItD0VGlEKR3iq7gIaL7TjBvc=",
"owner": "ipetkov",
"repo": "crane",
"rev": "c6b4d5308293d0d04fcfeee92705017537cad02f",
"rev": "7d8ec2c71771937ab99790b45e6d9b93d15d9379",
"type": "github"
},
"original": {
@@ -39,11 +39,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1761115517,
"narHash": "sha256-Fev/ag/c3Fp3JBwHfup3lpA5FlNXfkoshnQ7dssBgJ0=",
"lastModified": 1766299592,
"narHash": "sha256-7u+q5hexu2eAxL2VjhskHvaUKg+GexmelIR2ve9Nbb4=",
"owner": "nix-community",
"repo": "fenix",
"rev": "320433651636186ea32b387cff05d6bbfa30cea7",
"rev": "381579dee168d5ced412e2990e9637ecc7cf1c5d",
"type": "github"
},
"original": {
@@ -55,11 +55,11 @@
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1747046372,
"narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
"lastModified": 1765121682,
"narHash": "sha256-4VBOP18BFeiPkyhy9o4ssBNQEvfvv1kXkasAYd0+rrA=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
"rev": "65f23138d8d09a92e30f1e5c87611b23ef451bf3",
"type": "github"
},
"original": {
@@ -74,11 +74,11 @@
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1760948891,
"narHash": "sha256-TmWcdiUUaWk8J4lpjzu4gCGxWY6/Ok7mOK4fIFfBuU4=",
"lastModified": 1765835352,
"narHash": "sha256-XswHlK/Qtjasvhd1nOa1e8MgZ8GS//jBoTqWtrS1Giw=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "864599284fc7c0ba6357ed89ed5e2cd5040f0c04",
"rev": "a34fae9c08a15ad73f295041fec82323541400a9",
"type": "github"
},
"original": {
@@ -89,11 +89,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1760878510,
"narHash": "sha256-K5Osef2qexezUfs0alLvZ7nQFTGS9DL2oTVsIXsqLgs=",
"lastModified": 1766070988,
"narHash": "sha256-G/WVghka6c4bAzMhTwT2vjLccg/awmHkdKSd2JrycLc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5e2a59a5b1a82f89f2c7e598302a9cacebb72a67",
"rev": "c6245e83d836d0433170a16eb185cefe0572f8b8",
"type": "github"
},
"original": {
@@ -105,11 +105,11 @@
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1754788789,
"narHash": "sha256-x2rJ+Ovzq0sCMpgfgGaaqgBSwY+LST+WbZ6TytnT9Rk=",
"lastModified": 1765674936,
"narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "a73b9c743612e4244d865a2fdee11865283c04e6",
"rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85",
"type": "github"
},
"original": {
@@ -132,11 +132,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1761077270,
"narHash": "sha256-O1uTuvI/rUlubJ8AXKyzh1WSWV3qCZX0huTFUvWLN4E=",
"lastModified": 1766253897,
"narHash": "sha256-ChK07B1aOlJ4QzWXpJo+y8IGAxp1V9yQ2YloJ+RgHRw=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "39990a923c8bca38f5bd29dc4c96e20ee7808d5d",
"rev": "765b7bdb432b3740f2d564afccfae831d5a972e4",
"type": "github"
},
"original": {
@@ -153,11 +153,11 @@
]
},
"locked": {
"lastModified": 1760945191,
"narHash": "sha256-ZRVs8UqikBa4Ki3X4KCnMBtBW0ux1DaT35tgsnB1jM4=",
"lastModified": 1766000401,
"narHash": "sha256-+cqN4PJz9y0JQXfAK5J1drd0U05D5fcAGhzhfVrDlsI=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "f56b1934f5f8fcab8deb5d38d42fd692632b47c2",
"rev": "42d96e75aa56a3f70cab7e7dc4a32868db28e8fd",
"type": "github"
},
"original": {

View File

@@ -97,6 +97,9 @@ rec {
craneLib.buildPackage (
(commonAttrs commonAttrsArgs)
// {
postFixup = ''
patchelf --set-rpath "$(${pkgs.patchelf}/bin/patchelf --print-rpath $out/bin/${crateInfo.pname}):${rocksdb}/lib" $out/bin/${crateInfo.pname}
'';
cargoArtifacts = deps;
doCheck = true;
env = uwuenv.buildPackageEnv // rocksdbEnv;

View File

@@ -6,6 +6,69 @@
pkgs,
...
}:
let
baseTestScript =
pkgs.writers.writePython3Bin "do_test" { libraries = [ pkgs.python3Packages.matrix-nio ]; }
''
import asyncio
import nio
async def main() -> None:
# Connect to continuwuity
client = nio.AsyncClient("http://continuwuity:6167", "alice")
# Register as user alice
response = await client.register("alice", "my-secret-password")
# Log in as user alice
response = await client.login("my-secret-password")
# Create a new room
response = await client.room_create(federate=False)
print("Matrix room create response:", response)
assert isinstance(response, nio.RoomCreateResponse)
room_id = response.room_id
# Join the room
response = await client.join(room_id)
print("Matrix join response:", response)
assert isinstance(response, nio.JoinResponse)
# Send a message to the room
response = await client.room_send(
room_id=room_id,
message_type="m.room.message",
content={
"msgtype": "m.text",
"body": "Hello continuwuity!"
}
)
print("Matrix room send response:", response)
assert isinstance(response, nio.RoomSendResponse)
# Sync responses
response = await client.sync(timeout=30000)
print("Matrix sync response:", response)
assert isinstance(response, nio.SyncResponse)
# Check the message was received by continuwuity
last_message = response.rooms.join[room_id].timeline.events[-1].body
assert last_message == "Hello continuwuity!"
# Leave the room
response = await client.room_leave(room_id)
print("Matrix room leave response:", response)
assert isinstance(response, nio.RoomLeaveResponse)
# Close the client
await client.close()
if __name__ == "__main__":
asyncio.run(main())
'';
in
{
# run some nixos tests as checks
checks = lib.pipe self'.packages [
@@ -18,106 +81,69 @@
# this test was initially yoinked from
#
# https://github.com/NixOS/nixpkgs/blob/960ce26339661b1b69c6f12b9063ca51b688615f/nixos/tests/matrix/continuwuity.nix
(builtins.map (name: {
name = "test-${name}";
value = pkgs.testers.runNixOSTest {
inherit name;
(builtins.concatMap (
name:
builtins.map
(
{ config, suffix }:
{
name = "test-${name}-${suffix}";
value = pkgs.testers.runNixOSTest {
inherit name;
nodes = {
continuwuity = {
services.matrix-continuwuity = {
enable = true;
package = self'.packages.${name};
settings.global = {
nodes = {
continuwuity = {
services.matrix-continuwuity = {
enable = true;
package = self'.packages.${name};
settings = config;
extraEnvironment.RUST_BACKTRACE = "yes";
};
networking.firewall.allowedTCPPorts = [ 6167 ];
};
client.environment.systemPackages = [ baseTestScript ];
};
testScript = ''
start_all()
with subtest("start continuwuity"):
continuwuity.wait_for_unit("continuwuity.service")
continuwuity.wait_for_open_port(6167)
with subtest("ensure messages can be exchanged"):
client.succeed("${lib.getExe baseTestScript} >&2")
'';
};
}
)
[
{
suffix = "base";
config = {
global = {
server_name = name;
address = [ "0.0.0.0" ];
allow_registration = true;
yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true;
};
extraEnvironment.RUST_BACKTRACE = "yes";
};
networking.firewall.allowedTCPPorts = [ 6167 ];
};
client =
{ pkgs, ... }:
{
environment.systemPackages = [
(pkgs.writers.writePython3Bin "do_test" { libraries = [ pkgs.python3Packages.matrix-nio ]; } ''
import asyncio
import nio
async def main() -> None:
# Connect to continuwuity
client = nio.AsyncClient("http://continuwuity:6167", "alice")
# Register as user alice
response = await client.register("alice", "my-secret-password")
# Log in as user alice
response = await client.login("my-secret-password")
# Create a new room
response = await client.room_create(federate=False)
print("Matrix room create response:", response)
assert isinstance(response, nio.RoomCreateResponse)
room_id = response.room_id
# Join the room
response = await client.join(room_id)
print("Matrix join response:", response)
assert isinstance(response, nio.JoinResponse)
# Send a message to the room
response = await client.room_send(
room_id=room_id,
message_type="m.room.message",
content={
"msgtype": "m.text",
"body": "Hello continuwuity!"
}
)
print("Matrix room send response:", response)
assert isinstance(response, nio.RoomSendResponse)
# Sync responses
response = await client.sync(timeout=30000)
print("Matrix sync response:", response)
assert isinstance(response, nio.SyncResponse)
# Check the message was received by continuwuity
last_message = response.rooms.join[room_id].timeline.events[-1].body
assert last_message == "Hello continuwuity!"
# Leave the room
response = await client.room_leave(room_id)
print("Matrix room leave response:", response)
assert isinstance(response, nio.RoomLeaveResponse)
# Close the client
await client.close()
if __name__ == "__main__":
asyncio.run(main())
'')
];
}
{
suffix = "with-room-version";
config = {
global = {
server_name = name;
address = [ "0.0.0.0" ];
allow_registration = true;
yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true;
default_room_version = "12";
};
};
};
testScript = ''
start_all()
with subtest("start continuwuity"):
continuwuity.wait_for_unit("continuwuity.service")
continuwuity.wait_for_open_port(6167)
with subtest("ensure messages can be exchanged"):
client.succeed("do_test >&2")
'';
};
}))
}
]
))
builtins.listToAttrs
];
};

5171
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

31
package.json Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "continuwuation",
"version": "1.0.0",
"description": "<!-- ANCHOR: catchphrase -->",
"main": "index.js",
"directories": {
"doc": "docs",
"test": "tests"
},
"scripts": {
"docs:dev": "rspress dev docs",
"docs:build": "rspress build docs",
"docs:preview": "rspress preview docs",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "ssh://git@forgejo.ellis.link/continuwuation/continuwuity.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"devDependencies": {
"@rspress/core": "^2.0.0-rc.1",
"@rspress/plugin-client-redirects": "^2.0.0-alpha.12",
"@rspress/plugin-preview": "^2.0.0-beta.35",
"@rspress/plugin-sitemap": "^2.0.0-beta.23",
"typescript": "^5.9.3"
}
}

View File

@@ -63,7 +63,7 @@ Restart=on-failure
RestartSec=5
TimeoutStopSec=4m
TimeoutStartSec=4m
TimeoutStartSec=10m
StartLimitInterval=1m
StartLimitBurst=5

View File

@@ -22,7 +22,7 @@ # Update remote package lists
```
The `continuwuity` package conflicts with the old `conduwuit` package and will remove it automatically when installed.
See the [generic deployment guide](../deploying/generic.md) for additional information about using the Debian package.
See the [generic deployment guide](/deploying/generic.mdx) for additional information about using the Debian package.
### Configuration
@@ -32,8 +32,8 @@ ### Configuration
### Running
The package uses the [`conduwuit.service`](../configuration/examples.md#example-systemd-unit-file) systemd unit file to start and stop Continuwuity. The binary installs at `/usr/bin/conduwuit`.
The package uses the `conduwuit.service` systemd unit file to start and stop Continuwuity. The binary installs at `/usr/bin/conduwuit`.
By default, this package assumes that Continuwuity runs behind a reverse proxy. The default configuration options apply (listening on `localhost` and TCP port `6167`). Matrix federation requires a valid domain name and TLS. To federate properly, you must set up TLS certificates and certificate renewal.
For information about setting up a reverse proxy and TLS, consult online documentation and guides. The [generic deployment guide](../deploying/generic.md#setting-up-the-reverse-proxy) documents Caddy, which is the most user-friendly option for reverse proxy configuration.
For information about setting up a reverse proxy and TLS, consult online documentation and guides. The [generic deployment guide](/deploying/generic.md#setting-up-the-reverse-proxy) documents Caddy, which is the most user-friendly option for reverse proxy configuration.

60
rspress.config.ts Normal file
View File

@@ -0,0 +1,60 @@
import { defineConfig } from '@rspress/core';
import { pluginPreview } from '@rspress/plugin-preview';
import { pluginSitemap } from '@rspress/plugin-sitemap';
import { pluginClientRedirects } from '@rspress/plugin-client-redirects';
export default defineConfig({
root: 'docs',
title: 'Continuwuity',
description: 'A community-driven Matrix homeserver',
icon: '/assets/logo.svg',
logo: {
light: '/assets/logo.svg',
dark: '/assets/logo.svg',
},
themeConfig: {
socialLinks: [
{
icon: {
svg: `<svg role="img" viewBox="0 0 24 24" width="100%" xmlns="http://www.w3.org/2000/svg"><title>Matrix</title><path fill="currentColor" d="M.632.55v22.9H2.28V24H0V0h2.28v.55zm7.043 7.26v1.157h.033c.309-.443.683-.784 1.117-1.024.433-.245.936-.365 1.5-.365.54 0 1.033.107 1.481.314.448.208.785.582 1.02 1.108.254-.374.6-.706 1.034-.992.434-.287.95-.43 1.546-.43.453 0 .872.056 1.26.167.388.11.716.286.993.53.276.245.489.559.646.951.152.392.23.863.23 1.417v5.728h-2.349V11.52c0-.286-.01-.559-.032-.812a1.755 1.755 0 0 0-.18-.66 1.106 1.106 0 0 0-.438-.448c-.194-.11-.457-.166-.785-.166-.332 0-.6.064-.803.189a1.38 1.38 0 0 0-.48.499 1.946 1.946 0 0 0-.231.696 5.56 5.56 0 0 0-.06.785v4.768h-2.35v-4.8c0-.254-.004-.503-.018-.752a2.074 2.074 0 0 0-.143-.688 1.052 1.052 0 0 0-.415-.503c-.194-.125-.476-.19-.854-.19-.111 0-.259.024-.439.074-.18.051-.36.143-.53.282-.171.138-.319.337-.439.595-.12.259-.18.6-.18 1.02v4.966H5.46V7.81zm15.693 15.64V.55H21.72V0H24v24h-2.28v-.55z"/></svg>`
},
mode: 'link',
content: 'https://matrix.to/#/#continuwuity:continuwuity.org',
},
{
icon: {
svg: `<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Forgejo</title><path fill="currentColor" d="M16.7773 0c1.6018 0 2.9004 1.2986 2.9004 2.9005s-1.2986 2.9004-2.9004 2.9004c-1.0854 0-2.0315-.596-2.5288-1.4787H12.91c-2.3322 0-4.2272 1.8718-4.2649 4.195l-.0007 2.1175a7.0759 7.0759 0 0 1 4.148-1.4205l.1176-.001 1.3385.0002c.4973-.8827 1.4434-1.4788 2.5288-1.4788 1.6018 0 2.9004 1.2986 2.9004 2.9005s-1.2986 2.9004-2.9004 2.9004c-1.0854 0-2.0315-.596-2.5288-1.4787H12.91c-2.3322 0-4.2272 1.8718-4.2649 4.195l-.0007 2.319c.8827.4973 1.4788 1.4434 1.4788 2.5287 0 1.602-1.2986 2.9005-2.9005 2.9005-1.6018 0-2.9004-1.2986-2.9004-2.9005 0-1.0853.596-2.0314 1.4788-2.5287l-.0002-9.9831c0-3.887 3.1195-7.0453 6.9915-7.108l.1176-.001h1.3385C14.7458.5962 15.692 0 16.7773 0ZM7.2227 19.9052c-.6596 0-1.1943.5347-1.1943 1.1943s.5347 1.1943 1.1943 1.1943 1.1944-.5347 1.1944-1.1943-.5348-1.1943-1.1944-1.1943Zm9.5546-10.4644c-.6596 0-1.1944.5347-1.1944 1.1943s.5348 1.1943 1.1944 1.1943c.6596 0 1.1943-.5347 1.1943-1.1943s-.5347-1.1943-1.1943-1.1943Zm0-7.7346c-.6596 0-1.1944.5347-1.1944 1.1943s.5348 1.1943 1.1944 1.1943c.6596 0 1.1943-.5347 1.1943-1.1943s-.5347-1.1943-1.1943-1.1943Z"/></svg>`
},
mode: 'link',
content: 'https://forgejo.ellis.link/continuwuation/continuwuity'
},
{
icon: 'github',
mode: 'link',
content: 'https://github.com/continuwuity/continuwuity',
},
],
lastUpdated: true,
enableContentAnimation: true,
enableAppearanceAnimation: false,
footer: {
},
},
plugins: [pluginPreview(), pluginSitemap({
siteUrl: 'https://continuwuity.org', // TODO: Set automatically in build pipeline
}),
pluginClientRedirects({
redirects: [{
from: '/configuration/examples',
to: '/reference/config'
}, {
from: '/admin_reference',
to: '/reference/admin'
}, {
from: '/server_reference',
to: '/reference/server'
}
]
})],
});

View File

@@ -41,7 +41,7 @@ async fn changes_since(
let results: Vec<_> = self
.services
.account_data
.changes_since(room_id.as_deref(), &user_id, since, None)
.changes_since(room_id.as_deref(), &user_id, Some(since), None)
.collect()
.await;
let query_time = timer.elapsed();

View File

@@ -31,7 +31,7 @@ pub(super) async fn last(&self, room_id: OwnedRoomOrAliasId) -> Result {
.services
.rooms
.timeline
.last_timeline_count(None, &room_id)
.last_timeline_count(&room_id)
.await?;
self.write_str(&format!("{result:#?}")).await
@@ -52,7 +52,7 @@ pub(super) async fn pdus(
.services
.rooms
.timeline
.pdus_rev(None, &room_id, from)
.pdus_rev(&room_id, from)
.try_take(limit.unwrap_or(3))
.try_collect()
.await?;

View File

@@ -30,10 +30,31 @@ pub(super) async fn show_config(&self) -> Result {
#[admin_command]
pub(super) async fn reload_config(&self, path: Option<PathBuf>) -> Result {
let path = path.as_deref().into_iter();
self.services.config.reload(path)?;
// The path argument is only what's optionally passed via the admin command,
// so we need to merge it with the existing paths if any were given at startup.
let mut paths = Vec::new();
self.write_str("Successfully reconfigured.").await
// Add previously saved paths to the argument list
self.services
.config
.config_paths
.clone()
.unwrap_or_default()
.iter()
.for_each(|p| paths.push(p.to_owned()));
// If a path is given, and it's not already in the list,
// add it last, so that it overrides earlier files
if let Some(p) = path {
if !paths.contains(&p) {
paths.push(p);
}
}
self.services.config.reload(&paths)?;
self.write_str(&format!("Successfully reconfigured from paths: {paths:?}"))
.await
}
#[admin_command]

View File

@@ -1,4 +1,7 @@
use std::{collections::BTreeMap, fmt::Write as _};
use std::{
collections::{BTreeMap, HashSet},
fmt::Write as _,
};
use api::client::{
full_user_deactivate, join_room_by_id_helper, leave_all_rooms, leave_room, remote_leave_room,
@@ -12,7 +15,7 @@
};
use futures::{FutureExt, StreamExt};
use ruma::{
OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId, UserId,
OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, UserId,
events::{
RoomAccountDataEventType, StateEventType,
room::{
@@ -950,23 +953,38 @@ pub(super) async fn force_leave_remote_room(
&self,
user_id: String,
room_id: OwnedRoomOrAliasId,
via: Option<String>,
) -> Result {
let user_id = parse_local_user_id(self.services, &user_id)?;
let (room_id, _) = self
let (room_id, vias_raw) = self
.services
.rooms
.alias
.resolve_with_servers(&room_id, None)
.resolve_with_servers(
&room_id,
if let Some(v) = via.clone() {
Some(vec![OwnedServerName::parse(v)?])
} else {
None
},
)
.await?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
remote_leave_room(self.services, &user_id, &room_id, None)
let mut vias: HashSet<OwnedServerName> = HashSet::new();
if let Some(via) = via {
vias.insert(OwnedServerName::parse(via)?);
}
for server in vias_raw {
vias.insert(server);
}
remote_leave_room(self.services, &user_id, &room_id, None, vias)
.boxed()
.await?;
self.write_str(&format!("{user_id} has been joined to {room_id}.",))
self.write_str(&format!("{user_id} successfully left {room_id} via remote server."))
.await
}

View File

@@ -107,6 +107,7 @@ pub enum UserCommand {
ForceLeaveRemoteRoom {
user_id: String,
room_id: OwnedRoomOrAliasId,
via: Option<String>,
},
/// - Forces the specified user to drop their power levels to the room

View File

@@ -59,7 +59,7 @@ pub(crate) async fn get_context_route(
.rooms
.timeline
.get_pdu(event_id)
.map_err(|_| err!(Request(NotFound("Base event not found."))));
.map_err(|_| err!(Request(NotFound("Event not found."))));
let visible = services
.rooms
@@ -70,7 +70,7 @@ pub(crate) async fn get_context_route(
let (base_id, base_pdu, visible) = try_join3(base_id, base_pdu, visible).await?;
if base_pdu.room_id_or_hash() != *room_id || base_pdu.event_id != *event_id {
return Err!(Request(NotFound("Base event not found.")));
return Err!(Request(NotFound("Event not found.")));
}
if !visible {
@@ -82,11 +82,25 @@ pub(crate) async fn get_context_route(
let base_event = ignored_filter(&services, (base_count, base_pdu), sender_user);
// PDUs are used to get seen user IDs and then returned in response.
let events_before = services
.rooms
.timeline
.pdus_rev(Some(sender_user), room_id, Some(base_count))
.pdus_rev(room_id, Some(base_count))
.ignore_err()
.then(async |mut pdu| {
pdu.1.set_unsigned(Some(sender_user));
if let Err(e) = services
.rooms
.pdu_metadata
.add_bundled_aggregations_to_pdu(sender_user, &mut pdu.1)
.await
{
debug_warn!("Failed to add bundled aggregations: {e}");
}
pdu
})
.ready_filter_map(|item| event_filter(item, filter))
.wide_filter_map(|item| ignored_filter(&services, item, sender_user))
.wide_filter_map(|item| visibility_filter(&services, item, sender_user))
@@ -96,8 +110,20 @@ pub(crate) async fn get_context_route(
let events_after = services
.rooms
.timeline
.pdus(Some(sender_user), room_id, Some(base_count))
.pdus(room_id, Some(base_count))
.ignore_err()
.then(async |mut pdu| {
pdu.1.set_unsigned(Some(sender_user));
if let Err(e) = services
.rooms
.pdu_metadata
.add_bundled_aggregations_to_pdu(sender_user, &mut pdu.1)
.await
{
debug_warn!("Failed to add bundled aggregations: {e}");
}
pdu
})
.ready_filter_map(|item| event_filter(item, filter))
.wide_filter_map(|item| ignored_filter(&services, item, sender_user))
.wide_filter_map(|item| visibility_filter(&services, item, sender_user))

View File

@@ -389,7 +389,7 @@ pub(crate) async fn get_key_changes_route(
device_list_updates.extend(
services
.users
.keys_changed(sender_user, from, Some(to))
.keys_changed(sender_user, Some(from), Some(to))
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await,
@@ -401,7 +401,7 @@ pub(crate) async fn get_key_changes_route(
device_list_updates.extend(
services
.users
.room_keys_changed(room_id, from, Some(to))
.room_keys_changed(room_id, Some(from), Some(to))
.map(|(user_id, _)| user_id)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()

View File

@@ -44,6 +44,7 @@
rooms::{
state::RoomMutexGuard,
state_compressor::{CompressedState, HashSetCompressStateEvent},
timeline::pdu_fits,
},
};
@@ -573,6 +574,13 @@ async fn join_room_by_id_helper_remote(
return state;
},
};
if !pdu_fits(&mut value.clone()) {
warn!(
"dropping incoming PDU {event_id} in room {room_id} from room join because \
it exceeds 65535 bytes or is otherwise too large."
);
return state;
}
services.rooms.outlier.add_pdu_outlier(&event_id, &value);
if let Some(state_key) = &pdu.state_key {
let shortstatekey = services

View File

@@ -5,7 +5,7 @@
use conduwuit::{
Err, Result, debug, debug_info, debug_warn, err, info,
matrix::{
event::{Event, gen_event_id},
event::gen_event_id,
pdu::{PduBuilder, PduEvent},
},
result::FlatOk,
@@ -458,7 +458,7 @@ async fn knock_room_helper_local(
.await,
};
let send_knock_response = services
services
.sending
.send_federation_request(&remote_server, send_knock_request)
.await?;
@@ -477,20 +477,14 @@ async fn knock_room_helper_local(
.map_err(|e| err!(BadServerResponse("Invalid knock event PDU: {e:?}")))?;
info!("Updating membership locally to knock state with provided stripped state events");
// TODO: this call does not appear to do anything because `update_membership`
// doesn't call `mark_as_knock`. investigate further, ideally with the aim of
// removing this call entirely -- Ginger thinks `update_membership` should only
// be called from `force_state` and `append_pdu`.
services
.rooms
.state_cache
.update_membership(
room_id,
sender_user,
parsed_knock_pdu
.get_content::<RoomMemberEventContent>()
.expect("we just created this"),
sender_user,
Some(send_knock_response.knock_room_state),
None,
false,
)
.update_membership(room_id, sender_user, &parsed_knock_pdu, false)
.await?;
info!("Appending room knock event locally");
@@ -677,20 +671,11 @@ async fn knock_room_helper_remote(
.await?;
info!("Updating membership locally to knock state with provided stripped state events");
// TODO: see TODO on the other call to `update_membership`
services
.rooms
.state_cache
.update_membership(
room_id,
sender_user,
parsed_knock_pdu
.get_content::<RoomMemberEventContent>()
.expect("we just created this"),
sender_user,
Some(send_knock_response.knock_room_state),
None,
false,
)
.update_membership(room_id, sender_user, &parsed_knock_pdu, false)
.await?;
info!("Appending room knock event locally");

View File

@@ -2,12 +2,12 @@
use axum::extract::State;
use conduwuit::{
Err, Result, debug_info, debug_warn, err,
Err, Pdu, Result, debug_info, debug_warn, err,
matrix::{event::gen_event_id, pdu::PduBuilder},
utils::{self, FutureBoolExt, future::ReadyEqExt},
warn,
};
use futures::{FutureExt, StreamExt, TryFutureExt, pin_mut};
use futures::{FutureExt, StreamExt, pin_mut};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedServerName, RoomId, RoomVersionId, UserId,
api::{
@@ -81,42 +81,9 @@ pub async fn leave_room(
room_id: &RoomId,
reason: Option<String>,
) -> Result {
let default_member_content = RoomMemberEventContent {
membership: MembershipState::Leave,
reason: reason.clone(),
join_authorized_via_users_server: None,
is_direct: None,
avatar_url: None,
displayname: None,
third_party_invite: None,
blurhash: None,
redact_events: None,
};
let is_banned = services.rooms.metadata.is_banned(room_id);
let is_disabled = services.rooms.metadata.is_disabled(room_id);
pin_mut!(is_banned, is_disabled);
if is_banned.or(is_disabled).await {
// the room is banned/disabled, the room must be rejected locally since we
// cant/dont want to federate with this server
services
.rooms
.state_cache
.update_membership(
room_id,
user_id,
default_member_content,
user_id,
None,
None,
true,
)
.await?;
return Ok(());
}
let dont_have_room = services
.rooms
.state_cache
@@ -129,43 +96,41 @@ pub async fn leave_room(
.is_knocked(user_id, room_id)
.eq(&false);
// Ask a remote server if we don't have this room and are not knocking on it
if dont_have_room.and(not_knocked).await {
if let Err(e) = remote_leave_room(services, user_id, room_id, reason.clone())
.boxed()
.await
{
warn!(%user_id, "Failed to leave room {room_id} remotely: {e}");
// Don't tell the client about this error
}
pin_mut!(is_banned, is_disabled);
let last_state = services
.rooms
.state_cache
.invite_state(user_id, room_id)
.or_else(|_| services.rooms.state_cache.knock_state(user_id, room_id))
.or_else(|_| services.rooms.state_cache.left_state(user_id, room_id))
.await
.ok();
/*
there are three possible cases when leaving a room:
1. the room is banned or disabled, so we're not federating with it.
2. nobody on the homeserver is in the room, which can happen if the user is rejecting an invite
to a room that we don't have any members in.
3. someone else on the homeserver is in the room. in this case we can leave like normal by sending a PDU over federation.
// We always drop the invite, we can't rely on other servers
services
.rooms
.state_cache
.update_membership(
room_id,
user_id,
default_member_content,
user_id,
last_state,
None,
true,
)
.await?;
in cases 1 and 2, we have to update the state cache using `mark_as_left` directly.
otherwise `build_and_append_pdu` will take care of updating the state cache for us.
*/
// `leave_pdu` is the outlier `m.room.member` event which will be synced to the
// user. if it's None the sync handler will create a dummy PDU.
let leave_pdu = if is_banned.or(is_disabled).await {
// case 1: the room is banned/disabled. we don't want to federate with another
// server to leave, so we can't create an outlier PDU.
None
} else if dont_have_room.and(not_knocked).await {
// case 2: ask a remote server to assist us with leaving
// we always mark the room as left locally, regardless of if the federated leave
// failed
remote_leave_room(services, user_id, room_id, reason.clone(), HashSet::new())
.await
.inspect_err(|err| {
warn!(%user_id, "Failed to leave room {room_id} remotely: {err}");
})
.ok()
} else {
// case 3: we can leave by sending a PDU.
let state_lock = services.rooms.state.mutex.lock(room_id).await;
let Ok(event) = services
let user_member_event_content = services
.rooms
.state_accessor
.room_state_get_content::<RoomMemberEventContent>(
@@ -173,64 +138,84 @@ pub async fn leave_room(
&StateEventType::RoomMember,
user_id.as_str(),
)
.await
else {
debug_warn!(
"Trying to leave a room you are not a member of, marking room as left locally."
);
.await;
return services
.rooms
.state_cache
.update_membership(
room_id,
user_id,
default_member_content,
user_id,
None,
None,
true,
)
.await;
};
match user_member_event_content {
| Ok(content) => {
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
membership: MembershipState::Leave,
reason,
join_authorized_via_users_server: None,
is_direct: None,
..content
}),
user_id,
Some(room_id),
&state_lock,
)
.await?;
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
membership: MembershipState::Leave,
reason,
join_authorized_via_users_server: None,
is_direct: None,
..event
}),
user_id,
Some(room_id),
&state_lock,
)
.await?;
}
// `build_and_append_pdu` calls `mark_as_left` internally, so we return early.
return Ok(());
},
| Err(_) => {
// an exception to case 3 is if the user isn't even in the room they're trying
// to leave. this can happen if the client's caching is wrong.
debug_warn!(
"Trying to leave a room you are not a member of, marking room as left \
locally."
);
// return the existing leave state, if one exists. `mark_as_left` will then
// update the `roomuserid_leftcount` table, making the leave come down sync
// again.
services
.rooms
.state_cache
.left_state(user_id, room_id)
.await?
},
}
};
services
.rooms
.state_cache
.mark_as_left(user_id, room_id, leave_pdu)
.await;
services
.rooms
.state_cache
.update_joined_count(room_id)
.await;
Ok(())
}
pub async fn remote_leave_room(
pub async fn remote_leave_room<S: ::std::hash::BuildHasher>(
services: &Services,
user_id: &UserId,
room_id: &RoomId,
reason: Option<String>,
) -> Result<()> {
mut servers: HashSet<OwnedServerName, S>,
) -> Result<Pdu> {
let mut make_leave_response_and_server =
Err!(BadServerResponse("No remote server available to assist in leaving {room_id}."));
let mut servers: HashSet<OwnedServerName> = services
.rooms
.state_cache
.servers_invite_via(room_id)
.map(ToOwned::to_owned)
.collect()
.await;
servers.extend(
services
.rooms
.state_cache
.servers_invite_via(room_id)
.map(ToOwned::to_owned)
.collect::<HashSet<OwnedServerName>>()
.await,
);
match services
.rooms
@@ -277,6 +262,11 @@ pub async fn remote_leave_room(
if let Some(room_id_server_name) = room_id.server_name() {
servers.insert(room_id_server_name.to_owned());
}
if servers.is_empty() {
return Err!(BadServerResponse(warn!(
"No remote servers found to assist in leaving {room_id}."
)));
}
debug_info!("servers in remote_leave_room: {servers:?}");
@@ -284,7 +274,7 @@ pub async fn remote_leave_room(
let make_leave_response = services
.sending
.send_federation_request(
&remote_server,
remote_server.as_ref(),
federation::membership::prepare_leave_event::v1::Request {
room_id: room_id.to_owned(),
user_id: user_id.to_owned(),
@@ -292,11 +282,21 @@ pub async fn remote_leave_room(
)
.await;
make_leave_response_and_server = make_leave_response.map(|r| (r, remote_server));
let error = make_leave_response.as_ref().err().map(ToString::to_string);
make_leave_response_and_server = make_leave_response.map(|r| (r, remote_server.clone()));
if make_leave_response_and_server.is_ok() {
debug_info!(
"Received make_leave_response from {} for leaving {room_id}",
remote_server
);
break;
}
debug_warn!(
"Failed to get make_leave_response from {} for leaving {room_id}: {}",
remote_server,
error.unwrap()
);
}
let (make_leave_response, remote_server) = make_leave_response_and_server?;
@@ -304,13 +304,14 @@ pub async fn remote_leave_room(
let Some(room_version_id) = make_leave_response.room_version else {
return Err!(BadServerResponse(warn!(
"No room version was returned by {remote_server} for {room_id}, room version is \
likely not supported by conduwuit"
likely not supported by continuwuity"
)));
};
if !services.server.supported_room_version(&room_version_id) {
return Err!(BadServerResponse(warn!(
"Remote room version {room_version_id} for {room_id} is not supported by conduwuit",
"Remote room version {room_version_id} for {room_id} is not supported by \
continuwuity",
)));
}
@@ -373,7 +374,7 @@ pub async fn remote_leave_room(
&remote_server,
federation::membership::create_leave_event::v2::Request {
room_id: room_id.to_owned(),
event_id,
event_id: event_id.clone(),
pdu: services
.sending
.convert_to_outgoing_federation_event(leave_event.clone())
@@ -382,5 +383,14 @@ pub async fn remote_leave_room(
)
.await?;
Ok(())
services
.rooms
.outlier
.add_pdu_outlier(&event_id, &leave_event);
let leave_pdu = Pdu::from_id_val(&event_id, leave_event).map_err(|e| {
err!(BadServerResponse("Invalid leave PDU received during federated leave: {e:?}"))
})?;
Ok(leave_pdu)
}

View File

@@ -1,6 +1,7 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
Err, Result, at,
Err, Result, at, debug_warn,
matrix::{
event::{Event, Matches},
pdu::PduCount,
@@ -16,7 +17,7 @@
Services,
rooms::{
lazy_loading,
lazy_loading::{Options, Witness},
lazy_loading::{MemberSet, Options},
timeline::PdusIterItem,
},
};
@@ -70,6 +71,7 @@
/// where the user was joined, depending on `history_visibility`)
pub(crate) async fn get_message_events_route(
State(services): State<crate::State>,
InsecureClientIp(client_ip): InsecureClientIp,
body: Ruma<get_message_events::v3::Request>,
) -> Result<get_message_events::v3::Response> {
debug_assert!(IGNORED_MESSAGE_TYPES.is_sorted(), "IGNORED_MESSAGE_TYPES is not sorted");
@@ -78,6 +80,11 @@ pub(crate) async fn get_message_events_route(
let room_id = &body.room_id;
let filter = &body.filter;
services
.users
.update_device_last_seen(sender_user, sender_device, client_ip)
.await;
if !services.rooms.metadata.exists(room_id).await {
return Err!(Request(Forbidden("Room does not exist to this server")));
}
@@ -115,14 +122,14 @@ pub(crate) async fn get_message_events_route(
| Direction::Forward => services
.rooms
.timeline
.pdus(Some(sender_user), room_id, Some(from))
.pdus(room_id, Some(from))
.ignore_err()
.boxed(),
| Direction::Backward => services
.rooms
.timeline
.pdus_rev(Some(sender_user), room_id, Some(from))
.pdus_rev(room_id, Some(from))
.ignore_err()
.boxed(),
};
@@ -133,6 +140,18 @@ pub(crate) async fn get_message_events_route(
.wide_filter_map(|item| ignored_filter(&services, item, sender_user))
.wide_filter_map(|item| visibility_filter(&services, item, sender_user))
.take(limit)
.then(async |mut pdu| {
pdu.1.set_unsigned(Some(sender_user));
if let Err(e) = services
.rooms
.pdu_metadata
.add_bundled_aggregations_to_pdu(sender_user, &mut pdu.1)
.await
{
debug_warn!("Failed to add bundled aggregations: {e}");
}
pdu
})
.collect()
.await;
@@ -162,7 +181,7 @@ pub(crate) async fn get_message_events_route(
let state = witness
.map(Option::into_iter)
.map(|option| option.flat_map(Witness::into_iter))
.map(|option| option.flat_map(MemberSet::into_iter))
.map(IterStream::stream)
.into_stream()
.flatten()
@@ -192,7 +211,7 @@ pub(crate) async fn lazy_loading_witness<'a, I>(
services: &Services,
lazy_loading_context: &lazy_loading::Context<'_>,
events: I,
) -> Witness
) -> MemberSet
where
I: Iterator<Item = &'a PdusIterItem> + Clone + Send,
{
@@ -213,10 +232,10 @@ pub(crate) async fn lazy_loading_witness<'a, I>(
let receipts = services
.rooms
.read_receipt
.readreceipts_since(lazy_loading_context.room_id, oldest.into_unsigned());
.readreceipts_since(lazy_loading_context.room_id, Some(oldest.into_unsigned()));
pin_mut!(receipts);
let witness: Witness = events
let witness: MemberSet = events
.stream()
.map(ref_at!(1))
.map(Event::sender)
@@ -224,7 +243,7 @@ pub(crate) async fn lazy_loading_witness<'a, I>(
.chain(
receipts
.ready_take_while(|(_, c, _)| *c <= newest.into_unsigned())
.map(|(user_id, ..)| user_id.to_owned()),
.map(|(user_id, ..)| user_id),
)
.collect()
.await;
@@ -232,7 +251,7 @@ pub(crate) async fn lazy_loading_witness<'a, I>(
services
.rooms
.lazy_loading
.witness_retain(witness, lazy_loading_context)
.retain_lazy_members(witness, lazy_loading_context)
.await
}

View File

@@ -1,6 +1,7 @@
use std::collections::BTreeMap;
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{Err, PduCount, Result, err};
use ruma::{
MilliSecondsSinceUnixEpoch,
@@ -118,9 +119,14 @@ pub(crate) async fn set_read_marker_route(
/// Sets private read marker and public read receipt EDU.
pub(crate) async fn create_receipt_route(
State(services): State<crate::State>,
InsecureClientIp(client_ip): InsecureClientIp,
body: Ruma<create_receipt::v3::Request>,
) -> Result<create_receipt::v3::Response> {
let sender_user = body.sender_user();
services
.users
.update_device_last_seen(sender_user, body.sender_device.as_deref(), client_ip)
.await;
if matches!(
&body.receipt_type,

View File

@@ -1,4 +1,5 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{Err, Result, matrix::pdu::PduBuilder};
use ruma::{
api::client::redact::redact_event, events::room::redaction::RoomRedactionEventContent,
@@ -13,9 +14,14 @@
/// - TODO: Handle txn id
pub(crate) async fn redact_event_route(
State(services): State<crate::State>,
InsecureClientIp(client_ip): InsecureClientIp,
body: Ruma<redact_event::v3::Request>,
) -> Result<redact_event::v3::Response> {
let sender_user = body.sender_user();
services
.users
.update_device_last_seen(sender_user, body.sender_device.as_deref(), client_ip)
.await;
let body = &body.body;
if services.users.is_suspended(sender_user).await? {
// TODO: Users can redact their own messages while suspended

View File

@@ -1,6 +1,6 @@
use axum::extract::State;
use conduwuit::{
Result, at,
Err, Result, at, debug_warn,
matrix::{Event, event::RelationTypeEqual, pdu::PduCount},
utils::{IterStream, ReadyExt, result::FlatOk, stream::WidebandExt},
};
@@ -109,6 +109,16 @@ async fn paginate_relations_with_filter(
recurse: bool,
dir: Direction,
) -> Result<get_relating_events::v1::Response> {
if !services
.rooms
.state_accessor
.user_can_see_event(sender_user, room_id, target)
.await
{
debug_warn!(req_evt = ?target, ?room_id, "Event relations requested by {sender_user} but is not allowed to see it, returning 404");
return Err!(Request(NotFound("Event not found.")));
}
let start: PduCount = from
.map(str::parse)
.transpose()?
@@ -129,11 +139,6 @@ async fn paginate_relations_with_filter(
// Spec (v1.10) recommends depth of at least 3
let depth: u8 = if recurse { 3 } else { 1 };
// Check if this is a thread request
let is_thread = filter_rel_type
.as_ref()
.is_some_and(|rel| *rel == RelationType::Thread);
let events: Vec<_> = services
.rooms
.pdu_metadata
@@ -152,40 +157,24 @@ async fn paginate_relations_with_filter(
})
.stream()
.ready_take_while(|(count, _)| Some(*count) != to)
.wide_filter_map(|item| visibility_filter(services, sender_user, item))
.take(limit)
.wide_filter_map(|item| visibility_filter(services, sender_user, item))
.then(async |mut pdu| {
if let Err(e) = services
.rooms
.pdu_metadata
.add_bundled_aggregations_to_pdu(sender_user, &mut pdu.1)
.await
{
debug_warn!("Failed to add bundled aggregations to relation: {e}");
}
pdu
})
.collect()
.await;
// For threads, check if we should include the root event
let mut root_event = None;
if is_thread && dir == Direction::Backward {
// Check if we've reached the beginning of the thread
// (fewer events than requested means we've exhausted the thread)
if events.len() < limit {
// Try to get the thread root event
if let Ok(root_pdu) = services.rooms.timeline.get_pdu(target).await {
// Check visibility
if services
.rooms
.state_accessor
.user_can_see_event(sender_user, room_id, target)
.await
{
// Store the root event to add to the response
root_event = Some(root_pdu);
}
}
}
}
// Determine if there are more events to fetch
let has_more = if root_event.is_some() {
false // We've included the root, no more events
} else {
// Check if we got a full page of results (might be more)
events.len() >= limit
};
let has_more = events.len() >= limit;
let next_batch = if has_more {
match dir {
@@ -197,11 +186,10 @@ async fn paginate_relations_with_filter(
None
};
// Build the response chunk with thread root if needed
let chunk: Vec<_> = root_event
let chunk: Vec<_> = events
.into_iter()
.map(at!(1))
.map(Event::into_format)
.chain(events.into_iter().map(at!(1)).map(Event::into_format))
.collect();
Ok(get_relating_events::v1::Response {

View File

@@ -1,5 +1,5 @@
use axum::extract::State;
use conduwuit::{Err, Event, Result, err};
use conduwuit::{Err, Event, Result, debug_warn, err};
use futures::{FutureExt, TryFutureExt, future::try_join};
use ruma::api::client::room::get_room_event;
@@ -33,7 +33,16 @@ pub(crate) async fn get_room_event_route(
return Err!(Request(Forbidden("You don't have permission to view this event.")));
}
event.add_age().ok();
if let Err(e) = services
.rooms
.pdu_metadata
.add_bundled_aggregations_to_pdu(body.sender_user(), &mut event)
.await
{
debug_warn!("Failed to add bundled aggregations to event: {e}");
}
event.set_unsigned(body.sender_user.as_deref());
Ok(get_room_event::v3::Response { event: event.into_format() })
}

View File

@@ -1,6 +1,6 @@
use axum::extract::State;
use conduwuit::{
Err, Event, Result, at,
Err, Event, Result, at, debug_warn,
utils::{BoolExt, stream::TryTools},
};
use futures::{FutureExt, TryStreamExt, future::try_join4};
@@ -40,12 +40,28 @@ pub(crate) async fn room_initial_sync_route(
.map_ok(Event::into_format)
.try_collect::<Vec<_>>();
// Events are returned in body
let limit = LIMIT_MAX;
let events = services
.rooms
.timeline
.pdus_rev(None, room_id, None)
.pdus_rev(room_id, None)
.try_take(limit)
.and_then(async |mut pdu| {
pdu.1.set_unsigned(body.sender_user.as_deref());
if let Some(sender_user) = body.sender_user.as_deref() {
if let Err(e) = services
.rooms
.pdu_metadata
.add_bundled_aggregations_to_pdu(sender_user, &mut pdu.1)
.await
{
debug_warn!("Failed to add bundled aggregations: {e}");
}
}
Ok(pdu)
})
.try_collect::<Vec<_>>();
let (membership, visibility, state, events) =

View File

@@ -2,7 +2,7 @@
use axum::extract::State;
use conduwuit::{
Err, Result, at, is_true,
Err, Result, at, debug_warn, is_true,
matrix::Event,
result::FlatOk,
utils::{IterStream, stream::ReadyExt},
@@ -50,7 +50,7 @@ pub(crate) async fn search_events_route(
Ok(Response {
search_categories: ResultCategories {
room_events: room_events_result
room_events: Box::pin(room_events_result)
.await
.unwrap_or_else(|| Ok(ResultRoomEvents::default()))?,
},
@@ -110,7 +110,12 @@ async fn category_room_events(
limit,
};
let (count, results) = services.rooms.search.search_pdus(&query).await.ok()?;
let (count, results) = services
.rooms
.search
.search_pdus(&query, sender_user)
.await
.ok()?;
results
.collect::<Vec<_>>()
@@ -144,6 +149,17 @@ async fn category_room_events(
.map(at!(2))
.flatten()
.stream()
.then(|mut pdu| async {
if let Err(e) = services
.rooms
.pdu_metadata
.add_bundled_aggregations_to_pdu(sender_user, &mut pdu)
.await
{
debug_warn!("Failed to add bundled aggregations to search result: {e}");
}
pdu
})
.map(Event::into_format)
.map(|result| SearchResult {
rank: None,

View File

@@ -1,6 +1,7 @@
use std::collections::BTreeMap;
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{Err, Result, err, matrix::pdu::PduBuilder, utils};
use ruma::{api::client::message::send_message_event, events::MessageLikeEventType};
use serde_json::from_str;
@@ -18,6 +19,7 @@
/// allowed
pub(crate) async fn send_message_event_route(
State(services): State<crate::State>,
InsecureClientIp(client_ip): InsecureClientIp,
body: Ruma<send_message_event::v3::Request>,
) -> Result<send_message_event::v3::Response> {
let sender_user = body.sender_user();
@@ -27,6 +29,11 @@ pub(crate) async fn send_message_event_route(
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
services
.users
.update_device_last_seen(sender_user, body.sender_device.as_deref(), client_ip)
.await;
// Forbid m.room.encrypted if encryption is disabled
if MessageLikeEventType::RoomEncrypted == body.event_type && !services.config.allow_encryption
{

View File

@@ -1,4 +1,5 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
Err, Result, err,
matrix::{Event, pdu::PduBuilder},
@@ -7,7 +8,7 @@
use conduwuit_service::Services;
use futures::{FutureExt, TryStreamExt};
use ruma::{
OwnedEventId, RoomId, UserId,
MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, UserId,
api::client::state::{get_state_events, get_state_events_for_key, send_state_event},
events::{
AnyStateEventContent, StateEventType,
@@ -30,9 +31,14 @@
/// Sends a state event into the room.
pub(crate) async fn send_state_event_for_key_route(
State(services): State<crate::State>,
InsecureClientIp(ip): InsecureClientIp,
body: Ruma<send_state_event::v3::Request>,
) -> Result<send_state_event::v3::Response> {
let sender_user = body.sender_user();
services
.users
.update_device_last_seen(sender_user, body.sender_device.as_deref(), ip)
.await;
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
@@ -61,9 +67,10 @@ pub(crate) async fn send_state_event_for_key_route(
/// Sends a state event into the room.
pub(crate) async fn send_state_event_for_empty_key_route(
State(services): State<crate::State>,
InsecureClientIp(ip): InsecureClientIp,
body: Ruma<send_state_event::v3::Request>,
) -> Result<RumaResponse<send_state_event::v3::Response>> {
send_state_event_for_key_route(State(services), body)
send_state_event_for_key_route(State(services), InsecureClientIp(ip), body)
.boxed()
.await
.map(RumaResponse)
@@ -151,7 +158,7 @@ pub(crate) async fn get_state_events_for_key_route(
"content": event.content(),
"event_id": event.event_id(),
"origin_server_ts": event.origin_server_ts(),
"room_id": event.room_id(),
"room_id": event.room_id_or_hash(),
"sender": event.sender(),
"state_key": event.state_key(),
"type": event.kind(),
@@ -185,7 +192,7 @@ async fn send_state_event_for_key_helper(
event_type: &StateEventType,
json: &Raw<AnyStateEventContent>,
state_key: &str,
timestamp: Option<ruma::MilliSecondsSinceUnixEpoch>,
timestamp: Option<MilliSecondsSinceUnixEpoch>,
) -> Result<OwnedEventId> {
allowed_to_send_state_event(services, room_id, event_type, state_key, json).await?;
let state_lock = services.rooms.state.mutex.lock(room_id).await;

View File

@@ -1,65 +1,145 @@
mod v3;
mod v4;
mod v5;
use std::collections::VecDeque;
use conduwuit::{
Error, PduCount, Result,
Event, PduCount, Result, debug_warn, err,
matrix::pdu::PduEvent,
ref_at, trace,
utils::stream::{BroadbandExt, ReadyExt, TryIgnore},
};
use conduwuit_service::Services;
use futures::{StreamExt, pin_mut};
use futures::StreamExt;
use ruma::{
RoomId, UserId,
OwnedUserId, RoomId, UserId,
events::TimelineEventType::{
self, Beacon, CallInvite, PollStart, RoomEncrypted, RoomMessage, Sticker,
},
};
pub(crate) use self::{
v3::sync_events_route, v4::sync_events_v4_route, v5::sync_events_v5_route,
};
pub(crate) use self::{v3::sync_events_route, v5::sync_events_v5_route};
pub(crate) const DEFAULT_BUMP_TYPES: &[TimelineEventType; 6] =
&[CallInvite, PollStart, Beacon, RoomEncrypted, RoomMessage, Sticker];
#[derive(Default)]
pub(crate) struct TimelinePdus {
pub pdus: VecDeque<(PduCount, PduEvent)>,
pub limited: bool,
}
impl TimelinePdus {
fn senders(&self) -> impl Iterator<Item = OwnedUserId> {
self.pdus
.iter()
.map(ref_at!(1))
.map(Event::sender)
.map(Into::into)
}
}
/// Load up to `limit` PDUs in the range (starting_count, ending_count].
async fn load_timeline(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
roomsincecount: PduCount,
next_batch: Option<PduCount>,
starting_count: Option<PduCount>,
ending_count: Option<PduCount>,
limit: usize,
) -> Result<(Vec<(PduCount, PduEvent)>, bool), Error> {
let last_timeline_count = services
.rooms
.timeline
.last_timeline_count(Some(sender_user), room_id)
.await?;
) -> Result<TimelinePdus> {
let mut pdu_stream = match starting_count {
| Some(starting_count) => {
let last_timeline_count = services
.rooms
.timeline
.last_timeline_count(room_id)
.await
.map_err(|err| {
err!(Database(warn!("Failed to fetch end of room timeline: {}", err)))
})?;
if last_timeline_count <= roomsincecount {
return Ok((Vec::new(), false));
}
if last_timeline_count <= starting_count {
// no messages have been sent in this room since `starting_count`
return Ok(TimelinePdus::default());
}
let non_timeline_pdus = services
.rooms
.timeline
.pdus_rev(Some(sender_user), room_id, None)
.ignore_err()
.ready_skip_while(|&(pducount, _)| pducount > next_batch.unwrap_or_else(PduCount::max))
.ready_take_while(|&(pducount, _)| pducount > roomsincecount);
// for incremental sync, stream from the DB all PDUs which were sent after
// `starting_count` but before `ending_count`, including `ending_count` but
// not `starting_count`. this code is pretty similar to the initial sync
// branch, they're separate to allow for future optimization
services
.rooms
.timeline
.pdus_rev(room_id, ending_count.map(|count| count.saturating_add(1)))
.ignore_err()
.ready_take_while(move |&(pducount, _)| pducount > starting_count)
.map(move |mut pdu| {
pdu.1.set_unsigned(Some(sender_user));
pdu
})
.then(async move |mut pdu| {
if let Err(e) = services
.rooms
.pdu_metadata
.add_bundled_aggregations_to_pdu(sender_user, &mut pdu.1)
.await
{
debug_warn!("Failed to add bundled aggregations: {e}");
}
pdu
})
.boxed()
},
| None => {
// For initial sync, stream from the DB all PDUs before and including
// `ending_count` in reverse order
services
.rooms
.timeline
.pdus_rev(room_id, ending_count.map(|count| count.saturating_add(1)))
.ignore_err()
.map(move |mut pdu| {
pdu.1.set_unsigned(Some(sender_user));
pdu
})
.then(async move |mut pdu| {
if let Err(e) = services
.rooms
.pdu_metadata
.add_bundled_aggregations_to_pdu(sender_user, &mut pdu.1)
.await
{
debug_warn!("Failed to add bundled aggregations: {e}");
}
pdu
})
.boxed()
},
};
// Take the last events for the timeline
pin_mut!(non_timeline_pdus);
let timeline_pdus: Vec<_> = non_timeline_pdus.by_ref().take(limit).collect().await;
// Return at most `limit` PDUs from the stream
let pdus = pdu_stream
.by_ref()
.take(limit)
.ready_fold(VecDeque::with_capacity(limit), |mut pdus, item| {
pdus.push_front(item);
pdus
})
.await;
let timeline_pdus: Vec<_> = timeline_pdus.into_iter().rev().collect();
// The timeline is limited if there are still more PDUs in the stream
let limited = pdu_stream.next().await.is_some();
// They /sync response doesn't always return all messages, so we say the output
// is limited unless there are events in non_timeline_pdus
let limited = non_timeline_pdus.next().await.is_some();
trace!(
"syncing {:?} timeline pdus from {:?} to {:?} (limited = {:?})",
pdus.len(),
starting_count,
ending_count,
limited,
);
Ok((timeline_pdus, limited))
Ok(TimelinePdus { pdus, limited })
}
async fn share_encrypted_room(

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,852 @@
use std::collections::{BTreeMap, HashSet};
use conduwuit::{
Result, at, debug_warn, err, extract_variant,
matrix::{
Event,
pdu::{PduCount, PduEvent},
},
trace,
utils::{
BoolExt, IterStream, ReadyExt, TryFutureExtExt,
math::ruma_from_u64,
stream::{TryIgnore, WidebandExt},
},
warn,
};
use conduwuit_service::Services;
use futures::{
FutureExt, StreamExt, TryFutureExt,
future::{OptionFuture, join, join3, join4, try_join, try_join3},
};
use ruma::{
OwnedRoomId, OwnedUserId, RoomId, UserId,
api::client::sync::sync_events::{
UnreadNotificationsCount,
v3::{Ephemeral, JoinedRoom, RoomAccountData, RoomSummary, State as RoomState, Timeline},
},
events::{
AnyRawAccountDataEvent, StateEventType,
TimelineEventType::*,
room::member::{MembershipState, RoomMemberEventContent},
},
serde::Raw,
uint,
};
use service::rooms::short::ShortStateHash;
use super::{load_timeline, share_encrypted_room};
use crate::client::{
TimelinePdus, ignored_filter,
sync::v3::{
DEFAULT_TIMELINE_LIMIT, DeviceListUpdates, SyncContext, prepare_lazily_loaded_members,
state::{build_state_incremental, build_state_initial},
},
};
/// Generate the sync response for a room the user is joined to.
#[tracing::instrument(
name = "joined",
level = "debug",
skip_all,
fields(
room_id = ?room_id,
syncing_user = ?sync_context.syncing_user,
),
)]
pub(super) async fn load_joined_room(
services: &Services,
sync_context: SyncContext<'_>,
ref room_id: OwnedRoomId,
) -> Result<(JoinedRoom, DeviceListUpdates)> {
/*
Building a sync response involves many steps which all depend on each other.
To parallelize the process as much as possible, each step is divided into its own function,
and `join*` functions are used to perform steps in parallel which do not depend on each other.
*/
let (
account_data,
ephemeral,
StateAndTimeline {
state_events,
timeline,
summary,
notification_counts,
device_list_updates,
},
) = try_join3(
build_account_data(services, sync_context, room_id),
build_ephemeral(services, sync_context, room_id),
build_state_and_timeline(services, sync_context, room_id),
)
.boxed()
.await?;
if !timeline.is_empty() || !state_events.is_empty() {
trace!(
"syncing {} timeline events (limited = {}) and {} state events",
timeline.events.len(),
timeline.limited,
state_events.len()
);
}
let joined_room = JoinedRoom {
account_data,
summary: summary.unwrap_or_default(),
unread_notifications: notification_counts.unwrap_or_default(),
timeline,
state: RoomState {
events: state_events.into_iter().map(Event::into_format).collect(),
},
ephemeral,
unread_thread_notifications: BTreeMap::new(),
};
Ok((joined_room, device_list_updates))
}
/// Collect changes to the syncing user's account data events.
#[tracing::instrument(level = "debug", skip_all)]
async fn build_account_data(
services: &Services,
SyncContext {
syncing_user,
last_sync_end_count,
current_count,
..
}: SyncContext<'_>,
room_id: &RoomId,
) -> Result<RoomAccountData> {
let account_data_changes = services
.account_data
.changes_since(Some(room_id), syncing_user, last_sync_end_count, Some(current_count))
.ready_filter_map(|e| extract_variant!(e, AnyRawAccountDataEvent::Room))
.collect()
.await;
Ok(RoomAccountData { events: account_data_changes })
}
/// Collect new ephemeral events.
#[tracing::instrument(level = "debug", skip_all)]
async fn build_ephemeral(
services: &Services,
SyncContext { syncing_user, last_sync_end_count, .. }: SyncContext<'_>,
room_id: &RoomId,
) -> Result<Ephemeral> {
// note: some of the futures below are boxed. this is because, without the box,
// rustc produces over thirty inscrutable errors in `mod.rs` at the call-site
// of `load_joined_room`. I don't know why boxing them fixes this -- it seems
// to be related to the async closures and borrowing from the sync context.
// collect updates to read receipts
let receipt_events = services
.rooms
.read_receipt
.readreceipts_since(room_id, last_sync_end_count)
.filter_map(async |(read_user, _, edu)| {
let is_ignored = services
.users
.user_is_ignored(&read_user, syncing_user)
.await;
// filter out read receipts for ignored users
is_ignored.or_some(edu)
})
.collect::<Vec<_>>()
.boxed();
// collect the updated list of typing users, if it's changed
let typing_event = async {
let should_send_typing_event = match last_sync_end_count {
| Some(last_sync_end_count) => {
match services.rooms.typing.last_typing_update(room_id).await {
| Ok(last_typing_update) => {
// update the typing list if the users typing have changed since the last
// sync
last_typing_update > last_sync_end_count
},
| Err(err) => {
warn!("Error checking last typing update: {}", err);
return None;
},
}
},
// always update the typing list on an initial sync
| None => true,
};
if should_send_typing_event {
let event = services
.rooms
.typing
.typings_event_for_user(room_id, syncing_user)
.await;
if let Ok(event) = event {
return Some(
Raw::new(&event)
.expect("typing event should be valid")
.cast(),
);
}
}
None
};
// collect the syncing user's private-read marker, if it's changed
let private_read_event = async {
let should_send_private_read = match last_sync_end_count {
| Some(last_sync_end_count) => {
let last_privateread_update = services
.rooms
.read_receipt
.last_privateread_update(syncing_user, room_id)
.await;
// update the marker if it's changed since the last sync
last_privateread_update > last_sync_end_count
},
// always update the marker on an initial sync
| None => true,
};
if should_send_private_read {
services
.rooms
.read_receipt
.private_read_get(room_id, syncing_user)
.await
.ok()
} else {
None
}
};
let (receipt_events, typing_event, private_read_event) =
join3(receipt_events, typing_event, private_read_event).await;
let mut edus = receipt_events;
edus.extend(typing_event);
edus.extend(private_read_event);
Ok(Ephemeral { events: edus })
}
/// A struct to hold the state events, timeline, and other data which is
/// computed from them.
struct StateAndTimeline {
state_events: Vec<PduEvent>,
timeline: Timeline,
summary: Option<RoomSummary>,
notification_counts: Option<UnreadNotificationsCount>,
device_list_updates: DeviceListUpdates,
}
/// Compute changes to the room's state and timeline.
#[tracing::instrument(level = "debug", skip_all)]
async fn build_state_and_timeline(
services: &Services,
sync_context: SyncContext<'_>,
room_id: &RoomId,
) -> Result<StateAndTimeline> {
let (shortstatehashes, timeline) = try_join(
fetch_shortstatehashes(services, sync_context, room_id),
build_timeline(services, sync_context, room_id),
)
.await?;
let (state_events, notification_counts, joined_since_last_sync) = try_join3(
build_state_events(services, sync_context, room_id, shortstatehashes, &timeline),
build_notification_counts(services, sync_context, room_id, &timeline),
check_joined_since_last_sync(services, shortstatehashes, sync_context),
)
.await?;
// the timeline should always include at least one PDU if the syncing user
// joined since the last sync, that being the syncing user's join event. if
// it's empty something is wrong.
if joined_since_last_sync && timeline.pdus.is_empty() {
warn!("timeline for newly joined room is empty");
}
let (summary, device_list_updates) = try_join(
build_room_summary(
services,
sync_context,
room_id,
shortstatehashes,
&timeline,
&state_events,
joined_since_last_sync,
),
build_device_list_updates(
services,
sync_context,
room_id,
shortstatehashes,
&state_events,
joined_since_last_sync,
),
)
.await?;
// the token which may be passed to the messages endpoint to backfill room
// history
let prev_batch = timeline.pdus.front().map(at!(0));
// note: we always indicate a limited timeline if the syncing user just joined
// the room, to indicate to the client that it should request backfill (and to
// copy Synapse's behavior). for federated room joins, the `timeline` will
// usually only include the syncing user's join event.
let limited = timeline.limited || joined_since_last_sync;
// filter out ignored events from the timeline and convert the PDUs into Ruma's
// AnySyncTimelineEvent type
let filtered_timeline = timeline
.pdus
.into_iter()
.stream()
.wide_filter_map(|item| ignored_filter(services, item, sync_context.syncing_user))
.map(at!(1))
.map(Event::into_format)
.collect::<Vec<_>>()
.await;
Ok(StateAndTimeline {
state_events,
timeline: Timeline {
limited,
prev_batch: prev_batch.as_ref().map(ToString::to_string),
events: filtered_timeline,
},
summary,
notification_counts,
device_list_updates,
})
}
/// Shortstatehashes necessary to compute what state events to sync.
#[derive(Clone, Copy)]
struct ShortStateHashes {
/// The current state of the syncing room.
current_shortstatehash: ShortStateHash,
/// The state of the syncing room at the end of the last sync.
last_sync_end_shortstatehash: Option<ShortStateHash>,
}
/// Fetch the current_shortstatehash and last_sync_end_shortstatehash.
#[tracing::instrument(level = "debug", skip_all)]
async fn fetch_shortstatehashes(
services: &Services,
SyncContext { last_sync_end_count, current_count, .. }: SyncContext<'_>,
room_id: &RoomId,
) -> Result<ShortStateHashes> {
// the room state currently.
// TODO: this should be the room state as of `current_count`, but there's no way
// to get that right now.
let current_shortstatehash = services
.rooms
.state
.get_room_shortstatehash(room_id)
.map_err(|_| err!(Database(error!("Room {room_id} has no state"))));
// the room state as of the end of the last sync.
// this will be None if we are doing an initial sync or if we just joined this
// room.
let last_sync_end_shortstatehash =
OptionFuture::from(last_sync_end_count.map(|last_sync_end_count| {
// look up the shortstatehash saved by the last sync's call to
// `associate_token_shortstatehash`
services
.rooms
.user
.get_token_shortstatehash(room_id, last_sync_end_count)
.inspect_err(move |_| {
debug_warn!(
token = last_sync_end_count,
"Room has no shortstatehash for this token"
);
})
.ok()
}))
.map(Option::flatten)
.map(Ok);
let (current_shortstatehash, last_sync_end_shortstatehash) =
try_join(current_shortstatehash, last_sync_end_shortstatehash).await?;
/*
associate the `current_count` with the `current_shortstatehash`, so we can
use it on the next sync as the `last_sync_end_shortstatehash`.
TODO: the table written to by this call grows extremely fast, gaining one new entry for each
joined room on _every single sync request_. we need to find a better way to remember the shortstatehash
between syncs.
*/
services
.rooms
.user
.associate_token_shortstatehash(room_id, current_count, current_shortstatehash)
.await;
Ok(ShortStateHashes {
current_shortstatehash,
last_sync_end_shortstatehash,
})
}
/// Fetch recent timeline events.
#[tracing::instrument(level = "debug", skip_all)]
async fn build_timeline(
services: &Services,
sync_context: SyncContext<'_>,
room_id: &RoomId,
) -> Result<TimelinePdus> {
let SyncContext {
syncing_user,
last_sync_end_count,
current_count,
filter,
..
} = sync_context;
/*
determine the maximum number of events to return in this sync.
if the sync filter specifies a limit, that will be used, otherwise
`DEFAULT_TIMELINE_LIMIT` will be used. `DEFAULT_TIMELINE_LIMIT` will also be
used if the limit is somehow greater than usize::MAX.
*/
let timeline_limit = filter
.room
.timeline
.limit
.and_then(|limit| limit.try_into().ok())
.unwrap_or(DEFAULT_TIMELINE_LIMIT);
load_timeline(
services,
syncing_user,
room_id,
last_sync_end_count.map(PduCount::Normal),
Some(PduCount::Normal(current_count)),
timeline_limit,
)
.await
}
/// Calculate the state events to sync.
async fn build_state_events(
services: &Services,
sync_context: SyncContext<'_>,
room_id: &RoomId,
shortstatehashes: ShortStateHashes,
timeline: &TimelinePdus,
) -> Result<Vec<PduEvent>> {
let SyncContext {
syncing_user,
last_sync_end_count,
full_state,
..
} = sync_context;
let ShortStateHashes {
current_shortstatehash,
last_sync_end_shortstatehash,
} = shortstatehashes;
// the spec states that the `state` property only includes state events up to
// the beginning of the timeline, so we determine the state of the syncing room
// as of the first timeline event. NOTE: this explanation is not entirely
// accurate; see the implementation of `build_state_incremental`.
let timeline_start_shortstatehash = async {
if let Some((_, pdu)) = timeline.pdus.front() {
if let Ok(shortstatehash) = services
.rooms
.state_accessor
.pdu_shortstatehash(&pdu.event_id)
.await
{
return shortstatehash;
}
}
current_shortstatehash
};
// the user IDs of members whose membership needs to be sent to the client, if
// lazy-loading is enabled.
let lazily_loaded_members =
prepare_lazily_loaded_members(services, sync_context, room_id, timeline.senders());
let (timeline_start_shortstatehash, lazily_loaded_members) =
join(timeline_start_shortstatehash, lazily_loaded_members).await;
// compute the state delta between the previous sync and this sync.
match (last_sync_end_count, last_sync_end_shortstatehash) {
/*
if `last_sync_end_count` is Some (meaning this is an incremental sync), and `last_sync_end_shortstatehash`
is Some (meaning the syncing user didn't just join this room for the first time ever), and `full_state` is false,
then use `build_state_incremental`.
*/
| (Some(last_sync_end_count), Some(last_sync_end_shortstatehash)) if !full_state =>
build_state_incremental(
services,
syncing_user,
room_id,
PduCount::Normal(last_sync_end_count),
last_sync_end_shortstatehash,
timeline_start_shortstatehash,
current_shortstatehash,
timeline,
lazily_loaded_members.as_ref(),
)
.boxed()
.await,
/*
otherwise use `build_state_initial`. note that this branch will be taken if the user joined this room since the last sync
for the first time ever, because in that case we have no `last_sync_end_shortstatehash` and can't correctly calculate
the state using the incremental sync algorithm.
*/
| _ =>
build_state_initial(
services,
syncing_user,
timeline_start_shortstatehash,
lazily_loaded_members.as_ref(),
)
.boxed()
.await,
}
}
/// Compute the number of unread notifications in this room.
#[tracing::instrument(level = "debug", skip_all)]
async fn build_notification_counts(
services: &Services,
SyncContext { syncing_user, last_sync_end_count, .. }: SyncContext<'_>,
room_id: &RoomId,
timeline: &TimelinePdus,
) -> Result<Option<UnreadNotificationsCount>> {
// determine whether to actually update the notification counts
let should_send_notification_counts = async {
// if we're going to sync some timeline events, the notification count has
// definitely changed to include them
if !timeline.pdus.is_empty() {
return true;
}
// if this is an initial sync, we need to send notification counts because the
// client doesn't know what they are yet
let Some(last_sync_end_count) = last_sync_end_count else {
return true;
};
let last_notification_read = services
.rooms
.user
.last_notification_read(syncing_user, room_id)
.await;
// if the syncing user has read the events we sent during the last sync, we need
// to send a new notification count on this sync.
if last_notification_read > last_sync_end_count {
return true;
}
// otherwise, nothing's changed.
false
};
if should_send_notification_counts.await {
let (notification_count, highlight_count) = join(
services
.rooms
.user
.notification_count(syncing_user, room_id)
.map(TryInto::try_into)
.unwrap_or(uint!(0)),
services
.rooms
.user
.highlight_count(syncing_user, room_id)
.map(TryInto::try_into)
.unwrap_or(uint!(0)),
)
.await;
trace!(?notification_count, ?highlight_count, "syncing new notification counts");
Ok(Some(UnreadNotificationsCount {
notification_count: Some(notification_count),
highlight_count: Some(highlight_count),
}))
} else {
Ok(None)
}
}
/// Check if the syncing user joined the room since their last incremental sync.
#[tracing::instrument(level = "debug", skip_all)]
async fn check_joined_since_last_sync(
services: &Services,
ShortStateHashes { last_sync_end_shortstatehash, .. }: ShortStateHashes,
SyncContext { syncing_user, .. }: SyncContext<'_>,
) -> Result<bool> {
// fetch the syncing user's membership event during the last sync.
// this will be None if `previous_sync_end_shortstatehash` is None.
let membership_during_previous_sync = match last_sync_end_shortstatehash {
| Some(last_sync_end_shortstatehash) => services
.rooms
.state_accessor
.state_get_content(
last_sync_end_shortstatehash,
&StateEventType::RoomMember,
syncing_user.as_str(),
)
.await
.inspect_err(|_| debug_warn!("User has no previous membership"))
.ok(),
| None => None,
};
// TODO: If the requesting user got state-reset out of the room, this
// will be `true` when it shouldn't be. this function should never be called
// in that situation, but it may be if the membership cache didn't get updated.
// the root cause of this needs to be addressed
let joined_since_last_sync =
membership_during_previous_sync.is_none_or(|content: RoomMemberEventContent| {
content.membership != MembershipState::Join
});
if joined_since_last_sync {
trace!("user joined since last sync");
}
Ok(joined_since_last_sync)
}
/// Build the `summary` field of the room object, which includes
/// the number of joined and invited users and the room's heroes.
#[tracing::instrument(level = "debug", skip_all)]
async fn build_room_summary(
services: &Services,
SyncContext { syncing_user, .. }: SyncContext<'_>,
room_id: &RoomId,
ShortStateHashes { current_shortstatehash, .. }: ShortStateHashes,
timeline: &TimelinePdus,
state_events: &[PduEvent],
joined_since_last_sync: bool,
) -> Result<Option<RoomSummary>> {
// determine whether any events in the state or timeline are membership events.
let are_syncing_membership_events = timeline
.pdus
.iter()
.map(|(_, pdu)| pdu)
.chain(state_events.iter())
.any(|event| event.kind == RoomMember);
/*
we only need to send an updated room summary if:
1. there are membership events in the state or timeline, because they might have changed the
membership counts or heroes, or
2. the syncing user just joined this room, which usually implies #1 because their join event should be in the timeline.
*/
if !(are_syncing_membership_events || joined_since_last_sync) {
return Ok(None);
}
let joined_member_count = services
.rooms
.state_cache
.room_joined_count(room_id)
.unwrap_or(0);
let invited_member_count = services
.rooms
.state_cache
.room_invited_count(room_id)
.unwrap_or(0);
let has_name = services
.rooms
.state_accessor
.state_contains_type(current_shortstatehash, &StateEventType::RoomName);
let has_canonical_alias = services
.rooms
.state_accessor
.state_contains_type(current_shortstatehash, &StateEventType::RoomCanonicalAlias);
let (joined_member_count, invited_member_count, has_name, has_canonical_alias) =
join4(joined_member_count, invited_member_count, has_name, has_canonical_alias).await;
// only send heroes if the room has neither a name nor a canonical alias
let heroes = if !(has_name || has_canonical_alias) {
Some(build_heroes(services, room_id, syncing_user, current_shortstatehash).await)
} else {
None
};
trace!(
?joined_member_count,
?invited_member_count,
heroes_length = heroes.as_ref().map(HashSet::len),
"syncing updated summary"
);
Ok(Some(RoomSummary {
heroes: heroes
.map(|heroes| heroes.into_iter().collect())
.unwrap_or_default(),
joined_member_count: Some(ruma_from_u64(joined_member_count)),
invited_member_count: Some(ruma_from_u64(invited_member_count)),
}))
}
/// Fetch the user IDs to include in the `m.heroes` property of the room
/// summary.
async fn build_heroes(
services: &Services,
room_id: &RoomId,
syncing_user: &UserId,
current_shortstatehash: ShortStateHash,
) -> HashSet<OwnedUserId> {
const MAX_HERO_COUNT: usize = 5;
// fetch joined members from the state cache first
let joined_members_stream = services
.rooms
.state_cache
.room_members(room_id)
.map(ToOwned::to_owned);
// then fetch invited members
let invited_members_stream = services
.rooms
.state_cache
.room_members_invited(room_id)
.map(ToOwned::to_owned);
// then as a last resort fetch every membership event
let all_members_stream = services
.rooms
.short
.multi_get_statekey_from_short(
services
.rooms
.state_accessor
.state_full_shortids(current_shortstatehash)
.ignore_err()
.ready_filter_map(|(key, _)| Some(key)),
)
.ignore_err()
.ready_filter_map(|(event_type, state_key)| {
if event_type == StateEventType::RoomMember {
state_key.to_string().try_into().ok()
} else {
None
}
});
joined_members_stream
.chain(invited_members_stream)
.chain(all_members_stream)
// the hero list should never include the syncing user
.ready_filter(|user_id| user_id != syncing_user)
.take(MAX_HERO_COUNT)
.collect()
.await
}
/// Collect updates to users' device lists for E2EE.
#[tracing::instrument(level = "debug", skip_all)]
async fn build_device_list_updates(
services: &Services,
SyncContext {
syncing_user,
last_sync_end_count,
current_count,
..
}: SyncContext<'_>,
room_id: &RoomId,
ShortStateHashes { current_shortstatehash, .. }: ShortStateHashes,
state_events: &Vec<PduEvent>,
joined_since_last_sync: bool,
) -> Result<DeviceListUpdates> {
let is_encrypted_room = services
.rooms
.state_accessor
.state_get(current_shortstatehash, &StateEventType::RoomEncryption, "")
.is_ok();
// initial syncs don't include device updates, and rooms which aren't encrypted
// don't affect them, so return early in either of those cases
if last_sync_end_count.is_none() || !(is_encrypted_room.await) {
return Ok(DeviceListUpdates::new());
}
let mut device_list_updates = DeviceListUpdates::new();
// add users with changed keys to the `changed` list
services
.users
.room_keys_changed(room_id, last_sync_end_count, Some(current_count))
.map(at!(0))
.map(ToOwned::to_owned)
.ready_for_each(|user_id| {
device_list_updates.changed.insert(user_id);
})
.await;
// add users who now share encrypted rooms to `changed` and
// users who no longer share encrypted rooms to `left`
for state_event in state_events {
if state_event.kind == RoomMember {
let Some(content): Option<RoomMemberEventContent> = state_event.get_content().ok()
else {
continue;
};
let Some(user_id): Option<OwnedUserId> = state_event
.state_key
.as_ref()
.and_then(|key| key.parse().ok())
else {
continue;
};
{
use MembershipState::*;
if matches!(content.membership, Leave | Join) {
let shares_encrypted_room =
share_encrypted_room(services, syncing_user, &user_id, Some(room_id))
.await;
match content.membership {
| Leave if !shares_encrypted_room => {
device_list_updates.left.insert(user_id);
},
| Join if joined_since_last_sync || shares_encrypted_room => {
device_list_updates.changed.insert(user_id);
},
| _ => (),
}
}
}
}
}
if !device_list_updates.is_empty() {
trace!(
changed = device_list_updates.changed.len(),
left = device_list_updates.left.len(),
"syncing device list updates"
);
}
Ok(device_list_updates)
}

View File

@@ -0,0 +1,349 @@
use conduwuit::{
Event, PduCount, PduEvent, Result, at, debug_warn,
pdu::EventHash,
trace,
utils::{self, IterStream, future::ReadyEqExt, stream::WidebandExt as _},
};
use futures::{StreamExt, future::join};
use ruma::{
EventId, OwnedRoomId, RoomId,
api::client::sync::sync_events::v3::{LeftRoom, RoomAccountData, State, Timeline},
events::{StateEventType, TimelineEventType},
uint,
};
use serde_json::value::RawValue;
use service::{Services, rooms::short::ShortStateHash};
use crate::client::{
TimelinePdus, ignored_filter,
sync::{
load_timeline,
v3::{
DEFAULT_TIMELINE_LIMIT, SyncContext, prepare_lazily_loaded_members,
state::build_state_initial,
},
},
};
#[tracing::instrument(
name = "left",
level = "debug",
skip_all,
fields(
room_id = %room_id,
),
)]
#[allow(clippy::too_many_arguments)]
pub(super) async fn load_left_room(
services: &Services,
sync_context: SyncContext<'_>,
ref room_id: OwnedRoomId,
leave_membership_event: Option<PduEvent>,
) -> Result<Option<LeftRoom>> {
let SyncContext {
syncing_user,
last_sync_end_count,
current_count,
filter,
..
} = sync_context;
// the global count as of the moment the user left the room
let Some(left_count) = services
.rooms
.state_cache
.get_left_count(room_id, syncing_user)
.await
.ok()
else {
// if we get here, the membership cache is incorrect, likely due to a state
// reset
debug_warn!("attempting to sync left room but no left count exists");
return Ok(None);
};
// return early if we haven't gotten to this leave yet.
// this can happen if the user leaves while a sync response is being generated
if current_count < left_count {
return Ok(None);
}
// return early if this is an incremental sync, and we've already synced this
// leave to the user, and `include_leave` isn't set on the filter.
if !filter.room.include_leave && last_sync_end_count >= Some(left_count) {
return Ok(None);
}
if let Some(ref leave_membership_event) = leave_membership_event {
debug_assert_eq!(
leave_membership_event.kind,
TimelineEventType::RoomMember,
"leave PDU should be m.room.member"
);
}
let does_not_exist = services.rooms.metadata.exists(room_id).eq(&false).await;
let (timeline, state_events) = match leave_membership_event {
| Some(leave_membership_event) if does_not_exist => {
/*
we have none PDUs with left beef for this room, likely because it was a rejected invite to a room
which nobody on this homeserver is in. `leave_pdu` is the remote-assisted outlier leave event for the room,
which is all we can send to the client.
if this is an initial sync, don't include this room at all to keep the client from asking for
state that we don't have.
*/
if last_sync_end_count.is_none() {
return Ok(None);
}
trace!("syncing remote-assisted leave PDU");
(TimelinePdus::default(), vec![leave_membership_event])
},
| Some(leave_membership_event) => {
// we have this room in our DB, and can fetch the state and timeline from when
// the user left.
let leave_state_key = syncing_user;
debug_assert_eq!(
Some(leave_state_key.as_str()),
leave_membership_event.state_key(),
"leave PDU should be for the user requesting the sync"
);
// the shortstatehash of the state _immediately before_ the syncing user left
// this room. the state represented here _does not_ include
// `leave_membership_event`.
let leave_shortstatehash = services
.rooms
.state_accessor
.pdu_shortstatehash(&leave_membership_event.event_id)
.await?;
let prev_membership_event = services
.rooms
.state_accessor
.state_get(
leave_shortstatehash,
&StateEventType::RoomMember,
leave_state_key.as_str(),
)
.await?;
build_left_state_and_timeline(
services,
sync_context,
room_id,
leave_membership_event,
leave_shortstatehash,
prev_membership_event,
)
.await?
},
| None => {
/*
no leave event was actually sent in this room, but we still need to pretend
like the user left it. this is usually because the room was banned by a server admin.
if this is an incremental sync, generate a fake leave event to make the room vanish from clients.
otherwise we don't tell the client about this room at all.
*/
if last_sync_end_count.is_none() {
return Ok(None);
}
trace!("syncing dummy leave event");
(TimelinePdus::default(), vec![create_dummy_leave_event(
services,
sync_context,
room_id,
)])
},
};
let raw_timeline_pdus = timeline
.pdus
.into_iter()
.stream()
// filter out ignored events from the timeline
.wide_filter_map(|item| ignored_filter(services, item, syncing_user))
.map(at!(1))
.map(Event::into_format)
.collect::<Vec<_>>()
.await;
Ok(Some(LeftRoom {
account_data: RoomAccountData { events: Vec::new() },
timeline: Timeline {
limited: timeline.limited,
prev_batch: Some(current_count.to_string()),
events: raw_timeline_pdus,
},
state: State {
events: state_events.into_iter().map(Event::into_format).collect(),
},
}))
}
async fn build_left_state_and_timeline(
services: &Services,
sync_context: SyncContext<'_>,
room_id: &RoomId,
leave_membership_event: PduEvent,
leave_shortstatehash: ShortStateHash,
prev_membership_event: PduEvent,
) -> Result<(TimelinePdus, Vec<PduEvent>)> {
let SyncContext {
syncing_user,
last_sync_end_count,
filter,
..
} = sync_context;
let timeline_start_count = if let Some(last_sync_end_count) = last_sync_end_count {
// for incremental syncs, start the timeline after `since`
PduCount::Normal(last_sync_end_count)
} else {
// for initial syncs, start the timeline after the previous membership
// event. we don't want to include the membership event itself
// because clients get confused when they see a `join`
// membership event in a `leave` room.
services
.rooms
.timeline
.get_pdu_count(&prev_membership_event.event_id)
.await?
};
// end the timeline at the user's leave event
let timeline_end_count = services
.rooms
.timeline
.get_pdu_count(leave_membership_event.event_id())
.await?;
// limit the timeline using the same logic as for joined rooms
let timeline_limit = filter
.room
.timeline
.limit
.and_then(|limit| limit.try_into().ok())
.unwrap_or(DEFAULT_TIMELINE_LIMIT);
let timeline = load_timeline(
services,
syncing_user,
room_id,
Some(timeline_start_count),
Some(timeline_end_count),
timeline_limit,
)
.await?;
let timeline_start_shortstatehash = async {
if let Some((_, pdu)) = timeline.pdus.front() {
if let Ok(shortstatehash) = services
.rooms
.state_accessor
.pdu_shortstatehash(&pdu.event_id)
.await
{
return shortstatehash;
}
}
// the timeline generally should not be empty (see the TODO further down),
// but in case it is we use `leave_shortstatehash` as the state to
// send
leave_shortstatehash
};
let lazily_loaded_members =
prepare_lazily_loaded_members(services, sync_context, room_id, timeline.senders());
let (timeline_start_shortstatehash, lazily_loaded_members) =
join(timeline_start_shortstatehash, lazily_loaded_members).await;
// TODO: calculate incremental state for incremental syncs.
// always calculating initial state _works_ but returns more data and does
// more processing than strictly necessary.
let mut state = build_state_initial(
services,
syncing_user,
timeline_start_shortstatehash,
lazily_loaded_members.as_ref(),
)
.await?;
/*
remove membership events for the syncing user from state.
usually, `state` should include a `join` membership event and `timeline` should include a `leave` one.
however, the matrix-js-sdk gets confused when this happens (see [1]) and doesn't process the room leave,
so we have to filter out the membership from `state`.
NOTE: we are sending more information than synapse does in this scenario, because we always
calculate `state` for initial syncs, even when the sync being performed is incremental.
however, the specification does not forbid sending extraneous events in `state`.
TODO: there is an additional bug at play here. sometimes `load_joined_room` syncs the `leave` event
before `load_left_room` does, which means the `timeline` we sync immediately after a leave is empty.
this shouldn't happen -- `timeline` should always include the `leave` event. this is probably
a race condition with the membership state cache.
[1]: https://github.com/matrix-org/matrix-js-sdk/issues/5071
*/
// `state` should only ever include one membership event for the syncing user
let membership_event_index = state.iter().position(|pdu| {
*pdu.event_type() == TimelineEventType::RoomMember
&& pdu.state_key() == Some(syncing_user.as_str())
});
if let Some(index) = membership_event_index {
// the ordering of events in `state` does not matter
state.swap_remove(index);
}
trace!(
?timeline_start_count,
?timeline_end_count,
"syncing {} timeline events (limited = {}) and {} state events",
timeline.pdus.len(),
timeline.limited,
state.len()
);
Ok((timeline, state))
}
fn create_dummy_leave_event(
services: &Services,
SyncContext { syncing_user, .. }: SyncContext<'_>,
room_id: &RoomId,
) -> PduEvent {
// TODO: because this event ID is random, it could cause caching issues with
// clients. perhaps a database table could be created to hold these dummy
// events, or they could be stored as outliers?
PduEvent {
event_id: EventId::new(services.globals.server_name()),
sender: syncing_user.to_owned(),
origin: None,
origin_server_ts: utils::millis_since_unix_epoch()
.try_into()
.expect("Timestamp is valid js_int value"),
kind: TimelineEventType::RoomMember,
content: RawValue::from_string(r#"{"membership": "leave"}"#.to_owned()).unwrap(),
state_key: Some(syncing_user.as_str().into()),
unsigned: None,
// The following keys are dropped on conversion
room_id: Some(room_id.to_owned()),
prev_events: vec![],
depth: uint!(1),
auth_events: vec![],
redacts: None,
hashes: EventHash { sha256: String::new() },
signatures: None,
}
}

Some files were not shown because too many files have changed in this diff Show More