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:
Jonas Platte
2023-09-05 14:53:11 +02:00
committed by Jonas Platte
parent 11b3be1a03
commit 7d674b39aa
15 changed files with 0 additions and 2027 deletions

View File

@@ -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
View File

@@ -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"

View File

@@ -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/"

View File

@@ -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"

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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 })
}
}

View File

@@ -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 {
// Dont 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)
}
}

View File

@@ -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())
}
}
}

View File

@@ -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: []

View File

@@ -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" }

View File

@@ -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: []

View File

@@ -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(())
}

View File

@@ -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))?;