base: Add a state store inspector to the examples.
parent
8857335a7d
commit
e2225b2700
|
@ -52,6 +52,8 @@ tracing-subscriber = "0.2.13"
|
|||
tempfile = "3.1.0"
|
||||
mockito = "0.27.0"
|
||||
rustyline = "7.0.0"
|
||||
rustyline-derive = "0.4.0"
|
||||
clap = "2.33.3"
|
||||
syntect = "4.4.0"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
|
|
|
@ -0,0 +1,335 @@
|
|||
use std::{convert::TryFrom, env, io, process::exit, sync::Arc};
|
||||
|
||||
use clap::{App as Argparse, AppSettings as ArgParseSettings, Arg, ArgMatches, SubCommand};
|
||||
use futures::{executor::block_on, StreamExt};
|
||||
use rustyline::{
|
||||
completion::{Completer, Pair},
|
||||
error::ReadlineError,
|
||||
highlight::{Highlighter, MatchingBracketHighlighter},
|
||||
hint::{Hinter, HistoryHinter},
|
||||
validate::{MatchingBracketValidator, Validator},
|
||||
CompletionType, Config, Context, EditMode, Editor, OutputStreamType,
|
||||
};
|
||||
use rustyline_derive::Helper;
|
||||
|
||||
use syntect::{
|
||||
easy::HighlightLines,
|
||||
highlighting::{Style, ThemeSet},
|
||||
parsing::SyntaxSet,
|
||||
util::{as_24_bit_terminal_escaped, LinesWithEndings},
|
||||
};
|
||||
|
||||
use matrix_sdk_base::{
|
||||
events::EventType,
|
||||
identifiers::{RoomId, UserId},
|
||||
RoomInfo, Store,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Inspector {
|
||||
store: Store,
|
||||
printer: Printer,
|
||||
}
|
||||
|
||||
#[derive(Helper)]
|
||||
struct InspectorHelper {
|
||||
store: Store,
|
||||
_highlighter: MatchingBracketHighlighter,
|
||||
_validator: MatchingBracketValidator,
|
||||
_hinter: HistoryHinter,
|
||||
}
|
||||
|
||||
impl InspectorHelper {
|
||||
const EVENT_TYPES: &'static [&'static str] = &[
|
||||
"m.room.aliases",
|
||||
"m.room.avatar",
|
||||
"m.room.canonical_alias",
|
||||
"m.room.create",
|
||||
"m.room.encryption",
|
||||
"m.room.guest_access",
|
||||
"m.room.history_visibility",
|
||||
"m.room.join_rules",
|
||||
"m.room.name",
|
||||
"m.room.power_levels",
|
||||
"m.room.tombstone",
|
||||
"m.room.topic",
|
||||
];
|
||||
|
||||
fn new(store: Store) -> Self {
|
||||
Self {
|
||||
store,
|
||||
_highlighter: MatchingBracketHighlighter::new(),
|
||||
_validator: MatchingBracketValidator::new(),
|
||||
_hinter: HistoryHinter {},
|
||||
}
|
||||
}
|
||||
|
||||
fn complete_event_types(&self, arg: Option<&&str>) -> Vec<Pair> {
|
||||
Self::EVENT_TYPES
|
||||
.iter()
|
||||
.map(|t| Pair {
|
||||
display: t.to_string(),
|
||||
replacement: format!("{} ", t),
|
||||
})
|
||||
.filter(|r| {
|
||||
if let Some(arg) = arg {
|
||||
r.replacement.starts_with(arg)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn complete_rooms(&self, arg: Option<&&str>) -> Vec<Pair> {
|
||||
let rooms: Vec<RoomInfo> =
|
||||
block_on(async { self.store.get_room_infos().await.collect().await });
|
||||
|
||||
rooms
|
||||
.into_iter()
|
||||
.map(|r| Pair {
|
||||
display: r.room_id.to_string(),
|
||||
replacement: format!("{} ", r.room_id.to_string()),
|
||||
})
|
||||
.filter(|r| {
|
||||
if let Some(arg) = arg {
|
||||
r.replacement.starts_with(arg)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Completer for InspectorHelper {
|
||||
type Candidate = Pair;
|
||||
|
||||
fn complete(
|
||||
&self,
|
||||
line: &str,
|
||||
pos: usize,
|
||||
_: &Context<'_>,
|
||||
) -> Result<(usize, Vec<Pair>), ReadlineError> {
|
||||
let args: Vec<&str> = line.split_ascii_whitespace().collect();
|
||||
|
||||
let commands = vec![
|
||||
("get-state", "get a state event in the give room"),
|
||||
("list-rooms", "list all rooms"),
|
||||
(
|
||||
"get-members",
|
||||
"get all the membership events in the given room",
|
||||
),
|
||||
]
|
||||
.iter()
|
||||
.map(|(r, d)| Pair {
|
||||
display: format!("{} ({})", r, d),
|
||||
replacement: format!("{} ", r.to_string()),
|
||||
})
|
||||
.collect();
|
||||
|
||||
if args.is_empty() {
|
||||
Ok((pos, commands))
|
||||
} else if args.len() == 1 {
|
||||
if (args[0] == "get-state" || args[0] == "get-members") && line.ends_with(' ') {
|
||||
Ok((args[0].len() + 1, self.complete_rooms(args.get(1))))
|
||||
} else {
|
||||
Ok((
|
||||
0,
|
||||
commands
|
||||
.into_iter()
|
||||
.filter(|c| c.replacement.starts_with(args[0]))
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
} else if args.len() == 2 {
|
||||
if args[0] == "get-state" {
|
||||
if line.ends_with(' ') {
|
||||
Ok((
|
||||
args[0].len() + args[1].len() + 2,
|
||||
self.complete_event_types(args.get(2)),
|
||||
))
|
||||
} else {
|
||||
Ok((args[0].len() + 1, self.complete_rooms(args.get(1))))
|
||||
}
|
||||
} else if args[0] == "get-members" {
|
||||
Ok((args[0].len() + 1, self.complete_rooms(args.get(1))))
|
||||
} else {
|
||||
Ok((pos, vec![]))
|
||||
}
|
||||
} else if args.len() == 3 {
|
||||
if args[0] == "get-state" {
|
||||
Ok((
|
||||
args[0].len() + args[1].len() + 2,
|
||||
self.complete_event_types(args.get(2)),
|
||||
))
|
||||
} else {
|
||||
Ok((pos, vec![]))
|
||||
}
|
||||
} else {
|
||||
Ok((pos, vec![]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Hinter for InspectorHelper {
|
||||
type Hint = String;
|
||||
}
|
||||
|
||||
impl Highlighter for InspectorHelper {}
|
||||
|
||||
impl Validator for InspectorHelper {}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Printer {
|
||||
ps: Arc<SyntaxSet>,
|
||||
ts: Arc<ThemeSet>,
|
||||
}
|
||||
|
||||
impl Printer {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
ps: SyntaxSet::load_defaults_newlines().into(),
|
||||
ts: ThemeSet::load_from_folder("/home/poljar/local/cfg/bat/themes")
|
||||
.expect("Couldn't load themes")
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn pretty_print_struct(&self, data: &impl std::fmt::Debug) {
|
||||
let data = format!("{:#?}", data);
|
||||
|
||||
let syntax = self.ps.find_syntax_by_extension("rs").unwrap();
|
||||
let mut h = HighlightLines::new(syntax, &self.ts.themes["Forest Night"]);
|
||||
|
||||
for line in LinesWithEndings::from(&data) {
|
||||
let ranges: Vec<(Style, &str)> = h.highlight(line, &self.ps);
|
||||
let escaped = as_24_bit_terminal_escaped(&ranges[..], false);
|
||||
print!("{}", escaped);
|
||||
}
|
||||
// Clear the formatting
|
||||
println!("\x1b[0m");
|
||||
}
|
||||
}
|
||||
|
||||
impl Inspector {
|
||||
fn new(database_path: &str) -> Self {
|
||||
let printer = Printer::new();
|
||||
let store = Store::open_default(database_path);
|
||||
|
||||
Self { store, printer }
|
||||
}
|
||||
|
||||
async fn run(&self, matches: ArgMatches<'_>) {
|
||||
match matches.subcommand() {
|
||||
("get-members", args) => {
|
||||
let args = args.expect("No args provided for get-state");
|
||||
let room_id = RoomId::try_from(args.value_of("room-id").unwrap()).unwrap();
|
||||
|
||||
self.get_members(room_id).await;
|
||||
}
|
||||
("list-rooms", _) => self.list_rooms().await,
|
||||
("get-state", args) => {
|
||||
let args = args.expect("No args provided for get-state");
|
||||
let room_id = RoomId::try_from(args.value_of("room-id").unwrap()).unwrap();
|
||||
let event_type = EventType::try_from(args.value_of("event-type").unwrap()).unwrap();
|
||||
self.get_state(room_id, event_type).await;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_rooms(&self) {
|
||||
let rooms: Vec<RoomInfo> = self.store.get_room_infos().await.collect().await;
|
||||
self.printer.pretty_print_struct(&rooms);
|
||||
}
|
||||
|
||||
async fn get_members(&self, room_id: RoomId) {
|
||||
let joined: Vec<UserId> = self
|
||||
.store
|
||||
.get_joined_user_ids(&room_id)
|
||||
.await
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
for member in joined {
|
||||
let event = self.store.get_member_event(&room_id, &member).await;
|
||||
self.printer.pretty_print_struct(&event);
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_state(&self, room_id: RoomId, event_type: EventType) {
|
||||
self.printer
|
||||
.pretty_print_struct(&self.store.get_state_event(&room_id, event_type, "").await);
|
||||
}
|
||||
|
||||
async fn parse_and_run(&self, input: &str) {
|
||||
let argparse = Argparse::new("state-inspector")
|
||||
.global_setting(ArgParseSettings::DisableHelpFlags)
|
||||
.global_setting(ArgParseSettings::DisableVersion)
|
||||
.global_setting(ArgParseSettings::VersionlessSubcommands)
|
||||
.global_setting(ArgParseSettings::NoBinaryName)
|
||||
.setting(ArgParseSettings::SubcommandRequiredElseHelp)
|
||||
.subcommand(SubCommand::with_name("list-rooms"))
|
||||
.subcommand(SubCommand::with_name("get-members").arg(
|
||||
Arg::with_name("room-id").required(true).validator(|r| {
|
||||
RoomId::try_from(r)
|
||||
.map(|_| ())
|
||||
.map_err(|_| "Invalid room id given".to_owned())
|
||||
}),
|
||||
))
|
||||
.subcommand(
|
||||
SubCommand::with_name("get-state")
|
||||
.arg(Arg::with_name("room-id").required(true).validator(|r| {
|
||||
RoomId::try_from(r)
|
||||
.map(|_| ())
|
||||
.map_err(|_| "Invalid room id given".to_owned())
|
||||
}))
|
||||
.arg(Arg::with_name("event-type").required(true).validator(|e| {
|
||||
EventType::try_from(e)
|
||||
.map(|_| ())
|
||||
.map_err(|_| "Invalid event type".to_string())
|
||||
})),
|
||||
);
|
||||
|
||||
match argparse.get_matches_from_safe(input.split_ascii_whitespace()) {
|
||||
Ok(m) => {
|
||||
self.run(m).await;
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
let database_path = match env::args().nth(1) {
|
||||
Some(a) => a,
|
||||
_ => {
|
||||
eprintln!("Usage: {} <database_path>", env::args().next().unwrap());
|
||||
exit(1)
|
||||
}
|
||||
};
|
||||
|
||||
let inspector = Inspector::new(&database_path);
|
||||
|
||||
let config = Config::builder()
|
||||
.history_ignore_space(true)
|
||||
.completion_type(CompletionType::List)
|
||||
.edit_mode(EditMode::Emacs)
|
||||
.output_stream(OutputStreamType::Stdout)
|
||||
.build();
|
||||
|
||||
let helper = InspectorHelper::new(inspector.store.clone());
|
||||
|
||||
let mut rl = Editor::<InspectorHelper>::with_config(config);
|
||||
rl.set_helper(Some(helper));
|
||||
|
||||
while let Ok(input) = rl.readline(">> ") {
|
||||
rl.add_history_entry(input.as_str());
|
||||
block_on(inspector.parse_and_run(input.as_str()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -40,6 +40,17 @@ impl Store {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn open_default(path: impl AsRef<Path>) -> Self {
|
||||
let inner = SledStore::open_with_path(path);
|
||||
|
||||
Self {
|
||||
inner,
|
||||
session: Arc::new(RwLock::new(None)),
|
||||
rooms: DashMap::new().into(),
|
||||
stripped_rooms: DashMap::new().into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_bare_room(&self, room_id: &RoomId) -> Option<Room> {
|
||||
#[allow(clippy::map_clone)]
|
||||
self.rooms.get(room_id).map(|r| r.clone())
|
||||
|
|
Loading…
Reference in New Issue