1use std::ffi::OsString;
2
3use anyhow::anyhow;
4
5use radicle::node::{policy, Alias, AliasStore, Handle, NodeId};
6use radicle::{prelude::*, Node};
7use radicle_term::{Element as _, Paint, Table};
8
9use crate::terminal as term;
10use crate::terminal::args::{Args, Error, Help};
11
12pub const HELP: Help = Help {
13 name: "follow",
14 description: "Manage node follow policies",
15 version: env!("RADICLE_VERSION"),
16 usage: r#"
17Usage
18
19 rad follow [<nid>] [--alias <name>] [<option>...]
20
21 The `follow` command will print all nodes being followed, optionally filtered by alias, if no
22 Node ID is provided.
23 Otherwise, it takes a Node ID, optionally in DID format, and updates the follow policy
24 for that peer, optionally giving the peer the alias provided.
25
26Options
27
28 --alias <name> Associate an alias to a followed peer
29 --verbose, -v Verbose output
30 --help Print help
31"#,
32};
33
34#[derive(Debug)]
35pub enum Operation {
36 Follow { nid: NodeId, alias: Option<Alias> },
37 List { alias: Option<Alias> },
38}
39
40#[derive(Debug, Default)]
41pub enum OperationName {
42 Follow,
43 #[default]
44 List,
45}
46
47#[derive(Debug)]
48pub struct Options {
49 pub op: Operation,
50 pub verbose: bool,
51}
52
53impl Args for Options {
54 fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
55 use lexopt::prelude::*;
56
57 let mut parser = lexopt::Parser::from_args(args);
58 let mut verbose = false;
59 let mut nid: Option<NodeId> = None;
60 let mut alias: Option<Alias> = None;
61
62 while let Some(arg) = parser.next()? {
63 match &arg {
64 Value(val) if nid.is_none() => {
65 if let Ok(did) = term::args::did(val) {
66 nid = Some(did.into());
67 } else if let Ok(val) = term::args::nid(val) {
68 nid = Some(val);
69 } else {
70 anyhow::bail!("invalid Node ID `{}` specified", val.to_string_lossy());
71 }
72 }
73 Long("alias") if alias.is_none() => {
74 let name = parser.value()?;
75 let name = term::args::alias(&name)?;
76
77 alias = Some(name.to_owned());
78 }
79 Long("verbose") | Short('v') => verbose = true,
80 Long("help") | Short('h') => {
81 return Err(Error::Help.into());
82 }
83 _ => {
84 return Err(anyhow!(arg.unexpected()));
85 }
86 }
87 }
88
89 let op = match nid {
90 Some(nid) => Operation::Follow { nid, alias },
91 None => Operation::List { alias },
92 };
93 Ok((Options { op, verbose }, vec![]))
94 }
95}
96
97pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
98 let profile = ctx.profile()?;
99 let mut node = radicle::Node::new(profile.socket());
100
101 match options.op {
102 Operation::Follow { nid, alias } => follow(nid, alias, &mut node, &profile)?,
103 Operation::List { alias } => following(&profile, alias)?,
104 }
105
106 Ok(())
107}
108
109pub fn follow(
110 nid: NodeId,
111 alias: Option<Alias>,
112 node: &mut Node,
113 profile: &Profile,
114) -> Result<(), anyhow::Error> {
115 let followed = match node.follow(nid, alias.clone()) {
116 Ok(updated) => updated,
117 Err(e) if e.is_connection_err() => {
118 let mut config = profile.policies_mut()?;
119 config.follow(&nid, alias.as_ref())?
120 }
121 Err(e) => return Err(e.into()),
122 };
123 let outcome = if followed { "updated" } else { "exists" };
124
125 if let Some(alias) = alias {
126 term::success!(
127 "Follow policy {outcome} for {} ({alias})",
128 term::format::tertiary(nid),
129 );
130 } else {
131 term::success!(
132 "Follow policy {outcome} for {}",
133 term::format::tertiary(nid),
134 );
135 }
136
137 Ok(())
138}
139
140pub fn following(profile: &Profile, alias: Option<Alias>) -> anyhow::Result<()> {
141 let store = profile.policies()?;
142 let aliases = profile.aliases();
143 let mut t = term::Table::new(term::table::TableOptions::bordered());
144 t.header([
145 term::format::default(String::from("DID")),
146 term::format::default(String::from("Alias")),
147 term::format::default(String::from("Policy")),
148 ]);
149 t.divider();
150 push_policies(&mut t, &aliases, store.follow_policies()?, &alias);
151 t.print();
152 Ok(())
153}
154
155fn push_policies(
156 t: &mut Table<3, Paint<String>>,
157 aliases: &impl AliasStore,
158 policies: impl Iterator<Item = Result<policy::FollowPolicy, policy::store::Error>>,
159 filter: &Option<Alias>,
160) {
161 for policy in policies {
162 match policy {
163 Ok(policy::FollowPolicy {
164 nid: id,
165 alias,
166 policy,
167 }) => {
168 if match (filter, &alias) {
169 (None, _) => false,
170 (Some(filter), Some(alias)) => *filter != *alias,
171 (Some(_), None) => true,
172 } {
173 continue;
174 }
175
176 t.push([
177 term::format::highlight(Did::from(id).to_string()),
178 match alias {
179 None => term::format::secondary(fallback_alias(&id, aliases)),
180 Some(alias) => term::format::secondary(alias.to_string()),
181 },
182 term::format::secondary(policy.to_string()),
183 ]);
184 }
185 Err(err) => {
186 term::error(format!("Failed to read a follow policy: {err}"));
187 }
188 }
189 }
190}
191
192fn fallback_alias(nid: &PublicKey, aliases: &impl AliasStore) -> String {
193 aliases
194 .alias(nid)
195 .map_or("n/a".to_string(), |alias| alias.to_string())
196}