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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
//! Provides types and functionality for [Relationships](https://discord.com/developers/docs/game-sdk/relationships)

pub mod events;
pub mod state;

use crate::{user::User, Error};
use serde::Deserialize;
#[cfg(test)]
use serde::Serialize;

#[derive(Copy, Clone, Debug, PartialEq, Eq, serde_repr::Deserialize_repr)]
#[cfg_attr(test, derive(serde_repr::Serialize_repr))]
#[repr(u8)]
pub enum RelationKind {
    /// User has no intrinsic relationship
    None = 0,
    /// User is a friend
    Friend = 1,
    /// User is blocked
    Blocked = 2,
    /// User has a pending incoming friend request to connected user
    PendingIncoming = 3,
    /// Current user has a pending outgoing friend request to user
    PendingOutgoing = 4,
    /// User is not friends, but interacts with current user often (frequency + recency)
    Implicit = 5,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
#[cfg_attr(test, derive(Serialize))]
#[serde(rename_all = "snake_case")]
pub enum RelationStatus {
    /// The user is offline
    Offline,
    /// The user is online and active
    Online,
    /// The user is online, but inactive
    Idle,
    /// The user has set their status to Do Not Disturb
    #[serde(rename = "dnd")]
    DoNotDisturb,
}

/// The start and end timestamp of the activity. These are unix timestamps.
///
/// [API docs](https://discord.com/developers/docs/game-sdk/activities#data-models-activitytimestamps-struct)
#[derive(Default, Clone, Debug, Deserialize)]
#[cfg_attr(test, derive(Serialize))]
pub struct RelationshipActivityTimestamps {
    #[serde(
        skip_serializing_if = "Option::is_none",
        with = "crate::util::datetime_opt",
        default
    )]
    pub start: Option<time::OffsetDateTime>,
    #[serde(
        skip_serializing_if = "Option::is_none",
        with = "crate::util::datetime_opt",
        default
    )]
    pub end: Option<time::OffsetDateTime>,
}

use crate::activity;

#[derive(Default, Clone, Debug, Deserialize)]
#[cfg_attr(test, derive(Serialize))]
pub struct RelationshipActivity {
    /// The unique identifier for the activity
    #[serde(skip_serializing_if = "Option::is_none")]
    pub session_id: Option<String>,
    /// The timestamp the activity was created
    #[serde(skip_serializing, with = "crate::util::datetime_opt")]
    pub created_at: Option<time::OffsetDateTime>,
    /// The player's current party status
    #[serde(skip_serializing_if = "Option::is_none")]
    pub state: Option<String>,
    /// What the player is currently doing
    #[serde(skip_serializing_if = "Option::is_none")]
    pub details: Option<String>,
    /// Helps create elapsed/remaining timestamps on a player's profile
    #[serde(skip_serializing_if = "Option::is_none")]
    pub timestamps: Option<RelationshipActivityTimestamps>,
    /// Assets to display on the player's profile
    #[serde(skip_serializing_if = "Option::is_none")]
    pub assets: Option<activity::Assets>,
    /// Information about the player's party
    #[serde(skip_serializing_if = "Option::is_none")]
    pub party: Option<activity::Party>,
    /// Secret passwords for joining and spectating the player's game
    #[serde(skip_serializing_if = "Option::is_none")]
    pub secrets: Option<activity::Secrets>,
    #[serde(rename = "type")]
    pub kind: activity::ActivityKind,
    #[serde(default)]
    /// Whether this activity is an instanced context, like a match
    pub instance: bool,
}

#[derive(Debug, Clone, Deserialize)]
#[cfg_attr(test, derive(Serialize))]
pub struct RelationshipPresence {
    pub status: RelationStatus,
    pub activity: Option<RelationshipActivity>,
}

#[derive(Debug, Clone, Deserialize)]
#[cfg_attr(test, derive(Serialize))]
pub struct Relationship {
    /// What kind of relationship it is
    #[serde(rename = "type")]
    pub kind: RelationKind,
    pub user: User,
    pub presence: RelationshipPresence,
}

impl crate::Discord {
    /// The regular Game SDK does not really expose this functionality directly,
    /// but rather exposed via the "on refresh" event as described in the
    /// [docs](https://discord.com/developers/docs/game-sdk/relationships#onrefresh).
    ///
    /// Basically, this method should be used to bootstrap the relationships for
    /// the current user, with updates to that list coming via the
    /// [`RelationshipUpdate`](crate::Event::RelationshipUpdate) event
    pub async fn get_relationships(&self) -> Result<Vec<Relationship>, Error> {
        let rx = self.send_rpc(crate::proto::CommandKind::GetRelationships, ())?;

        handle_response!(rx, crate::proto::Command::GetRelationships { relationships } => {
            Ok(relationships)
        })
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::{activity, proto::event};

    #[test]
    fn deserializes() {
        let event = r#"{"cmd":"DISPATCH","data":{"type":1,"user":{"id":"682969165652689005","username":"jake.shadle","discriminator":"7557","avatar":"15bbd75c8ee6610d045852e7ea998a35","bot":false,"flags":0,"premium_type":0},"presence":{"status":"online","activity":{"created_at":"1632819046295","id":"e92ece5eb4ce629","name":"Ark [dev debug]","timestamps":{"start":"1632819046199"},"type":0}}},"evt":"RELATIONSHIP_UPDATE","nonce":null}"#;

        let update: crate::proto::event::EventFrame =
            serde_json::from_str(event).expect("failed to deserialize");

        insta::assert_json_snapshot!(update);
    }

    #[test]
    fn serde() {
        let eve = event::EventFrame {
            inner: event::Event::RelationshipUpdate(std::sync::Arc::new(Relationship {
                kind: RelationKind::Friend,
                user: User {
                    id: crate::types::Snowflake(123414231424),
                    username: "name".to_owned(),
                    discriminator: Some(52),
                    avatar: Some(crate::user::Avatar([
                        0xf6, 0x2f, 0x2a, 0x75, 0x5c, 0xb1, 0x8c, 0x94, 0xdc, 0x5c, 0xda, 0x94,
                        0x44, 0x10, 0x24, 0xf1,
                    ])),
                    is_bot: false,
                },
                presence: RelationshipPresence {
                    status: RelationStatus::DoNotDisturb,
                    activity: Some(RelationshipActivity {
                        session_id: Some("6bb1ddaea510750e905615286709d632".to_owned()),
                        created_at: Some(crate::util::timestamp(1628629162447)),
                        assets: Some(activity::Assets {
                            large_image: Some(
                                "spotify:ab67616d0000b273d1e326d10706f3d8562d77f8".to_owned(),
                            ),
                            large_text: Some("To the Moon".to_owned()),
                            small_image: None,
                            small_text: None,
                        }),
                        details: Some("To the Moon".to_owned()),
                        instance: false,
                        kind: activity::ActivityKind::Listening,
                        party: Some(activity::Party {
                            id: "spotify: 216453179196440576".to_owned(),
                            size: None,
                            privacy: None,
                        }),
                        secrets: None,
                        state: Some("Rob Curly".to_owned()),
                        timestamps: Some(RelationshipActivityTimestamps {
                            start: Some(crate::util::timestamp(1628629161811)),
                            end: Some(crate::util::timestamp(1628629327961)),
                        }),
                    }),
                },
            })),
        };

        insta::assert_json_snapshot!(eve);
    }
}