Compare commits

..

2 commits

Author SHA1 Message Date
Jade Ellis
791ab68d8a
feat: Generate admin command documentation
The first part of getting admin command docs on the website.

There's also the beginnings of manpage generation here, although it's
kinda sus and I'm not sure how it's supposed to work. I'll leave that to
anyone who wants to package it.

We introduce the beginings of the xtask pattern here - we do a lot of
file generation, I thought it would be best to avoid doing that on every
compilation. It also helps avoid lots of runtime deps.

We'll need to document generating this stuff & probably add pre-commit
hooks for it, though.
2025-07-02 22:31:51 +01:00
Jade Ellis
2b938c7d11
feat: Support logging to journald with tracing-journald
This stubs out on non-unix platforms.
2025-07-02 21:28:51 +01:00
136 changed files with 6935 additions and 7210 deletions

View file

@ -11,16 +11,16 @@ concurrency:
jobs:
build-and-deploy:
name: 🏗️ Build and Deploy
name: Build and Deploy Element Web
runs-on: ubuntu-latest
steps:
- name: 📦 Setup Node.js
uses: https://github.com/actions/setup-node@v4
- name: Setup Node.js
uses: https://code.forgejo.org/actions/setup-node@v4
with:
node-version: "22"
node-version: "20"
- name: 🔨 Clone, setup, and build Element Web
- name: Clone, setup, and build Element Web
run: |
echo "Cloning Element Web..."
git clone https://github.com/maunium/element-web
@ -64,7 +64,7 @@ jobs:
echo "Checking for build output..."
ls -la webapp/
- name: ⚙️ Create config.json
- name: Create config.json
run: |
cat <<EOF > ./element-web/webapp/config.json
{
@ -100,25 +100,28 @@ jobs:
echo "Created ./element-web/webapp/config.json"
cat ./element-web/webapp/config.json
- name: 📤 Upload Artifact
- name: Upload Artifact
uses: https://code.forgejo.org/actions/upload-artifact@v3
with:
name: element-web
path: ./element-web/webapp/
retention-days: 14
- name: 🛠️ Install Wrangler
- name: Install Wrangler
run: npm install --save-dev wrangler@latest
- name: 🚀 Deploy to Cloudflare Pages
if: vars.CLOUDFLARE_PROJECT_NAME != ''
id: deploy
- name: Deploy to Cloudflare Pages (Production)
if: github.ref == 'refs/heads/main' && vars.CLOUDFLARE_PROJECT_NAME != ''
uses: https://github.com/cloudflare/wrangler-action@v3
with:
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: >-
pages deploy ./element-web/webapp
--branch="${{ github.ref == 'refs/heads/main' && 'main' || github.head_ref || github.ref_name }}"
--commit-dirty=true
--project-name="${{ vars.CLOUDFLARE_PROJECT_NAME }}-element"
command: pages deploy ./element-web/webapp --branch="main" --commit-dirty=true --project-name="${{ vars.CLOUDFLARE_PROJECT_NAME }}-element"
- name: Deploy to Cloudflare Pages (Preview)
if: github.ref != 'refs/heads/main' && vars.CLOUDFLARE_PROJECT_NAME != ''
uses: https://github.com/cloudflare/wrangler-action@v3
with:
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: pages deploy ./element-web/webapp --branch="${{ github.head_ref || github.ref_name }}" --commit-dirty=true --project-name="${{ vars.CLOUDFLARE_PROJECT_NAME }}-element"

View file

@ -1218,8 +1218,8 @@ Respecting homeservers put this file here for listing administration, moderation
* `echo` — - Echo input of admin command
* `get-auth-chain` — - Get the auth_chain of a PDU
* `parse-pdu` — - Parse and print a PDU from a JSON
* `get-pdu` — - Retrieve and print a PDU by EventID from the Continuwuity database
* `get-short-pdu` — - Retrieve and print a PDU by PduId from the Continuwuity database
* `get-pdu` — - Retrieve and print a PDU by EventID from the conduwuit database
* `get-short-pdu` — - Retrieve and print a PDU by PduId from the conduwuit database
* `get-remote-pdu` — - Attempts to retrieve a PDU from a remote server. Inserts it into our database/timeline if found and we do not have this PDU already (following normal event auth rules, handles it as an incoming PDU)
* `get-remote-pdu-list` — - Same as `get-remote-pdu` but accepts a codeblock newline delimited list of PDUs and a single server to fetch from
* `get-room-state` — - Gets all the room state events for the specified room
@ -1234,7 +1234,7 @@ Respecting homeservers put this file here for listing administration, moderation
* `first-pdu-in-room` — - Prints the very first PDU in the specified room (typically m.room.create)
* `latest-pdu-in-room` — - Prints the latest ("last") PDU in the specified room (typically a message)
* `force-set-room-state-from-server` — - Forcefully replaces the room state of our local copy of the specified room, with the copy (auth chain and room state events) the specified remote server says
* `resolve-true-destination` — - Runs a server name through Continuwuity's true destination resolution process
* `resolve-true-destination` — - Runs a server name through conduwuit's true destination resolution process
* `memory-stats` — - Print extended memory usage
* `runtime-metrics` — - Print general tokio runtime metric totals
* `runtime-interval` — - Print detailed tokio runtime metrics accumulated since last command invocation
@ -1284,7 +1284,7 @@ This command needs a JSON blob provided in a Markdown code block below the comma
## `admin debug get-pdu`
- Retrieve and print a PDU by EventID from the Continuwuity database
- Retrieve and print a PDU by EventID from the conduwuit database
**Usage:** `admin debug get-pdu <EVENT_ID>`
@ -1296,7 +1296,7 @@ This command needs a JSON blob provided in a Markdown code block below the comma
## `admin debug get-short-pdu`
- Retrieve and print a PDU by PduId from the Continuwuity database
- Retrieve and print a PDU by PduId from the conduwuit database
**Usage:** `admin debug get-short-pdu <SHORTROOMID> <SHORTEVENTID>`
@ -1497,7 +1497,7 @@ This command will get the latest PDU in the room we know about, and request the
## `admin debug resolve-true-destination`
- Runs a server name through Continuwuity's true destination resolution process
- Runs a server name through conduwuit's true destination resolution process
Useful for debugging well-known issues

View file

@ -7,10 +7,7 @@ use std::{
use conduwuit::{
Err, Result, debug_error, err, info,
matrix::{
Event,
pdu::{PduEvent, PduId, RawPduId},
},
matrix::pdu::{PduEvent, PduId, RawPduId},
trace, utils,
utils::{
stream::{IterStream, ReadyExt},
@ -22,7 +19,7 @@ use futures::{FutureExt, StreamExt, TryStreamExt};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId,
OwnedRoomOrAliasId, OwnedServerName, RoomId, RoomVersionId,
api::federation::event::get_room_state, events::AnyStateEvent, serde::Raw,
api::federation::event::get_room_state,
};
use service::rooms::{
short::{ShortEventId, ShortRoomId},
@ -299,12 +296,12 @@ pub(super) async fn get_remote_pdu(
#[admin_command]
pub(super) async fn get_room_state(&self, room: OwnedRoomOrAliasId) -> Result {
let room_id = self.services.rooms.alias.resolve(&room).await?;
let room_state: Vec<Raw<AnyStateEvent>> = self
let room_state: Vec<_> = self
.services
.rooms
.state_accessor
.room_state_full_pdus(&room_id)
.map_ok(Event::into_format)
.map_ok(PduEvent::into_state_event)
.try_collect()
.await?;
@ -412,9 +409,7 @@ pub(super) async fn change_log_level(&self, filter: Option<String>, reset: bool)
.reload
.reload(&new_filter_layer, Some(handles))
{
| Ok(()) => {
return self.write_str("Successfully changed log level").await;
},
| Ok(()) => return self.write_str("Successfully changed log level").await,
| Err(e) => {
return Err!("Failed to modify and reload the global tracing log level: {e}");
},
@ -558,8 +553,8 @@ pub(super) async fn force_set_room_state_from_server(
.latest_pdu_in_room(&room_id)
.await
.map_err(|_| err!(Database("Failed to find the latest PDU in database")))?
.event_id()
.to_owned(),
.event_id
.clone(),
};
let room_version = self.services.rooms.state.get_room_version(&room_id).await?;

View file

@ -32,13 +32,13 @@ pub enum DebugCommand {
/// the command.
ParsePdu,
/// - Retrieve and print a PDU by EventID from the Continuwuity database
/// - Retrieve and print a PDU by EventID from the conduwuit database
GetPdu {
/// An event ID (a $ followed by the base64 reference hash)
event_id: OwnedEventId,
},
/// - Retrieve and print a PDU by PduId from the Continuwuity database
/// - Retrieve and print a PDU by PduId from the conduwuit database
GetShortPdu {
/// Shortroomid integer
shortroomid: ShortRoomId,
@ -182,7 +182,7 @@ pub enum DebugCommand {
event_id: Option<OwnedEventId>,
},
/// - Runs a server name through Continuwuity's true destination resolution
/// - Runs a server name through conduwuit's true destination resolution
/// process
///
/// Useful for debugging well-known issues

View file

@ -94,7 +94,8 @@ async fn process_command(services: Arc<Services>, input: &CommandInput) -> Proce
#[allow(clippy::result_large_err)]
fn handle_panic(error: &Error, command: &CommandInput) -> ProcessorResult {
let link = "Please submit a [bug report](https://forgejo.ellis.link/continuwuation/continuwuity/issues/new). 🥺";
let link =
"Please submit a [bug report](https://forgejo.ellis.link/continuwuation/continuwuity/issues/new). 🥺";
let msg = format!("Panic occurred while processing command:\n```\n{error:#?}\n```\n{link}");
let content = RoomMessageEventContent::notice_markdown(msg);
error!("Panic while processing command: {error:?}");

View file

@ -5,7 +5,7 @@ use conduwuit::{
utils::{IterStream, ReadyExt},
warn,
};
use futures::{FutureExt, StreamExt};
use futures::StreamExt;
use ruma::{OwnedRoomId, OwnedRoomOrAliasId, RoomAliasId, RoomId, RoomOrAliasId};
use crate::{admin_command, admin_command_dispatch, get_room_info};
@ -132,10 +132,7 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
evicting admins too)",
);
if let Err(e) = leave_room(self.services, user_id, &room_id, None)
.boxed()
.await
{
if let Err(e) = leave_room(self.services, user_id, &room_id, None).await {
warn!("Failed to leave room: {e}");
}
@ -300,10 +297,7 @@ async fn ban_list_of_rooms(&self) -> Result {
evicting admins too)",
);
if let Err(e) = leave_room(self.services, user_id, &room_id, None)
.boxed()
.await
{
if let Err(e) = leave_room(self.services, user_id, &room_id, None).await {
warn!("Failed to leave room: {e}");
}

View file

@ -1,16 +1,14 @@
use std::{collections::BTreeMap, fmt::Write as _};
use api::client::{
full_user_deactivate, join_room_by_id_helper, leave_all_rooms, leave_room, update_avatar_url,
update_displayname,
};
use api::client::{full_user_deactivate, join_room_by_id_helper, leave_room};
use conduwuit::{
Err, Result, debug, debug_warn, error, info, is_equal_to,
matrix::{Event, pdu::PduBuilder},
matrix::pdu::PduBuilder,
utils::{self, ReadyExt},
warn,
};
use futures::{FutureExt, StreamExt};
use conduwuit_api::client::{leave_all_rooms, update_avatar_url, update_displayname};
use futures::StreamExt;
use ruma::{
OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId, UserId,
events::{
@ -286,9 +284,8 @@ pub(super) async fn reset_password(&self, username: String, password: Option<Str
.set_password(&user_id, Some(new_password.as_str()))
{
| Err(e) => return Err!("Couldn't reset the password for user {user_id}: {e}"),
| Ok(()) => {
write!(self, "Successfully reset the password for user {user_id}: `{new_password}`")
},
| Ok(()) =>
write!(self, "Successfully reset the password for user {user_id}: `{new_password}`"),
}
.await
}
@ -699,9 +696,7 @@ pub(super) async fn force_leave_room(
return Err!("{user_id} is not joined in the room");
}
leave_room(self.services, &user_id, &room_id, None)
.boxed()
.await?;
leave_room(self.services, &user_id, &room_id, None).await?;
self.write_str(&format!("{user_id} has left {room_id}.",))
.await
@ -738,7 +733,7 @@ pub(super) async fn force_demote(&self, user_id: String, room_id: OwnedRoomOrAli
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomCreate, "")
.await
.is_ok_and(|event| event.sender() == user_id);
.is_ok_and(|event| event.sender == user_id);
if !user_can_demote_self {
return Err!("User is not allowed to modify their own power levels in the room.",);
@ -889,7 +884,10 @@ pub(super) async fn redact_event(&self, event_id: OwnedEventId) -> Result {
return Err!("Event is already redacted.");
}
if !self.services.globals.user_is_local(event.sender()) {
let room_id = event.room_id;
let sender_user = event.sender;
if !self.services.globals.user_is_local(&sender_user) {
return Err!("This command only works on local users.");
}
@ -899,21 +897,21 @@ pub(super) async fn redact_event(&self, event_id: OwnedEventId) -> Result {
);
let redaction_event_id = {
let state_lock = self.services.rooms.state.mutex.lock(event.room_id()).await;
let state_lock = self.services.rooms.state.mutex.lock(&room_id).await;
self.services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
redacts: Some(event.event_id().to_owned()),
redacts: Some(event.event_id.clone()),
..PduBuilder::timeline(&RoomRedactionEventContent {
redacts: Some(event.event_id().to_owned()),
redacts: Some(event.event_id.clone()),
reason: Some(reason),
})
},
event.sender(),
event.room_id(),
&sender_user,
&room_id,
&state_lock,
)
.await?

View file

@ -3,9 +3,10 @@ use std::fmt::Write;
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
Err, Error, Event, Result, debug_info, err, error, info, is_equal_to,
Err, Error, Result, debug_info, err, error, info, is_equal_to,
matrix::pdu::PduBuilder,
utils::{self, ReadyExt, stream::BroadbandExt},
utils,
utils::{ReadyExt, stream::BroadbandExt},
warn,
};
use conduwuit_service::Services;
@ -150,32 +151,16 @@ pub(crate) async fn register_route(
if !services.config.allow_registration && body.appservice_info.is_none() {
match (body.username.as_ref(), body.initial_device_display_name.as_ref()) {
| (Some(username), Some(device_display_name)) => {
info!(
%is_guest,
user = %username,
device_name = %device_display_name,
"Rejecting registration attempt as registration is disabled"
);
info!(%is_guest, user = %username, device_name = %device_display_name, "Rejecting registration attempt as registration is disabled");
},
| (Some(username), _) => {
info!(
%is_guest,
user = %username,
"Rejecting registration attempt as registration is disabled"
);
info!(%is_guest, user = %username, "Rejecting registration attempt as registration is disabled");
},
| (_, Some(device_display_name)) => {
info!(
%is_guest,
device_name = %device_display_name,
"Rejecting registration attempt as registration is disabled"
);
info!(%is_guest, device_name = %device_display_name, "Rejecting registration attempt as registration is disabled");
},
| (None, _) => {
info!(
%is_guest,
"Rejecting registration attempt as registration is disabled"
);
info!(%is_guest, "Rejecting registration attempt as registration is disabled");
},
}
@ -366,7 +351,8 @@ pub(crate) async fn register_route(
if !services.globals.new_user_displayname_suffix().is_empty()
&& body.appservice_info.is_none()
{
write!(displayname, " {}", services.server.config.new_user_displayname_suffix)?;
write!(displayname, " {}", services.server.config.new_user_displayname_suffix)
.expect("should be able to write to string buffer");
}
services
@ -384,7 +370,8 @@ pub(crate) async fn register_route(
content: ruma::events::push_rules::PushRulesEventContent {
global: push::Ruleset::server_default(&user_id),
},
})?,
})
.expect("to json always works"),
)
.await?;
@ -429,21 +416,32 @@ pub(crate) async fn register_route(
// log in conduit admin channel if a non-guest user registered
if body.appservice_info.is_none() && !is_guest {
if !device_display_name.is_empty() {
let notice = format!(
"New user \"{user_id}\" registered on this server from IP {client} and device \
display name \"{device_display_name}\""
info!(
"New user \"{user_id}\" registered on this server with device display name: \
\"{device_display_name}\""
);
info!("{notice}");
if services.server.config.admin_room_notices {
services.admin.notice(&notice).await;
services
.admin
.send_message(RoomMessageEventContent::notice_plain(format!(
"New user \"{user_id}\" registered on this server from IP {client} and \
device display name \"{device_display_name}\""
)))
.await
.ok();
}
} else {
let notice = format!("New user \"{user_id}\" registered on this server.");
info!("New user \"{user_id}\" registered on this server.");
info!("{notice}");
if services.server.config.admin_room_notices {
services.admin.notice(&notice).await;
services
.admin
.send_message(RoomMessageEventContent::notice_plain(format!(
"New user \"{user_id}\" registered on this server from IP {client}"
)))
.await
.ok();
}
}
}
@ -456,22 +454,24 @@ pub(crate) async fn register_route(
if services.server.config.admin_room_notices {
services
.admin
.notice(&format!(
.send_message(RoomMessageEventContent::notice_plain(format!(
"Guest user \"{user_id}\" with device display name \
\"{device_display_name}\" registered on this server from IP {client}"
))
.await;
)))
.await
.ok();
}
} else {
#[allow(clippy::collapsible_else_if)]
if services.server.config.admin_room_notices {
services
.admin
.notice(&format!(
.send_message(RoomMessageEventContent::notice_plain(format!(
"Guest user \"{user_id}\" with no device display name registered on \
this server from IP {client}",
))
.await;
)))
.await
.ok();
}
}
}
@ -603,6 +603,7 @@ pub(crate) async fn change_password_route(
.sender_user
.as_ref()
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
let sender_device = body.sender_device();
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
@ -616,7 +617,7 @@ pub(crate) async fn change_password_route(
| Some(auth) => {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, body.sender_device(), auth, &uiaainfo)
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;
if !worked {
@ -630,7 +631,7 @@ pub(crate) async fn change_password_route(
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, body.sender_device(), &uiaainfo, json);
.create(sender_user, sender_device, &uiaainfo, json);
return Err(Error::Uiaa(uiaainfo));
},
@ -649,7 +650,7 @@ pub(crate) async fn change_password_route(
services
.users
.all_device_ids(sender_user)
.ready_filter(|id| *id != body.sender_device())
.ready_filter(|id| *id != sender_device)
.for_each(|id| services.users.remove_device(sender_user, id))
.await;
@ -658,17 +659,17 @@ pub(crate) async fn change_password_route(
.pusher
.get_pushkeys(sender_user)
.map(ToOwned::to_owned)
.broad_filter_map(async |pushkey| {
.broad_filter_map(|pushkey| async move {
services
.pusher
.get_pusher_device(&pushkey)
.await
.ok()
.filter(|pusher_device| pusher_device != body.sender_device())
.filter(|pusher_device| pusher_device != sender_device)
.is_some()
.then_some(pushkey)
})
.for_each(async |pushkey| {
.for_each(|pushkey| async move {
services.pusher.delete_pusher(sender_user, &pushkey).await;
})
.await;
@ -679,8 +680,11 @@ pub(crate) async fn change_password_route(
if services.server.config.admin_room_notices {
services
.admin
.notice(&format!("User {sender_user} changed their password."))
.await;
.send_message(RoomMessageEventContent::notice_plain(format!(
"User {sender_user} changed their password."
)))
.await
.ok();
}
Ok(change_password::v3::Response {})
@ -695,10 +699,13 @@ pub(crate) async fn whoami_route(
State(services): State<crate::State>,
body: Ruma<whoami::v3::Request>,
) -> Result<whoami::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let device_id = body.sender_device.clone();
Ok(whoami::v3::Response {
user_id: body.sender_user().to_owned(),
device_id: body.sender_device.clone(),
is_guest: services.users.is_deactivated(body.sender_user()).await?
user_id: sender_user.clone(),
device_id,
is_guest: services.users.is_deactivated(sender_user).await?
&& body.appservice_info.is_none(),
})
}
@ -726,6 +733,7 @@ pub(crate) async fn deactivate_route(
.sender_user
.as_ref()
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
let sender_device = body.sender_device();
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
@ -739,7 +747,7 @@ pub(crate) async fn deactivate_route(
| Some(auth) => {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, body.sender_device(), auth, &uiaainfo)
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;
if !worked {
@ -752,7 +760,7 @@ pub(crate) async fn deactivate_route(
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, body.sender_device(), &uiaainfo, json);
.create(sender_user, sender_device, &uiaainfo, json);
return Err(Error::Uiaa(uiaainfo));
},
@ -774,17 +782,18 @@ pub(crate) async fn deactivate_route(
super::update_displayname(&services, sender_user, None, &all_joined_rooms).await;
super::update_avatar_url(&services, sender_user, None, None, &all_joined_rooms).await;
full_user_deactivate(&services, sender_user, &all_joined_rooms)
.boxed()
.await?;
full_user_deactivate(&services, sender_user, &all_joined_rooms).await?;
info!("User {sender_user} deactivated their account.");
if services.server.config.admin_room_notices {
services
.admin
.notice(&format!("User {sender_user} deactivated their account."))
.await;
.send_message(RoomMessageEventContent::notice_plain(format!(
"User {sender_user} deactivated their account."
)))
.await
.ok();
}
Ok(deactivate::v3::Response {
@ -861,7 +870,6 @@ pub async fn full_user_deactivate(
all_joined_rooms: &[OwnedRoomId],
) -> Result<()> {
services.users.deactivate_account(user_id).await.ok();
super::update_displayname(services, user_id, None, all_joined_rooms).await;
super::update_avatar_url(services, user_id, None, None, all_joined_rooms).await;
@ -898,7 +906,7 @@ pub async fn full_user_deactivate(
.state_accessor
.room_state_get(room_id, &StateEventType::RoomCreate, "")
.await
.is_ok_and(|event| event.sender() == user_id);
.is_ok_and(|event| event.sender == user_id);
if user_can_demote_self {
let mut power_levels_content = room_power_levels.unwrap_or_default();
@ -926,7 +934,7 @@ pub async fn full_user_deactivate(
}
}
super::leave_all_rooms(services, user_id).boxed().await;
super::leave_all_rooms(services, user_id).await;
Ok(())
}

View file

@ -17,7 +17,7 @@ pub(crate) async fn create_alias_route(
State(services): State<crate::State>,
body: Ruma<create_alias::v3::Request>,
) -> Result<create_alias::v3::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
@ -65,7 +65,7 @@ pub(crate) async fn delete_alias_route(
State(services): State<crate::State>,
body: Ruma<delete_alias::v3::Request>,
) -> Result<delete_alias::v3::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}

View file

@ -2,10 +2,8 @@ use std::cmp::Ordering;
use axum::extract::State;
use conduwuit::{Err, Result, err};
use conduwuit_service::Services;
use futures::{FutureExt, future::try_join};
use ruma::{
UInt, UserId,
UInt,
api::client::backup::{
add_backup_keys, add_backup_keys_for_room, add_backup_keys_for_session,
create_backup_version, delete_backup_keys, delete_backup_keys_for_room,
@ -60,9 +58,21 @@ pub(crate) async fn get_latest_backup_info_route(
.await
.map_err(|_| err!(Request(NotFound("Key backup does not exist."))))?;
let (count, etag) = get_count_etag(&services, body.sender_user(), &version).await?;
Ok(get_latest_backup_info::v3::Response { algorithm, count, etag, version })
Ok(get_latest_backup_info::v3::Response {
algorithm,
count: (UInt::try_from(
services
.key_backups
.count_keys(body.sender_user(), &version)
.await,
)
.expect("user backup keys count should not be that high")),
etag: services
.key_backups
.get_etag(body.sender_user(), &version)
.await,
version,
})
}
/// # `GET /_matrix/client/v3/room_keys/version/{version}`
@ -80,12 +90,17 @@ pub(crate) async fn get_backup_info_route(
err!(Request(NotFound("Key backup does not exist at version {:?}", body.version)))
})?;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
Ok(get_backup_info::v3::Response {
algorithm,
count,
etag,
count: services
.key_backups
.count_keys(body.sender_user(), &body.version)
.await
.try_into()?,
etag: services
.key_backups
.get_etag(body.sender_user(), &body.version)
.await,
version: body.version.clone(),
})
}
@ -140,9 +155,17 @@ pub(crate) async fn add_backup_keys_route(
}
}
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
Ok(add_backup_keys::v3::Response { count, etag })
Ok(add_backup_keys::v3::Response {
count: services
.key_backups
.count_keys(body.sender_user(), &body.version)
.await
.try_into()?,
etag: services
.key_backups
.get_etag(body.sender_user(), &body.version)
.await,
})
}
/// # `PUT /_matrix/client/r0/room_keys/keys/{roomId}`
@ -175,9 +198,17 @@ pub(crate) async fn add_backup_keys_for_room_route(
.await?;
}
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
Ok(add_backup_keys_for_room::v3::Response { count, etag })
Ok(add_backup_keys_for_room::v3::Response {
count: services
.key_backups
.count_keys(body.sender_user(), &body.version)
.await
.try_into()?,
etag: services
.key_backups
.get_etag(body.sender_user(), &body.version)
.await,
})
}
/// # `PUT /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
@ -275,9 +306,17 @@ pub(crate) async fn add_backup_keys_for_session_route(
.await?;
}
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
Ok(add_backup_keys_for_session::v3::Response { count, etag })
Ok(add_backup_keys_for_session::v3::Response {
count: services
.key_backups
.count_keys(body.sender_user(), &body.version)
.await
.try_into()?,
etag: services
.key_backups
.get_etag(body.sender_user(), &body.version)
.await,
})
}
/// # `GET /_matrix/client/r0/room_keys/keys`
@ -340,9 +379,17 @@ pub(crate) async fn delete_backup_keys_route(
.delete_all_keys(body.sender_user(), &body.version)
.await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
Ok(delete_backup_keys::v3::Response { count, etag })
Ok(delete_backup_keys::v3::Response {
count: services
.key_backups
.count_keys(body.sender_user(), &body.version)
.await
.try_into()?,
etag: services
.key_backups
.get_etag(body.sender_user(), &body.version)
.await,
})
}
/// # `DELETE /_matrix/client/r0/room_keys/keys/{roomId}`
@ -357,9 +404,17 @@ pub(crate) async fn delete_backup_keys_for_room_route(
.delete_room_keys(body.sender_user(), &body.version, &body.room_id)
.await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
Ok(delete_backup_keys_for_room::v3::Response { count, etag })
Ok(delete_backup_keys_for_room::v3::Response {
count: services
.key_backups
.count_keys(body.sender_user(), &body.version)
.await
.try_into()?,
etag: services
.key_backups
.get_etag(body.sender_user(), &body.version)
.await,
})
}
/// # `DELETE /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
@ -374,22 +429,15 @@ pub(crate) async fn delete_backup_keys_for_session_route(
.delete_room_key(body.sender_user(), &body.version, &body.room_id, &body.session_id)
.await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
Ok(delete_backup_keys_for_session::v3::Response { count, etag })
}
async fn get_count_etag(
services: &Services,
sender_user: &UserId,
version: &str,
) -> Result<(UInt, String)> {
let count = services
.key_backups
.count_keys(sender_user, version)
.map(TryInto::try_into);
let etag = services.key_backups.get_etag(sender_user, version).map(Ok);
Ok(try_join(count, etag).await?)
Ok(delete_backup_keys_for_session::v3::Response {
count: services
.key_backups
.count_keys(body.sender_user(), &body.version)
.await
.try_into()?,
etag: services
.key_backups
.get_etag(body.sender_user(), &body.version)
.await,
})
}

View file

@ -26,8 +26,8 @@ pub(crate) async fn get_capabilities_route(
let mut capabilities = Capabilities::default();
capabilities.room_versions = RoomVersionsCapability {
available,
default: services.server.config.default_room_version.clone(),
available,
};
// we do not implement 3PID stuff
@ -38,12 +38,16 @@ pub(crate) async fn get_capabilities_route(
};
// MSC4133 capability
capabilities.set("uk.tcpip.msc4133.profile_fields", json!({"enabled": true}))?;
capabilities
.set("uk.tcpip.msc4133.profile_fields", json!({"enabled": true}))
.expect("this is valid JSON we created");
capabilities.set(
"org.matrix.msc4267.forget_forced_upon_leave",
json!({"enabled": services.config.forget_forced_upon_leave}),
)?;
capabilities
.set(
"org.matrix.msc4267.forget_forced_upon_leave",
json!({"enabled": services.config.forget_forced_upon_leave}),
)
.expect("valid JSON we created");
Ok(get_capabilities::v3::Response { capabilities })
}

View file

@ -1,6 +1,8 @@
use axum::extract::State;
use conduwuit::{
Err, Event, Result, at, debug_warn, err, ref_at,
Err, Result, at, debug_warn, err,
matrix::pdu::PduEvent,
ref_at,
utils::{
IterStream,
future::TryExtExt,
@ -109,7 +111,7 @@ pub(crate) async fn get_context_route(
let lazy_loading_context = lazy_loading::Context {
user_id: sender_user,
device_id: Some(sender_device),
device_id: sender_device,
room_id,
token: Some(base_count.into_unsigned()),
options: Some(&filter.lazy_load_options),
@ -177,12 +179,12 @@ pub(crate) async fn get_context_route(
.broad_filter_map(|event_id: &OwnedEventId| {
services.rooms.timeline.get_pdu(event_id.as_ref()).ok()
})
.map(Event::into_format)
.map(PduEvent::into_state_event)
.collect()
.await;
Ok(get_context::v3::Response {
event: base_event.map(at!(1)).map(Event::into_format),
event: base_event.map(at!(1)).map(PduEvent::into_room_event),
start: events_before
.last()
@ -201,13 +203,13 @@ pub(crate) async fn get_context_route(
events_before: events_before
.into_iter()
.map(at!(1))
.map(Event::into_format)
.map(PduEvent::into_room_event)
.collect(),
events_after: events_after
.into_iter()
.map(at!(1))
.map(Event::into_format)
.map(PduEvent::into_room_event)
.collect(),
state,

View file

@ -21,9 +21,11 @@ pub(crate) async fn get_devices_route(
State(services): State<crate::State>,
body: Ruma<get_devices::v3::Request>,
) -> Result<get_devices::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let devices: Vec<device::Device> = services
.users
.all_devices_metadata(body.sender_user())
.all_devices_metadata(sender_user)
.collect()
.await;
@ -37,9 +39,11 @@ pub(crate) async fn get_device_route(
State(services): State<crate::State>,
body: Ruma<get_device::v3::Request>,
) -> Result<get_device::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let device = services
.users
.get_device_metadata(body.sender_user(), &body.body.device_id)
.get_device_metadata(sender_user, &body.body.device_id)
.await
.map_err(|_| err!(Request(NotFound("Device not found."))))?;

View file

@ -1,7 +1,7 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
Err, Event, Result, err, info,
Err, Result, err, info,
utils::{
TryFutureExtExt,
math::Expected,
@ -352,7 +352,7 @@ async fn user_can_publish_room(
.room_state_get(room_id, &StateEventType::RoomPowerLevels, "")
.await
{
| Ok(event) => serde_json::from_str(event.content().get())
| Ok(event) => serde_json::from_str(event.content.get())
.map_err(|_| err!(Database("Invalid event content for m.room.power_levels")))
.map(|content: RoomPowerLevelsEventContent| {
RoomPowerLevels::from(content)
@ -365,7 +365,7 @@ async fn user_can_publish_room(
.room_state_get(room_id, &StateEventType::RoomCreate, "")
.await
{
| Ok(event) => Ok(event.sender() == user_id),
| Ok(event) => Ok(event.sender == user_id),
| _ => Err!(Request(Forbidden("User is not allowed to publish this room"))),
}
},

View file

@ -13,9 +13,11 @@ pub(crate) async fn get_filter_route(
State(services): State<crate::State>,
body: Ruma<get_filter::v3::Request>,
) -> Result<get_filter::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
services
.users
.get_filter(body.sender_user(), &body.filter_id)
.get_filter(sender_user, &body.filter_id)
.await
.map(get_filter::v3::Response::new)
.map_err(|_| err!(Request(NotFound("Filter not found."))))
@ -28,9 +30,9 @@ pub(crate) async fn create_filter_route(
State(services): State<crate::State>,
body: Ruma<create_filter::v3::Request>,
) -> Result<create_filter::v3::Response> {
let filter_id = services
.users
.create_filter(body.sender_user(), &body.filter);
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let filter_id = services.users.create_filter(sender_user, &body.filter);
Ok(create_filter::v3::Response::new(filter_id))
}

View file

@ -126,7 +126,7 @@ pub(crate) async fn get_keys_route(
State(services): State<crate::State>,
body: Ruma<get_keys::v3::Request>,
) -> Result<get_keys::v3::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
get_keys_helper(
&services,
@ -157,7 +157,8 @@ pub(crate) async fn upload_signing_keys_route(
State(services): State<crate::State>,
body: Ruma<upload_signing_keys::v3::Request>,
) -> Result<upload_signing_keys::v3::Response> {
let (sender_user, sender_device) = body.sender();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
// UIAA
let mut uiaainfo = UiaaInfo {
@ -202,12 +203,12 @@ pub(crate) async fn upload_signing_keys_route(
}
// Success!
},
| _ => match body.json_body.as_ref() {
| _ => match body.json_body {
| Some(json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, sender_device, &uiaainfo, json);
.create(sender_user, sender_device, &uiaainfo, &json);
return Err(Error::Uiaa(uiaainfo));
},
@ -372,7 +373,7 @@ pub(crate) async fn get_key_changes_route(
State(services): State<crate::State>,
body: Ruma<get_key_changes::v3::Request>,
) -> Result<get_key_changes::v3::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let mut device_list_updates = HashSet::new();

View file

@ -51,7 +51,7 @@ pub(crate) async fn create_content_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<create_content::v3::Request>,
) -> Result<create_content::v3::Response> {
let user = body.sender_user();
let user = body.sender_user.as_ref().expect("user is authenticated");
if services.users.is_suspended(user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
@ -97,7 +97,7 @@ pub(crate) async fn get_content_thumbnail_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_content_thumbnail::v1::Request>,
) -> Result<get_content_thumbnail::v1::Response> {
let user = body.sender_user();
let user = body.sender_user.as_ref().expect("user is authenticated");
let dim = Dim::from_ruma(body.width, body.height, body.method.clone())?;
let mxc = Mxc {
@ -134,7 +134,7 @@ pub(crate) async fn get_content_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_content::v1::Request>,
) -> Result<get_content::v1::Response> {
let user = body.sender_user();
let user = body.sender_user.as_ref().expect("user is authenticated");
let mxc = Mxc {
server_name: &body.server_name,
@ -170,7 +170,7 @@ pub(crate) async fn get_content_as_filename_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_content_as_filename::v1::Request>,
) -> Result<get_content_as_filename::v1::Response> {
let user = body.sender_user();
let user = body.sender_user.as_ref().expect("user is authenticated");
let mxc = Mxc {
server_name: &body.server_name,
@ -206,7 +206,7 @@ pub(crate) async fn get_media_preview_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_media_preview::v1::Request>,
) -> Result<get_media_preview::v1::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let url = &body.url;
let url = Url::parse(&body.url).map_err(|e| {

View file

@ -55,7 +55,7 @@ pub(crate) async fn get_media_preview_legacy_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_media_preview::v3::Request>,
) -> Result<get_media_preview::v3::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let url = &body.url;
let url = Url::parse(&body.url).map_err(|e| {

2764
src/api/client/membership.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,60 +0,0 @@
use axum::extract::State;
use conduwuit::{Err, Result, matrix::pdu::PduBuilder};
use ruma::{
api::client::membership::ban_user,
events::room::member::{MembershipState, RoomMemberEventContent},
};
use crate::Ruma;
/// # `POST /_matrix/client/r0/rooms/{roomId}/ban`
///
/// Tries to send a ban event into the room.
pub(crate) async fn ban_user_route(
State(services): State<crate::State>,
body: Ruma<ban_user::v3::Request>,
) -> Result<ban_user::v3::Response> {
let sender_user = body.sender_user();
if sender_user == body.user_id {
return Err!(Request(Forbidden("You cannot ban yourself.")));
}
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
let current_member_content = services
.rooms
.state_accessor
.get_member(&body.room_id, &body.user_id)
.await
.unwrap_or_else(|_| RoomMemberEventContent::new(MembershipState::Ban));
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(body.user_id.to_string(), &RoomMemberEventContent {
membership: MembershipState::Ban,
reason: body.reason.clone(),
displayname: None, // display name may be offensive
avatar_url: None, // avatar may be offensive
is_direct: None,
join_authorized_via_users_server: None,
third_party_invite: None,
redact_events: body.redact_events,
..current_member_content
}),
sender_user,
&body.room_id,
&state_lock,
)
.await?;
drop(state_lock);
Ok(ban_user::v3::Response::new())
}

View file

@ -1,52 +0,0 @@
use axum::extract::State;
use conduwuit::{Err, Result, is_matching, result::NotFound, utils::FutureBoolExt};
use futures::pin_mut;
use ruma::{api::client::membership::forget_room, events::room::member::MembershipState};
use crate::Ruma;
/// # `POST /_matrix/client/v3/rooms/{roomId}/forget`
///
/// Forgets about a room.
///
/// - If the sender user currently left the room: Stops sender user from
/// receiving information about the room
///
/// Note: Other devices of the user have no way of knowing the room was
/// forgotten, so this has to be called from every device
pub(crate) async fn forget_room_route(
State(services): State<crate::State>,
body: Ruma<forget_room::v3::Request>,
) -> Result<forget_room::v3::Response> {
let user_id = body.sender_user();
let room_id = &body.room_id;
let joined = services.rooms.state_cache.is_joined(user_id, room_id);
let knocked = services.rooms.state_cache.is_knocked(user_id, room_id);
let invited = services.rooms.state_cache.is_invited(user_id, room_id);
pin_mut!(joined, knocked, invited);
if joined.or(knocked).or(invited).await {
return Err!(Request(Unknown("You must leave the room before forgetting it")));
}
let membership = services
.rooms
.state_accessor
.get_member(room_id, user_id)
.await;
if membership.is_not_found() {
return Err!(Request(Unknown("No membership event was found, room was never joined")));
}
let non_membership = membership
.map(|member| member.membership)
.is_ok_and(is_matching!(MembershipState::Leave | MembershipState::Ban));
if non_membership || services.rooms.state_cache.is_left(user_id, room_id).await {
services.rooms.state_cache.forget(room_id, user_id);
}
Ok(forget_room::v3::Response::new())
}

View file

@ -1,238 +0,0 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
Err, Result, debug_error, err, info,
matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder},
};
use futures::{FutureExt, join};
use ruma::{
OwnedServerName, RoomId, UserId,
api::{client::membership::invite_user, federation::membership::create_invite},
events::room::member::{MembershipState, RoomMemberEventContent},
};
use service::Services;
use super::banned_room_check;
use crate::Ruma;
/// # `POST /_matrix/client/r0/rooms/{roomId}/invite`
///
/// Tries to send an invite event into the room.
#[tracing::instrument(skip_all, fields(%client), name = "invite")]
pub(crate) async fn invite_user_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<invite_user::v3::Request>,
) -> Result<invite_user::v3::Response> {
let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
if !services.users.is_admin(sender_user).await && services.config.block_non_admin_invites {
debug_error!(
"User {sender_user} is not an admin and attempted to send an invite to room {}",
&body.room_id
);
return Err!(Request(Forbidden("Invites are not allowed on this server.")));
}
banned_room_check(
&services,
sender_user,
Some(&body.room_id),
body.room_id.server_name(),
client,
)
.await?;
match &body.recipient {
| invite_user::v3::InvitationRecipient::UserId { user_id } => {
let sender_ignored_recipient = services.users.user_is_ignored(sender_user, user_id);
let recipient_ignored_by_sender =
services.users.user_is_ignored(user_id, sender_user);
let (sender_ignored_recipient, recipient_ignored_by_sender) =
join!(sender_ignored_recipient, recipient_ignored_by_sender);
if sender_ignored_recipient {
return Ok(invite_user::v3::Response {});
}
if let Ok(target_user_membership) = services
.rooms
.state_accessor
.get_member(&body.room_id, user_id)
.await
{
if target_user_membership.membership == MembershipState::Ban {
return Err!(Request(Forbidden("User is banned from this room.")));
}
}
if recipient_ignored_by_sender {
// silently drop the invite to the recipient if they've been ignored by the
// sender, pretend it worked
return Ok(invite_user::v3::Response {});
}
invite_helper(
&services,
sender_user,
user_id,
&body.room_id,
body.reason.clone(),
false,
)
.boxed()
.await?;
Ok(invite_user::v3::Response {})
},
| _ => {
Err!(Request(NotFound("User not found.")))
},
}
}
pub(crate) async fn invite_helper(
services: &Services,
sender_user: &UserId,
user_id: &UserId,
room_id: &RoomId,
reason: Option<String>,
is_direct: bool,
) -> Result {
if !services.users.is_admin(sender_user).await && services.config.block_non_admin_invites {
info!(
"User {sender_user} is not an admin and attempted to send an invite to room \
{room_id}"
);
return Err!(Request(Forbidden("Invites are not allowed on this server.")));
}
if !services.globals.user_is_local(user_id) {
let (pdu, pdu_json, invite_room_state) = {
let state_lock = services.rooms.state.mutex.lock(room_id).await;
let content = RoomMemberEventContent {
avatar_url: services.users.avatar_url(user_id).await.ok(),
is_direct: Some(is_direct),
reason,
..RoomMemberEventContent::new(MembershipState::Invite)
};
let (pdu, pdu_json) = services
.rooms
.timeline
.create_hash_and_sign_event(
PduBuilder::state(user_id.to_string(), &content),
sender_user,
room_id,
&state_lock,
)
.await?;
let invite_room_state = services.rooms.state.summary_stripped(&pdu).await;
drop(state_lock);
(pdu, pdu_json, invite_room_state)
};
let room_version_id = services.rooms.state.get_room_version(room_id).await?;
let response = services
.sending
.send_federation_request(user_id.server_name(), create_invite::v2::Request {
room_id: room_id.to_owned(),
event_id: (*pdu.event_id).to_owned(),
room_version: room_version_id.clone(),
event: services
.sending
.convert_to_outgoing_federation_event(pdu_json.clone())
.await,
invite_room_state,
via: services
.rooms
.state_cache
.servers_route_via(room_id)
.await
.ok(),
})
.await?;
// We do not add the event_id field to the pdu here because of signature and
// hashes checks
let (event_id, value) = gen_event_id_canonical_json(&response.event, &room_version_id)
.map_err(|e| {
err!(Request(BadJson(warn!("Could not convert event to canonical JSON: {e}"))))
})?;
if pdu.event_id != event_id {
return Err!(Request(BadJson(warn!(
%pdu.event_id, %event_id,
"Server {} sent event with wrong event ID",
user_id.server_name()
))));
}
let origin: OwnedServerName = serde_json::from_value(serde_json::to_value(
value
.get("origin")
.ok_or_else(|| err!(Request(BadJson("Event missing origin field."))))?,
)?)
.map_err(|e| {
err!(Request(BadJson(warn!("Origin field in event is not a valid server name: {e}"))))
})?;
let pdu_id = services
.rooms
.event_handler
.handle_incoming_pdu(&origin, room_id, &event_id, value, true)
.boxed()
.await?
.ok_or_else(|| {
err!(Request(InvalidParam("Could not accept incoming PDU as timeline event.")))
})?;
return services.sending.send_pdu_room(room_id, &pdu_id).await;
}
if !services
.rooms
.state_cache
.is_joined(sender_user, room_id)
.await
{
return Err!(Request(Forbidden(
"You must be joined in the room you are trying to invite from."
)));
}
let state_lock = services.rooms.state.mutex.lock(room_id).await;
let content = RoomMemberEventContent {
displayname: services.users.displayname(user_id).await.ok(),
avatar_url: services.users.avatar_url(user_id).await.ok(),
blurhash: services.users.blurhash(user_id).await.ok(),
is_direct: Some(is_direct),
reason,
..RoomMemberEventContent::new(MembershipState::Invite)
};
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(user_id.to_string(), &content),
sender_user,
room_id,
&state_lock,
)
.await?;
drop(state_lock);
Ok(())
}

View file

@ -1,989 +0,0 @@
use std::{borrow::Borrow, collections::HashMap, iter::once, sync::Arc};
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
Err, Result, debug, debug_info, debug_warn, err, error, info,
matrix::{
StateKey,
event::{gen_event_id, gen_event_id_canonical_json},
pdu::{PduBuilder, PduEvent},
state_res,
},
result::FlatOk,
trace,
utils::{
self, shuffle,
stream::{IterStream, ReadyExt},
},
warn,
};
use futures::{FutureExt, StreamExt};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedRoomId, OwnedServerName, OwnedUserId, RoomId,
RoomVersionId, UserId,
api::{
client::{
error::ErrorKind,
membership::{ThirdPartySigned, join_room_by_id, join_room_by_id_or_alias},
},
federation::{self},
},
canonical_json::to_canonical_value,
events::{
StateEventType,
room::{
join_rules::{AllowRule, JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent},
},
},
};
use service::{
Services,
appservice::RegistrationInfo,
rooms::{
state::RoomMutexGuard,
state_compressor::{CompressedState, HashSetCompressStateEvent},
},
};
use super::banned_room_check;
use crate::Ruma;
/// # `POST /_matrix/client/r0/rooms/{roomId}/join`
///
/// Tries to join the sender user into a room.
///
/// - If the server knowns about this room: creates the join event and does auth
/// rules locally
/// - If the server does not know about the room: asks other servers over
/// federation
#[tracing::instrument(skip_all, fields(%client), name = "join")]
pub(crate) async fn join_room_by_id_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<join_room_by_id::v3::Request>,
) -> Result<join_room_by_id::v3::Response> {
let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
banned_room_check(
&services,
sender_user,
Some(&body.room_id),
body.room_id.server_name(),
client,
)
.await?;
// There is no body.server_name for /roomId/join
let mut servers: Vec<_> = services
.rooms
.state_cache
.servers_invite_via(&body.room_id)
.map(ToOwned::to_owned)
.collect()
.await;
servers.extend(
services
.rooms
.state_cache
.invite_state(sender_user, &body.room_id)
.await
.unwrap_or_default()
.iter()
.filter_map(|event| event.get_field("sender").ok().flatten())
.filter_map(|sender: &str| UserId::parse(sender).ok())
.map(|user| user.server_name().to_owned()),
);
if let Some(server) = body.room_id.server_name() {
servers.push(server.into());
}
servers.sort_unstable();
servers.dedup();
shuffle(&mut servers);
join_room_by_id_helper(
&services,
sender_user,
&body.room_id,
body.reason.clone(),
&servers,
body.third_party_signed.as_ref(),
&body.appservice_info,
)
.boxed()
.await
}
/// # `POST /_matrix/client/r0/join/{roomIdOrAlias}`
///
/// Tries to join the sender user into a room.
///
/// - If the server knowns about this room: creates the join event and does auth
/// rules locally
/// - If the server does not know about the room: use the server name query
/// param if specified. if not specified, asks other servers over federation
/// via room alias server name and room ID server name
#[tracing::instrument(skip_all, fields(%client), name = "join")]
pub(crate) async fn join_room_by_id_or_alias_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<join_room_by_id_or_alias::v3::Request>,
) -> Result<join_room_by_id_or_alias::v3::Response> {
let sender_user = body.sender_user();
let appservice_info = &body.appservice_info;
let body = &body.body;
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let (servers, room_id) = match OwnedRoomId::try_from(body.room_id_or_alias.clone()) {
| Ok(room_id) => {
banned_room_check(
&services,
sender_user,
Some(&room_id),
room_id.server_name(),
client,
)
.boxed()
.await?;
let mut servers = body.via.clone();
servers.extend(
services
.rooms
.state_cache
.servers_invite_via(&room_id)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await,
);
servers.extend(
services
.rooms
.state_cache
.invite_state(sender_user, &room_id)
.await
.unwrap_or_default()
.iter()
.filter_map(|event| event.get_field("sender").ok().flatten())
.filter_map(|sender: &str| UserId::parse(sender).ok())
.map(|user| user.server_name().to_owned()),
);
if let Some(server) = room_id.server_name() {
servers.push(server.to_owned());
}
servers.sort_unstable();
servers.dedup();
shuffle(&mut servers);
(servers, room_id)
},
| Err(room_alias) => {
let (room_id, mut servers) = services
.rooms
.alias
.resolve_alias(&room_alias, Some(body.via.clone()))
.await?;
banned_room_check(
&services,
sender_user,
Some(&room_id),
Some(room_alias.server_name()),
client,
)
.await?;
let addl_via_servers = services
.rooms
.state_cache
.servers_invite_via(&room_id)
.map(ToOwned::to_owned);
let addl_state_servers = services
.rooms
.state_cache
.invite_state(sender_user, &room_id)
.await
.unwrap_or_default();
let mut addl_servers: Vec<_> = addl_state_servers
.iter()
.map(|event| event.get_field("sender"))
.filter_map(FlatOk::flat_ok)
.map(|user: &UserId| user.server_name().to_owned())
.stream()
.chain(addl_via_servers)
.collect()
.await;
addl_servers.sort_unstable();
addl_servers.dedup();
shuffle(&mut addl_servers);
servers.append(&mut addl_servers);
(servers, room_id)
},
};
let join_room_response = join_room_by_id_helper(
&services,
sender_user,
&room_id,
body.reason.clone(),
&servers,
body.third_party_signed.as_ref(),
appservice_info,
)
.boxed()
.await?;
Ok(join_room_by_id_or_alias::v3::Response { room_id: join_room_response.room_id })
}
pub async fn join_room_by_id_helper(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
reason: Option<String>,
servers: &[OwnedServerName],
third_party_signed: Option<&ThirdPartySigned>,
appservice_info: &Option<RegistrationInfo>,
) -> Result<join_room_by_id::v3::Response> {
let state_lock = services.rooms.state.mutex.lock(room_id).await;
let user_is_guest = services
.users
.is_deactivated(sender_user)
.await
.unwrap_or(false)
&& appservice_info.is_none();
if user_is_guest && !services.rooms.state_accessor.guest_can_join(room_id).await {
return Err!(Request(Forbidden("Guests are not allowed to join this room")));
}
if services
.rooms
.state_cache
.is_joined(sender_user, room_id)
.await
{
debug_warn!("{sender_user} is already joined in {room_id}");
return Ok(join_room_by_id::v3::Response { room_id: room_id.into() });
}
let server_in_room = services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), room_id)
.await;
// Only check our known membership if we're already in the room.
// See: https://forgejo.ellis.link/continuwuation/continuwuity/issues/855
let membership = if server_in_room {
services
.rooms
.state_accessor
.get_member(room_id, sender_user)
.await
} else {
debug!("Ignoring local state for join {room_id}, we aren't in the room yet.");
Ok(RoomMemberEventContent::new(MembershipState::Leave))
};
if let Ok(m) = membership {
if m.membership == MembershipState::Ban {
debug_warn!("{sender_user} is banned from {room_id} but attempted to join");
// TODO: return reason
return Err!(Request(Forbidden("You are banned from the room.")));
}
}
let local_join = server_in_room
|| servers.is_empty()
|| (servers.len() == 1 && services.globals.server_is_ours(&servers[0]));
if local_join {
join_room_by_id_helper_local(
services,
sender_user,
room_id,
reason,
servers,
third_party_signed,
state_lock,
)
.boxed()
.await?;
} else {
// Ask a remote server if we are not participating in this room
join_room_by_id_helper_remote(
services,
sender_user,
room_id,
reason,
servers,
third_party_signed,
state_lock,
)
.boxed()
.await?;
}
Ok(join_room_by_id::v3::Response::new(room_id.to_owned()))
}
#[tracing::instrument(skip_all, fields(%sender_user, %room_id), name = "join_remote")]
async fn join_room_by_id_helper_remote(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
reason: Option<String>,
servers: &[OwnedServerName],
_third_party_signed: Option<&ThirdPartySigned>,
state_lock: RoomMutexGuard,
) -> Result {
info!("Joining {room_id} over federation.");
let (make_join_response, remote_server) =
make_join_request(services, sender_user, room_id, servers).await?;
info!("make_join finished");
let Some(room_version_id) = make_join_response.room_version else {
return Err!(BadServerResponse("Remote room version is not supported by conduwuit"));
};
if !services.server.supported_room_version(&room_version_id) {
return Err!(BadServerResponse(
"Remote room version {room_version_id} is not supported by conduwuit"
));
}
let mut join_event_stub: CanonicalJsonObject =
serde_json::from_str(make_join_response.event.get()).map_err(|e| {
err!(BadServerResponse(warn!(
"Invalid make_join event json received from server: {e:?}"
)))
})?;
let join_authorized_via_users_server = {
use RoomVersionId::*;
if !matches!(room_version_id, V1 | V2 | V3 | V4 | V5 | V6 | V7) {
join_event_stub
.get("content")
.map(|s| {
s.as_object()?
.get("join_authorised_via_users_server")?
.as_str()
})
.and_then(|s| OwnedUserId::try_from(s.unwrap_or_default()).ok())
} else {
None
}
};
join_event_stub.insert(
"origin".to_owned(),
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
);
join_event_stub.insert(
"origin_server_ts".to_owned(),
CanonicalJsonValue::Integer(
utils::millis_since_unix_epoch()
.try_into()
.expect("Timestamp is valid js_int value"),
),
);
join_event_stub.insert(
"content".to_owned(),
to_canonical_value(RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(),
reason,
join_authorized_via_users_server: join_authorized_via_users_server.clone(),
..RoomMemberEventContent::new(MembershipState::Join)
})
.expect("event is valid, we just created it"),
);
// We keep the "event_id" in the pdu only in v1 or
// v2 rooms
match room_version_id {
| RoomVersionId::V1 | RoomVersionId::V2 => {},
| _ => {
join_event_stub.remove("event_id");
},
}
// In order to create a compatible ref hash (EventID) the `hashes` field needs
// to be present
services
.server_keys
.hash_and_sign_event(&mut join_event_stub, &room_version_id)?;
// Generate event id
let event_id = gen_event_id(&join_event_stub, &room_version_id)?;
// Add event_id back
join_event_stub
.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.clone().into()));
// It has enough fields to be called a proper event now
let mut join_event = join_event_stub;
info!("Asking {remote_server} for send_join in room {room_id}");
let send_join_request = federation::membership::create_join_event::v2::Request {
room_id: room_id.to_owned(),
event_id: event_id.clone(),
omit_members: false,
pdu: services
.sending
.convert_to_outgoing_federation_event(join_event.clone())
.await,
};
let send_join_response = match services
.sending
.send_synapse_request(&remote_server, send_join_request)
.await
{
| Ok(response) => response,
| Err(e) => {
error!("send_join failed: {e}");
return Err(e);
},
};
info!("send_join finished");
if join_authorized_via_users_server.is_some() {
if let Some(signed_raw) = &send_join_response.room_state.event {
debug_info!(
"There is a signed event with join_authorized_via_users_server. This room is \
probably using restricted joins. Adding signature to our event"
);
let (signed_event_id, signed_value) =
gen_event_id_canonical_json(signed_raw, &room_version_id).map_err(|e| {
err!(Request(BadJson(warn!(
"Could not convert event to canonical JSON: {e}"
))))
})?;
if signed_event_id != event_id {
return Err!(Request(BadJson(warn!(
%signed_event_id, %event_id,
"Server {remote_server} sent event with wrong event ID"
))));
}
match signed_value["signatures"]
.as_object()
.ok_or_else(|| {
err!(BadServerResponse(warn!(
"Server {remote_server} sent invalid signatures type"
)))
})
.and_then(|e| {
e.get(remote_server.as_str()).ok_or_else(|| {
err!(BadServerResponse(warn!(
"Server {remote_server} did not send its signature for a restricted \
room"
)))
})
}) {
| Ok(signature) => {
join_event
.get_mut("signatures")
.expect("we created a valid pdu")
.as_object_mut()
.expect("we created a valid pdu")
.insert(remote_server.to_string(), signature.clone());
},
| Err(e) => {
warn!(
"Server {remote_server} sent invalid signature in send_join signatures \
for event {signed_value:?}: {e:?}",
);
},
}
}
}
services
.rooms
.short
.get_or_create_shortroomid(room_id)
.await;
info!("Parsing join event");
let parsed_join_pdu = PduEvent::from_id_val(&event_id, join_event.clone())
.map_err(|e| err!(BadServerResponse("Invalid join event PDU: {e:?}")))?;
info!("Acquiring server signing keys for response events");
let resp_events = &send_join_response.room_state;
let resp_state = &resp_events.state;
let resp_auth = &resp_events.auth_chain;
services
.server_keys
.acquire_events_pubkeys(resp_auth.iter().chain(resp_state.iter()))
.await;
info!("Going through send_join response room_state");
let cork = services.db.cork_and_flush();
let state = send_join_response
.room_state
.state
.iter()
.stream()
.then(|pdu| {
services
.server_keys
.validate_and_add_event_id_no_fetch(pdu, &room_version_id)
})
.ready_filter_map(Result::ok)
.fold(HashMap::new(), |mut state, (event_id, value)| async move {
let pdu = match PduEvent::from_id_val(&event_id, value.clone()) {
| Ok(pdu) => pdu,
| Err(e) => {
debug_warn!("Invalid PDU in send_join response: {e:?}: {value:#?}");
return state;
},
};
services.rooms.outlier.add_pdu_outlier(&event_id, &value);
if let Some(state_key) = &pdu.state_key {
let shortstatekey = services
.rooms
.short
.get_or_create_shortstatekey(&pdu.kind.to_string().into(), state_key)
.await;
state.insert(shortstatekey, pdu.event_id.clone());
}
state
})
.await;
drop(cork);
info!("Going through send_join response auth_chain");
let cork = services.db.cork_and_flush();
send_join_response
.room_state
.auth_chain
.iter()
.stream()
.then(|pdu| {
services
.server_keys
.validate_and_add_event_id_no_fetch(pdu, &room_version_id)
})
.ready_filter_map(Result::ok)
.ready_for_each(|(event_id, value)| {
services.rooms.outlier.add_pdu_outlier(&event_id, &value);
})
.await;
drop(cork);
debug!("Running send_join auth check");
let fetch_state = &state;
let state_fetch = |k: StateEventType, s: StateKey| async move {
let shortstatekey = services.rooms.short.get_shortstatekey(&k, &s).await.ok()?;
let event_id = fetch_state.get(&shortstatekey)?;
services.rooms.timeline.get_pdu(event_id).await.ok()
};
let auth_check = state_res::event_auth::auth_check(
&state_res::RoomVersion::new(&room_version_id)?,
&parsed_join_pdu,
None, // TODO: third party invite
|k, s| state_fetch(k.clone(), s.into()),
)
.await
.map_err(|e| err!(Request(Forbidden(warn!("Auth check failed: {e:?}")))))?;
if !auth_check {
return Err!(Request(Forbidden("Auth check failed")));
}
info!("Compressing state from send_join");
let compressed: CompressedState = services
.rooms
.state_compressor
.compress_state_events(state.iter().map(|(ssk, eid)| (ssk, eid.borrow())))
.collect()
.await;
debug!("Saving compressed state");
let HashSetCompressStateEvent {
shortstatehash: statehash_before_join,
added,
removed,
} = services
.rooms
.state_compressor
.save_state(room_id, Arc::new(compressed))
.await?;
debug!("Forcing state for new room");
services
.rooms
.state
.force_state(room_id, statehash_before_join, added, removed, &state_lock)
.await?;
info!("Updating joined counts for new room");
services
.rooms
.state_cache
.update_joined_count(room_id)
.await;
// We append to state before appending the pdu, so we don't have a moment in
// time with the pdu without it's state. This is okay because append_pdu can't
// fail.
let statehash_after_join = services
.rooms
.state
.append_to_state(&parsed_join_pdu)
.await?;
info!("Appending new room join event");
services
.rooms
.timeline
.append_pdu(
&parsed_join_pdu,
join_event,
once(parsed_join_pdu.event_id.borrow()),
&state_lock,
)
.await?;
info!("Setting final room state for new room");
// We set the room state after inserting the pdu, so that we never have a moment
// in time where events in the current room state do not exist
services
.rooms
.state
.set_room_state(room_id, statehash_after_join, &state_lock);
Ok(())
}
#[tracing::instrument(skip_all, fields(%sender_user, %room_id), name = "join_local")]
async fn join_room_by_id_helper_local(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
reason: Option<String>,
servers: &[OwnedServerName],
_third_party_signed: Option<&ThirdPartySigned>,
state_lock: RoomMutexGuard,
) -> Result {
debug_info!("We can join locally");
let join_rules_event_content = services
.rooms
.state_accessor
.room_state_get_content::<RoomJoinRulesEventContent>(
room_id,
&StateEventType::RoomJoinRules,
"",
)
.await;
let restriction_rooms = match join_rules_event_content {
| Ok(RoomJoinRulesEventContent {
join_rule: JoinRule::Restricted(restricted) | JoinRule::KnockRestricted(restricted),
}) => restricted
.allow
.into_iter()
.filter_map(|a| match a {
| AllowRule::RoomMembership(r) => Some(r.room_id),
| _ => None,
})
.collect(),
| _ => Vec::new(),
};
let join_authorized_via_users_server: Option<OwnedUserId> = {
if restriction_rooms
.iter()
.stream()
.any(|restriction_room_id| {
services
.rooms
.state_cache
.is_joined(sender_user, restriction_room_id)
})
.await
{
services
.rooms
.state_cache
.local_users_in_room(room_id)
.filter(|user| {
services.rooms.state_accessor.user_can_invite(
room_id,
user,
sender_user,
&state_lock,
)
})
.boxed()
.next()
.await
.map(ToOwned::to_owned)
} else {
None
}
};
let content = RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(),
reason: reason.clone(),
join_authorized_via_users_server,
..RoomMemberEventContent::new(MembershipState::Join)
};
// Try normal join first
let Err(error) = services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(sender_user.to_string(), &content),
sender_user,
room_id,
&state_lock,
)
.await
else {
return Ok(());
};
if restriction_rooms.is_empty()
&& (servers.is_empty()
|| servers.len() == 1 && services.globals.server_is_ours(&servers[0]))
{
return Err(error);
}
warn!(
"We couldn't do the join locally, maybe federation can help to satisfy the restricted \
join requirements"
);
let Ok((make_join_response, remote_server)) =
make_join_request(services, sender_user, room_id, servers).await
else {
return Err(error);
};
let Some(room_version_id) = make_join_response.room_version else {
return Err!(BadServerResponse("Remote room version is not supported by conduwuit"));
};
if !services.server.supported_room_version(&room_version_id) {
return Err!(BadServerResponse(
"Remote room version {room_version_id} is not supported by conduwuit"
));
}
let mut join_event_stub: CanonicalJsonObject =
serde_json::from_str(make_join_response.event.get()).map_err(|e| {
err!(BadServerResponse("Invalid make_join event json received from server: {e:?}"))
})?;
let join_authorized_via_users_server = join_event_stub
.get("content")
.map(|s| {
s.as_object()?
.get("join_authorised_via_users_server")?
.as_str()
})
.and_then(|s| OwnedUserId::try_from(s.unwrap_or_default()).ok());
join_event_stub.insert(
"origin".to_owned(),
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
);
join_event_stub.insert(
"origin_server_ts".to_owned(),
CanonicalJsonValue::Integer(
utils::millis_since_unix_epoch()
.try_into()
.expect("Timestamp is valid js_int value"),
),
);
join_event_stub.insert(
"content".to_owned(),
to_canonical_value(RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(),
reason,
join_authorized_via_users_server,
..RoomMemberEventContent::new(MembershipState::Join)
})
.expect("event is valid, we just created it"),
);
// We keep the "event_id" in the pdu only in v1 or
// v2 rooms
match room_version_id {
| RoomVersionId::V1 | RoomVersionId::V2 => {},
| _ => {
join_event_stub.remove("event_id");
},
}
// In order to create a compatible ref hash (EventID) the `hashes` field needs
// to be present
services
.server_keys
.hash_and_sign_event(&mut join_event_stub, &room_version_id)?;
// Generate event id
let event_id = gen_event_id(&join_event_stub, &room_version_id)?;
// Add event_id back
join_event_stub
.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.clone().into()));
// It has enough fields to be called a proper event now
let join_event = join_event_stub;
let send_join_response = services
.sending
.send_synapse_request(
&remote_server,
federation::membership::create_join_event::v2::Request {
room_id: room_id.to_owned(),
event_id: event_id.clone(),
omit_members: false,
pdu: services
.sending
.convert_to_outgoing_federation_event(join_event.clone())
.await,
},
)
.await?;
if let Some(signed_raw) = send_join_response.room_state.event {
let (signed_event_id, signed_value) =
gen_event_id_canonical_json(&signed_raw, &room_version_id).map_err(|e| {
err!(Request(BadJson(warn!("Could not convert event to canonical JSON: {e}"))))
})?;
if signed_event_id != event_id {
return Err!(Request(BadJson(
warn!(%signed_event_id, %event_id, "Server {remote_server} sent event with wrong event ID")
)));
}
drop(state_lock);
services
.rooms
.event_handler
.handle_incoming_pdu(&remote_server, room_id, &signed_event_id, signed_value, true)
.boxed()
.await?;
} else {
return Err(error);
}
Ok(())
}
async fn make_join_request(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
servers: &[OwnedServerName],
) -> Result<(federation::membership::prepare_join_event::v1::Response, OwnedServerName)> {
let mut make_join_response_and_server =
Err!(BadServerResponse("No server available to assist in joining."));
let mut make_join_counter: usize = 0;
let mut incompatible_room_version_count: usize = 0;
for remote_server in servers {
if services.globals.server_is_ours(remote_server) {
continue;
}
info!("Asking {remote_server} for make_join ({make_join_counter})");
let make_join_response = services
.sending
.send_federation_request(
remote_server,
federation::membership::prepare_join_event::v1::Request {
room_id: room_id.to_owned(),
user_id: sender_user.to_owned(),
ver: services.server.supported_room_versions().collect(),
},
)
.await;
trace!("make_join response: {:?}", make_join_response);
make_join_counter = make_join_counter.saturating_add(1);
if let Err(ref e) = make_join_response {
if matches!(
e.kind(),
ErrorKind::IncompatibleRoomVersion { .. } | ErrorKind::UnsupportedRoomVersion
) {
incompatible_room_version_count =
incompatible_room_version_count.saturating_add(1);
}
if incompatible_room_version_count > 15 {
info!(
"15 servers have responded with M_INCOMPATIBLE_ROOM_VERSION or \
M_UNSUPPORTED_ROOM_VERSION, assuming that conduwuit does not support the \
room version {room_id}: {e}"
);
make_join_response_and_server =
Err!(BadServerResponse("Room version is not supported by Conduwuit"));
return make_join_response_and_server;
}
if make_join_counter > 40 {
warn!(
"40 servers failed to provide valid make_join response, assuming no server \
can assist in joining."
);
make_join_response_and_server =
Err!(BadServerResponse("No server available to assist in joining."));
return make_join_response_and_server;
}
}
make_join_response_and_server = make_join_response.map(|r| (r, remote_server.clone()));
if make_join_response_and_server.is_ok() {
break;
}
}
make_join_response_and_server
}

View file

@ -1,65 +0,0 @@
use axum::extract::State;
use conduwuit::{Err, Result, matrix::pdu::PduBuilder};
use ruma::{
api::client::membership::kick_user,
events::room::member::{MembershipState, RoomMemberEventContent},
};
use crate::Ruma;
/// # `POST /_matrix/client/r0/rooms/{roomId}/kick`
///
/// Tries to send a kick event into the room.
pub(crate) async fn kick_user_route(
State(services): State<crate::State>,
body: Ruma<kick_user::v3::Request>,
) -> Result<kick_user::v3::Response> {
let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
let Ok(event) = services
.rooms
.state_accessor
.get_member(&body.room_id, &body.user_id)
.await
else {
// copy synapse's behaviour of returning 200 without any change to the state
// instead of erroring on left users
return Ok(kick_user::v3::Response::new());
};
if !matches!(
event.membership,
MembershipState::Invite | MembershipState::Knock | MembershipState::Join,
) {
return Err!(Request(Forbidden(
"Cannot kick a user who is not apart of the room (current membership: {})",
event.membership
)));
}
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(body.user_id.to_string(), &RoomMemberEventContent {
membership: MembershipState::Leave,
reason: body.reason.clone(),
is_direct: None,
join_authorized_via_users_server: None,
third_party_invite: None,
..event
}),
sender_user,
&body.room_id,
&state_lock,
)
.await?;
drop(state_lock);
Ok(kick_user::v3::Response::new())
}

View file

@ -1,770 +0,0 @@
use std::{borrow::Borrow, collections::HashMap, iter::once, sync::Arc};
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
Err, Result, debug, debug_info, debug_warn, err, info,
matrix::{
event::{Event, gen_event_id},
pdu::{PduBuilder, PduEvent},
},
result::FlatOk,
trace,
utils::{self, shuffle, stream::IterStream},
warn,
};
use futures::{FutureExt, StreamExt};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedRoomId, OwnedServerName, RoomId,
RoomVersionId, UserId,
api::{
client::knock::knock_room,
federation::{self},
},
canonical_json::to_canonical_value,
events::{
StateEventType,
room::{
join_rules::{AllowRule, JoinRule},
member::{MembershipState, RoomMemberEventContent},
},
},
};
use service::{
Services,
rooms::{
state::RoomMutexGuard,
state_compressor::{CompressedState, HashSetCompressStateEvent},
},
};
use super::{banned_room_check, join::join_room_by_id_helper};
use crate::Ruma;
/// # `POST /_matrix/client/*/knock/{roomIdOrAlias}`
///
/// Tries to knock the room to ask permission to join for the sender user.
#[tracing::instrument(skip_all, fields(%client), name = "knock")]
pub(crate) async fn knock_room_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<knock_room::v3::Request>,
) -> Result<knock_room::v3::Response> {
let sender_user = body.sender_user();
let body = &body.body;
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let (servers, room_id) = match OwnedRoomId::try_from(body.room_id_or_alias.clone()) {
| Ok(room_id) => {
banned_room_check(
&services,
sender_user,
Some(&room_id),
room_id.server_name(),
client,
)
.await?;
let mut servers = body.via.clone();
servers.extend(
services
.rooms
.state_cache
.servers_invite_via(&room_id)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await,
);
servers.extend(
services
.rooms
.state_cache
.invite_state(sender_user, &room_id)
.await
.unwrap_or_default()
.iter()
.filter_map(|event| event.get_field("sender").ok().flatten())
.filter_map(|sender: &str| UserId::parse(sender).ok())
.map(|user| user.server_name().to_owned()),
);
if let Some(server) = room_id.server_name() {
servers.push(server.to_owned());
}
servers.sort_unstable();
servers.dedup();
shuffle(&mut servers);
(servers, room_id)
},
| Err(room_alias) => {
let (room_id, mut servers) = services
.rooms
.alias
.resolve_alias(&room_alias, Some(body.via.clone()))
.await?;
banned_room_check(
&services,
sender_user,
Some(&room_id),
Some(room_alias.server_name()),
client,
)
.await?;
let addl_via_servers = services
.rooms
.state_cache
.servers_invite_via(&room_id)
.map(ToOwned::to_owned);
let addl_state_servers = services
.rooms
.state_cache
.invite_state(sender_user, &room_id)
.await
.unwrap_or_default();
let mut addl_servers: Vec<_> = addl_state_servers
.iter()
.map(|event| event.get_field("sender"))
.filter_map(FlatOk::flat_ok)
.map(|user: &UserId| user.server_name().to_owned())
.stream()
.chain(addl_via_servers)
.collect()
.await;
addl_servers.sort_unstable();
addl_servers.dedup();
shuffle(&mut addl_servers);
servers.append(&mut addl_servers);
(servers, room_id)
},
};
knock_room_by_id_helper(&services, sender_user, &room_id, body.reason.clone(), &servers)
.boxed()
.await
}
async fn knock_room_by_id_helper(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
reason: Option<String>,
servers: &[OwnedServerName],
) -> Result<knock_room::v3::Response> {
let state_lock = services.rooms.state.mutex.lock(room_id).await;
if services
.rooms
.state_cache
.is_invited(sender_user, room_id)
.await
{
debug_warn!("{sender_user} is already invited in {room_id} but attempted to knock");
return Err!(Request(Forbidden(
"You cannot knock on a room you are already invited/accepted to."
)));
}
if services
.rooms
.state_cache
.is_joined(sender_user, room_id)
.await
{
debug_warn!("{sender_user} is already joined in {room_id} but attempted to knock");
return Err!(Request(Forbidden("You cannot knock on a room you are already joined in.")));
}
if services
.rooms
.state_cache
.is_knocked(sender_user, room_id)
.await
{
debug_warn!("{sender_user} is already knocked in {room_id}");
return Ok(knock_room::v3::Response { room_id: room_id.into() });
}
if let Ok(membership) = services
.rooms
.state_accessor
.get_member(room_id, sender_user)
.await
{
if membership.membership == MembershipState::Ban {
debug_warn!("{sender_user} is banned from {room_id} but attempted to knock");
return Err!(Request(Forbidden("You cannot knock on a room you are banned from.")));
}
}
// 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
.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, state_lock)
.boxed()
.await?;
} else {
knock_room_helper_remote(services, sender_user, room_id, reason, servers, state_lock)
.boxed()
.await?;
}
Ok(knock_room::v3::Response::new(room_id.to_owned()))
}
async fn knock_room_helper_local(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
reason: Option<String>,
servers: &[OwnedServerName],
state_lock: RoomMutexGuard,
) -> Result {
debug_info!("We can knock locally");
let room_version_id = services.rooms.state.get_room_version(room_id).await?;
if matches!(
room_version_id,
RoomVersionId::V1
| RoomVersionId::V2
| RoomVersionId::V3
| RoomVersionId::V4
| RoomVersionId::V5
| RoomVersionId::V6
) {
return Err!(Request(Forbidden("This room does not support knocking.")));
}
let content = RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(),
reason: reason.clone(),
..RoomMemberEventContent::new(MembershipState::Knock)
};
// Try normal knock first
let Err(error) = services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(sender_user.to_string(), &content),
sender_user,
room_id,
&state_lock,
)
.await
else {
return Ok(());
};
if servers.is_empty() || (servers.len() == 1 && services.globals.server_is_ours(&servers[0]))
{
return Err(error);
}
warn!("We couldn't do the knock locally, maybe federation can help to satisfy the knock");
let (make_knock_response, remote_server) =
make_knock_request(services, sender_user, room_id, servers).await?;
info!("make_knock finished");
let room_version_id = make_knock_response.room_version;
if !services.server.supported_room_version(&room_version_id) {
return Err!(BadServerResponse(
"Remote room version {room_version_id} is not supported by conduwuit"
));
}
let mut knock_event_stub = serde_json::from_str::<CanonicalJsonObject>(
make_knock_response.event.get(),
)
.map_err(|e| {
err!(BadServerResponse("Invalid make_knock event json received from server: {e:?}"))
})?;
knock_event_stub.insert(
"origin".to_owned(),
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
);
knock_event_stub.insert(
"origin_server_ts".to_owned(),
CanonicalJsonValue::Integer(
utils::millis_since_unix_epoch()
.try_into()
.expect("Timestamp is valid js_int value"),
),
);
knock_event_stub.insert(
"content".to_owned(),
to_canonical_value(RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(),
reason,
..RoomMemberEventContent::new(MembershipState::Knock)
})
.expect("event is valid, we just created it"),
);
// In order to create a compatible ref hash (EventID) the `hashes` field needs
// to be present
services
.server_keys
.hash_and_sign_event(&mut knock_event_stub, &room_version_id)?;
// Generate event id
let event_id = gen_event_id(&knock_event_stub, &room_version_id)?;
// Add event_id
knock_event_stub
.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.clone().into()));
// It has enough fields to be called a proper event now
let knock_event = knock_event_stub;
info!("Asking {remote_server} for send_knock in room {room_id}");
let send_knock_request = federation::knock::send_knock::v1::Request {
room_id: room_id.to_owned(),
event_id: event_id.clone(),
pdu: services
.sending
.convert_to_outgoing_federation_event(knock_event.clone())
.await,
};
let send_knock_response = services
.sending
.send_federation_request(&remote_server, send_knock_request)
.await?;
info!("send_knock finished");
services
.rooms
.short
.get_or_create_shortroomid(room_id)
.await;
info!("Parsing knock event");
let parsed_knock_pdu = PduEvent::from_id_val(&event_id, knock_event.clone())
.map_err(|e| err!(BadServerResponse("Invalid knock event PDU: {e:?}")))?;
info!("Updating membership locally to knock state with provided stripped state events");
services
.rooms
.state_cache
.update_membership(
room_id,
sender_user,
parsed_knock_pdu
.get_content::<RoomMemberEventContent>()
.expect("we just created this"),
sender_user,
Some(send_knock_response.knock_room_state),
None,
false,
)
.await?;
info!("Appending room knock event locally");
services
.rooms
.timeline
.append_pdu(
&parsed_knock_pdu,
knock_event,
once(parsed_knock_pdu.event_id.borrow()),
&state_lock,
)
.await?;
Ok(())
}
async fn knock_room_helper_remote(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
reason: Option<String>,
servers: &[OwnedServerName],
state_lock: RoomMutexGuard,
) -> Result {
info!("Knocking {room_id} over federation.");
let (make_knock_response, remote_server) =
make_knock_request(services, sender_user, room_id, servers).await?;
info!("make_knock finished");
let room_version_id = make_knock_response.room_version;
if !services.server.supported_room_version(&room_version_id) {
return Err!(BadServerResponse(
"Remote room version {room_version_id} is not supported by conduwuit"
));
}
let mut knock_event_stub: CanonicalJsonObject =
serde_json::from_str(make_knock_response.event.get()).map_err(|e| {
err!(BadServerResponse("Invalid make_knock event json received from server: {e:?}"))
})?;
knock_event_stub.insert(
"origin".to_owned(),
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
);
knock_event_stub.insert(
"origin_server_ts".to_owned(),
CanonicalJsonValue::Integer(
utils::millis_since_unix_epoch()
.try_into()
.expect("Timestamp is valid js_int value"),
),
);
knock_event_stub.insert(
"content".to_owned(),
to_canonical_value(RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(),
reason,
..RoomMemberEventContent::new(MembershipState::Knock)
})
.expect("event is valid, we just created it"),
);
// In order to create a compatible ref hash (EventID) the `hashes` field needs
// to be present
services
.server_keys
.hash_and_sign_event(&mut knock_event_stub, &room_version_id)?;
// Generate event id
let event_id = gen_event_id(&knock_event_stub, &room_version_id)?;
// Add event_id
knock_event_stub
.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.clone().into()));
// It has enough fields to be called a proper event now
let knock_event = knock_event_stub;
info!("Asking {remote_server} for send_knock in room {room_id}");
let send_knock_request = federation::knock::send_knock::v1::Request {
room_id: room_id.to_owned(),
event_id: event_id.clone(),
pdu: services
.sending
.convert_to_outgoing_federation_event(knock_event.clone())
.await,
};
let send_knock_response = services
.sending
.send_federation_request(&remote_server, send_knock_request)
.await?;
info!("send_knock finished");
services
.rooms
.short
.get_or_create_shortroomid(room_id)
.await;
info!("Parsing knock event");
let parsed_knock_pdu = PduEvent::from_id_val(&event_id, knock_event.clone())
.map_err(|e| err!(BadServerResponse("Invalid knock event PDU: {e:?}")))?;
info!("Going through send_knock response knock state events");
let state = send_knock_response
.knock_room_state
.iter()
.map(|event| serde_json::from_str::<CanonicalJsonObject>(event.clone().into_json().get()))
.filter_map(Result::ok);
let mut state_map: HashMap<u64, OwnedEventId> = HashMap::new();
for event in state {
let Some(state_key) = event.get("state_key") else {
debug_warn!("send_knock stripped state event missing state_key: {event:?}");
continue;
};
let Some(event_type) = event.get("type") else {
debug_warn!("send_knock stripped state event missing event type: {event:?}");
continue;
};
let Ok(state_key) = serde_json::from_value::<String>(state_key.clone().into()) else {
debug_warn!("send_knock stripped state event has invalid state_key: {event:?}");
continue;
};
let Ok(event_type) = serde_json::from_value::<StateEventType>(event_type.clone().into())
else {
debug_warn!("send_knock stripped state event has invalid event type: {event:?}");
continue;
};
let event_id = gen_event_id(&event, &room_version_id)?;
let shortstatekey = services
.rooms
.short
.get_or_create_shortstatekey(&event_type, &state_key)
.await;
services.rooms.outlier.add_pdu_outlier(&event_id, &event);
state_map.insert(shortstatekey, event_id.clone());
}
info!("Compressing state from send_knock");
let compressed: CompressedState = services
.rooms
.state_compressor
.compress_state_events(state_map.iter().map(|(ssk, eid)| (ssk, eid.borrow())))
.collect()
.await;
debug!("Saving compressed state");
let HashSetCompressStateEvent {
shortstatehash: statehash_before_knock,
added,
removed,
} = services
.rooms
.state_compressor
.save_state(room_id, Arc::new(compressed))
.await?;
debug!("Forcing state for new room");
services
.rooms
.state
.force_state(room_id, statehash_before_knock, added, removed, &state_lock)
.await?;
let statehash_after_knock = services
.rooms
.state
.append_to_state(&parsed_knock_pdu)
.await?;
info!("Updating membership locally to knock state with provided stripped state events");
services
.rooms
.state_cache
.update_membership(
room_id,
sender_user,
parsed_knock_pdu
.get_content::<RoomMemberEventContent>()
.expect("we just created this"),
sender_user,
Some(send_knock_response.knock_room_state),
None,
false,
)
.await?;
info!("Appending room knock event locally");
services
.rooms
.timeline
.append_pdu(
&parsed_knock_pdu,
knock_event,
once(parsed_knock_pdu.event_id.borrow()),
&state_lock,
)
.await?;
info!("Setting final room state for new room");
// We set the room state after inserting the pdu, so that we never have a moment
// in time where events in the current room state do not exist
services
.rooms
.state
.set_room_state(room_id, statehash_after_knock, &state_lock);
Ok(())
}
async fn make_knock_request(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
servers: &[OwnedServerName],
) -> Result<(federation::knock::create_knock_event_template::v1::Response, OwnedServerName)> {
let mut make_knock_response_and_server =
Err!(BadServerResponse("No server available to assist in knocking."));
let mut make_knock_counter: usize = 0;
for remote_server in servers {
if services.globals.server_is_ours(remote_server) {
continue;
}
info!("Asking {remote_server} for make_knock ({make_knock_counter})");
let make_knock_response = services
.sending
.send_federation_request(
remote_server,
federation::knock::create_knock_event_template::v1::Request {
room_id: room_id.to_owned(),
user_id: sender_user.to_owned(),
ver: services.server.supported_room_versions().collect(),
},
)
.await;
trace!("make_knock response: {make_knock_response:?}");
make_knock_counter = make_knock_counter.saturating_add(1);
make_knock_response_and_server = make_knock_response.map(|r| (r, remote_server.clone()));
if make_knock_response_and_server.is_ok() {
break;
}
if make_knock_counter > 40 {
warn!(
"50 servers failed to provide valid make_knock response, assuming no server can \
assist in knocking."
);
make_knock_response_and_server =
Err!(BadServerResponse("No server available to assist in knocking."));
return make_knock_response_and_server;
}
}
make_knock_response_and_server
}

View file

@ -1,386 +0,0 @@
use std::collections::HashSet;
use axum::extract::State;
use conduwuit::{
Err, Result, debug_info, debug_warn, err,
matrix::{event::gen_event_id, pdu::PduBuilder},
utils::{self, FutureBoolExt, future::ReadyEqExt},
warn,
};
use futures::{FutureExt, StreamExt, TryFutureExt, pin_mut};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedServerName, RoomId, RoomVersionId, UserId,
api::{
client::membership::leave_room,
federation::{self},
},
events::{
StateEventType,
room::member::{MembershipState, RoomMemberEventContent},
},
};
use service::Services;
use crate::Ruma;
/// # `POST /_matrix/client/v3/rooms/{roomId}/leave`
///
/// Tries to leave the sender user from a room.
///
/// - This should always work if the user is currently joined.
pub(crate) async fn leave_room_route(
State(services): State<crate::State>,
body: Ruma<leave_room::v3::Request>,
) -> Result<leave_room::v3::Response> {
leave_room(&services, body.sender_user(), &body.room_id, body.reason.clone())
.boxed()
.await
.map(|()| leave_room::v3::Response::new())
}
// Make a user leave all their joined rooms, rescinds knocks, forgets all rooms,
// and ignores errors
pub async fn leave_all_rooms(services: &Services, user_id: &UserId) {
let rooms_joined = services
.rooms
.state_cache
.rooms_joined(user_id)
.map(ToOwned::to_owned);
let rooms_invited = services
.rooms
.state_cache
.rooms_invited(user_id)
.map(|(r, _)| r);
let rooms_knocked = services
.rooms
.state_cache
.rooms_knocked(user_id)
.map(|(r, _)| r);
let all_rooms: Vec<_> = rooms_joined
.chain(rooms_invited)
.chain(rooms_knocked)
.collect()
.await;
for room_id in all_rooms {
// ignore errors
if let Err(e) = leave_room(services, user_id, &room_id, None).boxed().await {
warn!(%user_id, "Failed to leave {room_id} remotely: {e}");
}
services.rooms.state_cache.forget(&room_id, user_id);
}
}
pub async fn leave_room(
services: &Services,
user_id: &UserId,
room_id: &RoomId,
reason: Option<String>,
) -> Result {
let default_member_content = RoomMemberEventContent {
membership: MembershipState::Leave,
reason: reason.clone(),
join_authorized_via_users_server: None,
is_direct: None,
avatar_url: None,
displayname: None,
third_party_invite: None,
blurhash: None,
redact_events: None,
};
let is_banned = services.rooms.metadata.is_banned(room_id);
let is_disabled = services.rooms.metadata.is_disabled(room_id);
pin_mut!(is_banned, is_disabled);
if is_banned.or(is_disabled).await {
// the room is banned/disabled, the room must be rejected locally since we
// cant/dont want to federate with this server
services
.rooms
.state_cache
.update_membership(
room_id,
user_id,
default_member_content,
user_id,
None,
None,
true,
)
.await?;
return Ok(());
}
let dont_have_room = services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), room_id)
.eq(&false);
let not_knocked = services
.rooms
.state_cache
.is_knocked(user_id, room_id)
.eq(&false);
// Ask a remote server if we don't have this room and are not knocking on it
if dont_have_room.and(not_knocked).await {
if let Err(e) = remote_leave_room(services, user_id, room_id, reason.clone())
.boxed()
.await
{
warn!(%user_id, "Failed to leave room {room_id} remotely: {e}");
// Don't tell the client about this error
}
let last_state = services
.rooms
.state_cache
.invite_state(user_id, room_id)
.or_else(|_| services.rooms.state_cache.knock_state(user_id, room_id))
.or_else(|_| services.rooms.state_cache.left_state(user_id, room_id))
.await
.ok();
// We always drop the invite, we can't rely on other servers
services
.rooms
.state_cache
.update_membership(
room_id,
user_id,
default_member_content,
user_id,
last_state,
None,
true,
)
.await?;
} else {
let state_lock = services.rooms.state.mutex.lock(room_id).await;
let Ok(event) = services
.rooms
.state_accessor
.room_state_get_content::<RoomMemberEventContent>(
room_id,
&StateEventType::RoomMember,
user_id.as_str(),
)
.await
else {
debug_warn!(
"Trying to leave a room you are not a member of, marking room as left locally."
);
return services
.rooms
.state_cache
.update_membership(
room_id,
user_id,
default_member_content,
user_id,
None,
None,
true,
)
.await;
};
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
membership: MembershipState::Leave,
reason,
join_authorized_via_users_server: None,
is_direct: None,
..event
}),
user_id,
room_id,
&state_lock,
)
.await?;
}
Ok(())
}
async fn remote_leave_room(
services: &Services,
user_id: &UserId,
room_id: &RoomId,
reason: Option<String>,
) -> Result<()> {
let mut make_leave_response_and_server =
Err!(BadServerResponse("No remote server available to assist in leaving {room_id}."));
let mut servers: HashSet<OwnedServerName> = services
.rooms
.state_cache
.servers_invite_via(room_id)
.map(ToOwned::to_owned)
.collect()
.await;
match services
.rooms
.state_cache
.invite_state(user_id, room_id)
.await
{
| Ok(invite_state) => {
servers.extend(
invite_state
.iter()
.filter_map(|event| event.get_field("sender").ok().flatten())
.filter_map(|sender: &str| UserId::parse(sender).ok())
.map(|user| user.server_name().to_owned()),
);
},
| _ => {
match services
.rooms
.state_cache
.knock_state(user_id, room_id)
.await
{
| Ok(knock_state) => {
servers.extend(
knock_state
.iter()
.filter_map(|event| event.get_field("sender").ok().flatten())
.filter_map(|sender: &str| UserId::parse(sender).ok())
.filter_map(|sender| {
if !services.globals.user_is_local(sender) {
Some(sender.server_name().to_owned())
} else {
None
}
}),
);
},
| _ => {},
}
},
}
if let Some(room_id_server_name) = room_id.server_name() {
servers.insert(room_id_server_name.to_owned());
}
debug_info!("servers in remote_leave_room: {servers:?}");
for remote_server in servers {
let make_leave_response = services
.sending
.send_federation_request(
&remote_server,
federation::membership::prepare_leave_event::v1::Request {
room_id: room_id.to_owned(),
user_id: user_id.to_owned(),
},
)
.await;
make_leave_response_and_server = make_leave_response.map(|r| (r, remote_server));
if make_leave_response_and_server.is_ok() {
break;
}
}
let (make_leave_response, remote_server) = make_leave_response_and_server?;
let Some(room_version_id) = make_leave_response.room_version else {
return Err!(BadServerResponse(warn!(
"No room version was returned by {remote_server} for {room_id}, room version is \
likely not supported by conduwuit"
)));
};
if !services.server.supported_room_version(&room_version_id) {
return Err!(BadServerResponse(warn!(
"Remote room version {room_version_id} for {room_id} is not supported by conduwuit",
)));
}
let mut leave_event_stub = serde_json::from_str::<CanonicalJsonObject>(
make_leave_response.event.get(),
)
.map_err(|e| {
err!(BadServerResponse(warn!(
"Invalid make_leave event json received from {remote_server} for {room_id}: {e:?}"
)))
})?;
// TODO: Is origin needed?
leave_event_stub.insert(
"origin".to_owned(),
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
);
leave_event_stub.insert(
"origin_server_ts".to_owned(),
CanonicalJsonValue::Integer(
utils::millis_since_unix_epoch()
.try_into()
.expect("Timestamp is valid js_int value"),
),
);
// Inject the reason key into the event content dict if it exists
if let Some(reason) = reason {
if let Some(CanonicalJsonValue::Object(content)) = leave_event_stub.get_mut("content") {
content.insert("reason".to_owned(), CanonicalJsonValue::String(reason));
}
}
// room v3 and above removed the "event_id" field from remote PDU format
match room_version_id {
| RoomVersionId::V1 | RoomVersionId::V2 => {},
| _ => {
leave_event_stub.remove("event_id");
},
}
// In order to create a compatible ref hash (EventID) the `hashes` field needs
// to be present
services
.server_keys
.hash_and_sign_event(&mut leave_event_stub, &room_version_id)?;
// Generate event id
let event_id = gen_event_id(&leave_event_stub, &room_version_id)?;
// Add event_id back
leave_event_stub
.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.clone().into()));
// It has enough fields to be called a proper event now
let leave_event = leave_event_stub;
services
.sending
.send_federation_request(
&remote_server,
federation::membership::create_leave_event::v2::Request {
room_id: room_id.to_owned(),
event_id,
pdu: services
.sending
.convert_to_outgoing_federation_event(leave_event.clone())
.await,
},
)
.await?;
Ok(())
}

View file

@ -1,147 +0,0 @@
use axum::extract::State;
use conduwuit::{
Err, Event, Result, at,
utils::{
future::TryExtExt,
stream::{BroadbandExt, ReadyExt},
},
};
use futures::{FutureExt, StreamExt, future::join};
use ruma::{
api::client::membership::{
get_member_events::{self, v3::MembershipEventFilter},
joined_members::{self, v3::RoomMember},
},
events::{
StateEventType,
room::member::{MembershipState, RoomMemberEventContent},
},
};
use crate::Ruma;
/// # `POST /_matrix/client/r0/rooms/{roomId}/members`
///
/// Lists all joined users in a room (TODO: at a specific point in time, with a
/// specific membership).
///
/// - Only works if the user is currently joined
pub(crate) async fn get_member_events_route(
State(services): State<crate::State>,
body: Ruma<get_member_events::v3::Request>,
) -> Result<get_member_events::v3::Response> {
let sender_user = body.sender_user();
let membership = body.membership.as_ref();
let not_membership = body.not_membership.as_ref();
if !services
.rooms
.state_accessor
.user_can_see_state_events(sender_user, &body.room_id)
.await
{
return Err!(Request(Forbidden("You don't have permission to view this room.")));
}
Ok(get_member_events::v3::Response {
chunk: services
.rooms
.state_accessor
.room_state_full(&body.room_id)
.ready_filter_map(Result::ok)
.ready_filter(|((ty, _), _)| *ty == StateEventType::RoomMember)
.map(at!(1))
.ready_filter_map(|pdu| membership_filter(pdu, membership, not_membership))
.map(Event::into_format)
.collect()
.boxed()
.await,
})
}
/// # `POST /_matrix/client/r0/rooms/{roomId}/joined_members`
///
/// Lists all members of a room.
///
/// - The sender user must be in the room
/// - TODO: An appservice just needs a puppet joined
pub(crate) async fn joined_members_route(
State(services): State<crate::State>,
body: Ruma<joined_members::v3::Request>,
) -> Result<joined_members::v3::Response> {
if !services
.rooms
.state_accessor
.user_can_see_state_events(body.sender_user(), &body.room_id)
.await
{
return Err!(Request(Forbidden("You don't have permission to view this room.")));
}
Ok(joined_members::v3::Response {
joined: services
.rooms
.state_cache
.room_members(&body.room_id)
.map(ToOwned::to_owned)
.broad_then(|user_id| async move {
let (display_name, avatar_url) = join(
services.users.displayname(&user_id).ok(),
services.users.avatar_url(&user_id).ok(),
)
.await;
(user_id, RoomMember { display_name, avatar_url })
})
.collect()
.await,
})
}
fn membership_filter<Pdu: Event>(
pdu: Pdu,
for_membership: Option<&MembershipEventFilter>,
not_membership: Option<&MembershipEventFilter>,
) -> Option<impl Event> {
let membership_state_filter = match for_membership {
| Some(MembershipEventFilter::Ban) => MembershipState::Ban,
| Some(MembershipEventFilter::Invite) => MembershipState::Invite,
| Some(MembershipEventFilter::Knock) => MembershipState::Knock,
| Some(MembershipEventFilter::Leave) => MembershipState::Leave,
| Some(_) | None => MembershipState::Join,
};
let not_membership_state_filter = match not_membership {
| Some(MembershipEventFilter::Ban) => MembershipState::Ban,
| Some(MembershipEventFilter::Invite) => MembershipState::Invite,
| Some(MembershipEventFilter::Join) => MembershipState::Join,
| Some(MembershipEventFilter::Knock) => MembershipState::Knock,
| Some(_) | None => MembershipState::Leave,
};
let evt_membership = pdu.get_content::<RoomMemberEventContent>().ok()?.membership;
if for_membership.is_some() && not_membership.is_some() {
if membership_state_filter != evt_membership
|| not_membership_state_filter == evt_membership
{
None
} else {
Some(pdu)
}
} else if for_membership.is_some() && not_membership.is_none() {
if membership_state_filter != evt_membership {
None
} else {
Some(pdu)
}
} else if not_membership.is_some() && for_membership.is_none() {
if not_membership_state_filter == evt_membership {
None
} else {
Some(pdu)
}
} else {
Some(pdu)
}
}

View file

@ -1,156 +0,0 @@
mod ban;
mod forget;
mod invite;
mod join;
mod kick;
mod knock;
mod leave;
mod members;
mod unban;
use std::net::IpAddr;
use axum::extract::State;
use conduwuit::{Err, Result, warn};
use futures::{FutureExt, StreamExt};
use ruma::{OwnedRoomId, RoomId, ServerName, UserId, api::client::membership::joined_rooms};
use service::Services;
pub(crate) use self::{
ban::ban_user_route,
forget::forget_room_route,
invite::{invite_helper, invite_user_route},
join::{join_room_by_id_or_alias_route, join_room_by_id_route},
kick::kick_user_route,
knock::knock_room_route,
leave::leave_room_route,
members::{get_member_events_route, joined_members_route},
unban::unban_user_route,
};
pub use self::{
join::join_room_by_id_helper,
leave::{leave_all_rooms, leave_room},
};
use crate::{Ruma, client::full_user_deactivate};
/// # `POST /_matrix/client/r0/joined_rooms`
///
/// Lists all rooms the user has joined.
pub(crate) async fn joined_rooms_route(
State(services): State<crate::State>,
body: Ruma<joined_rooms::v3::Request>,
) -> Result<joined_rooms::v3::Response> {
Ok(joined_rooms::v3::Response {
joined_rooms: services
.rooms
.state_cache
.rooms_joined(body.sender_user())
.map(ToOwned::to_owned)
.collect()
.await,
})
}
/// Checks if the room is banned in any way possible and the sender user is not
/// an admin.
///
/// Performs automatic deactivation if `auto_deactivate_banned_room_attempts` is
/// enabled
#[tracing::instrument(skip(services))]
pub(crate) async fn banned_room_check(
services: &Services,
user_id: &UserId,
room_id: Option<&RoomId>,
server_name: Option<&ServerName>,
client_ip: IpAddr,
) -> Result {
if services.users.is_admin(user_id).await {
return Ok(());
}
if let Some(room_id) = room_id {
if services.rooms.metadata.is_banned(room_id).await
|| services
.moderation
.is_remote_server_forbidden(room_id.server_name().expect("legacy room mxid"))
{
warn!(
"User {user_id} who is not an admin attempted to send an invite for or \
attempted to join a banned room or banned room server name: {room_id}"
);
if services.server.config.auto_deactivate_banned_room_attempts {
warn!(
"Automatically deactivating user {user_id} due to attempted banned room join"
);
if services.server.config.admin_room_notices {
services
.admin
.send_text(&format!(
"Automatically deactivating user {user_id} due to attempted banned \
room join from IP {client_ip}"
))
.await;
}
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
.rooms_joined(user_id)
.map(Into::into)
.collect()
.await;
full_user_deactivate(services, user_id, &all_joined_rooms)
.boxed()
.await?;
}
return Err!(Request(Forbidden("This room is banned on this homeserver.")));
}
} else if let Some(server_name) = server_name {
if services
.config
.forbidden_remote_server_names
.is_match(server_name.host())
{
warn!(
"User {user_id} who is not an admin tried joining a room which has the server \
name {server_name} that is globally forbidden. Rejecting.",
);
if services.server.config.auto_deactivate_banned_room_attempts {
warn!(
"Automatically deactivating user {user_id} due to attempted banned room join"
);
if services.server.config.admin_room_notices {
services
.admin
.send_text(&format!(
"Automatically deactivating user {user_id} due to attempted banned \
room join from IP {client_ip}"
))
.await;
}
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
.rooms_joined(user_id)
.map(Into::into)
.collect()
.await;
full_user_deactivate(services, user_id, &all_joined_rooms)
.boxed()
.await?;
}
return Err!(Request(Forbidden("This remote server is banned on this homeserver.")));
}
}
Ok(())
}

View file

@ -1,58 +0,0 @@
use axum::extract::State;
use conduwuit::{Err, Result, matrix::pdu::PduBuilder};
use ruma::{
api::client::membership::unban_user,
events::room::member::{MembershipState, RoomMemberEventContent},
};
use crate::Ruma;
/// # `POST /_matrix/client/r0/rooms/{roomId}/unban`
///
/// Tries to send an unban event into the room.
pub(crate) async fn unban_user_route(
State(services): State<crate::State>,
body: Ruma<unban_user::v3::Request>,
) -> Result<unban_user::v3::Response> {
let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
let current_member_content = services
.rooms
.state_accessor
.get_member(&body.room_id, &body.user_id)
.await
.unwrap_or_else(|_| RoomMemberEventContent::new(MembershipState::Leave));
if current_member_content.membership != MembershipState::Ban {
return Err!(Request(Forbidden(
"Cannot unban a user who is not banned (current membership: {})",
current_member_content.membership
)));
}
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(body.user_id.to_string(), &RoomMemberEventContent {
membership: MembershipState::Leave,
reason: body.reason.clone(),
join_authorized_via_users_server: None,
third_party_invite: None,
is_direct: None,
..current_member_content
}),
sender_user,
&body.room_id,
&state_lock,
)
.await?;
drop(state_lock);
Ok(unban_user::v3::Response::new())
}

