this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(relationship): add inbox, help, groups commands

+219 -3
+87
src/commands/user/relationship/logic.rs
··· 7 7 pub created_new_group: bool, 8 8 } 9 9 10 + #[derive(Debug)] 11 + pub struct InviteInboxItem { 12 + pub relationship_id: i64, 13 + pub relationship_type: String, 14 + pub inviter_id: u64, 15 + pub created_at: i64, 16 + } 17 + 18 + #[derive(Debug)] 19 + pub struct GroupSummary { 20 + pub relationship_id: i64, 21 + pub relationship_type: String, 22 + pub member_ids: Vec<u64>, 23 + } 24 + 10 25 pub fn normalize_relationship_type(raw: &str) -> Result<String> { 11 26 let trimmed = raw.trim().to_lowercase(); 12 27 if trimmed.is_empty() { ··· 304 319 } 305 320 306 321 Ok(values) 322 + } 323 + 324 + pub fn list_pending_invites_for_user( 325 + conn: &Connection, 326 + guild_id: u64, 327 + user_id: u64, 328 + ) -> Result<Vec<InviteInboxItem>> { 329 + let mut stmt = conn.prepare( 330 + "SELECT i.relationship_id, r.relationship_type, i.inviter_id, i.created_at 331 + FROM relationship_invites i 332 + INNER JOIN relationships r ON r.id = i.relationship_id 333 + WHERE r.guild_id = ? 334 + AND i.invitee_id = ? 335 + AND i.status = 'pending' 336 + AND r.status = 'active' 337 + ORDER BY i.created_at ASC, i.relationship_id ASC", 338 + )?; 339 + 340 + let rows = stmt.query_map( 341 + params![guild_id.cast_signed(), user_id.cast_signed()], 342 + |row| { 343 + let relationship_id: i64 = row.get(0)?; 344 + let relationship_type: String = row.get(1)?; 345 + let inviter_id: i64 = row.get(2)?; 346 + let created_at: i64 = row.get(3)?; 347 + Ok(InviteInboxItem { 348 + relationship_id, 349 + relationship_type, 350 + inviter_id: inviter_id.cast_unsigned(), 351 + created_at, 352 + }) 353 + }, 354 + )?; 355 + 356 + let mut values = Vec::new(); 357 + for row in rows { 358 + values.push(row?); 359 + } 360 + 361 + Ok(values) 362 + } 363 + 364 + pub fn list_active_relationship_groups( 365 + conn: &Connection, 366 + guild_id: u64, 367 + ) -> Result<Vec<GroupSummary>> { 368 + let mut stmt = conn.prepare( 369 + "SELECT id, relationship_type 370 + FROM relationships 371 + WHERE guild_id = ? 372 + AND status = 'active' 373 + ORDER BY relationship_type ASC, id ASC", 374 + )?; 375 + 376 + let rows = stmt.query_map([guild_id.cast_signed()], |row| { 377 + let relationship_id: i64 = row.get(0)?; 378 + let relationship_type: String = row.get(1)?; 379 + Ok((relationship_id, relationship_type)) 380 + })?; 381 + 382 + let mut groups = Vec::new(); 383 + for row in rows { 384 + let (relationship_id, relationship_type) = row?; 385 + let member_ids = active_member_ids(conn, relationship_id)?; 386 + groups.push(GroupSummary { 387 + relationship_id, 388 + relationship_type, 389 + member_ids, 390 + }); 391 + } 392 + 393 + Ok(groups) 307 394 } 308 395 309 396 pub fn active_member_ids(conn: &Connection, relationship_id: i64) -> Result<Vec<u64>> {
+132 -3
src/commands/user/relationship/mod.rs
··· 10 10 use db::RELATIONSHIP_DB; 11 11 use logic::{ 12 12 accept_invite, active_member_ids, decline_invite, has_pending_invite, is_active_member, 13 - leave_relationship, list_active_relationships_for_user, normalize_relationship_type, 14 - resolve_relationship_for_make, shared_relationship_ids, try_create_invite, 13 + leave_relationship, list_active_relationship_groups, list_active_relationships_for_user, 14 + list_pending_invites_for_user, normalize_relationship_type, resolve_relationship_for_make, 15 + shared_relationship_ids, try_create_invite, 15 16 }; 16 17 use reply::safe_reply; 17 18 18 19 #[poise::command( 19 20 slash_command, 20 21 guild_only, 21 - subcommands("make", "accept", "decline", "end", "leave", "list") 22 + subcommands( 23 + "help", "make", "accept", "decline", "end", "leave", "list", "inbox", "groups" 24 + ) 22 25 )] 23 26 pub async fn relationship(_: Context<'_>) -> Result<()> { 27 + Ok(()) 28 + } 29 + 30 + /// Show how relationship commands work. 31 + #[poise::command(slash_command, guild_only)] 32 + pub async fn help(ctx: Context<'_>) -> Result<()> { 33 + let text = [ 34 + "relationshipdb guide :3", 35 + "", 36 + "**How work**", 37 + "- Relationship groups are identified by an ID (like `#12`).", 38 + "- Types are free-form like `marriage`, `friend`, `adopted-sibling`.", 39 + "- `make` sends an invite. The other user must `accept`.", 40 + "", 41 + "**How to use**", 42 + "1. `/relationship make relationship_type:marriage user:@kitten`", 43 + "2. Check invites with `/relationship inbox`", 44 + "3. Accept with `/relationship accept relationship_id:<id>`", 45 + "", 46 + "**Commands**", 47 + "- `/relationship make <type> <user> [relationship_id]`: create/invite.", 48 + "- `/relationship inbox`: show your pending invites.", 49 + "- `/relationship accept <relationship_id>`: join invited group.", 50 + "- `/relationship decline <relationship_id>`: decline invite.", 51 + "- `/relationship list [user]`: show active groups for a user.", 52 + "- `/relationship groups`: show all active groups in this server.", 53 + "- `/relationship end <type> <user>`: leave a shared group by type.", 54 + "- `/relationship leave <relationship_id>`: leave a group by ID.", 55 + "", 56 + "**Notes**", 57 + "- If `end` is ambiguous (multiple matches), use `leave` with an ID.", 58 + "- If a group drops below 2 active members, it auto-ends.", 59 + ] 60 + .join("\n"); 61 + 62 + ctx.send(safe_reply().content(text).ephemeral(true)).await?; 24 63 Ok(()) 25 64 } 26 65 ··· 431 470 ctx.send(safe_reply().content(lines.join("\n"))).await?; 432 471 Ok(()) 433 472 } 473 + 474 + /// Show your pending relationship invites. 475 + #[poise::command(slash_command, guild_only)] 476 + pub async fn inbox(ctx: Context<'_>) -> Result<()> { 477 + let Some(guild_id) = ctx.guild_id() else { 478 + ctx.send( 479 + safe_reply() 480 + .content("This command can only be used in a server.") 481 + .ephemeral(true), 482 + ) 483 + .await?; 484 + return Ok(()); 485 + }; 486 + 487 + let caller_id = ctx.author().id.get(); 488 + let invites = { 489 + let conn = RELATIONSHIP_DB.lock().unwrap(); 490 + list_pending_invites_for_user(&conn, guild_id.get(), caller_id)? 491 + }; 492 + 493 + if invites.is_empty() { 494 + ctx.send( 495 + safe_reply() 496 + .content("You have no pending relationship invites.") 497 + .ephemeral(true), 498 + ) 499 + .await?; 500 + return Ok(()); 501 + } 502 + 503 + let mut lines = vec!["**Your pending relationship invites**".to_string()]; 504 + for invite in invites { 505 + lines.push(format!( 506 + "- `#{}` `{}` from <@{}> (created <t:{}:R>)\n Accept: `/relationship accept relationship_id:{}` | Decline: `/relationship decline relationship_id:{}`", 507 + invite.relationship_id, 508 + invite.relationship_type, 509 + invite.inviter_id, 510 + invite.created_at, 511 + invite.relationship_id, 512 + invite.relationship_id 513 + )); 514 + } 515 + 516 + ctx.send(safe_reply().content(lines.join("\n")).ephemeral(true)) 517 + .await?; 518 + Ok(()) 519 + } 520 + 521 + /// Show all active relationship groups in this server. 522 + #[poise::command(slash_command, guild_only)] 523 + pub async fn groups(ctx: Context<'_>) -> Result<()> { 524 + let Some(guild_id) = ctx.guild_id() else { 525 + ctx.send( 526 + safe_reply() 527 + .content("This command can only be used in a server.") 528 + .ephemeral(true), 529 + ) 530 + .await?; 531 + return Ok(()); 532 + }; 533 + 534 + let groups = { 535 + let conn = RELATIONSHIP_DB.lock().unwrap(); 536 + list_active_relationship_groups(&conn, guild_id.get())? 537 + }; 538 + 539 + if groups.is_empty() { 540 + ctx.send(safe_reply().content("No active relationship groups exist in this server.")) 541 + .await?; 542 + return Ok(()); 543 + } 544 + 545 + let mut lines = vec!["**Active relationship groups in this server**".to_string()]; 546 + for group in groups { 547 + let members = group 548 + .member_ids 549 + .iter() 550 + .map(|id| format!("<@{id}>")) 551 + .collect::<Vec<_>>() 552 + .join(", "); 553 + 554 + lines.push(format!( 555 + "- `#{}` `{}`: {}", 556 + group.relationship_id, group.relationship_type, members 557 + )); 558 + } 559 + 560 + ctx.send(safe_reply().content(lines.join("\n"))).await?; 561 + Ok(()) 562 + }