Skip to main content

Channel

Struct Channel 

Source
pub struct Channel { /* private fields */ }
Expand description

A unified handle for a chat or channel — a DM and a Community channel behave the same. Every method routes to the right transport under the hood, so a bot author never branches on DM-vs- channel. Obtained from VectorBot::channel / dm / community, or IncomingMessage::channel.

Implementations§

Source§

impl Channel

Source

pub fn id(&self) -> &str

The id of this chat or channel — an npub for a DM, a channel id for a Community channel.

Source

pub fn kind(&self) -> ChannelKind

Whether this is a DM or a Community channel.

Source

pub fn is_dm(&self) -> bool

true for a direct message.

Source

pub fn is_community(&self) -> bool

true for a Community channel.

Source

pub async fn send(&self, text: &str) -> Result<String>

Send a text message. Returns the new message’s event id.

Examples found in repository?
examples/whitelist_bot.rs (line 43)
17async fn main() -> vector_sdk::Result<()> {
18    let nsec = std::env::var("VECTOR_NSEC").expect("set VECTOR_NSEC to your bot's nsec");
19    let whitelist: Vec<String> = std::env::var("VECTOR_WHITELIST")
20        .unwrap_or_default()
21        .split(',')
22        .map(|s| s.trim().to_string())
23        .filter(|s| !s.is_empty())
24        .collect();
25
26    // Only invites from these npubs are auto-accepted; all others stay parked.
27    let bot = VectorBot::builder().nsec(nsec).whitelist(whitelist).build().await?;
28
29    println!("Private bot online as {}", bot.npub());
30    for c in bot.communities().await {
31        println!("  already a member of community {}", c.id());
32    }
33
34    bot.on_event(|bot, event| async move {
35        match event {
36            // An invite arrived. A whitelisted inviter means it's already being joined;
37            // anyone else leaves it parked for you to review via bot.pending_invites().
38            BotEvent::Invite { community_id } => {
39                println!("invite to {community_id} (auto-join attempted per whitelist)");
40            }
41            // Best-effort hello once we land in a community we accepted.
42            BotEvent::MemberJoin { channel_id, npub } if npub == bot.npub() => {
43                let _ = bot.channel(channel_id).send("Hello! Reporting for duty 🫡").await;
44            }
45            BotEvent::Message(msg) if !msg.is_mine() => {
46                let _ = msg.reply("At your service. 🛡️").await;
47            }
48            _ => {}
49        }
50    })
51    .await?;
52
53    Ok(())
54}
More examples
Hide additional examples
examples/moderation_bot.rs (line 35)
23async fn main() -> vector_sdk::Result<()> {
24    let nsec = std::env::var("VECTOR_NSEC").expect("set VECTOR_NSEC to your bot's nsec");
25
26    // `.public()` lets anyone invite the bot into their community to moderate it.
27    let bot = VectorBot::builder().nsec(nsec).public().build().await?;
28    println!("Moderation bot online as {}", bot.npub());
29
30    bot.on_event(|bot, event| async move {
31        match event {
32            // Greet new arrivals in the channel they joined.
33            BotEvent::MemberJoin { channel_id, npub } => {
34                let who = &npub[..npub.len().min(12)];
35                let _ = bot.channel(channel_id).send(&format!("👋 Welcome, {who}!")).await;
36            }
37
38            // Screen community messages; ban posters of banned words (never admins or ourselves).
39            BotEvent::Message(msg) if msg.is_group && !msg.is_mine() => {
40                let lower = msg.text().to_lowercase();
41                if !BANNED_WORDS.iter().any(|w| lower.contains(w)) {
42                    return;
43                }
44                if let Some(member) = msg.member() {
45                    if member.is_admin() {
46                        return; // never moderate admins or the owner
47                    }
48                    match member.ban().await {
49                        Ok(()) => println!("Banned {} for a banned word", member.npub()),
50                        Err(e) => eprintln!("Couldn't ban {}: {e}", member.npub()),
51                    }
52                }
53            }
54
55            _ => {}
56        }
57    })
58    .await?;
59
60    Ok(())
61}
examples/v2_send_once.rs (line 58)
13async fn main() -> vector_sdk::Result<()> {
14    let mut args = std::env::args().skip(1);
15    let invite = args.next().expect("arg 1: invite link");
16    let channel_name = args.next().expect("arg 2: channel name");
17    let text = args.next().expect("arg 3: message text");
18
19    let data_dir = std::env::temp_dir().join("v2_send_once_data");
20    let bot = VectorBot::builder().data_dir(&data_dir).build().await?;
21    println!("── sender online as {}", bot.npub());
22
23    println!("── joining via link…");
24    let summary = bot.core().join_community(&invite).await?;
25    let name = summary.get("name").and_then(|v| v.as_str()).unwrap_or("?");
26    println!("── joined \"{name}\"");
27
28    // The join-time fold is owner-only; admin-created channels arrive on the first
29    // control follow. Poll the local list until the target channel appears.
30    let mut channel_id: Option<String> = None;
31    for attempt in 0..10 {
32        for c in bot.core().list_communities().await {
33            if c.get("version").and_then(|v| v.as_u64()) != Some(2) {
34                continue;
35            }
36            let Some(chans) = c.get("channels").and_then(|v| v.as_array()) else { continue };
37            for ch in chans {
38                if ch.get("name").and_then(|n| n.as_str()) == Some(channel_name.as_str()) {
39                    channel_id = ch.get("channel_id").and_then(|i| i.as_str()).map(String::from);
40                }
41            }
42        }
43        if channel_id.is_some() {
44            break;
45        }
46        if attempt == 0 {
47            println!("── \"{channel_name}\" not in the join snapshot; syncing for the control fold…");
48        }
49        let _ = bot.core().sync_communities().await;
50        tokio::time::sleep(std::time::Duration::from_secs(2)).await;
51    }
52    let Some(channel_id) = channel_id else {
53        eprintln!("!! channel \"{channel_name}\" never appeared; giving up");
54        std::process::exit(1);
55    };
56    println!("── channel \"{channel_name}\" = {channel_id}");
57
58    match bot.channel(&channel_id).send(&text).await {
59        Ok(id) => println!("── sent ✅  message id {id}"),
60        Err(e) => {
61            eprintln!("!! send failed: {e}");
62            std::process::exit(1);
63        }
64    }
65    // Give the gift-wrap publish + persistence a moment to flush before exit.
66    tokio::time::sleep(std::time::Duration::from_secs(5)).await;
67    Ok(())
68}
examples/v2_typing_probe.rs (line 115)
15async fn main() -> vector_sdk::Result<()> {
16    let channel_name = std::env::args().nth(1).unwrap_or_else(|| "spam and testing".into());
17
18    let data_dir = std::env::temp_dir().join("v2_send_once_data");
19    let bot = VectorBot::builder().data_dir(&data_dir).build().await?;
20    println!("── probe online as {}", bot.npub());
21
22    // Find the joined v2 community + the target channel id.
23    let (mut community_hex, mut channel_hex) = (None::<String>, None::<String>);
24    for c in bot.core().list_communities().await {
25        if c.get("version").and_then(|v| v.as_u64()) != Some(2) {
26            continue;
27        }
28        let cid = c.get("community_id").and_then(|v| v.as_str()).map(String::from);
29        for ch in c.get("channels").and_then(|v| v.as_array()).cloned().unwrap_or_default() {
30            if ch.get("name").and_then(|n| n.as_str()) == Some(channel_name.as_str()) {
31                community_hex = cid.clone();
32                channel_hex = ch.get("channel_id").and_then(|i| i.as_str()).map(String::from);
33            }
34        }
35    }
36    let (community_hex, channel_hex) = match (community_hex, channel_hex) {
37        (Some(c), Some(ch)) => (c, ch),
38        _ => {
39            eprintln!("!! channel \"{channel_name}\" not held locally — join first (run v2_send_once once)");
40            std::process::exit(1);
41        }
42    };
43
44    // Load the community row for relays + root, and derive the channel plane pk
45    // exactly as the send/subscribe paths do.
46    let cid = vector_core::community::CommunityId(vector_core::simd::hex::hex_to_bytes_32(&community_hex));
47    let community = vector_core::db::community::load_community_v2(&cid)
48        .map_err(vector_core::VectorError::Other)?
49        .ok_or(vector_core::VectorError::Other("community row missing".into()))?;
50    let chid = vector_core::community::ChannelId(vector_core::simd::hex::hex_to_bytes_32(&channel_hex));
51    let ch = community.channel(&chid).ok_or(vector_core::VectorError::Other("channel missing".into()))?;
52    let (secret, epoch) = community.channel_secret(ch);
53    let plane_pk = vector_core::community::v2::derive::channel_group_key(&secret, &chid, epoch).pk();
54    println!("── channel \"{channel_name}\" plane pk = {plane_pk}");
55    println!("── community relays: {:?}", community.relays);
56
57    // One RAW single-relay client per community relay, each with its own live
58    // typing subscription — per-relay forwarding becomes directly observable.
59    // Every arriving wrap is opened with the plane key and its rumor dumped
60    // verbatim: exactly what Armada's `openWrap` + freshness check would see.
61    let group = vector_core::community::v2::derive::channel_group_key(&secret, &chid, epoch);
62    for relay in community.relays.clone() {
63        let plane_pk = plane_pk;
64        let group = group.clone();
65        tokio::spawn(async move {
66            let client = Client::default();
67            if client.add_relay(&relay).await.is_err() {
68                println!("[{relay}] add failed");
69                return;
70            }
71            client.connect().await;
72            let filter = Filter::new().kind(Kind::Custom(21059)).author(plane_pk);
73            if let Err(e) = client.subscribe(filter, None).await {
74                println!("[{relay}] subscribe failed: {e}");
75                return;
76            }
77            println!("[{relay}] subscribed");
78            let mut notifications = client.notifications();
79            while let Ok(n) = notifications.recv().await {
80                if let RelayPoolNotification::Event { event, .. } = n {
81                    println!(
82                        "[{relay}] ← 21059 wrap id={} wrap_created_at={} size={}B",
83                        &event.id.to_hex()[..12],
84                        event.created_at,
85                        event.content.len()
86                    );
87                    match vector_core::community::v2::stream::open_wrap(&event, &group) {
88                        Ok(opened) => {
89                            let now = std::time::SystemTime::now()
90                                .duration_since(std::time::UNIX_EPOCH)
91                                .map(|d| d.as_millis() as u64)
92                                .unwrap_or(0);
93                            let age = now as i128 - opened.at_ms as i128;
94                            println!(
95                                "    rumor: kind={} author={} created_at={} content={:?}",
96                                opened.rumor.kind,
97                                &opened.author.to_hex()[..12],
98                                opened.rumor.created_at,
99                                opened.rumor.content
100                            );
101                            println!("    tags:  {:?}", opened.rumor.tags.iter().map(|t| t.as_slice().to_vec()).collect::<Vec<_>>());
102                            println!("    seal:  kind={} created_at={}", opened.seal.kind, opened.seal.created_at);
103                            println!("    at_ms={} (age {age} ms → Armada freshness gate {} 8000ms)", opened.at_ms, if age <= 8000 { "PASSES ≤" } else { "FAILS >" });
104                        }
105                        Err(e) => println!("    !! open failed: {e}"),
106                    }
107                }
108            }
109        });
110    }
111    tokio::time::sleep(std::time::Duration::from_secs(4)).await;
112
113    // Trigger the resident bot: a normal chat message it answers with ch.typing().
114    println!("── sending !typing into \"{channel_name}\"…");
115    match bot.channel(&channel_hex).send("!typing").await {
116        Ok(_) => println!("── trigger sent; watching 25s for the bot's typing wrap…"),
117        Err(e) => eprintln!("!! trigger send failed: {e}"),
118    }
119    tokio::time::sleep(std::time::Duration::from_secs(25)).await;
120    println!("── probe done");
121    Ok(())
122}
examples/concordia.rs (line 204)
54async fn main() -> vector_sdk::Result<()> {
55    // Identity: VECTOR_NSEC override, else the persisted <data_dir>/identity.nsec
56    // (created on first run by the builder). Public invite policy: any member can
57    // pull the bot into their community with a direct invite (v1 or v2).
58    let mut builder = VectorBot::builder().public();
59    if let Ok(nsec) = std::env::var("VECTOR_NSEC") {
60        builder = builder.nsec(nsec);
61    }
62    let bot = builder.build().await?;
63    println!("── {NAME} online as {}", bot.npub());
64
65    // Optional one-shot profile publish: upload the avatar plainly (avatars are
66    // public) and publish the kind-0 with the bot flag. Existing fields are
67    // carried forward by the profile pipeline, so this never clobbers. Deferred
68    // until after listen() has connected the community relays — a kind-0 that
69    // only reaches the login relays is invisible to community peers' clients.
70    if let Ok(avatar_path) = std::env::var("CONCORDIA_AVATAR") {
71        let bot = bot.clone();
72        tokio::spawn(async move {
73            tokio::time::sleep(std::time::Duration::from_secs(10)).await;
74            let avatar_path = shellexpand_home(&avatar_path);
75            println!("── uploading avatar {avatar_path}…");
76            match bot.core().upload_public_image(&avatar_path).await {
77                Ok(url) => {
78                    let ok = bot.core().update_bot_profile(NAME, &url, "", ABOUT).await;
79                    println!("── profile publish {}  avatar={url}", if ok { "✅" } else { "FAILED" });
80                    if ok {
81                        push_profile_to_communities().await;
82                    }
83                }
84                Err(e) => eprintln!("!! avatar upload failed: {e}"),
85            }
86        });
87    }
88
89    // Optional invite link: join on first run; later runs come up with the
90    // persisted memberships and need no arguments.
91    if let Some(invite) = std::env::args().nth(1).or_else(|| std::env::var("VECTOR_INVITE").ok()) {
92        println!("── joining via link…");
93        match bot.core().join_community(&invite).await {
94            Ok(summary) => {
95                let name = summary.get("name").and_then(|v| v.as_str()).unwrap_or("?");
96                let ver = summary.get("version").and_then(|v| v.as_u64()).unwrap_or(0);
97                println!("── joined \"{name}\"  protocol=v{ver}");
98            }
99            Err(e) => {
100                eprintln!("!! join failed: {e}");
101                return Err(e);
102            }
103        }
104    }
105
106    // Slash commands (Bot Interface Phase 1): registered here, published as the
107    // kind-10304 manifest at listen start, invoked by PLAIN TEXT from any client.
108    // Vector's `/` picker renders exactly this registry with argument hints; a
109    // matched invocation is consumed before the legacy !bang console below.
110    bot.command("help", "List everything Concordia can do").run(|ctx| async move {
111        let _ = ctx.reply(HELP).await;
112    });
113    bot.command("ping", "Round-trip latency check").run(|ctx| async move {
114        let _ = ctx.reply(format!("pong 🏓 ({} ms)", now_ms())).await;
115    });
116    bot.command("roll", "Roll a die")
117        .int("sides", "How many sides (default 6)", false)
118        .run(|ctx| async move {
119            let sides = ctx.int("sides").unwrap_or(6).clamp(2, 1_000_000);
120            let roll = (now_ms() as i64 % sides) + 1;
121            let _ = ctx.reply(format!("🎲 rolled {roll} on a d{sides}")).await;
122        });
123    bot.command("announce", "Format a two-part announcement")
124        .string("title", "Announcement title", true)
125        .string("body", "Announcement body", true)
126        .run(|ctx| async move {
127            let title = ctx.str("title").unwrap_or("(untitled)").to_string();
128            let body = ctx.str("body").unwrap_or_default().to_string();
129            let _ = ctx.reply(format!("📣 {title}\n{body}")).await;
130        });
131    // ── Param-type test battery: every ArgType, solo and mixed ──────────────
132    bot.command("typetest", "Echo back every param type, parsed")
133        .string("text", "Any free text", true)
134        .int("count", "A whole number", true)
135        .number("ratio", "A decimal number", true)
136        .flag("loud", "true or false", true)
137        .user("who", "Any user (npub)", true)
138        .choice("color", "Pick a color", ["red", "green", "blue"], true)
139        .run(|ctx| async move {
140            let report = format!(
141                "typetest received:\n\
142                 text   (String) = {:?}\n\
143                 count  (Int)    = {}\n\
144                 ratio  (Number) = {}\n\
145                 loud   (Bool)   = {}\n\
146                 who    (User)   = {}\n\
147                 color  (Choice) = {}",
148                ctx.str("text").unwrap_or_default(),
149                ctx.int("count").unwrap_or_default(),
150                ctx.number("ratio").unwrap_or_default(),
151                ctx.flag("loud").unwrap_or_default(),
152                ctx.str("who").unwrap_or_default(),
153                ctx.str("color").unwrap_or_default(),
154            );
155            let _ = ctx.reply(report).await;
156        });
157    bot.command("greet", "Greet a member in a chosen style")
158        .user("who", "Who to greet", true)
159        .choice("style", "Greeting style", ["formal", "casual", "pirate"], true)
160        .int("times", "Repeat 1-5 times (default 1)", false)
161        .run(|ctx| async move {
162            let who = ctx.str("who").unwrap_or_default().to_string();
163            let line = match ctx.str("style").unwrap_or("casual") {
164                "formal" => format!("Good day to you, {who}."),
165                "pirate" => format!("Ahoy, {who}! 🏴‍☠️"),
166                _ => format!("yo {who} 👋"),
167            };
168            let times = ctx.int("times").unwrap_or(1).clamp(1, 5) as usize;
169            let _ = ctx.reply(vec![line; times].join("\n")).await;
170        });
171    bot.command("calc", "Do some arithmetic")
172        .number("a", "First operand", true)
173        .choice("op", "Operation", ["add", "sub", "mul", "div"], true)
174        .number("b", "Second operand", true)
175        .run(|ctx| async move {
176            let a = ctx.number("a").unwrap_or_default();
177            let b = ctx.number("b").unwrap_or_default();
178            let answer = match ctx.str("op").unwrap_or_default() {
179                "add" => Some(a + b),
180                "sub" => Some(a - b),
181                "mul" => Some(a * b),
182                "div" if b != 0.0 => Some(a / b),
183                _ => None,
184            };
185            let _ = ctx
186                .reply(match answer {
187                    Some(v) => format!("🧮 {v}"),
188                    None => "cannot divide by zero".to_string(),
189                })
190                .await;
191        });
192
193    bot.command("reply", "Get a threaded reply to your message").run(|ctx| async move {
194        let _ = ctx.reply("this is a threaded reply ✅ (I quoted your message)").await;
195    });
196    bot.command("react", "React to your message")
197        .choice("emoji", "Which reaction", ["🔥", "👍", "❤️", "😂", "🫡"], false)
198        .run(|ctx| async move {
199            let emoji = ctx.str("emoji").unwrap_or("🔥").to_string();
200            log_err("react", ctx.msg.react(&emoji).await.map(|_| String::new()));
201        });
202    bot.command("edit", "Watch me send then edit a message").run(|ctx| async move {
203        let ch = ctx.msg.channel();
204        if let Ok(id) = ch.send("editing this in one second…").await {
205            log_err("edit", ch.edit(&id, "edited ✏️ (this text was changed)").await.map(|_| String::new()));
206        }
207    });
208    bot.command("delete", "Watch a message self-destruct").run(|ctx| async move {
209        let ch = ctx.msg.channel();
210        if let Ok(id) = ch.send("…this message will self-destruct").await {
211            log_err("delete", ch.delete(&id).await.map(|_| String::new()));
212        }
213    });
214    bot.command("typing", "Emit a typing indicator").run(|ctx| async move {
215        log_err("typing", ctx.msg.channel().typing().await.map(|_| String::new()));
216    });
217    bot.command("file", "Receive a small encrypted attachment").run(|ctx| async move {
218        send_test_file(&ctx.msg.channel()).await;
219    });
220    bot.command("members", "The folded member list").run(|ctx| async move {
221        if let Some(community) = ctx.msg.community() {
222            let members = community.members().await;
223            let list: Vec<String> = members.iter().map(|m| short(m.npub())).collect();
224            let _ = ctx.reply(format!("{} member(s): {}", members.len(), list.join(", "))).await;
225        } else {
226            let _ = ctx.reply("not in a community here").await;
227        }
228    });
229    bot.command("channels", "The channels I can see here").run(|ctx| async move {
230        diagnostics(&ctx.bot, &ctx.msg, "!channels").await;
231    });
232    bot.command("caps", "My capabilities here (roles engine)").run(|ctx| async move {
233        diagnostics(&ctx.bot, &ctx.msg, "!caps").await;
234    });
235    bot.command("roles", "The community roster").run(|ctx| async move {
236        diagnostics(&ctx.bot, &ctx.msg, "!roles").await;
237    });
238    bot.command("info", "Community protocol + ownership summary").run(|ctx| async move {
239        diagnostics(&ctx.bot, &ctx.msg, "!info").await;
240    });
241    bot.command("whoami", "My npub and this channel id").run(|ctx| async move {
242        diagnostics(&ctx.bot, &ctx.msg, "!whoami").await;
243    });
244    bot.command("reconnect", "Bounce my relay sockets (reconnect drill)").run(|ctx| async move {
245        let bounced = bounce_community_relays().await;
246        let _ = ctx.reply(format!("bounced {bounced} relay socket(s) — /ping to test the post-reconnect subscription")).await;
247    });
248
249    // The join snapshot folds only owner-authored channels; admin-created ones
250    // arrive on the first control follow, so re-print once that has landed.
251    print_channels(&bot, "channels visible (startup)").await;
252    {
253        let bot = bot.clone();
254        tokio::spawn(async move {
255            tokio::time::sleep(std::time::Duration::from_secs(25)).await;
256            print_channels(&bot, "channels visible (after control follow)").await;
257        });
258    }
259    println!("── listening. Message me `!help` from your client.\n");
260
261    bot.on_event(|bot, event| async move {
262        match event {
263            BotEvent::Message(msg) => {
264                if msg.is_mine() {
265                    return; // never react to our own sends
266                }
267                let author = short(msg.message.npub.as_deref().unwrap_or("?"));
268                let text = msg.text().trim().to_string();
269                println!("[MSG]    {author}: {text}");
270
271                // Command console — each arm fires a distinct SDK send-path.
272                let ch = msg.channel();
273                match text.as_str() {
274                    "!help" => reply(&msg, HELP).await,
275                    "!ping" => reply(&msg, &format!("pong 🏓 ({} ms)", now_ms())).await,
276                    "!reply" => reply(&msg, "this is a threaded reply ✅ (I quoted your message)").await,
277                    "!react" => log_err("react", msg.react("🔥").await.map(|_| String::new())),
278                    "!typing" => log_err("typing", ch.typing().await.map(|_| String::new())),
279                    "!edit" => {
280                        if let Ok(id) = ch.send("editing this in one second…").await {
281                            log_err("edit", ch.edit(&id, "edited ✏️ (this text was changed)").await.map(|_| String::new()));
282                        }
283                    }
284                    "!delete" => {
285                        if let Ok(id) = ch.send("…this message will self-destruct").await {
286                            log_err("delete", ch.delete(&id).await.map(|_| String::new()));
287                        }
288                    }
289                    "!file" => send_test_file(&ch).await,
290                    "!members" => {
291                        if let Some(community) = msg.community() {
292                            let members = community.members().await;
293                            let list: Vec<String> = members.iter().map(|m| short(m.npub())).collect();
294                            reply(&msg, &format!("{} member(s): {}", members.len(), list.join(", "))).await;
295                        } else {
296                            reply(&msg, "not in a community here").await;
297                        }
298                    }
299                    "!reconnect" => {
300                        // Ops/repro tool: bounce this bot's community relay sockets (a
301                        // relay restart or idle drop in miniature) — then a !ping proves
302                        // whether the subscription survived the fresh AUTH gate.
303                        let bounced = bounce_community_relays().await;
304                        reply(&msg, &format!("bounced {bounced} relay socket(s) — send !ping to test the post-reconnect subscription")).await;
305                    }
306                    "!channels" | "!info" | "!caps" | "!roles" | "!whoami" => diagnostics(&bot, &msg, &text).await,
307                    other if other.starts_with('!') => reply(&msg, &format!("unknown command `{other}` — try !help")).await,
308                    // Non-command chatter is logged above but never replied to, so
309                    // the bot can sit in a busy community without spamming echoes.
310                    _ => {}
311                }
312            }
313            BotEvent::MessageUpdate { message, .. } => {
314                println!("[UPDATE] {} → \"{}\"  ({} reaction(s))", short(message.npub.as_deref().unwrap_or("?")), message.content, message.reactions.len());
315            }
316            BotEvent::Delete { message_id, .. } => println!("[DELETE] message {}", short(&message_id)),
317            BotEvent::MemberJoin { npub, .. } => println!("[JOIN]   {}", short(&npub)),
318            BotEvent::MemberLeave { npub, .. } => println!("[LEAVE]  {}", short(&npub)),
319            BotEvent::Typing { npub, .. } => println!("[TYPING] {}", short(&npub)),
320            BotEvent::Invite { community_id } => println!("[INVITE] for community {}", short(&community_id)),
321            BotEvent::Removed { community_id } => println!("[REMOVED] from community {} — I was kicked/banned", short(&community_id)),
322        }
323    })
324    .await?;
325
326    Ok(())
327}
Source

