mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a561fcd3a | |||
| 25c305f473 | |||
| c900350164 | |||
| c565e6ffbc | |||
| 442f887c98 | |||
| 03220845e5 | |||
| f8c1e9bcde | |||
| 21324b748f | |||
| b7bf36443b | |||
| d72192aa32 | |||
| 38ecc41780 | |||
| 7ae958bb03 | |||
| f676fa53f1 | |||
| 978bdc6466 | |||
| 7c741e62cf | |||
| 12aecf8091 | |||
| 19372f0b15 | |||
| a66b90cb3d | |||
| 7234ce6cbe | |||
| beb0c2ad9a | |||
| 39aaf95d09 | |||
| 5e0edd5a1c | |||
| d180f5a759 | |||
| f163264a82 | |||
| 5e7bc590d2 | |||
| 08df35946b | |||
| c4ebf289fa | |||
| 1fc6010f9a | |||
| 1d91331275 | |||
| 77e62ad772 | |||
| 696a1e6a4d | |||
| f41bbd7361 | |||
| 7350266c80 | |||
| 322c0900c6 | |||
| 1237e60aaf |
@@ -64,6 +64,7 @@ runs:
|
|||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
flavor: |
|
flavor: |
|
||||||
|
latest=auto
|
||||||
suffix=${{ inputs.tag_suffix }},onlatest=true
|
suffix=${{ inputs.tag_suffix }},onlatest=true
|
||||||
tags: |
|
tags: |
|
||||||
type=semver,pattern={{version}},prefix=v
|
type=semver,pattern={{version}},prefix=v
|
||||||
@@ -72,7 +73,6 @@ runs:
|
|||||||
type=ref,event=branch,prefix=${{ format('refs/heads/{0}', github.event.repository.default_branch) != github.ref && 'branch-' || '' }},
|
type=ref,event=branch,prefix=${{ format('refs/heads/{0}', github.event.repository.default_branch) != github.ref && 'branch-' || '' }},
|
||||||
type=ref,event=pr
|
type=ref,event=pr
|
||||||
type=sha,format=short
|
type=sha,format=short
|
||||||
type=raw,value=latest${{ inputs.tag_suffix }},enable=${{ startsWith(github.ref, 'refs/tags/v') }},priority=1100
|
|
||||||
images: ${{ inputs.images }}
|
images: ${{ inputs.images }}
|
||||||
# default labels & annotations: https://github.com/docker/metadata-action/blob/master/src/meta.ts#L509
|
# default labels & annotations: https://github.com/docker/metadata-action/blob/master/src/meta.ts#L509
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
name: 'New pull request'
|
||||||
|
about: 'Open a new pull request to contribute to continuwuity'
|
||||||
|
ref: 'main'
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
In order to help reviewers know what your pull request does at a glance, you should ensure that
|
||||||
|
|
||||||
|
1. Your PR title is a short, single sentence describing what you changed
|
||||||
|
2. You have described in more detail what you have changed, why you have changed it, what the
|
||||||
|
intended effect is, and why you think this will be beneficial to the project.
|
||||||
|
|
||||||
|
If you have made any potentially strange/questionable design choices, but didn't feel they'd benefit
|
||||||
|
from code comments, please don't mention them here - after opening your pull request,
|
||||||
|
go to "files changed", and click on the "+" symbol in the line number gutter,
|
||||||
|
and attach comments to the lines that you think would benefit from some clarification.
|
||||||
|
-->
|
||||||
|
|
||||||
|
This pull request...
|
||||||
|
|
||||||
|
<!-- Example:
|
||||||
|
This pull request allows us to warp through time and space ten times faster than before by
|
||||||
|
double-inverting the warp drive with hyperheated jump fluid, both making the drive faster and more
|
||||||
|
efficient. This resolves the common issue where we have to wait more than 10 milliseconds to
|
||||||
|
engage, use, and disengage the warp drive when travelling between galaxies.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Closes: #... -->
|
||||||
|
<!-- Fixes: #... -->
|
||||||
|
<!-- Uncomment the above line(s) if your pull request fixes an issue or closes another pull request
|
||||||
|
by superseding it. Replace `#...` with the issue/pr number, such as `#123`. -->
|
||||||
|
|
||||||
|
**Pull request checklist:**
|
||||||
|
|
||||||
|
<!-- You need to complete these before your PR can be considered.
|
||||||
|
If you aren't sure about some, feel free to ask for clarification in #dev:continuwuity.org. -->
|
||||||
|
- [ ] This pull request targets the `main` branch, and the branch is named something other than
|
||||||
|
`main`.
|
||||||
|
- [ ] I have written an appropriate pull request title and my description is clear.
|
||||||
|
- [ ] I understand I am responsible for the contents of this pull request.
|
||||||
|
- I have followed the [contributing guidelines][c1]:
|
||||||
|
- [ ] My contribution follows the [code style][c2], if applicable.
|
||||||
|
- [ ] I ran [pre-commit checks][c1pc] before opening/drafting this pull request.
|
||||||
|
- [ ] I have [tested my contribution][c1t] (or proof-read it for documentation-only changes)
|
||||||
|
myself, if applicable. This includes ensuring code compiles.
|
||||||
|
- [ ] My commit messages follow the [commit message format][c1cm] and are descriptive.
|
||||||
|
- [ ] I have written a [news fragment][n1] for this PR, if applicable<!--(can be done after hitting open!)-->.
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Notes on these requirements:
|
||||||
|
|
||||||
|
- While not required, we encourage you to sign your commits with GPG or SSH to attest the
|
||||||
|
authenticity of your changes.
|
||||||
|
- While we allow LLM-assisted contributions, we do not appreciate contributions that are
|
||||||
|
low quality, which is typical of machine-generated contributions that have not had a lot of love
|
||||||
|
and care from a human. Please do not open a PR if all you have done is asked ChatGPT to tidy up
|
||||||
|
the codebase with a +-100,000 diff.
|
||||||
|
- In the case of code style violations, reviewers may leave review comments/change requests
|
||||||
|
indicating what the ideal change would look like. For example, a reviewer may suggest you lower
|
||||||
|
a log level, or use `match` instead of `if/else` etc.
|
||||||
|
- In the case of code style violations, pre-commit check failures, minor things like typos/spelling
|
||||||
|
errors, and in some cases commit format violations, reviewers may modify your branch directly,
|
||||||
|
typically by making changes and adding a commit. Particularly in the latter case, a reviewer may
|
||||||
|
rebase your commits to squash "spammy" ones (like "fix", "fix", "actually fix"), and reword
|
||||||
|
commit messages that don't satisfy the format.
|
||||||
|
- Pull requests MUST pass the `Checks` CI workflows to be capable of being merged. This can only be
|
||||||
|
bypassed in exceptional circumstances.
|
||||||
|
If your CI flakes, let us know in matrix:r/dev:continuwuity.org.
|
||||||
|
- Pull requests have to be based on the latest `main` commit before being merged. If the main branch
|
||||||
|
changes while you're making your changes, you should make sure you rebase on main before
|
||||||
|
opening a PR. Your branch will be rebased on main before it is merged if it has fallen behind.
|
||||||
|
- We typically only do fast-forward merges, so your entire commit log will be included. Once in
|
||||||
|
main, it's difficult to get out cleanly, so put on your best dress, smile for the cameras!
|
||||||
|
-->
|
||||||
|
|
||||||
|
[c1]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md
|
||||||
|
[c2]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/docs/development/code_style.mdx
|
||||||
|
[c1pc]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md#pre-commit-checks
|
||||||
|
[c1t]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md#running-tests-locally
|
||||||
|
[c1cm]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md#commit-messages
|
||||||
|
[n1]: https://towncrier.readthedocs.io/en/stable/tutorial.html#creating-news-fragments
|
||||||
@@ -59,10 +59,9 @@ jobs:
|
|||||||
# Aggressive GC since cache restores don't increment counter
|
# Aggressive GC since cache restores don't increment counter
|
||||||
echo "CARGO_INCREMENTAL_GC_TRIGGER=5" >> $GITHUB_ENV
|
echo "CARGO_INCREMENTAL_GC_TRIGGER=5" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Setup Rust nightly
|
- name: Setup Rust
|
||||||
uses: ./.forgejo/actions/setup-rust
|
uses: ./.forgejo/actions/setup-rust
|
||||||
with:
|
with:
|
||||||
rust-version: nightly
|
|
||||||
github-token: ${{ secrets.GH_PUBLIC_RO }}
|
github-token: ${{ secrets.GH_PUBLIC_RO }}
|
||||||
|
|
||||||
- name: Get package version and component
|
- name: Get package version and component
|
||||||
|
|||||||
@@ -18,12 +18,6 @@ on:
|
|||||||
- "v*.*.*"
|
- "v*.*.*"
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
|
||||||
commit:
|
|
||||||
description: 'Commit SHA to build (default: latest commit on the branch)'
|
|
||||||
required: false
|
|
||||||
default: ""
|
|
||||||
type: string
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
BUILTIN_REGISTRY: forgejo.ellis.link
|
BUILTIN_REGISTRY: forgejo.ellis.link
|
||||||
@@ -48,14 +42,10 @@ jobs:
|
|||||||
slug: "linux-arm64"
|
slug: "linux-arm64"
|
||||||
|
|
||||||
steps:
|
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
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
ref: ${{ env.COMMIT_REF }}
|
|
||||||
- name: Prepare Docker build environment
|
- name: Prepare Docker build environment
|
||||||
id: prepare
|
id: prepare
|
||||||
uses: ./.forgejo/actions/prepare-docker-build
|
uses: ./.forgejo/actions/prepare-docker-build
|
||||||
|
|||||||
@@ -24,3 +24,4 @@ extend-ignore-re = [
|
|||||||
"continuwuity" = "continuwuity"
|
"continuwuity" = "continuwuity"
|
||||||
"continuwity" = "continuwuity"
|
"continuwity" = "continuwuity"
|
||||||
"execuse" = "execuse"
|
"execuse" = "execuse"
|
||||||
|
"oltp" = "OTLP"
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Continuwuity 0.5.0 (2025-12-30)
|
||||||
|
|
||||||
|
**This release contains a CRITICAL vulnerability patch, and you must update as soon as possible**
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Enabled the OTLP exporter in default builds, and allow configuring the exporter protocol. (@Jade). (#1251)
|
||||||
|
|
||||||
|
## Bug Fixes
|
||||||
|
|
||||||
|
- Don't allow admin room upgrades, as this can break the admin room (@timedout) (#1245)
|
||||||
|
- Fix invalid creators in power levels during upgrade to v12 (@timedout) (#1245)
|
||||||
Generated
+14
-12
@@ -940,7 +940,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "conduwuit"
|
name = "conduwuit"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"conduwuit_admin",
|
"conduwuit_admin",
|
||||||
@@ -972,7 +972,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "conduwuit_admin"
|
name = "conduwuit_admin"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"conduwuit_api",
|
"conduwuit_api",
|
||||||
@@ -994,7 +994,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "conduwuit_api"
|
name = "conduwuit_api"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum 0.7.9",
|
"axum 0.7.9",
|
||||||
@@ -1027,14 +1027,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "conduwuit_build_metadata"
|
name = "conduwuit_build_metadata"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"built",
|
"built",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "conduwuit_core"
|
name = "conduwuit_core"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
@@ -1095,7 +1095,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "conduwuit_database"
|
name = "conduwuit_database"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-channel",
|
"async-channel",
|
||||||
"conduwuit_core",
|
"conduwuit_core",
|
||||||
@@ -1114,7 +1114,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "conduwuit_macros"
|
name = "conduwuit_macros"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -1124,7 +1124,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "conduwuit_router"
|
name = "conduwuit_router"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum 0.7.9",
|
"axum 0.7.9",
|
||||||
"axum-client-ip",
|
"axum-client-ip",
|
||||||
@@ -1159,7 +1159,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "conduwuit_service"
|
name = "conduwuit_service"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
@@ -1200,7 +1200,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "conduwuit_web"
|
name = "conduwuit_web"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"askama",
|
"askama",
|
||||||
"axum 0.7.9",
|
"axum 0.7.9",
|
||||||
@@ -3305,6 +3305,8 @@ dependencies = [
|
|||||||
"prost",
|
"prost",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
|
"tokio",
|
||||||
|
"tonic",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -6204,7 +6206,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xtask"
|
name = "xtask"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -6213,7 +6215,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xtask-generate-commands"
|
name = "xtask-generate-commands"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap-markdown",
|
"clap-markdown",
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
|
|||||||
+2
-2
@@ -21,7 +21,7 @@ license = "Apache-2.0"
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
|
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
|
||||||
rust-version = "1.86.0"
|
rust-version = "1.86.0"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
|
|
||||||
[workspace.metadata.crane]
|
[workspace.metadata.crane]
|
||||||
name = "conduwuit"
|
name = "conduwuit"
|
||||||
@@ -426,7 +426,7 @@ features = ["rt-tokio"]
|
|||||||
|
|
||||||
[workspace.dependencies.opentelemetry-otlp]
|
[workspace.dependencies.opentelemetry-otlp]
|
||||||
version = "0.31.0"
|
version = "0.31.0"
|
||||||
features = ["http", "trace", "logs", "metrics"]
|
features = ["http", "grpc-tonic", "trace", "logs", "metrics"]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
The `console` feature is now enabled by default, allowing the server console to be used for running admin commands directly.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Certain potentially dangerous admin commands are now restricted to only be usable in the admin room and server console.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Implemented a configuration defined admin list independent of the admin room. (@Terryiscool160).
|
||||||
+22
-4
@@ -26,8 +26,8 @@
|
|||||||
# Also see the `[global.well_known]` config section at the very bottom.
|
# Also see the `[global.well_known]` config section at the very bottom.
|
||||||
#
|
#
|
||||||
# Examples of delegation:
|
# Examples of delegation:
|
||||||
# - https://puppygock.gay/.well-known/matrix/server
|
# - https://continuwuity.org/.well-known/matrix/server
|
||||||
# - https://puppygock.gay/.well-known/matrix/client
|
# - https://continuwuity.org/.well-known/matrix/client
|
||||||
#
|
#
|
||||||
# YOU NEED TO EDIT THIS. THIS CANNOT BE CHANGED AFTER WITHOUT A DATABASE
|
# YOU NEED TO EDIT THIS. THIS CANNOT BE CHANGED AFTER WITHOUT A DATABASE
|
||||||
# WIPE.
|
# WIPE.
|
||||||
@@ -608,6 +608,11 @@
|
|||||||
#
|
#
|
||||||
#otlp_filter = "info"
|
#otlp_filter = "info"
|
||||||
|
|
||||||
|
# Protocol to use for OTLP tracing export. Options are "http" or "grpc".
|
||||||
|
# The HTTP protocol uses port 4318 by default, while gRPC uses port 4317.
|
||||||
|
#
|
||||||
|
#otlp_protocol = "http"
|
||||||
|
|
||||||
# If the 'perf_measurements' compile-time feature is enabled, enables
|
# If the 'perf_measurements' compile-time feature is enabled, enables
|
||||||
# collecting folded stack trace profile of tracing spans using
|
# collecting folded stack trace profile of tracing spans using
|
||||||
# tracing_flame. The resulting profile can be visualized with inferno[1],
|
# tracing_flame. The resulting profile can be visualized with inferno[1],
|
||||||
@@ -1533,7 +1538,7 @@
|
|||||||
# a normal continuwuity admin command. The reply will be publicly visible
|
# a normal continuwuity admin command. The reply will be publicly visible
|
||||||
# to the room, originating from the sender.
|
# to the room, originating from the sender.
|
||||||
#
|
#
|
||||||
# example: \\!admin debug ping puppygock.gay
|
# example: \\!admin debug ping continuwuity.org
|
||||||
#
|
#
|
||||||
#admin_escape_commands = true
|
#admin_escape_commands = true
|
||||||
|
|
||||||
@@ -1551,7 +1556,8 @@
|
|||||||
# For example: `./continuwuity --execute "server admin-notice continuwuity
|
# For example: `./continuwuity --execute "server admin-notice continuwuity
|
||||||
# has started up at $(date)"`
|
# has started up at $(date)"`
|
||||||
#
|
#
|
||||||
# example: admin_execute = ["debug ping puppygock.gay", "debug echo hi"]`
|
# example: admin_execute = ["debug ping continuwuity.org", "debug echo
|
||||||
|
# hi"]`
|
||||||
#
|
#
|
||||||
#admin_execute = []
|
#admin_execute = []
|
||||||
|
|
||||||
@@ -1584,6 +1590,18 @@
|
|||||||
#
|
#
|
||||||
#admin_room_tag = "m.server_notice"
|
#admin_room_tag = "m.server_notice"
|
||||||
|
|
||||||
|
# A list of Matrix IDs that are qualified as server admins.
|
||||||
|
#
|
||||||
|
# Any Matrix IDs within this list are regarded as an admin
|
||||||
|
# regardless of whether they are in the admin room or not
|
||||||
|
#
|
||||||
|
#admins_list = []
|
||||||
|
|
||||||
|
# Defines whether those within the admin room are added to the
|
||||||
|
# admins_list.
|
||||||
|
#
|
||||||
|
#admins_from_room = true
|
||||||
|
|
||||||
# Sentry.io crash/panic reporting, performance monitoring/metrics, etc.
|
# Sentry.io crash/panic reporting, performance monitoring/metrics, etc.
|
||||||
# This is NOT enabled by default.
|
# This is NOT enabled by default.
|
||||||
#
|
#
|
||||||
|
|||||||
+1
-1
@@ -48,7 +48,7 @@ EOF
|
|||||||
|
|
||||||
# Developer tool versions
|
# Developer tool versions
|
||||||
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
|
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
|
||||||
ENV BINSTALL_VERSION=1.16.2
|
ENV BINSTALL_VERSION=1.16.6
|
||||||
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
||||||
ENV CARGO_SBOM_VERSION=0.9.1
|
ENV CARGO_SBOM_VERSION=0.9.1
|
||||||
# renovate: datasource=crate depName=lddtree
|
# renovate: datasource=crate depName=lddtree
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ RUN --mount=type=cache,target=/etc/apk/cache apk add \
|
|||||||
|
|
||||||
# Developer tool versions
|
# Developer tool versions
|
||||||
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
|
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
|
||||||
ENV BINSTALL_VERSION=1.16.2
|
ENV BINSTALL_VERSION=1.16.6
|
||||||
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
||||||
ENV CARGO_SBOM_VERSION=0.9.1
|
ENV CARGO_SBOM_VERSION=0.9.1
|
||||||
# renovate: datasource=crate depName=lddtree
|
# renovate: datasource=crate depName=lddtree
|
||||||
|
|||||||
@@ -114,6 +114,10 @@ services:
|
|||||||
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_HTTPCHALLENGE_ENTRYPOINT: web
|
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_HTTPCHALLENGE_ENTRYPOINT: web
|
||||||
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_STORAGE: "/etc/traefik/acme/acme.json"
|
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_STORAGE: "/etc/traefik/acme/acme.json"
|
||||||
|
|
||||||
|
# Since Traefik 3.6.3, paths with certain "encoded characters" are now blocked by default; we need a couple, or else things *will* break
|
||||||
|
TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDSLASH: true
|
||||||
|
TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDHASH: true
|
||||||
|
|
||||||
TRAEFIK_PROVIDERS_DOCKER: true
|
TRAEFIK_PROVIDERS_DOCKER: true
|
||||||
TRAEFIK_PROVIDERS_DOCKER_ENDPOINT: "unix:///var/run/docker.sock"
|
TRAEFIK_PROVIDERS_DOCKER_ENDPOINT: "unix:///var/run/docker.sock"
|
||||||
TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT: false
|
TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT: false
|
||||||
|
|||||||
@@ -149,11 +149,12 @@ of it, especially when the CI completed successfully and everything so it
|
|||||||
*looks* done.
|
*looks* done.
|
||||||
|
|
||||||
Before submitting a pull request, please ensure:
|
Before submitting a pull request, please ensure:
|
||||||
1. Your code passes all CI checks (formatting, linting, typo detection, etc.)
|
1. Your code passes all CI checks (formatting, linting, typo detection, etc.). Run pre-commit for this.
|
||||||
2. Your code follows the [code style guide](./code_style)
|
2. Your code follows the [code style guide](./code_style)
|
||||||
3. Your commit messages follow the conventional commits format
|
3. Your commit messages follow the conventional commits format
|
||||||
4. Tests are added for new functionality
|
4. Tests are added for new functionality
|
||||||
5. Documentation is updated if needed
|
5. Documentation is updated if needed
|
||||||
|
6. You have written a [news fragment](#writing-news-fragments) for your changes
|
||||||
|
|
||||||
Direct all PRs/MRs to the `main` branch.
|
Direct all PRs/MRs to the `main` branch.
|
||||||
|
|
||||||
@@ -171,3 +172,32 @@ continuwuity Matrix rooms for Code of Conduct violations.
|
|||||||
[sytest]: https://github.com/matrix-org/sytest/
|
[sytest]: https://github.com/matrix-org/sytest/
|
||||||
[mdbook]: https://rust-lang.github.io/mdBook/
|
[mdbook]: https://rust-lang.github.io/mdBook/
|
||||||
[documentation.yml]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/.forgejo/workflows/documentation.yml
|
[documentation.yml]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/.forgejo/workflows/documentation.yml
|
||||||
|
|
||||||
|
#### Writing news fragments
|
||||||
|
|
||||||
|
In order to make writing our changelogs easier, we make use of [Towncrier]. Towncrier builds changelogs based on
|
||||||
|
"news fragments", which are little markdown files in the `changelog.d/` directory that describe individual changes.
|
||||||
|
|
||||||
|
When you make a pull request that changes functionality, fixes a bug, or adds documentation, please add a news fragment
|
||||||
|
describing your change. The file name *MUST* be in the format of `{pull_request_number}.{type}`, where `{type}` is one
|
||||||
|
of the following:
|
||||||
|
|
||||||
|
- `feature` - for new features
|
||||||
|
- `bugfix` - for bug fixes
|
||||||
|
- `doc` - for documentation changes
|
||||||
|
- `misc` - for other changes that don't fit the above categories
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ echo "Fixed the quantum flux stabiliser. Contributed by @alice." > changelog.d/42.bugfix
|
||||||
|
```
|
||||||
|
|
||||||
|
(Note: If you want to credit yourself, you should reference your forgejo handle, however links to other platforms are also acceptable.)
|
||||||
|
|
||||||
|
When the next release is made, Towncrier will automatically include your news fragment in the changelog.
|
||||||
|
|
||||||
|
You can read more about writing news fragments in the [Towncrier tutorial][tt].
|
||||||
|
|
||||||
|
[Towncrier]: https://towncrier.readthedocs.io/
|
||||||
|
[tt]: https://towncrier.readthedocs.io/en/stable/tutorial.html#creating-news-fragments
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
"message": "Welcome to Continuwuity! Important announcements about the project will appear here."
|
"message": "Welcome to Continuwuity! Important announcements about the project will appear here."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 6,
|
"id": 7,
|
||||||
"mention_room": true,
|
"mention_room": true,
|
||||||
"date": "2025-12-22",
|
"date": "2025-12-30",
|
||||||
"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"
|
"message": "Continuwuity v0.5.1 has been released. **The release contains a fix for the critical vulnerability [GHSA-m5p2-vccg-8c9v](https://github.com/continuwuity/continuwuity/security/advisories/GHSA-m5p2-vccg-8c9v) (embargoed) affecting all Conduit-derived servers. Update as soon as possible.**\n\nThis has been *actively exploited* to attempt account takeover and forge events bricking the Continuwuity rooms. The new space is accessible at [Continuwuity (room list)](https://matrix.to/#/!8cR4g-i9ucof69E4JHNg9LbPVkGprHb3SzcrGBDDJgk?via=continuwuity.org&via=starstruck.systems&via=gingershaped.computer)\n"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
tag-message = "chore: Release v{{version}}"
|
||||||
+15
-3
@@ -53,14 +53,26 @@ pub(super) async fn process(command: AdminCommand, context: &Context<'_>) -> Res
|
|||||||
use AdminCommand::*;
|
use AdminCommand::*;
|
||||||
|
|
||||||
match command {
|
match command {
|
||||||
| Appservices(command) => appservice::process(command, context).await,
|
| Appservices(command) => {
|
||||||
|
// appservice commands are all restricted
|
||||||
|
context.bail_restricted()?;
|
||||||
|
appservice::process(command, context).await
|
||||||
|
},
|
||||||
| Media(command) => media::process(command, context).await,
|
| Media(command) => media::process(command, context).await,
|
||||||
| Users(command) => user::process(command, context).await,
|
| Users(command) => {
|
||||||
|
// user commands are all restricted
|
||||||
|
context.bail_restricted()?;
|
||||||
|
user::process(command, context).await
|
||||||
|
},
|
||||||
| Rooms(command) => room::process(command, context).await,
|
| Rooms(command) => room::process(command, context).await,
|
||||||
| Federation(command) => federation::process(command, context).await,
|
| Federation(command) => federation::process(command, context).await,
|
||||||
| Server(command) => server::process(command, context).await,
|
| Server(command) => server::process(command, context).await,
|
||||||
| Debug(command) => debug::process(command, context).await,
|
| Debug(command) => debug::process(command, context).await,
|
||||||
| Query(command) => query::process(command, context).await,
|
| Query(command) => {
|
||||||
|
// query commands are all restricted
|
||||||
|
context.bail_restricted()?;
|
||||||
|
query::process(command, context).await
|
||||||
|
},
|
||||||
| Check(command) => check::process(command, context).await,
|
| Check(command) => check::process(command, context).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-1
@@ -1,6 +1,6 @@
|
|||||||
use std::{fmt, time::SystemTime};
|
use std::{fmt, time::SystemTime};
|
||||||
|
|
||||||
use conduwuit::Result;
|
use conduwuit::{Err, Result};
|
||||||
use conduwuit_service::Services;
|
use conduwuit_service::Services;
|
||||||
use futures::{
|
use futures::{
|
||||||
Future, FutureExt, TryFutureExt,
|
Future, FutureExt, TryFutureExt,
|
||||||
@@ -8,6 +8,7 @@ use futures::{
|
|||||||
lock::Mutex,
|
lock::Mutex,
|
||||||
};
|
};
|
||||||
use ruma::{EventId, UserId};
|
use ruma::{EventId, UserId};
|
||||||
|
use service::admin::InvocationSource;
|
||||||
|
|
||||||
pub(crate) struct Context<'a> {
|
pub(crate) struct Context<'a> {
|
||||||
pub(crate) services: &'a Services,
|
pub(crate) services: &'a Services,
|
||||||
@@ -16,6 +17,7 @@ pub(crate) struct Context<'a> {
|
|||||||
pub(crate) reply_id: Option<&'a EventId>,
|
pub(crate) reply_id: Option<&'a EventId>,
|
||||||
pub(crate) sender: Option<&'a UserId>,
|
pub(crate) sender: Option<&'a UserId>,
|
||||||
pub(crate) output: Mutex<BufWriter<Vec<u8>>>,
|
pub(crate) output: Mutex<BufWriter<Vec<u8>>>,
|
||||||
|
pub(crate) source: InvocationSource,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context<'_> {
|
impl Context<'_> {
|
||||||
@@ -43,4 +45,22 @@ impl Context<'_> {
|
|||||||
self.sender
|
self.sender
|
||||||
.unwrap_or_else(|| self.services.globals.server_user.as_ref())
|
.unwrap_or_else(|| self.services.globals.server_user.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns an Err if the [`Self::source`] of this context does not allow
|
||||||
|
/// restricted commands to be executed.
|
||||||
|
///
|
||||||
|
/// This is intended to be placed at the start of restricted commands'
|
||||||
|
/// implementations, like so:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// self.bail_restricted()?;
|
||||||
|
/// // actual command impl
|
||||||
|
/// ```
|
||||||
|
pub(crate) fn bail_restricted(&self) -> Result {
|
||||||
|
if self.source.allows_restricted() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err!("This command can only be used in the admin room.")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -291,6 +291,8 @@ pub(super) async fn get_remote_pdu(
|
|||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn get_room_state(&self, room: OwnedRoomOrAliasId) -> Result {
|
pub(super) async fn get_room_state(&self, room: OwnedRoomOrAliasId) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
let room_id = self.services.rooms.alias.resolve(&room).await?;
|
let room_id = self.services.rooms.alias.resolve(&room).await?;
|
||||||
let room_state: Vec<Raw<AnyStateEvent>> = self
|
let room_state: Vec<Raw<AnyStateEvent>> = self
|
||||||
.services
|
.services
|
||||||
@@ -417,27 +419,6 @@ pub(super) async fn change_log_level(&self, filter: Option<String>, reset: bool)
|
|||||||
Err!("No log level was specified.")
|
Err!("No log level was specified.")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[admin_command]
|
|
||||||
pub(super) async fn sign_json(&self) -> Result {
|
|
||||||
if self.body.len() < 2
|
|
||||||
|| !self.body[0].trim().starts_with("```")
|
|
||||||
|| self.body.last().unwrap_or(&"").trim() != "```"
|
|
||||||
{
|
|
||||||
return Err!("Expected code block in command body. Add --help for details.");
|
|
||||||
}
|
|
||||||
|
|
||||||
let string = self.body[1..self.body.len().checked_sub(1).unwrap()].join("\n");
|
|
||||||
match serde_json::from_str(&string) {
|
|
||||||
| Err(e) => return Err!("Invalid json: {e}"),
|
|
||||||
| Ok(mut value) => {
|
|
||||||
self.services.server_keys.sign_json(&mut value)?;
|
|
||||||
let json_text = serde_json::to_string_pretty(&value)?;
|
|
||||||
write!(self, "{json_text}")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn verify_json(&self) -> Result {
|
pub(super) async fn verify_json(&self) -> Result {
|
||||||
if self.body.len() < 2
|
if self.body.len() < 2
|
||||||
@@ -477,6 +458,8 @@ pub(super) async fn verify_pdu(&self, event_id: OwnedEventId) -> Result {
|
|||||||
#[admin_command]
|
#[admin_command]
|
||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
pub(super) async fn first_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
|
pub(super) async fn first_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
if !self
|
if !self
|
||||||
.services
|
.services
|
||||||
.rooms
|
.rooms
|
||||||
@@ -502,6 +485,8 @@ pub(super) async fn first_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
|
|||||||
#[admin_command]
|
#[admin_command]
|
||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
pub(super) async fn latest_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
|
pub(super) async fn latest_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
if !self
|
if !self
|
||||||
.services
|
.services
|
||||||
.rooms
|
.rooms
|
||||||
@@ -532,6 +517,8 @@ pub(super) async fn force_set_room_state_from_server(
|
|||||||
server_name: OwnedServerName,
|
server_name: OwnedServerName,
|
||||||
at_event: Option<OwnedEventId>,
|
at_event: Option<OwnedEventId>,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
if !self
|
if !self
|
||||||
.services
|
.services
|
||||||
.rooms
|
.rooms
|
||||||
|
|||||||
@@ -47,9 +47,9 @@ pub enum DebugCommand {
|
|||||||
shorteventid: ShortEventId,
|
shorteventid: ShortEventId,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// - Attempts to retrieve a PDU from a remote server. Inserts it into our
|
/// - Attempts to retrieve a PDU from a remote server. **Does not** insert
|
||||||
/// database/timeline if found and we do not have this PDU already
|
/// it into the database
|
||||||
/// (following normal event auth rules, handles it as an incoming PDU).
|
/// or persist it anywhere.
|
||||||
GetRemotePdu {
|
GetRemotePdu {
|
||||||
/// An event ID (a $ followed by the base64 reference hash)
|
/// An event ID (a $ followed by the base64 reference hash)
|
||||||
event_id: OwnedEventId,
|
event_id: OwnedEventId,
|
||||||
@@ -125,12 +125,6 @@ pub enum DebugCommand {
|
|||||||
reset: bool,
|
reset: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// - Sign JSON blob
|
|
||||||
///
|
|
||||||
/// This command needs a JSON blob provided in a Markdown code block below
|
|
||||||
/// the command.
|
|
||||||
SignJson,
|
|
||||||
|
|
||||||
/// - Verify JSON signatures
|
/// - Verify JSON signatures
|
||||||
///
|
///
|
||||||
/// This command needs a JSON blob provided in a Markdown code block below
|
/// This command needs a JSON blob provided in a Markdown code block below
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ use crate::{admin_command, get_room_info};
|
|||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn disable_room(&self, room_id: OwnedRoomId) -> Result {
|
pub(super) async fn disable_room(&self, room_id: OwnedRoomId) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
self.services.rooms.metadata.disable_room(&room_id, true);
|
self.services.rooms.metadata.disable_room(&room_id, true);
|
||||||
self.write_str("Room disabled.").await
|
self.write_str("Room disabled.").await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn enable_room(&self, room_id: OwnedRoomId) -> Result {
|
pub(super) async fn enable_room(&self, room_id: OwnedRoomId) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
self.services.rooms.metadata.disable_room(&room_id, false);
|
self.services.rooms.metadata.disable_room(&room_id, false);
|
||||||
self.write_str("Room enabled.").await
|
self.write_str("Room enabled.").await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ pub(super) async fn delete(
|
|||||||
mxc: Option<OwnedMxcUri>,
|
mxc: Option<OwnedMxcUri>,
|
||||||
event_id: Option<OwnedEventId>,
|
event_id: Option<OwnedEventId>,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
if event_id.is_some() && mxc.is_some() {
|
if event_id.is_some() && mxc.is_some() {
|
||||||
return Err!("Please specify either an MXC or an event ID, not both.",);
|
return Err!("Please specify either an MXC or an event ID, not both.",);
|
||||||
}
|
}
|
||||||
@@ -176,6 +178,8 @@ pub(super) async fn delete(
|
|||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn delete_list(&self) -> Result {
|
pub(super) async fn delete_list(&self) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
if self.body.len() < 2
|
if self.body.len() < 2
|
||||||
|| !self.body[0].trim().starts_with("```")
|
|| !self.body[0].trim().starts_with("```")
|
||||||
|| self.body.last().unwrap_or(&"").trim() != "```"
|
|| self.body.last().unwrap_or(&"").trim() != "```"
|
||||||
@@ -231,6 +235,8 @@ pub(super) async fn delete_past_remote_media(
|
|||||||
after: bool,
|
after: bool,
|
||||||
yes_i_want_to_delete_local_media: bool,
|
yes_i_want_to_delete_local_media: bool,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
if before && after {
|
if before && after {
|
||||||
return Err!("Please only pick one argument, --before or --after.",);
|
return Err!("Please only pick one argument, --before or --after.",);
|
||||||
}
|
}
|
||||||
@@ -273,6 +279,8 @@ pub(super) async fn delete_all_from_server(
|
|||||||
server_name: OwnedServerName,
|
server_name: OwnedServerName,
|
||||||
yes_i_want_to_delete_local_media: bool,
|
yes_i_want_to_delete_local_media: bool,
|
||||||
) -> Result {
|
) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
if server_name == self.services.globals.server_name() && !yes_i_want_to_delete_local_media {
|
if server_name == self.services.globals.server_name() && !yes_i_want_to_delete_local_media {
|
||||||
return Err!("This command only works for remote media by default.",);
|
return Err!("This command only works for remote media by default.",);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ async fn process_command(services: Arc<Services>, input: &CommandInput) -> Proce
|
|||||||
reply_id: input.reply_id.as_deref(),
|
reply_id: input.reply_id.as_deref(),
|
||||||
sender: input.sender.as_deref(),
|
sender: input.sender.as_deref(),
|
||||||
output: BufWriter::new(Vec::new()).into(),
|
output: BufWriter::new(Vec::new()).into(),
|
||||||
|
source: input.source,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (result, mut logs) = process(&context, command, &args).await;
|
let (result, mut logs) = process(&context, command, &args).await;
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ pub(super) async fn uptime(&self) -> Result {
|
|||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn show_config(&self) -> Result {
|
pub(super) async fn show_config(&self) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
self.write_str(&format!("{}", *self.services.server.config))
|
self.write_str(&format!("{}", *self.services.server.config))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -118,6 +120,8 @@ pub(super) async fn list_backups(&self) -> Result {
|
|||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn backup_database(&self) -> Result {
|
pub(super) async fn backup_database(&self) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
let db = Arc::clone(&self.services.db);
|
let db = Arc::clone(&self.services.db);
|
||||||
let result = self
|
let result = self
|
||||||
.services
|
.services
|
||||||
@@ -144,6 +148,8 @@ pub(super) async fn admin_notice(&self, message: Vec<String>) -> Result {
|
|||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn reload_mods(&self) -> Result {
|
pub(super) async fn reload_mods(&self) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
self.services.server.reload()?;
|
self.services.server.reload()?;
|
||||||
|
|
||||||
self.write_str("Reloading server...").await
|
self.write_str("Reloading server...").await
|
||||||
@@ -168,6 +174,8 @@ pub(super) async fn restart(&self, force: bool) -> Result {
|
|||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn shutdown(&self) -> Result {
|
pub(super) async fn shutdown(&self) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
warn!("shutdown command");
|
warn!("shutdown command");
|
||||||
self.services.server.shutdown()?;
|
self.services.server.shutdown()?;
|
||||||
|
|
||||||
|
|||||||
@@ -461,8 +461,10 @@ pub(super) async fn force_join_list_of_local_users(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let Ok(admin_room) = self.services.admin.get_admin_room().await else {
|
let server_admins = self.services.admin.get_admins().await;
|
||||||
return Err!("There is not an admin room to check for server admins.",);
|
|
||||||
|
if server_admins.is_empty() {
|
||||||
|
return Err!("There are no admins set for this server.");
|
||||||
};
|
};
|
||||||
|
|
||||||
let (room_id, servers) = self
|
let (room_id, servers) = self
|
||||||
@@ -482,15 +484,6 @@ pub(super) async fn force_join_list_of_local_users(
|
|||||||
return Err!("We are not joined in this room.");
|
return Err!("We are not joined in this room.");
|
||||||
}
|
}
|
||||||
|
|
||||||
let server_admins: Vec<_> = self
|
|
||||||
.services
|
|
||||||
.rooms
|
|
||||||
.state_cache
|
|
||||||
.active_local_users_in_room(&admin_room)
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.collect()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if !self
|
if !self
|
||||||
.services
|
.services
|
||||||
.rooms
|
.rooms
|
||||||
@@ -583,8 +576,10 @@ pub(super) async fn force_join_all_local_users(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let Ok(admin_room) = self.services.admin.get_admin_room().await else {
|
let server_admins = self.services.admin.get_admins().await;
|
||||||
return Err!("There is not an admin room to check for server admins.",);
|
|
||||||
|
if server_admins.is_empty() {
|
||||||
|
return Err!("There are no admins set for this server.");
|
||||||
};
|
};
|
||||||
|
|
||||||
let (room_id, servers) = self
|
let (room_id, servers) = self
|
||||||
@@ -604,15 +599,6 @@ pub(super) async fn force_join_all_local_users(
|
|||||||
return Err!("We are not joined in this room.");
|
return Err!("We are not joined in this room.");
|
||||||
}
|
}
|
||||||
|
|
||||||
let server_admins: Vec<_> = self
|
|
||||||
.services
|
|
||||||
.rooms
|
|
||||||
.state_cache
|
|
||||||
.active_local_users_in_room(&admin_room)
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.collect()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if !self
|
if !self
|
||||||
.services
|
.services
|
||||||
.rooms
|
.rooms
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ use service::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::banned_room_check;
|
use super::{banned_room_check, validate_remote_member_event_stub};
|
||||||
use crate::Ruma;
|
use crate::Ruma;
|
||||||
|
|
||||||
/// # `POST /_matrix/client/r0/rooms/{roomId}/join`
|
/// # `POST /_matrix/client/r0/rooms/{roomId}/join`
|
||||||
@@ -837,6 +837,13 @@ async fn join_room_by_id_helper_local(
|
|||||||
err!(BadServerResponse("Invalid make_join event json received from server: {e:?}"))
|
err!(BadServerResponse("Invalid make_join event json received from server: {e:?}"))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
validate_remote_member_event_stub(
|
||||||
|
&MembershipState::Join,
|
||||||
|
sender_user,
|
||||||
|
room_id,
|
||||||
|
&join_event_stub,
|
||||||
|
)?;
|
||||||
|
|
||||||
let join_authorized_via_users_server = join_event_stub
|
let join_authorized_via_users_server = join_event_stub
|
||||||
.get("content")
|
.get("content")
|
||||||
.map(|s| {
|
.map(|s| {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ use service::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{banned_room_check, join::join_room_by_id_helper};
|
use super::{banned_room_check, join::join_room_by_id_helper, validate_remote_member_event_stub};
|
||||||
use crate::Ruma;
|
use crate::Ruma;
|
||||||
|
|
||||||
/// # `POST /_matrix/client/*/knock/{roomIdOrAlias}`
|
/// # `POST /_matrix/client/*/knock/{roomIdOrAlias}`
|
||||||
@@ -408,6 +408,13 @@ async fn knock_room_helper_local(
|
|||||||
err!(BadServerResponse("Invalid make_knock event json received from server: {e:?}"))
|
err!(BadServerResponse("Invalid make_knock event json received from server: {e:?}"))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
validate_remote_member_event_stub(
|
||||||
|
&MembershipState::Knock,
|
||||||
|
sender_user,
|
||||||
|
room_id,
|
||||||
|
&knock_event_stub,
|
||||||
|
)?;
|
||||||
|
|
||||||
knock_event_stub.insert(
|
knock_event_stub.insert(
|
||||||
"origin".to_owned(),
|
"origin".to_owned(),
|
||||||
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
|
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use ruma::{
|
|||||||
};
|
};
|
||||||
use service::Services;
|
use service::Services;
|
||||||
|
|
||||||
|
use super::validate_remote_member_event_stub;
|
||||||
use crate::Ruma;
|
use crate::Ruma;
|
||||||
|
|
||||||
/// # `POST /_matrix/client/v3/rooms/{roomId}/leave`
|
/// # `POST /_matrix/client/v3/rooms/{roomId}/leave`
|
||||||
@@ -324,6 +325,13 @@ pub async fn remote_leave_room<S: ::std::hash::BuildHasher>(
|
|||||||
)))
|
)))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
validate_remote_member_event_stub(
|
||||||
|
&MembershipState::Leave,
|
||||||
|
user_id,
|
||||||
|
room_id,
|
||||||
|
&leave_event_stub,
|
||||||
|
)?;
|
||||||
|
|
||||||
// TODO: Is origin needed?
|
// TODO: Is origin needed?
|
||||||
leave_event_stub.insert(
|
leave_event_stub.insert(
|
||||||
"origin".to_owned(),
|
"origin".to_owned(),
|
||||||
|
|||||||
@@ -13,7 +13,14 @@ use std::net::IpAddr;
|
|||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use conduwuit::{Err, Result, warn};
|
use conduwuit::{Err, Result, warn};
|
||||||
use futures::{FutureExt, StreamExt};
|
use futures::{FutureExt, StreamExt};
|
||||||
use ruma::{OwnedRoomId, RoomId, ServerName, UserId, api::client::membership::joined_rooms};
|
use ruma::{
|
||||||
|
CanonicalJsonObject, OwnedRoomId, RoomId, ServerName, UserId,
|
||||||
|
api::client::membership::joined_rooms,
|
||||||
|
events::{
|
||||||
|
StaticEventContent,
|
||||||
|
room::member::{MembershipState, RoomMemberEventContent},
|
||||||
|
},
|
||||||
|
};
|
||||||
use service::Services;
|
use service::Services;
|
||||||
|
|
||||||
pub(crate) use self::{
|
pub(crate) use self::{
|
||||||
@@ -153,3 +160,80 @@ pub(crate) async fn banned_room_check(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validates that an event returned from a remote server by `/make_*`
|
||||||
|
/// actually is a membership event with the expected fields.
|
||||||
|
///
|
||||||
|
/// Without checking this, the remote server could use the remote membership
|
||||||
|
/// mechanism to trick our server into signing arbitrary malicious events.
|
||||||
|
pub(crate) fn validate_remote_member_event_stub(
|
||||||
|
membership: &MembershipState,
|
||||||
|
user_id: &UserId,
|
||||||
|
room_id: &RoomId,
|
||||||
|
event_stub: &CanonicalJsonObject,
|
||||||
|
) -> Result<()> {
|
||||||
|
let Some(event_type) = event_stub.get("type") else {
|
||||||
|
return Err!(BadServerResponse(
|
||||||
|
"Remote server returned member event with missing type field"
|
||||||
|
));
|
||||||
|
};
|
||||||
|
if event_type != &RoomMemberEventContent::TYPE {
|
||||||
|
return Err!(BadServerResponse(
|
||||||
|
"Remote server returned member event with invalid event type"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(sender) = event_stub.get("sender") else {
|
||||||
|
return Err!(BadServerResponse(
|
||||||
|
"Remote server returned member event with missing sender field"
|
||||||
|
));
|
||||||
|
};
|
||||||
|
if sender != &user_id.as_str() {
|
||||||
|
return Err!(BadServerResponse(
|
||||||
|
"Remote server returned member event with incorrect sender"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(state_key) = event_stub.get("state_key") else {
|
||||||
|
return Err!(BadServerResponse(
|
||||||
|
"Remote server returned member event with missing state_key field"
|
||||||
|
));
|
||||||
|
};
|
||||||
|
if state_key != &user_id.as_str() {
|
||||||
|
return Err!(BadServerResponse(
|
||||||
|
"Remote server returned member event with incorrect state_key"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(event_room_id) = event_stub.get("room_id") else {
|
||||||
|
return Err!(BadServerResponse(
|
||||||
|
"Remote server returned member event with missing room_id field"
|
||||||
|
));
|
||||||
|
};
|
||||||
|
if event_room_id != &room_id.as_str() {
|
||||||
|
return Err!(BadServerResponse(
|
||||||
|
"Remote server returned member event with incorrect room_id"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(content) = event_stub
|
||||||
|
.get("content")
|
||||||
|
.and_then(|content| content.as_object())
|
||||||
|
else {
|
||||||
|
return Err!(BadServerResponse(
|
||||||
|
"Remote server returned member event with missing content field"
|
||||||
|
));
|
||||||
|
};
|
||||||
|
let Some(event_membership) = content.get("membership") else {
|
||||||
|
return Err!(BadServerResponse(
|
||||||
|
"Remote server returned member event with missing membership field"
|
||||||
|
));
|
||||||
|
};
|
||||||
|
if event_membership != &membership.as_str() {
|
||||||
|
return Err!(BadServerResponse(
|
||||||
|
"Remote server returned member event with incorrect room_id"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,6 +68,12 @@ pub(crate) async fn upgrade_room_route(
|
|||||||
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
|
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make sure this isn't the admin room
|
||||||
|
// Admin room upgrades are hacky and should be done manually instead.
|
||||||
|
if services.admin.is_admin_room(&body.room_id).await {
|
||||||
|
return Err!(Request(Forbidden("Upgrading the admin room this way is not allowed.")));
|
||||||
|
}
|
||||||
|
|
||||||
// First, check if the user has permission to upgrade the room (send tombstone
|
// First, check if the user has permission to upgrade the room (send tombstone
|
||||||
// event)
|
// event)
|
||||||
let old_room_state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
|
let old_room_state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
|
||||||
@@ -266,7 +272,7 @@ pub(crate) async fn upgrade_room_route(
|
|||||||
.room_state_keys(&body.room_id, event_type)
|
.room_state_keys(&body.room_id, event_type)
|
||||||
.await?;
|
.await?;
|
||||||
for state_key in state_keys {
|
for state_key in state_keys {
|
||||||
let event_content = match services
|
let mut event_content = match services
|
||||||
.rooms
|
.rooms
|
||||||
.state_accessor
|
.state_accessor
|
||||||
.room_state_get(&body.room_id, event_type, &state_key)
|
.room_state_get(&body.room_id, event_type, &state_key)
|
||||||
@@ -279,6 +285,21 @@ pub(crate) async fn upgrade_room_route(
|
|||||||
// If the event content is empty, we skip it
|
// If the event content is empty, we skip it
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// If this is a power levels event, and the new room version has creators,
|
||||||
|
// we need to make sure they dont appear in the users block of power levels.
|
||||||
|
if *event_type == StateEventType::RoomPowerLevels {
|
||||||
|
// TODO(v12): additional creators
|
||||||
|
let creators = vec![sender_user];
|
||||||
|
let mut power_levels_event_content: RoomPowerLevelsEventContent =
|
||||||
|
serde_json::from_str(event_content.get()).map_err(|_| {
|
||||||
|
err!(Request(BadJson("Power levels event content is not valid")))
|
||||||
|
})?;
|
||||||
|
for creator in creators {
|
||||||
|
power_levels_event_content.users.remove(creator);
|
||||||
|
}
|
||||||
|
event_content = to_raw_value(&power_levels_event_content)
|
||||||
|
.expect("event is valid, we just deserialized and modified it");
|
||||||
|
}
|
||||||
|
|
||||||
services
|
services
|
||||||
.rooms
|
.rooms
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use axum::{Json, extract::State, response::IntoResponse};
|
use axum::{Json, extract::State, response::IntoResponse};
|
||||||
use conduwuit::{Error, Result};
|
use conduwuit::{Error, Result};
|
||||||
use futures::StreamExt;
|
|
||||||
use ruma::api::client::{
|
use ruma::api::client::{
|
||||||
discovery::{
|
discovery::{
|
||||||
discover_homeserver::{self, HomeserverInfo, SlidingSyncProxyInfo},
|
discover_homeserver::{self, HomeserverInfo, SlidingSyncProxyInfo},
|
||||||
@@ -71,21 +70,18 @@ pub(crate) async fn well_known_support(
|
|||||||
|
|
||||||
// Try to add admin users as contacts if no contacts are configured
|
// Try to add admin users as contacts if no contacts are configured
|
||||||
if contacts.is_empty() {
|
if contacts.is_empty() {
|
||||||
if let Ok(admin_room) = services.admin.get_admin_room().await {
|
let admin_users = services.admin.get_admins().await;
|
||||||
let admin_users = services.rooms.state_cache.room_members(&admin_room);
|
|
||||||
let mut stream = admin_users;
|
|
||||||
|
|
||||||
while let Some(user_id) = stream.next().await {
|
for user_id in admin_users.iter() {
|
||||||
// Skip server user
|
if *user_id == services.globals.server_user {
|
||||||
if *user_id == services.globals.server_user {
|
continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
contacts.push(Contact {
|
|
||||||
role: role_value.clone(),
|
|
||||||
email_address: None,
|
|
||||||
matrix_id: Some(user_id.to_owned()),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
contacts.push(Contact {
|
||||||
|
role: role_value.clone(),
|
||||||
|
email_address: None,
|
||||||
|
matrix_id: Some(user_id.to_owned()),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+30
-4
@@ -69,8 +69,8 @@ pub struct Config {
|
|||||||
/// Also see the `[global.well_known]` config section at the very bottom.
|
/// Also see the `[global.well_known]` config section at the very bottom.
|
||||||
///
|
///
|
||||||
/// Examples of delegation:
|
/// Examples of delegation:
|
||||||
/// - https://puppygock.gay/.well-known/matrix/server
|
/// - https://continuwuity.org/.well-known/matrix/server
|
||||||
/// - https://puppygock.gay/.well-known/matrix/client
|
/// - https://continuwuity.org/.well-known/matrix/client
|
||||||
///
|
///
|
||||||
/// YOU NEED TO EDIT THIS. THIS CANNOT BE CHANGED AFTER WITHOUT A DATABASE
|
/// YOU NEED TO EDIT THIS. THIS CANNOT BE CHANGED AFTER WITHOUT A DATABASE
|
||||||
/// WIPE.
|
/// WIPE.
|
||||||
@@ -737,6 +737,13 @@ pub struct Config {
|
|||||||
#[serde(default = "default_otlp_filter", alias = "jaeger_filter")]
|
#[serde(default = "default_otlp_filter", alias = "jaeger_filter")]
|
||||||
pub otlp_filter: String,
|
pub otlp_filter: String,
|
||||||
|
|
||||||
|
/// Protocol to use for OTLP tracing export. Options are "http" or "grpc".
|
||||||
|
/// The HTTP protocol uses port 4318 by default, while gRPC uses port 4317.
|
||||||
|
///
|
||||||
|
/// default: "http"
|
||||||
|
#[serde(default = "default_otlp_protocol")]
|
||||||
|
pub otlp_protocol: String,
|
||||||
|
|
||||||
/// If the 'perf_measurements' compile-time feature is enabled, enables
|
/// If the 'perf_measurements' compile-time feature is enabled, enables
|
||||||
/// collecting folded stack trace profile of tracing spans using
|
/// collecting folded stack trace profile of tracing spans using
|
||||||
/// tracing_flame. The resulting profile can be visualized with inferno[1],
|
/// tracing_flame. The resulting profile can be visualized with inferno[1],
|
||||||
@@ -1752,7 +1759,7 @@ pub struct Config {
|
|||||||
/// a normal continuwuity admin command. The reply will be publicly visible
|
/// a normal continuwuity admin command. The reply will be publicly visible
|
||||||
/// to the room, originating from the sender.
|
/// to the room, originating from the sender.
|
||||||
///
|
///
|
||||||
/// example: \\!admin debug ping puppygock.gay
|
/// example: \\!admin debug ping continuwuity.org
|
||||||
#[serde(default = "true_fn")]
|
#[serde(default = "true_fn")]
|
||||||
pub admin_escape_commands: bool,
|
pub admin_escape_commands: bool,
|
||||||
|
|
||||||
@@ -1770,7 +1777,8 @@ pub struct Config {
|
|||||||
/// For example: `./continuwuity --execute "server admin-notice continuwuity
|
/// For example: `./continuwuity --execute "server admin-notice continuwuity
|
||||||
/// has started up at $(date)"`
|
/// has started up at $(date)"`
|
||||||
///
|
///
|
||||||
/// example: admin_execute = ["debug ping puppygock.gay", "debug echo hi"]`
|
/// example: admin_execute = ["debug ping continuwuity.org", "debug echo
|
||||||
|
/// hi"]`
|
||||||
///
|
///
|
||||||
/// default: []
|
/// default: []
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -1811,6 +1819,22 @@ pub struct Config {
|
|||||||
#[serde(default = "default_admin_room_tag")]
|
#[serde(default = "default_admin_room_tag")]
|
||||||
pub admin_room_tag: String,
|
pub admin_room_tag: String,
|
||||||
|
|
||||||
|
/// A list of Matrix IDs that are qualified as server admins.
|
||||||
|
///
|
||||||
|
/// Any Matrix IDs within this list are regarded as an admin
|
||||||
|
/// regardless of whether they are in the admin room or not
|
||||||
|
///
|
||||||
|
/// default: []
|
||||||
|
#[serde(default)]
|
||||||
|
pub admins_list: Vec<OwnedUserId>,
|
||||||
|
|
||||||
|
/// Defines whether those within the admin room are added to the
|
||||||
|
/// admins_list.
|
||||||
|
///
|
||||||
|
/// default: true
|
||||||
|
#[serde(default = "true_fn")]
|
||||||
|
pub admins_from_room: bool,
|
||||||
|
|
||||||
/// Sentry.io crash/panic reporting, performance monitoring/metrics, etc.
|
/// Sentry.io crash/panic reporting, performance monitoring/metrics, etc.
|
||||||
/// This is NOT enabled by default.
|
/// This is NOT enabled by default.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -2418,6 +2442,8 @@ fn default_otlp_filter() -> String {
|
|||||||
.to_owned()
|
.to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_otlp_protocol() -> String { "http".to_owned() }
|
||||||
|
|
||||||
fn default_tracing_flame_output_path() -> String { "./tracing.folded".to_owned() }
|
fn default_tracing_flame_output_path() -> String { "./tracing.folded".to_owned() }
|
||||||
|
|
||||||
fn default_trusted_servers() -> Vec<OwnedServerName> {
|
fn default_trusted_servers() -> Vec<OwnedServerName> {
|
||||||
|
|||||||
@@ -558,12 +558,19 @@ where
|
|||||||
// If type is m.room.power_levels
|
// If type is m.room.power_levels
|
||||||
if *incoming_event.event_type() == TimelineEventType::RoomPowerLevels {
|
if *incoming_event.event_type() == TimelineEventType::RoomPowerLevels {
|
||||||
debug!("starting m.room.power_levels check");
|
debug!("starting m.room.power_levels check");
|
||||||
|
let mut creators = BTreeSet::new();
|
||||||
|
if room_version.explicitly_privilege_room_creators {
|
||||||
|
creators.insert(create_event.sender().to_owned());
|
||||||
|
for creator in room_create_content.additional_creators.iter().flatten() {
|
||||||
|
creators.insert(creator.deserialize()?);
|
||||||
|
}
|
||||||
|
}
|
||||||
match check_power_levels(
|
match check_power_levels(
|
||||||
room_version,
|
room_version,
|
||||||
incoming_event,
|
incoming_event,
|
||||||
power_levels_event.as_ref(),
|
power_levels_event.as_ref(),
|
||||||
sender_power_level,
|
sender_power_level,
|
||||||
|
&creators,
|
||||||
) {
|
) {
|
||||||
| Some(required_pwr_lvl) =>
|
| Some(required_pwr_lvl) =>
|
||||||
if !required_pwr_lvl {
|
if !required_pwr_lvl {
|
||||||
@@ -1221,8 +1228,8 @@ fn check_power_levels(
|
|||||||
power_event: &impl Event,
|
power_event: &impl Event,
|
||||||
previous_power_event: Option<&impl Event>,
|
previous_power_event: Option<&impl Event>,
|
||||||
user_level: Int,
|
user_level: Int,
|
||||||
|
creators: &BTreeSet<OwnedUserId>,
|
||||||
) -> Option<bool> {
|
) -> Option<bool> {
|
||||||
// TODO(hydra): This function does not care about creators!
|
|
||||||
match power_event.state_key() {
|
match power_event.state_key() {
|
||||||
| Some("") => {},
|
| Some("") => {},
|
||||||
| Some(key) => {
|
| Some(key) => {
|
||||||
@@ -1287,6 +1294,10 @@ fn check_power_levels(
|
|||||||
for user in user_levels_to_check {
|
for user in user_levels_to_check {
|
||||||
let old_level = old_state.users.get(user);
|
let old_level = old_state.users.get(user);
|
||||||
let new_level = new_state.users.get(user);
|
let new_level = new_state.users.get(user);
|
||||||
|
if new_level.is_some() && creators.contains(user) {
|
||||||
|
warn!("creators cannot appear in the users list of m.room.power_levels");
|
||||||
|
return Some(false); // cannot alter creator power level
|
||||||
|
}
|
||||||
if old_level.is_some() && new_level.is_some() && old_level == new_level {
|
if old_level.is_some() && new_level.is_some() && old_level == new_level {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-5
@@ -63,15 +63,16 @@ standard = [
|
|||||||
"systemd",
|
"systemd",
|
||||||
"url_preview",
|
"url_preview",
|
||||||
"zstd_compression",
|
"zstd_compression",
|
||||||
"sentry_telemetry"
|
"sentry_telemetry",
|
||||||
|
"otlp_telemetry",
|
||||||
|
"console",
|
||||||
]
|
]
|
||||||
full = [
|
full = [
|
||||||
"standard",
|
"standard",
|
||||||
# "hardened_malloc", # Conflicts with jemalloc
|
# "hardened_malloc", # Conflicts with jemalloc
|
||||||
"jemalloc_prof",
|
"jemalloc_prof",
|
||||||
"perf_measurements",
|
"perf_measurements",
|
||||||
"tokio_console"
|
"tokio_console",
|
||||||
# sentry_telemetry
|
|
||||||
]
|
]
|
||||||
|
|
||||||
blurhashing = [
|
blurhashing = [
|
||||||
@@ -124,12 +125,15 @@ ldap = [
|
|||||||
media_thumbnail = [
|
media_thumbnail = [
|
||||||
"conduwuit-service/media_thumbnail",
|
"conduwuit-service/media_thumbnail",
|
||||||
]
|
]
|
||||||
perf_measurements = [
|
otlp_telemetry = [
|
||||||
"dep:opentelemetry",
|
"dep:opentelemetry",
|
||||||
"dep:tracing-flame",
|
|
||||||
"dep:tracing-opentelemetry",
|
"dep:tracing-opentelemetry",
|
||||||
"dep:opentelemetry_sdk",
|
"dep:opentelemetry_sdk",
|
||||||
"dep:opentelemetry-otlp",
|
"dep:opentelemetry-otlp",
|
||||||
|
]
|
||||||
|
perf_measurements = [
|
||||||
|
"dep:tracing-flame",
|
||||||
|
"otlp_telemetry",
|
||||||
"conduwuit-core/perf_measurements",
|
"conduwuit-core/perf_measurements",
|
||||||
"conduwuit-core/sentry_telemetry",
|
"conduwuit-core/sentry_telemetry",
|
||||||
]
|
]
|
||||||
|
|||||||
+55
-30
@@ -7,8 +7,10 @@ use conduwuit_core::{
|
|||||||
log::{ConsoleFormat, ConsoleWriter, LogLevelReloadHandles, capture, fmt_span},
|
log::{ConsoleFormat, ConsoleWriter, LogLevelReloadHandles, capture, fmt_span},
|
||||||
result::UnwrapOrErr,
|
result::UnwrapOrErr,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "perf_measurements")]
|
#[cfg(feature = "otlp_telemetry")]
|
||||||
use opentelemetry::trace::TracerProvider;
|
use opentelemetry::trace::TracerProvider;
|
||||||
|
#[cfg(feature = "otlp_telemetry")]
|
||||||
|
use opentelemetry_otlp::WithExportConfig;
|
||||||
use tracing_subscriber::{EnvFilter, Layer, Registry, fmt, layer::SubscriberExt, reload};
|
use tracing_subscriber::{EnvFilter, Layer, Registry, fmt, layer::SubscriberExt, reload};
|
||||||
|
|
||||||
#[cfg(feature = "perf_measurements")]
|
#[cfg(feature = "perf_measurements")]
|
||||||
@@ -70,6 +72,57 @@ pub(crate) fn init(
|
|||||||
subscriber.with(sentry_layer.with_filter(sentry_reload_filter))
|
subscriber.with(sentry_layer.with_filter(sentry_reload_filter))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "otlp_telemetry")]
|
||||||
|
let subscriber = {
|
||||||
|
let otlp_filter = EnvFilter::try_new(&config.otlp_filter)
|
||||||
|
.map_err(|e| err!(Config("otlp_filter", "{e}.")))?;
|
||||||
|
|
||||||
|
let otlp_layer = config.allow_otlp.then(|| {
|
||||||
|
opentelemetry::global::set_text_map_propagator(
|
||||||
|
opentelemetry_sdk::propagation::TraceContextPropagator::new(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let exporter = match config.otlp_protocol.as_str() {
|
||||||
|
| "grpc" => opentelemetry_otlp::SpanExporter::builder()
|
||||||
|
.with_tonic()
|
||||||
|
.with_protocol(opentelemetry_otlp::Protocol::Grpc)
|
||||||
|
.build()
|
||||||
|
.expect("Failed to create OTLP gRPC exporter"),
|
||||||
|
| "http" => opentelemetry_otlp::SpanExporter::builder()
|
||||||
|
.with_http()
|
||||||
|
.build()
|
||||||
|
.expect("Failed to create OTLP HTTP exporter"),
|
||||||
|
| protocol => {
|
||||||
|
debug_warn!(
|
||||||
|
"Invalid OTLP protocol '{}', falling back to HTTP. Valid options are \
|
||||||
|
'http' or 'grpc'.",
|
||||||
|
protocol
|
||||||
|
);
|
||||||
|
opentelemetry_otlp::SpanExporter::builder()
|
||||||
|
.with_http()
|
||||||
|
.build()
|
||||||
|
.expect("Failed to create OTLP HTTP exporter")
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
|
||||||
|
.with_batch_exporter(exporter)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let tracer = provider.tracer(conduwuit_core::name());
|
||||||
|
|
||||||
|
let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
|
||||||
|
|
||||||
|
let (otlp_reload_filter, otlp_reload_handle) =
|
||||||
|
reload::Layer::new(otlp_filter.clone());
|
||||||
|
reload_handles.add("otlp", Box::new(otlp_reload_handle));
|
||||||
|
|
||||||
|
Some(telemetry.with_filter(otlp_reload_filter))
|
||||||
|
});
|
||||||
|
|
||||||
|
subscriber.with(otlp_layer)
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(feature = "perf_measurements")]
|
#[cfg(feature = "perf_measurements")]
|
||||||
let (subscriber, flame_guard) = {
|
let (subscriber, flame_guard) = {
|
||||||
let (flame_layer, flame_guard) = if config.tracing_flame {
|
let (flame_layer, flame_guard) = if config.tracing_flame {
|
||||||
@@ -89,35 +142,7 @@ pub(crate) fn init(
|
|||||||
(None, None)
|
(None, None)
|
||||||
};
|
};
|
||||||
|
|
||||||
let otlp_filter = EnvFilter::try_new(&config.otlp_filter)
|
let subscriber = subscriber.with(flame_layer);
|
||||||
.map_err(|e| err!(Config("otlp_filter", "{e}.")))?;
|
|
||||||
|
|
||||||
let otlp_layer = config.allow_otlp.then(|| {
|
|
||||||
opentelemetry::global::set_text_map_propagator(
|
|
||||||
opentelemetry_sdk::propagation::TraceContextPropagator::new(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let exporter = opentelemetry_otlp::SpanExporter::builder()
|
|
||||||
.with_http()
|
|
||||||
.build()
|
|
||||||
.expect("Failed to create OTLP exporter");
|
|
||||||
|
|
||||||
let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
|
|
||||||
.with_batch_exporter(exporter)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let tracer = provider.tracer(conduwuit_core::name());
|
|
||||||
|
|
||||||
let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
|
|
||||||
|
|
||||||
let (otlp_reload_filter, otlp_reload_handle) =
|
|
||||||
reload::Layer::new(otlp_filter.clone());
|
|
||||||
reload_handles.add("otlp", Box::new(otlp_reload_handle));
|
|
||||||
|
|
||||||
Some(telemetry.with_filter(otlp_reload_filter))
|
|
||||||
});
|
|
||||||
|
|
||||||
let subscriber = subscriber.with(flame_layer).with(otlp_layer);
|
|
||||||
(subscriber, flame_guard)
|
(subscriber, flame_guard)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,10 @@ pub(crate) fn build(services: &Arc<Services>) -> Result<(Router, Guard)> {
|
|||||||
.layer(RequestBodyTimeoutLayer::new(Duration::from_secs(
|
.layer(RequestBodyTimeoutLayer::new(Duration::from_secs(
|
||||||
server.config.client_receive_timeout,
|
server.config.client_receive_timeout,
|
||||||
)))
|
)))
|
||||||
.layer(TimeoutLayer::with_status_code(StatusCode::REQUEST_TIMEOUT, Duration::from_secs(server.config.client_request_timeout)))
|
.layer(TimeoutLayer::with_status_code(
|
||||||
|
StatusCode::REQUEST_TIMEOUT,
|
||||||
|
Duration::from_secs(server.config.client_request_timeout),
|
||||||
|
))
|
||||||
.layer(SetResponseHeaderLayer::if_not_present(
|
.layer(SetResponseHeaderLayer::if_not_present(
|
||||||
HeaderName::from_static("origin-agent-cluster"), // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin-Agent-Cluster
|
HeaderName::from_static("origin-agent-cluster"), // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin-Agent-Cluster
|
||||||
HeaderValue::from_static("?1"),
|
HeaderValue::from_static("?1"),
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ use rustyline_async::{Readline, ReadlineError, ReadlineEvent};
|
|||||||
use termimad::MadSkin;
|
use termimad::MadSkin;
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
|
|
||||||
use crate::{Dep, admin};
|
use crate::{
|
||||||
|
Dep,
|
||||||
|
admin::{self, InvocationSource},
|
||||||
|
};
|
||||||
|
|
||||||
pub struct Console {
|
pub struct Console {
|
||||||
server: Arc<Server>,
|
server: Arc<Server>,
|
||||||
@@ -160,7 +163,11 @@ impl Console {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn process(self: Arc<Self>, line: String) {
|
async fn process(self: Arc<Self>, line: String) {
|
||||||
match self.admin.command_in_place(line, None).await {
|
match self
|
||||||
|
.admin
|
||||||
|
.command_in_place(line, None, InvocationSource::Console)
|
||||||
|
.await
|
||||||
|
{
|
||||||
| Ok(Some(ref content)) => self.output(content),
|
| Ok(Some(ref content)) => self.output(content),
|
||||||
| Err(ref content) => self.output_err(content),
|
| Err(ref content) => self.output_err(content),
|
||||||
| _ => unreachable!(),
|
| _ => unreachable!(),
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ use conduwuit::{Err, Result, debug, debug_info, error, implement, info};
|
|||||||
use ruma::events::room::message::RoomMessageEventContent;
|
use ruma::events::room::message::RoomMessageEventContent;
|
||||||
use tokio::time::{Duration, sleep};
|
use tokio::time::{Duration, sleep};
|
||||||
|
|
||||||
|
use crate::admin::InvocationSource;
|
||||||
|
|
||||||
pub(super) const SIGNAL: &str = "SIGUSR2";
|
pub(super) const SIGNAL: &str = "SIGUSR2";
|
||||||
|
|
||||||
/// Possibly spawn the terminal console at startup if configured.
|
/// Possibly spawn the terminal console at startup if configured.
|
||||||
@@ -88,7 +90,10 @@ pub(super) async fn signal_execute(&self) -> Result {
|
|||||||
async fn execute_command(&self, i: usize, command: String) -> Result {
|
async fn execute_command(&self, i: usize, command: String) -> Result {
|
||||||
debug!("Execute command #{i}: executing {command:?}");
|
debug!("Execute command #{i}: executing {command:?}");
|
||||||
|
|
||||||
match self.command_in_place(command, None).await {
|
match self
|
||||||
|
.command_in_place(command, None, InvocationSource::Console)
|
||||||
|
.await
|
||||||
|
{
|
||||||
| Ok(Some(output)) => Self::execute_command_output(i, &output),
|
| Ok(Some(output)) => Self::execute_command_output(i, &output),
|
||||||
| Err(output) => Self::execute_command_error(i, &output),
|
| Err(output) => Self::execute_command_error(i, &output),
|
||||||
| Ok(None) => {
|
| Ok(None) => {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use conduwuit::{Err, Result, debug_info, debug_warn, error, implement, matrix::pdu::PduBuilder};
|
use conduwuit::{
|
||||||
|
Err, Result, debug_info, debug_warn, error, implement, matrix::pdu::PduBuilder, warn,
|
||||||
|
};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
RoomId, UserId,
|
RoomId, UserId,
|
||||||
events::{
|
events::{
|
||||||
@@ -176,6 +178,19 @@ async fn set_room_tag(&self, room_id: &RoomId, user_id: &UserId, tag: &str) -> R
|
|||||||
pub async fn revoke_admin(&self, user_id: &UserId) -> Result {
|
pub async fn revoke_admin(&self, user_id: &UserId) -> Result {
|
||||||
use MembershipState::{Invite, Join, Knock, Leave};
|
use MembershipState::{Invite, Join, Knock, Leave};
|
||||||
|
|
||||||
|
if self
|
||||||
|
.services
|
||||||
|
.server
|
||||||
|
.config
|
||||||
|
.admins_list
|
||||||
|
.contains(&user_id.to_owned())
|
||||||
|
{
|
||||||
|
warn!(
|
||||||
|
"Revoking the admin status of {user_id} will not work correctly as they are within \
|
||||||
|
the admins_list config."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
let Ok(room_id) = self.get_admin_room().await else {
|
let Ok(room_id) = self.get_admin_room().await else {
|
||||||
return Err!(error!("No admin room available or created."));
|
return Err!(error!("No admin room available or created."));
|
||||||
};
|
};
|
||||||
|
|||||||
+122
-61
@@ -14,10 +14,10 @@ use conduwuit_core::{
|
|||||||
Error, Event, Result, Server, debug, err, error, error::default_log, pdu::PduBuilder,
|
Error, Event, Result, Server, debug, err, error, error::default_log, pdu::PduBuilder,
|
||||||
};
|
};
|
||||||
pub use create::create_admin_room;
|
pub use create::create_admin_room;
|
||||||
use futures::{Future, FutureExt, TryFutureExt};
|
use futures::{Future, FutureExt, StreamExt, TryFutureExt};
|
||||||
use loole::{Receiver, Sender};
|
use loole::{Receiver, Sender};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
Mxc, OwnedEventId, OwnedMxcUri, OwnedRoomId, RoomId, UInt, UserId,
|
Mxc, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedUserId, RoomId, UInt, UserId,
|
||||||
events::{
|
events::{
|
||||||
Mentions,
|
Mentions,
|
||||||
room::{
|
room::{
|
||||||
@@ -54,15 +54,37 @@ struct Services {
|
|||||||
media: Dep<crate::media::Service>,
|
media: Dep<crate::media::Service>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inputs to a command are a multi-line string, optional reply_id, and optional
|
/// Inputs to a command are a multi-line string, invocation source, optional
|
||||||
/// sender.
|
/// reply_id, and optional sender.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct CommandInput {
|
pub struct CommandInput {
|
||||||
pub command: String,
|
pub command: String,
|
||||||
pub reply_id: Option<OwnedEventId>,
|
pub reply_id: Option<OwnedEventId>,
|
||||||
|
pub source: InvocationSource,
|
||||||
pub sender: Option<Box<UserId>>,
|
pub sender: Option<Box<UserId>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Where a command is being invoked from.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum InvocationSource {
|
||||||
|
/// The server's private admin room
|
||||||
|
AdminRoom,
|
||||||
|
/// An escaped `\!admin` command in a public room
|
||||||
|
EscapedCommand,
|
||||||
|
/// The server's admin console
|
||||||
|
Console,
|
||||||
|
/// Some other trusted internal source
|
||||||
|
Internal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InvocationSource {
|
||||||
|
/// Returns whether this invocation source allows "restricted"
|
||||||
|
/// commands, i.e. ones that could be potentially dangerous if executed by
|
||||||
|
/// an attacker or in a public room.
|
||||||
|
#[must_use]
|
||||||
|
pub fn allows_restricted(&self) -> bool { !matches!(self, Self::EscapedCommand) }
|
||||||
|
}
|
||||||
|
|
||||||
/// Prototype of the tab-completer. The input is buffered text when tab
|
/// Prototype of the tab-completer. The input is buffered text when tab
|
||||||
/// asserted; the output will fully replace the input buffer.
|
/// asserted; the output will fully replace the input buffer.
|
||||||
pub type Completer = fn(&str) -> String;
|
pub type Completer = fn(&str) -> String;
|
||||||
@@ -276,10 +298,15 @@ impl Service {
|
|||||||
/// Posts a command to the command processor queue and returns. Processing
|
/// Posts a command to the command processor queue and returns. Processing
|
||||||
/// will take place on the service worker's task asynchronously. Errors if
|
/// will take place on the service worker's task asynchronously. Errors if
|
||||||
/// the queue is full.
|
/// the queue is full.
|
||||||
pub fn command(&self, command: String, reply_id: Option<OwnedEventId>) -> Result<()> {
|
pub fn command(
|
||||||
|
&self,
|
||||||
|
command: String,
|
||||||
|
reply_id: Option<OwnedEventId>,
|
||||||
|
source: InvocationSource,
|
||||||
|
) -> Result<()> {
|
||||||
self.channel
|
self.channel
|
||||||
.0
|
.0
|
||||||
.send(CommandInput { command, reply_id, sender: None })
|
.send(CommandInput { command, reply_id, source, sender: None })
|
||||||
.map_err(|e| err!("Failed to enqueue admin command: {e:?}"))
|
.map_err(|e| err!("Failed to enqueue admin command: {e:?}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,11 +317,17 @@ impl Service {
|
|||||||
&self,
|
&self,
|
||||||
command: String,
|
command: String,
|
||||||
reply_id: Option<OwnedEventId>,
|
reply_id: Option<OwnedEventId>,
|
||||||
|
source: InvocationSource,
|
||||||
sender: Box<UserId>,
|
sender: Box<UserId>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
self.channel
|
self.channel
|
||||||
.0
|
.0
|
||||||
.send(CommandInput { command, reply_id, sender: Some(sender) })
|
.send(CommandInput {
|
||||||
|
command,
|
||||||
|
reply_id,
|
||||||
|
source,
|
||||||
|
sender: Some(sender),
|
||||||
|
})
|
||||||
.map_err(|e| err!("Failed to enqueue admin command: {e:?}"))
|
.map_err(|e| err!("Failed to enqueue admin command: {e:?}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,8 +337,9 @@ impl Service {
|
|||||||
&self,
|
&self,
|
||||||
command: String,
|
command: String,
|
||||||
reply_id: Option<OwnedEventId>,
|
reply_id: Option<OwnedEventId>,
|
||||||
|
source: InvocationSource,
|
||||||
) -> ProcessorResult {
|
) -> ProcessorResult {
|
||||||
self.process_command(CommandInput { command, reply_id, sender: None })
|
self.process_command(CommandInput { command, reply_id, source, sender: None })
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,16 +383,43 @@ impl Service {
|
|||||||
handle(services, command).await
|
handle(services, command).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the list of admins for this server. First loads
|
||||||
|
/// the admin_list from the configuration, then adds users from
|
||||||
|
/// the admin room if applicable.
|
||||||
|
pub async fn get_admins(&self) -> Vec<OwnedUserId> {
|
||||||
|
let mut generated_admin_list: Vec<OwnedUserId> =
|
||||||
|
self.services.server.config.admins_list.clone();
|
||||||
|
|
||||||
|
if self.services.server.config.admins_from_room {
|
||||||
|
if let Ok(admin_room) = self.get_admin_room().await {
|
||||||
|
let admin_users = self.services.state_cache.room_members(&admin_room);
|
||||||
|
let mut stream = admin_users;
|
||||||
|
|
||||||
|
while let Some(user_id) = stream.next().await {
|
||||||
|
generated_admin_list.push(user_id.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generated_admin_list
|
||||||
|
}
|
||||||
|
|
||||||
/// Checks whether a given user is an admin of this server
|
/// Checks whether a given user is an admin of this server
|
||||||
pub async fn user_is_admin(&self, user_id: &UserId) -> bool {
|
pub async fn user_is_admin(&self, user_id: &UserId) -> bool {
|
||||||
let Ok(admin_room) = self.get_admin_room().await else {
|
if self.services.server.config.admins_list.contains(user_id) {
|
||||||
return false;
|
return true;
|
||||||
};
|
}
|
||||||
|
|
||||||
self.services
|
if self.services.server.config.admins_from_room {
|
||||||
.state_cache
|
if let Ok(admin_room) = self.get_admin_room().await {
|
||||||
.is_joined(user_id, &admin_room)
|
self.services
|
||||||
.await
|
.state_cache
|
||||||
|
.is_joined(user_id, &admin_room)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the room ID of the admin room
|
/// Gets the room ID of the admin room
|
||||||
@@ -459,59 +520,59 @@ impl Service {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn is_admin_command<E>(&self, event: &E, body: &str) -> bool
|
pub async fn is_admin_command<E>(&self, event: &E, body: &str) -> Option<InvocationSource>
|
||||||
where
|
where
|
||||||
E: Event + Send + Sync,
|
E: Event + Send + Sync,
|
||||||
{
|
{
|
||||||
// Server-side command-escape with public echo
|
// If the user isn't an admin they definitely can't run admin commands
|
||||||
let is_escape = body.starts_with('\\');
|
|
||||||
let is_public_escape = is_escape && body.trim_start_matches('\\').starts_with("!admin");
|
|
||||||
|
|
||||||
// Admin command with public echo (in admin room)
|
|
||||||
let server_user = &self.services.globals.server_user;
|
|
||||||
let is_public_prefix =
|
|
||||||
body.starts_with("!admin") || body.starts_with(server_user.as_str());
|
|
||||||
|
|
||||||
// Expected backward branch
|
|
||||||
if !is_public_escape && !is_public_prefix {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let user_is_local = self.services.globals.user_is_local(event.sender());
|
|
||||||
|
|
||||||
// only allow public escaped commands by local admins
|
|
||||||
if is_public_escape && !user_is_local {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if server-side command-escape is disabled by configuration
|
|
||||||
if is_public_escape && !self.services.server.config.admin_escape_commands {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent unescaped !admin from being used outside of the admin room
|
|
||||||
if event.room_id().is_some()
|
|
||||||
&& is_public_prefix
|
|
||||||
&& !self.is_admin_room(event.room_id().unwrap()).await
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only senders who are admin can proceed
|
|
||||||
if !self.user_is_admin(event.sender()).await {
|
if !self.user_is_admin(event.sender()).await {
|
||||||
return false;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This will evaluate to false if the emergency password is set up so that
|
if let Some(room_id) = event.room_id()
|
||||||
// the administrator can execute commands as the server user
|
&& self.is_admin_room(room_id).await
|
||||||
let emergency_password_set = self.services.server.config.emergency_password.is_some();
|
{
|
||||||
let from_server = event.sender() == server_user && !emergency_password_set;
|
// This is a message in the admin room
|
||||||
if from_server && self.is_admin_room(event.room_id().unwrap()).await {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authentic admin command
|
// Ignore messages which aren't admin commands
|
||||||
true
|
let server_user = &self.services.globals.server_user;
|
||||||
|
if !(body.starts_with("!admin") || body.starts_with(server_user.as_str())) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore messages from the server user _unless_ the emergency password is set
|
||||||
|
let emergency_password_set = self.services.server.config.emergency_password.is_some();
|
||||||
|
if event.sender() == server_user && !emergency_password_set {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looks good
|
||||||
|
Some(InvocationSource::AdminRoom)
|
||||||
|
} else {
|
||||||
|
// This is a message outside the admin room
|
||||||
|
|
||||||
|
// Is it an escaped admin command? i.e. `\!admin --help`
|
||||||
|
let is_public_escape =
|
||||||
|
body.starts_with('\\') && body.trim_start_matches('\\').starts_with("!admin");
|
||||||
|
|
||||||
|
// Ignore the message if it's not
|
||||||
|
if !is_public_escape {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only admin users belonging to this server can use escaped commands
|
||||||
|
if !self.services.globals.user_is_local(event.sender()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if escaped commands are disabled in the config
|
||||||
|
if !self.services.server.config.admin_escape_commands {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looks good
|
||||||
|
Some(InvocationSource::EscapedCommand)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|||||||
@@ -335,10 +335,11 @@ where
|
|||||||
if let Some(body) = content.body {
|
if let Some(body) = content.body {
|
||||||
self.services.search.index_pdu(shortroomid, &pdu_id, &body);
|
self.services.search.index_pdu(shortroomid, &pdu_id, &body);
|
||||||
|
|
||||||
if self.services.admin.is_admin_command(pdu, &body).await {
|
if let Some(source) = self.services.admin.is_admin_command(pdu, &body).await {
|
||||||
self.services.admin.command_with_sender(
|
self.services.admin.command_with_sender(
|
||||||
body,
|
body,
|
||||||
Some((pdu.event_id()).into()),
|
Some((pdu.event_id()).into()),
|
||||||
|
source,
|
||||||
pdu.sender.clone().into(),
|
pdu.sender.clone().into(),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[tool.towncrier]
|
||||||
|
name = "Continuwuity"
|
||||||
|
directory = "changelog.d"
|
||||||
|
filename = "CHANGELOG.md"
|
||||||
Reference in New Issue
Block a user