View file

@ -1,11 +1,12 @@
use core::panic;
use axum::extract::State;
use conduwuit::{
Err, Result, at,
matrix::{
event::{Event, Matches},
pdu::PduCount,
Event,
pdu::{PduCount, PduEvent},
},
ref_at,
utils::{
IterStream, ReadyExt,
result::{FlatOk, LogErr},
@ -33,7 +34,6 @@ use ruma::{
},
serde::Raw,
};
use tracing::warn;
use crate::Ruma;
@ -73,7 +73,7 @@ pub(crate) async fn get_message_events_route(
) -> Result<get_message_events::v3::Response> {
debug_assert!(IGNORED_MESSAGE_TYPES.is_sorted(), "IGNORED_MESSAGE_TYPES is not sorted");
let sender_user = body.sender_user();
let sender_device = body.sender_device.as_deref();
let sender_device = body.sender_device.as_ref();
let room_id = &body.room_id;
let filter = &body.filter;
@ -137,17 +137,18 @@ pub(crate) async fn get_message_events_route(
let lazy_loading_context = lazy_loading::Context {
user_id: sender_user,
device_id: sender_device.or_else(|| {
if let Some(registration) = body.appservice_info.as_ref() {
Some(<&DeviceId>::from(registration.registration.id.as_str()))
} else {
warn!(
"No device_id provided and no appservice registration found, this should be \
unreachable"
);
None
}
}),
device_id: match sender_device {
| Some(device_id) => device_id,
| None =>
if let Some(registration) = body.appservice_info.as_ref() {
<&DeviceId>::from(registration.registration.id.as_str())
} else {
panic!(
"No device_id provided and no appservice registration found, this \
should be unreachable"
);
},
},
room_id,
token: Some(from.into_unsigned()),
options: Some(&filter.lazy_load_options),
@ -176,7 +177,7 @@ pub(crate) async fn get_message_events_route(
let chunk = events
.into_iter()
.map(at!(1))
.map(Event::into_format)
.map(PduEvent::into_room_event)
.collect();
Ok(get_message_events::v3::Response {
@ -217,9 +218,7 @@ where
pin_mut!(receipts);
let witness: Witness = events
.stream()
.map(ref_at!(1))
.map(Event::sender)
.map(ToOwned::to_owned)
.map(|(_, pdu)| pdu.sender.clone())
.chain(
receipts
.ready_take_while(|(_, c, _)| *c <= newest.into_unsigned())
@ -244,7 +243,7 @@ async fn get_member_event(
.rooms
.state_accessor
.room_state_get(room_id, &StateEventType::RoomMember, user_id.as_str())
.map_ok(Event::into_format)
.map_ok(PduEvent::into_state_event)
.await
.ok()
}
@ -264,33 +263,27 @@ pub(crate) async fn ignored_filter(
}
#[inline]
pub(crate) async fn is_ignored_pdu<Pdu>(
pub(crate) async fn is_ignored_pdu(
services: &Services,
event: &Pdu,
pdu: &PduEvent,
user_id: &UserId,
) -> bool
where
Pdu: Event + Send + Sync,
{
) -> bool {
// exclude Synapse's dummy events from bloating up response bodies. clients
// don't need to see this.
if event.kind().to_cow_str() == "org.matrix.dummy_event" {
if pdu.kind.to_cow_str() == "org.matrix.dummy_event" {
return true;
}
let ignored_type = IGNORED_MESSAGE_TYPES.binary_search(event.kind()).is_ok();
let ignored_type = IGNORED_MESSAGE_TYPES.binary_search(&pdu.kind).is_ok();
let ignored_server = services
.moderation
.is_remote_server_ignored(event.sender().server_name());
.is_remote_server_ignored(pdu.sender().server_name());
if ignored_type
&& (ignored_server
|| (!services.config.send_messages_from_ignored_users_to_client
&& services
.users
.user_is_ignored(event.sender(), user_id)
.await))
&& services.users.user_is_ignored(&pdu.sender, user_id).await))
{
return true;
}
@ -309,7 +302,7 @@ pub(crate) async fn visibility_filter(
services
.rooms
.state_accessor
.user_can_see_event(user_id, pdu.room_id(), pdu.event_id())
.user_can_see_event(user_id, &pdu.room_id, &pdu.event_id)
.await
.then_some(item)
}
@ -317,7 +310,7 @@ pub(crate) async fn visibility_filter(
#[inline]
pub(crate) fn event_filter(item: PdusIterItem, filter: &RoomEventFilter) -> Option<PdusIterItem> {
let (_, pdu) = &item;
filter.matches(pdu).then_some(item)
pdu.matches(filter).then_some(item)
}
#[cfg_attr(debug_assertions, conduwuit::ctor)]

View file

@ -1,8 +1,11 @@
use std::time::Duration;
use axum::extract::State;
use conduwuit::{Err, Result, utils};
use ruma::{api::client::account, authentication::TokenType};
use conduwuit::{Error, Result, utils};
use ruma::{
api::client::{account, error::ErrorKind},
authentication::TokenType,
};
use super::TOKEN_LENGTH;
use crate::Ruma;
@ -16,15 +19,17 @@ pub(crate) async fn create_openid_token_route(
State(services): State<crate::State>,
body: Ruma<account::request_openid_token::v3::Request>,
) -> Result<account::request_openid_token::v3::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if sender_user != body.user_id {
return Err!(Request(InvalidParam(
if sender_user != &body.user_id {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Not allowed to request OpenID tokens on behalf of other users",
)));
));
}
let access_token = utils::random_string(TOKEN_LENGTH);
let expires_in = services
.users
.create_openid_token(&body.user_id, &access_token)?;

View file

@ -2,21 +2,21 @@ use std::collections::BTreeMap;
use axum::extract::State;
use conduwuit::{
Err, Result,
Err, Error, Result,
matrix::pdu::PduBuilder,
utils::{IterStream, future::TryExtExt, stream::TryIgnore},
utils::{IterStream, stream::TryIgnore},
warn,
};
use conduwuit_service::Services;
use futures::{
StreamExt, TryStreamExt,
future::{join, join3, join4},
};
use futures::{StreamExt, TryStreamExt, future::join3};
use ruma::{
OwnedMxcUri, OwnedRoomId, UserId,
api::{
client::profile::{
get_avatar_url, get_display_name, get_profile, set_avatar_url, set_display_name,
client::{
error::ErrorKind,
profile::{
get_avatar_url, get_display_name, get_profile, set_avatar_url, set_display_name,
},
},
federation,
},
@ -35,7 +35,7 @@ pub(crate) async fn set_displayname_route(
State(services): State<crate::State>,
body: Ruma<set_display_name::v3::Request>,
) -> Result<set_display_name::v3::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
@ -110,7 +110,7 @@ pub(crate) async fn get_displayname_route(
if !services.users.exists(&body.user_id).await {
// Return 404 if this user doesn't exist and we couldn't fetch it over
// federation
return Err!(Request(NotFound("Profile was not found.")));
return Err(Error::BadRequest(ErrorKind::NotFound, "Profile was not found."));
}
Ok(get_display_name::v3::Response {
@ -127,7 +127,7 @@ pub(crate) async fn set_avatar_url_route(
State(services): State<crate::State>,
body: Ruma<set_avatar_url::v3::Request>,
) -> Result<set_avatar_url::v3::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
@ -195,9 +195,11 @@ pub(crate) async fn get_avatar_url_route(
services
.users
.set_displayname(&body.user_id, response.displayname.clone());
services
.users
.set_avatar_url(&body.user_id, response.avatar_url.clone());
services
.users
.set_blurhash(&body.user_id, response.blurhash.clone());
@ -212,16 +214,13 @@ pub(crate) async fn get_avatar_url_route(
if !services.users.exists(&body.user_id).await {
// Return 404 if this user doesn't exist and we couldn't fetch it over
// federation
return Err!(Request(NotFound("Profile was not found.")));
return Err(Error::BadRequest(ErrorKind::NotFound, "Profile was not found."));
}
let (avatar_url, blurhash) = join(
services.users.avatar_url(&body.user_id).ok(),
services.users.blurhash(&body.user_id).ok(),
)
.await;
Ok(get_avatar_url::v3::Response { avatar_url, blurhash })
Ok(get_avatar_url::v3::Response {
avatar_url: services.users.avatar_url(&body.user_id).await.ok(),
blurhash: services.users.blurhash(&body.user_id).await.ok(),
})
}
/// # `GET /_matrix/client/v3/profile/{userId}`
@ -254,12 +253,15 @@ pub(crate) async fn get_profile_route(
services
.users
.set_displayname(&body.user_id, response.displayname.clone());
services
.users
.set_avatar_url(&body.user_id, response.avatar_url.clone());
services
.users
.set_blurhash(&body.user_id, response.blurhash.clone());
services
.users
.set_timezone(&body.user_id, response.tz.clone());
@ -285,7 +287,7 @@ pub(crate) async fn get_profile_route(
if !services.users.exists(&body.user_id).await {
// Return 404 if this user doesn't exist and we couldn't fetch it over
// federation
return Err!(Request(NotFound("Profile was not found.")));
return Err(Error::BadRequest(ErrorKind::NotFound, "Profile was not found."));
}
let mut custom_profile_fields: BTreeMap<String, serde_json::Value> = services
@ -298,19 +300,11 @@ pub(crate) async fn get_profile_route(
custom_profile_fields.remove("us.cloke.msc4175.tz");
custom_profile_fields.remove("m.tz");
let (avatar_url, blurhash, displayname, tz) = join4(
services.users.avatar_url(&body.user_id).ok(),
services.users.blurhash(&body.user_id).ok(),
services.users.displayname(&body.user_id).ok(),
services.users.timezone(&body.user_id).ok(),
)
.await;
Ok(get_profile::v3::Response {
avatar_url,
blurhash,
displayname,
tz,
avatar_url: services.users.avatar_url(&body.user_id).await.ok(),
blurhash: services.users.blurhash(&body.user_id).await.ok(),
displayname: services.users.displayname(&body.user_id).await.ok(),
tz: services.users.timezone(&body.user_id).await.ok(),
custom_profile_fields,
})
}
@ -322,12 +316,16 @@ pub async fn update_displayname(
all_joined_rooms: &[OwnedRoomId],
) {
let (current_avatar_url, current_blurhash, current_displayname) = join3(
services.users.avatar_url(user_id).ok(),
services.users.blurhash(user_id).ok(),
services.users.displayname(user_id).ok(),
services.users.avatar_url(user_id),
services.users.blurhash(user_id),
services.users.displayname(user_id),
)
.await;
let current_avatar_url = current_avatar_url.ok();
let current_blurhash = current_blurhash.ok();
let current_displayname = current_displayname.ok();
if displayname == current_displayname {
return;
}
@ -371,12 +369,16 @@ pub async fn update_avatar_url(
all_joined_rooms: &[OwnedRoomId],
) {
let (current_avatar_url, current_blurhash, current_displayname) = join3(
services.users.avatar_url(user_id).ok(),
services.users.blurhash(user_id).ok(),
services.users.displayname(user_id).ok(),
services.users.avatar_url(user_id),
services.users.blurhash(user_id),
services.users.displayname(user_id),
)
.await;
let current_avatar_url = current_avatar_url.ok();
let current_blurhash = current_blurhash.ok();
let current_displayname = current_displayname.ok();
if current_avatar_url == avatar_url && current_blurhash == blurhash {
return;
}

View file

@ -79,14 +79,17 @@ pub(crate) async fn get_pushrules_all_route(
global_ruleset.update_with_server_default(Ruleset::server_default(sender_user));
let ty = GlobalAccountDataEventType::PushRules;
let event = PushRulesEvent {
content: PushRulesEventContent { global: global_ruleset.clone() },
};
services
.account_data
.update(None, sender_user, ty.to_string().into(), &serde_json::to_value(event)?)
.update(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(PushRulesEvent {
content: PushRulesEventContent { global: global_ruleset.clone() },
})
.expect("to json always works"),
)
.await?;
}
};
@ -103,7 +106,7 @@ pub(crate) async fn get_pushrules_global_route(
State(services): State<crate::State>,
body: Ruma<get_pushrules_global_scope::v3::Request>,
) -> Result<get_pushrules_global_scope::v3::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let Some(content_value) = services
.account_data
@ -115,17 +118,19 @@ pub(crate) async fn get_pushrules_global_route(
else {
// user somehow has non-existent push rule event. recreate it and return server
// default silently
let ty = GlobalAccountDataEventType::PushRules;
let event = PushRulesEvent {
content: PushRulesEventContent {
global: Ruleset::server_default(sender_user),
},
};
services
.account_data
.update(None, sender_user, ty.to_string().into(), &serde_json::to_value(event)?)
.update(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(PushRulesEvent {
content: PushRulesEventContent {
global: Ruleset::server_default(sender_user),
},
})
.expect("to json always works"),
)
.await?;
return Ok(get_pushrules_global_scope::v3::Response {
@ -218,7 +223,7 @@ pub(crate) async fn get_pushrule_route(
if let Some(rule) = rule {
Ok(get_pushrule::v3::Response { rule })
} else {
Err!(Request(NotFound("Push rule not found.")))
Err(Error::BadRequest(ErrorKind::NotFound, "Push rule not found."))
}
}
@ -229,8 +234,9 @@ pub(crate) async fn set_pushrule_route(
State(services): State<crate::State>,
body: Ruma<set_pushrule::v3::Request>,
) -> Result<set_pushrule::v3::Response> {
let sender_user = body.sender_user();
let body = &body.body;
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let body = body.body;
let mut account_data: PushRulesEvent = services
.account_data
.get_global(sender_user, GlobalAccountDataEventType::PushRules)
@ -269,10 +275,14 @@ pub(crate) async fn set_pushrule_route(
return Err(err);
}
let ty = GlobalAccountDataEventType::PushRules;
services
.account_data
.update(None, sender_user, ty.to_string().into(), &serde_json::to_value(account_data)?)
.update(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(account_data).expect("to json value always works"),
)
.await?;
Ok(set_pushrule::v3::Response {})
@ -285,7 +295,7 @@ pub(crate) async fn get_pushrule_actions_route(
State(services): State<crate::State>,
body: Ruma<get_pushrule_actions::v3::Request>,
) -> Result<get_pushrule_actions::v3::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
// remove old deprecated mentions push rules as per MSC4210
#[allow(deprecated)]
@ -319,7 +329,7 @@ pub(crate) async fn set_pushrule_actions_route(
State(services): State<crate::State>,
body: Ruma<set_pushrule_actions::v3::Request>,
) -> Result<set_pushrule_actions::v3::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let mut account_data: PushRulesEvent = services
.account_data
@ -333,13 +343,17 @@ pub(crate) async fn set_pushrule_actions_route(
.set_actions(body.kind.clone(), &body.rule_id, body.actions.clone())
.is_err()
{
return Err!(Request(NotFound("Push rule not found.")));
return Err(Error::BadRequest(ErrorKind::NotFound, "Push rule not found."));
}
let ty = GlobalAccountDataEventType::PushRules;
services
.account_data
.update(None, sender_user, ty.to_string().into(), &serde_json::to_value(account_data)?)
.update(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(account_data).expect("to json value always works"),
)
.await?;
Ok(set_pushrule_actions::v3::Response {})
@ -352,7 +366,7 @@ pub(crate) async fn get_pushrule_enabled_route(
State(services): State<crate::State>,
body: Ruma<get_pushrule_enabled::v3::Request>,
) -> Result<get_pushrule_enabled::v3::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
// remove old deprecated mentions push rules as per MSC4210
#[allow(deprecated)]
@ -386,7 +400,7 @@ pub(crate) async fn set_pushrule_enabled_route(
State(services): State<crate::State>,
body: Ruma<set_pushrule_enabled::v3::Request>,
) -> Result<set_pushrule_enabled::v3::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let mut account_data: PushRulesEvent = services
.account_data
@ -400,13 +414,17 @@ pub(crate) async fn set_pushrule_enabled_route(
.set_enabled(body.kind.clone(), &body.rule_id, body.enabled)
.is_err()
{
return Err!(Request(NotFound("Push rule not found.")));
return Err(Error::BadRequest(ErrorKind::NotFound, "Push rule not found."));
}
let ty = GlobalAccountDataEventType::PushRules;
services
.account_data
.update(None, sender_user, ty.to_string().into(), &serde_json::to_value(account_data)?)
.update(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(account_data).expect("to json value always works"),
)
.await?;
Ok(set_pushrule_enabled::v3::Response {})
@ -419,7 +437,7 @@ pub(crate) async fn delete_pushrule_route(
State(services): State<crate::State>,
body: Ruma<delete_pushrule::v3::Request>,
) -> Result<delete_pushrule::v3::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let mut account_data: PushRulesEvent = services
.account_data
@ -445,10 +463,14 @@ pub(crate) async fn delete_pushrule_route(
return Err(err);
}
let ty = GlobalAccountDataEventType::PushRules;
services
.account_data
.update(None, sender_user, ty.to_string().into(), &serde_json::to_value(account_data)?)
.update(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(account_data).expect("to json value always works"),
)
.await?;
Ok(delete_pushrule::v3::Response {})
@ -461,7 +483,7 @@ pub(crate) async fn get_pushers_route(
State(services): State<crate::State>,
body: Ruma<get_pushers::v3::Request>,
) -> Result<get_pushers::v3::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
Ok(get_pushers::v3::Response {
pushers: services.pusher.get_pushers(sender_user).await,
@ -477,7 +499,7 @@ pub(crate) async fn set_pushers_route(
State(services): State<crate::State>,
body: Ruma<set_pusher::v3::Request>,
) -> Result<set_pusher::v3::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
services
.pusher
@ -493,16 +515,19 @@ async fn recreate_push_rules_and_return(
services: &Services,
sender_user: &ruma::UserId,
) -> Result<get_pushrules_all::v3::Response> {
let ty = GlobalAccountDataEventType::PushRules;
let event = PushRulesEvent {
content: PushRulesEventContent {
global: Ruleset::server_default(sender_user),
},
};
services
.account_data
.update(None, sender_user, ty.to_string().into(), &serde_json::to_value(event)?)
.update(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(PushRulesEvent {
content: PushRulesEventContent {
global: Ruleset::server_default(sender_user),
},
})
.expect("to json always works"),
)
.await?;
Ok(get_pushrules_all::v3::Response {

View file

@ -37,7 +37,7 @@ pub(crate) async fn set_read_marker_route(
Some(&body.room_id),
sender_user,
RoomAccountDataEventType::FullyRead,
&serde_json::to_value(fully_read_event)?,
&serde_json::to_value(fully_read_event).expect("to json value always works"),
)
.await?;
}
@ -151,7 +151,7 @@ pub(crate) async fn create_receipt_route(
Some(&body.room_id),
sender_user,
RoomAccountDataEventType::FullyRead,
&serde_json::to_value(fully_read_event)?,
&serde_json::to_value(fully_read_event).expect("to json value always works"),
)
.await?;
},

View file

@ -15,8 +15,8 @@ pub(crate) async fn redact_event_route(
State(services): State<crate::State>,
body: Ruma<redact_event::v3::Request>,
) -> Result<redact_event::v3::Response> {
let sender_user = body.sender_user();
let body = &body.body;
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let body = body.body;
if services.users.is_suspended(sender_user).await? {
// TODO: Users can redact their own messages while suspended
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));

View file

@ -1,10 +1,10 @@
use axum::extract::State;
use conduwuit::{
Result, at,
matrix::{Event, event::RelationTypeEqual, pdu::PduCount},
matrix::pdu::PduCount,
utils::{IterStream, ReadyExt, result::FlatOk, stream::WidebandExt},
};
use conduwuit_service::Services;
use conduwuit_service::{Services, rooms::timeline::PdusIterItem};
use futures::StreamExt;
use ruma::{
EventId, RoomId, UInt, UserId,
@ -129,7 +129,7 @@ async fn paginate_relations_with_filter(
// Spec (v1.10) recommends depth of at least 3
let depth: u8 = if recurse { 3 } else { 1 };
let events: Vec<_> = services
let events: Vec<PdusIterItem> = services
.rooms
.pdu_metadata
.get_relations(sender_user, room_id, target, start, limit, depth, dir)
@ -138,12 +138,12 @@ async fn paginate_relations_with_filter(
.filter(|(_, pdu)| {
filter_event_type
.as_ref()
.is_none_or(|kind| kind == pdu.kind())
.is_none_or(|kind| *kind == pdu.kind)
})
.filter(|(_, pdu)| {
filter_rel_type
.as_ref()
.is_none_or(|rel_type| rel_type.relation_type_equal(pdu))
.is_none_or(|rel_type| pdu.relation_type_equal(rel_type))
})
.stream()
.ready_take_while(|(count, _)| Some(*count) != to)
@ -167,22 +167,22 @@ async fn paginate_relations_with_filter(
chunk: events
.into_iter()
.map(at!(1))
.map(Event::into_format)
.map(|pdu| pdu.to_message_like_event())
.collect(),
})
}
async fn visibility_filter<Pdu: Event + Send + Sync>(
async fn visibility_filter(
services: &Services,
sender_user: &UserId,
item: (PduCount, Pdu),
) -> Option<(PduCount, Pdu)> {
item: PdusIterItem,
) -> Option<PdusIterItem> {
let (_, pdu) = &item;
services
.rooms
.state_accessor
.user_can_see_event(sender_user, pdu.room_id(), pdu.event_id())
.user_can_see_event(sender_user, &pdu.room_id, &pdu.event_id)
.await
.then_some(item)
}

View file

@ -2,12 +2,13 @@ use std::{fmt::Write as _, ops::Mul, time::Duration};
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{Err, Result, debug_info, info, matrix::pdu::PduEvent, utils::ReadyExt};
use conduwuit::{Err, Error, Result, debug_info, info, matrix::pdu::PduEvent, utils::ReadyExt};
use conduwuit_service::Services;
use rand::Rng;
use ruma::{
EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
api::client::{
error::ErrorKind,
report_user,
room::{report_content, report_room},
},
@ -37,14 +38,16 @@ pub(crate) async fn report_room_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<report_room::v3::Request>,
) -> Result<report_room::v3::Response> {
let sender_user = body.sender_user();
// user authentication
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
if body.reason.as_ref().is_some_and(|s| s.len() > 750) {
return Err!(Request(
InvalidParam("Reason too long, should be 750 characters or fewer",)
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Reason too long, should be 750 characters or fewer",
));
}
@ -91,7 +94,7 @@ pub(crate) async fn report_event_route(
body: Ruma<report_content::v3::Request>,
) -> Result<report_content::v3::Response> {
// user authentication
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
@ -147,8 +150,9 @@ pub(crate) async fn report_user_route(
}
if body.reason.as_ref().is_some_and(|s| s.len() > 750) {
return Err!(Request(
InvalidParam("Reason too long, should be 750 characters or fewer",)
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Reason too long, should be 750 characters or fewer",
));
}
@ -201,16 +205,23 @@ async fn is_event_report_valid(
);
if room_id != pdu.room_id {
return Err!(Request(NotFound("Event ID does not belong to the reported room",)));
return Err(Error::BadRequest(
ErrorKind::NotFound,
"Event ID does not belong to the reported room",
));
}
if score.is_some_and(|s| s > int!(0) || s < int!(-100)) {
return Err!(Request(InvalidParam("Invalid score, must be within 0 to -100",)));
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Invalid score, must be within 0 to -100",
));
}
if reason.as_ref().is_some_and(|s| s.len() > 750) {
return Err!(Request(
InvalidParam("Reason too long, should be 750 characters or fewer",)
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Reason too long, should be 750 characters or fewer",
));
}
@ -221,7 +232,10 @@ async fn is_event_report_valid(
.ready_any(|user_id| user_id == sender_user)
.await
{
return Err!(Request(NotFound("You are not in the room you are reporting.",)));
return Err(Error::BadRequest(
ErrorKind::NotFound,
"You are not in the room you are reporting.",
));
}
Ok(())
@ -259,6 +273,5 @@ async fn delay_response() {
"Got successful /report request, waiting {time_to_wait} seconds before sending \
successful response."
);
sleep(Duration::from_secs(time_to_wait)).await;
}

View file

@ -1,7 +1,7 @@
use axum::extract::State;
use conduwuit::{Err, Result};
use conduwuit::{Error, Result};
use futures::StreamExt;
use ruma::api::client::room::aliases;
use ruma::api::client::{error::ErrorKind, room::aliases};
use crate::Ruma;
@ -15,7 +15,7 @@ pub(crate) async fn get_room_aliases_route(
State(services): State<crate::State>,
body: Ruma<aliases::v3::Request>,
) -> Result<aliases::v3::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if !services
.rooms
@ -23,7 +23,10 @@ pub(crate) async fn get_room_aliases_route(
.user_can_see_state_events(sender_user, &body.room_id)
.await
{
return Err!(Request(Forbidden("You don't have permission to view this room.",)));
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"You don't have permission to view this room.",
));
}
Ok(aliases::v3::Response {

View file

@ -2,7 +2,7 @@ use std::collections::BTreeMap;
use axum::extract::State;
use conduwuit::{
Err, Result, debug_info, debug_warn, err, info,
Err, Error, Result, debug_info, debug_warn, err, error, info,
matrix::{StateKey, pdu::PduBuilder},
warn,
};
@ -10,7 +10,10 @@ use conduwuit_service::{Services, appservice::RegistrationInfo};
use futures::FutureExt;
use ruma::{
CanonicalJsonObject, Int, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId, RoomVersionId,
api::client::room::{self, create_room},
api::client::{
error::ErrorKind,
room::{self, create_room},
},
events::{
TimelineEventType,
room::{
@ -55,13 +58,16 @@ pub(crate) async fn create_room_route(
) -> Result<create_room::v3::Response> {
use create_room::v3::RoomPreset;
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if !services.globals.allow_room_creation()
&& body.appservice_info.is_none()
&& !services.users.is_admin(sender_user).await
{
return Err!(Request(Forbidden("Room creation has been disabled.",)));
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"Room creation has been disabled.",
));
}
if services.users.is_suspended(sender_user).await? {
@ -75,7 +81,10 @@ pub(crate) async fn create_room_route(
// check if room ID doesn't already exist instead of erroring on auth check
if services.rooms.short.get_shortroomid(&room_id).await.is_ok() {
return Err!(Request(RoomInUse("Room with that custom room ID already exists",)));
return Err(Error::BadRequest(
ErrorKind::RoomInUse,
"Room with that custom room ID already exists",
));
}
if body.visibility == room::Visibility::Public
@ -83,17 +92,19 @@ pub(crate) async fn create_room_route(
&& !services.users.is_admin(sender_user).await
&& body.appservice_info.is_none()
{
warn!(
"Non-admin user {sender_user} tried to publish {room_id} to the room directory \
while \"lockdown_public_room_directory\" is enabled"
info!(
"Non-admin user {sender_user} tried to publish {0} to the room directory while \
\"lockdown_public_room_directory\" is enabled",
&room_id
);
if services.server.config.admin_room_notices {
services
.admin
.notice(&format!(
"Non-admin user {sender_user} tried to publish {room_id} to the room \
directory while \"lockdown_public_room_directory\" is enabled"
.send_text(&format!(
"Non-admin user {sender_user} tried to publish {0} to the room directory \
while \"lockdown_public_room_directory\" is enabled",
&room_id
))
.await;
}
@ -118,9 +129,10 @@ pub(crate) async fn create_room_route(
if services.server.supported_room_version(&room_version) {
room_version
} else {
return Err!(Request(UnsupportedRoomVersion(
"This server does not support that room version."
)));
return Err(Error::BadRequest(
ErrorKind::UnsupportedRoomVersion,
"This server does not support that room version.",
));
},
| None => services.server.config.default_room_version.clone(),
};
@ -132,17 +144,16 @@ pub(crate) async fn create_room_route(
let mut content = content
.deserialize_as::<CanonicalJsonObject>()
.map_err(|e| {
err!(Request(BadJson(error!(
"Failed to deserialise content as canonical JSON: {e}"
))))
error!("Failed to deserialise content as canonical JSON: {}", e);
Error::bad_database("Failed to deserialise content as canonical JSON.")
})?;
match room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => {
content.insert(
"creator".into(),
json!(&sender_user).try_into().map_err(|e| {
err!(Request(BadJson(debug_error!("Invalid creation content: {e}"))))
info!("Invalid creation content: {e}");
Error::BadRequest(ErrorKind::BadJson, "Invalid creation content")
})?,
);
},
@ -152,9 +163,9 @@ pub(crate) async fn create_room_route(
}
content.insert(
"room_version".into(),
json!(room_version.as_str())
.try_into()
.map_err(|e| err!(Request(BadJson("Invalid creation content: {e}"))))?,
json!(room_version.as_str()).try_into().map_err(|_| {
Error::BadRequest(ErrorKind::BadJson, "Invalid creation content")
})?,
);
content
},
@ -163,13 +174,21 @@ pub(crate) async fn create_room_route(
let content = match room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 =>
RoomCreateEventContent::new_v1(sender_user.to_owned()),
RoomCreateEventContent::new_v1(sender_user.clone()),
| _ => RoomCreateEventContent::new_v11(),
};
let mut content =
serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&content)?.get())
.unwrap();
content.insert("room_version".into(), json!(room_version.as_str()).try_into()?);
let mut content = serde_json::from_str::<CanonicalJsonObject>(
to_raw_value(&content)
.expect("we just created this as content was None")
.get(),
)
.unwrap();
content.insert(
"room_version".into(),
json!(room_version.as_str())
.try_into()
.expect("we just created this as content was None"),
);
content
},
};
@ -181,7 +200,8 @@ pub(crate) async fn create_room_route(
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&create_content)?,
content: to_raw_value(&create_content)
.expect("create event content serialization"),
state_key: Some(StateKey::new()),
..Default::default()
},
@ -219,7 +239,7 @@ pub(crate) async fn create_room_route(
| _ => RoomPreset::PrivateChat, // Room visibility should not be custom
});
let mut users = BTreeMap::from_iter([(sender_user.to_owned(), int!(100))]);
let mut users = BTreeMap::from_iter([(sender_user.clone(), int!(100))]);
if preset == RoomPreset::TrustedPrivateChat {
for invite in &body.invite {
@ -247,7 +267,8 @@ pub(crate) async fn create_room_route(
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomPowerLevels,
content: to_raw_value(&power_levels_content)?,
content: to_raw_value(&power_levels_content)
.expect("serialized power_levels event content"),
state_key: Some(StateKey::new()),
..Default::default()
},
@ -336,7 +357,8 @@ pub(crate) async fn create_room_route(
// 6. Events listed in initial_state
for event in &body.initial_state {
let mut pdu_builder = event.deserialize_as::<PduBuilder>().map_err(|e| {
err!(Request(InvalidParam(warn!("Invalid initial state event: {e:?}"))))
warn!("Invalid initial state event: {:?}", e);
Error::BadRequest(ErrorKind::InvalidParam, "Invalid initial state event.")
})?;
debug_info!("Room creation initial state event: {event:?}");
@ -345,7 +367,7 @@ pub(crate) async fn create_room_route(
// state event in there with the content of literally `{}` (not null or empty
// string), let's just skip it over and warn.
if pdu_builder.content.get().eq("{}") {
debug_warn!("skipping empty initial state event with content of `{{}}`: {event:?}");
info!("skipping empty initial state event with content of `{{}}`: {event:?}");
debug_warn!("content: {}", pdu_builder.content.get());
continue;
}
@ -492,7 +514,9 @@ fn default_power_levels_content(
if let Some(power_level_content_override) = power_level_content_override {
let json: JsonObject = serde_json::from_str(power_level_content_override.json().get())
.map_err(|e| err!(Request(BadJson("Invalid power_level_content_override: {e:?}"))))?;
.map_err(|_| {
Error::BadRequest(ErrorKind::BadJson, "Invalid power_level_content_override.")
})?;
for (key, value) in json {
power_levels_content[key] = value;
@ -510,14 +534,16 @@ async fn room_alias_check(
) -> Result<OwnedRoomAliasId> {
// Basic checks on the room alias validity
if room_alias_name.contains(':') {
return Err!(Request(InvalidParam(
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Room alias contained `:` which is not allowed. Please note that this expects a \
localpart, not the full room alias.",
)));
));
} else if room_alias_name.contains(char::is_whitespace) {
return Err!(Request(InvalidParam(
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Room alias contained spaces which is not a valid room alias.",
)));
));
}
// check if room alias is forbidden
@ -526,7 +552,7 @@ async fn room_alias_check(
.forbidden_alias_names()
.is_match(room_alias_name)
{
return Err!(Request(Unknown("Room alias name is forbidden.")));
return Err(Error::BadRequest(ErrorKind::Unknown, "Room alias name is forbidden."));
}
let server_name = services.globals.server_name();
@ -546,19 +572,25 @@ async fn room_alias_check(
.await
.is_ok()
{
return Err!(Request(RoomInUse("Room alias already exists.")));
return Err(Error::BadRequest(ErrorKind::RoomInUse, "Room alias already exists."));
}
if let Some(info) = appservice_info {
if !info.aliases.is_match(full_room_alias.as_str()) {
return Err!(Request(Exclusive("Room alias is not in namespace.")));
return Err(Error::BadRequest(
ErrorKind::Exclusive,
"Room alias is not in namespace.",
));
}
} else if services
.appservice
.is_exclusive_alias(&full_room_alias)
.await
{
return Err!(Request(Exclusive("Room alias reserved by appservice.",)));
return Err(Error::BadRequest(
ErrorKind::Exclusive,
"Room alias reserved by appservice.",
));
}
debug_info!("Full room alias: {full_room_alias}");
@ -574,33 +606,24 @@ fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result<Own
.forbidden_alias_names()
.is_match(custom_room_id)
{
return Err!(Request(Unknown("Custom room ID is forbidden.")));
}
if custom_room_id.contains(':') {
return Err!(Request(InvalidParam(
"Custom room ID contained `:` which is not allowed. Please note that this expects a \
localpart, not the full room ID.",
)));
} else if custom_room_id.contains(char::is_whitespace) {
return Err!(Request(InvalidParam(
"Custom room ID contained spaces which is not valid."
)));
return Err(Error::BadRequest(ErrorKind::Unknown, "Custom room ID is forbidden."));
}
let server_name = services.globals.server_name();
let mut room_id = custom_room_id.to_owned();
if custom_room_id.contains(':') {
if !custom_room_id.starts_with('!') {
return Err!(Request(InvalidParam(
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Custom room ID contains an unexpected `:` which is not allowed.",
)));
));
}
} else if custom_room_id.starts_with('!') {
return Err!(Request(InvalidParam(
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Room ID is prefixed with !, but is not fully qualified. You likely did not want \
this.",
)));
));
} else {
room_id = format!("!{custom_room_id}:{server_name}");
}
@ -612,7 +635,10 @@ fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result<Own
.expect("failed to extract server name from room ID")
!= server_name
{
Err!(Request(InvalidParam("Custom room ID must be on this server.",)))
Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Custom room ID must be on this server.",
))
} else {
Ok(full_room_id)
}

View file

@ -40,5 +40,5 @@ pub(crate) async fn get_room_event_route(
event.add_age().ok();
Ok(get_room_event::v3::Response { event: event.into_format() })
Ok(get_room_event::v3::Response { event: event.into_room_event() })
}

View file

@ -1,9 +1,9 @@
use axum::extract::State;
use conduwuit::{
Err, Event, Result, at,
Err, PduEvent, Result, at,
utils::{BoolExt, stream::TryTools},
};
use futures::{FutureExt, TryStreamExt, future::try_join4};
use futures::TryStreamExt;
use ruma::api::client::room::initial_sync::v3::{PaginationChunk, Request, Response};
use crate::Ruma;
@ -25,33 +25,22 @@ pub(crate) async fn room_initial_sync_route(
return Err!(Request(Forbidden("No room preview available.")));
}
let membership = services
.rooms
.state_cache
.user_membership(body.sender_user(), room_id)
.map(Ok);
let visibility = services.rooms.directory.visibility(room_id).map(Ok);
let state = services
.rooms
.state_accessor
.room_state_full_pdus(room_id)
.map_ok(Event::into_format)
.try_collect::<Vec<_>>();
let limit = LIMIT_MAX;
let events = services
let events: Vec<_> = services
.rooms
.timeline
.pdus_rev(None, room_id, None)
.try_take(limit)
.try_collect::<Vec<_>>();
.try_collect()
.await?;
let (membership, visibility, state, events) =
try_join4(membership, visibility, state, events)
.boxed()
.await?;
let state: Vec<_> = services
.rooms
.state_accessor
.room_state_full_pdus(room_id)
.map_ok(PduEvent::into_state_event)
.try_collect()
.await?;
let messages = PaginationChunk {
start: events.last().map(at!(0)).as_ref().map(ToString::to_string),
@ -66,7 +55,7 @@ pub(crate) async fn room_initial_sync_route(
chunk: events
.into_iter()
.map(at!(1))
.map(Event::into_format)
.map(PduEvent::into_room_event)
.collect(),
};
@ -75,7 +64,11 @@ pub(crate) async fn room_initial_sync_route(
account_data: None,
state: state.into(),
messages: messages.chunk.is_empty().or_some(messages),
visibility: visibility.into(),
membership,
visibility: services.rooms.directory.visibility(room_id).await.into(),
membership: services
.rooms
.state_cache
.user_membership(body.sender_user(), room_id)
.await,
})
}

View file

@ -112,15 +112,13 @@ async fn local_room_summary_response(
) -> Result<get_summary::msc3266::Response> {
trace!(?sender_user, "Sending local room summary response for {room_id:?}");
let join_rule = services.rooms.state_accessor.get_join_rules(room_id);
let world_readable = services.rooms.state_accessor.is_world_readable(room_id);
let guest_can_join = services.rooms.state_accessor.guest_can_join(room_id);
let (join_rule, world_readable, guest_can_join) =
join3(join_rule, world_readable, guest_can_join).await;
trace!("{join_rule:?}, {world_readable:?}, {guest_can_join:?}");
user_can_see_summary(
services,
room_id,

View file

@ -2,7 +2,7 @@ use std::cmp::max;
use axum::extract::State;
use conduwuit::{
Err, Error, Event, Result, err, info,
Err, Error, Result, err, info,
matrix::{StateKey, pdu::PduBuilder},
};
use futures::StreamExt;
@ -215,7 +215,7 @@ pub(crate) async fn upgrade_room_route(
.room_state_get(&body.room_id, event_type, "")
.await
{
| Ok(v) => v.content().to_owned(),
| Ok(v) => v.content.clone(),
| Err(_) => continue, // Skipping missing events.
};

View file

@ -3,7 +3,7 @@ use std::collections::BTreeMap;
use axum::extract::State;
use conduwuit::{
Err, Result, at, is_true,
matrix::Event,
matrix::pdu::PduEvent,
result::FlatOk,
utils::{IterStream, stream::ReadyExt},
};
@ -144,7 +144,7 @@ async fn category_room_events(
.map(at!(2))
.flatten()
.stream()
.map(Event::into_format)
.map(PduEvent::into_room_event)
.map(|result| SearchResult {
rank: None,
result: Some(result),
@ -185,7 +185,7 @@ async fn procure_room_state(services: &Services, room_id: &RoomId) -> Result<Roo
.rooms
.state_accessor
.room_state_full_pdus(room_id)
.map_ok(Event::into_format)
.map_ok(PduEvent::into_state_event)
.try_collect()
.await?;

View file

@ -269,9 +269,11 @@ pub(crate) async fn login_token_route(
return Err!(Request(Forbidden("Login via an existing session is not enabled")));
}
let sender_user = body.sender_user();
let sender_device = body.sender_device();
// This route SHOULD have UIA
// TODO: How do we make only UIA sessions that have not been used before valid?
let (sender_user, sender_device) = body.sender();
let mut uiaainfo = uiaa::UiaaInfo {
flows: vec![uiaa::AuthFlow { stages: vec![uiaa::AuthType::Password] }],
@ -333,9 +335,12 @@ pub(crate) async fn logout_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<logout::v3::Request>,
) -> Result<logout::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
services
.users
.remove_device(body.sender_user(), body.sender_device())
.remove_device(sender_user, sender_device)
.await;
Ok(logout::v3::Response::new())
@ -360,10 +365,12 @@ pub(crate) async fn logout_all_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<logout_all::v3::Request>,
) -> Result<logout_all::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
services
.users
.all_device_ids(body.sender_user())
.for_each(|device_id| services.users.remove_device(body.sender_user(), device_id))
.all_device_ids(sender_user)
.for_each(|device_id| services.users.remove_device(sender_user, device_id))
.await;
Ok(logout_all::v3::Response::new())

View file

@ -1,11 +1,11 @@
use axum::extract::State;
use conduwuit::{
Err, Result, err,
matrix::{Event, pdu::PduBuilder},
matrix::pdu::{PduBuilder, PduEvent},
utils::BoolExt,
};
use conduwuit_service::Services;
use futures::{FutureExt, TryStreamExt};
use futures::TryStreamExt;
use ruma::{
OwnedEventId, RoomId, UserId,
api::client::state::{get_state_events, get_state_events_for_key, send_state_event},
@ -21,7 +21,6 @@ use ruma::{
},
serde::Raw,
};
use serde_json::json;
use crate::{Ruma, RumaResponse};
@ -64,7 +63,6 @@ pub(crate) async fn send_state_event_for_empty_key_route(
body: Ruma<send_state_event::v3::Request>,
) -> Result<RumaResponse<send_state_event::v3::Response>> {
send_state_event_for_key_route(State(services), body)
.boxed()
.await
.map(RumaResponse)
}
@ -79,7 +77,7 @@ pub(crate) async fn get_state_events_route(
State(services): State<crate::State>,
body: Ruma<get_state_events::v3::Request>,
) -> Result<get_state_events::v3::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if !services
.rooms
@ -95,7 +93,7 @@ pub(crate) async fn get_state_events_route(
.rooms
.state_accessor
.room_state_full_pdus(&body.room_id)
.map_ok(Event::into_format)
.map_ok(PduEvent::into_state_event)
.try_collect()
.await?,
})
@ -146,18 +144,7 @@ pub(crate) async fn get_state_events_for_key_route(
Ok(get_state_events_for_key::v3::Response {
content: event_format.or(|| event.get_content_as_value()),
event: event_format.then(|| {
json!({
"content": event.content(),
"event_id": event.event_id(),
"origin_server_ts": event.origin_server_ts(),
"room_id": event.room_id(),
"sender": event.sender(),
"state_key": event.state_key(),
"type": event.kind(),
"unsigned": event.unsigned(),
})
}),
event: event_format.then(|| event.into_state_event_value()),
})
}

View file

@ -473,7 +473,9 @@ async fn handle_left_room(
prev_batch: Some(next_batch.to_string()),
events: Vec::new(),
},
state: RoomState { events: vec![event.into_format()] },
state: RoomState {
events: vec![event.into_sync_state_event()],
},
}));
}
@ -557,7 +559,7 @@ async fn handle_left_room(
continue;
}
left_state_events.push(pdu.into_format());
left_state_events.push(pdu.into_sync_state_event());
}
}
@ -643,7 +645,7 @@ async fn load_joined_room(
let lazy_loading_context = &lazy_loading::Context {
user_id: sender_user,
device_id: Some(sender_device),
device_id: sender_device,
room_id,
token: Some(since),
options: Some(&filter.room.state.lazy_load_options),
@ -753,7 +755,7 @@ async fn load_joined_room(
.wide_filter_map(|item| ignored_filter(services, item, sender_user))
.map(at!(1))
.chain(joined_sender_member.into_iter().stream())
.map(Event::into_format)
.map(|pdu| pdu.to_sync_room_event())
.collect::<Vec<_>>();
let account_data_events = services
@ -875,7 +877,10 @@ async fn load_joined_room(
events: room_events,
},
state: RoomState {
events: state_events.into_iter().map(Event::into_format).collect(),
events: state_events
.into_iter()
.map(PduEvent::into_sync_state_event)
.collect(),
},
ephemeral: Ephemeral { events: edus },
unread_thread_notifications: BTreeMap::new(),

View file

@ -6,7 +6,7 @@ use std::{
use axum::extract::State;
use conduwuit::{
Err, Error, Event, PduCount, Result, at, debug, error, extract_variant,
Err, Error, PduCount, PduEvent, Result, debug, error, extract_variant,
matrix::TypeStateKey,
utils::{
BoolExt, IterStream, ReadyExt, TryFutureExtExt,
@ -604,8 +604,7 @@ pub(crate) async fn sync_events_v4_route(
.iter()
.stream()
.filter_map(|item| ignored_filter(&services, item.clone(), sender_user))
.map(at!(1))
.map(Event::into_format)
.map(|(_, pdu)| pdu.to_sync_room_event())
.collect()
.await;
@ -627,7 +626,7 @@ pub(crate) async fn sync_events_v4_route(
.state_accessor
.room_state_get(room_id, &state.0, &state.1)
.await
.map(Event::into_format)
.map(PduEvent::into_sync_state_event)
.ok()
})
.collect()

View file

@ -7,8 +7,11 @@ use std::{
use axum::extract::State;
use conduwuit::{
Err, Error, Result, at, error, extract_variant, is_equal_to,
matrix::{Event, TypeStateKey, pdu::PduCount},
Err, Error, Result, error, extract_variant, is_equal_to,
matrix::{
TypeStateKey,
pdu::{PduCount, PduEvent},
},
trace,
utils::{
BoolExt, FutureBoolExt, IterStream, ReadyExt, TryFutureExtExt,
@ -512,8 +515,7 @@ where
.iter()
.stream()
.filter_map(|item| ignored_filter(services, item.clone(), sender_user))
.map(at!(1))
.map(Event::into_format)
.map(|(_, pdu)| pdu.to_sync_room_event())
.collect()
.await;
@ -535,7 +537,7 @@ where
.state_accessor
.room_state_get(room_id, &state.0, &state.1)
.await
.map(Event::into_format)
.map(PduEvent::into_sync_state_event)
.ok()
})
.collect()

View file

@ -21,7 +21,7 @@ pub(crate) async fn update_tag_route(
State(services): State<crate::State>,
body: Ruma<create_tag::v3::Request>,
) -> Result<create_tag::v3::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let mut tags_event = services
.account_data
@ -42,7 +42,7 @@ pub(crate) async fn update_tag_route(
Some(&body.room_id),
sender_user,
RoomAccountDataEventType::Tag,
&serde_json::to_value(tags_event)?,
&serde_json::to_value(tags_event).expect("to json value always works"),
)
.await?;
@ -58,7 +58,7 @@ pub(crate) async fn delete_tag_route(
State(services): State<crate::State>,
body: Ruma<delete_tag::v3::Request>,
) -> Result<delete_tag::v3::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let mut tags_event = services
.account_data
@ -76,7 +76,7 @@ pub(crate) async fn delete_tag_route(
Some(&body.room_id),
sender_user,
RoomAccountDataEventType::Tag,
&serde_json::to_value(tags_event)?,
&serde_json::to_value(tags_event).expect("to json value always works"),
)
.await?;
@ -92,7 +92,7 @@ pub(crate) async fn get_tags_route(
State(services): State<crate::State>,
body: Ruma<get_tags::v3::Request>,
) -> Result<get_tags::v3::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let tags_event = services
.account_data

View file

@ -1,10 +1,7 @@
use axum::extract::State;
use conduwuit::{
Result, at,
matrix::{
Event,
pdu::{PduCount, PduEvent},
},
matrix::pdu::{PduCount, PduEvent},
};
use futures::StreamExt;
use ruma::{api::client::threads::get_threads, uint};
@ -59,7 +56,7 @@ pub(crate) async fn get_threads_route(
chunk: threads
.into_iter()
.map(at!(1))
.map(Event::into_format)
.map(PduEvent::into_room_event)
.collect(),
})
}

View file

@ -21,7 +21,7 @@ pub(crate) async fn send_event_to_device_route(
State(services): State<crate::State>,
body: Ruma<send_event_to_device::v3::Request>,
) -> Result<send_event_to_device::v3::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_device = body.sender_device.as_deref();
// Check if this is a new transaction id
@ -47,7 +47,7 @@ pub(crate) async fn send_event_to_device_route(
serde_json::to_writer(
&mut buf,
&federation::transactions::edu::Edu::DirectToDevice(DirectDeviceContent {
sender: sender_user.to_owned(),
sender: sender_user.clone(),
ev_type: body.event_type.clone(),
message_id: count.to_string().into(),
messages,

View file

@ -69,7 +69,7 @@ pub(crate) async fn delete_timezone_key_route(
State(services): State<crate::State>,
body: Ruma<delete_timezone_key::unstable::Request>,
) -> Result<delete_timezone_key::unstable::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if *sender_user != body.user_id && body.appservice_info.is_none() {
return Err!(Request(Forbidden("You cannot update the profile of another user")));
@ -97,7 +97,7 @@ pub(crate) async fn set_timezone_key_route(
State(services): State<crate::State>,
body: Ruma<set_timezone_key::unstable::Request>,
) -> Result<set_timezone_key::unstable::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if *sender_user != body.user_id && body.appservice_info.is_none() {
return Err!(Request(Forbidden("You cannot update the profile of another user")));
@ -125,7 +125,7 @@ pub(crate) async fn set_profile_key_route(
State(services): State<crate::State>,
body: Ruma<set_profile_key::unstable::Request>,
) -> Result<set_profile_key::unstable::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if *sender_user != body.user_id && body.appservice_info.is_none() {
return Err!(Request(Forbidden("You cannot update the profile of another user")));
@ -218,7 +218,7 @@ pub(crate) async fn delete_profile_key_route(
State(services): State<crate::State>,
body: Ruma<delete_profile_key::unstable::Request>,
) -> Result<delete_profile_key::unstable::Response> {
let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if *sender_user != body.user_id && body.appservice_info.is_none() {
return Err!(Request(Forbidden("You cannot update the profile of another user")));

View file

@ -1,7 +1,10 @@
use axum::extract::State;
use conduwuit::{
Result,
utils::{future::BoolExt, stream::BroadbandExt},
utils::{
future::BoolExt,
stream::{BroadbandExt, ReadyExt},
},
};
use futures::{FutureExt, StreamExt, pin_mut};
use ruma::{
@ -34,18 +37,17 @@ pub(crate) async fn search_users_route(
let mut users = services
.users
.stream()
.ready_filter(|user_id| user_id.as_str().to_lowercase().contains(&search_term))
.map(ToOwned::to_owned)
.broad_filter_map(async |user_id| {
let display_name = services.users.displayname(&user_id).await.ok();
let user_id_matches = user_id.as_str().to_lowercase().contains(&search_term);
let display_name_matches = display_name
.as_deref()
.map(str::to_lowercase)
.is_some_and(|display_name| display_name.contains(&search_term));
if !user_id_matches && !display_name_matches {
if !display_name_matches {
return None;
}

View file

@ -2,10 +2,7 @@ use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use base64::{Engine as _, engine::general_purpose};
use conduwuit::{
Err, Error, PduEvent, Result, err,
matrix::{Event, event::gen_event_id},
utils::{self, hash::sha256},
warn,
Err, Error, PduEvent, Result, err, pdu::gen_event_id, utils, utils::hash::sha256, warn,
};
use ruma::{
CanonicalJsonValue, OwnedUserId, UserId,
@ -59,7 +56,7 @@ pub(crate) async fn create_invite_route(
}
let mut signed_event = utils::to_canonical_object(&body.event)
.map_err(|_| err!(Request(InvalidParam("Invite event is invalid."))))?;
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invite event is invalid."))?;
let invited_user: OwnedUserId = signed_event
.get("state_key")
@ -114,7 +111,7 @@ pub(crate) async fn create_invite_route(
let pdu: PduEvent = serde_json::from_value(event.into())
.map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))?;
invite_state.push(pdu.to_format());
invite_state.push(pdu.to_stripped_state_event());
// If we are active in the room, the remote server will notify us about the
// join/invite through /send. If we are not in the room, we need to manually
@ -147,7 +144,7 @@ pub(crate) async fn create_invite_route(
.send_appservice_request(
appservice.registration.clone(),
ruma::api::appservice::event::push_events::v1::Request {
events: vec![pdu.to_format()],
events: vec![pdu.to_room_event()],
txn_id: general_purpose::URL_SAFE_NO_PAD
.encode(sha256::hash(pdu.event_id.as_bytes()))
.into(),

View file

@ -5,7 +5,7 @@ use std::borrow::Borrow;
use axum::extract::State;
use conduwuit::{
Err, Result, at, err,
matrix::event::gen_event_id_canonical_json,
pdu::gen_event_id_canonical_json,
utils::stream::{IterStream, TryBroadbandExt},
warn,
};

View file

@ -1,7 +1,7 @@
use axum::extract::State;
use conduwuit::{
Err, Result, err,
matrix::{event::gen_event_id_canonical_json, pdu::PduEvent},
matrix::pdu::{PduEvent, gen_event_id_canonical_json},
warn,
};
use futures::FutureExt;

View file

@ -1,7 +1,7 @@
#![allow(deprecated)]
use axum::extract::State;
use conduwuit::{Err, Result, err, matrix::event::gen_event_id_canonical_json};
use conduwuit::{Err, Result, err, matrix::pdu::gen_event_id_canonical_json};
use conduwuit_service::Services;
use futures::FutureExt;
use ruma::{

View file

@ -88,7 +88,10 @@ impl PartialProxyConfig {
}
}
match (included_because, excluded_because) {
| (Some(a), Some(b)) if a.more_specific_than(b) => Some(&self.url),
| (Some(a), Some(b)) if a.more_specific_than(b) => Some(&self.url), /* included for
* a more specific
* reason */
// than excluded
| (Some(_), None) => Some(&self.url),
| _ => None,
}

View file

@ -84,12 +84,10 @@ fn append_features(features: &mut Vec<String>, manifest: &str) -> Result<()> {
fn init_dependencies() -> Result<DepsSet> {
let manifest = Manifest::from_str(WORKSPACE_MANIFEST)?;
let deps_set = manifest
Ok(manifest
.workspace
.as_ref()
.expect("manifest has workspace section")
.dependencies
.clone();
Ok(deps_set)
.clone())
}

View file

@ -1,188 +1,63 @@
mod content;
mod filter;
mod format;
mod id;
mod redact;
mod relation;
mod type_ext;
mod unsigned;
use std::fmt::Debug;
use ruma::{
CanonicalJsonObject, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId,
RoomVersionId, UserId, events::TimelineEventType,
};
use serde::Deserialize;
use serde_json::{Value as JsonValue, value::RawValue as RawJsonValue};
pub use self::{filter::Matches, id::*, relation::RelationTypeEqual, type_ext::TypeExt};
use super::{pdu::Pdu, state_key::StateKey};
use crate::{Result, utils};
use ruma::{EventId, MilliSecondsSinceUnixEpoch, RoomId, UserId, events::TimelineEventType};
use serde_json::value::RawValue as RawJsonValue;
/// Abstraction of a PDU so users can have their own PDU types.
pub trait Event: Clone + Debug {
/// Serialize into a Ruma JSON format, consuming.
#[inline]
fn into_format<T>(self) -> T
where
T: From<format::Owned<Self>>,
Self: Sized,
{
format::Owned(self).into()
}
/// Serialize into a Ruma JSON format
#[inline]
fn to_format<'a, T>(&'a self) -> T
where
T: From<format::Ref<'a, Self>>,
Self: Sized + 'a,
{
format::Ref(self).into()
}
#[inline]
fn contains_unsigned_property<T>(&self, property: &str, is_type: T) -> bool
where
T: FnOnce(&JsonValue) -> bool,
Self: Sized,
{
unsigned::contains_unsigned_property::<T, _>(self, property, is_type)
}
#[inline]
fn get_unsigned_property<T>(&self, property: &str) -> Result<T>
where
T: for<'de> Deserialize<'de>,
Self: Sized,
{
unsigned::get_unsigned_property::<T, _>(self, property)
}
#[inline]
fn get_unsigned_as_value(&self) -> JsonValue
where
Self: Sized,
{
unsigned::get_unsigned_as_value(self)
}
#[inline]
fn get_unsigned<T>(&self) -> Result<T>
where
T: for<'de> Deserialize<'de>,
Self: Sized,
{
unsigned::get_unsigned::<T, _>(self)
}
#[inline]
fn get_content_as_value(&self) -> JsonValue
where
Self: Sized,
{
content::as_value(self)
}
#[inline]
fn get_content<T>(&self) -> Result<T>
where
for<'de> T: Deserialize<'de>,
Self: Sized,
{
content::get::<T, _>(self)
}
#[inline]
fn redacts_id(&self, room_version: &RoomVersionId) -> Option<OwnedEventId>
where
Self: Sized,
{
redact::redacts_id(self, room_version)
}
#[inline]
fn is_redacted(&self) -> bool
where
Self: Sized,
{
redact::is_redacted(self)
}
#[inline]
fn into_canonical_object(self) -> CanonicalJsonObject
where
Self: Sized,
{
utils::to_canonical_object(self.into_pdu()).expect("failed to create Value::Object")
}
#[inline]
fn to_canonical_object(&self) -> CanonicalJsonObject {
utils::to_canonical_object(self.as_pdu()).expect("failed to create Value::Object")
}
#[inline]
fn into_value(self) -> JsonValue
where
Self: Sized,
{
serde_json::to_value(self.into_pdu()).expect("failed to create JSON Value")
}
#[inline]
fn to_value(&self) -> JsonValue {
serde_json::to_value(self.as_pdu()).expect("failed to create JSON Value")
}
#[inline]
fn as_mut_pdu(&mut self) -> &mut Pdu { unimplemented!("not a mutable Pdu") }
fn as_pdu(&self) -> &Pdu;
fn into_pdu(self) -> Pdu;
fn is_owned(&self) -> bool;
//
// Canonical properties
//
/// All the authenticating events for this event.
fn auth_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Clone + Send + '_;
/// The event's content.
fn content(&self) -> &RawJsonValue;
pub trait Event {
/// The `EventId` of this event.
fn event_id(&self) -> &EventId;
/// The time of creation on the originating server.
fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch;
/// The events before this event.
fn prev_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Clone + Send + '_;
/// If this event is a redaction event this is the event it redacts.
fn redacts(&self) -> Option<&EventId>;
/// The `RoomId` of this event.
fn room_id(&self) -> &RoomId;
/// The `UserId` of this event.
fn sender(&self) -> &UserId;
/// The time of creation on the originating server.
fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch;
/// The event type.
fn event_type(&self) -> &TimelineEventType;
/// The event's content.
fn content(&self) -> &RawJsonValue;
/// The state key for this event.
fn state_key(&self) -> Option<&str>;
/// The event type.
fn kind(&self) -> &TimelineEventType;
/// The events before this event.
// Requires GATs to avoid boxing (and TAIT for making it convenient).
fn prev_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Send + '_;
/// Metadata container; peer-trusted only.
fn unsigned(&self) -> Option<&RawJsonValue>;
/// All the authenticating events for this event.
// Requires GATs to avoid boxing (and TAIT for making it convenient).
fn auth_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Send + '_;
//#[deprecated]
#[inline]
fn event_type(&self) -> &TimelineEventType { self.kind() }
/// If this event is a redaction event this is the event it redacts.
fn redacts(&self) -> Option<&EventId>;
}
impl<T: Event> Event for &T {
fn event_id(&self) -> &EventId { (*self).event_id() }
fn room_id(&self) -> &RoomId { (*self).room_id() }
fn sender(&self) -> &UserId { (*self).sender() }
fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch { (*self).origin_server_ts() }
fn event_type(&self) -> &TimelineEventType { (*self).event_type() }
fn content(&self) -> &RawJsonValue { (*self).content() }
fn state_key(&self) -> Option<&str> { (*self).state_key() }
fn prev_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Send + '_ {
(*self).prev_events()
}
fn auth_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Send + '_ {
(*self).auth_events()
}
fn redacts(&self) -> Option<&EventId> { (*self).redacts() }
}

View file

@ -1,21 +0,0 @@
use serde::Deserialize;
use serde_json::value::Value as JsonValue;
use super::Event;
use crate::{Result, err};
#[inline]
#[must_use]
pub(super) fn as_value<E: Event>(event: &E) -> JsonValue {
get(event).expect("Failed to represent Event content as JsonValue")
}
#[inline]
pub(super) fn get<T, E>(event: &E) -> Result<T>
where
T: for<'de> Deserialize<'de>,
E: Event,
{
serde_json::from_str(event.content().get())
.map_err(|e| err!(Request(BadJson("Failed to deserialize content into type: {e}"))))
}

View file

@ -1,93 +0,0 @@
use ruma::api::client::filter::{RoomEventFilter, UrlFilter};
use serde_json::Value;
use super::Event;
use crate::is_equal_to;
pub trait Matches<E: Event> {
fn matches(&self, event: &E) -> bool;
}
impl<E: Event> Matches<E> for &RoomEventFilter {
#[inline]
fn matches(&self, event: &E) -> bool {
if !matches_sender(event, self) {
return false;
}
if !matches_room(event, self) {
return false;
}
if !matches_type(event, self) {
return false;
}
if !matches_url(event, self) {
return false;
}
true
}
}
fn matches_room<E: Event>(event: &E, filter: &RoomEventFilter) -> bool {
if filter.not_rooms.iter().any(is_equal_to!(event.room_id())) {
return false;
}
if let Some(rooms) = filter.rooms.as_ref() {
if !rooms.iter().any(is_equal_to!(event.room_id())) {
return false;
}
}
true
}
fn matches_sender<E: Event>(event: &E, filter: &RoomEventFilter) -> bool {
if filter.not_senders.iter().any(is_equal_to!(event.sender())) {
return false;
}
if let Some(senders) = filter.senders.as_ref() {
if !senders.iter().any(is_equal_to!(event.sender())) {
return false;
}
}
true
}
fn matches_type<E: Event>(event: &E, filter: &RoomEventFilter) -> bool {
let kind = event.kind().to_cow_str();
if filter.not_types.iter().any(is_equal_to!(&kind)) {
return false;
}
if let Some(types) = filter.types.as_ref() {
if !types.iter().any(is_equal_to!(&kind)) {
return false;
}
}
true
}
fn matches_url<E: Event>(event: &E, filter: &RoomEventFilter) -> bool {
let Some(url_filter) = filter.url_filter.as_ref() else {
return true;
};
//TODO: might be better to use Ruma's Raw rather than serde here
let url = event
.get_content_as_value()
.get("url")
.is_some_and(Value::is_string);
match url_filter {
| UrlFilter::EventsWithUrl => url,
| UrlFilter::EventsWithoutUrl => !url,
}
}

View file

@ -1,219 +0,0 @@
use ruma::{
events::{
AnyMessageLikeEvent, AnyStateEvent, AnyStrippedStateEvent, AnySyncStateEvent,
AnySyncTimelineEvent, AnyTimelineEvent, StateEvent, room::member::RoomMemberEventContent,
space::child::HierarchySpaceChildEvent,
},
serde::Raw,
};
use serde_json::json;
use super::{Event, redact};
pub struct Owned<E: Event>(pub(super) E);
pub struct Ref<'a, E: Event>(pub(super) &'a E);
impl<E: Event> From<Owned<E>> for Raw<AnySyncTimelineEvent> {
fn from(event: Owned<E>) -> Self { Ref(&event.0).into() }
}
impl<'a, E: Event> From<Ref<'a, E>> for Raw<AnySyncTimelineEvent> {
fn from(event: Ref<'a, E>) -> Self {
let event = event.0;
let (redacts, content) = redact::copy(event);
let mut json = json!({
"content": content,
"event_id": event.event_id(),
"origin_server_ts": event.origin_server_ts(),
"sender": event.sender(),
"type": event.event_type(),
});
if let Some(redacts) = redacts {
json["redacts"] = json!(redacts);
}
if let Some(state_key) = event.state_key() {
json["state_key"] = json!(state_key);
}
if let Some(unsigned) = event.unsigned() {
json["unsigned"] = json!(unsigned);
}
serde_json::from_value(json).expect("Failed to serialize Event value")
}
}
impl<E: Event> From<Owned<E>> for Raw<AnyTimelineEvent> {
fn from(event: Owned<E>) -> Self { Ref(&event.0).into() }
}
impl<'a, E: Event> From<Ref<'a, E>> for Raw<AnyTimelineEvent> {
fn from(event: Ref<'a, E>) -> Self {
let event = event.0;
let (redacts, content) = redact::copy(event);
let mut json = json!({
"content": content,
"event_id": event.event_id(),
"origin_server_ts": event.origin_server_ts(),
"room_id": event.room_id(),
"sender": event.sender(),
"type": event.kind(),
});
if let Some(redacts) = redacts {
json["redacts"] = json!(redacts);
}
if let Some(state_key) = event.state_key() {
json["state_key"] = json!(state_key);
}
if let Some(unsigned) = event.unsigned() {
json["unsigned"] = json!(unsigned);
}
serde_json::from_value(json).expect("Failed to serialize Event value")
}
}
impl<E: Event> From<Owned<E>> for Raw<AnyMessageLikeEvent> {
fn from(event: Owned<E>) -> Self { Ref(&event.0).into() }
}
impl<'a, E: Event> From<Ref<'a, E>> for Raw<AnyMessageLikeEvent> {
fn from(event: Ref<'a, E>) -> Self {
let event = event.0;
let (redacts, content) = redact::copy(event);
let mut json = json!({
"content": content,
"event_id": event.event_id(),
"origin_server_ts": event.origin_server_ts(),
"room_id": event.room_id(),
"sender": event.sender(),
"type": event.kind(),
});
if let Some(redacts) = &redacts {
json["redacts"] = json!(redacts);
}
if let Some(state_key) = event.state_key() {
json["state_key"] = json!(state_key);
}
if let Some(unsigned) = event.unsigned() {
json["unsigned"] = json!(unsigned);
}
serde_json::from_value(json).expect("Failed to serialize Event value")
}
}
impl<E: Event> From<Owned<E>> for Raw<AnyStateEvent> {
fn from(event: Owned<E>) -> Self { Ref(&event.0).into() }
}
impl<'a, E: Event> From<Ref<'a, E>> for Raw<AnyStateEvent> {
fn from(event: Ref<'a, E>) -> Self {
let event = event.0;
let mut json = json!({
"content": event.content(),
"event_id": event.event_id(),
"origin_server_ts": event.origin_server_ts(),
"room_id": event.room_id(),
"sender": event.sender(),
"state_key": event.state_key(),
"type": event.kind(),
});
if let Some(unsigned) = event.unsigned() {
json["unsigned"] = json!(unsigned);
}
serde_json::from_value(json).expect("Failed to serialize Event value")
}
}
impl<E: Event> From<Owned<E>> for Raw<AnySyncStateEvent> {
fn from(event: Owned<E>) -> Self { Ref(&event.0).into() }
}
impl<'a, E: Event> From<Ref<'a, E>> for Raw<AnySyncStateEvent> {
fn from(event: Ref<'a, E>) -> Self {
let event = event.0;
let mut json = json!({
"content": event.content(),
"event_id": event.event_id(),
"origin_server_ts": event.origin_server_ts(),
"sender": event.sender(),
"state_key": event.state_key(),
"type": event.kind(),
});
if let Some(unsigned) = event.unsigned() {
json["unsigned"] = json!(unsigned);
}
serde_json::from_value(json).expect("Failed to serialize Event value")
}
}
impl<E: Event> From<Owned<E>> for Raw<AnyStrippedStateEvent> {
fn from(event: Owned<E>) -> Self { Ref(&event.0).into() }
}
impl<'a, E: Event> From<Ref<'a, E>> for Raw<AnyStrippedStateEvent> {
fn from(event: Ref<'a, E>) -> Self {
let event = event.0;
let json = json!({
"content": event.content(),
"sender": event.sender(),
"state_key": event.state_key(),
"type": event.kind(),
});
serde_json::from_value(json).expect("Failed to serialize Event value")
}
}
impl<E: Event> From<Owned<E>> for Raw<HierarchySpaceChildEvent> {
fn from(event: Owned<E>) -> Self { Ref(&event.0).into() }
}
impl<'a, E: Event> From<Ref<'a, E>> for Raw<HierarchySpaceChildEvent> {
fn from(event: Ref<'a, E>) -> Self {
let event = event.0;
let json = json!({
"content": event.content(),
"origin_server_ts": event.origin_server_ts(),
"sender": event.sender(),
"state_key": event.state_key(),
"type": event.kind(),
});
serde_json::from_value(json).expect("Failed to serialize Event value")
}
}
impl<E: Event> From<Owned<E>> for Raw<StateEvent<RoomMemberEventContent>> {
fn from(event: Owned<E>) -> Self { Ref(&event.0).into() }
}
impl<'a, E: Event> From<Ref<'a, E>> for Raw<StateEvent<RoomMemberEventContent>> {
fn from(event: Ref<'a, E>) -> Self {
let event = event.0;
let mut json = json!({
"content": event.content(),
"event_id": event.event_id(),
"origin_server_ts": event.origin_server_ts(),
"redacts": event.redacts(),
"room_id": event.room_id(),
"sender": event.sender(),
"state_key": event.state_key(),
"type": event.kind(),
});
if let Some(unsigned) = event.unsigned() {
json["unsigned"] = json!(unsigned);
}
serde_json::from_value(json).expect("Failed to serialize Event value")
}
}

View file

@ -1,86 +0,0 @@
use ruma::{
OwnedEventId, RoomVersionId,
events::{TimelineEventType, room::redaction::RoomRedactionEventContent},
};
use serde::Deserialize;
use serde_json::value::{RawValue as RawJsonValue, to_raw_value};
use super::Event;
/// Copies the `redacts` property of the event to the `content` dict and
/// vice-versa.
///
/// This follows the specification's
/// [recommendation](https://spec.matrix.org/v1.10/rooms/v11/#moving-the-redacts-property-of-mroomredaction-events-to-a-content-property):
///
/// > For backwards-compatibility with older clients, servers should add a
/// > redacts property to the top level of m.room.redaction events in when
/// > serving such events over the Client-Server API.
///
/// > For improved compatibility with newer clients, servers should add a
/// > redacts property to the content of m.room.redaction events in older
/// > room versions when serving such events over the Client-Server API.
#[must_use]
pub(super) fn copy<E: Event>(event: &E) -> (Option<OwnedEventId>, Box<RawJsonValue>) {
if *event.event_type() != TimelineEventType::RoomRedaction {
return (event.redacts().map(ToOwned::to_owned), event.content().to_owned());
}
let Ok(mut content) = event.get_content::<RoomRedactionEventContent>() else {
return (event.redacts().map(ToOwned::to_owned), event.content().to_owned());
};
if let Some(redacts) = content.redacts {
return (Some(redacts), event.content().to_owned());
}
if let Some(redacts) = event.redacts().map(ToOwned::to_owned) {
content.redacts = Some(redacts);
return (
event.redacts().map(ToOwned::to_owned),
to_raw_value(&content).expect("Must be valid, we only added redacts field"),
);
}
(event.redacts().map(ToOwned::to_owned), event.content().to_owned())
}
#[must_use]
pub(super) fn is_redacted<E: Event>(event: &E) -> bool {
let Some(unsigned) = event.unsigned() else {
return false;
};
let Ok(unsigned) = ExtractRedactedBecause::deserialize(unsigned) else {
return false;
};
unsigned.redacted_because.is_some()
}
#[must_use]
pub(super) fn redacts_id<E: Event>(
event: &E,
room_version: &RoomVersionId,
) -> Option<OwnedEventId> {
use RoomVersionId::*;
if *event.kind() != TimelineEventType::RoomRedaction {
return None;
}
match *room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 =>
event.redacts().map(ToOwned::to_owned),
| _ =>
event
.get_content::<RoomRedactionEventContent>()
.ok()?
.redacts,
}
}
#[derive(Deserialize)]
struct ExtractRedactedBecause {
redacted_because: Option<serde::de::IgnoredAny>,
}

View file

@ -1,28 +0,0 @@
use ruma::events::relation::RelationType;
use serde::Deserialize;
use super::Event;
pub trait RelationTypeEqual<E: Event> {
fn relation_type_equal(&self, event: &E) -> bool;
}
#[derive(Clone, Debug, Deserialize)]
struct ExtractRelatesToEventId {
#[serde(rename = "m.relates_to")]
relates_to: ExtractRelType,
}
#[derive(Clone, Debug, Deserialize)]
struct ExtractRelType {
rel_type: RelationType,
}
impl<E: Event> RelationTypeEqual<E> for RelationType {
fn relation_type_equal(&self, event: &E) -> bool {
event
.get_content()
.map(|c: ExtractRelatesToEventId| c.relates_to.rel_type)
.is_ok_and(|r| r == *self)
}
}

View file

@ -1,32 +0,0 @@
use ruma::events::{StateEventType, TimelineEventType};
use super::StateKey;
/// Convenience trait for adding event type plus state key to state maps.
pub trait TypeExt {
fn with_state_key(self, state_key: impl Into<StateKey>) -> (StateEventType, StateKey);
}
impl TypeExt for StateEventType {
fn with_state_key(self, state_key: impl Into<StateKey>) -> (StateEventType, StateKey) {
(self, state_key.into())
}
}
impl TypeExt for &StateEventType {
fn with_state_key(self, state_key: impl Into<StateKey>) -> (StateEventType, StateKey) {
(self.clone(), state_key.into())
}
}
impl TypeExt for TimelineEventType {
fn with_state_key(self, state_key: impl Into<StateKey>) -> (StateEventType, StateKey) {
(self.into(), state_key.into())
}
}
impl TypeExt for &TimelineEventType {
fn with_state_key(self, state_key: impl Into<StateKey>) -> (StateEventType, StateKey) {
(self.clone().into(), state_key.into())
}
}

View file

@ -1,51 +0,0 @@
use serde::Deserialize;
use serde_json::value::Value as JsonValue;
use super::Event;
use crate::{Result, err, is_true};
pub(super) fn contains_unsigned_property<F, E>(event: &E, property: &str, is_type: F) -> bool
where
F: FnOnce(&JsonValue) -> bool,
E: Event,
{
get_unsigned_as_value(event)
.get(property)
.map(is_type)
.is_some_and(is_true!())
}
pub(super) fn get_unsigned_property<T, E>(event: &E, property: &str) -> Result<T>
where
T: for<'de> Deserialize<'de>,
E: Event,
{
get_unsigned_as_value(event)
.get_mut(property)
.map(JsonValue::take)
.map(serde_json::from_value)
.ok_or(err!(Request(NotFound("property not found in unsigned object"))))?
.map_err(|e| err!(Database("Failed to deserialize unsigned.{property} into type: {e}")))
}
#[must_use]
pub(super) fn get_unsigned_as_value<E>(event: &E) -> JsonValue
where
E: Event,
{
get_unsigned::<JsonValue, E>(event).unwrap_or_default()
}
pub(super) fn get_unsigned<T, E>(event: &E) -> Result<T>
where
T: for<'de> Deserialize<'de>,
E: Event,
{
event
.unsigned()
.as_ref()
.map(|raw| raw.get())
.map(serde_json::from_str)
.ok_or(err!(Request(NotFound("\"unsigned\" property not found in pdu"))))?
.map_err(|e| err!(Database("Failed to deserialize \"unsigned\" into value: {e}")))
}

View file

@ -2,10 +2,8 @@
pub mod event;
pub mod pdu;
pub mod state_key;
pub mod state_res;
pub use event::{Event, TypeExt as EventTypeExt};
pub use pdu::{Pdu, PduBuilder, PduCount, PduEvent, PduId, RawPduId, ShortId};
pub use state_key::StateKey;
pub use state_res::{RoomVersion, StateMap, TypeStateKey};
pub use event::Event;
pub use pdu::{PduBuilder, PduCount, PduEvent, PduId, RawPduId, StateKey};
pub use state_res::{EventTypeExt, RoomVersion, StateMap, TypeStateKey};

View file

@ -1,8 +1,14 @@
mod builder;
mod content;
mod count;
mod event_id;
mod filter;
mod id;
mod raw_id;
mod redact;
mod relation;
mod state_key;
mod strip;
#[cfg(test)]
mod tests;
mod unsigned;
@ -20,50 +26,38 @@ pub use self::{
Count as PduCount, Id as PduId, Pdu as PduEvent, RawId as RawPduId,
builder::{Builder, Builder as PduBuilder},
count::Count,
id::{ShortId, *},
event_id::*,
id::*,
raw_id::*,
state_key::{ShortStateKey, StateKey},
};
use super::{Event, StateKey};
use super::Event;
use crate::Result;
/// Persistent Data Unit (Event)
#[derive(Clone, Deserialize, Serialize, Debug)]
pub struct Pdu {
pub event_id: OwnedEventId,
pub room_id: OwnedRoomId,
pub sender: OwnedUserId,
#[serde(skip_serializing_if = "Option::is_none")]
pub origin: Option<OwnedServerName>,
pub origin_server_ts: UInt,
#[serde(rename = "type")]
pub kind: TimelineEventType,
pub content: Box<RawJsonValue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub state_key: Option<StateKey>,
pub prev_events: Vec<OwnedEventId>,
pub depth: UInt,
pub auth_events: Vec<OwnedEventId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub redacts: Option<OwnedEventId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub unsigned: Option<Box<RawJsonValue>>,
pub hashes: EventHash,
// BTreeMap<Box<ServerName>, BTreeMap<ServerSigningKeyId, String>>
#[serde(default, skip_serializing_if = "Option::is_none")]
// BTreeMap<Box<ServerName>, BTreeMap<ServerSigningKeyId, String>>
pub signatures: Option<Box<RawJsonValue>>,
}
@ -85,106 +79,31 @@ impl Pdu {
}
impl Event for Pdu {
#[inline]
fn auth_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Clone + Send + '_ {
self.auth_events.iter().map(AsRef::as_ref)
}
#[inline]
fn content(&self) -> &RawJsonValue { &self.content }
#[inline]
fn event_id(&self) -> &EventId { &self.event_id }
#[inline]
fn room_id(&self) -> &RoomId { &self.room_id }
fn sender(&self) -> &UserId { &self.sender }
fn event_type(&self) -> &TimelineEventType { &self.kind }
fn content(&self) -> &RawJsonValue { &self.content }
fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch {
MilliSecondsSinceUnixEpoch(self.origin_server_ts)
}
#[inline]
fn prev_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Clone + Send + '_ {
fn state_key(&self) -> Option<&str> { self.state_key.as_deref() }
fn prev_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Send + '_ {
self.prev_events.iter().map(AsRef::as_ref)
}
#[inline]
fn redacts(&self) -> Option<&EventId> { self.redacts.as_deref() }
#[inline]
fn room_id(&self) -> &RoomId { &self.room_id }
#[inline]
fn sender(&self) -> &UserId { &self.sender }
#[inline]
fn state_key(&self) -> Option<&str> { self.state_key.as_deref() }
#[inline]
fn kind(&self) -> &TimelineEventType { &self.kind }
#[inline]
fn unsigned(&self) -> Option<&RawJsonValue> { self.unsigned.as_deref() }
#[inline]
fn as_mut_pdu(&mut self) -> &mut Pdu { self }
#[inline]
fn as_pdu(&self) -> &Pdu { self }
#[inline]
fn into_pdu(self) -> Pdu { self }
#[inline]
fn is_owned(&self) -> bool { true }
}
impl Event for &Pdu {
#[inline]
fn auth_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Clone + Send + '_ {
fn auth_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Send + '_ {
self.auth_events.iter().map(AsRef::as_ref)
}
#[inline]
fn content(&self) -> &RawJsonValue { &self.content }
#[inline]
fn event_id(&self) -> &EventId { &self.event_id }
#[inline]
fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch {
MilliSecondsSinceUnixEpoch(self.origin_server_ts)
}
#[inline]
fn prev_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Clone + Send + '_ {
self.prev_events.iter().map(AsRef::as_ref)
}
#[inline]
fn redacts(&self) -> Option<&EventId> { self.redacts.as_deref() }
#[inline]
fn room_id(&self) -> &RoomId { &self.room_id }
#[inline]
fn sender(&self) -> &UserId { &self.sender }
#[inline]
fn state_key(&self) -> Option<&str> { self.state_key.as_deref() }
#[inline]
fn kind(&self) -> &TimelineEventType { &self.kind }
#[inline]
fn unsigned(&self) -> Option<&RawJsonValue> { self.unsigned.as_deref() }
#[inline]
fn as_pdu(&self) -> &Pdu { self }
#[inline]
fn into_pdu(self) -> Pdu { self.clone() }
#[inline]
fn is_owned(&self) -> bool { false }
}
/// Prevent derived equality which wouldn't limit itself to event_id

View file

@ -0,0 +1,20 @@
use serde::Deserialize;
use serde_json::value::Value as JsonValue;
use crate::{Result, err, implement};
#[must_use]
#[implement(super::Pdu)]
pub fn get_content_as_value(&self) -> JsonValue {
self.get_content()
.expect("pdu content must be a valid JSON value")
}
#[implement(super::Pdu)]
pub fn get_content<T>(&self) -> Result<T>
where
T: for<'de> Deserialize<'de>,
{
serde_json::from_str(self.content.get())
.map_err(|e| err!(Database("Failed to deserialize pdu content into type: {e}")))
}

View file

@ -0,0 +1,90 @@
use ruma::api::client::filter::{RoomEventFilter, UrlFilter};
use serde_json::Value;
use crate::{implement, is_equal_to};
#[implement(super::Pdu)]
#[must_use]
pub fn matches(&self, filter: &RoomEventFilter) -> bool {
if !self.matches_sender(filter) {
return false;
}
if !self.matches_room(filter) {
return false;
}
if !self.matches_type(filter) {
return false;
}
if !self.matches_url(filter) {
return false;
}
true
}
#[implement(super::Pdu)]
fn matches_room(&self, filter: &RoomEventFilter) -> bool {
if filter.not_rooms.contains(&self.room_id) {
return false;
}
if let Some(rooms) = filter.rooms.as_ref() {
if !rooms.contains(&self.room_id) {
return false;
}
}
true
}
#[implement(super::Pdu)]
fn matches_sender(&self, filter: &RoomEventFilter) -> bool {
if filter.not_senders.contains(&self.sender) {
return false;
}
if let Some(senders) = filter.senders.as_ref() {
if !senders.contains(&self.sender) {
return false;
}
}
true
}
#[implement(super::Pdu)]
fn matches_type(&self, filter: &RoomEventFilter) -> bool {
let event_type = &self.kind.to_cow_str();
if filter.not_types.iter().any(is_equal_to!(event_type)) {
return false;
}
if let Some(types) = filter.types.as_ref() {
if !types.iter().any(is_equal_to!(event_type)) {
return false;
}
}
true
}
#[implement(super::Pdu)]
fn matches_url(&self, filter: &RoomEventFilter) -> bool {
let Some(url_filter) = filter.url_filter.as_ref() else {
return true;
};
//TODO: might be better to use Ruma's Raw rather than serde here
let url = serde_json::from_str::<Value>(self.content.get())
.expect("parsing content JSON failed")
.get("url")
.is_some_and(Value::is_string);
match url_filter {
| UrlFilter::EventsWithUrl => url,
| UrlFilter::EventsWithoutUrl => !url,
}
}

View file

@ -3,7 +3,6 @@ use crate::utils::u64_from_u8x8;
pub type ShortRoomId = ShortId;
pub type ShortEventId = ShortId;
pub type ShortStateKey = ShortId;
pub type ShortId = u64;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]

View file

@ -1,29 +1,117 @@
use ruma::{RoomVersionId, canonical_json::redact_content_in_place};
use serde_json::{Value as JsonValue, json, value::to_raw_value};
use ruma::{
OwnedEventId, RoomVersionId,
canonical_json::redact_content_in_place,
events::{TimelineEventType, room::redaction::RoomRedactionEventContent},
};
use serde::Deserialize;
use serde_json::{
json,
value::{RawValue as RawJsonValue, to_raw_value},
};
use crate::{Error, Result, err, implement};
use crate::{Error, Result, implement};
#[derive(Deserialize)]
struct ExtractRedactedBecause {
redacted_because: Option<serde::de::IgnoredAny>,
}
#[implement(super::Pdu)]
pub fn redact(&mut self, room_version_id: &RoomVersionId, reason: JsonValue) -> Result {
pub fn redact(&mut self, room_version_id: &RoomVersionId, reason: &Self) -> Result {
self.unsigned = None;
let mut content = serde_json::from_str(self.content.get())
.map_err(|e| err!(Request(BadJson("Failed to deserialize content into type: {e}"))))?;
.map_err(|_| Error::bad_database("PDU in db has invalid content."))?;
redact_content_in_place(&mut content, room_version_id, self.kind.to_string())
.map_err(|e| Error::Redaction(self.sender.server_name().to_owned(), e))?;
let reason = serde_json::to_value(reason).expect("Failed to preserialize reason");
self.unsigned = Some(
to_raw_value(&json!({
"redacted_because": serde_json::to_value(reason).expect("to_value(Pdu) always works")
}))
.expect("to string always works"),
);
let redacted_because = json!({
"redacted_because": reason,
});
self.unsigned = to_raw_value(&redacted_because)
.expect("Failed to serialize unsigned")
.into();
self.content = to_raw_value(&content).expect("Failed to serialize content");
self.content = to_raw_value(&content).expect("to string always works");
Ok(())
}
#[implement(super::Pdu)]
#[must_use]
pub fn is_redacted(&self) -> bool {
let Some(unsigned) = &self.unsigned else {
return false;
};
let Ok(unsigned) = ExtractRedactedBecause::deserialize(&**unsigned) else {
return false;
};
unsigned.redacted_because.is_some()
}
/// Copies the `redacts` property of the event to the `content` dict and
/// vice-versa.
///
/// This follows the specification's
/// [recommendation](https://spec.matrix.org/v1.10/rooms/v11/#moving-the-redacts-property-of-mroomredaction-events-to-a-content-property):
///
/// > For backwards-compatibility with older clients, servers should add a
/// > redacts
/// > property to the top level of m.room.redaction events in when serving
/// > such events
/// > over the Client-Server API.
///
/// > For improved compatibility with newer clients, servers should add a
/// > redacts property
/// > to the content of m.room.redaction events in older room versions when
/// > serving
/// > such events over the Client-Server API.
#[implement(super::Pdu)]
#[must_use]
pub fn copy_redacts(&self) -> (Option<OwnedEventId>, Box<RawJsonValue>) {
if self.kind == TimelineEventType::RoomRedaction {
if let Ok(mut content) =
serde_json::from_str::<RoomRedactionEventContent>(self.content.get())
{
match content.redacts {
| Some(redacts) => {
return (Some(redacts), self.content.clone());
},
| _ => match self.redacts.clone() {
| Some(redacts) => {
content.redacts = Some(redacts);
return (
self.redacts.clone(),
to_raw_value(&content)
.expect("Must be valid, we only added redacts field"),
);
},
| _ => {},
},
}
}
}
(self.redacts.clone(), self.content.clone())
}
#[implement(super::Pdu)]
#[must_use]
pub fn redacts_id(&self, room_version: &RoomVersionId) -> Option<OwnedEventId> {
use RoomVersionId::*;
if self.kind != TimelineEventType::RoomRedaction {
return None;
}
match *room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => self.redacts.clone(),
| _ =>
self.get_content::<RoomRedactionEventContent>()
.ok()?
.redacts,
}
}

View file

@ -0,0 +1,22 @@
use ruma::events::relation::RelationType;
use serde::Deserialize;
use crate::implement;
#[derive(Clone, Debug, Deserialize)]
struct ExtractRelType {
rel_type: RelationType,
}
#[derive(Clone, Debug, Deserialize)]
struct ExtractRelatesToEventId {
#[serde(rename = "m.relates_to")]
relates_to: ExtractRelType,
}
#[implement(super::Pdu)]
#[must_use]
pub fn relation_type_equal(&self, rel_type: &RelationType) -> bool {
self.get_content()
.map(|c: ExtractRelatesToEventId| c.relates_to.rel_type)
.is_ok_and(|r| r == *rel_type)
}

View file

@ -1,5 +1,8 @@
use smallstr::SmallString;
use super::ShortId;
pub type StateKey = SmallString<[u8; INLINE_SIZE]>;
pub type ShortStateKey = ShortId;
const INLINE_SIZE: usize = 48;

View file

@ -0,0 +1,257 @@
use ruma::{
events::{
AnyMessageLikeEvent, AnyStateEvent, AnyStrippedStateEvent, AnySyncStateEvent,
AnySyncTimelineEvent, AnyTimelineEvent, StateEvent, room::member::RoomMemberEventContent,
space::child::HierarchySpaceChildEvent,
},
serde::Raw,
};
use serde_json::{json, value::Value as JsonValue};
use crate::implement;
#[implement(super::Pdu)]
#[must_use]
#[inline]
pub fn into_room_event(self) -> Raw<AnyTimelineEvent> { self.to_room_event() }
#[implement(super::Pdu)]
#[must_use]
pub fn to_room_event(&self) -> Raw<AnyTimelineEvent> {
let value = self.to_room_event_value();
serde_json::from_value(value).expect("Failed to serialize Event value")
}
#[implement(super::Pdu)]
#[must_use]
#[inline]
pub fn to_room_event_value(&self) -> JsonValue {
let (redacts, content) = self.copy_redacts();
let mut json = json!({
"content": content,
"type": self.kind,
"event_id": self.event_id,
"sender": self.sender,
"origin_server_ts": self.origin_server_ts,
"room_id": self.room_id,
});
if let Some(unsigned) = &self.unsigned {
json["unsigned"] = json!(unsigned);
}
if let Some(state_key) = &self.state_key {
json["state_key"] = json!(state_key);
}
if let Some(redacts) = &redacts {
json["redacts"] = json!(redacts);
}
json
}
#[implement(super::Pdu)]
#[must_use]
#[inline]
pub fn into_message_like_event(self) -> Raw<AnyMessageLikeEvent> { self.to_message_like_event() }
#[implement(super::Pdu)]
#[must_use]
pub fn to_message_like_event(&self) -> Raw<AnyMessageLikeEvent> {
let value = self.to_message_like_event_value();
serde_json::from_value(value).expect("Failed to serialize Event value")
}
#[implement(super::Pdu)]
#[must_use]
#[inline]
pub fn to_message_like_event_value(&self) -> JsonValue {
let (redacts, content) = self.copy_redacts();
let mut json = json!({
"content": content,
"type": self.kind,
"event_id": self.event_id,
"sender": self.sender,
"origin_server_ts": self.origin_server_ts,
"room_id": self.room_id,
});
if let Some(unsigned) = &self.unsigned {
json["unsigned"] = json!(unsigned);
}
if let Some(state_key) = &self.state_key {
json["state_key"] = json!(state_key);
}
if let Some(redacts) = &redacts {
json["redacts"] = json!(redacts);
}
json
}
#[implement(super::Pdu)]
#[must_use]
#[inline]
pub fn into_sync_room_event(self) -> Raw<AnySyncTimelineEvent> { self.to_sync_room_event() }
#[implement(super::Pdu)]
#[must_use]
pub fn to_sync_room_event(&self) -> Raw<AnySyncTimelineEvent> {
let value = self.to_sync_room_event_value();
serde_json::from_value(value).expect("Failed to serialize Event value")
}
#[implement(super::Pdu)]
#[must_use]
#[inline]
pub fn to_sync_room_event_value(&self) -> JsonValue {
let (redacts, content) = self.copy_redacts();
let mut json = json!({
"content": content,
"type": self.kind,
"event_id": self.event_id,
"sender": self.sender,
"origin_server_ts": self.origin_server_ts,
});
if let Some(unsigned) = &self.unsigned {
json["unsigned"] = json!(unsigned);
}
if let Some(state_key) = &self.state_key {
json["state_key"] = json!(state_key);
}
if let Some(redacts) = &redacts {
json["redacts"] = json!(redacts);
}
json
}
#[implement(super::Pdu)]
#[must_use]
pub fn into_state_event(self) -> Raw<AnyStateEvent> {
let value = self.into_state_event_value();
serde_json::from_value(value).expect("Failed to serialize Event value")
}
#[implement(super::Pdu)]
#[must_use]
#[inline]
pub fn into_state_event_value(self) -> JsonValue {
let mut json = json!({
"content": self.content,
"type": self.kind,
"event_id": self.event_id,
"sender": self.sender,
"origin_server_ts": self.origin_server_ts,
"room_id": self.room_id,
"state_key": self.state_key,
});
if let Some(unsigned) = self.unsigned {
json["unsigned"] = json!(unsigned);
}
json
}
#[implement(super::Pdu)]
#[must_use]
pub fn into_sync_state_event(self) -> Raw<AnySyncStateEvent> {
let value = self.into_sync_state_event_value();
serde_json::from_value(value).expect("Failed to serialize Event value")
}
#[implement(super::Pdu)]
#[must_use]
#[inline]
pub fn into_sync_state_event_value(self) -> JsonValue {
let mut json = json!({
"content": self.content,
"type": self.kind,
"event_id": self.event_id,
"sender": self.sender,
"origin_server_ts": self.origin_server_ts,
"state_key": self.state_key,
});
if let Some(unsigned) = &self.unsigned {
json["unsigned"] = json!(unsigned);
}
json
}
#[implement(super::Pdu)]
#[must_use]
#[inline]
pub fn into_stripped_state_event(self) -> Raw<AnyStrippedStateEvent> {
self.to_stripped_state_event()
}
#[implement(super::Pdu)]
#[must_use]
pub fn to_stripped_state_event(&self) -> Raw<AnyStrippedStateEvent> {
let value = self.to_stripped_state_event_value();
serde_json::from_value(value).expect("Failed to serialize Event value")
}
#[implement(super::Pdu)]
#[must_use]
#[inline]
pub fn to_stripped_state_event_value(&self) -> JsonValue {
json!({
"content": self.content,
"type": self.kind,
"sender": self.sender,
"state_key": self.state_key,
})
}
#[implement(super::Pdu)]
#[must_use]
pub fn into_stripped_spacechild_state_event(self) -> Raw<HierarchySpaceChildEvent> {
let value = self.into_stripped_spacechild_state_event_value();
serde_json::from_value(value).expect("Failed to serialize Event value")
}
#[implement(super::Pdu)]
#[must_use]
#[inline]
pub fn into_stripped_spacechild_state_event_value(self) -> JsonValue {
json!({
"content": self.content,
"type": self.kind,
"sender": self.sender,
"state_key": self.state_key,
"origin_server_ts": self.origin_server_ts,
})
}
#[implement(super::Pdu)]
#[must_use]
pub fn into_member_event(self) -> Raw<StateEvent<RoomMemberEventContent>> {
let value = self.into_member_event_value();
serde_json::from_value(value).expect("Failed to serialize Event value")
}
#[implement(super::Pdu)]
#[must_use]
#[inline]
pub fn into_member_event_value(self) -> JsonValue {
let mut json = json!({
"content": self.content,
"type": self.kind,
"event_id": self.event_id,
"sender": self.sender,
"origin_server_ts": self.origin_server_ts,
"redacts": self.redacts,
"room_id": self.room_id,
"state_key": self.state_key,
});
if let Some(unsigned) = self.unsigned {
json["unsigned"] = json!(unsigned);
}
json
}

View file

@ -1,10 +1,11 @@
use std::collections::BTreeMap;
use ruma::MilliSecondsSinceUnixEpoch;
use serde::Deserialize;
use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue, to_raw_value};
use super::Pdu;
use crate::{Result, err, implement};
use crate::{Result, err, implement, is_true};
#[implement(Pdu)]
pub fn remove_transaction_id(&mut self) -> Result {
@ -73,3 +74,43 @@ pub fn add_relation(&mut self, name: &str, pdu: Option<&Pdu>) -> Result {
Ok(())
}
#[implement(Pdu)]
pub fn contains_unsigned_property<F>(&self, property: &str, is_type: F) -> bool
where
F: FnOnce(&JsonValue) -> bool,
{
self.get_unsigned_as_value()
.get(property)
.map(is_type)
.is_some_and(is_true!())
}
#[implement(Pdu)]
pub fn get_unsigned_property<T>(&self, property: &str) -> Result<T>
where
T: for<'de> Deserialize<'de>,
{
self.get_unsigned_as_value()
.get_mut(property)
.map(JsonValue::take)
.map(serde_json::from_value)
.ok_or(err!(Request(NotFound("property not found in unsigned object"))))?
.map_err(|e| err!(Database("Failed to deserialize unsigned.{property} into type: {e}")))
}
#[implement(Pdu)]
#[must_use]
pub fn get_unsigned_as_value(&self) -> JsonValue {
self.get_unsigned::<JsonValue>().unwrap_or_default()
}
#[implement(Pdu)]
pub fn get_unsigned<T>(&self) -> Result<JsonValue> {
self.unsigned
.as_ref()
.map(|raw| raw.get())
.map(serde_json::from_str)
.ok_or(err!(Request(NotFound("\"unsigned\" property not found in pdu"))))?
.map_err(|e| err!(Database("Failed to deserialize \"unsigned\" into value: {e}")))
}

View file

@ -13,6 +13,7 @@ use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, Signatures, UserId,
events::{
StateEventType, TimelineEventType,
pdu::{EventHash, Pdu, RoomV3Pdu},
room::{
join_rules::{JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent},
@ -25,10 +26,8 @@ use serde_json::{
value::{RawValue as RawJsonValue, to_raw_value as to_raw_json_value},
};
use crate::{
matrix::{Event, Pdu, pdu::EventHash},
state_res::{self as state_res, Error, Result, StateMap},
};
use self::event::PduEvent;
use crate::state_res::{self as state_res, Error, Event, Result, StateMap};
static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
@ -61,7 +60,7 @@ fn resolution_shallow_auth_chain(c: &mut test::Bencher) {
c.iter(|| async {
let ev_map = store.0.clone();
let state_sets = [&state_at_bob, &state_at_charlie];
let fetch = |id: OwnedEventId| ready(ev_map.get(&id).map(ToOwned::to_owned));
let fetch = |id: OwnedEventId| ready(ev_map.get(&id).clone());
let exists = |id: OwnedEventId| ready(ev_map.get(&id).is_some());
let auth_chain_sets: Vec<HashSet<_>> = state_sets
.iter()
@ -143,7 +142,7 @@ fn resolve_deeper_event_set(c: &mut test::Bencher) {
})
.collect();
let fetch = |id: OwnedEventId| ready(inner.get(&id).map(ToOwned::to_owned));
let fetch = |id: OwnedEventId| ready(inner.get(&id).clone());
let exists = |id: OwnedEventId| ready(inner.get(&id).is_some());
let _ = match state_res::resolve(
&RoomVersionId::V6,
@ -247,7 +246,7 @@ impl<E: Event + Clone> TestStore<E> {
}
}
impl TestStore<Pdu> {
impl TestStore<PduEvent> {
#[allow(clippy::type_complexity)]
fn set_up(
&mut self,
@ -381,7 +380,7 @@ fn to_pdu_event<S>(
content: Box<RawJsonValue>,
auth_events: &[S],
prev_events: &[S],
) -> Pdu
) -> PduEvent
where
S: AsRef<str>,
{
@ -404,28 +403,30 @@ where
.map(event_id)
.collect::<Vec<_>>();
Pdu {
let state_key = state_key.map(ToOwned::to_owned);
PduEvent {
event_id: id.try_into().unwrap(),
room_id: room_id().to_owned(),
sender: sender.to_owned(),
origin_server_ts: ts.try_into().unwrap(),
state_key: state_key.map(Into::into),
kind: ev_type,
content,
origin: None,
redacts: None,
unsigned: None,
auth_events,
prev_events,
depth: uint!(0),
hashes: EventHash { sha256: String::new() },
signatures: None,
rest: Pdu::RoomV3Pdu(RoomV3Pdu {
room_id: room_id().to_owned(),
sender: sender.to_owned(),
origin_server_ts: MilliSecondsSinceUnixEpoch(ts.try_into().unwrap()),
state_key,
kind: ev_type,
content,
redacts: None,
unsigned: btreemap! {},
auth_events,
prev_events,
depth: uint!(0),
hashes: EventHash::new(String::new()),
signatures: Signatures::new(),
}),
}
}
// all graphs start with these input events
#[allow(non_snake_case)]
fn INITIAL_EVENTS() -> HashMap<OwnedEventId, Pdu> {
fn INITIAL_EVENTS() -> HashMap<OwnedEventId, PduEvent> {
vec![
to_pdu_event::<&EventId>(
"CREATE",
@ -507,7 +508,7 @@ fn INITIAL_EVENTS() -> HashMap<OwnedEventId, Pdu> {
// all graphs start with these input events
#[allow(non_snake_case)]
fn BAN_STATE_SET() -> HashMap<OwnedEventId, Pdu> {
fn BAN_STATE_SET() -> HashMap<OwnedEventId, PduEvent> {
vec![
to_pdu_event(
"PA",
@ -550,3 +551,119 @@ fn BAN_STATE_SET() -> HashMap<OwnedEventId, Pdu> {
.map(|ev| (ev.event_id().to_owned(), ev))
.collect()
}
/// Convenience trait for adding event type plus state key to state maps.
trait EventTypeExt {
fn with_state_key(self, state_key: impl Into<String>) -> (StateEventType, String);
}
impl EventTypeExt for &TimelineEventType {
fn with_state_key(self, state_key: impl Into<String>) -> (StateEventType, String) {
(self.to_string().into(), state_key.into())
}
}
mod event {
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, UserId,
events::{TimelineEventType, pdu::Pdu},
};
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue as RawJsonValue;
use super::Event;
impl Event for PduEvent {
fn event_id(&self) -> &EventId { &self.event_id }
fn room_id(&self) -> &RoomId {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => &ev.room_id,
| Pdu::RoomV3Pdu(ev) => &ev.room_id,
#[cfg(not(feature = "unstable-exhaustive-types"))]
| _ => unreachable!("new PDU version"),
}
}
fn sender(&self) -> &UserId {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => &ev.sender,
| Pdu::RoomV3Pdu(ev) => &ev.sender,
#[cfg(not(feature = "unstable-exhaustive-types"))]
| _ => unreachable!("new PDU version"),
}
}
fn event_type(&self) -> &TimelineEventType {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => &ev.kind,
| Pdu::RoomV3Pdu(ev) => &ev.kind,
#[cfg(not(feature = "unstable-exhaustive-types"))]
| _ => unreachable!("new PDU version"),
}
}
fn content(&self) -> &RawJsonValue {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => &ev.content,
| Pdu::RoomV3Pdu(ev) => &ev.content,
#[cfg(not(feature = "unstable-exhaustive-types"))]
| _ => unreachable!("new PDU version"),
}
}
fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => ev.origin_server_ts,
| Pdu::RoomV3Pdu(ev) => ev.origin_server_ts,
#[cfg(not(feature = "unstable-exhaustive-types"))]
| _ => unreachable!("new PDU version"),
}
}
fn state_key(&self) -> Option<&str> {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => ev.state_key.as_deref(),
| Pdu::RoomV3Pdu(ev) => ev.state_key.as_deref(),
#[cfg(not(feature = "unstable-exhaustive-types"))]
| _ => unreachable!("new PDU version"),
}
}
fn prev_events(&self) -> Box<dyn DoubleEndedIterator<Item = &EventId> + Send + '_> {
match &self.rest {
| Pdu::RoomV1Pdu(ev) =>
Box::new(ev.prev_events.iter().map(|(id, _)| id.as_ref())),
| Pdu::RoomV3Pdu(ev) => Box::new(ev.prev_events.iter().map(AsRef::as_ref)),
#[cfg(not(feature = "unstable-exhaustive-types"))]
| _ => unreachable!("new PDU version"),
}
}
fn auth_events(&self) -> Box<dyn DoubleEndedIterator<Item = &EventId> + Send + '_> {
match &self.rest {
| Pdu::RoomV1Pdu(ev) =>
Box::new(ev.auth_events.iter().map(|(id, _)| id.as_ref())),
| Pdu::RoomV3Pdu(ev) => Box::new(ev.auth_events.iter().map(AsRef::as_ref)),
#[cfg(not(feature = "unstable-exhaustive-types"))]
| _ => unreachable!("new PDU version"),
}
}
fn redacts(&self) -> Option<&EventId> {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => ev.redacts.as_deref(),
| Pdu::RoomV3Pdu(ev) => ev.redacts.as_deref(),
#[cfg(not(feature = "unstable-exhaustive-types"))]
| _ => unreachable!("new PDU version"),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub(crate) struct PduEvent {
pub(crate) event_id: OwnedEventId,
#[serde(flatten)]
pub(crate) rest: Pdu,
}
}

View file

@ -136,17 +136,17 @@ pub fn auth_types_for_event(
event_id = incoming_event.event_id().as_str(),
)
)]
pub async fn auth_check<E, F, Fut>(
pub async fn auth_check<F, Fut, Fetched, Incoming>(
room_version: &RoomVersion,
incoming_event: &E,
current_third_party_invite: Option<&E>,
incoming_event: &Incoming,
current_third_party_invite: Option<&Incoming>,
fetch_state: F,
) -> Result<bool, Error>
where
F: Fn(&StateEventType, &str) -> Fut + Send,
Fut: Future<Output = Option<E>> + Send,
E: Event + Send + Sync,
for<'a> &'a E: Event + Send,
Fut: Future<Output = Option<Fetched>> + Send,
Fetched: Event + Send,
Incoming: Event + Send + Sync,
{
debug!(
event_id = format!("{}", incoming_event.event_id()),
@ -514,24 +514,20 @@ where
/// event and the current State.
#[allow(clippy::too_many_arguments)]
#[allow(clippy::cognitive_complexity)]
fn valid_membership_change<E>(
fn valid_membership_change(
room_version: &RoomVersion,
target_user: &UserId,
target_user_membership_event: Option<&E>,
target_user_membership_event: Option<&impl Event>,
sender: &UserId,
sender_membership_event: Option<&E>,
current_event: &E,
current_third_party_invite: Option<&E>,
power_levels_event: Option<&E>,
join_rules_event: Option<&E>,
sender_membership_event: Option<&impl Event>,
current_event: impl Event,
current_third_party_invite: Option<&impl Event>,
power_levels_event: Option<&impl Event>,
join_rules_event: Option<&impl Event>,
user_for_join_auth: Option<&UserId>,
user_for_join_auth_membership: &MembershipState,
create_room: &E,
) -> Result<bool>
where
E: Event + Send + Sync,
for<'a> &'a E: Event + Send,
{
create_room: &impl Event,
) -> Result<bool> {
#[derive(Deserialize)]
struct GetThirdPartyInvite {
third_party_invite: Option<Raw<ThirdPartyInvite>>,
@ -824,7 +820,7 @@ where
///
/// Does the event have the correct userId as its state_key if it's not the ""
/// state_key.
fn can_send_event(event: &impl Event, ple: Option<&impl Event>, user_level: Int) -> bool {
fn can_send_event(event: impl Event, ple: Option<impl Event>, user_level: Int) -> bool {
let event_type_power_level = get_send_level(event.event_type(), event.state_key(), ple);
debug!(
@ -850,8 +846,8 @@ fn can_send_event(event: &impl Event, ple: Option<&impl Event>, user_level: Int)
/// Confirm that the event sender has the required power levels.
fn check_power_levels(
room_version: &RoomVersion,
power_event: &impl Event,
previous_power_event: Option<&impl Event>,
power_event: impl Event,
previous_power_event: Option<impl Event>,
user_level: Int,
) -> Option<bool> {
match power_event.state_key() {
@ -1014,7 +1010,7 @@ fn get_deserialize_levels(
/// given event.
fn check_redaction(
_room_version: &RoomVersion,
redaction_event: &impl Event,
redaction_event: impl Event,
user_level: Int,
redact_level: Int,
) -> Result<bool> {
@ -1043,7 +1039,7 @@ fn check_redaction(
fn get_send_level(
e_type: &TimelineEventType,
state_key: Option<&str>,
power_lvl: Option<&impl Event>,
power_lvl: Option<impl Event>,
) -> Int {
power_lvl
.and_then(|ple| {
@ -1066,7 +1062,7 @@ fn verify_third_party_invite(
target_user: Option<&UserId>,
sender: &UserId,
tp_id: &ThirdPartyInvite,
current_third_party_invite: Option<&impl Event>,
current_third_party_invite: Option<impl Event>,
) -> bool {
// 1. Check for user being banned happens before this is called
// checking for mxid and token keys is done by ruma when deserializing
@ -1132,15 +1128,12 @@ mod tests {
};
use serde_json::value::to_raw_value as to_raw_json_value;
use crate::{
matrix::{Event, EventTypeExt, Pdu as PduEvent},
state_res::{
RoomVersion, StateMap,
event_auth::valid_membership_change,
test_utils::{
INITIAL_EVENTS, INITIAL_EVENTS_CREATE_ROOM, alice, charlie, ella, event_id,
member_content_ban, member_content_join, room_id, to_pdu_event,
},
use crate::state_res::{
Event, EventTypeExt, RoomVersion, StateMap,
event_auth::valid_membership_change,
test_utils::{
INITIAL_EVENTS, INITIAL_EVENTS_CREATE_ROOM, PduEvent, alice, charlie, ella, event_id,
member_content_ban, member_content_join, room_id, to_pdu_event,
},
};

View file

@ -37,7 +37,7 @@ pub use self::{
};
use crate::{
debug, debug_error,
matrix::{Event, StateKey},
matrix::{event::Event, pdu::StateKey},
trace,
utils::stream::{BroadbandExt, IterStream, ReadyExt, TryBroadbandExt, WidebandExt},
warn,
@ -74,7 +74,7 @@ type Result<T, E = Error> = crate::Result<T, E>;
/// event is part of the same room.
//#[tracing::instrument(level = "debug", skip(state_sets, auth_chain_sets,
//#[tracing::instrument(level event_fetch))]
pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, ExistsFut>(
pub async fn resolve<'a, E, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, ExistsFut>(
room_version: &RoomVersionId,
state_sets: Sets,
auth_chain_sets: &'a [HashSet<OwnedEventId, Hasher>],
@ -83,14 +83,14 @@ pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, Ex
) -> Result<StateMap<OwnedEventId>>
where
Fetch: Fn(OwnedEventId) -> FetchFut + Sync,
FetchFut: Future<Output = Option<Pdu>> + Send,
FetchFut: Future<Output = Option<E>> + Send,
Exists: Fn(OwnedEventId) -> ExistsFut + Sync,
ExistsFut: Future<Output = bool> + Send,
Sets: IntoIterator<IntoIter = SetIter> + Send,
SetIter: Iterator<Item = &'a StateMap<OwnedEventId>> + Clone + Send,
Hasher: BuildHasher + Send + Sync,
Pdu: Event + Clone + Send + Sync,
for<'b> &'b Pdu: Event + Send,
E: Event + Clone + Send + Sync,
for<'b> &'b E: Send,
{
debug!("State resolution starting");
@ -221,7 +221,6 @@ where
let state_sets_iter =
state_sets_iter.inspect(|_| state_set_count = state_set_count.saturating_add(1));
for (k, v) in state_sets_iter.flatten() {
occurrences
.entry(k)
@ -306,7 +305,6 @@ where
let pl = get_power_level_for_sender(&event_id, fetch_event)
.await
.ok()?;
Some((event_id, pl))
})
.inspect(|(event_id, pl)| {
@ -524,7 +522,6 @@ where
Fut: Future<Output = Option<E>> + Send,
S: Stream<Item = &'a EventId> + Send + 'a,
E: Event + Clone + Send + Sync,
for<'b> &'b E: Event + Send,
{
debug!("starting iterative auth check");
@ -555,7 +552,7 @@ where
let auth_events = &auth_events;
let mut resolved_state = unconflicted_state;
for event in events_to_check {
for event in &events_to_check {
let state_key = event
.state_key()
.ok_or_else(|| Error::InvalidPdu("State event had no state key".to_owned()))?;
@ -610,15 +607,11 @@ where
});
let fetch_state = |ty: &StateEventType, key: &str| {
future::ready(
auth_state
.get(&ty.with_state_key(key))
.map(ToOwned::to_owned),
)
future::ready(auth_state.get(&ty.with_state_key(key)))
};
let auth_result =
auth_check(room_version, &event, current_third_party, fetch_state).await;
auth_check(room_version, &event, current_third_party.as_ref(), fetch_state).await;
match auth_result {
| Ok(true) => {
@ -801,11 +794,11 @@ where
}
}
fn is_type_and_key(ev: &impl Event, ev_type: &TimelineEventType, state_key: &str) -> bool {
fn is_type_and_key(ev: impl Event, ev_type: &TimelineEventType, state_key: &str) -> bool {
ev.event_type() == ev_type && ev.state_key() == Some(state_key)
}
fn is_power_event(event: &impl Event) -> bool {
fn is_power_event(event: impl Event) -> bool {
match event.event_type() {
| TimelineEventType::RoomPowerLevels
| TimelineEventType::RoomJoinRules
@ -866,19 +859,15 @@ mod tests {
use serde_json::{json, value::to_raw_value as to_raw_json_value};
use super::{
StateMap, is_power_event,
Event, EventTypeExt, StateMap, is_power_event,
room_version::RoomVersion,
test_utils::{
INITIAL_EVENTS, TestStore, alice, bob, charlie, do_check, ella, event_id,
INITIAL_EVENTS, PduEvent, TestStore, alice, bob, charlie, do_check, ella, event_id,
member_content_ban, member_content_join, room_id, to_init_pdu_event, to_pdu_event,
zara,
},
};
use crate::{
debug,
matrix::{Event, EventTypeExt, Pdu as PduEvent},
utils::stream::IterStream,
};
use crate::{debug, utils::stream::IterStream};
async fn test_event_sort() {
use futures::future::ready;

View file

@ -10,6 +10,7 @@ use ruma::{
UserId, event_id,
events::{
TimelineEventType,
pdu::{EventHash, Pdu, RoomV3Pdu},
room::{
join_rules::{JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent},
@ -22,16 +23,17 @@ use serde_json::{
value::{RawValue as RawJsonValue, to_raw_value as to_raw_json_value},
};
pub(crate) use self::event::PduEvent;
use super::auth_types_for_event;
use crate::{
Result, info,
matrix::{Event, EventTypeExt, Pdu, StateMap, pdu::EventHash},
matrix::{Event, EventTypeExt, StateMap},
};
static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
pub(crate) async fn do_check(
events: &[Pdu],
events: &[PduEvent],
edges: Vec<Vec<OwnedEventId>>,
expected_state_ids: Vec<OwnedEventId>,
) {
@ -79,8 +81,8 @@ pub(crate) async fn do_check(
}
}
// event_id -> Pdu
let mut event_map: HashMap<OwnedEventId, Pdu> = HashMap::new();
// event_id -> PduEvent
let mut event_map: HashMap<OwnedEventId, PduEvent> = HashMap::new();
// event_id -> StateMap<OwnedEventId>
let mut state_at_event: HashMap<OwnedEventId, StateMap<OwnedEventId>> = HashMap::new();
@ -263,7 +265,7 @@ impl<E: Event + Clone> TestStore<E> {
// A StateStore implementation for testing
#[allow(clippy::type_complexity)]
impl TestStore<Pdu> {
impl TestStore<PduEvent> {
pub(crate) fn set_up(
&mut self,
) -> (StateMap<OwnedEventId>, StateMap<OwnedEventId>, StateMap<OwnedEventId>) {
@ -388,7 +390,7 @@ pub(crate) fn to_init_pdu_event(
ev_type: TimelineEventType,
state_key: Option<&str>,
content: Box<RawJsonValue>,
) -> Pdu {
) -> PduEvent {
let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst);
let id = if id.contains('$') {
id.to_owned()
@ -396,22 +398,24 @@ pub(crate) fn to_init_pdu_event(
format!("${id}:foo")
};
Pdu {
let state_key = state_key.map(ToOwned::to_owned);
PduEvent {
event_id: id.try_into().unwrap(),
room_id: room_id().to_owned(),
sender: sender.to_owned(),
origin_server_ts: ts.try_into().unwrap(),
state_key: state_key.map(Into::into),
kind: ev_type,
content,
origin: None,
redacts: None,
unsigned: None,
auth_events: vec![],
prev_events: vec![],
depth: uint!(0),
hashes: EventHash { sha256: "".to_owned() },
signatures: None,
rest: Pdu::RoomV3Pdu(RoomV3Pdu {
room_id: room_id().to_owned(),
sender: sender.to_owned(),
origin_server_ts: MilliSecondsSinceUnixEpoch(ts.try_into().unwrap()),
state_key,
kind: ev_type,
content,
redacts: None,
unsigned: BTreeMap::new(),
auth_events: vec![],
prev_events: vec![],
depth: uint!(0),
hashes: EventHash::new("".to_owned()),
signatures: ServerSignatures::default(),
}),
}
}
@ -423,7 +427,7 @@ pub(crate) fn to_pdu_event<S>(
content: Box<RawJsonValue>,
auth_events: &[S],
prev_events: &[S],
) -> Pdu
) -> PduEvent
where
S: AsRef<str>,
{
@ -444,28 +448,30 @@ where
.map(event_id)
.collect::<Vec<_>>();
Pdu {
let state_key = state_key.map(ToOwned::to_owned);
PduEvent {
event_id: id.try_into().unwrap(),
room_id: room_id().to_owned(),
sender: sender.to_owned(),
origin_server_ts: ts.try_into().unwrap(),
state_key: state_key.map(Into::into),
kind: ev_type,
content,
origin: None,
redacts: None,
unsigned: None,
auth_events,
prev_events,
depth: uint!(0),
hashes: EventHash { sha256: "".to_owned() },
signatures: None,
rest: Pdu::RoomV3Pdu(RoomV3Pdu {
room_id: room_id().to_owned(),
sender: sender.to_owned(),
origin_server_ts: MilliSecondsSinceUnixEpoch(ts.try_into().unwrap()),
state_key,
kind: ev_type,
content,
redacts: None,
unsigned: BTreeMap::new(),
auth_events,
prev_events,
depth: uint!(0),
hashes: EventHash::new("".to_owned()),
signatures: ServerSignatures::default(),
}),
}
}
// all graphs start with these input events
#[allow(non_snake_case)]
pub(crate) fn INITIAL_EVENTS() -> HashMap<OwnedEventId, Pdu> {
pub(crate) fn INITIAL_EVENTS() -> HashMap<OwnedEventId, PduEvent> {
vec![
to_pdu_event::<&EventId>(
"CREATE",
@ -547,7 +553,7 @@ pub(crate) fn INITIAL_EVENTS() -> HashMap<OwnedEventId, Pdu> {
// all graphs start with these input events
#[allow(non_snake_case)]
pub(crate) fn INITIAL_EVENTS_CREATE_ROOM() -> HashMap<OwnedEventId, Pdu> {
pub(crate) fn INITIAL_EVENTS_CREATE_ROOM() -> HashMap<OwnedEventId, PduEvent> {
vec![to_pdu_event::<&EventId>(
"CREATE",
alice(),
@ -569,3 +575,111 @@ pub(crate) fn INITIAL_EDGES() -> Vec<OwnedEventId> {
.map(event_id)
.collect::<Vec<_>>()
}
pub(crate) mod event {
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, UserId,
events::{TimelineEventType, pdu::Pdu},
};
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue as RawJsonValue;
use crate::Event;
impl Event for PduEvent {
fn event_id(&self) -> &EventId { &self.event_id }
fn room_id(&self) -> &RoomId {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => &ev.room_id,
| Pdu::RoomV3Pdu(ev) => &ev.room_id,
#[allow(unreachable_patterns)]
| _ => unreachable!("new PDU version"),
}
}
fn sender(&self) -> &UserId {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => &ev.sender,
| Pdu::RoomV3Pdu(ev) => &ev.sender,
#[allow(unreachable_patterns)]
| _ => unreachable!("new PDU version"),
}
}
fn event_type(&self) -> &TimelineEventType {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => &ev.kind,
| Pdu::RoomV3Pdu(ev) => &ev.kind,
#[allow(unreachable_patterns)]
| _ => unreachable!("new PDU version"),
}
}
fn content(&self) -> &RawJsonValue {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => &ev.content,
| Pdu::RoomV3Pdu(ev) => &ev.content,
#[allow(unreachable_patterns)]
| _ => unreachable!("new PDU version"),
}
}
fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => ev.origin_server_ts,
| Pdu::RoomV3Pdu(ev) => ev.origin_server_ts,
#[allow(unreachable_patterns)]
| _ => unreachable!("new PDU version"),
}
}
fn state_key(&self) -> Option<&str> {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => ev.state_key.as_deref(),
| Pdu::RoomV3Pdu(ev) => ev.state_key.as_deref(),
#[allow(unreachable_patterns)]
| _ => unreachable!("new PDU version"),
}
}
#[allow(refining_impl_trait)]
fn prev_events(&self) -> Box<dyn DoubleEndedIterator<Item = &EventId> + Send + '_> {
match &self.rest {
| Pdu::RoomV1Pdu(ev) =>
Box::new(ev.prev_events.iter().map(|(id, _)| id.as_ref())),
| Pdu::RoomV3Pdu(ev) => Box::new(ev.prev_events.iter().map(AsRef::as_ref)),
#[allow(unreachable_patterns)]
| _ => unreachable!("new PDU version"),
}
}
#[allow(refining_impl_trait)]
fn auth_events(&self) -> Box<dyn DoubleEndedIterator<Item = &EventId> + Send + '_> {
match &self.rest {
| Pdu::RoomV1Pdu(ev) =>
Box::new(ev.auth_events.iter().map(|(id, _)| id.as_ref())),
| Pdu::RoomV3Pdu(ev) => Box::new(ev.auth_events.iter().map(AsRef::as_ref)),
#[allow(unreachable_patterns)]
| _ => unreachable!("new PDU version"),
}
}
fn redacts(&self) -> Option<&EventId> {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => ev.redacts.as_deref(),
| Pdu::RoomV3Pdu(ev) => ev.redacts.as_deref(),
#[allow(unreachable_patterns)]
| _ => unreachable!("new PDU version"),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[allow(clippy::exhaustive_structs)]
pub(crate) struct PduEvent {
pub(crate) event_id: OwnedEventId,
#[serde(flatten)]
pub(crate) rest: Pdu,
}
}

View file

@ -25,9 +25,7 @@ pub use info::{
rustc_flags_capture, version,
version::{name, version},
};
pub use matrix::{
Event, EventTypeExt, Pdu, PduCount, PduEvent, PduId, RoomVersion, pdu, state_res,
};
pub use matrix::{Event, EventTypeExt, PduCount, PduEvent, PduId, RoomVersion, pdu, state_res};
pub use server::Server;
pub use utils::{ctor, dtor, implement, result, result::Result};

View file

@ -44,7 +44,6 @@ impl Module {
.handle
.as_ref()
.expect("backing library loaded by this instance");
// SAFETY: Calls dlsym(3) on unix platforms. This might not have to be unsafe
// if wrapped in libloading with_dlerror().
let sym = unsafe { handle.get::<Prototype>(cname.as_bytes()) };

View file

@ -27,7 +27,6 @@ pub fn to_name(path: &OsStr) -> Result<String> {
.expect("path file stem")
.to_str()
.expect("name string");
let name = name.strip_prefix("lib").unwrap_or(name).to_owned();
Ok(name)

View file

@ -23,10 +23,8 @@ impl fmt::Display for Escape<'_> {
| '"' => "&quot;",
| _ => continue,
};
fmt.write_str(&pile_o_bits[last..i])?;
fmt.write_str(s)?;
// NOTE: we only expect single byte characters here - which is fine as long as
// we only match single byte characters
last = i.saturating_add(1);

View file

@ -1,4 +1,4 @@
use std::{fmt, marker::PhantomData, str::FromStr};
use std::{fmt, str::FromStr};
use ruma::{CanonicalJsonError, CanonicalJsonObject, canonical_json::try_from_json_map};
@ -11,28 +11,25 @@ use crate::Result;
pub fn to_canonical_object<T: serde::Serialize>(
value: T,
) -> Result<CanonicalJsonObject, CanonicalJsonError> {
use CanonicalJsonError::SerDe;
use serde::ser::Error;
match serde_json::to_value(value).map_err(SerDe)? {
match serde_json::to_value(value).map_err(CanonicalJsonError::SerDe)? {
| serde_json::Value::Object(map) => try_from_json_map(map),
| _ => Err(SerDe(serde_json::Error::custom("Value must be an object"))),
| _ =>
Err(CanonicalJsonError::SerDe(serde_json::Error::custom("Value must be an object"))),
}
}
pub fn deserialize_from_str<'de, D, T, E>(deserializer: D) -> Result<T, D::Error>
where
pub fn deserialize_from_str<
'de,
D: serde::de::Deserializer<'de>,
T: FromStr<Err = E>,
E: fmt::Display,
{
struct Visitor<T: FromStr<Err = E>, E>(PhantomData<T>);
impl<T, Err> serde::de::Visitor<'_> for Visitor<T, Err>
where
T: FromStr<Err = Err>,
Err: fmt::Display,
{
>(
deserializer: D,
) -> Result<T, D::Error> {
struct Visitor<T: FromStr<Err = E>, E>(std::marker::PhantomData<T>);
impl<T: FromStr<Err = Err>, Err: fmt::Display> serde::de::Visitor<'_> for Visitor<T, Err> {
type Value = T;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
@ -46,6 +43,5 @@ where
v.parse().map_err(serde::de::Error::custom)
}
}
deserializer.deserialize_str(Visitor(PhantomData))
deserializer.deserialize_str(Visitor(std::marker::PhantomData))
}

View file

@ -105,11 +105,14 @@ pub fn whole_unit(d: Duration) -> Unit {
| 86_400.. => Days(d.as_secs() / 86_400),
| 3_600..=86_399 => Hours(d.as_secs() / 3_600),
| 60..=3_599 => Mins(d.as_secs() / 60),
| _ => match d.as_micros() {
| 1_000_000.. => Secs(d.as_secs()),
| 1_000..=999_999 => Millis(d.subsec_millis().into()),
| _ => match d.as_nanos() {
| 1_000.. => Micros(d.subsec_micros().into()),
| _ => Nanos(d.subsec_nanos().into()),
},
},

View file

@ -37,6 +37,7 @@ impl Watchers {
pub(crate) fn wake(&self, key: &[u8]) {
let watchers = self.watchers.read().unwrap();
let mut triggered = Vec::new();
for length in 0..=key.len() {
if watchers.contains_key(&key[..length]) {
triggered.push(&key[..length]);

View file

@ -22,12 +22,10 @@ pub(crate) fn init(
let reload_handles = LogLevelReloadHandles::default();
let console_span_events = fmt_span::from_str(&config.log_span_events).unwrap_or_err();
let console_filter = EnvFilter::builder()
.with_regex(config.log_filter_regex)
.parse(&config.log)
.map_err(|e| err!(Config("log", "{e}.")))?;
let console_layer = fmt::Layer::new()
.with_span_events(console_span_events)
.event_format(ConsoleFormat::new(config))
@ -36,7 +34,6 @@ pub(crate) fn init(
let (console_reload_filter, console_reload_handle) =
reload::Layer::new(console_filter.clone());
reload_handles.add("console", Box::new(console_reload_handle));
let cap_state = Arc::new(capture::State::new());
@ -60,10 +57,8 @@ pub(crate) fn init(
let subscriber = {
let sentry_filter = EnvFilter::try_new(&config.sentry_filter)
.map_err(|e| err!(Config("sentry_filter", "{e}.")))?;
let sentry_layer = sentry_tracing::layer();
let (sentry_reload_filter, sentry_reload_handle) = reload::Layer::new(sentry_filter);
reload_handles.add("sentry", Box::new(sentry_reload_handle));
subscriber.with(sentry_layer.with_filter(sentry_reload_filter))
};
@ -73,15 +68,12 @@ pub(crate) fn init(
let (flame_layer, flame_guard) = if config.tracing_flame {
let flame_filter = EnvFilter::try_new(&config.tracing_flame_filter)
.map_err(|e| err!(Config("tracing_flame_filter", "{e}.")))?;
let (flame_layer, flame_guard) =
tracing_flame::FlameLayer::with_file(&config.tracing_flame_output_path)
.map_err(|e| err!(Config("tracing_flame_output_path", "{e}.")))?;
let flame_layer = flame_layer
.with_empty_samples(false)
.with_filter(flame_filter);
(Some(flame_layer), Some(flame_guard))
} else {
(None, None)
@ -89,24 +81,19 @@ pub(crate) fn init(
let jaeger_filter = EnvFilter::try_new(&config.jaeger_filter)
.map_err(|e| err!(Config("jaeger_filter", "{e}.")))?;
let jaeger_layer = config.allow_jaeger.then(|| {
opentelemetry::global::set_text_map_propagator(
opentelemetry_jaeger::Propagator::new(),
);
let tracer = opentelemetry_jaeger::new_agent_pipeline()
.with_auto_split_batch(true)
.with_service_name(conduwuit_core::name())
.install_batch(opentelemetry_sdk::runtime::Tokio)
.expect("jaeger agent pipeline");
let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
let (jaeger_reload_filter, jaeger_reload_handle) =
reload::Layer::new(jaeger_filter.clone());
reload_handles.add("jaeger", Box::new(jaeger_reload_handle));
Some(telemetry.with_filter(jaeger_reload_filter))
});

View file

@ -2,7 +2,6 @@
use conduwuit_core::rustc_flags_capture;
pub(crate) mod clap;
mod logging;
mod mods;
mod restart;

View file

@ -51,9 +51,7 @@ pub(crate) async fn run(server: &Arc<Server>, starts: bool) -> Result<(bool, boo
},
};
}
server.server.stopping.store(false, Ordering::Release);
let run = main_mod.get::<RunFuncProto>("run")?;
if let Err(error) = run(server
.services
@ -66,9 +64,7 @@ pub(crate) async fn run(server: &Arc<Server>, starts: bool) -> Result<(bool, boo
error!("Running server: {error}");
return Err(error);
}
let reloads = server.server.reloading.swap(false, Ordering::AcqRel);
let stops = !reloads || stale(server).await? <= restart_thresh();
let starts = reloads && stops;
if stops {

View file

@ -35,13 +35,11 @@ fn options(config: &Config) -> ClientOptions {
.expect("init_sentry should only be called if sentry is enabled and this is not None")
.as_str();
let server_name = config
.sentry_send_server_name
.then(|| config.server_name.to_string().into());
ClientOptions {
dsn: Some(Dsn::from_str(dsn).expect("sentry_endpoint must be a valid URL")),
server_name,
server_name: config
.sentry_send_server_name
.then(|| config.server_name.to_string().into()),
traces_sample_rate: config.sentry_traces_sample_rate,
debug: cfg!(debug_assertions),
release: sentry::release_name!(),

View file

@ -98,8 +98,8 @@ async fn execute(
fn handle_result(method: &Method, uri: &Uri, result: Response) -> Result<Response, StatusCode> {
let status = result.status();
let code = status.as_u16();
let reason = status.canonical_reason().unwrap_or("Unknown Reason");
let code = status.as_u16();
if status.is_server_error() {
error!(method = ?method, uri = ?uri, "{code} {reason}");

View file

@ -170,56 +170,3 @@ async fn set_room_tag(&self, room_id: &RoomId, user_id: &UserId, tag: &str) -> R
)
.await
}
/// Demote an admin, removing its rights.
#[implement(super::Service)]
pub async fn revoke_admin(&self, user_id: &UserId) -> Result {
use MembershipState::{Invite, Join, Knock, Leave};
let Ok(room_id) = self.get_admin_room().await else {
return Err!(error!("No admin room available or created."));
};
let state_lock = self.services.state.mutex.lock(&room_id).await;
let event = match self
.services
.state_accessor
.get_member(&room_id, user_id)
.await
{
| Err(e) if e.is_not_found() => return Err!("{user_id} was never an admin."),
| Err(e) => return Err!(error!(?e, "Failure occurred while attempting revoke.")),
| Ok(event) if !matches!(event.membership, Invite | Knock | Join) =>
return Err!("Cannot revoke {user_id} in membership state {:?}.", event.membership),
| Ok(event) => {
assert!(
matches!(event.membership, Invite | Knock | Join),
"Incorrect membership state to remove user."
);
event
},
};
self.services
.timeline
.build_and_append_pdu(
PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
membership: Leave,
reason: Some("Admin Revoked".into()),
is_direct: None,
join_authorized_via_users_server: None,
third_party_invite: None,
..event
}),
self.services.globals.server_user.as_ref(),
&room_id,
&state_lock,
)
.await
.map(|_| ())
}

View file

@ -4,16 +4,17 @@ mod execute;
mod grant;
use std::{
future::Future,
pin::Pin,
sync::{Arc, RwLock as StdRwLock, Weak},
};
use async_trait::async_trait;
use conduwuit_core::{
Error, Event, Result, Server, debug, err, error, error::default_log, pdu::PduBuilder,
use conduwuit::{
Error, PduEvent, Result, Server, debug, err, error, error::default_log, pdu::PduBuilder,
};
pub use create::create_admin_room;
use futures::{Future, FutureExt, TryFutureExt};
use futures::{FutureExt, TryFutureExt};
use loole::{Receiver, Sender};
use ruma::{
OwnedEventId, OwnedRoomId, RoomId, UserId,
@ -142,13 +143,6 @@ impl crate::Service for Service {
}
impl Service {
/// Sends markdown notice to the admin room as the admin user.
pub async fn notice(&self, body: &str) {
self.send_message(RoomMessageEventContent::notice_markdown(body))
.await
.ok();
}
/// Sends markdown message (not an m.notice for notification reasons) to the
/// admin room as the admin user.
pub async fn send_text(&self, body: &str) {
@ -305,13 +299,13 @@ impl Service {
return Ok(());
};
let response_sender = if self.is_admin_room(pdu.room_id()).await {
let response_sender = if self.is_admin_room(&pdu.room_id).await {
&self.services.globals.server_user
} else {
pdu.sender()
&pdu.sender
};
self.respond_to_room(content, pdu.room_id(), response_sender)
self.respond_to_room(content, &pdu.room_id, response_sender)
.boxed()
.await
}
@ -361,10 +355,7 @@ impl Service {
Ok(())
}
pub async fn is_admin_command<E>(&self, event: &E, body: &str) -> bool
where
E: Event + Send + Sync,
{
pub async fn is_admin_command(&self, pdu: &PduEvent, body: &str) -> bool {
// Server-side command-escape with public echo
let is_escape = body.starts_with('\\');
let is_public_escape = is_escape && body.trim_start_matches('\\').starts_with("!admin");
@ -379,10 +370,8 @@ impl Service {
return false;
}
let user_is_local = self.services.globals.user_is_local(event.sender());
// only allow public escaped commands by local admins
if is_public_escape && !user_is_local {
if is_public_escape && !self.services.globals.user_is_local(&pdu.sender) {
return false;
}
@ -392,20 +381,20 @@ impl Service {
}
// Prevent unescaped !admin from being used outside of the admin room
if is_public_prefix && !self.is_admin_room(event.room_id()).await {
if is_public_prefix && !self.is_admin_room(&pdu.room_id).await {
return false;
}
// Only senders who are admin can proceed
if !self.user_is_admin(event.sender()).await {
if !self.user_is_admin(&pdu.sender).await {
return false;
}
// This will evaluate to false if the emergency password is set up so that
// the administrator can execute commands as the server user
let emergency_password_set = self.services.server.config.emergency_password.is_some();
let from_server = event.sender() == server_user && !emergency_password_set;
if from_server && self.is_admin_room(event.room_id()).await {
let from_server = pdu.sender == *server_user && !emergency_password_set;
if from_server && self.is_admin_room(&pdu.room_id).await {
return false;
}

View file

@ -242,14 +242,12 @@ async fn db_lt_12(services: &Services) -> Result<()> {
[".m.rules.contains_user_name", ".m.rule.contains_user_name"];
let rule = rules_list.content.get(content_rule_transformation[0]);
if let Some(rule) = rule {
let mut rule = rule.clone();
if rule.is_some() {
let mut rule = rule.unwrap().clone();
content_rule_transformation[1].clone_into(&mut rule.rule_id);
rules_list
.content
.shift_remove(content_rule_transformation[0]);
rules_list.content.insert(rule);
}
}

View file

@ -1,12 +1,12 @@
use std::{fmt::Debug, mem, sync::Arc};
use bytes::BytesMut;
use conduwuit_core::{
Err, Event, Result, debug_warn, err, trace,
use conduwuit::{
Err, PduEvent, Result, debug_warn, err, trace,
utils::{stream::TryIgnore, string_from_bytes},
warn,
};
use conduwuit_database::{Deserialized, Ignore, Interfix, Json, Map};
use database::{Deserialized, Ignore, Interfix, Json, Map};
use futures::{Stream, StreamExt};
use ipaddress::IPAddress;
use ruma::{
@ -272,33 +272,32 @@ impl Service {
}
}
#[tracing::instrument(skip(self, user, unread, pusher, ruleset, event))]
pub async fn send_push_notice<E>(
#[tracing::instrument(skip(self, user, unread, pusher, ruleset, pdu))]
pub async fn send_push_notice(
&self,
user: &UserId,
unread: UInt,
pusher: &Pusher,
ruleset: Ruleset,
event: &E,
) -> Result
where
E: Event + Send + Sync,
for<'a> &'a E: Event + Send,
{
pdu: &PduEvent,
) -> Result<()> {
let mut notify = None;
let mut tweaks = Vec::new();
let power_levels: RoomPowerLevelsEventContent = self
.services
.state_accessor
.room_state_get(event.room_id(), &StateEventType::RoomPowerLevels, "")
.room_state_get(&pdu.room_id, &StateEventType::RoomPowerLevels, "")
.await
.and_then(|event| event.get_content())
.and_then(|ev| {
serde_json::from_str(ev.content.get()).map_err(|e| {
err!(Database(error!("invalid m.room.power_levels event: {e:?}")))
})
})
.unwrap_or_default();
let serialized = event.to_format();
for action in self
.get_actions(user, &ruleset, &power_levels, &serialized, event.room_id())
.get_actions(user, &ruleset, &power_levels, &pdu.to_sync_room_event(), &pdu.room_id)
.await
{
let n = match action {
@ -320,7 +319,7 @@ impl Service {
}
if notify == Some(true) {
self.send_notice(unread, pusher, tweaks, event).await?;
self.send_notice(unread, pusher, tweaks, pdu).await?;
}
// Else the event triggered no actions
@ -370,16 +369,13 @@ impl Service {
}
#[tracing::instrument(skip(self, unread, pusher, tweaks, event))]
async fn send_notice<E>(
async fn send_notice(
&self,
unread: UInt,
pusher: &Pusher,
tweaks: Vec<Tweak>,
event: &E,
) -> Result
where
E: Event + Send + Sync,
{
event: &PduEvent,
) -> Result {
// TODO: email
match &pusher.kind {
| PusherKind::Http(http) => {
@ -425,8 +421,8 @@ impl Service {
let d = vec![device];
let mut notifi = Notification::new(d);
notifi.event_id = Some(event.event_id().to_owned());
notifi.room_id = Some(event.room_id().to_owned());
notifi.event_id = Some((*event.event_id).to_owned());
notifi.room_id = Some((*event.room_id).to_owned());
if http
.data
.get("org.matrix.msc4076.disable_badge_count")
@ -446,7 +442,7 @@ impl Service {
)
.await?;
} else {
if *event.kind() == TimelineEventType::RoomEncrypted
if event.kind == TimelineEventType::RoomEncrypted
|| tweaks
.iter()
.any(|t| matches!(t, Tweak::Highlight(true) | Tweak::Sound(_)))
@ -455,29 +451,29 @@ impl Service {
} else {
notifi.prio = NotificationPriority::Low;
}
notifi.sender = Some(event.sender().to_owned());
notifi.event_type = Some(event.kind().to_owned());
notifi.content = serde_json::value::to_raw_value(event.content()).ok();
notifi.sender = Some(event.sender.clone());
notifi.event_type = Some(event.kind.clone());
notifi.content = serde_json::value::to_raw_value(&event.content).ok();
if *event.kind() == TimelineEventType::RoomMember {
if event.kind == TimelineEventType::RoomMember {
notifi.user_is_target =
event.state_key() == Some(event.sender().as_str());
event.state_key.as_deref() == Some(event.sender.as_str());
}
notifi.sender_display_name =
self.services.users.displayname(event.sender()).await.ok();
self.services.users.displayname(&event.sender).await.ok();
notifi.room_name = self
.services
.state_accessor
.get_name(event.room_id())
.get_name(&event.room_id)
.await
.ok();
notifi.room_alias = self
.services
.state_accessor
.get_canonical_alias(event.room_id())
.get_canonical_alias(&event.room_id)
.await
.ok();

View file

@ -3,7 +3,7 @@ mod remote;
use std::sync::Arc;
use conduwuit::{
Err, Event, Result, Server, err,
Err, Result, Server, err,
utils::{ReadyExt, stream::TryIgnore},
};
use database::{Deserialized, Ignore, Interfix, Map};
@ -241,7 +241,7 @@ impl Service {
.room_state_get(&room_id, &StateEventType::RoomCreate, "")
.await
{
return Ok(event.sender() == user_id);
return Ok(event.sender == user_id);
}
Err!(Database("Room has no m.room.create event"))

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