pub async fn reply(&self, replied_to: &str, text: &str) -> Result<String>

Send a text message as a threaded reply to replied_to (an existing message’s id). Works for DMs and Community channels. Returns the new message’s event id.

Source

pub async fn react(&self, message_id: &str, emoji: &str) -> Result<()>

React to a message with a unicode emoji (e.g. "👍").

Source

pub async fn react_custom( &self, message_id: &str, shortcode_emoji: &str, image_url: &str, ) -> Result<()>

React with a custom NIP-30 pack emoji: a :shortcode: plus its image URL.

Source

pub async fn typing(&self) -> Result<()>

Send an ephemeral typing indicator. Useful while the bot is “thinking”.

Examples found in repository?
examples/echo_bot.rs (line 23)
11async fn main() -> vector_sdk::Result<()> {
12    let nsec = std::env::var("VECTOR_NSEC").expect("set VECTOR_NSEC to your bot's nsec");
13
14    let bot = VectorBot::builder().nsec(nsec).build().await?;
15    println!("Echo bot online as {}", bot.npub());
16
17    // Blocks, processing inbound messages until the client disconnects. The SAME handler and
18    // reply work for DMs and Community channels — the SDK hides the transport difference.
19    bot.on_message(|_bot, msg| async move {
20        if msg.is_mine() {
21            return; // don't echo our own messages
22        }
23        let _ = msg.channel().typing().await;
24        let _ = msg.reply(&format!("Echo: {}", msg.text())).await;
25    })
26    .await?;
27
28    Ok(())
29}
More examples
Hide additional examples
examples/v2_community_bot.rs (line 49)
18async fn main() -> vector_sdk::Result<()> {
19    let nsec = std::env::var("VECTOR_NSEC").expect("set VECTOR_NSEC to your bot's nsec");
20
21    let bot = VectorBot::builder().nsec(nsec).build().await?;
22    println!("v2 community bot online as {}", bot.npub());
23
24    // Create-and-invite is opt-in so a restart doesn't spawn a fresh community
25    // each time (a real bot persists its community id and reuses it).
26    if std::env::var("VECTOR_CREATE").is_ok() {
27        // Creation is one hop through the core facade (the ergonomic surface stays at
28        // the stable published API). A fresh community is created on the modern protocol.
29        let summary = bot.core().create_community_v2("Vector v2 Demo").await?;
30        let id = summary.get("community_id").and_then(|v| v.as_str()).unwrap_or_default().to_string();
31        let community = bot.community(id);
32        println!("created v2 community {}", community.id());
33
34        let url = community.create_invite().await?;
35        println!("shareable invite link: {url}");
36
37        if let Ok(guest) = std::env::var("VECTOR_INVITE_NPUB") {
38            community.invite(&guest).await?;
39            println!("direct-invited {guest}");
40        }
41    }
42
43    // One handler serves DMs and every Community channel — the SDK hides the
44    // transport (and the protocol) difference.
45    bot.on_message(|_bot, msg| async move {
46        if msg.is_mine() {
47            return; // never reply to our own messages (no echo loop)
48        }
49        let _ = msg.channel().typing().await;
50        let _ = msg.reply(&format!("v2 heard: {}", msg.text())).await;
51    })
52    .await?;
53
54    Ok(())
55}
examples/ai_bot.rs (line 47)
29async fn main() -> vector_sdk::Result<()> {
30    let nsec = std::env::var("VECTOR_NSEC").expect("set VECTOR_NSEC to your bot's nsec");
31    std::env::var("OPENAI_API_KEY").expect("set OPENAI_API_KEY for the LLM endpoint");
32
33    let bot = VectorBot::builder().nsec(nsec).build().await?;
34    println!("AI bot online as {}", bot.npub());
35
36    // Per-conversation rolling history, shared across handler invocations.
37    let history: Arc<Mutex<HashMap<String, Vec<Value>>>> = Arc::new(Mutex::new(HashMap::new()));
38
39    bot.on_message(move |_bot, msg| {
40        let history = history.clone();
41        async move {
42            if msg.is_mine() || msg.text().trim().is_empty() {
43                return;
44            }
45
46            // Let the user see we're working while the model generates.
47            let _ = msg.channel().typing().await;
48
49            // Prompt = system + recent history for this conversation + the new turn.
50            let key = msg.chat_id.clone();
51            let mut messages = vec![json!({ "role": "system", "content": SYSTEM_PROMPT })];
52            if let Some(prior) = history.lock().unwrap().get(&key) {
53                messages.extend(prior.iter().cloned());
54            }
55            messages.push(json!({ "role": "user", "content": msg.text() }));
56
57            match ask_llm(&messages).await {
58                Ok(answer) => {
59                    let _ = msg.reply(&answer).await;
60                    // Remember both sides, trimmed to the last HISTORY_LIMIT messages.
61                    let mut store = history.lock().unwrap();
62                    let convo = store.entry(key).or_default();
63                    convo.push(json!({ "role": "user", "content": msg.text() }));
64                    convo.push(json!({ "role": "assistant", "content": answer }));
65                    let overflow = convo.len().saturating_sub(HISTORY_LIMIT);
66                    convo.drain(0..overflow);
67                }
68                Err(e) => {
69                    let _ = msg.reply(&format!("(LLM error: {e})")).await;
70                }
71            }
72        }
73    })
74    .await?;
75
76    Ok(())
77}
examples/concordia.rs (line 215)
54async fn main() -> vector_sdk::Result<()> {
55    // Identity: VECTOR_NSEC override, else the persisted <data_dir>/identity.nsec
56    // (created on first run by the builder). Public invite policy: any member can
57    // pull the bot into their community with a direct invite (v1 or v2).
58    let mut builder = VectorBot::builder().public();
59    if let Ok(nsec) = std::env::var("VECTOR_NSEC") {
60        builder = builder.nsec(nsec);
61    }
62    let bot = builder.build().await?;
63    println!("── {NAME} online as {}", bot.npub());
64
65    // Optional one-shot profile publish: upload the avatar plainly (avatars are
66    // public) and publish the kind-0 with the bot flag. Existing fields are
67    // carried forward by the profile pipeline, so this never clobbers. Deferred
68    // until after listen() has connected the community relays — a kind-0 that
69    // only reaches the login relays is invisible to community peers' clients.
70    if let Ok(avatar_path) = std::env::var("CONCORDIA_AVATAR") {
71        let bot = bot.clone();
72        tokio::spawn(async move {
73            tokio::time::sleep(std::time::Duration::from_secs(10)).await;
74            let avatar_path = shellexpand_home(&avatar_path);
75            println!("── uploading avatar {avatar_path}…");
76            match bot.core().upload_public_image(&avatar_path).await {
77                Ok(url) => {
78                    let ok = bot.core().update_bot_profile(NAME, &url, "", ABOUT).await;
79                    println!("── profile publish {}  avatar={url}", if ok { "✅" } else { "FAILED" });
80                    if ok {
81                        push_profile_to_communities().await;
82                    }
83                }
84                Err(e) => eprintln!("!! avatar upload failed: {e}"),
85            }
86        });
87    }
88
89    // Optional invite link: join on first run; later runs come up with the
90    // persisted memberships and need no arguments.
91    if let Some(invite) = std::env::args().nth(1).or_else(|| std::env::var("VECTOR_INVITE").ok()) {
92        println!("── joining via link…");
93        match bot.core().join_community(&invite).await {
94            Ok(summary) => {
95                let name = summary.get("name").and_then(|v| v.as_str()).unwrap_or("?");
96                let ver = summary.get("version").and_then(|v| v.as_u64()).unwrap_or(0);
97                println!("── joined \"{name}\"  protocol=v{ver}");
98            }
99            Err(e) => {
100                eprintln!("!! join failed: {e}");
101                return Err(e);
102            }
103        }
104    }
105
106    // Slash commands (Bot Interface Phase 1): registered here, published as the
107    // kind-10304 manifest at listen start, invoked by PLAIN TEXT from any client.
108    // Vector's `/` picker renders exactly this registry with argument hints; a
109    // matched invocation is consumed before the legacy !bang console below.
110    bot.command("help", "List everything Concordia can do").run(|ctx| async move {
111        let _ = ctx.reply(HELP).await;
112    });
113    bot.command("ping", "Round-trip latency check").run(|ctx| async move {
114        let _ = ctx.reply(format!("pong 🏓 ({} ms)", now_ms())).await;
115    });
116    bot.command("roll", "Roll a die")
117        .int("sides", "How many sides (default 6)", false)
118        .run(|ctx| async move {
119            let sides = ctx.int("sides").unwrap_or(6).clamp(2, 1_000_000);
120            let roll = (now_ms() as i64 % sides) + 1;
121            let _ = ctx.reply(format!("🎲 rolled {roll} on a d{sides}")).await;
122        });
123    bot.command("announce", "Format a two-part announcement")
124        .string("title", "Announcement title", true)
125        .string("body", "Announcement body", true)
126        .run(|ctx| async move {
127            let title = ctx.str("title").unwrap_or("(untitled)").to_string();
128            let body = ctx.str("body").unwrap_or_default().to_string();
129            let _ = ctx.reply(format!("📣 {title}\n{body}")).await;
130        });
131    // ── Param-type test battery: every ArgType, solo and mixed ──────────────
132    bot.command("typetest", "Echo back every param type, parsed")
133        .string("text", "Any free text", true)
134        .int("count", "A whole number", true)
135        .number("ratio", "A decimal number", true)
136        .flag("loud", "true or false", true)
137        .user("who", "Any user (npub)", true)
138        .choice("color", "Pick a color", ["red", "green", "blue"], true)
139        .run(|ctx| async move {
140            let report = format!(
141                "typetest received:\n\
142                 text   (String) = {:?}\n\
143                 count  (Int)    = {}\n\
144                 ratio  (Number) = {}\n\
145                 loud   (Bool)   = {}\n\
146                 who    (User)   = {}\n\
147                 color  (Choice) = {}",
148                ctx.str("text").unwrap_or_default(),
149                ctx.int("count").unwrap_or_default(),
150                ctx.number("ratio").unwrap_or_default(),
151                ctx.flag("loud").unwrap_or_default(),
152                ctx.str("who").unwrap_or_default(),
153                ctx.str("color").unwrap_or_default(),
154            );
155            let _ = ctx.reply(report).await;
156        });
157    bot.command("greet", "Greet a member in a chosen style")
158        .user("who", "Who to greet", true)
159        .choice("style", "Greeting style", ["formal", "casual", "pirate"], true)
160        .int("times", "Repeat 1-5 times (default 1)", false)
161        .run(|ctx| async move {
162            let who = ctx.str("who").unwrap_or_default().to_string();
163            let line = match ctx.str("style").unwrap_or("casual") {
164                "formal" => format!("Good day to you, {who}."),
165                "pirate" => format!("Ahoy, {who}! 🏴‍☠️"),
166                _ => format!("yo {who} 👋"),
167            };
168            let times = ctx.int("times").unwrap_or(1).clamp(1, 5) as usize;
169            let _ = ctx.reply(vec![line; times].join("\n")).await;
170        });
171    bot.command("calc", "Do some arithmetic")
172        .number("a", "First operand", true)
173        .choice("op", "Operation", ["add", "sub", "mul", "div"], true)
174        .number("b", "Second operand", true)
175        .run(|ctx| async move {
176            let a = ctx.number("a").unwrap_or_default();
177            let b = ctx.number("b").unwrap_or_default();
178            let answer = match ctx.str("op").unwrap_or_default() {
179                "add" => Some(a + b),
180                "sub" => Some(a - b),
181                "mul" => Some(a * b),
182                "div" if b != 0.0 => Some(a / b),
183                _ => None,
184            };
185            let _ = ctx
186                .reply(match answer {
187                    Some(v) => format!("🧮 {v}"),
188                    None => "cannot divide by zero".to_string(),
189                })
190                .await;
191        });
192
193    bot.command("reply", "Get a threaded reply to your message").run(|ctx| async move {
194        let _ = ctx.reply("this is a threaded reply ✅ (I quoted your message)").await;
195    });
196    bot.command("react", "React to your message")
197        .choice("emoji", "Which reaction", ["🔥", "👍", "❤️", "😂", "🫡"], false)
198        .run(|ctx| async move {
199            let emoji = ctx.str("emoji").unwrap_or("🔥").to_string();
200            log_err("react", ctx.msg.react(&emoji).await.map(|_| String::new()));
201        });
202    bot.command("edit", "Watch me send then edit a message").run(|ctx| async move {
203        let ch = ctx.msg.channel();
204        if let Ok(id) = ch.send("editing this in one second…").await {
205            log_err("edit", ch.edit(&id, "edited ✏️ (this text was changed)").await.map(|_| String::new()));
206        }
207    });
208    bot.command("delete", "Watch a message self-destruct").run(|ctx| async move {
209        let ch = ctx.msg.channel();
210        if let Ok(id) = ch.send("…this message will self-destruct").await {
211            log_err("delete", ch.delete(&id).await.map(|_| String::new()));
212        }
213    });
214    bot.command("typing", "Emit a typing indicator").run(|ctx| async move {
215        log_err("typing", ctx.msg.channel().typing().await.map(|_| String::new()));
216    });
217    bot.command("file", "Receive a small encrypted attachment").run(|ctx| async move {
218        send_test_file(&ctx.msg.channel()).await;
219    });
220    bot.command("members", "The folded member list").run(|ctx| async move {
221        if let Some(community) = ctx.msg.community() {
222            let members = community.members().await;
223            let list: Vec<String> = members.iter().map(|m| short(m.npub())).collect();
224            let _ = ctx.reply(format!("{} member(s): {}", members.len(), list.join(", "))).await;
225        } else {
226            let _ = ctx.reply("not in a community here").await;
227        }
228    });
229    bot.command("channels", "The channels I can see here").run(|ctx| async move {
230        diagnostics(&ctx.bot, &ctx.msg, "!channels").await;
231    });
232    bot.command("caps", "My capabilities here (roles engine)").run(|ctx| async move {
233        diagnostics(&ctx.bot, &ctx.msg, "!caps").await;
234    });
235    bot.command("roles", "The community roster").run(|ctx| async move {
236        diagnostics(&ctx.bot, &ctx.msg, "!roles").await;
237    });
238    bot.command("info", "Community protocol + ownership summary").run(|ctx| async move {
239        diagnostics(&ctx.bot, &ctx.msg, "!info").await;
240    });
241    bot.command("whoami", "My npub and this channel id").run(|ctx| async move {
242        diagnostics(&ctx.bot, &ctx.msg, "!whoami").await;
243    });
244    bot.command("reconnect", "Bounce my relay sockets (reconnect drill)").run(|ctx| async move {
245        let bounced = bounce_community_relays().await;
246        let _ = ctx.reply(format!("bounced {bounced} relay socket(s) — /ping to test the post-reconnect subscription")).await;
247    });
248
249    // The join snapshot folds only owner-authored channels; admin-created ones
250    // arrive on the first control follow, so re-print once that has landed.
251    print_channels(&bot, "channels visible (startup)").await;
252    {
253        let bot = bot.clone();
254        tokio::spawn(async move {
255            tokio::time::sleep(std::time::Duration::from_secs(25)).await;
256            print_channels(&bot, "channels visible (after control follow)").await;
257        });
258    }
259    println!("── listening. Message me `!help` from your client.\n");
260
261    bot.on_event(|bot, event| async move {
262        match event {
263            BotEvent::Message(msg) => {
264                if msg.is_mine() {
265                    return; // never react to our own sends
266                }
267                let author = short(msg.message.npub.as_deref().unwrap_or("?"));
268                let text = msg.text().trim().to_string();
269                println!("[MSG]    {author}: {text}");
270
271                // Command console — each arm fires a distinct SDK send-path.
272                let ch = msg.channel();
273                match text.as_str() {
274                    "!help" => reply(&msg, HELP).await,
275                    "!ping" => reply(&msg, &format!("pong 🏓 ({} ms)", now_ms())).await,
276                    "!reply" => reply(&msg, "this is a threaded reply ✅ (I quoted your message)").await,
277                    "!react" => log_err("react", msg.react("🔥").await.map(|_| String::new())),
278                    "!typing" => log_err("typing", ch.typing().await.map(|_| String::new())),
279                    "!edit" => {
280                        if let Ok(id) = ch.send("editing this in one second…").await {
281                            log_err("edit", ch.edit(&id, "edited ✏️ (this text was changed)").await.map(|_| String::new()));
282                        }
283                    }
284                    "!delete" => {
285                        if let Ok(id) = ch.send("…this message will self-destruct").await {
286                            log_err("delete", ch.delete(&id).await.map(|_| String::new()));
287                        }
288                    }
289                    "!file" => send_test_file(&ch).await,
290                    "!members" => {
291                        if let Some(community) = msg.community() {
292                            let members = community.members().await;
293                            let list: Vec<String> = members.iter().map(|m| short(m.npub())).collect();
294                            reply(&msg, &format!("{} member(s): {}", members.len(), list.join(", "))).await;
295                        } else {
296                            reply(&msg, "not in a community here").await;
297                        }
298                    }
299                    "!reconnect" => {
300                        // Ops/repro tool: bounce this bot's community relay sockets (a
301                        // relay restart or idle drop in miniature) — then a !ping proves
302                        // whether the subscription survived the fresh AUTH gate.
303                        let bounced = bounce_community_relays().await;
304                        reply(&msg, &format!("bounced {bounced} relay socket(s) — send !ping to test the post-reconnect subscription")).await;
305                    }
306                    "!channels" | "!info" | "!caps" | "!roles" | "!whoami" => diagnostics(&bot, &msg, &text).await,
307                    other if other.starts_with('!') => reply(&msg, &format!("unknown command `{other}` — try !help")).await,
308                    // Non-command chatter is logged above but never replied to, so
309                    // the bot can sit in a busy community without spamming echoes.
310                    _ => {}
311                }
312            }
313            BotEvent::MessageUpdate { message, .. } => {
314                println!("[UPDATE] {} → \"{}\"  ({} reaction(s))", short(message.npub.as_deref().unwrap_or("?")), message.content, message.reactions.len());
315            }
316            BotEvent::Delete { message_id, .. } => println!("[DELETE] message {}", short(&message_id)),
317            BotEvent::MemberJoin { npub, .. } => println!("[JOIN]   {}", short(&npub)),
318            BotEvent::MemberLeave { npub, .. } => println!("[LEAVE]  {}", short(&npub)),
319            BotEvent::Typing { npub, .. } => println!("[TYPING] {}", short(&npub)),
320            BotEvent::Invite { community_id } => println!("[INVITE] for community {}", short(&community_id)),
321            BotEvent::Removed { community_id } => println!("[REMOVED] from community {} — I was kicked/banned", short(&community_id)),
322        }
323    })
324    .await?;
325
326    Ok(())
327}
Source

