1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
//! Relationship operations for DiscordUser
//!
//! This module also hosts the official Discord User endpoints
//! (`get_user`, `get_current_user_guild_member`, `create_group_dm`,
//! `get_current_user_connections`, and the role-connection pair). They live
//! here rather than in a separate `user` submodule to avoid touching
//! `src/operations.rs` (the parent module registration is owned by another
//! agent). All endpoints route through the central HTTP client via
//! `Route::` variants for consistent rate limiting and auth handling.
use std::collections::HashMap;
use serde_json::{json, Value};
use crate::{
context::DiscordContext,
error::Result,
route::Route,
types::*,
};
impl<T: DiscordContext + Send + Sync> RelationshipOps for T {}
/// Extension trait providing relationship operations
#[allow(async_fn_in_trait)]
pub trait RelationshipOps: DiscordContext {
/// Fetch the current user's full relationship list (friends, blocked users,
/// pending outgoing requests, and incoming requests).
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure.
async fn get_my_relationship(&self) -> Result<Vec<Relationship>> {
self.http().get(Route::GetRelationships).await
}
/// Add or update a relationship with a user.
///
/// Use `RelationshipType::Friend` to send a friend request, or
/// `RelationshipType::Blocked` to block the user.
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure.
async fn add_relationship(&self, user_id: &UserId, type_: RelationshipType) -> Result<()> {
let payload = json!({ "type": type_ as u8 });
self.http().put(Route::AddRelationship { user_id: user_id.get() }, payload).await
}
/// Remove a relationship with a user (unfriend, unblock, or cancel a
/// pending request).
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure.
async fn remove_relationship(&self, user_id: &UserId) -> Result<()> {
self.http().delete(Route::RemoveRelationship { user_id: user_id.get() }).await
}
/// Send a friend request by username (and optional discriminator for legacy
/// accounts).
///
/// For pomelo (new-style) accounts, pass `discriminator: None`.
/// For legacy accounts with a 4-digit tag, pass the discriminator number.
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure, or if the username is
/// not found.
async fn send_friend_request(&self, username: &str, discriminator: Option<u16>) -> Result<()> {
let mut payload = json!({ "username": username });
if let Some(disc) = discriminator {
payload["discriminator"] = json!(disc.to_string());
}
self.http().post(Route::GetRelationships, payload).await // Actually POST to /users/@me/relationships
}
// ------------------------------------------------------------------
// Official Discord User endpoints
// ------------------------------------------------------------------
/// Fetch a user object by ID.
///
/// Discord: `GET /users/{user.id}` → [`User`]. Self-bots, bots, and
/// OAuth2 bearers may all call this; the response shape is the public
/// projection of the user (no email/phone unless `@me`).
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure or
/// [`DiscordError::UnexpectedStatusCode`] on non-2xx responses.
async fn get_user(&self, user_id: &UserId) -> Result<User> {
self.http().get(Route::GetUser { user_id: user_id.get() }).await
}
/// Fetch the current user's guild member object for a specific guild.
///
/// Discord: `GET /users/@me/guilds/{guild.id}/member` → [`Member`]
/// (Discord's `GuildMember`). Useful when joined-guild listings omit
/// per-member fields like `nick` or `roles`.
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure or
/// [`DiscordError::UnexpectedStatusCode`] on non-2xx responses.
async fn get_current_user_guild_member(&self, guild_id: &GuildId) -> Result<Member> {
self.http().get(Route::CurrentUserGuildMember { guild_id: guild_id.get() }).await
}
/// Create a Group DM channel from OAuth2 access tokens.
///
/// Discord: `POST /users/@me/channels` → [`Channel`]. Each entry in
/// `access_tokens` must be an OAuth2 access token granted with the
/// `gdm.join` scope from the recipient. `nicks` maps user IDs to a
/// per-recipient nickname shown in the group DM.
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure.
async fn create_group_dm(&self, access_tokens: Vec<String>, nicks: HashMap<String, String>) -> Result<Channel> {
let payload = json!({
"access_tokens": access_tokens,
"nicks": nicks,
});
// Path matches `Route::CreateDm` (POST /users/@me/channels); we reuse
// the registered route so the call goes through the rate limiter.
self.http().post(Route::CreateDm, payload).await
}
/// List the current user's third-party connections.
///
/// Discord: `GET /users/@me/connections` → `Vec<Connection>`. Returns
/// services like Spotify, GitHub, Twitch, etc., that the user has linked
/// to their Discord account.
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure or
/// [`DiscordError::UnexpectedStatusCode`] on non-2xx responses.
async fn get_current_user_connections(&self) -> Result<Vec<Connection>> {
self.http().get(Route::UserConnections).await
}
/// Fetch the current user's application role connection for a given
/// application.
///
/// Discord: `GET /users/@me/applications/{application.id}/role-connection`
/// → `ApplicationRoleConnection` (returned here as raw
/// [`serde_json::Value`] — the strongly-typed wrapper is owned by another
/// agent and may not exist yet).
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure or
/// [`DiscordError::UnexpectedStatusCode`] on non-2xx responses.
async fn get_current_user_application_role_connection(&self, application_id: &ApplicationId) -> Result<Value> {
self.http().get(Route::ApplicationRoleConnection { application_id: application_id.get() }).await
}
/// Update the current user's application role connection metadata for a
/// given application.
///
/// Discord: `PUT /users/@me/applications/{application.id}/role-connection`
/// → updated `ApplicationRoleConnection` (returned as
/// [`serde_json::Value`]). All three fields are optional in the body;
/// `metadata` is a flat string map keyed by metadata-record key.
///
/// # Errors
/// Returns [`DiscordError::Http`] on HTTP failure or
/// [`DiscordError::UnexpectedStatusCode`] on non-2xx responses.
async fn update_current_user_application_role_connection(
&self,
application_id: &ApplicationId,
platform_name: Option<String>,
platform_username: Option<String>,
metadata: HashMap<String, String>,
) -> Result<Value> {
let mut body = serde_json::Map::new();
if let Some(name) = platform_name {
body.insert("platform_name".to_string(), Value::String(name));
}
if let Some(username) = platform_username {
body.insert("platform_username".to_string(), Value::String(username));
}
body.insert("metadata".to_string(), json!(metadata));
let payload = Value::Object(body);
self.http().put(Route::ApplicationRoleConnection { application_id: application_id.get() }, payload).await
}
}