pub struct Member { /* private fields */ }Expand description
A handle to a member of a community — act on them directly. Obtained from
Community::member or IncomingMessage::member.
Implementations§
Source§impl Member
impl Member
Sourcepub fn npub(&self) -> &str
pub fn npub(&self) -> &str
This member’s npub.
Examples found in repository?
examples/moderation_bot.rs (line 49)
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}More examples
examples/concordia.rs (line 223)
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}Sourcepub fn community_id(&self) -> &str
pub fn community_id(&self) -> &str
The id of the community this handle is scoped to.
Sourcepub async fn kick(&self) -> Result<()>
pub async fn kick(&self) -> Result<()>
Cooperatively kick them (they can rejoin). Requires KICK + outranking them.
Sourcepub async fn ban(&self) -> Result<()>
pub async fn ban(&self) -> Result<()>
Ban them (terminal; in a private community this triggers a read-cut rekey). Requires BAN.
Examples found in repository?
examples/moderation_bot.rs (line 48)
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}Sourcepub async fn grant_admin(&self) -> Result<()>
pub async fn grant_admin(&self) -> Result<()>
Grant them the @admin role (requires MANAGE_ROLES).
Sourcepub async fn revoke_admin(&self) -> Result<()>
pub async fn revoke_admin(&self) -> Result<()>
Revoke their @admin role.
Sourcepub async fn profile(&self) -> Option<SlimProfile>
pub async fn profile(&self) -> Option<SlimProfile>
Fetch this member’s profile.
Sourcepub fn is_admin(&self) -> bool
pub fn is_admin(&self) -> bool
Whether this member is an admin (the owner counts as admin).
Examples found in repository?
examples/moderation_bot.rs (line 45)
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}Trait Implementations§
Auto Trait Implementations§
impl Freeze for Member
impl RefUnwindSafe for Member
impl Send for Member
impl Sync for Member
impl Unpin for Member
impl UnsafeUnpin for Member
impl UnwindSafe for Member
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
Source§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
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