pub async fn edit(&self, message_id: &str, new_content: &str) -> Result<()>

Edit a message you previously sent.

Examples found in repository?
examples/concordia.rs (line 205)
54async fn main() -> vector_sdk::Result<()> {
55    // Identity: VECTOR_NSEC override, else the persisted <data_dir>/identity.nsec
56    // (created on first run by the builder). Public invite policy: any member can
57    // pull the bot into their community with a direct invite (v1 or v2).
58    let mut builder = VectorBot::builder().public();
59    if let Ok(nsec) = std::env::var("VECTOR_NSEC") {
60        builder = builder.nsec(nsec);
61    }
62    let bot = builder.build().await?;
63    println!("── {NAME} online as {}", bot.npub());
64
65    // Optional one-shot profile publish: upload the avatar plainly (avatars are
66    // public) and publish the kind-0 with the bot flag. Existing fields are
67    // carried forward by the profile pipeline, so this never clobbers. Deferred
68    // until after listen() has connected the community relays — a kind-0 that
69    // only reaches the login relays is invisible to community peers' clients.
70    if let Ok(avatar_path) = std::env::var("CONCORDIA_AVATAR") {
71        let bot = bot.clone();
72        tokio::spawn(async move {
73            tokio::time::sleep(std::time::Duration::from_secs(10)).await;
74            let avatar_path = shellexpand_home(&avatar_path);
75            println!("── uploading avatar {avatar_path}…");
76            match bot.core().upload_public_image(&avatar_path).await {
77                Ok(url) => {
78                    let ok = bot.core().update_bot_profile(NAME, &url, "", ABOUT).await;
79                    println!("── profile publish {}  avatar={url}", if ok { "✅" } else { "FAILED" });
80                    if ok {
81                        push_profile_to_communities().await;
82                    }
83                }
84                Err(e) => eprintln!("!! avatar upload failed: {e}"),
85            }
86        });
87    }
88
89    // Optional invite link: join on first run; later runs come up with the
90    // persisted memberships and need no arguments.
91    if let Some(invite) = std::env::args().nth(1).or_else(|| std::env::var("VECTOR_INVITE").ok()) {
92        println!("── joining via link…");
93        match bot.core().join_community(&invite).await {
94            Ok(summary) => {
95                let name = summary.get("name").and_then(|v| v.as_str()).unwrap_or("?");
96                let ver = summary.get("version").and_then(|v| v.as_u64()).unwrap_or(0);
97                println!("── joined \"{name}\"  protocol=v{ver}");
98            }
99            Err(e) => {
100                eprintln!("!! join failed: {e}");
101                return Err(e);
102            }
103        }
104    }
105
106    // Slash commands (Bot Interface Phase 1): registered here, published as the
107    // kind-10304 manifest at listen start, invoked by PLAIN TEXT from any client.
108    // Vector's `/` picker renders exactly this registry with argument hints; a
109    // matched invocation is consumed before the legacy !bang console below.
110    bot.command("help", "List everything Concordia can do").run(|ctx| async move {
111        let _ = ctx.reply(HELP).await;
112    });
113    bot.command("ping", "Round-trip latency check").run(|ctx| async move {
114        let _ = ctx.reply(format!("pong 🏓 ({} ms)", now_ms())).await;
115    });
116    bot.command("roll", "Roll a die")
117        .int("sides", "How many sides (default 6)", false)
118        .run(|ctx| async move {
119            let sides = ctx.int("sides").unwrap_or(6).clamp(2, 1_000_000);
120            let roll = (now_ms() as i64 % sides) + 1;
121            let _ = ctx.reply(format!("🎲 rolled {roll} on a d{sides}")).await;
122        });
123    bot.command("announce", "Format a two-part announcement")
124        .string("title", "Announcement title", true)
125        .string("body", "Announcement body", true)
126        .run(|ctx| async move {
127            let title = ctx.str("title").unwrap_or("(untitled)").to_string();
128            let body = ctx.str("body").unwrap_or_default().to_string();
129            let _ = ctx.reply(format!("📣 {title}\n{body}")).await;
130        });
131    // ── Param-type test battery: every ArgType, solo and mixed ──────────────
132    bot.command("typetest", "Echo back every param type, parsed")
133        .string("text", "Any free text", true)
134        .int("count", "A whole number", true)
135        .number("ratio", "A decimal number", true)
136        .flag("loud", "true or false", true)
137        .user("who", "Any user (npub)", true)
138        .choice("color", "Pick a color", ["red", "green", "blue"], true)
139        .run(|ctx| async move {
140            let report = format!(
141                "typetest received:\n\
142                 text   (String) = {:?}\n\
143                 count  (Int)    = {}\n\
144                 ratio  (Number) = {}\n\
145                 loud   (Bool)   = {}\n\
146                 who    (User)   = {}\n\
147                 color  (Choice) = {}",
148                ctx.str("text").unwrap_or_default(),
149                ctx.int("count").unwrap_or_default(),
150                ctx.number("ratio").unwrap_or_default(),
151                ctx.flag("loud").unwrap_or_default(),
152                ctx.str("who").unwrap_or_default(),
153                ctx.str("color").unwrap_or_default(),
154            );
155            let _ = ctx.reply(report).await;
156        });
157    bot.command("greet", "Greet a member in a chosen style")
158        .user("who", "Who to greet", true)
159        .choice("style", "Greeting style", ["formal", "casual", "pirate"], true)
160        .int("times", "Repeat 1-5 times (default 1)", false)
161        .run(|ctx| async move {
162            let who = ctx.str("who").unwrap_or_default().to_string();
163            let line = match ctx.str("style").unwrap_or("casual") {
164                "formal" => format!("Good day to you, {who}."),
165                "pirate" => format!("Ahoy, {who}! 🏴‍☠️"),
166                _ => format!("yo {who} 👋"),
167            };
168            let times = ctx.int("times").unwrap_or(1).clamp(1, 5) as usize;
169            let _ = ctx.reply(vec![line; times].join("\n")).await;
170        });
171    bot.command("calc", "Do some arithmetic")
172        .number("a", "First operand", true)
173        .choice("op", "Operation", ["add", "sub", "mul", "div"], true)
174        .number("b", "Second operand", true)
175        .run(|ctx| async move {
176            let a = ctx.number("a").unwrap_or_default();
177            let b = ctx.number("b").unwrap_or_default();
178            let answer = match ctx.str("op").unwrap_or_default() {
179                "add" => Some(a + b),
180                "sub" => Some(a - b),
181                "mul" => Some(a * b),
182                "div" if b != 0.0 => Some(a / b),
183                _ => None,
184            };
185            let _ = ctx
186                .reply(match answer {
187                    Some(v) => format!("🧮 {v}"),
188                    None => "cannot divide by zero".to_string(),
189                })
190                .await;
191        });
192
193    bot.command("reply", "Get a threaded reply to your message").run(|ctx| async move {
194        let _ = ctx.reply("this is a threaded reply ✅ (I quoted your message)").await;
195    });
196    bot.command("react", "React to your message")
197        .choice("emoji", "Which reaction", ["🔥", "👍", "❤️", "😂", "🫡"], false)
198        .run(|ctx| async move {
199            let emoji = ctx.str("emoji").unwrap_or("🔥").to_string();
200            log_err("react", ctx.msg.react(&emoji).await.map(|_| String::new()));
201        });
202    bot.command("edit", "Watch me send then edit a message").run(|ctx| async move {
203        let ch = ctx.msg.channel();
204        if let Ok(id) = ch.send("editing this in one second…").await {
205            log_err("edit", ch.edit(&id, "edited ✏️ (this text was changed)").await.map(|_| String::new()));
206        }
207    });
208    bot.command("delete", "Watch a message self-destruct").run(|ctx| async move {
209        let ch = ctx.msg.channel();
210        if let Ok(id) = ch.send("…this message will self-destruct").await {
211            log_err("delete", ch.delete(&id).await.map(|_| String::new()));
212        }
213    });
214    bot.command("typing", "Emit a typing indicator").run(|ctx| async move {
215        log_err("typing", ctx.msg.channel().typing().await.map(|_| String::new()));
216    });
217    bot.command("file", "Receive a small encrypted attachment").run(|ctx| async move {
218        send_test_file(&ctx.msg.channel()).await;
219    });
220    bot.command("members", "The folded member list").run(|ctx| async move {
221        if let Some(community) = ctx.msg.community() {
222            let members = community.members().await;
223            let list: Vec<String> = members.iter().map(|m| short(m.npub())).collect();
224            let _ = ctx.reply(format!("{} member(s): {}", members.len(), list.join(", "))).await;
225        } else {
226            let _ = ctx.reply("not in a community here").await;
227        }
228    });
229    bot.command("channels", "The channels I can see here").run(|ctx| async move {
230        diagnostics(&ctx.bot, &ctx.msg, "!channels").await;
231    });
232    bot.command("caps", "My capabilities here (roles engine)").run(|ctx| async move {
233        diagnostics(&ctx.bot, &ctx.msg, "!caps").await;
234    });
235    bot.command("roles", "The community roster").run(|ctx| async move {
236        diagnostics(&ctx.bot, &ctx.msg, "!roles").await;
237    });
238    bot.command("info", "Community protocol + ownership summary").run(|ctx| async move {
239        diagnostics(&ctx.bot, &ctx.msg, "!info").await;
240    });
241    bot.command("whoami", "My npub and this channel id").run(|ctx| async move {
242        diagnostics(&ctx.bot, &ctx.msg, "!whoami").await;
243    });
244    bot.command("reconnect", "Bounce my relay sockets (reconnect drill)").run(|ctx| async move {
245        let bounced = bounce_community_relays().await;
246        let _ = ctx.reply(format!("bounced {bounced} relay socket(s) — /ping to test the post-reconnect subscription")).await;
247    });
248
249    // The join snapshot folds only owner-authored channels; admin-created ones
250    // arrive on the first control follow, so re-print once that has landed.
251    print_channels(&bot, "channels visible (startup)").await;
252    {
253        let bot = bot.clone();
254        tokio::spawn(async move {
255            tokio::time::sleep(std::time::Duration::from_secs(25)).await;
256            print_channels(&bot, "channels visible (after control follow)").await;
257        });
258    }
259    println!("── listening. Message me `!help` from your client.\n");
260
261    bot.on_event(|bot, event| async move {
262        match event {
263            BotEvent::Message(msg) => {
264                if msg.is_mine() {
265                    return; // never react to our own sends
266                }
267                let author = short(msg.message.npub.as_deref().unwrap_or("?"));
268                let text = msg.text().trim().to_string();
269                println!("[MSG]    {author}: {text}");
270
271                // Command console — each arm fires a distinct SDK send-path.
272                let ch = msg.channel();
273                match text.as_str() {
274                    "!help" => reply(&msg, HELP).await,
275                    "!ping" => reply(&msg, &format!("pong 🏓 ({} ms)", now_ms())).await,
276                    "!reply" => reply(&msg, "this is a threaded reply ✅ (I quoted your message)").await,
277                    "!react" => log_err("react", msg.react("🔥").await.map(|_| String::new())),
278                    "!typing" => log_err("typing", ch.typing().await.map(|_| String::new())),
279                    "!edit" => {
280                        if let Ok(id) = ch.send("editing this in one second…").await {
281                            log_err("edit", ch.edit(&id, "edited ✏️ (this text was changed)").await.map(|_| String::new()));
282                        }
283                    }
284                    "!delete" => {
285                        if let Ok(id) = ch.send("…this message will self-destruct").await {
286                            log_err("delete", ch.delete(&id).await.map(|_| String::new()));
287                        }
288                    }
289                    "!file" => send_test_file(&ch).await,
290                    "!members" => {
291                        if let Some(community) = msg.community() {
292                            let members = community.members().await;
293                            let list: Vec<String> = members.iter().map(|m| short(m.npub())).collect();
294                            reply(&msg, &format!("{} member(s): {}", members.len(), list.join(", "))).await;
295                        } else {
296                            reply(&msg, "not in a community here").await;
297                        }
298                    }
299                    "!reconnect" => {
300                        // Ops/repro tool: bounce this bot's community relay sockets (a
301                        // relay restart or idle drop in miniature) — then a !ping proves
302                        // whether the subscription survived the fresh AUTH gate.
303                        let bounced = bounce_community_relays().await;
304                        reply(&msg, &format!("bounced {bounced} relay socket(s) — send !ping to test the post-reconnect subscription")).await;
305                    }
306                    "!channels" | "!info" | "!caps" | "!roles" | "!whoami" => diagnostics(&bot, &msg, &text).await,
307                    other if other.starts_with('!') => reply(&msg, &format!("unknown command `{other}` — try !help")).await,
308                    // Non-command chatter is logged above but never replied to, so
309                    // the bot can sit in a busy community without spamming echoes.
310                    _ => {}
311                }
312            }
313            BotEvent::MessageUpdate { message, .. } => {
314                println!("[UPDATE] {} → \"{}\"  ({} reaction(s))", short(message.npub.as_deref().unwrap_or("?")), message.content, message.reactions.len());
315            }
316            BotEvent::Delete { message_id, .. } => println!("[DELETE] message {}", short(&message_id)),
317            BotEvent::MemberJoin { npub, .. } => println!("[JOIN]   {}", short(&npub)),
318            BotEvent::MemberLeave { npub, .. } => println!("[LEAVE]  {}", short(&npub)),
319            BotEvent::Typing { npub, .. } => println!("[TYPING] {}", short(&npub)),
320            BotEvent::Invite { community_id } => println!("[INVITE] for community {}", short(&community_id)),
321            BotEvent::Removed { community_id } => println!("[REMOVED] from community {} — I was kicked/banned", short(&community_id)),
322        }
323    })
324    .await?;
325
326    Ok(())
327}
Source

