mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2025-07-06 14:56:11 +02:00
Compare commits
37 commits
dd372f77eb
...
a37151c665
Author | SHA1 | Date | |
---|---|---|---|
|
a37151c665 | ||
|
2b3980116c | ||
|
49ffec3876 | ||
|
ccdbf963de | ||
|
63fbb5dec3 | ||
|
cba9ee5240 | ||
|
5bb8c8e858 | ||
|
066cf68622 | ||
|
efdcca0b49 | ||
|
84d61fd032 | ||
|
dfdf6cd2f0 | ||
|
d939be460c | ||
|
22918bf5ef | ||
|
e84c3ebccf | ||
|
60718b029f | ||
|
4c313aac9e | ||
|
e8da89d79c | ||
|
d519028c5c | ||
|
aec255320c | ||
|
d66c78675d | ||
|
6250b07f6a | ||
|
94f2792d99 | ||
|
737f9b9788 | ||
|
942308ea57 | ||
|
8ccdec6516 | ||
|
80da18a325 | ||
|
531170594d | ||
|
1aafd1163d | ||
|
a9d9580aa4 | ||
|
1518ce0878 | ||
|
2e8abe1071 | ||
|
0c09c3651b | ||
|
0c5e4fdc20 | ||
|
2c043cfabf | ||
|
ebfbca59a7 | ||
|
78c2a07524 | ||
|
1c8ca527db |
26 changed files with 62 additions and 513 deletions
|
@ -15,7 +15,6 @@ docker/
|
||||||
.gitea
|
.gitea
|
||||||
.gitlab
|
.gitlab
|
||||||
.github
|
.github
|
||||||
.forgejo
|
|
||||||
|
|
||||||
# Dot files
|
# Dot files
|
||||||
.env
|
.env
|
||||||
|
|
|
@ -22,7 +22,3 @@ indent_size = 2
|
||||||
[*.rs]
|
[*.rs]
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
max_line_length = 98
|
max_line_length = 98
|
||||||
|
|
||||||
[{.forgejo/**/*.yml,.github/**/*.yml}]
|
|
||||||
indent_size = 2
|
|
||||||
indent_style = space
|
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
name: rust-toolchain
|
|
||||||
description: |
|
|
||||||
Install a Rust toolchain using rustup.
|
|
||||||
See https://rust-lang.github.io/rustup/concepts/toolchains.html#toolchain-specification
|
|
||||||
for more information about toolchains.
|
|
||||||
inputs:
|
|
||||||
toolchain:
|
|
||||||
description: |
|
|
||||||
Rust toolchain name.
|
|
||||||
See https://rust-lang.github.io/rustup/concepts/toolchains.html#toolchain-specification
|
|
||||||
required: false
|
|
||||||
target:
|
|
||||||
description: Target triple to install for this toolchain
|
|
||||||
required: false
|
|
||||||
components:
|
|
||||||
description: Space-separated list of components to be additionally installed for a new toolchain
|
|
||||||
required: false
|
|
||||||
outputs:
|
|
||||||
rustc_version:
|
|
||||||
description: The rustc version installed
|
|
||||||
value: ${{ steps.rustc-version.outputs.version }}
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: composite
|
|
||||||
steps:
|
|
||||||
- name: Cache rustup toolchains
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.rustup
|
|
||||||
!~/.rustup/tmp
|
|
||||||
!~/.rustup/downloads
|
|
||||||
# Requires repo to be cloned if toolchain is not specified
|
|
||||||
key: ${{ runner.os }}-rustup-${{ inputs.toolchain || hashFiles('**/rust-toolchain.toml') }}
|
|
||||||
- name: Install Rust toolchain
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
if ! command -v rustup &> /dev/null ; then
|
|
||||||
curl --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused -fsSL "https://sh.rustup.rs" | sh -s -- --default-toolchain none -y
|
|
||||||
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
|
|
||||||
fi
|
|
||||||
- shell: bash
|
|
||||||
run: |
|
|
||||||
set -x
|
|
||||||
${{ inputs.toolchain && format('rustup override set {0}', inputs.toolchain) }}
|
|
||||||
${{ inputs.target && format('rustup target add {0}', inputs.target) }}
|
|
||||||
${{ inputs.components && format('rustup component add {0}', inputs.components) }}
|
|
||||||
cargo --version
|
|
||||||
rustc --version
|
|
||||||
- id: rustc-version
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "version=$(rustc --version)" >> $GITHUB_OUTPUT
|
|
|
@ -1,29 +0,0 @@
|
||||||
name: sccache
|
|
||||||
description: |
|
|
||||||
Install sccache for caching builds in GitHub Actions.
|
|
||||||
|
|
||||||
inputs:
|
|
||||||
token:
|
|
||||||
description: 'A Github PAT'
|
|
||||||
required: false
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: composite
|
|
||||||
steps:
|
|
||||||
- name: Install sccache
|
|
||||||
uses: https://github.com/mozilla-actions/sccache-action@v0.0.9
|
|
||||||
with:
|
|
||||||
token: ${{ inputs.token }}
|
|
||||||
- name: Configure sccache
|
|
||||||
uses: https://github.com/actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
|
|
||||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
|
||||||
- shell: bash
|
|
||||||
run: |
|
|
||||||
echo "SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV
|
|
||||||
echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV
|
|
||||||
echo "CMAKE_C_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV
|
|
||||||
echo "CMAKE_CXX_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV
|
|
||||||
echo "CMAKE_CUDA_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV
|
|
|
@ -1,46 +0,0 @@
|
||||||
name: timelord
|
|
||||||
description: |
|
|
||||||
Use timelord to set file timestamps
|
|
||||||
inputs:
|
|
||||||
key:
|
|
||||||
description: |
|
|
||||||
The key to use for caching the timelord data.
|
|
||||||
This should be unique to the repository and the runner.
|
|
||||||
required: true
|
|
||||||
default: timelord-v0
|
|
||||||
path:
|
|
||||||
description: |
|
|
||||||
The path to the directory to be timestamped.
|
|
||||||
This should be the root of the repository.
|
|
||||||
required: true
|
|
||||||
default: .
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: composite
|
|
||||||
steps:
|
|
||||||
- name: Cache timelord-cli installation
|
|
||||||
id: cache-timelord-bin
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: ~/.cargo/bin/timelord
|
|
||||||
key: timelord-cli-v3.0.1
|
|
||||||
- name: Install timelord-cli
|
|
||||||
uses: https://github.com/cargo-bins/cargo-binstall@main
|
|
||||||
if: steps.cache-timelord-bin.outputs.cache-hit != 'true'
|
|
||||||
- run: cargo binstall timelord-cli@3.0.1
|
|
||||||
shell: bash
|
|
||||||
if: steps.cache-timelord-bin.outputs.cache-hit != 'true'
|
|
||||||
|
|
||||||
- name: Load timelord files
|
|
||||||
uses: actions/cache/restore@v3
|
|
||||||
with:
|
|
||||||
path: /timelord/
|
|
||||||
key: ${{ inputs.key }}
|
|
||||||
- name: Run timelord to set timestamps
|
|
||||||
shell: bash
|
|
||||||
run: timelord sync --source-dir ${{ inputs.path }} --cache-dir /timelord/
|
|
||||||
- name: Save timelord
|
|
||||||
uses: actions/cache/save@v3
|
|
||||||
with:
|
|
||||||
path: /timelord/
|
|
||||||
key: ${{ inputs.key }}
|
|
|
@ -1,142 +0,0 @@
|
||||||
name: Rust Checks
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
format:
|
|
||||||
name: Format
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install rust
|
|
||||||
uses: ./.forgejo/actions/rust-toolchain
|
|
||||||
with:
|
|
||||||
toolchain: "nightly"
|
|
||||||
components: "rustfmt"
|
|
||||||
|
|
||||||
- name: Check formatting
|
|
||||||
run: |
|
|
||||||
cargo +nightly fmt --all -- --check
|
|
||||||
|
|
||||||
clippy:
|
|
||||||
name: Clippy
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install rust
|
|
||||||
uses: ./.forgejo/actions/rust-toolchain
|
|
||||||
|
|
||||||
- uses: https://github.com/actions/create-github-app-token@v2
|
|
||||||
id: app-token
|
|
||||||
with:
|
|
||||||
app-id: ${{ vars.GH_APP_ID }}
|
|
||||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
|
||||||
github-api-url: https://api.github.com
|
|
||||||
owner: ${{ vars.GH_APP_OWNER }}
|
|
||||||
repositories: ""
|
|
||||||
- name: Install sccache
|
|
||||||
uses: ./.forgejo/actions/sccache
|
|
||||||
with:
|
|
||||||
token: ${{ steps.app-token.outputs.token }}
|
|
||||||
- run: sudo apt-get update
|
|
||||||
- name: Install system dependencies
|
|
||||||
uses: https://github.com/awalsh128/cache-apt-pkgs-action@v1
|
|
||||||
with:
|
|
||||||
packages: clang liburing-dev
|
|
||||||
version: 1
|
|
||||||
- name: Cache Rust registry
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/git
|
|
||||||
!~/.cargo/git/checkouts
|
|
||||||
~/.cargo/registry
|
|
||||||
!~/.cargo/registry/src
|
|
||||||
key: rust-registry-${{hashFiles('**/Cargo.lock') }}
|
|
||||||
- name: Timelord
|
|
||||||
uses: ./.forgejo/actions/timelord
|
|
||||||
with:
|
|
||||||
key: sccache-v0
|
|
||||||
path: .
|
|
||||||
- name: Clippy
|
|
||||||
run: |
|
|
||||||
cargo clippy \
|
|
||||||
--workspace \
|
|
||||||
--locked \
|
|
||||||
--no-deps \
|
|
||||||
--profile test \
|
|
||||||
-- \
|
|
||||||
-D warnings
|
|
||||||
|
|
||||||
- name: Show sccache stats
|
|
||||||
if: always()
|
|
||||||
run: sccache --show-stats
|
|
||||||
|
|
||||||
cargo-test:
|
|
||||||
name: Cargo Test
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install rust
|
|
||||||
uses: ./.forgejo/actions/rust-toolchain
|
|
||||||
|
|
||||||
- uses: https://github.com/actions/create-github-app-token@v2
|
|
||||||
id: app-token
|
|
||||||
with:
|
|
||||||
app-id: ${{ vars.GH_APP_ID }}
|
|
||||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
|
||||||
github-api-url: https://api.github.com
|
|
||||||
owner: ${{ vars.GH_APP_OWNER }}
|
|
||||||
repositories: ""
|
|
||||||
- name: Install sccache
|
|
||||||
uses: ./.forgejo/actions/sccache
|
|
||||||
with:
|
|
||||||
token: ${{ steps.app-token.outputs.token }}
|
|
||||||
- run: sudo apt-get update
|
|
||||||
- name: Install system dependencies
|
|
||||||
uses: https://github.com/awalsh128/cache-apt-pkgs-action@v1
|
|
||||||
with:
|
|
||||||
packages: clang liburing-dev
|
|
||||||
version: 1
|
|
||||||
- name: Cache Rust registry
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/git
|
|
||||||
!~/.cargo/git/checkouts
|
|
||||||
~/.cargo/registry
|
|
||||||
!~/.cargo/registry/src
|
|
||||||
key: rust-registry-${{hashFiles('**/Cargo.lock') }}
|
|
||||||
- name: Timelord
|
|
||||||
uses: ./.forgejo/actions/timelord
|
|
||||||
with:
|
|
||||||
key: sccache-v0
|
|
||||||
path: .
|
|
||||||
- name: Cargo Test
|
|
||||||
run: |
|
|
||||||
cargo test \
|
|
||||||
--workspace \
|
|
||||||
--locked \
|
|
||||||
--profile test \
|
|
||||||
--all-targets \
|
|
||||||
--no-fail-fast
|
|
||||||
|
|
||||||
- name: Show sccache stats
|
|
||||||
if: always()
|
|
||||||
run: sccache --show-stats
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
[files]
|
||||||
|
extend-exclude = ["*.csr"]
|
||||||
|
|
||||||
[default.extend-words]
|
[default.extend-words]
|
||||||
"allocatedp" = "allocatedp"
|
"allocatedp" = "allocatedp"
|
||||||
"conduwuit" = "conduwuit"
|
"conduwuit" = "conduwuit"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Contributing guide
|
# Contributing guide
|
||||||
|
|
||||||
This page is for about contributing to Continuwuity. The
|
This page is for about contributing to conduwuit. The
|
||||||
[development](./development.md) page may be of interest for you as well.
|
[development](./development.md) page may be of interest for you as well.
|
||||||
|
|
||||||
If you would like to work on an [issue][issues] that is not assigned, preferably
|
If you would like to work on an [issue][issues] that is not assigned, preferably
|
||||||
|
@ -73,7 +73,7 @@ If you'd like to run Complement locally using Nix, see the
|
||||||
|
|
||||||
### Writing documentation
|
### Writing documentation
|
||||||
|
|
||||||
Continuwuity's website uses [`mdbook`][mdbook] and deployed via CI using GitHub
|
conduwuit's website uses [`mdbook`][mdbook] and deployed via CI using GitHub
|
||||||
Pages in the [`documentation.yml`][documentation.yml] workflow file with Nix's
|
Pages in the [`documentation.yml`][documentation.yml] workflow file with Nix's
|
||||||
mdbook in the devshell. All documentation is in the `docs/` directory at the top
|
mdbook in the devshell. All documentation is in the `docs/` directory at the top
|
||||||
level. The compiled mdbook website is also uploaded as an artifact.
|
level. The compiled mdbook website is also uploaded as an artifact.
|
||||||
|
|
|
@ -7,15 +7,10 @@
|
||||||
<!-- ANCHOR_END: catchphrase -->
|
<!-- ANCHOR_END: catchphrase -->
|
||||||
|
|
||||||
[continuwuity] is a Matrix homeserver written in Rust.
|
[continuwuity] is a Matrix homeserver written in Rust.
|
||||||
It's a community continuation of the [conduwuit](https://github.com/girlbossceo/conduwuit) homeserver.
|
It's a community continuation of the [conduwuit](https://github.com/girlbossceo/conduwuit) homeserver.
|
||||||
|
|
||||||
<!-- ANCHOR: body -->
|
<!-- ANCHOR: body -->
|
||||||
|
|
||||||
[](https://forgejo.ellis.link/continuwuation/continuwuity)  [](https://forgejo.ellis.link/continuwuation/continuwuity/issues?state=open) [](https://forgejo.ellis.link/continuwuation/continuwuity/pulls?state=open)
|
|
||||||
|
|
||||||
[](https://github.com/continuwuity/continuwuity) 
|
|
||||||
|
|
||||||
[](https://codeberg.org/nexy7574/continuwuity) 
|
|
||||||
|
|
||||||
### Why does this exist?
|
### Why does this exist?
|
||||||
|
|
||||||
|
@ -117,3 +112,4 @@ Join our [Matrix room](https://matrix.to/#/#continuwuity:continuwuity.org) and [
|
||||||
|
|
||||||
|
|
||||||
[continuwuity]: https://forgejo.ellis.link/continuwuation/continuwuity
|
[continuwuity]: https://forgejo.ellis.link/continuwuation/continuwuity
|
||||||
|
|
||||||
|
|
63
SECURITY.md
63
SECURITY.md
|
@ -1,63 +0,0 @@
|
||||||
# Security Policy for Continuwuity
|
|
||||||
|
|
||||||
This document outlines the security policy for Continuwuity. Our goal is to maintain a secure platform for all users, and we take security matters seriously.
|
|
||||||
|
|
||||||
## Supported Versions
|
|
||||||
|
|
||||||
We provide security updates for the following versions of Continuwuity:
|
|
||||||
|
|
||||||
| Version | Supported |
|
|
||||||
| -------------- |:----------------:|
|
|
||||||
| Latest release | ✅ |
|
|
||||||
| Main branch | ✅ |
|
|
||||||
| Older releases | ❌ |
|
|
||||||
|
|
||||||
We may backport fixes to the previous release at our discretion, but we don't guarantee this.
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
### Responsible Disclosure
|
|
||||||
|
|
||||||
We appreciate the efforts of security researchers and the community in identifying and reporting vulnerabilities. To ensure that potential vulnerabilities are addressed properly, please follow these guidelines:
|
|
||||||
|
|
||||||
1. Contact members of the team over E2EE private message.
|
|
||||||
- [@jade:ellis.link](https://matrix.to/#/@jade:ellis.link)
|
|
||||||
- [@nex:nexy7574.co.uk](https://matrix.to/#/@nex:nexy7574.co.uk) <!-- ? -->
|
|
||||||
2. **Email the security team** directly 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:
|
|
||||||
- A clear description of the issue
|
|
||||||
- Steps to reproduce
|
|
||||||
- Potential impact
|
|
||||||
- Any possible mitigations
|
|
||||||
- Version(s) affected, including specific commits if possible
|
|
||||||
|
|
||||||
If you have any doubts about a potential security vulnerability, contact us via private channels first! We'd prefer that you bother us, instead of having a vulnerability disclosed without a fix.
|
|
||||||
|
|
||||||
### What to Expect
|
|
||||||
|
|
||||||
When you report a security vulnerability:
|
|
||||||
|
|
||||||
1. **Acknowledgment**: We will acknowledge receipt of your report.
|
|
||||||
2. **Assessment**: We will assess the vulnerability and determine its impact on our users
|
|
||||||
3. **Updates**: We will provide updates on our progress in addressing the vulnerability, and may request you help test mitigations
|
|
||||||
4. **Resolution**: Once resolved, we will notify you and discuss coordinated disclosure
|
|
||||||
5. **Credit**: We will recognize your contribution (unless you prefer to remain anonymous)
|
|
||||||
|
|
||||||
## Security Update Process
|
|
||||||
|
|
||||||
When security vulnerabilities are identified:
|
|
||||||
|
|
||||||
1. We will develop and test fixes in a private branch
|
|
||||||
2. Security updates will be released as soon as possible
|
|
||||||
3. Release notes will include information about the vulnerabilities, avoiding details that could facilitate exploitation where possible
|
|
||||||
4. Critical security updates may be backported to the previous stable release
|
|
||||||
|
|
||||||
## Additional Resources
|
|
||||||
|
|
||||||
- [Matrix Security Disclosure Policy](https://matrix.org/security-disclosure-policy/)
|
|
||||||
- [Continuwuity Documentation](https://continuwuity.org/introduction)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
This security policy was last updated on May 25, 2025.
|
|
4
debian/README.md
vendored
4
debian/README.md
vendored
|
@ -1,4 +1,4 @@
|
||||||
# Continuwuity for Debian
|
# conduwuit for Debian
|
||||||
|
|
||||||
Information about downloading and deploying the Debian package. This may also be
|
Information about downloading and deploying the Debian package. This may also be
|
||||||
referenced for other `apt`-based distros such as Ubuntu.
|
referenced for other `apt`-based distros such as Ubuntu.
|
||||||
|
@ -22,7 +22,7 @@ options in `/etc/conduwuit/conduwuit.toml`.
|
||||||
|
|
||||||
### Running
|
### Running
|
||||||
|
|
||||||
The package uses the [`conduwuit.service`](../configuration/examples.md#example-systemd-unit-file) systemd unit file to start and stop Continuwuity. The binary is installed at `/usr/sbin/conduwuit`.
|
The package uses the [`conduwuit.service`](../configuration/examples.md#example-systemd-unit-file) systemd unit file to start and stop conduwuit. The binary is installed at `/usr/sbin/conduwuit`.
|
||||||
|
|
||||||
This package assumes by default that conduwuit will be placed behind a reverse proxy. The default config options apply (listening on `localhost` and TCP port `6167`). Matrix federation requires a valid domain name and TLS, so you will need to set up TLS certificates and renewal for it to work properly if you intend to federate.
|
This package assumes by default that conduwuit will be placed behind a reverse proxy. The default config options apply (listening on `localhost` and TCP port `6167`). Matrix federation requires a valid domain name and TLS, so you will need to set up TLS certificates and renewal for it to work properly if you intend to federate.
|
||||||
|
|
||||||
|
|
|
@ -20,4 +20,3 @@
|
||||||
- [Testing](development/testing.md)
|
- [Testing](development/testing.md)
|
||||||
- [Hot Reloading ("Live" Development)](development/hot_reload.md)
|
- [Hot Reloading ("Live" Development)](development/hot_reload.md)
|
||||||
- [Community (and Guidelines)](community.md)
|
- [Community (and Guidelines)](community.md)
|
||||||
- [Security](security.md)
|
|
||||||
|
|
|
@ -6,11 +6,11 @@ services:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.docker.network=proxy" # Change this to the name of your Traefik docker proxy network
|
- "traefik.docker.network=proxy" # Change this to the name of your Traefik docker proxy network
|
||||||
|
|
||||||
- "traefik.http.routers.to-continuwuity.rule=Host(`<SUBDOMAIN>.<DOMAIN>`)" # Change to the address on which Continuwuity is hosted
|
- "traefik.http.routers.to-conduwuit.rule=Host(`<SUBDOMAIN>.<DOMAIN>`)" # Change to the address on which Continuwuity is hosted
|
||||||
- "traefik.http.routers.to-continuwuity.tls=true"
|
- "traefik.http.routers.to-conduwuit.tls=true"
|
||||||
- "traefik.http.routers.to-continuwuity.tls.certresolver=letsencrypt"
|
- "traefik.http.routers.to-conduwuit.tls.certresolver=letsencrypt"
|
||||||
- "traefik.http.routers.to-continuwuity.middlewares=cors-headers@docker"
|
- "traefik.http.routers.to-conduwuit.middlewares=cors-headers@docker"
|
||||||
- "traefik.http.services.to_continuwuity.loadbalancer.server.port=6167"
|
- "traefik.http.services.to_conduwuit.loadbalancer.server.port=6167"
|
||||||
|
|
||||||
- "traefik.http.middlewares.cors-headers.headers.accessControlAllowOriginList=*"
|
- "traefik.http.middlewares.cors-headers.headers.accessControlAllowOriginList=*"
|
||||||
- "traefik.http.middlewares.cors-headers.headers.accessControlAllowHeaders=Origin, X-Requested-With, Content-Type, Accept, Authorization"
|
- "traefik.http.middlewares.cors-headers.headers.accessControlAllowHeaders=Origin, X-Requested-With, Content-Type, Accept, Authorization"
|
||||||
|
|
|
@ -25,23 +25,23 @@ services:
|
||||||
image: forgejo.ellis.link/continuwuation/continuwuity:latest
|
image: forgejo.ellis.link/continuwuation/continuwuity:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- db:/var/lib/continuwuity
|
- db:/var/lib/conduwuit
|
||||||
- /etc/resolv.conf:/etc/resolv.conf:ro # Use the host's DNS resolver rather than Docker's.
|
- /etc/resolv.conf:/etc/resolv.conf:ro # Use the host's DNS resolver rather than Docker's.
|
||||||
#- ./continuwuity.toml:/etc/continuwuity.toml
|
#- ./conduwuit.toml:/etc/conduwuit.toml
|
||||||
environment:
|
environment:
|
||||||
CONTINUWUITY_SERVER_NAME: example.com # EDIT THIS
|
CONDUWUIT_SERVER_NAME: example.com # EDIT THIS
|
||||||
CONTINUWUITY_DATABASE_PATH: /var/lib/continuwuity
|
CONDUWUIT_DATABASE_PATH: /var/lib/conduwuit
|
||||||
CONTINUWUITY_PORT: 6167
|
CONDUWUIT_PORT: 6167
|
||||||
CONTINUWUITY_MAX_REQUEST_SIZE: 20000000 # in bytes, ~20 MB
|
CONDUWUIT_MAX_REQUEST_SIZE: 20000000 # in bytes, ~20 MB
|
||||||
CONTINUWUITY_ALLOW_REGISTRATION: 'true'
|
CONDUWUIT_ALLOW_REGISTRATION: 'true'
|
||||||
CONTINUWUITY_REGISTRATION_TOKEN: 'YOUR_TOKEN' # A registration token is required when registration is allowed.
|
CONDUWUIT_REGISTRATION_TOKEN: 'YOUR_TOKEN' # A registration token is required when registration is allowed.
|
||||||
#CONTINUWUITY_YES_I_AM_VERY_VERY_SURE_I_WANT_AN_OPEN_REGISTRATION_SERVER_PRONE_TO_ABUSE: 'true'
|
#CONDUWUIT_YES_I_AM_VERY_VERY_SURE_I_WANT_AN_OPEN_REGISTRATION_SERVER_PRONE_TO_ABUSE: 'true'
|
||||||
CONTINUWUITY_ALLOW_FEDERATION: 'true'
|
CONDUWUIT_ALLOW_FEDERATION: 'true'
|
||||||
CONTINUWUITY_ALLOW_CHECK_FOR_UPDATES: 'true'
|
CONDUWUIT_ALLOW_CHECK_FOR_UPDATES: 'true'
|
||||||
CONTINUWUITY_TRUSTED_SERVERS: '["matrix.org"]'
|
CONDUWUIT_TRUSTED_SERVERS: '["matrix.org"]'
|
||||||
#CONTINUWUITY_LOG: warn,state_res=warn
|
#CONDUWUIT_LOG: warn,state_res=warn
|
||||||
CONTINUWUITY_ADDRESS: 0.0.0.0
|
CONDUWUIT_ADDRESS: 0.0.0.0
|
||||||
#CONTINUWUITY_CONFIG: '/etc/continuwuity.toml' # Uncomment if you mapped config toml above
|
#CONDUWUIT_CONFIG: '/etc/conduwuit.toml' # Uncomment if you mapped config toml above
|
||||||
networks:
|
networks:
|
||||||
- caddy
|
- caddy
|
||||||
labels:
|
labels:
|
||||||
|
|
|
@ -9,22 +9,22 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- 8448:6167
|
- 8448:6167
|
||||||
volumes:
|
volumes:
|
||||||
- db:/var/lib/continuwuity
|
- db:/var/lib/conduwuit
|
||||||
#- ./continuwuity.toml:/etc/continuwuity.toml
|
#- ./conduwuit.toml:/etc/conduwuit.toml
|
||||||
environment:
|
environment:
|
||||||
CONTINUWUITY_SERVER_NAME: your.server.name # EDIT THIS
|
CONDUWUIT_SERVER_NAME: your.server.name # EDIT THIS
|
||||||
CONTINUWUITY_DATABASE_PATH: /var/lib/continuwuity
|
CONDUWUIT_DATABASE_PATH: /var/lib/conduwuit
|
||||||
CONTINUWUITY_PORT: 6167
|
CONDUWUIT_PORT: 6167
|
||||||
CONTINUWUITY_MAX_REQUEST_SIZE: 20000000 # in bytes, ~20 MB
|
CONDUWUIT_MAX_REQUEST_SIZE: 20000000 # in bytes, ~20 MB
|
||||||
CONTINUWUITY_ALLOW_REGISTRATION: 'true'
|
CONDUWUIT_ALLOW_REGISTRATION: 'true'
|
||||||
CONTINUWUITY_REGISTRATION_TOKEN: 'YOUR_TOKEN' # A registration token is required when registration is allowed.
|
CONDUWUIT_REGISTRATION_TOKEN: 'YOUR_TOKEN' # A registration token is required when registration is allowed.
|
||||||
#CONTINUWUITY_YES_I_AM_VERY_VERY_SURE_I_WANT_AN_OPEN_REGISTRATION_SERVER_PRONE_TO_ABUSE: 'true'
|
#CONDUWUIT_YES_I_AM_VERY_VERY_SURE_I_WANT_AN_OPEN_REGISTRATION_SERVER_PRONE_TO_ABUSE: 'true'
|
||||||
CONTINUWUITY_ALLOW_FEDERATION: 'true'
|
CONDUWUIT_ALLOW_FEDERATION: 'true'
|
||||||
CONTINUWUITY_ALLOW_CHECK_FOR_UPDATES: 'true'
|
CONDUWUIT_ALLOW_CHECK_FOR_UPDATES: 'true'
|
||||||
CONTINUWUITY_TRUSTED_SERVERS: '["matrix.org"]'
|
CONDUWUIT_TRUSTED_SERVERS: '["matrix.org"]'
|
||||||
#CONTINUWUITY_LOG: warn,state_res=warn
|
#CONDUWUIT_LOG: warn,state_res=warn
|
||||||
CONTINUWUITY_ADDRESS: 0.0.0.0
|
CONDUWUIT_ADDRESS: 0.0.0.0
|
||||||
#CONTINUWUITY_CONFIG: '/etc/continuwuity.toml' # Uncomment if you mapped config toml above
|
#CONDUWUIT_CONFIG: '/etc/conduwuit.toml' # Uncomment if you mapped config toml above
|
||||||
#
|
#
|
||||||
### Uncomment if you want to use your own Element-Web App.
|
### Uncomment if you want to use your own Element-Web App.
|
||||||
### Note: You need to provide a config.json for Element and you also need a second
|
### Note: You need to provide a config.json for Element and you also need a second
|
||||||
|
|
|
@ -30,16 +30,16 @@ When you have the image you can simply run it with
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d -p 8448:6167 \
|
docker run -d -p 8448:6167 \
|
||||||
-v db:/var/lib/continuwuity/ \
|
-v db:/var/lib/conduwuit/ \
|
||||||
-e CONTINUWUITY_SERVER_NAME="your.server.name" \
|
-e CONDUWUIT_SERVER_NAME="your.server.name" \
|
||||||
-e CONTINUWUITY_ALLOW_REGISTRATION=false \
|
-e CONDUWUIT_ALLOW_REGISTRATION=false \
|
||||||
--name continuwuity $LINK
|
--name conduwuit $LINK
|
||||||
```
|
```
|
||||||
|
|
||||||
or you can use [docker compose](#docker-compose).
|
or you can use [docker compose](#docker-compose).
|
||||||
|
|
||||||
The `-d` flag lets the container run in detached mode. You may supply an
|
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
|
optional `conduwuit.toml` config file, the example config can be found
|
||||||
[here](../configuration/examples.md). You can pass in different env vars to
|
[here](../configuration/examples.md). You can pass in different env vars to
|
||||||
change config values on the fly. You can even configure Continuwuity completely by
|
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
|
using env vars. For an overview of possible values, please take a look at the
|
||||||
|
|
|
@ -115,7 +115,7 @@ ReadWritePaths=/path/to/custom/database/path
|
||||||
## Creating the Continuwuity configuration file
|
## Creating the Continuwuity configuration file
|
||||||
|
|
||||||
Now we need to create the Continuwuity's config file in
|
Now we need to create the Continuwuity's config file in
|
||||||
`/etc/continuwuity/continuwuity.toml`. The example config can be found at
|
`/etc/conduwuit/conduwuit.toml`. The example config can be found at
|
||||||
[conduwuit-example.toml](../configuration/examples.md).
|
[conduwuit-example.toml](../configuration/examples.md).
|
||||||
|
|
||||||
**Please take a moment to read the config. You need to change at least the
|
**Please take a moment to read the config. You need to change at least the
|
||||||
|
|
|
@ -190,7 +190,7 @@ The initial implementation PR is available [here][1].
|
||||||
- [Workspace-level metadata
|
- [Workspace-level metadata
|
||||||
(cargo-deb)](https://github.com/kornelski/cargo-deb/issues/68)
|
(cargo-deb)](https://github.com/kornelski/cargo-deb/issues/68)
|
||||||
|
|
||||||
[1]: https://forgejo.ellis.link/continuwuation/continuwuity/pulls/387
|
[1]: https://github.com/girlbossceo/conduwuit/pull/387
|
||||||
[2]: https://wiki.musl-libc.org/functional-differences-from-glibc.html#Unloading-libraries
|
[2]: https://wiki.musl-libc.org/functional-differences-from-glibc.html#Unloading-libraries
|
||||||
[3]: https://github.com/rust-lang/rust/issues/28794
|
[3]: https://github.com/rust-lang/rust/issues/28794
|
||||||
[4]: https://github.com/rust-lang/rust/issues/28794#issuecomment-368693049
|
[4]: https://github.com/rust-lang/rust/issues/28794#issuecomment-368693049
|
||||||
|
|
|
@ -24,9 +24,8 @@ and run the script.
|
||||||
If you're on macOS and need to build an image, run `nix build .#linux-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
|
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: <https://github.com/girlbossceo/complement>
|
||||||
|
|
||||||
[ci-workflows]:
|
[ci-workflows]: https://github.com/girlbossceo/conduwuit/actions/workflows/ci.yml?query=event%3Apush+is%3Asuccess+actor%3Agirlbossceo
|
||||||
https://forgejo.ellis.link/continuwuation/continuwuity/actions/?workflow=ci.yml&actor=0&status=1
|
|
||||||
[complement]: https://github.com/matrix-org/complement
|
[complement]: https://github.com/matrix-org/complement
|
||||||
[direnv]: https://direnv.net/docs/hook.html
|
[direnv]: https://direnv.net/docs/hook.html
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
{{#include ../SECURITY.md}}
|
|
|
@ -33,13 +33,13 @@ dockerTools.buildLayeredImage {
|
||||||
<jason@zemos.net>";
|
<jason@zemos.net>";
|
||||||
"org.opencontainers.image.created" ="@${toString inputs.self.lastModified}";
|
"org.opencontainers.image.created" ="@${toString inputs.self.lastModified}";
|
||||||
"org.opencontainers.image.description" = "a very cool Matrix chat homeserver written in Rust";
|
"org.opencontainers.image.description" = "a very cool Matrix chat homeserver written in Rust";
|
||||||
"org.opencontainers.image.documentation" = "https://continuwuity.org/";
|
"org.opencontainers.image.documentation" = "https://conduwuit.puppyirl.gay/";
|
||||||
"org.opencontainers.image.licenses" = "Apache-2.0";
|
"org.opencontainers.image.licenses" = "Apache-2.0";
|
||||||
"org.opencontainers.image.revision" = inputs.self.rev or inputs.self.dirtyRev or "";
|
"org.opencontainers.image.revision" = inputs.self.rev or inputs.self.dirtyRev or "";
|
||||||
"org.opencontainers.image.source" = "https://forgejo.ellis.link/continuwuation/continuwuity";
|
"org.opencontainers.image.source" = "https://github.com/girlbossceo/conduwuit";
|
||||||
"org.opencontainers.image.title" = main.pname;
|
"org.opencontainers.image.title" = main.pname;
|
||||||
"org.opencontainers.image.url" = "https://continuwuity.org/";
|
"org.opencontainers.image.url" = "https://conduwuit.puppyirl.gay/";
|
||||||
"org.opencontainers.image.vendor" = "continuwuation";
|
"org.opencontainers.image.vendor" = "girlbossceo";
|
||||||
"org.opencontainers.image.version" = main.version;
|
"org.opencontainers.image.version" = main.version;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -94,7 +94,7 @@ async fn process_command(services: Arc<Services>, input: &CommandInput) -> Proce
|
||||||
#[allow(clippy::result_large_err)]
|
#[allow(clippy::result_large_err)]
|
||||||
fn handle_panic(error: &Error, command: &CommandInput) -> ProcessorResult {
|
fn handle_panic(error: &Error, command: &CommandInput) -> ProcessorResult {
|
||||||
let link =
|
let link =
|
||||||
"Please submit a [bug report](https://forgejo.ellis.link/continuwuation/continuwuity/issues/new). 🥺";
|
"Please submit a [bug report](https://github.com/girlbossceo/conduwuit/issues/new). 🥺";
|
||||||
let msg = format!("Panic occurred while processing command:\n```\n{error:#?}\n```\n{link}");
|
let msg = format!("Panic occurred while processing command:\n```\n{error:#?}\n```\n{link}");
|
||||||
let content = RoomMessageEventContent::notice_markdown(msg);
|
let content = RoomMessageEventContent::notice_markdown(msg);
|
||||||
error!("Panic while processing command: {error:?}");
|
error!("Panic while processing command: {error:?}");
|
||||||
|
|
|
@ -2171,109 +2171,6 @@ async fn knock_room_by_id_helper(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For knock_restricted rooms, check if the user meets the restricted conditions
|
|
||||||
// If they do, attempt to join instead of knock
|
|
||||||
// This is not mentioned in the spec, but should be allowable (we're allowed to
|
|
||||||
// auto-join invites to knocked rooms)
|
|
||||||
let join_rule = services.rooms.state_accessor.get_join_rules(room_id).await;
|
|
||||||
if let JoinRule::KnockRestricted(restricted) = &join_rule {
|
|
||||||
let restriction_rooms: Vec<_> = restricted
|
|
||||||
.allow
|
|
||||||
.iter()
|
|
||||||
.filter_map(|a| match a {
|
|
||||||
| AllowRule::RoomMembership(r) => Some(&r.room_id),
|
|
||||||
| _ => None,
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Check if the user is in any of the allowed rooms
|
|
||||||
let mut user_meets_restrictions = false;
|
|
||||||
for restriction_room_id in &restriction_rooms {
|
|
||||||
if services
|
|
||||||
.rooms
|
|
||||||
.state_cache
|
|
||||||
.is_joined(sender_user, restriction_room_id)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
user_meets_restrictions = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the user meets the restrictions, try joining instead
|
|
||||||
if user_meets_restrictions {
|
|
||||||
debug_info!(
|
|
||||||
"{sender_user} meets the restricted criteria in knock_restricted room \
|
|
||||||
{room_id}, attempting to join instead of knock"
|
|
||||||
);
|
|
||||||
// For this case, we need to drop the state lock and get a new one in
|
|
||||||
// join_room_by_id_helper We need to release the lock here and let
|
|
||||||
// join_room_by_id_helper acquire it again
|
|
||||||
drop(state_lock);
|
|
||||||
match join_room_by_id_helper(
|
|
||||||
services,
|
|
||||||
sender_user,
|
|
||||||
room_id,
|
|
||||||
reason.clone(),
|
|
||||||
servers,
|
|
||||||
None,
|
|
||||||
&None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
| Ok(_) => return Ok(knock_room::v3::Response::new(room_id.to_owned())),
|
|
||||||
| Err(e) => {
|
|
||||||
debug_warn!(
|
|
||||||
"Failed to convert knock to join for {sender_user} in {room_id}: {e:?}"
|
|
||||||
);
|
|
||||||
// Get a new state lock for the remaining knock logic
|
|
||||||
let new_state_lock = services.rooms.state.mutex.lock(room_id).await;
|
|
||||||
|
|
||||||
let server_in_room = services
|
|
||||||
.rooms
|
|
||||||
.state_cache
|
|
||||||
.server_in_room(services.globals.server_name(), room_id)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let local_knock = server_in_room
|
|
||||||
|| servers.is_empty()
|
|
||||||
|| (servers.len() == 1 && services.globals.server_is_ours(&servers[0]));
|
|
||||||
|
|
||||||
if local_knock {
|
|
||||||
knock_room_helper_local(
|
|
||||||
services,
|
|
||||||
sender_user,
|
|
||||||
room_id,
|
|
||||||
reason,
|
|
||||||
servers,
|
|
||||||
new_state_lock,
|
|
||||||
)
|
|
||||||
.boxed()
|
|
||||||
.await?;
|
|
||||||
} else {
|
|
||||||
knock_room_helper_remote(
|
|
||||||
services,
|
|
||||||
sender_user,
|
|
||||||
room_id,
|
|
||||||
reason,
|
|
||||||
servers,
|
|
||||||
new_state_lock,
|
|
||||||
)
|
|
||||||
.boxed()
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(knock_room::v3::Response::new(room_id.to_owned()));
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if !matches!(join_rule, JoinRule::Knock | JoinRule::KnockRestricted(_)) {
|
|
||||||
debug_warn!(
|
|
||||||
"{sender_user} attempted to knock on room {room_id} but its join rule is \
|
|
||||||
{join_rule:?}, not knock or knock_restricted"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let server_in_room = services
|
let server_in_room = services
|
||||||
.rooms
|
.rooms
|
||||||
.state_cache
|
.state_cache
|
||||||
|
@ -2321,12 +2218,6 @@ async fn knock_room_helper_local(
|
||||||
return Err!(Request(Forbidden("This room does not support knocking.")));
|
return Err!(Request(Forbidden("This room does not support knocking.")));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify that this room has a valid knock or knock_restricted join rule
|
|
||||||
let join_rule = services.rooms.state_accessor.get_join_rules(room_id).await;
|
|
||||||
if !matches!(join_rule, JoinRule::Knock | JoinRule::KnockRestricted(_)) {
|
|
||||||
return Err!(Request(Forbidden("This room's join rule does not allow knocking.")));
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = RoomMemberEventContent {
|
let content = RoomMemberEventContent {
|
||||||
displayname: services.users.displayname(sender_user).await.ok(),
|
displayname: services.users.displayname(sender_user).await.ok(),
|
||||||
avatar_url: services.users.avatar_url(sender_user).await.ok(),
|
avatar_url: services.users.avatar_url(sender_user).await.ok(),
|
||||||
|
|
|
@ -118,7 +118,7 @@ pub fn check(config: &Config) -> Result {
|
||||||
if cfg!(not(debug_assertions)) && config.server_name == "your.server.name" {
|
if cfg!(not(debug_assertions)) && config.server_name == "your.server.name" {
|
||||||
return Err!(Config(
|
return Err!(Config(
|
||||||
"server_name",
|
"server_name",
|
||||||
"You must specify a valid server name for production usage of continuwuity."
|
"You must specify a valid server name for production usage of conduwuit."
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -290,7 +290,7 @@ fn warn_deprecated(config: &Config) {
|
||||||
|
|
||||||
if was_deprecated {
|
if was_deprecated {
|
||||||
warn!(
|
warn!(
|
||||||
"Read continuwuity config documentation at https://continuwuity.org/configuration.html and check your \
|
"Read conduwuit config documentation at https://conduwuit.puppyirl.gay/configuration.html and check your \
|
||||||
configuration if any new configuration parameters should be adjusted"
|
configuration if any new configuration parameters should be adjusted"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -638,7 +638,7 @@ fn valid_membership_change(
|
||||||
warn!(?target_user_membership_event_id, "Banned user can't join");
|
warn!(?target_user_membership_event_id, "Banned user can't join");
|
||||||
false
|
false
|
||||||
} else if (join_rules == JoinRule::Invite
|
} else if (join_rules == JoinRule::Invite
|
||||||
|| room_version.allow_knocking && (join_rules == JoinRule::Knock || matches!(join_rules, JoinRule::KnockRestricted(_))))
|
|| room_version.allow_knocking && join_rules == JoinRule::Knock)
|
||||||
// If the join_rule is invite then allow if membership state is invite or join
|
// If the join_rule is invite then allow if membership state is invite or join
|
||||||
&& (target_user_current_membership == MembershipState::Join
|
&& (target_user_current_membership == MembershipState::Join
|
||||||
|| target_user_current_membership == MembershipState::Invite)
|
|| target_user_current_membership == MembershipState::Invite)
|
||||||
|
|
|
@ -165,7 +165,7 @@ pub async fn create_admin_room(services: &Services) -> Result {
|
||||||
.timeline
|
.timeline
|
||||||
.build_and_append_pdu(
|
.build_and_append_pdu(
|
||||||
PduBuilder::state(String::new(), &RoomTopicEventContent {
|
PduBuilder::state(String::new(), &RoomTopicEventContent {
|
||||||
topic: format!("Manage {} | Run commands prefixed with `!admin` | Run `!admin -h` for help | Documentation: https://continuwuity.org/", services.config.server_name),
|
topic: format!("Manage {} | Run commands prefixed with `!admin` | Run `!admin -h` for help | Documentation: https://conduwuit.puppyirl.gay/", services.config.server_name),
|
||||||
}),
|
}),
|
||||||
server_user,
|
server_user,
|
||||||
&room_id,
|
&room_id,
|
||||||
|
|
Loading…
Add table
Reference in a new issue