discord_sdk/
lib.rs

1#![doc = include_str!("../README.md")]
2
3#[macro_use]
4mod util;
5pub mod activity;
6pub mod error;
7mod handler;
8mod io;
9pub mod overlay;
10mod proto;
11pub mod registration;
12pub mod relations;
13mod types;
14pub mod user;
15
16pub use error::{DiscordApiErr, DiscordErr, Error};
17pub use handler::{handlers, wheel, DiscordHandler, DiscordMsg};
18pub use proto::event::Event;
19use proto::{Command, CommandKind};
20pub use time::OffsetDateTime;
21pub use types::Snowflake;
22pub type AppId = i64;
23
24pub use crossbeam_channel as cc;
25use parking_lot::Mutex;
26use std::sync::Arc;
27
28/// The details on the [Application](https://discord.com/developers/docs/game-sdk/sdk-starter-guide#get-set-up)
29/// you've created in Discord.
30pub enum DiscordApp {
31    /// Registers this application with Discord so that Discord can launch it
32    /// to eg. join another user's game
33    Register(registration::Application),
34    /// The unique application id. Note that Discord will not be able launch
35    /// this application when this variant is used, unless you've registered it
36    /// in some other way
37    PlainId(AppId),
38}
39
40impl From<AppId> for DiscordApp {
41    fn from(id: AppId) -> Self {
42        Self::PlainId(id)
43    }
44}
45
46impl From<registration::Application> for DiscordApp {
47    fn from(app: registration::Application) -> Self {
48        Self::Register(app)
49    }
50}
51
52bitflags::bitflags! {
53    #[derive(Copy, Clone)]
54    pub struct Subscriptions: u32 {
55        const ACTIVITY = 0x1;
56        const USER = 0x4;
57        const OVERLAY = 0x8;
58        const RELATIONSHIPS = 0x10;
59
60        const ALL = Self::ACTIVITY.bits() | Self::USER.bits() | Self::OVERLAY.bits() | Self::RELATIONSHIPS.bits();
61    }
62}
63
64pub struct Discord {
65    nonce: std::sync::atomic::AtomicUsize,
66    /// Queue for messages to be sent to Discord
67    send_queue: cc::Sender<Option<Vec<u8>>>,
68    /// The handle to the task actually driving the I/O with Discord
69    io_task: tokio::task::JoinHandle<()>,
70    /// The handle to the task dispatching messages to the [`DiscordHandler`]
71    handler_task: tokio::task::JoinHandle<()>,
72    state: State,
73}
74
75impl Discord {
76    /// Creates a new Discord connection for the specified application, providing
77    /// a [`DiscordHandler`] which can handle events as they arrive from Discord
78    pub fn new(
79        app: impl Into<DiscordApp>,
80        subscriptions: Subscriptions,
81        handler: Box<dyn DiscordHandler>,
82    ) -> Result<Self, Error> {
83        let app_id = match app.into() {
84            DiscordApp::PlainId(id) => id,
85            DiscordApp::Register(inner) => {
86                let id = inner.id;
87                registration::register_app(inner)?;
88                id
89            }
90        };
91
92        let io_task = io::start_io_task(app_id);
93
94        let state = State::default();
95
96        let handler_task = handler::handler_task(
97            handler,
98            subscriptions,
99            io_task.stx.clone(),
100            io_task.rrx,
101            state.clone(),
102        );
103
104        Ok(Self {
105            nonce: std::sync::atomic::AtomicUsize::new(1),
106            send_queue: io_task.stx,
107            io_task: io_task.handle,
108            handler_task,
109            state,
110        })
111    }
112
113    /// Disconnects from Discord, shutting down the tasks that have been created
114    /// to handle sending and receiving messages from it.
115    pub async fn disconnect(self) {
116        let _ = self.send_queue.send(None);
117        let _ = self.io_task.await;
118        let _ = self.handler_task.await;
119    }
120
121    /// Serializes an RPC ands adds a notification oneshot so that we can be notified
122    /// with the response from Discord
123    fn send_rpc<Msg>(
124        &self,
125        cmd: CommandKind,
126        msg: Msg,
127    ) -> Result<tokio::sync::oneshot::Receiver<Result<Command, Error>>, Error>
128    where
129        Msg: serde::Serialize,
130    {
131        // Increment the nonce, we use this in the handler task to pair the response
132        // to this request
133        let nonce = self
134            .nonce
135            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
136
137        let rpc = proto::Rpc {
138            cmd,
139            args: Some(msg),
140            nonce: nonce.to_string(),
141            evt: None,
142        };
143
144        let (tx, rx) = tokio::sync::oneshot::channel();
145
146        self.state
147            .notify_queue
148            .lock()
149            .push(NotifyItem { nonce, tx, cmd });
150
151        let mut buffer = Vec::with_capacity(128);
152        io::serialize_message(io::OpCode::Frame, &rpc, &mut buffer)?;
153        self.send_queue.send(Some(buffer))?;
154
155        Ok(rx)
156    }
157}
158
159pub(crate) struct NotifyItem {
160    /// The nonce we sent on the original request, the nonce in the response
161    /// will be used to match this and remove it from the queue
162    pub(crate) nonce: usize,
163    /// The channel used to communicate back to the original caller of the RPC
164    pub(crate) tx: tokio::sync::oneshot::Sender<Result<Command, Error>>,
165    /// The expected command kind of the response, this is used to sanity check
166    /// that Discord doesn't send us a response with a nonce that matches a
167    /// different command
168    pub(crate) cmd: CommandKind,
169}
170
171/// State shared between the top level [`Discord`] object and the handler task
172#[derive(Clone)]
173pub(crate) struct State {
174    /// Queue of RPCs sent to Discord that are awaiting a response
175    notify_queue: Arc<Mutex<Vec<NotifyItem>>>,
176}
177
178impl Default for State {
179    fn default() -> Self {
180        Self {
181            notify_queue: Arc::new(Mutex::new(Vec::new())),
182        }
183    }
184}