pub async fn delete(&self, message_id: &str) -> Result<()>

Delete a message you sent.

Examples found in repository?
examples/concordia.rs (line 211)
54async fn main() -> vector_sdk::Result<()> {
55    // Identity: VECTOR_NSEC override, else the persisted <data_dir>/identity.nsec
56    // (created on first run by the builder). Public invite policy: any member can
57    // pull the bot into their community with a direct invite (v1 or v2).
58    let mut builder = VectorBot::builder().public();
59    if let Ok(nsec) = std::env::var("VECTOR_NSEC") {
60        builder = builder.nsec(nsec);
61    }
62    let bot = builder.build().await?;
63    println!("── {NAME} online as {}", bot.npub());
64
65    // Optional one-shot profile publish: upload the avatar plainly (avatars are
66    // public) and publish the kind-0 with the bot flag. Existing fields are
67    // carried forward by the profile pipeline, so this never clobbers. Deferred
68    // until after listen() has connected the community relays — a kind-0 that
69    // only reaches the login relays is invisible to community peers' clients.
70    if let Ok(avatar_path) = std::env::var("CONCORDIA_AVATAR") {
71        let bot = bot.clone();
72        tokio::spawn(async move {
73            tokio::time::sleep(std::time::Duration::from_secs(10)).await;
74            let avatar_path = shellexpand_home(&avatar_path);
75            println!("── uploading avatar {avatar_path}…");
76            match bot.core().upload_public_image(&avatar_path).await {
77                Ok(url) => {
78                    let ok = bot.core().update_bot_profile(NAME, &url, "", ABOUT).await;
79                    println!("── profile publish {}  avatar={url}", if ok { "✅" } else { "FAILED" });
80                    if ok {
81                        push_profile_to_communities().await;
82                    }
83                }
84                Err(e) => eprintln!("!! avatar upload failed: {e}"),
85            }
86        });
87    }
88
89    // Optional invite link: join on first run; later runs come up with the
90    // persisted memberships and need no arguments.
91    if let Some(invite) = std::env::args().nth(1).or_else(|| std::env::var("VECTOR_INVITE").ok()) {
92        println!("── joining via link…");
93        match bot.core().join_community(&invite).await {
94            Ok(summary) => {
95                let name = summary.get("name").and_then(|v| v.as_str()).unwrap_or("?");
96                let ver = summary.get("version").and_then(|v| v.as_u64()).unwrap_or(0);
97                println!("── joined \"{name}\"  protocol=v{ver}");
98            }
99            Err(e) => {
100                eprintln!("!! join failed: {e}");
101                return Err(e);
102            }
103        }
104    }
105
106    // Slash commands (Bot Interface Phase 1): registered here, published as the
107    // kind-10304 manifest at listen start, invoked by PLAIN TEXT from any client.
108    // Vector's `/` picker renders exactly this registry with argument hints; a
109    // matched invocation is consumed before the legacy !bang console below.
110    bot.command("help", "List everything Concordia can do").run(|ctx| async move {
111        let _ = ctx.reply(HELP).await;
112    });
113    bot.command("ping", "Round-trip latency check").run(|ctx| async move {
114        let _ = ctx.reply(format!("pong 🏓 ({} ms)", now_ms())).await;
115    });
116    bot.command("roll", "Roll a die")
117        .int("sides", "How many sides (default 6)", false)
118        .run(|ctx| async move {
119            let sides = ctx.int("sides").unwrap_or(6).clamp(2, 1_000_000);
120            let roll = (now_ms() as i64 % sides) + 1;
121            let _ = ctx.reply(format!("🎲 rolled {roll} on a d{sides}")).await;
122        });
123    bot.command("announce", "Format a two-part announcement")
124        .string("title", "Announcement title", true)
125        .string("body", "Announcement body", true)
126        .run(|ctx| async move {
127            let title = ctx.str("title").unwrap_or("(untitled)").to_string();
128            let body = ctx.str("body").unwrap_or_default().to_string();
129            let _ = ctx.reply(format!("📣 {title}\n{body}")).await;
130        });
131    // ── Param-type test battery: every ArgType, solo and mixed ──────────────
132    bot.command("typetest", "Echo back every param type, parsed")
133        .string("text", "Any free text", true)
134        .int("count", "A whole number", true)
135        .number("ratio", "A decimal number", true)
136        .flag("loud", "true or false", true)
137        .user("who", "Any user (npub)", true)
138        .choice("color", "Pick a color", ["red", "green", "blue"], true)
139        .run(|ctx| async move {
140            let report = format!(
141                "typetest received:\n\
142                 text   (String) = {:?}\n\
143                 count  (Int)    = {}\n\
144                 ratio  (Number) = {}\n\
145                 loud   (Bool)   = {}\n\
146                 who    (User)   = {}\n\
147                 color  (Choice) = {}",
148                ctx.str("text").unwrap_or_default(),
149                ctx.int("count").unwrap_or_default(),
150                ctx.number("ratio").unwrap_or_default(),
151                ctx.flag("loud").unwrap_or_default(),
152                ctx.str("who").unwrap_or_default(),
153                ctx.str("color").unwrap_or_default(),
154            );
155            let _ = ctx.reply(report).await;
156        });
157    bot.command("greet", "Greet a member in a chosen style")
158        .user("who", "Who to greet", true)
159        .choice("style", "Greeting style", ["formal", "casual", "pirate"], true)
160        .int("times", "Repeat 1-5 times (default 1)", false)
161        .run(|ctx| async move {
162            let who = ctx.str("who").unwrap_or_default().to_string();
163            let line = match ctx.str("style").unwrap_or("casual") {
164                "formal" => format!("Good day to you, {who}."),
165                "pirate" => format!("Ahoy, {who}! 🏴‍☠️"),
166                _ => format!("yo {who} 👋"),
167            };
168            let times = ctx.int("times").unwrap_or(1).clamp(1, 5) as usize;
169            let _ = ctx.reply(vec![line; times].join("\n")).await;
170        });
171    bot.command("calc", "Do some arithmetic")
172        .number("a", "First operand", true)
173        .choice("op", "Operation", ["add", "sub", "mul", "div"], true)
174        .number("b", "Second operand", true)
175        .run(|ctx| async move {
176            let a = ctx.number("a").unwrap_or_default();
177            let b = ctx.number("b").unwrap_or_default();
178            let answer = match ctx.str("op").unwrap_or_default() {
179                "add" => Some(a + b),
180                "sub" => Some(a - b),
181                "mul" => Some(a * b),
182                "div" if b != 0.0 => Some(a / b),
183                _ => None,
184            };
185            let _ = ctx
186                .reply(match answer {
187                    Some(v) => format!("🧮 {v}"),
188                    None => "cannot divide by zero".to_string(),
189                })
190                .await;
191        });
192
193    bot.command("reply", "Get a threaded reply to your message").run(|ctx| async move {
194        let _ = ctx.reply("this is a threaded reply ✅ (I quoted your message)").await;
195    });
196    bot.command("react", "React to your message")
197        .choice("emoji", "Which reaction", ["🔥", "👍", "❤️", "😂", "🫡"], false)
198        .run(|ctx| async move {
199            let emoji = ctx.str("emoji").unwrap_or("🔥").to_string();
200            log_err("react", ctx.msg.react(&emoji).await.map(|_| String::new()));
201        });
202    bot.command("edit", "Watch me send then edit a message").run(|ctx| async move {
203        let ch = ctx.msg.channel();
204        if let Ok(id) = ch.send("editing this in one second…").await {
205            log_err("edit", ch.edit(&id, "edited ✏️ (this text was changed)").await.map(|_| String::new()));
206        }
207    });
208    bot.command("delete", "Watch a message self-destruct").run(|ctx| async move {
209        let ch = ctx.msg.channel();
210        if let Ok(id) = ch.send("…this message will self-destruct").await {
211            log_err("delete", ch.delete(&id).await.map(|_| String::new()));
212        }
213    });
214    bot.command("typing", "Emit a typing indicator").run(|ctx| async move {
215        log_err("typing", ctx.msg.channel().typing().await.map(|_| String::new()));
216    });
217    bot.command("file", "Receive a small encrypted attachment").run(|ctx| async move {
218        send_test_file(&ctx.msg.channel()).await;
219    });
220    bot.command("members", "The folded member list").run(|ctx| async move {
221        if let Some(community) = ctx.msg.community() {
222            let members = community.members().await;
223            let list: Vec<String> = members.iter().map(|m| short(m.npub())).collect();
224            let _ = ctx.reply(format!("{} member(s): {}", members.len(), list.join(", "))).await;
225        } else {
226            let _ = ctx.reply("not in a community here").await;
227        }
228    });
229    bot.command("channels", "The channels I can see here").run(|ctx| async move {
230        diagnostics(&ctx.bot, &ctx.msg, "!channels").await;
231    });
232    bot.command("caps", "My capabilities here (roles engine)").run(|ctx| async move {
233        diagnostics(&ctx.bot, &ctx.msg, "!caps").await;
234    });
235    bot.command("roles", "The community roster").run(|ctx| async move {
236        diagnostics(&ctx.bot, &ctx.msg, "!roles").await;
237    });
238    bot.command("info", "Community protocol + ownership summary").run(|ctx| async move {
239        diagnostics(&ctx.bot, &ctx.msg, "!info").await;
240    });
241    bot.command("whoami", "My npub and this channel id").run(|ctx| async move {
242        diagnostics(&ctx.bot, &ctx.msg, "!whoami").await;
243    });
244    bot.command("reconnect", "Bounce my relay sockets (reconnect drill)").run(|ctx| async move {
245        let bounced = bounce_community_relays().await;
246        let _ = ctx.reply(format!("bounced {bounced} relay socket(s) — /ping to test the post-reconnect subscription")).await;
247    });
248
249    // The join snapshot folds only owner-authored channels; admin-created ones
250    // arrive on the first control follow, so re-print once that has landed.
251    print_channels(&bot, "channels visible (startup)").await;
252    {
253        let bot = bot.clone();
254        tokio::spawn(async move {
255            tokio::time::sleep(std::time::Duration::from_secs(25)).await;
256            print_channels(&bot, "channels visible (after control follow)").await;
257        });
258    }
259    println!("── listening. Message me `!help` from your client.\n");
260
261    bot.on_event(|bot, event| async move {
262        match event {
263            BotEvent::Message(msg) => {
264                if msg.is_mine() {
265                    return; // never react to our own sends
266                }
267                let author = short(msg.message.npub.as_deref().unwrap_or("?"));
268                let text = msg.text().trim().to_string();
269                println!("[MSG]    {author}: {text}");
270
271                // Command console — each arm fires a distinct SDK send-path.
272                let ch = msg.channel();
273                match text.as_str() {
274                    "!help" => reply(&msg, HELP).await,
275                    "!ping" => reply(&msg, &format!("pong 🏓 ({} ms)", now_ms())).await,
276                    "!reply" => reply(&msg, "this is a threaded reply ✅ (I quoted your message)").await,
277                    "!react" => log_err("react", msg.react("🔥").await.map(|_| String::new())),
278                    "!typing" => log_err("typing", ch.typing().await.map(|_| String::new())),
279                    "!edit" => {
280                        if let Ok(id) = ch.send("editing this in one second…").await {
281                            log_err("edit", ch.edit(&id, "edited ✏️ (this text was changed)").await.map(|_| String::new()));
282                        }
283                    }
284                    "!delete" => {
285                        if let Ok(id) = ch.send("…this message will self-destruct").await {
286                            log_err("delete", ch.delete(&id).await.map(|_| String::new()));
287                        }
288                    }
289                    "!file" => send_test_file(&ch).await,
290                    "!members" => {
291                        if let Some(community) = msg.community() {
292                            let members = community.members().await;
293                            let list: Vec<String> = members.iter().map(|m| short(m.npub())).collect();
294                            reply(&msg, &format!("{} member(s): {}", members.len(), list.join(", "))).await;
295                        } else {
296                            reply(&msg, "not in a community here").await;
297                        }
298                    }
299                    "!reconnect" => {
300                        // Ops/repro tool: bounce this bot's community relay sockets (a
301                        // relay restart or idle drop in miniature) — then a !ping proves
302                        // whether the subscription survived the fresh AUTH gate.
303                        let bounced = bounce_community_relays().await;
304                        reply(&msg, &format!("bounced {bounced} relay socket(s) — send !ping to test the post-reconnect subscription")).await;
305                    }
306                    "!channels" | "!info" | "!caps" | "!roles" | "!whoami" => diagnostics(&bot, &msg, &text).await,
307                    other if other.starts_with('!') => reply(&msg, &format!("unknown command `{other}` — try !help")).await,
308                    // Non-command chatter is logged above but never replied to, so
309                    // the bot can sit in a busy community without spamming echoes.
310                    _ => {}
311                }
312            }
313            BotEvent::MessageUpdate { message, .. } => {
314                println!("[UPDATE] {} → \"{}\"  ({} reaction(s))", short(message.npub.as_deref().unwrap_or("?")), message.content, message.reactions.len());
315            }
316            BotEvent::Delete { message_id, .. } => println!("[DELETE] message {}", short(&message_id)),
317            BotEvent::MemberJoin { npub, .. } => println!("[JOIN]   {}", short(&npub)),
318            BotEvent::MemberLeave { npub, .. } => println!("[LEAVE]  {}", short(&npub)),
319            BotEvent::Typing { npub, .. } => println!("[TYPING] {}", short(&npub)),
320            BotEvent::Invite { community_id } => println!("[INVITE] for community {}", short(&community_id)),
321            BotEvent::Removed { community_id } => println!("[REMOVED] from community {} — I was kicked/banned", short(&community_id)),
322        }
323    })
324    .await?;
325
326    Ok(())
327}
Source

