@shieldbattery/danimoth 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +15 -0
- package/.gitattributes +25 -0
- package/.prettierignore +1 -0
- package/.prettierrc +10 -0
- package/AGENTS.md +25 -0
- package/LICENSE +21 -0
- package/README.md +98 -2
- package/index.d.ts +229 -0
- package/index.js +1 -0
- package/native/Cargo.lock +180 -0
- package/native/Cargo.toml +16 -0
- package/native/src/lib.rs +673 -0
- package/native/src/names.rs +103 -0
- package/native/src/sys.rs +364 -0
- package/package.json +35 -4
- package/prebuilds/x64.node +0 -0
- package/tag-prebuild.js +16 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
mod names;
|
|
2
|
+
mod sys;
|
|
3
|
+
|
|
4
|
+
use std::cell::RefCell;
|
|
5
|
+
use std::ffi::c_void;
|
|
6
|
+
use std::path::Path;
|
|
7
|
+
use std::sync::{Arc, OnceLock};
|
|
8
|
+
|
|
9
|
+
use neon::prelude::*;
|
|
10
|
+
use neon::types::Deferred;
|
|
11
|
+
use neon::types::function::TryIntoArguments;
|
|
12
|
+
|
|
13
|
+
use names::*;
|
|
14
|
+
use sys::*;
|
|
15
|
+
|
|
16
|
+
static SDK: OnceLock<Sdk> = OnceLock::new();
|
|
17
|
+
|
|
18
|
+
fn sdk(cx: &mut Cx) -> NeonResult<&'static Sdk> {
|
|
19
|
+
match SDK.get() {
|
|
20
|
+
Some(sdk) => Ok(sdk),
|
|
21
|
+
None => cx.throw_error("The Discord Social SDK isn't loaded, call loadSdk first"),
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Returns the version of this module, so a caller can tell which build of it was loaded.
|
|
26
|
+
fn version(mut cx: FunctionContext) -> JsResult<JsString> {
|
|
27
|
+
Ok(cx.string(env!("CARGO_PKG_VERSION")))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fn load_sdk(mut cx: FunctionContext) -> JsResult<JsUndefined> {
|
|
31
|
+
let path = cx.argument::<JsString>(0)?.value(&mut cx);
|
|
32
|
+
if SDK.get().is_none() {
|
|
33
|
+
match unsafe { Sdk::load(Path::new(&path)) } {
|
|
34
|
+
Ok(sdk) => {
|
|
35
|
+
let _ = SDK.set(sdk);
|
|
36
|
+
}
|
|
37
|
+
Err(e) => {
|
|
38
|
+
return cx.throw_error(format!(
|
|
39
|
+
"Failed to load the Discord Social SDK from {path}: {e}"
|
|
40
|
+
));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
Ok(cx.undefined())
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fn sdk_version(mut cx: FunctionContext) -> JsResult<JsObject> {
|
|
48
|
+
let sdk = sdk(&mut cx)?;
|
|
49
|
+
let (major, minor, patch, hash) = unsafe {
|
|
50
|
+
(
|
|
51
|
+
(sdk.Discord_Client_GetVersionMajor)(),
|
|
52
|
+
(sdk.Discord_Client_GetVersionMinor)(),
|
|
53
|
+
(sdk.Discord_Client_GetVersionPatch)(),
|
|
54
|
+
sdk.get_string(|s| (sdk.Discord_Client_GetVersionHash)(s)),
|
|
55
|
+
)
|
|
56
|
+
};
|
|
57
|
+
let version = cx.empty_object();
|
|
58
|
+
version.prop(&mut cx, "major").set(major)?;
|
|
59
|
+
version.prop(&mut cx, "minor").set(minor)?;
|
|
60
|
+
version.prop(&mut cx, "patch").set(patch)?;
|
|
61
|
+
version.prop(&mut cx, "hash").set(hash)?;
|
|
62
|
+
Ok(version)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fn run_callbacks(mut cx: FunctionContext) -> JsResult<JsUndefined> {
|
|
66
|
+
let sdk = sdk(&mut cx)?;
|
|
67
|
+
unsafe { (sdk.Discord_RunCallbacks)() };
|
|
68
|
+
Ok(cx.undefined())
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// A JS function the SDK calls back into. Calls are queued to the JS thread through `channel`, so
|
|
72
|
+
/// it doesn't matter which thread the SDK calls from.
|
|
73
|
+
struct JsCallback {
|
|
74
|
+
channel: Channel,
|
|
75
|
+
function: Arc<Root<JsFunction>>,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
impl JsCallback {
|
|
79
|
+
/// Wraps `function` and returns it as user data for an SDK callback setter, to be paired with
|
|
80
|
+
/// `free_js_callback`.
|
|
81
|
+
fn into_user_data(cx: &mut Cx, function: Handle<JsFunction>) -> *mut c_void {
|
|
82
|
+
let mut channel = cx.channel();
|
|
83
|
+
// The app keeps its client until it exits, so the callbacks must not hold the process open.
|
|
84
|
+
channel.unref(cx);
|
|
85
|
+
let callback = Self {
|
|
86
|
+
channel,
|
|
87
|
+
function: Arc::new(function.root(cx)),
|
|
88
|
+
};
|
|
89
|
+
Box::into_raw(Box::new(callback)) as *mut c_void
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// # Safety
|
|
93
|
+
///
|
|
94
|
+
/// `user_data` must come from `into_user_data` and not have been freed.
|
|
95
|
+
unsafe fn from_user_data<'a>(user_data: *mut c_void) -> &'a Self {
|
|
96
|
+
unsafe { &*(user_data as *const Self) }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fn call<A>(&self, args: A)
|
|
100
|
+
where
|
|
101
|
+
A: for<'cx> TryIntoArguments<'cx> + Send + 'static,
|
|
102
|
+
{
|
|
103
|
+
let function = self.function.clone();
|
|
104
|
+
// This only fails once the JS thread is shutting down, when there's no one left to call.
|
|
105
|
+
let _ = self.channel.try_send(move |mut cx| {
|
|
106
|
+
let function = function.to_inner(&mut cx);
|
|
107
|
+
function.bind(&mut cx).args(args)?.exec()
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
unsafe extern "C" fn free_js_callback(user_data: *mut c_void) {
|
|
113
|
+
drop(unsafe { Box::from_raw(user_data as *mut JsCallback) });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
unsafe extern "C" fn log_callback(message: Discord_String, severity: i32, user_data: *mut c_void) {
|
|
117
|
+
let Some(sdk) = SDK.get() else { return };
|
|
118
|
+
let message = unsafe { sdk.take_string(message) };
|
|
119
|
+
let callback = unsafe { JsCallback::from_user_data(user_data) };
|
|
120
|
+
callback.call((message, log_severity_name(severity)));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
unsafe extern "C" fn status_changed_callback(
|
|
124
|
+
status: i32,
|
|
125
|
+
error: i32,
|
|
126
|
+
error_detail: i32,
|
|
127
|
+
user_data: *mut c_void,
|
|
128
|
+
) {
|
|
129
|
+
let callback = unsafe { JsCallback::from_user_data(user_data) };
|
|
130
|
+
callback.call((
|
|
131
|
+
client_status_name(status),
|
|
132
|
+
client_error_name(error),
|
|
133
|
+
error_detail,
|
|
134
|
+
));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
unsafe extern "C" fn token_expiration_callback(user_data: *mut c_void) {
|
|
138
|
+
let callback = unsafe { JsCallback::from_user_data(user_data) };
|
|
139
|
+
// Neon can't call with an empty argument list here (`()` isn't `TryIntoArguments`), so this
|
|
140
|
+
// passes a single `undefined`, which looks the same to a callback that takes no arguments.
|
|
141
|
+
callback.call(((),));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
unsafe extern "C" fn activity_join_callback(join_secret: Discord_String, user_data: *mut c_void) {
|
|
145
|
+
let Some(sdk) = SDK.get() else { return };
|
|
146
|
+
let join_secret = unsafe { sdk.take_string(join_secret) };
|
|
147
|
+
let callback = unsafe { JsCallback::from_user_data(user_data) };
|
|
148
|
+
callback.call((join_secret,));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Why an SDK call failed, copied out of its `Discord_ClientResult`.
|
|
152
|
+
struct CallError {
|
|
153
|
+
message: String,
|
|
154
|
+
error_type: &'static str,
|
|
155
|
+
error_code: i32,
|
|
156
|
+
retryable: bool,
|
|
157
|
+
retry_after: f32,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
impl CallError {
|
|
161
|
+
fn to_js<'cx>(&self, cx: &mut Cx<'cx>) -> JsResult<'cx, JsError> {
|
|
162
|
+
let error = cx.error(&self.message)?;
|
|
163
|
+
error.prop(cx, "errorType").set(self.error_type)?;
|
|
164
|
+
error.prop(cx, "errorCode").set(self.error_code)?;
|
|
165
|
+
error.prop(cx, "retryable").set(self.retryable)?;
|
|
166
|
+
error.prop(cx, "retryAfter").set(self.retry_after)?;
|
|
167
|
+
Ok(error)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// Copies the outcome out of a result the SDK handed to a callback, and drops the result.
|
|
172
|
+
///
|
|
173
|
+
/// # Safety
|
|
174
|
+
///
|
|
175
|
+
/// `result` must be a result the SDK handed to a callback, which hasn't been dropped yet.
|
|
176
|
+
unsafe fn take_result(sdk: &Sdk, result: *mut Discord_ClientResult) -> Result<(), CallError> {
|
|
177
|
+
// Callbacks own the result they're given: copy the handle and drop it through the copy.
|
|
178
|
+
let mut result =
|
|
179
|
+
unsafe { Owned::from_raw(std::ptr::read(result), sdk.Discord_ClientResult_Drop) };
|
|
180
|
+
let ptr = result.as_ptr();
|
|
181
|
+
unsafe {
|
|
182
|
+
if (sdk.Discord_ClientResult_Successful)(ptr) {
|
|
183
|
+
return Ok(());
|
|
184
|
+
}
|
|
185
|
+
let mut message = sdk.get_string(|s| (sdk.Discord_ClientResult_Error)(ptr, s));
|
|
186
|
+
if message.is_empty() {
|
|
187
|
+
message = sdk.get_string(|s| (sdk.Discord_ClientResult_ToString)(ptr, s));
|
|
188
|
+
}
|
|
189
|
+
Err(CallError {
|
|
190
|
+
message,
|
|
191
|
+
error_type: error_type_name((sdk.Discord_ClientResult_Type)(ptr)),
|
|
192
|
+
error_code: (sdk.Discord_ClientResult_ErrorCode)(ptr),
|
|
193
|
+
retryable: (sdk.Discord_ClientResult_Retryable)(ptr),
|
|
194
|
+
retry_after: (sdk.Discord_ClientResult_RetryAfter)(ptr),
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// An SDK call whose completion settles a JS promise. Until then its channel holds the process open.
|
|
200
|
+
struct PendingCall {
|
|
201
|
+
unsettled: Option<(Channel, Deferred)>,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
impl PendingCall {
|
|
205
|
+
/// Returns the promise for a call, and the user data to pass to the SDK along with
|
|
206
|
+
/// `call_result_callback` and `free_pending_call`.
|
|
207
|
+
fn start<'cx>(cx: &mut Cx<'cx>) -> (Handle<'cx, JsPromise>, *mut c_void) {
|
|
208
|
+
let (deferred, promise) = cx.promise();
|
|
209
|
+
let pending = Box::new(PendingCall {
|
|
210
|
+
unsettled: Some((cx.channel(), deferred)),
|
|
211
|
+
});
|
|
212
|
+
(promise, Box::into_raw(pending) as *mut c_void)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
fn settle(&mut self, outcome: Result<(), CallError>) {
|
|
216
|
+
// The SDK frees the user data (and so the channel) only during a later `runCallbacks`, so
|
|
217
|
+
// the channel is dropped here instead, to stop holding the process open once settled.
|
|
218
|
+
let Some((channel, deferred)) = self.unsettled.take() else {
|
|
219
|
+
return;
|
|
220
|
+
};
|
|
221
|
+
// This only fails once the JS thread is shutting down, when no one is awaiting the promise.
|
|
222
|
+
let _ = deferred.try_settle_with(&channel, move |mut cx| match outcome {
|
|
223
|
+
Ok(()) => Ok(cx.undefined()),
|
|
224
|
+
Err(e) => {
|
|
225
|
+
let error = e.to_js(&mut cx)?;
|
|
226
|
+
cx.throw(error)
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
unsafe extern "C" fn call_result_callback(
|
|
233
|
+
result: *mut Discord_ClientResult,
|
|
234
|
+
user_data: *mut c_void,
|
|
235
|
+
) {
|
|
236
|
+
let Some(sdk) = SDK.get() else { return };
|
|
237
|
+
let outcome = unsafe { take_result(sdk, result) };
|
|
238
|
+
let pending = unsafe { &mut *(user_data as *mut PendingCall) };
|
|
239
|
+
pending.settle(outcome);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
unsafe extern "C" fn free_pending_call(user_data: *mut c_void) {
|
|
243
|
+
let mut pending = unsafe { Box::from_raw(user_data as *mut PendingCall) };
|
|
244
|
+
// The SDK frees a call's user data after running its callback, or without running it when the
|
|
245
|
+
// client is dropped first. Only the latter leaves the promise unsettled.
|
|
246
|
+
pending.settle(Err(CallError {
|
|
247
|
+
message: "The Discord client was destroyed before the call completed".into(),
|
|
248
|
+
error_type: error_type_name(Discord_ErrorType_ClientDestroyed),
|
|
249
|
+
error_code: 0,
|
|
250
|
+
retryable: false,
|
|
251
|
+
retry_after: 0.0,
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/// A Discord Social SDK client, handed to JS as an opaque handle.
|
|
256
|
+
struct Client {
|
|
257
|
+
raw: RefCell<Option<Owned<Discord_Client>>>,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
impl Finalize for Client {}
|
|
261
|
+
|
|
262
|
+
impl Client {
|
|
263
|
+
/// Runs `f` with the SDK and this client's handle, throwing if the client was destroyed.
|
|
264
|
+
///
|
|
265
|
+
/// The client stays borrowed while `f` runs, so `f` must not run JS (e.g. by reading properties
|
|
266
|
+
/// of a JS object, which may be getters): JS could call back in with the same client.
|
|
267
|
+
fn with<'cx, R>(
|
|
268
|
+
&self,
|
|
269
|
+
cx: &mut Cx<'cx>,
|
|
270
|
+
f: impl FnOnce(&mut Cx<'cx>, &'static Sdk, *mut Discord_Client) -> NeonResult<R>,
|
|
271
|
+
) -> NeonResult<R> {
|
|
272
|
+
let sdk = sdk(cx)?;
|
|
273
|
+
let mut raw = self.raw.borrow_mut();
|
|
274
|
+
let Some(raw) = raw.as_mut() else {
|
|
275
|
+
return cx.throw_error("This Discord client was destroyed");
|
|
276
|
+
};
|
|
277
|
+
f(cx, sdk, raw.as_ptr())
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
fn create_client(mut cx: FunctionContext) -> JsResult<JsBox<Client>> {
|
|
282
|
+
let options = cx.argument::<JsObject>(0)?;
|
|
283
|
+
let sdk = sdk(&mut cx)?;
|
|
284
|
+
|
|
285
|
+
let on_log = options
|
|
286
|
+
.prop(&mut cx, "onLog")
|
|
287
|
+
.get::<Option<Handle<JsFunction>>>()?;
|
|
288
|
+
let log_severity = match options
|
|
289
|
+
.prop(&mut cx, "logSeverity")
|
|
290
|
+
.get::<Option<String>>()?
|
|
291
|
+
{
|
|
292
|
+
Some(name) => parse_log_severity(&mut cx, &name)?,
|
|
293
|
+
None => Discord_LoggingSeverity_Info,
|
|
294
|
+
};
|
|
295
|
+
let on_status_changed = options
|
|
296
|
+
.prop(&mut cx, "onStatusChanged")
|
|
297
|
+
.get::<Option<Handle<JsFunction>>>()?;
|
|
298
|
+
let on_token_expiring = options
|
|
299
|
+
.prop(&mut cx, "onTokenExpiring")
|
|
300
|
+
.get::<Option<Handle<JsFunction>>>()?;
|
|
301
|
+
let on_activity_join = options
|
|
302
|
+
.prop(&mut cx, "onActivityJoin")
|
|
303
|
+
.get::<Option<Handle<JsFunction>>>()?;
|
|
304
|
+
|
|
305
|
+
let mut raw = unsafe { Owned::new(sdk.Discord_Client_Init, sdk.Discord_Client_Drop) };
|
|
306
|
+
let ptr = raw.as_ptr();
|
|
307
|
+
// The SDK recommends adding the log callback first, so it sees everything the client logs, and
|
|
308
|
+
// the join callback must be set before connecting, so a join that launched the app isn't
|
|
309
|
+
// missed.
|
|
310
|
+
unsafe {
|
|
311
|
+
if let Some(f) = on_log {
|
|
312
|
+
let user_data = JsCallback::into_user_data(&mut cx, f);
|
|
313
|
+
(sdk.Discord_Client_AddLogCallback)(
|
|
314
|
+
ptr,
|
|
315
|
+
log_callback,
|
|
316
|
+
free_js_callback,
|
|
317
|
+
user_data,
|
|
318
|
+
log_severity,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
if let Some(f) = on_status_changed {
|
|
322
|
+
let user_data = JsCallback::into_user_data(&mut cx, f);
|
|
323
|
+
(sdk.Discord_Client_SetStatusChangedCallback)(
|
|
324
|
+
ptr,
|
|
325
|
+
status_changed_callback,
|
|
326
|
+
free_js_callback,
|
|
327
|
+
user_data,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
if let Some(f) = on_token_expiring {
|
|
331
|
+
let user_data = JsCallback::into_user_data(&mut cx, f);
|
|
332
|
+
(sdk.Discord_Client_SetTokenExpirationCallback)(
|
|
333
|
+
ptr,
|
|
334
|
+
token_expiration_callback,
|
|
335
|
+
free_js_callback,
|
|
336
|
+
user_data,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
if let Some(f) = on_activity_join {
|
|
340
|
+
let user_data = JsCallback::into_user_data(&mut cx, f);
|
|
341
|
+
(sdk.Discord_Client_SetActivityJoinCallback)(
|
|
342
|
+
ptr,
|
|
343
|
+
activity_join_callback,
|
|
344
|
+
free_js_callback,
|
|
345
|
+
user_data,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
Ok(cx.boxed(Client {
|
|
351
|
+
raw: RefCell::new(Some(raw)),
|
|
352
|
+
}))
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
fn destroy_client(mut cx: FunctionContext) -> JsResult<JsUndefined> {
|
|
356
|
+
let client = cx.argument::<JsBox<Client>>(0)?;
|
|
357
|
+
// Dropping the SDK client frees the JS callbacks it holds and rejects any pending calls.
|
|
358
|
+
drop(client.raw.borrow_mut().take());
|
|
359
|
+
Ok(cx.undefined())
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
fn parse_application_id(cx: &mut Cx, id: &str) -> NeonResult<u64> {
|
|
363
|
+
match id.parse::<u64>() {
|
|
364
|
+
Ok(id) => Ok(id),
|
|
365
|
+
Err(_) => cx.throw_type_error(format!("Invalid Discord application ID: {id}")),
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
fn set_application_id(mut cx: FunctionContext) -> JsResult<JsUndefined> {
|
|
370
|
+
let client = cx.argument::<JsBox<Client>>(0)?;
|
|
371
|
+
let application_id = cx.argument::<JsString>(1)?.value(&mut cx);
|
|
372
|
+
let application_id = parse_application_id(&mut cx, &application_id)?;
|
|
373
|
+
client.with(&mut cx, |_, sdk, ptr| {
|
|
374
|
+
unsafe { (sdk.Discord_Client_SetApplicationId)(ptr, application_id) };
|
|
375
|
+
Ok(())
|
|
376
|
+
})?;
|
|
377
|
+
Ok(cx.undefined())
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
fn update_token(mut cx: FunctionContext) -> JsResult<JsPromise> {
|
|
381
|
+
let client = cx.argument::<JsBox<Client>>(0)?;
|
|
382
|
+
let token = cx.argument::<JsString>(1)?.value(&mut cx);
|
|
383
|
+
client.with(&mut cx, |cx, sdk, ptr| {
|
|
384
|
+
let (promise, user_data) = PendingCall::start(cx);
|
|
385
|
+
unsafe {
|
|
386
|
+
(sdk.Discord_Client_UpdateToken)(
|
|
387
|
+
ptr,
|
|
388
|
+
Discord_AuthorizationTokenType_Bearer,
|
|
389
|
+
Discord_String::borrow(&token),
|
|
390
|
+
call_result_callback,
|
|
391
|
+
free_pending_call,
|
|
392
|
+
user_data,
|
|
393
|
+
)
|
|
394
|
+
};
|
|
395
|
+
Ok(promise)
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
fn connect(mut cx: FunctionContext) -> JsResult<JsUndefined> {
|
|
400
|
+
let client = cx.argument::<JsBox<Client>>(0)?;
|
|
401
|
+
client.with(&mut cx, |_, sdk, ptr| {
|
|
402
|
+
unsafe { (sdk.Discord_Client_Connect)(ptr) };
|
|
403
|
+
Ok(())
|
|
404
|
+
})?;
|
|
405
|
+
Ok(cx.undefined())
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
fn disconnect(mut cx: FunctionContext) -> JsResult<JsUndefined> {
|
|
409
|
+
let client = cx.argument::<JsBox<Client>>(0)?;
|
|
410
|
+
client.with(&mut cx, |_, sdk, ptr| {
|
|
411
|
+
unsafe { (sdk.Discord_Client_Disconnect)(ptr) };
|
|
412
|
+
Ok(())
|
|
413
|
+
})?;
|
|
414
|
+
Ok(cx.undefined())
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
fn get_status(mut cx: FunctionContext) -> JsResult<JsString> {
|
|
418
|
+
let client = cx.argument::<JsBox<Client>>(0)?;
|
|
419
|
+
let status = client.with(&mut cx, |_, sdk, ptr| {
|
|
420
|
+
Ok(unsafe { (sdk.Discord_Client_GetStatus)(ptr) })
|
|
421
|
+
})?;
|
|
422
|
+
Ok(cx.string(client_status_name(status)))
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/// Passes an optional string property of `object` to an SDK setter that takes an optional string.
|
|
426
|
+
fn set_optional_string<'cx, T>(
|
|
427
|
+
cx: &mut Cx<'cx>,
|
|
428
|
+
object: Handle<'cx, JsObject>,
|
|
429
|
+
key: &str,
|
|
430
|
+
target: *mut T,
|
|
431
|
+
setter: unsafe extern "C" fn(*mut T, *mut Discord_String),
|
|
432
|
+
) -> NeonResult<()> {
|
|
433
|
+
if let Some(value) = object.prop(cx, key).get::<Option<String>>()? {
|
|
434
|
+
let mut value = Discord_String::borrow(&value);
|
|
435
|
+
unsafe { setter(target, &mut value) };
|
|
436
|
+
}
|
|
437
|
+
Ok(())
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
fn get_timestamp<'cx>(
|
|
441
|
+
cx: &mut Cx<'cx>,
|
|
442
|
+
object: Handle<'cx, JsObject>,
|
|
443
|
+
key: &str,
|
|
444
|
+
) -> NeonResult<Option<u64>> {
|
|
445
|
+
match object.prop(cx, key).get::<Option<f64>>()? {
|
|
446
|
+
Some(ms) if ms.is_finite() && ms >= 0.0 => Ok(Some(ms as u64)),
|
|
447
|
+
Some(ms) => cx.throw_range_error(format!("Invalid timestamp for {key}: {ms}")),
|
|
448
|
+
None => Ok(None),
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
fn get_party_size<'cx>(
|
|
453
|
+
cx: &mut Cx<'cx>,
|
|
454
|
+
object: Handle<'cx, JsObject>,
|
|
455
|
+
key: &str,
|
|
456
|
+
) -> NeonResult<i32> {
|
|
457
|
+
let size = object.prop(cx, key).get::<f64>()?;
|
|
458
|
+
if size.fract() != 0.0 || !(0.0..=i32::MAX as f64).contains(&size) {
|
|
459
|
+
return cx.throw_range_error(format!("Invalid party size for {key}: {size}"));
|
|
460
|
+
}
|
|
461
|
+
Ok(size as i32)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/// Builds an SDK activity from a JS `Activity` (see `index.d.ts`). The SDK's setters copy what
|
|
465
|
+
/// they're given, so each nested object is dropped once it's set.
|
|
466
|
+
fn build_activity<'cx>(
|
|
467
|
+
cx: &mut Cx<'cx>,
|
|
468
|
+
sdk: &Sdk,
|
|
469
|
+
object: Handle<'cx, JsObject>,
|
|
470
|
+
) -> NeonResult<Owned<Discord_Activity>> {
|
|
471
|
+
let mut activity = unsafe { Owned::new(sdk.Discord_Activity_Init, sdk.Discord_Activity_Drop) };
|
|
472
|
+
let ptr = activity.as_ptr();
|
|
473
|
+
|
|
474
|
+
if let Some(name) = object.prop(cx, "type").get::<Option<String>>()? {
|
|
475
|
+
let activity_type = parse_activity_type(cx, &name)?;
|
|
476
|
+
unsafe { (sdk.Discord_Activity_SetType)(ptr, activity_type) };
|
|
477
|
+
}
|
|
478
|
+
if let Some(name) = object
|
|
479
|
+
.prop(cx, "statusDisplayType")
|
|
480
|
+
.get::<Option<String>>()?
|
|
481
|
+
{
|
|
482
|
+
let mut display_type = parse_status_display_type(cx, &name)?;
|
|
483
|
+
unsafe { (sdk.Discord_Activity_SetStatusDisplayType)(ptr, &mut display_type) };
|
|
484
|
+
}
|
|
485
|
+
set_optional_string(cx, object, "details", ptr, sdk.Discord_Activity_SetDetails)?;
|
|
486
|
+
set_optional_string(cx, object, "state", ptr, sdk.Discord_Activity_SetState)?;
|
|
487
|
+
|
|
488
|
+
if let Some(timestamps) = object
|
|
489
|
+
.prop(cx, "timestamps")
|
|
490
|
+
.get::<Option<Handle<JsObject>>>()?
|
|
491
|
+
{
|
|
492
|
+
let mut native = unsafe {
|
|
493
|
+
Owned::new(
|
|
494
|
+
sdk.Discord_ActivityTimestamps_Init,
|
|
495
|
+
sdk.Discord_ActivityTimestamps_Drop,
|
|
496
|
+
)
|
|
497
|
+
};
|
|
498
|
+
if let Some(start) = get_timestamp(cx, timestamps, "start")? {
|
|
499
|
+
unsafe { (sdk.Discord_ActivityTimestamps_SetStart)(native.as_ptr(), start) };
|
|
500
|
+
}
|
|
501
|
+
if let Some(end) = get_timestamp(cx, timestamps, "end")? {
|
|
502
|
+
unsafe { (sdk.Discord_ActivityTimestamps_SetEnd)(native.as_ptr(), end) };
|
|
503
|
+
}
|
|
504
|
+
unsafe { (sdk.Discord_Activity_SetTimestamps)(ptr, native.as_ptr()) };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if let Some(assets) = object
|
|
508
|
+
.prop(cx, "assets")
|
|
509
|
+
.get::<Option<Handle<JsObject>>>()?
|
|
510
|
+
{
|
|
511
|
+
let mut native = unsafe {
|
|
512
|
+
Owned::new(
|
|
513
|
+
sdk.Discord_ActivityAssets_Init,
|
|
514
|
+
sdk.Discord_ActivityAssets_Drop,
|
|
515
|
+
)
|
|
516
|
+
};
|
|
517
|
+
let target = native.as_ptr();
|
|
518
|
+
for (key, setter) in [
|
|
519
|
+
("largeImage", sdk.Discord_ActivityAssets_SetLargeImage),
|
|
520
|
+
("largeText", sdk.Discord_ActivityAssets_SetLargeText),
|
|
521
|
+
("smallImage", sdk.Discord_ActivityAssets_SetSmallImage),
|
|
522
|
+
("smallText", sdk.Discord_ActivityAssets_SetSmallText),
|
|
523
|
+
] {
|
|
524
|
+
set_optional_string(cx, assets, key, target, setter)?;
|
|
525
|
+
}
|
|
526
|
+
unsafe { (sdk.Discord_Activity_SetAssets)(ptr, target) };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if let Some(party) = object.prop(cx, "party").get::<Option<Handle<JsObject>>>()? {
|
|
530
|
+
let id = party.prop(cx, "id").get::<String>()?;
|
|
531
|
+
let current_size = get_party_size(cx, party, "currentSize")?;
|
|
532
|
+
let max_size = get_party_size(cx, party, "maxSize")?;
|
|
533
|
+
let privacy = match party
|
|
534
|
+
.prop(cx, "privacy")
|
|
535
|
+
.get::<Option<String>>()?
|
|
536
|
+
.as_deref()
|
|
537
|
+
{
|
|
538
|
+
None | Some("private") => Discord_ActivityPartyPrivacy_Private,
|
|
539
|
+
Some("public") => Discord_ActivityPartyPrivacy_Public,
|
|
540
|
+
Some(other) => return cx.throw_type_error(format!("Unknown party privacy: {other}")),
|
|
541
|
+
};
|
|
542
|
+
let mut native = unsafe {
|
|
543
|
+
Owned::new(
|
|
544
|
+
sdk.Discord_ActivityParty_Init,
|
|
545
|
+
sdk.Discord_ActivityParty_Drop,
|
|
546
|
+
)
|
|
547
|
+
};
|
|
548
|
+
let target = native.as_ptr();
|
|
549
|
+
unsafe {
|
|
550
|
+
(sdk.Discord_ActivityParty_SetId)(target, Discord_String::borrow(&id));
|
|
551
|
+
(sdk.Discord_ActivityParty_SetCurrentSize)(target, current_size);
|
|
552
|
+
(sdk.Discord_ActivityParty_SetMaxSize)(target, max_size);
|
|
553
|
+
(sdk.Discord_ActivityParty_SetPrivacy)(target, privacy);
|
|
554
|
+
(sdk.Discord_Activity_SetParty)(ptr, target);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if let Some(join_secret) = object.prop(cx, "joinSecret").get::<Option<String>>()? {
|
|
559
|
+
let mut native = unsafe {
|
|
560
|
+
Owned::new(
|
|
561
|
+
sdk.Discord_ActivitySecrets_Init,
|
|
562
|
+
sdk.Discord_ActivitySecrets_Drop,
|
|
563
|
+
)
|
|
564
|
+
};
|
|
565
|
+
let target = native.as_ptr();
|
|
566
|
+
unsafe {
|
|
567
|
+
(sdk.Discord_ActivitySecrets_SetJoin)(target, Discord_String::borrow(&join_secret));
|
|
568
|
+
(sdk.Discord_Activity_SetSecrets)(ptr, target);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if let Some(platforms) = object
|
|
573
|
+
.prop(cx, "supportedPlatforms")
|
|
574
|
+
.get::<Option<Handle<JsArray>>>()?
|
|
575
|
+
{
|
|
576
|
+
let mut mask = 0;
|
|
577
|
+
for platform in platforms.to_vec(cx)? {
|
|
578
|
+
let name = platform.downcast_or_throw::<JsString, _>(cx)?.value(cx);
|
|
579
|
+
mask |= parse_platform(cx, &name)?;
|
|
580
|
+
}
|
|
581
|
+
unsafe { (sdk.Discord_Activity_SetSupportedPlatforms)(ptr, mask) };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if let Some(buttons) = object
|
|
585
|
+
.prop(cx, "buttons")
|
|
586
|
+
.get::<Option<Handle<JsArray>>>()?
|
|
587
|
+
{
|
|
588
|
+
for button in buttons.to_vec(cx)? {
|
|
589
|
+
let button = button.downcast_or_throw::<JsObject, _>(cx)?;
|
|
590
|
+
let label = button.prop(cx, "label").get::<String>()?;
|
|
591
|
+
let url = button.prop(cx, "url").get::<String>()?;
|
|
592
|
+
let mut native = unsafe {
|
|
593
|
+
Owned::new(
|
|
594
|
+
sdk.Discord_ActivityButton_Init,
|
|
595
|
+
sdk.Discord_ActivityButton_Drop,
|
|
596
|
+
)
|
|
597
|
+
};
|
|
598
|
+
let target = native.as_ptr();
|
|
599
|
+
unsafe {
|
|
600
|
+
(sdk.Discord_ActivityButton_SetLabel)(target, Discord_String::borrow(&label));
|
|
601
|
+
(sdk.Discord_ActivityButton_SetUrl)(target, Discord_String::borrow(&url));
|
|
602
|
+
(sdk.Discord_Activity_AddButton)(ptr, target);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
Ok(activity)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
fn update_rich_presence(mut cx: FunctionContext) -> JsResult<JsPromise> {
|
|
611
|
+
let client = cx.argument::<JsBox<Client>>(0)?;
|
|
612
|
+
let activity = cx.argument::<JsObject>(1)?;
|
|
613
|
+
let sdk = sdk(&mut cx)?;
|
|
614
|
+
let mut activity = build_activity(&mut cx, sdk, activity)?;
|
|
615
|
+
client.with(&mut cx, |cx, sdk, ptr| {
|
|
616
|
+
let (promise, user_data) = PendingCall::start(cx);
|
|
617
|
+
unsafe {
|
|
618
|
+
(sdk.Discord_Client_UpdateRichPresence)(
|
|
619
|
+
ptr,
|
|
620
|
+
activity.as_ptr(),
|
|
621
|
+
call_result_callback,
|
|
622
|
+
free_pending_call,
|
|
623
|
+
user_data,
|
|
624
|
+
)
|
|
625
|
+
};
|
|
626
|
+
Ok(promise)
|
|
627
|
+
})
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
fn clear_rich_presence(mut cx: FunctionContext) -> JsResult<JsUndefined> {
|
|
631
|
+
let client = cx.argument::<JsBox<Client>>(0)?;
|
|
632
|
+
client.with(&mut cx, |_, sdk, ptr| {
|
|
633
|
+
unsafe { (sdk.Discord_Client_ClearRichPresence)(ptr) };
|
|
634
|
+
Ok(())
|
|
635
|
+
})?;
|
|
636
|
+
Ok(cx.undefined())
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
fn register_launch_command(mut cx: FunctionContext) -> JsResult<JsBoolean> {
|
|
640
|
+
let client = cx.argument::<JsBox<Client>>(0)?;
|
|
641
|
+
let application_id = cx.argument::<JsString>(1)?.value(&mut cx);
|
|
642
|
+
let application_id = parse_application_id(&mut cx, &application_id)?;
|
|
643
|
+
let command = cx.argument::<JsString>(2)?.value(&mut cx);
|
|
644
|
+
let registered = client.with(&mut cx, |_, sdk, ptr| {
|
|
645
|
+
Ok(unsafe {
|
|
646
|
+
(sdk.Discord_Client_RegisterLaunchCommand)(
|
|
647
|
+
ptr,
|
|
648
|
+
application_id,
|
|
649
|
+
Discord_String::borrow(&command),
|
|
650
|
+
)
|
|
651
|
+
})
|
|
652
|
+
})?;
|
|
653
|
+
Ok(cx.boolean(registered))
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
#[neon::main]
|
|
657
|
+
fn main(mut cx: ModuleContext) -> NeonResult<()> {
|
|
658
|
+
cx.export_function("version", version)?;
|
|
659
|
+
cx.export_function("loadSdk", load_sdk)?;
|
|
660
|
+
cx.export_function("sdkVersion", sdk_version)?;
|
|
661
|
+
cx.export_function("runCallbacks", run_callbacks)?;
|
|
662
|
+
cx.export_function("createClient", create_client)?;
|
|
663
|
+
cx.export_function("destroyClient", destroy_client)?;
|
|
664
|
+
cx.export_function("setApplicationId", set_application_id)?;
|
|
665
|
+
cx.export_function("updateToken", update_token)?;
|
|
666
|
+
cx.export_function("connect", connect)?;
|
|
667
|
+
cx.export_function("disconnect", disconnect)?;
|
|
668
|
+
cx.export_function("getStatus", get_status)?;
|
|
669
|
+
cx.export_function("updateRichPresence", update_rich_presence)?;
|
|
670
|
+
cx.export_function("clearRichPresence", clear_rich_presence)?;
|
|
671
|
+
cx.export_function("registerLaunchCommand", register_launch_command)?;
|
|
672
|
+
Ok(())
|
|
673
|
+
}
|