mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2025-12-24 00:01:03 -05:00
Remove matrix-sdk-appservice
There is unfortunately no capacity for maintaining it as a first-party component of the Rust SDK.
This commit is contained in:
committed by
Jonas Platte
parent
11b3be1a03
commit
7d674b39aa
44
.github/workflows/ci.yml
vendored
44
.github/workflows/ci.yml
vendored
@@ -269,50 +269,6 @@ jobs:
|
||||
run: |
|
||||
target/debug/xtask ci wasm-pack ${{ matrix.cmd }}
|
||||
|
||||
test-appservice:
|
||||
name: ${{ matrix.os-name }} [m]-appservice
|
||||
needs: xtask
|
||||
if: github.event_name == 'push' || !github.event.pull_request.draft
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
os-name: 🐧
|
||||
xtask-cachekey: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
|
||||
- os: macos-latest
|
||||
os-name: 🍏
|
||||
xtask-cachekey: "${{ needs.xtask.outputs.cachekey-macos }}"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ matrix.xtask-cachekey }}"
|
||||
fail-on-cache-miss: true
|
||||
|
||||
- name: Run checks
|
||||
run: |
|
||||
target/debug/xtask ci test-appservice
|
||||
|
||||
formatting:
|
||||
name: Check Formatting
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
67
Cargo.lock
generated
67
Cargo.lock
generated
@@ -492,8 +492,6 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"sync_wrapper",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
@@ -1554,17 +1552,6 @@ version = "2.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
||||
|
||||
[[package]]
|
||||
name = "example-appservice-autojoin"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"matrix-sdk-appservice",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "example-autojoin"
|
||||
version = "0.1.0"
|
||||
@@ -3102,31 +3089,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matrix-sdk-appservice"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"dashmap",
|
||||
"http",
|
||||
"hyper",
|
||||
"matrix-sdk",
|
||||
"matrix-sdk-test",
|
||||
"regex",
|
||||
"ruma",
|
||||
"serde",
|
||||
"serde_html_form",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matrix-sdk-base"
|
||||
version = "0.6.1"
|
||||
@@ -5378,16 +5340,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_path_to_error"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_qs"
|
||||
version = "0.8.5"
|
||||
@@ -5449,19 +5401,6 @@ dependencies = [
|
||||
"syn 2.0.28",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.9.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574"
|
||||
dependencies = [
|
||||
"indexmap 2.0.0",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.5"
|
||||
@@ -6415,12 +6354,6 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.7.1"
|
||||
|
||||
@@ -8,7 +8,6 @@ coverage:
|
||||
threshold: 1%
|
||||
paths:
|
||||
- "crates/matrix-sdk/"
|
||||
- "crates/matrix-sdk-appservice/"
|
||||
- "crates/matrix-sdk-base/"
|
||||
- "crates/matrix-sdk-common/"
|
||||
- "crates/matrix-sdk-crypto/"
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
[package]
|
||||
authors = ["Johannes Becker <j.becker@famedly.com>"]
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
description = "Appservice SDK based on the matrix-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ruma", "nio", "appservice"]
|
||||
license = "Apache-2.0"
|
||||
name = "matrix-sdk-appservice"
|
||||
version = "0.1.0"
|
||||
rust-version = { workspace = true }
|
||||
publish = false
|
||||
|
||||
[features]
|
||||
default = ["native-tls"]
|
||||
|
||||
anyhow = ["matrix-sdk/anyhow"]
|
||||
e2e-encryption = [
|
||||
"matrix-sdk/e2e-encryption"
|
||||
]
|
||||
eyre = ["matrix-sdk/eyre"]
|
||||
sqlite = ["matrix-sdk/sqlite"]
|
||||
|
||||
markdown = ["matrix-sdk/markdown"]
|
||||
native-tls = ["matrix-sdk/native-tls"]
|
||||
rustls-tls = ["matrix-sdk/rustls-tls"]
|
||||
socks = ["matrix-sdk/socks"]
|
||||
sso-login = ["matrix-sdk/sso-login"]
|
||||
|
||||
docs = []
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.6.1", default-features = false, features = ["json"] }
|
||||
dashmap = { workspace = true }
|
||||
http = { workspace = true }
|
||||
hyper = { version = "0.14.20", features = ["http1", "http2", "server"] }
|
||||
matrix-sdk = { version = "0.6.0", path = "../matrix-sdk", default-features = false, features = ["appservice"] }
|
||||
regex = "1.5.5"
|
||||
ruma = { workspace = true, features = ["appservice-api-s"] }
|
||||
serde = { workspace = true }
|
||||
serde_html_form = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = "0.9.4"
|
||||
tokio = { workspace = true, features = ["rt-multi-thread"] }
|
||||
thiserror = { workspace = true }
|
||||
tower = { version = "0.4.13", default-features = false }
|
||||
tracing = { workspace = true }
|
||||
url = "2.2.2"
|
||||
|
||||
[dev-dependencies]
|
||||
matrix-sdk-test = { version = "0.6.0", path = "../../testing/matrix-sdk-test", features = ["appservice"] }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
tracing-subscriber = "0.3.11"
|
||||
wiremock = "0.5.13"
|
||||
@@ -1,114 +0,0 @@
|
||||
// Copyright 2021 Famedly GmbH
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use ruma::api::client::uiaa::UiaaInfo;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("missing access token")]
|
||||
MissingAccessToken,
|
||||
|
||||
#[error("missing host on registration url")]
|
||||
MissingRegistrationHost,
|
||||
|
||||
#[error("http request builder error")]
|
||||
UnknownHttpRequestBuilder,
|
||||
|
||||
#[error("no port found")]
|
||||
MissingRegistrationPort,
|
||||
|
||||
#[error("no client for localpart found")]
|
||||
NoClientForLocalpart,
|
||||
|
||||
#[error("could not convert host:port to socket addr")]
|
||||
HostPortToSocketAddrs,
|
||||
|
||||
#[error("uri has empty path")]
|
||||
UriEmptyPath,
|
||||
|
||||
#[error("uri path is unknown")]
|
||||
UriPathUnknown,
|
||||
|
||||
#[error("HTTP request parsing error: {0}")]
|
||||
FromHttpRequest(#[from] ruma::api::error::FromHttpRequestError),
|
||||
|
||||
#[error("identifier failed to parse: {0}")]
|
||||
Identifier(#[from] ruma::IdParseError),
|
||||
|
||||
#[error("HTTP error: {0}")]
|
||||
Http(#[from] http::Error),
|
||||
|
||||
#[error("url parse error: {0}")]
|
||||
Url(#[from] url::ParseError),
|
||||
|
||||
#[error("deserialization error: {0}")]
|
||||
Serde(#[from] serde::de::value::Error),
|
||||
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("http uri invalid error: {0}")]
|
||||
InvalidUri(#[from] http::uri::InvalidUri),
|
||||
|
||||
#[error(transparent)]
|
||||
Matrix(#[from] matrix_sdk::Error),
|
||||
|
||||
#[error("regex error: {0}")]
|
||||
Regex(#[from] regex::Error),
|
||||
|
||||
#[error("serde yaml error: {0}")]
|
||||
SerdeYaml(#[from] serde_yaml::Error),
|
||||
|
||||
#[error("serde json error: {0}")]
|
||||
SerdeJson(#[from] serde_json::Error),
|
||||
|
||||
#[error("utf8 error: {0}")]
|
||||
Utf8(#[from] std::str::Utf8Error),
|
||||
|
||||
#[error("hyper error: {0}")]
|
||||
Hyper(#[from] hyper::Error),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Try to destructure the error into an universal interactive auth info.
|
||||
///
|
||||
/// Some requests require universal interactive auth, doing such a request
|
||||
/// will always fail the first time with a 401 status code, the response
|
||||
/// body will contain info how the client can authenticate.
|
||||
///
|
||||
/// The request will need to be retried, this time containing additional
|
||||
/// authentication data.
|
||||
///
|
||||
/// This method is an convenience method to get to the info the server
|
||||
/// returned on the first, failed request.
|
||||
pub fn as_uiaa_response(&self) -> Option<&UiaaInfo> {
|
||||
match self {
|
||||
Error::Matrix(matrix) => matrix.as_uiaa_response(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::HttpError> for Error {
|
||||
fn from(e: matrix_sdk::HttpError) -> Self {
|
||||
matrix_sdk::Error::from(e).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::StoreError> for Error {
|
||||
fn from(e: matrix_sdk::StoreError) -> Self {
|
||||
matrix_sdk::Error::from(e).into()
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// Copyright 2022 Famedly GmbH
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{future::Future, pin::Pin, sync::Arc};
|
||||
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
ruma::api::appservice::query::{
|
||||
query_room_alias::v1 as query_room, query_user_id::v1 as query_user,
|
||||
},
|
||||
AppService,
|
||||
};
|
||||
|
||||
pub(crate) type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
|
||||
pub(crate) type AppserviceFn<A, R> =
|
||||
Box<dyn FnMut(AppService, A) -> BoxFuture<'static, R> + Send + Sync + 'static>;
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct EventHandler {
|
||||
pub users: Arc<Mutex<Option<AppserviceFn<query_user::Request, bool>>>>,
|
||||
pub rooms: Arc<Mutex<Option<AppserviceFn<query_room::Request, bool>>>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for EventHandler {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut debug = f.debug_struct("EventHandler");
|
||||
match self.users.try_lock() {
|
||||
Ok(lock) => debug.field("users", &lock.is_some()),
|
||||
Err(_) => debug.field("users", &format_args!("<locked>")),
|
||||
};
|
||||
match self.rooms.try_lock() {
|
||||
Ok(lock) => debug.field("rooms", &lock.is_some()),
|
||||
Err(_) => debug.field("rooms", &format_args!("<locked>")),
|
||||
};
|
||||
debug.finish()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,124 +0,0 @@
|
||||
// Copyright 2022 Famedly GmbH
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! AppService Registration.
|
||||
|
||||
use std::{fs::File, ops::Deref, path::PathBuf};
|
||||
|
||||
use http::Uri;
|
||||
use regex::Regex;
|
||||
use ruma::api::appservice::Registration;
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
pub type Host = String;
|
||||
pub type Port = u16;
|
||||
|
||||
/// AppService Registration
|
||||
///
|
||||
/// Wrapper around [`Registration`]. See also <https://matrix.org/docs/spec/application_service/r0.1.2#registration>.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AppServiceRegistration {
|
||||
inner: Registration,
|
||||
}
|
||||
|
||||
impl AppServiceRegistration {
|
||||
/// Try to load registration from yaml string
|
||||
///
|
||||
/// See the fields of [`Registration`] for the required format
|
||||
pub fn try_from_yaml_str(value: impl AsRef<str>) -> Result<Self> {
|
||||
Ok(Self { inner: serde_yaml::from_str(value.as_ref())? })
|
||||
}
|
||||
|
||||
/// Try to load registration from yaml file
|
||||
///
|
||||
/// See the fields of [`Registration`] for the required format
|
||||
pub fn try_from_yaml_file(path: impl Into<PathBuf>) -> Result<Self> {
|
||||
let file = File::open(path.into())?;
|
||||
|
||||
Ok(Self { inner: serde_yaml::from_reader(file)? })
|
||||
}
|
||||
|
||||
/// Get the host and port from the registration URL
|
||||
///
|
||||
/// If no port is found it falls back to scheme defaults: 80 for http and
|
||||
/// 443 for https
|
||||
pub fn get_host_and_port(&self) -> Result<(Host, Port)> {
|
||||
let uri = Uri::try_from(&self.inner.url)?;
|
||||
|
||||
let host = uri.host().ok_or(Error::MissingRegistrationHost)?.to_owned();
|
||||
let port = match uri.port() {
|
||||
Some(port) => Ok(port.as_u16()),
|
||||
None => match uri.scheme_str() {
|
||||
Some("http") => Ok(80),
|
||||
Some("https") => Ok(443),
|
||||
_ => Err(Error::MissingRegistrationPort),
|
||||
},
|
||||
}?;
|
||||
|
||||
Ok((host, port))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Registration> for AppServiceRegistration {
|
||||
fn from(value: Registration) -> Self {
|
||||
Self { inner: value }
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for AppServiceRegistration {
|
||||
type Target = Registration;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache data for the registration namespaces.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NamespaceCache {
|
||||
/// List of user regexes in our namespace
|
||||
pub(crate) users: Vec<Regex>,
|
||||
/// List of alias regexes in our namespace
|
||||
#[allow(dead_code)]
|
||||
aliases: Vec<Regex>,
|
||||
/// List of room id regexes in our namespace
|
||||
#[allow(dead_code)]
|
||||
rooms: Vec<Regex>,
|
||||
}
|
||||
|
||||
impl NamespaceCache {
|
||||
/// Creates a new registration cache from a [`Registration`] value
|
||||
pub fn from_registration(registration: &Registration) -> Result<Self> {
|
||||
let users = registration
|
||||
.namespaces
|
||||
.users
|
||||
.iter()
|
||||
.map(|user| Regex::new(&user.regex))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let aliases = registration
|
||||
.namespaces
|
||||
.aliases
|
||||
.iter()
|
||||
.map(|user| Regex::new(&user.regex))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let rooms = registration
|
||||
.namespaces
|
||||
.rooms
|
||||
.iter()
|
||||
.map(|user| Regex::new(&user.regex))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(NamespaceCache { users, aliases, rooms })
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
// Copyright 2022 Famedly GmbH
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! AppService users.
|
||||
|
||||
use matrix_sdk::{
|
||||
config::RequestConfig,
|
||||
matrix_auth::{Session, SessionTokens},
|
||||
Client, ClientBuildError, ClientBuilder, SessionMeta,
|
||||
};
|
||||
use ruma::{
|
||||
api::client::{session::login, uiaa::UserIdentifier},
|
||||
assign, DeviceId, OwnedDeviceId, UserId,
|
||||
};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{AppService, Result};
|
||||
|
||||
/// Builder for an appservice user
|
||||
#[derive(Debug)]
|
||||
pub struct UserBuilder<'a> {
|
||||
appservice: &'a AppService,
|
||||
localpart: &'a str,
|
||||
device_id: Option<OwnedDeviceId>,
|
||||
client_builder: ClientBuilder,
|
||||
log_in: bool,
|
||||
restored_session: Option<Session>,
|
||||
}
|
||||
|
||||
impl<'a> UserBuilder<'a> {
|
||||
/// Create a new appservice user builder
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `localpart` - The localpart of the appservice user
|
||||
pub fn new(appservice: &'a AppService, localpart: &'a str) -> Self {
|
||||
Self {
|
||||
appservice,
|
||||
localpart,
|
||||
device_id: None,
|
||||
client_builder: Client::builder(),
|
||||
log_in: false,
|
||||
restored_session: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the device ID of the appservice user
|
||||
pub fn device_id(mut self, device_id: Option<OwnedDeviceId>) -> Self {
|
||||
self.device_id = device_id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the client builder to use for the appservice user
|
||||
pub fn client_builder(mut self, client_builder: ClientBuilder) -> Self {
|
||||
self.client_builder = client_builder;
|
||||
self
|
||||
}
|
||||
|
||||
/// Log in as the appservice user
|
||||
///
|
||||
/// In some cases it is necessary to log in as the user, such as to
|
||||
/// upload device keys
|
||||
pub fn login(mut self) -> Self {
|
||||
self.log_in = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Restore a persisted session
|
||||
///
|
||||
/// This is primarily useful if you enable
|
||||
/// [`UserBuilder::login()`] and want to restore a session
|
||||
/// from a previous run.
|
||||
pub fn restored_session(mut self, session: Session) -> Self {
|
||||
self.restored_session = Some(session);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the appservice user
|
||||
///
|
||||
/// # Errors
|
||||
/// This function returns an error if an invalid localpart is provided.
|
||||
pub async fn build(self) -> Result<Client> {
|
||||
if let Some(client) = self.appservice.clients.get(self.localpart) {
|
||||
return Ok(client.clone());
|
||||
}
|
||||
|
||||
let user_id = UserId::parse_with_server_name(self.localpart, &self.appservice.server_name)?;
|
||||
if !(self.appservice.user_id_is_in_namespace(&user_id)
|
||||
|| self.localpart == self.appservice.registration.sender_localpart)
|
||||
{
|
||||
warn!("Client id '{user_id}' is not in the namespace")
|
||||
}
|
||||
|
||||
let mut builder = self.client_builder;
|
||||
|
||||
if !self.log_in && self.localpart != self.appservice.registration.sender_localpart {
|
||||
builder = builder.assert_identity();
|
||||
}
|
||||
|
||||
let client = builder
|
||||
.homeserver_url(self.appservice.homeserver_url.clone())
|
||||
.appservice_mode()
|
||||
.build()
|
||||
.await
|
||||
.map_err(ClientBuildError::assert_valid_builder_args)?;
|
||||
|
||||
let session = if let Some(session) = self.restored_session {
|
||||
session
|
||||
} else if self.log_in && self.localpart != self.appservice.registration.sender_localpart {
|
||||
let login_info =
|
||||
login::v3::LoginInfo::ApplicationService(login::v3::ApplicationService::new(
|
||||
UserIdentifier::UserIdOrLocalpart(self.localpart.to_owned()),
|
||||
));
|
||||
|
||||
let request = assign!(login::v3::Request::new(login_info), {
|
||||
device_id: self.device_id,
|
||||
initial_device_display_name: None,
|
||||
});
|
||||
|
||||
let response =
|
||||
client.send(request, Some(RequestConfig::short_retry().force_auth())).await?;
|
||||
|
||||
Session::from(&response)
|
||||
} else {
|
||||
// Don’t log in
|
||||
Session {
|
||||
meta: SessionMeta {
|
||||
user_id: user_id.clone(),
|
||||
device_id: self.device_id.unwrap_or_else(DeviceId::new),
|
||||
},
|
||||
tokens: SessionTokens {
|
||||
access_token: self.appservice.registration.as_token.clone(),
|
||||
refresh_token: None,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
client.restore_session(session).await?;
|
||||
|
||||
self.appservice.clients.insert(self.localpart.to_owned(), client.clone());
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
// Copyright 2021 Famedly GmbH
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
future::Future,
|
||||
net::ToSocketAddrs,
|
||||
pin::Pin,
|
||||
task::{self, Poll},
|
||||
};
|
||||
|
||||
use axum::{
|
||||
async_trait,
|
||||
body::{Bytes, HttpBody},
|
||||
extract::{FromRequest, FromRequestParts, Path},
|
||||
middleware::{self, Next},
|
||||
response::{ErrorResponse, IntoResponse, Response},
|
||||
routing::{future::RouteFuture, get, put},
|
||||
BoxError, Extension, Json, Router, ServiceExt,
|
||||
};
|
||||
use http::StatusCode;
|
||||
use hyper::Body;
|
||||
use matrix_sdk::ruma::api::IncomingRequest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tower::{Service, ServiceBuilder};
|
||||
|
||||
use crate::{AppService, Error, Result};
|
||||
|
||||
pub async fn run_server(
|
||||
appservice: AppService,
|
||||
host: impl Into<String>,
|
||||
port: impl Into<u16>,
|
||||
) -> Result<()> {
|
||||
let router: AppServiceRouter = router(appservice);
|
||||
|
||||
let mut addr = (host.into(), port.into()).to_socket_addrs()?;
|
||||
if let Some(addr) = addr.next() {
|
||||
hyper::Server::bind(&addr).serve(router.into_make_service()).await?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::HostPortToSocketAddrs)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn router<B>(appservice: AppService) -> AppServiceRouter<B>
|
||||
where
|
||||
B: HttpBody + Send + 'static,
|
||||
B::Data: Send,
|
||||
B::Error: Into<BoxError>,
|
||||
{
|
||||
AppServiceRouter(
|
||||
Router::new()
|
||||
.route("/_matrix/app/v1/users/:user_id", get(handlers::user))
|
||||
.route("/_matrix/app/v1/rooms/:room_id", get(handlers::room))
|
||||
.route("/_matrix/app/v1/transactions/:txn_id", put(handlers::transaction))
|
||||
.route("/users/:user_id", get(handlers::user))
|
||||
.route("/rooms/:room_id", get(handlers::room))
|
||||
.route("/transactions/:txn_id", put(handlers::transaction))
|
||||
// FIXME: Use Route::with_state instead of an Extension layer in axum 0.6
|
||||
.layer(
|
||||
ServiceBuilder::new()
|
||||
.layer(Extension(appservice))
|
||||
.layer(middleware::from_fn(validate_access_token)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AppServiceRouter<B = Body>(Router<(), B>);
|
||||
|
||||
impl<B> Clone for AppServiceRouter<B> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> Service<http::Request<B>> for AppServiceRouter<B>
|
||||
where
|
||||
B: HttpBody + Send + 'static,
|
||||
B::Data: Send,
|
||||
B::Error: Into<BoxError>,
|
||||
{
|
||||
// axum's Response type is part of the signature because axum::Router::nest
|
||||
// requires the inner service to have that exact response (body) type in
|
||||
// 0.5.x; this will be fixed in axum 0.6.0.
|
||||
type Response = Response;
|
||||
type Error = Infallible;
|
||||
type Future = AppServiceRouteFuture<B>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut task::Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.0.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, mut req: http::Request<B>) -> Self::Future {
|
||||
// When the AppServiceRouter is nested inside another axum Router under
|
||||
// a path that includes path parameters, those should not be received by
|
||||
// the Path extractor used inside the `handlers` module.
|
||||
req.extensions_mut().clear();
|
||||
|
||||
AppServiceRouteFuture(self.0.call(req))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AppServiceRouteFuture<B>(RouteFuture<B, Infallible>);
|
||||
|
||||
impl<B> Future for AppServiceRouteFuture<B>
|
||||
where
|
||||
B: HttpBody,
|
||||
{
|
||||
type Output = Result<Response, Infallible>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<Self::Output> {
|
||||
Pin::new(&mut self.0).poll(cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MatrixRequest<T>(T);
|
||||
|
||||
#[async_trait]
|
||||
impl<S, B, T> FromRequest<S, B> for MatrixRequest<T>
|
||||
where
|
||||
S: Send + Sync,
|
||||
B: HttpBody + Send + 'static,
|
||||
B::Data: Send,
|
||||
B::Error: Into<BoxError>,
|
||||
T: IncomingRequest,
|
||||
{
|
||||
type Rejection = Response;
|
||||
|
||||
async fn from_request(
|
||||
req: http::request::Request<B>,
|
||||
state: &S,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let (mut parts, body) = req.into_parts();
|
||||
let path_params = Path::<Vec<String>>::from_request_parts(&mut parts, state)
|
||||
.await
|
||||
.map_err(IntoResponse::into_response)?;
|
||||
let bytes = Bytes::from_request(http::Request::new(body), state)
|
||||
.await
|
||||
.map_err(IntoResponse::into_response)?;
|
||||
let http_request = http::Request::from_parts(parts, bytes);
|
||||
|
||||
let request = T::try_from_http_request(http_request, &path_params).map_err(|_e| {
|
||||
// TODO: JSON error response
|
||||
StatusCode::BAD_REQUEST.into_response()
|
||||
})?;
|
||||
|
||||
Ok(Self(request))
|
||||
}
|
||||
}
|
||||
|
||||
mod handlers {
|
||||
use axum::{response::IntoResponse, Extension, Json};
|
||||
use http::StatusCode;
|
||||
use ruma::api::appservice::{
|
||||
event::push_events,
|
||||
query::{query_room_alias, query_user_id},
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
use super::{ErrorMessage, MatrixRequest};
|
||||
use crate::AppService;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct EmptyObject {}
|
||||
|
||||
pub async fn user(
|
||||
Extension(appservice): Extension<AppService>,
|
||||
MatrixRequest(request): MatrixRequest<query_user_id::v1::Request>,
|
||||
) -> impl IntoResponse {
|
||||
if let Some(user_exists) = appservice.event_handler.users.lock().await.as_mut() {
|
||||
if user_exists(appservice.clone(), request).await {
|
||||
Ok(Json(EmptyObject {}))
|
||||
} else {
|
||||
Err(StatusCode::NOT_FOUND)
|
||||
}
|
||||
} else {
|
||||
Ok(Json(EmptyObject {}))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn room(
|
||||
Extension(appservice): Extension<AppService>,
|
||||
MatrixRequest(request): MatrixRequest<query_room_alias::v1::Request>,
|
||||
) -> impl IntoResponse {
|
||||
if let Some(room_exists) = appservice.event_handler.rooms.lock().await.as_mut() {
|
||||
if room_exists(appservice.clone(), request).await {
|
||||
Ok(Json(&EmptyObject {}))
|
||||
} else {
|
||||
Err(StatusCode::NOT_FOUND)
|
||||
}
|
||||
} else {
|
||||
Ok(Json(&EmptyObject {}))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn transaction(
|
||||
appservice: Extension<AppService>,
|
||||
MatrixRequest(request): MatrixRequest<push_events::v1::Request>,
|
||||
) -> impl IntoResponse {
|
||||
match appservice.receive_transaction(request).await {
|
||||
Ok(_) => Ok(Json(&EmptyObject {})),
|
||||
Err(e) => {
|
||||
let status_code = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
Err((
|
||||
status_code,
|
||||
Json(ErrorMessage { code: status_code.as_u16(), message: e.to_string() }),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct QueryParameters {
|
||||
access_token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ErrorMessage {
|
||||
code: u16,
|
||||
message: String,
|
||||
}
|
||||
|
||||
async fn validate_access_token<B>(
|
||||
req: http::Request<B>,
|
||||
next: Next<B>,
|
||||
) -> Result<Response, ErrorResponse> {
|
||||
let appservice =
|
||||
req.extensions().get::<AppService>().ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let query_string = req.uri().query().unwrap_or("");
|
||||
match serde_html_form::from_str::<QueryParameters>(query_string) {
|
||||
Ok(query) if query.access_token == appservice.registration.hs_token => {
|
||||
Ok(next.run(req).await)
|
||||
}
|
||||
_ => {
|
||||
let status_code = StatusCode::UNAUTHORIZED;
|
||||
let message =
|
||||
ErrorMessage { code: status_code.as_u16(), message: "UNAUTHORIZED".into() };
|
||||
Err((status_code, Json(message)).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
id: appservice
|
||||
url: http://localhost:9009
|
||||
as_token: as_token
|
||||
hs_token: hs_token
|
||||
sender_localpart: _appservice
|
||||
namespaces:
|
||||
aliases: []
|
||||
rooms: []
|
||||
users:
|
||||
- exclusive: true
|
||||
regex: '@_appservice_.*'
|
||||
rate_limited: false
|
||||
protocols: []
|
||||
@@ -1,18 +0,0 @@
|
||||
[package]
|
||||
name = "example-appservice-autojoin"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "example-appservice-autojoin"
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread"] }
|
||||
tracing-subscriber = "0.3.15"
|
||||
tracing = { workspace = true }
|
||||
# when copy-pasting this, please use a git dependency or make sure that you
|
||||
# have copied the example as it was at the time of the release you use.
|
||||
matrix-sdk-appservice = { path = "../../crates/matrix-sdk-appservice" }
|
||||
@@ -1,13 +0,0 @@
|
||||
id: appservice
|
||||
url: http://localhost:9009
|
||||
as_token: as_token
|
||||
hs_token: hs_token
|
||||
sender_localpart: _appservice
|
||||
namespaces:
|
||||
aliases: []
|
||||
rooms: []
|
||||
users:
|
||||
- exclusive: true
|
||||
regex: '@_appservice_.*'
|
||||
rate_limited: false
|
||||
protocols: []
|
||||
@@ -1,80 +0,0 @@
|
||||
use std::env;
|
||||
|
||||
use matrix_sdk_appservice::{
|
||||
matrix_sdk::{
|
||||
event_handler::Ctx,
|
||||
ruma::{
|
||||
events::room::member::{MembershipState, OriginalSyncRoomMemberEvent},
|
||||
UserId,
|
||||
},
|
||||
Room,
|
||||
},
|
||||
ruma::api::client::error::ErrorKind,
|
||||
AppService, AppServiceBuilder, AppServiceRegistration, Result,
|
||||
};
|
||||
use tracing::trace;
|
||||
|
||||
pub async fn handle_room_member(
|
||||
appservice: AppService,
|
||||
room: Room,
|
||||
event: OriginalSyncRoomMemberEvent,
|
||||
) -> Result<()> {
|
||||
if !appservice.user_id_is_in_namespace(&event.state_key) {
|
||||
trace!("not an appservice user: {}", event.state_key);
|
||||
} else if let MembershipState::Invite = event.content.membership {
|
||||
let user_id = UserId::parse(event.state_key.as_str())?;
|
||||
if let Err(error) = appservice.register_user(user_id.localpart(), None).await {
|
||||
error_if_user_not_in_use(error)?;
|
||||
}
|
||||
|
||||
let client = appservice.user(Some(user_id.localpart())).await?;
|
||||
client.join_room_by_id(room.room_id()).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn error_if_user_not_in_use(error: matrix_sdk_appservice::Error) -> Result<()> {
|
||||
// FIXME: Use if-let chain once available
|
||||
match &error {
|
||||
// If user is already in use that's OK.
|
||||
matrix_sdk_appservice::Error::Matrix(err)
|
||||
if err.client_api_error_kind() == Some(&ErrorKind::UserInUse) =>
|
||||
{
|
||||
Ok(())
|
||||
}
|
||||
// In all other cases return with an error.
|
||||
_ => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn main() -> anyhow::Result<()> {
|
||||
env::set_var("RUST_LOG", "matrix_sdk=debug,matrix_sdk_appservice=debug");
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let homeserver_url = "http://localhost:8008";
|
||||
let server_name = "localhost";
|
||||
let registration =
|
||||
AppServiceRegistration::try_from_yaml_file("./appservice-registration.yaml")?;
|
||||
let appservice =
|
||||
AppServiceBuilder::new(homeserver_url.parse()?, server_name.parse()?, registration)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
appservice.register_user_query(Box::new(|_, _| Box::pin(async { true }))).await;
|
||||
|
||||
let user = appservice.user(None).await?;
|
||||
|
||||
user.add_event_handler_context(appservice.clone());
|
||||
user.add_event_handler(
|
||||
move |event: OriginalSyncRoomMemberEvent, room: Room, Ctx(appservice): Ctx<AppService>| {
|
||||
handle_room_member(appservice, room, event)
|
||||
},
|
||||
);
|
||||
|
||||
let (host, port) = appservice.registration().get_host_and_port()?;
|
||||
appservice.run(host, port).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -35,9 +35,6 @@ enum CiCommand {
|
||||
cmd: Option<FeatureSet>,
|
||||
},
|
||||
|
||||
/// Run tests for the appservice crate
|
||||
TestAppservice,
|
||||
|
||||
/// Run checks for the wasm target
|
||||
Wasm {
|
||||
#[clap(subcommand)]
|
||||
@@ -97,7 +94,6 @@ impl CiArgs {
|
||||
CiCommand::Clippy => check_clippy(),
|
||||
CiCommand::Docs => check_docs(),
|
||||
CiCommand::TestFeatures { cmd } => run_feature_tests(cmd),
|
||||
CiCommand::TestAppservice => run_appservice_tests(),
|
||||
CiCommand::Wasm { cmd } => run_wasm_checks(cmd),
|
||||
CiCommand::WasmPack { cmd } => run_wasm_pack_tests(cmd),
|
||||
CiCommand::TestCrypto => run_crypto_tests(),
|
||||
@@ -110,7 +106,6 @@ impl CiArgs {
|
||||
check_typos()?;
|
||||
check_docs()?;
|
||||
run_feature_tests(None)?;
|
||||
run_appservice_tests()?;
|
||||
run_wasm_checks(None)?;
|
||||
run_crypto_tests()?;
|
||||
check_examples()?;
|
||||
@@ -265,14 +260,6 @@ fn run_crypto_tests() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_appservice_tests() -> Result<()> {
|
||||
cmd!("rustup run stable cargo clippy -p matrix-sdk-appservice -- -D warnings").run()?;
|
||||
cmd!("rustup run stable cargo nextest run -p matrix-sdk-appservice").run()?;
|
||||
cmd!("rustup run stable cargo test --doc -p matrix-sdk-appservice").run()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_wasm_checks(cmd: Option<WasmFeatureSet>) -> Result<()> {
|
||||
if let Some(WasmFeatureSet::Indexeddb) = cmd {
|
||||
run_wasm_checks(Some(WasmFeatureSet::IndexeddbNoCrypto))?;
|
||||
|
||||
Reference in New Issue
Block a user