pub async fn send_file(&self, path: impl AsRef<Path>) -> Result<String>

Send a file from disk as an encrypted attachment — works for DMs and Community channels.

Examples found in repository?
examples/concordia.rs (line 448)
442async fn send_test_file(ch: &vector_sdk::Channel) {
443    let path = std::env::temp_dir().join(format!("concordia_{}.txt", now_ms()));
444    if let Err(e) = std::fs::write(&path, format!("Hello from {NAME}!\nsent at {} ms\n", now_ms())) {
445        eprintln!("!! temp file write failed: {e}");
446        return;
447    }
448    log_err("send_file", ch.send_file(&path).await.map(|_| String::new()));
449    let _ = std::fs::remove_file(&path);
450}
More examples
Hide additional examples
examples/file_bot.rs (line 28)
17async fn main() -> vector_sdk::Result<()> {
18    let nsec = std::env::var("VECTOR_NSEC").expect("set VECTOR_NSEC to your bot's nsec");
19    let target = std::env::var("VECTOR_TARGET").expect("set VECTOR_TARGET to a recipient npub");
20    let file = std::env::var("VECTOR_FILE").expect("set VECTOR_FILE to a file path");
21
22    let bot = VectorBot::builder().nsec(nsec).build().await?;
23    println!("Sending {} as {}", file, bot.npub());
24
25    // Give the relay connections a moment to come up before the first send.
26    tokio::time::sleep(Duration::from_secs(2)).await;
27
28    let event_id = bot.channel(&target).send_file(&file).await?;
29    println!("Sent — event id: {}", event_id);
30
31    Ok(())
32}

Trait Implementations§

Source§

impl Clone for Channel

Source§

fn clone(&self) -> Channel

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more