mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b8e476626f |
+1
-1
@@ -1,9 +1,9 @@
|
|||||||
# Local build and dev artifacts
|
# Local build and dev artifacts
|
||||||
target/
|
target/
|
||||||
!target/debug/conduwuit
|
|
||||||
|
|
||||||
# Docker files
|
# Docker files
|
||||||
Dockerfile*
|
Dockerfile*
|
||||||
|
docker/
|
||||||
|
|
||||||
# IDE files
|
# IDE files
|
||||||
.vscode
|
.vscode
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ FROM ubuntu:latest
|
|||||||
EXPOSE 8008
|
EXPOSE 8008
|
||||||
EXPOSE 8448
|
EXPOSE 8448
|
||||||
RUN apt-get update && apt-get install -y ca-certificates liburing2 && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y ca-certificates liburing2 && rm -rf /var/lib/apt/lists/*
|
||||||
RUN mkdir -p /etc/continuwuity /var/lib/continuwuity /usr/local/bin/
|
RUN mkdir -p /etc/continuwuity /var/lib/continuwuity
|
||||||
COPY complement/complement-entrypoint.sh /usr/local/bin/complement-entrypoint.sh
|
COPY docker/complement-entrypoint.sh /usr/local/bin/complement-entrypoint.sh
|
||||||
COPY complement/complement.config.toml /etc/continuwuity/config.toml
|
COPY docker/complement.config.toml /etc/continuwuity/config.toml
|
||||||
COPY target/debug/conduwuit /usr/local/bin/conduwuit
|
COPY target/debug/conduwuit /usr/local/bin/conduwuit
|
||||||
RUN chmod +x /usr/local/bin/conduwuit /usr/local/bin/complement-entrypoint.sh
|
RUN chmod +x /usr/local/bin/conduwuit /usr/local/bin/complement-entrypoint.sh
|
||||||
#HEALTHCHECK --interval=30s --timeout=5s CMD curl --fail http://localhost:8008/_continuwuity/server_version || exit 1
|
#HEALTHCHECK --interval=30s --timeout=5s CMD curl --fail http://localhost:8008/_continuwuity/server_version || exit 1
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ curl https://your.server.name:8448/_matrix/federation/v1/version
|
|||||||
```
|
```
|
||||||
|
|
||||||
- To check if your server can communicate with other homeservers, use the
|
- To check if your server can communicate with other homeservers, use the
|
||||||
[Matrix Federation Tester](https://federationtester.mtrnord.blog/). If you can
|
[Matrix Federation Tester](https://federationtester.matrix.org/). If you can
|
||||||
register but cannot join federated rooms, check your configuration and verify
|
register but cannot join federated rooms, check your configuration and verify
|
||||||
that port 8448 is open and forwarded correctly.
|
that port 8448 is open and forwarded correctly.
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,16 @@ hero:
|
|||||||
src: /assets/logo.svg
|
src: /assets/logo.svg
|
||||||
alt: continuwuity logo
|
alt: continuwuity logo
|
||||||
|
|
||||||
|
beforeFeatures:
|
||||||
|
- title: Matrix for Discord users
|
||||||
|
details: New to Matrix? Learn how Matrix compares to Discord
|
||||||
|
link: https://joinmatrix.org/guide/matrix-vs-discord/
|
||||||
|
buttonText: Find Out the Difference
|
||||||
|
- title: How Matrix Works
|
||||||
|
details: Learn how Matrix works under the hood, and what that means
|
||||||
|
link: https://matrix.org/docs/matrix-concepts/elements-of-matrix/
|
||||||
|
buttonText: Read the Guide
|
||||||
|
|
||||||
features:
|
features:
|
||||||
- title: 🚀 High Performance
|
- title: 🚀 High Performance
|
||||||
details: Built with Rust for exceptional speed and efficiency. Designed to run smoothly even on modest hardware.
|
details: Built with Rust for exceptional speed and efficiency. Designed to run smoothly even on modest hardware.
|
||||||
|
|||||||
Generated
+2
-9
@@ -124,7 +124,6 @@
|
|||||||
"integrity": "sha512-m7L3oi4evTDODcY+Qk3cmY/p7GCaauSRe00D0AkXVohNvxFBt7F49uPwBSThS24I9d31zFuAED2jFqBeBlDqWw==",
|
"integrity": "sha512-m7L3oi4evTDODcY+Qk3cmY/p7GCaauSRe00D0AkXVohNvxFBt7F49uPwBSThS24I9d31zFuAED2jFqBeBlDqWw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rspack/core": "2.0.0-alpha.1",
|
"@rspack/core": "2.0.0-alpha.1",
|
||||||
"@swc/helpers": "^0.5.18",
|
"@swc/helpers": "^0.5.18",
|
||||||
@@ -377,7 +376,6 @@
|
|||||||
"integrity": "sha512-tU8rUVaPyC8o8k4ezgigRVQuZhBAC41KWdwZZ0BldN6o+QXSEIb722RnxCTpa9FGK2riqcwJgM+OqqcqXsFpmw==",
|
"integrity": "sha512-tU8rUVaPyC8o8k4ezgigRVQuZhBAC41KWdwZZ0BldN6o+QXSEIb722RnxCTpa9FGK2riqcwJgM+OqqcqXsFpmw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdx-js/mdx": "^3.1.1",
|
"@mdx-js/mdx": "^3.1.1",
|
||||||
"@mdx-js/react": "^3.1.1",
|
"@mdx-js/react": "^3.1.1",
|
||||||
@@ -564,7 +562,6 @@
|
|||||||
"integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==",
|
"integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.8.0"
|
"tslib": "^2.8.0"
|
||||||
}
|
}
|
||||||
@@ -688,7 +685,6 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -942,7 +938,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
@@ -2972,7 +2969,6 @@
|
|||||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -2983,7 +2979,6 @@
|
|||||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -3020,7 +3015,6 @@
|
|||||||
"integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
|
"integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -3488,7 +3482,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,29 +1,12 @@
|
|||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
use api::client::leave_room;
|
use api::client::leave_room;
|
||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Err, Result, RoomVersion, debug, info,
|
Err, Result, debug, info,
|
||||||
utils::{IterStream, ReadyExt},
|
utils::{IterStream, ReadyExt},
|
||||||
warn,
|
warn,
|
||||||
};
|
};
|
||||||
use futures::{FutureExt, StreamExt};
|
use futures::{FutureExt, StreamExt};
|
||||||
use ruma::{
|
use ruma::{OwnedRoomId, OwnedRoomOrAliasId, RoomAliasId, RoomId, RoomOrAliasId};
|
||||||
Int, OwnedRoomId, OwnedRoomOrAliasId, RoomAliasId, RoomId, RoomOrAliasId,
|
|
||||||
events::{
|
|
||||||
StateEventType,
|
|
||||||
room::{
|
|
||||||
create::RoomCreateEventContent,
|
|
||||||
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
|
|
||||||
join_rules::{JoinRule, RoomJoinRulesEventContent},
|
|
||||||
member::{MembershipState, RoomMemberEventContent},
|
|
||||||
power_levels::RoomPowerLevelsEventContent,
|
|
||||||
tombstone::RoomTombstoneEventContent,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
exports::serde::Deserialize,
|
|
||||||
};
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
use crate::{admin_command, admin_command_dispatch, get_room_info};
|
use crate::{admin_command, admin_command_dispatch, get_room_info};
|
||||||
|
|
||||||
@@ -60,59 +43,6 @@ pub enum RoomModerationCommand {
|
|||||||
/// information
|
/// information
|
||||||
no_details: bool,
|
no_details: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// - Take over a room by puppeting a local user into giving you a higher
|
|
||||||
/// power level
|
|
||||||
Takeover {
|
|
||||||
/// Whether to force joining the room if no local users are in the room
|
|
||||||
#[arg(long)]
|
|
||||||
force: bool,
|
|
||||||
/// The room in the format of `!roomid:example.com` or a room alias in
|
|
||||||
/// the format of `#roomalias:example.com`
|
|
||||||
room: OwnedRoomOrAliasId,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// - Shut down a room, as much is possible. **This is immediate and
|
|
||||||
/// irreversible**.
|
|
||||||
///
|
|
||||||
/// This command requires that you have a local user in the room with at
|
|
||||||
/// least a moderator power level. It will first attempt to raise power
|
|
||||||
/// levels so that nobody can use the room further, then remove the
|
|
||||||
/// canonical alias event, sets the history visibility to `joined`,
|
|
||||||
/// sets the join rules to `org.continuwuity.shutdown` (preventing anyone
|
|
||||||
/// from joining even with an invite), and then bans or kicks all users,
|
|
||||||
/// setting the MSC4293 "redact events" flag on those users if possible.
|
|
||||||
/// Finally, it will send a room tombstone event, which will effectively
|
|
||||||
/// make the room unusable on most clients even if the room state resets.
|
|
||||||
///
|
|
||||||
/// This effectively will make the room unusable, unjoinable, and removes
|
|
||||||
/// everyone from it. This is as close to a "shutdown" as you can get with
|
|
||||||
/// federation.
|
|
||||||
ShutdownRoom {
|
|
||||||
/// If no local users with a power level are joined to the room, setting
|
|
||||||
/// this flag will attempt one, and will join the user with the
|
|
||||||
/// highest power level to the room to perform the shutdown.
|
|
||||||
///
|
|
||||||
/// If this flag is not set, and no local users can perform the
|
|
||||||
/// shutdown, no further attempt will be made.
|
|
||||||
#[arg(long)]
|
|
||||||
force: bool,
|
|
||||||
/// Whether to use MSC4293 fields to indicate that all messages in the
|
|
||||||
/// room should be redacted. This will make it more difficult for
|
|
||||||
/// clients that implement MSC4293 (like Element) to render the room
|
|
||||||
/// in the event users manage to rejoin.
|
|
||||||
#[arg(long)]
|
|
||||||
redact: bool,
|
|
||||||
|
|
||||||
///
|
|
||||||
#[arg(long)]
|
|
||||||
yes_i_am_sure_i_want_to_irreversibly_shutdown_this_room_destroying_it_in_the_process:
|
|
||||||
bool,
|
|
||||||
|
|
||||||
/// The room in the format of `!roomid:example.com` or a room alias in
|
|
||||||
/// the format of `#roomalias:example.com`
|
|
||||||
room: OwnedRoomOrAliasId,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
@@ -538,553 +468,3 @@ async fn list_banned_rooms(&self, no_details: bool) -> Result {
|
|||||||
self.write_str(&format!("Rooms Banned ({num}):\n```\n{body}\n```",))
|
self.write_str(&format!("Rooms Banned ({num}):\n```\n{body}\n```",))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[admin_command]
|
|
||||||
async fn takeover(&self, force: bool, room: OwnedRoomOrAliasId) -> Result {
|
|
||||||
let room_id = if room.is_room_id() {
|
|
||||||
let room_id = match RoomId::parse(&room) {
|
|
||||||
| Ok(room_id) => room_id,
|
|
||||||
| Err(e) => {
|
|
||||||
return Err!(
|
|
||||||
"Failed to parse room ID {room}. Please note that this requires a full room \
|
|
||||||
ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \
|
|
||||||
(`#roomalias:example.com`): {e}"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
room_id.to_owned()
|
|
||||||
} else if room.is_room_alias_id() {
|
|
||||||
let room_alias = match RoomAliasId::parse(&room) {
|
|
||||||
| Ok(room_alias) => room_alias,
|
|
||||||
| Err(e) => {
|
|
||||||
return Err!(
|
|
||||||
"Failed to parse room ID {room}. Please note that this requires a full room \
|
|
||||||
ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \
|
|
||||||
(`#roomalias:example.com`): {e}"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
match self
|
|
||||||
.services
|
|
||||||
.rooms
|
|
||||||
.alias
|
|
||||||
.resolve_alias(room_alias, None)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
| Ok((room_id, servers)) => {
|
|
||||||
debug!(
|
|
||||||
?room_id,
|
|
||||||
?servers,
|
|
||||||
"Got federation response fetching room ID for room {room}"
|
|
||||||
);
|
|
||||||
room_id
|
|
||||||
},
|
|
||||||
| Err(e) => {
|
|
||||||
return Err!("Failed to resolve room alias {room} to a room ID: {e}");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err!(
|
|
||||||
"Room specified is not a room ID or room alias. Please note that this requires a \
|
|
||||||
full room ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \
|
|
||||||
(`#roomalias:example.com`)",
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
let room_version =
|
|
||||||
RoomVersion::new(&self.services.rooms.state.get_room_version(&room_id).await?)?;
|
|
||||||
let Ok(create_content) = self
|
|
||||||
.services
|
|
||||||
.rooms
|
|
||||||
.state_accessor
|
|
||||||
.room_state_get_content::<RoomCreateEventContent>(
|
|
||||||
&room_id,
|
|
||||||
&StateEventType::RoomCreate,
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
else {
|
|
||||||
return Err!("Failed to get room create event");
|
|
||||||
};
|
|
||||||
let mut power_levels = match self
|
|
||||||
.services
|
|
||||||
.rooms
|
|
||||||
.state_accessor
|
|
||||||
.room_state_get_content::<RoomPowerLevelsEventContent>(
|
|
||||||
&room_id,
|
|
||||||
&StateEventType::RoomPowerLevels,
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
| Ok(content) => content,
|
|
||||||
| Err(e) => {
|
|
||||||
return Err!("Failed to get power levels for room {room_id}: {e}");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
let local_creators = if room_version.explicitly_privilege_room_creators
|
|
||||||
&& create_content.additional_creators.is_some()
|
|
||||||
{
|
|
||||||
create_content
|
|
||||||
.additional_creators
|
|
||||||
.clone()
|
|
||||||
.unwrap()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|user_id| self.services.globals.user_is_local(user_id))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
};
|
|
||||||
let local_users = power_levels
|
|
||||||
.users
|
|
||||||
.iter()
|
|
||||||
.filter(|(user_id, _)| self.services.globals.user_is_local(user_id))
|
|
||||||
.map(|(user_id, level)| (user_id.clone(), *level))
|
|
||||||
.collect::<BTreeMap<_, _>>();
|
|
||||||
let min_pl = power_levels
|
|
||||||
.events
|
|
||||||
.get(&StateEventType::RoomPowerLevels.into())
|
|
||||||
.copied()
|
|
||||||
.unwrap_or(power_levels.state_default);
|
|
||||||
let mut ordered_users = local_users
|
|
||||||
.iter()
|
|
||||||
.chain(local_creators.iter().map(|user_id| (user_id, &Int::MAX)))
|
|
||||||
.map(|(user_id, level)| {
|
|
||||||
if local_creators.contains(user_id) {
|
|
||||||
(user_id, Int::MAX)
|
|
||||||
} else {
|
|
||||||
(user_id, *level)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(|(user_id, level)| *level >= min_pl || local_creators.contains(*user_id))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
ordered_users.sort_by_key(|(_, level)| level.saturating_mul(Int::from(-1)));
|
|
||||||
|
|
||||||
for (user_id, powerlevel) in ordered_users {
|
|
||||||
if !self
|
|
||||||
.services
|
|
||||||
.rooms
|
|
||||||
.state_cache
|
|
||||||
.is_joined(user_id.as_ref(), &room_id)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
if !force {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
info!("Joining {user_id} to room {room_id} to perform takeover");
|
|
||||||
let lock = self.services.rooms.state.mutex.lock(&room_id).await;
|
|
||||||
if let Err(e) = self
|
|
||||||
.services
|
|
||||||
.rooms
|
|
||||||
.timeline
|
|
||||||
.build_and_append_pdu(
|
|
||||||
conduwuit::pdu::Builder::state(
|
|
||||||
String::from(user_id.as_str()),
|
|
||||||
&RoomMemberEventContent::new(MembershipState::Join),
|
|
||||||
),
|
|
||||||
user_id,
|
|
||||||
Some(&room_id),
|
|
||||||
&lock,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
warn!("Failed to join {user_id} to room {room_id} to perform takeover: {e}");
|
|
||||||
drop(lock);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
drop(lock);
|
|
||||||
}
|
|
||||||
info!("Promoting you to power level {powerlevel} in room {room_id} via {user_id}");
|
|
||||||
let lock = self.services.rooms.state.mutex.lock(&room_id).await;
|
|
||||||
power_levels
|
|
||||||
.users
|
|
||||||
.insert(self.sender.expect("you should exist").to_owned(), powerlevel);
|
|
||||||
if let Err(e) = self
|
|
||||||
.services
|
|
||||||
.rooms
|
|
||||||
.timeline
|
|
||||||
.build_and_append_pdu(
|
|
||||||
conduwuit::pdu::Builder::state(String::new(), &power_levels),
|
|
||||||
user_id,
|
|
||||||
Some(&room_id),
|
|
||||||
&lock,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
warn!(
|
|
||||||
"Failed to promote you to power level {powerlevel} in room {room_id} via \
|
|
||||||
{user_id}: {e}"
|
|
||||||
);
|
|
||||||
drop(lock);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return self
|
|
||||||
.write_str(&format!(
|
|
||||||
"Successfully promoted you to power level {powerlevel} in room {room_id} via \
|
|
||||||
{user_id}"
|
|
||||||
))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.write_str("Failed to promote you, no local users with sufficient power level found.")
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[admin_command]
|
|
||||||
async fn shutdown_room(
|
|
||||||
&self,
|
|
||||||
force: bool,
|
|
||||||
redact: bool,
|
|
||||||
yes_i_am_sure_i_want_to_irreversibly_shutdown_this_room_destroying_it_in_the_process: bool,
|
|
||||||
room: OwnedRoomOrAliasId,
|
|
||||||
) -> Result {
|
|
||||||
let room_id = if room.is_room_id() {
|
|
||||||
let room_id = match RoomId::parse(&room) {
|
|
||||||
| Ok(room_id) => room_id,
|
|
||||||
| Err(e) => {
|
|
||||||
return Err!(
|
|
||||||
"Failed to parse room ID {room}. Please note that this requires a full room \
|
|
||||||
ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \
|
|
||||||
(`#roomalias:example.com`): {e}"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
room_id.to_owned()
|
|
||||||
} else if room.is_room_alias_id() {
|
|
||||||
let room_alias = match RoomAliasId::parse(&room) {
|
|
||||||
| Ok(room_alias) => room_alias,
|
|
||||||
| Err(e) => {
|
|
||||||
return Err!(
|
|
||||||
"Failed to parse room ID {room}. Please note that this requires a full room \
|
|
||||||
ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \
|
|
||||||
(`#roomalias:example.com`): {e}"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
match self
|
|
||||||
.services
|
|
||||||
.rooms
|
|
||||||
.alias
|
|
||||||
.resolve_alias(room_alias, None)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
| Ok((room_id, servers)) => {
|
|
||||||
debug!(
|
|
||||||
?room_id,
|
|
||||||
?servers,
|
|
||||||
"Got federation response fetching room ID for room {room}"
|
|
||||||
);
|
|
||||||
room_id
|
|
||||||
},
|
|
||||||
| Err(e) => {
|
|
||||||
return Err!("Failed to resolve room alias {room} to a room ID: {e}");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err!(
|
|
||||||
"Room specified is not a room ID or room alias. Please note that this requires a \
|
|
||||||
full room ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \
|
|
||||||
(`#roomalias:example.com`)",
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if !yes_i_am_sure_i_want_to_irreversibly_shutdown_this_room_destroying_it_in_the_process {
|
|
||||||
return Err!(
|
|
||||||
"This command is irreversible and will immediately shutdown the room, making it \
|
|
||||||
completely unusable if successful. If you are sure you want to do this, add the \
|
|
||||||
flag --yes-i-am-sure-i-want-to-irreversibly-shutdown-this-room-destroying-it-in-the-process \
|
|
||||||
to your command."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut power_levels: RoomPowerLevelsEventContent = match self
|
|
||||||
.services
|
|
||||||
.rooms
|
|
||||||
.state_accessor
|
|
||||||
.room_state_get_content(&room_id, &StateEventType::RoomPowerLevels, "")
|
|
||||||
.await
|
|
||||||
.map_err(|e| Err!("Failed to get power levels for room {room_id}: {e}"))
|
|
||||||
{
|
|
||||||
| Ok(content) => content,
|
|
||||||
| Err(e) => {
|
|
||||||
return e;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut joined_users = self
|
|
||||||
.services
|
|
||||||
.rooms
|
|
||||||
.state_cache
|
|
||||||
.room_members(&room_id)
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let room_version =
|
|
||||||
RoomVersion::new(&self.services.rooms.state.get_room_version(&room_id).await?)?;
|
|
||||||
let Ok(create_content) = self
|
|
||||||
.services
|
|
||||||
.rooms
|
|
||||||
.state_accessor
|
|
||||||
.room_state_get_content::<RoomCreateEventContent>(
|
|
||||||
&room_id,
|
|
||||||
&StateEventType::RoomCreate,
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
else {
|
|
||||||
return Err!("Failed to get room create event");
|
|
||||||
};
|
|
||||||
let local_creators = if room_version.explicitly_privilege_room_creators
|
|
||||||
&& create_content.additional_creators.is_some()
|
|
||||||
{
|
|
||||||
create_content
|
|
||||||
.additional_creators
|
|
||||||
.unwrap()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|user_id| self.services.globals.user_is_local(user_id))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
};
|
|
||||||
let local_users = power_levels
|
|
||||||
.users
|
|
||||||
.iter()
|
|
||||||
.filter(|(user_id, _)| self.services.globals.user_is_local(user_id))
|
|
||||||
.map(|(user_id, level)| (user_id.clone(), *level))
|
|
||||||
.collect::<BTreeMap<_, _>>();
|
|
||||||
|
|
||||||
let mut ordered_users = local_users
|
|
||||||
.iter()
|
|
||||||
.chain(local_creators.iter().map(|user_id| (user_id, &Int::MAX)))
|
|
||||||
.map(|(user_id, level)| {
|
|
||||||
if local_creators.contains(user_id) {
|
|
||||||
(user_id, Int::MAX)
|
|
||||||
} else {
|
|
||||||
(user_id, *level)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
ordered_users.sort_by_key(|(_, level)| level.saturating_mul(Int::from(-1)));
|
|
||||||
|
|
||||||
let mut changed_join_rules = false;
|
|
||||||
let mut changed_history_visibility = false;
|
|
||||||
let mut changed_power_levels = false;
|
|
||||||
let mut sent_tombstone = false;
|
|
||||||
let mut removed_ok: u32 = 0;
|
|
||||||
|
|
||||||
for (user_id, powerlevel) in ordered_users {
|
|
||||||
if !self
|
|
||||||
.services
|
|
||||||
.rooms
|
|
||||||
.state_cache
|
|
||||||
.is_joined(user_id.as_ref(), &room_id)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
if !force {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
info!("Joining {user_id} to room {room_id} to perform shutdown");
|
|
||||||
let lock = self.services.rooms.state.mutex.lock(&room_id).await;
|
|
||||||
if let Err(e) = self
|
|
||||||
.services
|
|
||||||
.rooms
|
|
||||||
.timeline
|
|
||||||
.build_and_append_pdu(
|
|
||||||
conduwuit::pdu::Builder::state(
|
|
||||||
String::from(user_id.as_str()),
|
|
||||||
&RoomMemberEventContent::new(MembershipState::Join),
|
|
||||||
),
|
|
||||||
user_id,
|
|
||||||
Some(&room_id),
|
|
||||||
&lock,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
warn!("Failed to join {user_id} to room {room_id} to perform shutdown: {e}");
|
|
||||||
drop(lock);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
drop(lock);
|
|
||||||
}
|
|
||||||
if !changed_power_levels {
|
|
||||||
info!("Raising minimum power levels to {powerlevel} via {user_id}");
|
|
||||||
power_levels.events_default = power_levels.events_default.max(powerlevel);
|
|
||||||
power_levels.state_default = power_levels.state_default.max(powerlevel);
|
|
||||||
if power_levels.users_default < powerlevel {
|
|
||||||
power_levels.users_default = Int::MIN;
|
|
||||||
}
|
|
||||||
power_levels.kick = power_levels.kick.max(powerlevel);
|
|
||||||
power_levels.ban = power_levels.ban.max(powerlevel);
|
|
||||||
for (event_type, event_pl) in power_levels.events.clone() {
|
|
||||||
power_levels
|
|
||||||
.events
|
|
||||||
.insert(event_type, event_pl.max(powerlevel));
|
|
||||||
}
|
|
||||||
for (user, user_pl) in power_levels.users.clone() {
|
|
||||||
if user_pl < powerlevel {
|
|
||||||
power_levels.users.remove(&user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let lock = self.services.rooms.state.mutex.lock(&room_id).await;
|
|
||||||
if let Err(e) = self
|
|
||||||
.services
|
|
||||||
.rooms
|
|
||||||
.timeline
|
|
||||||
.build_and_append_pdu(
|
|
||||||
conduwuit::pdu::Builder::state(String::new(), &power_levels.clone()),
|
|
||||||
user_id,
|
|
||||||
Some(&room_id),
|
|
||||||
&lock,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
warn!(
|
|
||||||
"Failed to raise power levels to {powerlevel} in room {room_id} via \
|
|
||||||
{user_id}: {e}"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
changed_power_levels = true;
|
|
||||||
}
|
|
||||||
drop(lock);
|
|
||||||
}
|
|
||||||
if !changed_join_rules {
|
|
||||||
info!("Setting room to private via {user_id}");
|
|
||||||
// NOTE: Setting the room to `private` soft-bricks it, as new joins with this
|
|
||||||
// join rule can actually be authorised.
|
|
||||||
let lock = self.services.rooms.state.mutex.lock(&room_id).await;
|
|
||||||
if let Err(e) = self
|
|
||||||
.services
|
|
||||||
.rooms
|
|
||||||
.timeline
|
|
||||||
.build_and_append_pdu(
|
|
||||||
conduwuit::pdu::Builder::state(
|
|
||||||
String::new(),
|
|
||||||
&RoomJoinRulesEventContent::new(
|
|
||||||
JoinRule::deserialize(json!("\"org.continuwuity.shutdown\""))
|
|
||||||
.expect("valid fixed json"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
user_id,
|
|
||||||
Some(&room_id),
|
|
||||||
&lock,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
warn!("Failed to set room to private in room {room_id} via {user_id}: {e}");
|
|
||||||
} else {
|
|
||||||
changed_join_rules = true;
|
|
||||||
}
|
|
||||||
drop(lock);
|
|
||||||
}
|
|
||||||
if !changed_history_visibility {
|
|
||||||
info!("Setting history visibility to joined via {user_id}");
|
|
||||||
let lock = self.services.rooms.state.mutex.lock(&room_id).await;
|
|
||||||
if let Err(e) = self
|
|
||||||
.services
|
|
||||||
.rooms
|
|
||||||
.timeline
|
|
||||||
.build_and_append_pdu(
|
|
||||||
conduwuit::pdu::Builder::state(
|
|
||||||
String::new(),
|
|
||||||
&RoomHistoryVisibilityEventContent::new(HistoryVisibility::Joined),
|
|
||||||
),
|
|
||||||
user_id,
|
|
||||||
Some(&room_id),
|
|
||||||
&lock,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
warn!(
|
|
||||||
"Failed to set history visibility to joined in room {room_id} via \
|
|
||||||
{user_id}: {e}"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
changed_history_visibility = true;
|
|
||||||
}
|
|
||||||
drop(lock);
|
|
||||||
}
|
|
||||||
info!("Removing {} users in {room_id} via {user_id}", joined_users.len());
|
|
||||||
let lock = self.services.rooms.state.mutex.lock(&room_id).await;
|
|
||||||
for remove_user in &joined_users.clone() {
|
|
||||||
if remove_user == user_id || self.services.admin.user_is_admin(user_id).await {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let user_pl = power_levels
|
|
||||||
.users
|
|
||||||
.get(remove_user)
|
|
||||||
.copied()
|
|
||||||
.unwrap_or(power_levels.users_default);
|
|
||||||
let new_membership = if power_levels.ban <= powerlevel && user_pl < powerlevel {
|
|
||||||
MembershipState::Ban
|
|
||||||
} else {
|
|
||||||
MembershipState::Leave
|
|
||||||
};
|
|
||||||
debug!("Removing {remove_user} via {user_id}");
|
|
||||||
if let Err(e) = self
|
|
||||||
.services
|
|
||||||
.rooms
|
|
||||||
.timeline
|
|
||||||
.build_and_append_pdu(
|
|
||||||
conduwuit::pdu::Builder::state(
|
|
||||||
String::from(remove_user.as_str()),
|
|
||||||
&RoomMemberEventContent {
|
|
||||||
membership: new_membership.clone(),
|
|
||||||
redact_events: if redact { Some(true) } else { None },
|
|
||||||
..RoomMemberEventContent::new(new_membership.clone())
|
|
||||||
},
|
|
||||||
),
|
|
||||||
user_id,
|
|
||||||
Some(&room_id),
|
|
||||||
&lock,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
warn!("Failed to remove {remove_user} via {user_id}: {e}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
removed_ok = removed_ok.saturating_add(1);
|
|
||||||
if self.services.globals.user_is_local(remove_user) {
|
|
||||||
self.services
|
|
||||||
.rooms
|
|
||||||
.state_cache
|
|
||||||
.forget(&room_id, remove_user);
|
|
||||||
}
|
|
||||||
joined_users.retain(|u| u != remove_user);
|
|
||||||
}
|
|
||||||
if !sent_tombstone {
|
|
||||||
info!("Sending tombstone event for {room_id} via {user_id}");
|
|
||||||
if let Err(e) = self
|
|
||||||
.services
|
|
||||||
.rooms
|
|
||||||
.timeline
|
|
||||||
.build_and_append_pdu(
|
|
||||||
conduwuit::pdu::Builder::state(
|
|
||||||
String::new(),
|
|
||||||
&RoomTombstoneEventContent::new(
|
|
||||||
format!("Room {room_id} has been shut down"),
|
|
||||||
room_id.clone(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
user_id,
|
|
||||||
Some(&room_id),
|
|
||||||
&lock,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
warn!("Failed to send tombstone event for {room_id} via {user_id}: {e}");
|
|
||||||
} else {
|
|
||||||
sent_tombstone = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.write_str(&format!(
|
|
||||||
"Room shutdown complete, removed {removed_ok} users, changed join rules: \
|
|
||||||
{changed_join_rules}.\nConsider banning the room with `ban-room`.",
|
|
||||||
))
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
use std::collections::{HashSet, VecDeque};
|
|
||||||
|
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use conduwuit::{Err, Event, Result, debug, info, trace, utils::to_canonical_object, warn};
|
use conduwuit::{Err, Result, debug, debug_error, info, utils::to_canonical_object};
|
||||||
use ruma::{OwnedEventId, api::federation::event::get_missing_events};
|
use ruma::api::federation::event::get_missing_events;
|
||||||
use serde_json::{json, value::RawValue};
|
|
||||||
|
|
||||||
use super::AccessCheck;
|
use super::AccessCheck;
|
||||||
use crate::Ruma;
|
use crate::Ruma;
|
||||||
@@ -48,76 +45,59 @@ pub(crate) async fn get_missing_events_route(
|
|||||||
.unwrap_or(LIMIT_DEFAULT)
|
.unwrap_or(LIMIT_DEFAULT)
|
||||||
.min(LIMIT_MAX);
|
.min(LIMIT_MAX);
|
||||||
|
|
||||||
let room_version = services.rooms.state.get_room_version(&body.room_id).await?;
|
let mut queued_events = body.latest_events.clone();
|
||||||
|
// the vec will never have more entries the limit
|
||||||
|
let mut events = Vec::with_capacity(limit);
|
||||||
|
|
||||||
let mut queue: VecDeque<OwnedEventId> = VecDeque::from(body.latest_events.clone());
|
let mut i: usize = 0;
|
||||||
let mut results: Vec<Box<RawValue>> = Vec::with_capacity(limit);
|
while i < queued_events.len() && events.len() < limit {
|
||||||
let mut seen: HashSet<OwnedEventId> = HashSet::from_iter(body.earliest_events.clone());
|
let Ok(pdu) = services.rooms.timeline.get_pdu(&queued_events[i]).await else {
|
||||||
|
debug!(
|
||||||
while let Some(next_event_id) = queue.pop_front() {
|
body.origin = body.origin.as_ref().map(tracing::field::display),
|
||||||
if seen.contains(&next_event_id) {
|
"Event {} does not exist locally, skipping", &queued_events[i]
|
||||||
trace!(%next_event_id, "already seen event, skipping");
|
);
|
||||||
|
i = i.saturating_add(1);
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
if results.len() >= limit {
|
|
||||||
debug!(%next_event_id, "reached limit of events to return, breaking");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut pdu = match services.rooms.timeline.get_pdu(&next_event_id).await {
|
|
||||||
| Ok(pdu) => pdu,
|
|
||||||
| Err(e) => {
|
|
||||||
warn!("could not find event {next_event_id} while walking missing events: {e}");
|
|
||||||
continue;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
if pdu.room_id_or_hash() != body.room_id {
|
|
||||||
return Err!(Request(Unknown(
|
if body.earliest_events.contains(&queued_events[i]) {
|
||||||
"Event {next_event_id} is not in room {}",
|
i = i.saturating_add(1);
|
||||||
body.room_id
|
continue;
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !services
|
if !services
|
||||||
.rooms
|
.rooms
|
||||||
.state_accessor
|
.state_accessor
|
||||||
.server_can_see_event(body.origin(), &body.room_id, pdu.event_id())
|
.server_can_see_event(body.origin(), &body.room_id, &queued_events[i])
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
debug!(%next_event_id, origin = %body.origin(), "redacting event origin cannot see");
|
debug!(
|
||||||
pdu.redact(&room_version, json!({}))?;
|
body.origin = body.origin.as_ref().map(tracing::field::display),
|
||||||
|
"Server cannot see {:?} in {:?}, skipping", pdu.event_id, pdu.room_id
|
||||||
|
);
|
||||||
|
i = i.saturating_add(1);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
trace!(
|
i = i.saturating_add(1);
|
||||||
%next_event_id,
|
let Ok(event) = to_canonical_object(&pdu) else {
|
||||||
prev_events = ?pdu.prev_events().collect::<Vec<_>>(),
|
debug_error!(
|
||||||
"adding event to results and queueing prev events"
|
body.origin = body.origin.as_ref().map(tracing::field::display),
|
||||||
);
|
"Failed to convert PDU in database to canonical JSON: {pdu:?}"
|
||||||
queue.extend(pdu.prev_events.clone());
|
);
|
||||||
seen.insert(next_event_id.clone());
|
continue;
|
||||||
if body.latest_events.contains(&next_event_id) {
|
};
|
||||||
continue; // Don't include latest_events in results,
|
|
||||||
// but do include their prev_events in the queue
|
let prev_events = pdu.prev_events.iter().map(ToOwned::to_owned);
|
||||||
}
|
|
||||||
results.push(
|
let event = services
|
||||||
services
|
.sending
|
||||||
.sending
|
.convert_to_outgoing_federation_event(event)
|
||||||
.convert_to_outgoing_federation_event(to_canonical_object(pdu)?)
|
.await;
|
||||||
.await,
|
|
||||||
);
|
queued_events.extend(prev_events);
|
||||||
trace!(
|
events.push(event);
|
||||||
%next_event_id,
|
|
||||||
queue_len = queue.len(),
|
|
||||||
seen_len = seen.len(),
|
|
||||||
results_len = results.len(),
|
|
||||||
"event added to results"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !queue.is_empty() {
|
Ok(get_missing_events::v1::Response { events })
|
||||||
debug!("limit reached before queue was empty");
|
|
||||||
}
|
|
||||||
results.reverse(); // return oldest first
|
|
||||||
Ok(get_missing_events::v1::Response { events: results })
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<li>Read the <a href="https://continuwuity.org/introduction">documentation</a></li>
|
<li>Read the <a href="https://continuwuity.org/introduction">documentation</a></li>
|
||||||
<li>Join the <a href="https://matrix.to/#/#continuwuity:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org">Continuwuity Matrix room</a> or <a href="https://matrix.to/#/#space:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org">space</a></li>
|
<li>Join the <a href="https://matrix.to/#/#continuwuity:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org">Continuwuity Matrix room</a> or <a href="https://matrix.to/#/#space:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org">space</a></li>
|
||||||
<li>Log in with a <a href="https://matrix.org/ecosystem/clients/">client</a></li>
|
<li>Log in with a <a href="https://matrix.org/ecosystem/clients/">client</a></li>
|
||||||
<li>Ensure <a href="https://federationtester.mtrnord.blog/?serverName={{ server_name }}">federation</a> works</li>
|
<li>Ensure <a href="https://federationtester.matrix.org/#{{ server_name }}">federation</a> works</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -105,3 +105,68 @@ body:not(.notTopArrived) header.rp-nav {
|
|||||||
.rspress-logo {
|
.rspress-logo {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* pre-hero */
|
||||||
|
.custom-section {
|
||||||
|
padding: 4rem 1.5rem;
|
||||||
|
background: var(--rp-c-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-cards {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-card {
|
||||||
|
padding: 2rem;
|
||||||
|
border: 1px solid var(--rp-c-divider-light);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--rp-c-bg-soft);
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--rp-c-text-1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 280px;
|
||||||
|
max-width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-card:hover {
|
||||||
|
border-color: var(--rp-c-brand);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-card h3 {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--rp-c-text-0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-card p {
|
||||||
|
margin: 0 0 1.5rem 0;
|
||||||
|
color: var(--rp-c-text-2);
|
||||||
|
line-height: 1.6;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-card-button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
background: var(--rp-c-brand);
|
||||||
|
color: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-card:hover .custom-card-button {
|
||||||
|
background: var(--rp-c-brand-light);
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,23 @@ function HomeLayout(props: HomeLayoutProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BasicHomeLayout
|
<BasicHomeLayout
|
||||||
|
beforeFeatures={
|
||||||
|
frontmatter.beforeFeatures ? (
|
||||||
|
<section className="custom-section">
|
||||||
|
<div className="rp-container">
|
||||||
|
<div className="custom-cards">
|
||||||
|
{frontmatter.beforeFeatures.map((item: any, index: number) => (
|
||||||
|
<a key={index} href={item.link} className="custom-card" target="_blank" rel="noopener noreferrer">
|
||||||
|
<h3>{item.title}</h3>
|
||||||
|
<p>{item.details}</p>
|
||||||
|
<span className="custom-card-button">{item.buttonText || 'Learn More'} →</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
) : <></>
|
||||||
|
}
|
||||||
afterFeatures={
|
afterFeatures={
|
||||||
(frontmatter.doc) ?
|
(frontmatter.doc) ?
|
||||||
<main className="rp-doc-layout__doc-container">
|
<main className="rp-doc-layout__doc-container">
|
||||||
|
|||||||
Reference in New Issue
Block a user