Initial commit
All tools successfully used for what I needed them for
This commit is contained in:
commit
4c8e4bf726
3
.envrc
Normal file
3
.envrc
Normal file
@ -0,0 +1,3 @@
|
||||
source_url "https://raw.githubusercontent.com/cachix/devenv/95f329d49a8a5289d31e0982652f7058a189bfca/direnvrc" "sha256-d+8cBpDfDBj41inrADaJt+bDWhOktwslgoP5YiGJ1v0="
|
||||
|
||||
use devenv
|
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
# Devenv
|
||||
.devenv*
|
||||
devenv.local.nix
|
||||
|
||||
# direnv
|
||||
.direnv
|
||||
|
||||
# pre-commit
|
||||
.pre-commit-config.yaml
|
||||
|
||||
|
||||
# Added by cargo
|
||||
|
||||
/target
|
3217
Cargo.lock
generated
Normal file
3217
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "matricks"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.21", features = ["derive"] }
|
||||
errify = { version = "0.3.0", features = ["eyre"] }
|
||||
eyre = "0.6.12"
|
||||
matrix-sdk = { version = "0.8.0", default-features = false, features = ["eyre", "rustls-tls"] }
|
||||
regex = "1.11.1"
|
||||
tokio = { version = "1.41.1", features = ["full"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
104
README.md
Normal file
104
README.md
Normal file
@ -0,0 +1,104 @@
|
||||
# Matricks: tricks (tools) for Matrix
|
||||
|
||||
a small collection of tools for Matrix...
|
||||
|
||||
## Usage
|
||||
|
||||
First you need to already have an access token and URL for your homeserver.
|
||||
Then expose them as env vars, e.g. with the following shell snippet.
|
||||
(You could also put this in a file and source it with `. matricks_auth.sh` if desired!)
|
||||
|
||||
```
|
||||
read MATRICKS_HOMESERVER # enter your homeserver, e.g. https://matrix.librepush.net
|
||||
read MATRICKS_TOKEN # enter your access token
|
||||
export MATRICKS_HOMESERVER MATRICKS_TOKEN
|
||||
```
|
||||
|
||||
### Space tools
|
||||
|
||||
#### `space tree`: List a space hierarchy from a root space
|
||||
|
||||
You just need to provide a room ID or room alias for the root space.
|
||||
|
||||
```
|
||||
matricks space tree #myspace:example.org
|
||||
```
|
||||
|
||||
The output of this command is a restricted Markdown subset, suitable both for human reading and for use in later commands as a Room Batch File.
|
||||
|
||||
|
||||
### Room tools
|
||||
|
||||
All these commands take a source of room IDs, either a single room ID as `--one !roomid:example.com`, or a list of room IDs from a Room Batch File as `--file path/to/file.md`.
|
||||
This room source argument should be *before* the next subcommand.
|
||||
|
||||
#### `rooms aliases`: List aliases from rooms
|
||||
|
||||
```
|
||||
matricks rooms --one '!roomid:example.org` aliases --if-not-match '#nomatch.*' --if-match '#[0-9a-f]*:.*'
|
||||
```
|
||||
|
||||
The `--if-match` and `--if-not-match` predicates arguments can be specified multiple times and let you specify
|
||||
regular expressions to match aliases to include or omit, respectively.
|
||||
When multiple predicates are specified, ALL predicates must be satisfied (i.e. it's a big logical AND across all of them).
|
||||
|
||||
|
||||
#### `rooms make-read-only`: Make a room read-only
|
||||
|
||||
```
|
||||
matricks rooms --one '!roomid:example.org` make-read-only --power-level 50
|
||||
```
|
||||
|
||||
This command makes the specified room(s) effectively read-only to unprivileged users by
|
||||
updating the `m.room.power_levels` state event to:
|
||||
|
||||
- set `event_default` to at least the threshold PL
|
||||
- set all event types' required power level to at least the threshold PL
|
||||
|
||||
The `--power-level` argument specifies the threshold power level (PL); that is,
|
||||
which power level or higher is allowed to send events from now on.
|
||||
|
||||
|
||||
### Room Alias Tools
|
||||
|
||||
All these commands take a source of room aliases, either a single room alias as `--one #room:example.com`, or a list of room aliases from an Alias Batch File as `--file path/to/file.md`.
|
||||
This room source argument should be *before* the next subcommand.
|
||||
|
||||
#### `aliases remove`: Remove room aliases
|
||||
|
||||
```
|
||||
matricks aliases --one '#room:example.org' remove
|
||||
```
|
||||
|
||||
This command removes the specified room alias(es).
|
||||
|
||||
|
||||
## File Formats
|
||||
|
||||
### Room Batch File
|
||||
|
||||
A Room Batch File is a Markdown file that conveniently happens to contain Room IDs embedded in backticks (i.e. code segments).
|
||||
|
||||
Room IDs must not split across lines.
|
||||
The position and formatting of Room IDs is otherwise unimportant.
|
||||
|
||||
Example
|
||||
|
||||
```markdown
|
||||
Text text text `!room:example.org`
|
||||
|
||||
- Text text text `!room2:example.org`
|
||||
- Text text text `!room3:example.org`
|
||||
```
|
||||
|
||||
### Alias Batch File
|
||||
|
||||
This is the same as a Room Batch File, but instead of room IDs, it contains room aliases.
|
||||
|
||||
Example
|
||||
|
||||
```markdown
|
||||
- Some room `#room:example.org`
|
||||
- Some room 2 `#room2:example.org`
|
||||
```
|
||||
|
116
devenv.lock
Normal file
116
devenv.lock
Normal file
@ -0,0 +1,116 @@
|
||||
{
|
||||
"nodes": {
|
||||
"devenv": {
|
||||
"locked": {
|
||||
"dir": "src/modules",
|
||||
"lastModified": 1732585607,
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"rev": "a520f05c40ebecaf5e17064b27e28ba8e70c49fb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"dir": "src/modules",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"pre-commit-hooks",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709087332,
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1732238832,
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "8edf06bea5bcbee082df1b7369ff973b91618b8d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-stable": {
|
||||
"locked": {
|
||||
"lastModified": 1731797254,
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e8c38b73aeb218e27163376a2d617e61a2ad9b59",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-24.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"pre-commit-hooks": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"nixpkgs-stable": "nixpkgs-stable"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1732021966,
|
||||
"owner": "cachix",
|
||||
"repo": "pre-commit-hooks.nix",
|
||||
"rev": "3308484d1a443fc5bc92012435d79e80458fe43c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "pre-commit-hooks.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"devenv": "devenv",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"pre-commit-hooks": "pre-commit-hooks"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
10
devenv.nix
Normal file
10
devenv.nix
Normal file
@ -0,0 +1,10 @@
|
||||
{ pkgs, lib, config, inputs, ... }:
|
||||
|
||||
{
|
||||
cachix.enable = false;
|
||||
|
||||
# https://devenv.sh/languages/
|
||||
languages.rust.enable = true;
|
||||
|
||||
# See full reference at https://devenv.sh/reference/options/
|
||||
}
|
15
devenv.yaml
Normal file
15
devenv.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json
|
||||
inputs:
|
||||
nixpkgs:
|
||||
url: github:NixOS/nixpkgs/nixpkgs-unstable
|
||||
|
||||
# If you're using non-OSS software, you can set allowUnfree to true.
|
||||
# allowUnfree: true
|
||||
|
||||
# If you're willing to use a package that's vulnerable
|
||||
# permittedInsecurePackages:
|
||||
# - "openssl-1.1.1w"
|
||||
|
||||
# If you have more than one devenv you can merge them
|
||||
#imports:
|
||||
# - ./backend
|
17
src/aliases_remove.rs
Normal file
17
src/aliases_remove.rs
Normal file
@ -0,0 +1,17 @@
|
||||
use eyre::Context;
|
||||
use matrix_sdk::{
|
||||
ruma::{api::client::alias::delete_alias, OwnedRoomAliasId},
|
||||
Client,
|
||||
};
|
||||
|
||||
pub async fn run(client: &Client, aliases: &[OwnedRoomAliasId]) -> eyre::Result<()> {
|
||||
for alias in aliases {
|
||||
let request = delete_alias::v3::Request::new(alias.clone());
|
||||
client
|
||||
.send(request, None)
|
||||
.await
|
||||
.with_context(|| format!("failed to request deletion of {alias}"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
171
src/bin/matricks.rs
Normal file
171
src/bin/matricks.rs
Normal file
@ -0,0 +1,171 @@
|
||||
use std::{path::PathBuf, process::ExitCode};
|
||||
|
||||
use clap::Parser;
|
||||
use eyre::{bail, Context};
|
||||
use matricks::{
|
||||
aliases_remove, parse_alias_batch_file, parse_room_batch_file,
|
||||
rooms_aliases::{self, AliasesArgs},
|
||||
rooms_make_read_only, space_tree,
|
||||
};
|
||||
use matrix_sdk::ruma::{OwnedRoomAliasId, OwnedRoomId, OwnedRoomOrAliasId};
|
||||
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
#[derive(Clone, Parser)]
|
||||
pub struct Options {
|
||||
#[command(subcommand)]
|
||||
command: TopLevelCommand,
|
||||
}
|
||||
|
||||
#[derive(Clone, Parser)]
|
||||
pub enum TopLevelCommand {
|
||||
#[command(subcommand)]
|
||||
Space(SpaceCommand),
|
||||
|
||||
Rooms {
|
||||
/// Operate on one room
|
||||
#[arg(long = "one")]
|
||||
one: Option<OwnedRoomId>,
|
||||
/// Operate on all room IDs found in the given Room Batch File.
|
||||
/// See the README for documentation on what a Room Batch File looks like.
|
||||
#[arg(long = "file")]
|
||||
file: Option<PathBuf>,
|
||||
#[command(subcommand)]
|
||||
subcommand: RoomsCommand,
|
||||
},
|
||||
|
||||
Aliases {
|
||||
/// Operate on one alias
|
||||
#[arg(long = "one")]
|
||||
one: Option<OwnedRoomAliasId>,
|
||||
/// Operate on all room aliases found in the given Alias Batch File.
|
||||
/// See the README for documentation on what a Alias Batch File looks like.
|
||||
#[arg(long = "file")]
|
||||
file: Option<PathBuf>,
|
||||
#[command(subcommand)]
|
||||
subcommand: AliasesCommand,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Parser)]
|
||||
pub enum SpaceCommand {
|
||||
Tree { root_space: OwnedRoomOrAliasId },
|
||||
}
|
||||
|
||||
#[derive(Clone, Parser)]
|
||||
pub enum RoomsCommand {
|
||||
/// Make the room(s) read-only except to sufficiently privileged users.
|
||||
///
|
||||
/// This is suitable for archiving rooms that are no longer used.
|
||||
MakeReadOnly {
|
||||
/// The power level to set as the required threshold for sending room messages.
|
||||
/// 100 (Admin) or 50 (Moderator) is usually a sensible choice.
|
||||
#[arg(long = "power-level")]
|
||||
power_level: u32,
|
||||
},
|
||||
|
||||
/// Lists aliases assigned to the rooms, with some filtering options.
|
||||
///
|
||||
/// The output format is in Alias Batch File.
|
||||
Aliases(AliasesArgs),
|
||||
}
|
||||
|
||||
#[derive(Clone, Parser)]
|
||||
pub enum AliasesCommand {
|
||||
/// Remove the aliases.
|
||||
Remove,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> eyre::Result<ExitCode> {
|
||||
tracing_subscriber::registry()
|
||||
.with(fmt::layer().with_writer(std::io::stderr))
|
||||
.with(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||
let matricks_level = std::env::var("MATRICKS_LOG").unwrap_or("info".to_owned());
|
||||
format!("matricks={matricks_level},matrix_sdk_base=error,info").into()
|
||||
}),
|
||||
)
|
||||
.init();
|
||||
|
||||
let options = Options::parse();
|
||||
|
||||
match &options.command {
|
||||
TopLevelCommand::Space(space_command) => match space_command {
|
||||
SpaceCommand::Tree { root_space } => {
|
||||
let client = matricks::get_client_from_env()
|
||||
.await
|
||||
.context("could not get client from env")?;
|
||||
|
||||
space_tree::run(&client, root_space).await?;
|
||||
}
|
||||
},
|
||||
TopLevelCommand::Rooms {
|
||||
one,
|
||||
file,
|
||||
subcommand,
|
||||
} => {
|
||||
let room_sources_count = one.is_some() as u32 + file.is_some() as u32;
|
||||
if room_sources_count != 1 {
|
||||
bail!("Exactly one of --one and --file must be provided.");
|
||||
}
|
||||
|
||||
let room_ids = if let Some(one) = one {
|
||||
vec![one.clone()]
|
||||
} else if let Some(file) = file {
|
||||
parse_room_batch_file(file)
|
||||
.await
|
||||
.context("could not parse --file Room Batch File")?
|
||||
} else {
|
||||
// We already checked if one of the sources was provided, so we can't hit this
|
||||
unreachable!();
|
||||
};
|
||||
|
||||
match subcommand {
|
||||
RoomsCommand::MakeReadOnly { power_level } => {
|
||||
let client = matricks::get_client_from_env()
|
||||
.await
|
||||
.context("could not get client from env")?;
|
||||
rooms_make_read_only::run(&client, &room_ids, *power_level).await?
|
||||
}
|
||||
RoomsCommand::Aliases(args) => {
|
||||
let client = matricks::get_client_from_env()
|
||||
.await
|
||||
.context("could not get client from env")?;
|
||||
rooms_aliases::run(&client, &room_ids, args).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
TopLevelCommand::Aliases {
|
||||
one,
|
||||
file,
|
||||
subcommand,
|
||||
} => {
|
||||
let alias_sources_count = one.is_some() as u32 + file.is_some() as u32;
|
||||
if alias_sources_count != 1 {
|
||||
bail!("Exactly one of --one and --file must be provided.");
|
||||
}
|
||||
|
||||
let room_aliases = if let Some(one) = one {
|
||||
vec![one.clone()]
|
||||
} else if let Some(file) = file {
|
||||
parse_alias_batch_file(file)
|
||||
.await
|
||||
.context("could not parse --file Alias Batch File")?
|
||||
} else {
|
||||
// We already checked if one of the sources was provided, so we can't hit this
|
||||
unreachable!();
|
||||
};
|
||||
|
||||
match subcommand {
|
||||
AliasesCommand::Remove => {
|
||||
let client = matricks::get_client_from_env()
|
||||
.await
|
||||
.context("could not get client from env")?;
|
||||
aliases_remove::run(&client, &room_aliases).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
151
src/lib.rs
Normal file
151
src/lib.rs
Normal file
@ -0,0 +1,151 @@
|
||||
use std::path::Path;
|
||||
|
||||
use eyre::Context;
|
||||
use matrix_sdk::{
|
||||
config::SyncSettings,
|
||||
matrix_auth::{MatrixAuth, MatrixSession, MatrixSessionTokens},
|
||||
ruma::{
|
||||
api::client::account::whoami, device_id, user_id, OwnedRoomAliasId, OwnedRoomId,
|
||||
RoomAliasId, RoomId,
|
||||
},
|
||||
AuthSession, Client, SessionMeta,
|
||||
};
|
||||
use regex::{Regex, RegexBuilder};
|
||||
use tracing::debug;
|
||||
|
||||
pub mod space_tree;
|
||||
|
||||
pub mod rooms_aliases;
|
||||
pub mod rooms_make_read_only;
|
||||
|
||||
pub mod aliases_remove;
|
||||
|
||||
pub const USER_AGENT: &str = concat!("matricks/", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
pub async fn get_client_from_env() -> eyre::Result<Client> {
|
||||
let access_token = std::env::var("MATRICKS_TOKEN")
|
||||
.context("you should supply an access token as env var MATRICKS_TOKEN")?;
|
||||
let homeserver_uri = std::env::var("MATRICKS_HOMESERVER")
|
||||
.context("you should supply a homeserver URI as MATRICKS_HOMESERVER env var")?;
|
||||
|
||||
let client = Client::builder()
|
||||
.homeserver_url(&homeserver_uri)
|
||||
.user_agent(USER_AGENT)
|
||||
.build()
|
||||
.await
|
||||
.context("could not build client")?;
|
||||
|
||||
client
|
||||
.restore_session(MatrixSession {
|
||||
meta: SessionMeta {
|
||||
user_id: user_id!("@nobody:example.org").to_owned(),
|
||||
device_id: device_id!("???unknown???").to_owned(),
|
||||
},
|
||||
tokens: MatrixSessionTokens {
|
||||
access_token: access_token.clone(),
|
||||
refresh_token: None,
|
||||
},
|
||||
})
|
||||
.await
|
||||
.context("could not restore pre-session")?;
|
||||
|
||||
let whoami = client
|
||||
.send(whoami::v3::Request::new(), None)
|
||||
.await
|
||||
.context("could not issue /whoami call")?;
|
||||
|
||||
debug!("logged in as {}/{:?}", whoami.user_id, whoami.device_id);
|
||||
|
||||
let client = Client::builder()
|
||||
.homeserver_url(&homeserver_uri)
|
||||
.user_agent(USER_AGENT)
|
||||
.build()
|
||||
.await
|
||||
.context("could not build client")?;
|
||||
|
||||
client
|
||||
.restore_session(MatrixSession {
|
||||
meta: SessionMeta {
|
||||
user_id: whoami.user_id,
|
||||
device_id: whoami
|
||||
.device_id
|
||||
.unwrap_or_else(|| device_id!("???NoDeviceId???").to_owned()),
|
||||
},
|
||||
tokens: MatrixSessionTokens {
|
||||
access_token: access_token.clone(),
|
||||
refresh_token: None,
|
||||
},
|
||||
})
|
||||
.await
|
||||
.context("could not restore pre-session")?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
async fn initial_sync(client: &Client) -> eyre::Result<()> {
|
||||
// Sync once so we don't process old messages
|
||||
for attempt in 0..3 {
|
||||
match client
|
||||
.sync_once(SyncSettings::default())
|
||||
.await
|
||||
.context("Failed initial sync")
|
||||
{
|
||||
Ok(_) => {
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
if attempt == 2 {
|
||||
return Err(err);
|
||||
}
|
||||
eprintln!("{err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extracts the room IDs from a Room Batch File.
|
||||
pub async fn parse_room_batch_file(path: &Path) -> eyre::Result<Vec<OwnedRoomId>> {
|
||||
let room_id_finder_regex = RegexBuilder::new(r#"`(![^:\s]+:[a-z0-9-_.]+)`"#)
|
||||
.build()
|
||||
.expect("static regex");
|
||||
|
||||
let file_content = tokio::fs::read_to_string(&path)
|
||||
.await
|
||||
.with_context(|| format!("could not read file: {path:?}"))?;
|
||||
|
||||
let mut out = Vec::new();
|
||||
for line in file_content.lines() {
|
||||
for found_room_id in room_id_finder_regex.captures_iter(line) {
|
||||
if let Ok(room_id) = <&RoomId>::try_from(found_room_id.get(1).unwrap().as_str()) {
|
||||
out.push(room_id.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Extracts the room aliases from an Alias Batch File.
|
||||
pub async fn parse_alias_batch_file(path: &Path) -> eyre::Result<Vec<OwnedRoomAliasId>> {
|
||||
let room_id_finder_regex = RegexBuilder::new(r#"`(#[^:\s]+:[a-z0-9-_.]+)`"#)
|
||||
.build()
|
||||
.expect("static regex");
|
||||
|
||||
let file_content = tokio::fs::read_to_string(&path)
|
||||
.await
|
||||
.with_context(|| format!("could not read file: {path:?}"))?;
|
||||
|
||||
let mut out = Vec::new();
|
||||
for line in file_content.lines() {
|
||||
for found_room_alias in room_id_finder_regex.captures_iter(line) {
|
||||
if let Ok(room_alias) =
|
||||
<&RoomAliasId>::try_from(found_room_alias.get(1).unwrap().as_str())
|
||||
{
|
||||
out.push(room_alias.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
96
src/rooms_aliases.rs
Normal file
96
src/rooms_aliases.rs
Normal file
@ -0,0 +1,96 @@
|
||||
use std::{collections::BTreeSet, ops::Deref, str::FromStr};
|
||||
|
||||
use clap::Args;
|
||||
use eyre::ContextCompat;
|
||||
use matrix_sdk::{
|
||||
ruma::{OwnedRoomAliasId, OwnedRoomId, RoomAliasId},
|
||||
Client,
|
||||
};
|
||||
use regex::Regex;
|
||||
|
||||
use crate::initial_sync;
|
||||
|
||||
#[derive(Clone, Args)]
|
||||
pub struct AliasesArgs {
|
||||
#[arg(long = "if-match")]
|
||||
if_match: Vec<AnchoredRegex>,
|
||||
|
||||
#[arg(long = "if-not-match")]
|
||||
if_not_match: Vec<AnchoredRegex>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AnchoredRegex(Regex);
|
||||
|
||||
impl Deref for AnchoredRegex {
|
||||
type Target = Regex;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for AnchoredRegex {
|
||||
type Err = regex::Error;
|
||||
|
||||
fn from_str(value: &'_ str) -> Result<Self, Self::Err> {
|
||||
Regex::try_from(format!("^{value}$")).map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(
|
||||
client: &Client,
|
||||
room_ids: &[OwnedRoomId],
|
||||
args: &AliasesArgs,
|
||||
) -> eyre::Result<()> {
|
||||
initial_sync(client).await?;
|
||||
|
||||
let aliases = find_aliases(client, room_ids, args).await?;
|
||||
|
||||
for alias in aliases {
|
||||
println!("- `{alias}`");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn find_aliases(
|
||||
client: &Client,
|
||||
room_ids: &[OwnedRoomId],
|
||||
args: &AliasesArgs,
|
||||
) -> eyre::Result<BTreeSet<OwnedRoomAliasId>> {
|
||||
let mut out = BTreeSet::new();
|
||||
|
||||
for room_id in room_ids {
|
||||
let room = client
|
||||
.get_room(room_id)
|
||||
.with_context(|| format!("client not in room {room_id}"))?;
|
||||
|
||||
if let Some(canon_alias) = room.canonical_alias() {
|
||||
if does_alias_match(&canon_alias, args) {
|
||||
out.insert(canon_alias);
|
||||
}
|
||||
}
|
||||
for alias in room.alt_aliases() {
|
||||
if does_alias_match(&alias, args) {
|
||||
out.insert(alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn does_alias_match(alias: &RoomAliasId, args: &AliasesArgs) -> bool {
|
||||
for rule in &args.if_match {
|
||||
if !rule.is_match(alias.as_str()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for rule in &args.if_not_match {
|
||||
if rule.is_match(alias.as_str()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
63
src/rooms_make_read_only.rs
Normal file
63
src/rooms_make_read_only.rs
Normal file
@ -0,0 +1,63 @@
|
||||
use errify::{errify, errify_with};
|
||||
use eyre::{bail, Context, ContextCompat};
|
||||
use matrix_sdk::{
|
||||
ruma::{
|
||||
events::room::power_levels::{self, RoomPowerLevelsEventContent},
|
||||
OwnedRoomId,
|
||||
},
|
||||
Client, Room,
|
||||
};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::initial_sync;
|
||||
|
||||
pub async fn run(client: &Client, rooms: &[OwnedRoomId], power_level: u32) -> eyre::Result<()> {
|
||||
initial_sync(client).await?;
|
||||
for room_id in rooms {
|
||||
let Some(room) = client.get_room(room_id) else {
|
||||
bail!("could not find room {room_id} in client")
|
||||
};
|
||||
make_room_read_only(&room, power_level).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Perform a Read-Modify-Write to the `m.room.power_levels` event to
|
||||
/// effectively make the room read-only.
|
||||
///
|
||||
/// The following modifications are performed:
|
||||
///
|
||||
/// - The `event_default` power level is increased to at least `new_min_power_level`.
|
||||
/// - The power level for each specified event type in `events` is increased to at least `new_min_power_level`.
|
||||
#[errify("failed to make room {room:?} read-only")]
|
||||
async fn make_room_read_only(room: &Room, new_min_power_level: u32) -> eyre::Result<()> {
|
||||
let power_level_state = room
|
||||
.get_state_event_static::<RoomPowerLevelsEventContent>()
|
||||
.await
|
||||
.context("fetching power level event")?
|
||||
.context("there are no power levels in this room")?;
|
||||
|
||||
debug!("In room {}:", room.room_id());
|
||||
|
||||
let mut power_levels = power_level_state
|
||||
.deserialize()
|
||||
.context("could not deserialise power levels")?
|
||||
.power_levels();
|
||||
|
||||
debug!("Original PLs: {power_levels:?}");
|
||||
|
||||
// Modify the power levels
|
||||
power_levels.events_default = power_levels.events_default.max(new_min_power_level.into());
|
||||
for event_power_level in power_levels.events.values_mut() {
|
||||
*event_power_level = (*event_power_level).max(new_min_power_level.into());
|
||||
}
|
||||
|
||||
debug!("New PLs: {power_levels:?}");
|
||||
|
||||
// Send the new event
|
||||
room.send_state_event(RoomPowerLevelsEventContent::from(power_levels))
|
||||
.await
|
||||
.context("failed to send power levels")?;
|
||||
Ok(())
|
||||
}
|
119
src/space_tree.rs
Normal file
119
src/space_tree.rs
Normal file
@ -0,0 +1,119 @@
|
||||
use std::{collections::BTreeSet, future::Future, pin::Pin};
|
||||
|
||||
use eyre::{bail, Context, ContextCompat};
|
||||
use matrix_sdk::{
|
||||
config::SyncSettings,
|
||||
ruma::{
|
||||
events::space::child::SpaceChildEventContent, OwnedRoomAliasId, OwnedRoomId, RoomAliasId,
|
||||
RoomId, RoomOrAliasId,
|
||||
},
|
||||
Client, Room,
|
||||
};
|
||||
|
||||
use crate::initial_sync;
|
||||
|
||||
struct SpaceChild {
|
||||
pub room_id: OwnedRoomId,
|
||||
pub name: String,
|
||||
pub aliases: BTreeSet<OwnedRoomAliasId>,
|
||||
pub children: Vec<SpaceChild>,
|
||||
}
|
||||
|
||||
pub async fn run(client: &Client, root_space: &RoomOrAliasId) -> eyre::Result<()> {
|
||||
let room_id = if root_space.is_room_id() {
|
||||
<&RoomId>::try_from(root_space).unwrap().to_owned()
|
||||
} else {
|
||||
let room_alias = <&RoomAliasId>::try_from(root_space).unwrap();
|
||||
client
|
||||
.resolve_room_alias(room_alias)
|
||||
.await
|
||||
.context("could not resolve alias of root space")?
|
||||
.room_id
|
||||
};
|
||||
initial_sync(client).await?;
|
||||
|
||||
let summary = find_and_summarise_room(client, &room_id)
|
||||
.await?
|
||||
.context("did not find that room; are we in it?")?;
|
||||
|
||||
print_tree(&summary, 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn find_and_summarise_room<'a>(
|
||||
client: &'a Client,
|
||||
room_id: &'a RoomId,
|
||||
) -> Pin<Box<impl Future<Output = eyre::Result<Option<SpaceChild>>> + 'a>> {
|
||||
Box::pin(async move {
|
||||
for room in client.rooms() {
|
||||
if room.room_id() == room_id {
|
||||
return summarise_room(client, &room).await.map(Some);
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
})
|
||||
}
|
||||
|
||||
async fn summarise_room(client: &Client, room: &Room) -> eyre::Result<SpaceChild> {
|
||||
let mut children = Vec::new();
|
||||
|
||||
let child_space_events = room
|
||||
.get_state_events_static::<SpaceChildEventContent>()
|
||||
.await
|
||||
.with_context(|| format!("could not get state for room {}", room.room_id()))?;
|
||||
for child_space_state_event in child_space_events {
|
||||
let child = child_space_state_event.deserialize().with_context(|| {
|
||||
format!("could not deserialise state event: {child_space_state_event:?}")
|
||||
})?;
|
||||
let child_room_id = child.state_key();
|
||||
let summarise_room_opt = find_and_summarise_room(client, child_room_id)
|
||||
.await
|
||||
.with_context(|| format!("could not summarise {child_room_id:?}"))?;
|
||||
if let Some(summary) = summarise_room_opt {
|
||||
children.push(summary);
|
||||
}
|
||||
}
|
||||
|
||||
let mut aliases = BTreeSet::new();
|
||||
if let Some(canon_alias) = room.canonical_alias() {
|
||||
aliases.insert(canon_alias);
|
||||
}
|
||||
aliases.extend(room.alt_aliases());
|
||||
|
||||
Ok(SpaceChild {
|
||||
room_id: room.room_id().to_owned(),
|
||||
name: room.name().unwrap_or_default(),
|
||||
aliases,
|
||||
children,
|
||||
})
|
||||
}
|
||||
|
||||
fn print_tree(node: &SpaceChild, depth: u32) {
|
||||
let SpaceChild {
|
||||
room_id,
|
||||
name,
|
||||
aliases,
|
||||
children,
|
||||
} = &node;
|
||||
let prefix = " ".repeat(depth as usize * 2);
|
||||
|
||||
print!("{prefix}- {name} `{room_id}`");
|
||||
|
||||
if !aliases.is_empty() {
|
||||
print!(" = ");
|
||||
for (idx, alias) in aliases.iter().enumerate() {
|
||||
if idx > 0 {
|
||||
print!(", ");
|
||||
}
|
||||
|
||||
print!("{alias}");
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
|
||||
for child in children {
|
||||
print_tree(child, depth + 1);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user