Skip to main content

nimbuspulse_client/
lib.rs

1use std::path::PathBuf;
2
3use anyhow::{Ok, Result, bail};
4use serde::{Deserialize, Serialize};
5pub use types::billing::BillingType;
6pub use types::dcs_api::{
7    AddMissionsResponse, BanPlayerRequest, BanPlayerResponse, DeleteMissionsResponse,
8    GetPauseServerResponse, GetResumeServerResponse, KickPlayerRequest, KickPlayerResponse,
9    SendChatRequest, SendChatResponse, SetServerSettingsRequest, SetServerSettingsResponse,
10    StartMissionResponse, StartServerResponse,
11};
12pub use types::dcs_chat::DcsChat;
13pub use types::dcs_runtime::{
14    AdvancedSettings, BannedPlayer, CurrentRuntimeAction, DcsRuntime, GetMissionInfoResponse,
15    GetMissionListResponse, GetPlayersResponse, GetServerSettingsResponse, Player, Players,
16    Settings,
17};
18pub use types::dcs_settings::{DcsSettings, DcsSettingsPayload, DcsSettingsUpdatePayload};
19pub use types::files::{
20    FileDownloadResponse, FileInfo, FileListResponse, FileUploadRequest, FmRoot, MoveFileRequest,
21};
22pub use types::instance::{
23    ApiError, GameRuntime, GameType, Instance, InstanceNodeResource, InstancePermissions,
24    InstanceResource, InstanceStatus, InstanceStoppedReason, InstancesResponse, Permission,
25    UserResource,
26};
27pub use types::logs::{GameServerLogRow, GameServerLogsResponse, InstanceLogRequest};
28pub use types::mods::{ChangeModsRequest, ModConfigType};
29pub use types::products::{
30    ACE_TIER, CADET_TIER, COMMANDER_TIER, PRODUCTS, Product, STRATEGIST_TIER, VETERAN_TIER,
31    WINGMAN_TIER, product_by_id,
32};
33pub use types::region::Region;
34pub use types::srs::{SrsClient, SrsModRequest, SrsServerInfo};
35pub use types::system_resources::{
36    PrometheusSeries, ResourceKind, ResourceUsage, ServerResourcesResponse,
37};
38pub use types::system_resources_periode::SystemResourcesPeriod;
39pub use types::triggers::{
40    ComparisonOperator, CreateTriggerRequest, Trigger, TriggerAction, TriggerCondition,
41};
42pub use types::webconsole::WebConsoleExecuteRequest;
43
44use reqwest::multipart::{Form, Part};
45use tokio::fs::File;
46use tokio::io::AsyncWriteExt;
47use types::files::FilePathQuery;
48
49pub use uuid::Uuid;
50
51mod types;
52
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
54pub struct CreateInstanceRequest {
55    pub product_id: Uuid,
56    pub region: Region,
57    pub billing_type: BillingType,
58    pub partner_code: Option<String>,
59    pub settings: DcsSettingsPayload,
60    pub active_mods: Vec<String>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
64pub struct CreateInstanceMonthlyResponse {
65    pub url: String,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
69#[serde(untagged)]
70pub enum CreateInstanceResponse {
71    Hourly(Instance),
72    Monthly(CreateInstanceMonthlyResponse),
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
76#[serde(tag = "game_type", content = "settings", rename_all = "snake_case")]
77pub enum EditInstanceRequest {
78    Dcs(DcsSettingsUpdatePayload),
79}
80
81impl EditInstanceRequest {
82    pub fn dcs(settings: DcsSettingsUpdatePayload) -> Self {
83        Self::Dcs(settings)
84    }
85}
86
87impl From<DcsSettingsUpdatePayload> for EditInstanceRequest {
88    fn from(settings: DcsSettingsUpdatePayload) -> Self {
89        Self::dcs(settings)
90    }
91}
92
93#[derive(Debug, Clone)]
94pub struct Client {
95    api_key: String,
96    reqwest_client: reqwest::Client,
97}
98
99impl Client {
100    const BASE_URL: &'static str = "https://coordinator.nimbuspulse.com";
101
102    pub fn new(api_key: impl Into<String>) -> Self {
103        Self {
104            reqwest_client: reqwest::Client::new(),
105            api_key: api_key.into(),
106        }
107    }
108
109    pub fn set_api_key(&mut self, api_key: impl Into<String>) {
110        self.api_key = api_key.into();
111    }
112
113    async fn send(&self, request: reqwest::RequestBuilder) -> Result<reqwest::Response> {
114        Ok(request.bearer_auth(self.api_key.clone()).send().await?)
115    }
116
117    async fn send_json<T>(&self, request: reqwest::RequestBuilder) -> Result<T>
118    where
119        T: serde::de::DeserializeOwned,
120    {
121        let response = self.send(request).await?;
122        if !response.status().is_success() {
123            bail!(
124                response
125                    .text()
126                    .await
127                    .unwrap_or_else(|_| "request failed".to_string())
128            );
129        }
130
131        Ok(response.json::<T>().await?)
132    }
133
134    async fn send_unit(&self, request: reqwest::RequestBuilder, context: &str) -> Result<()> {
135        let response = self.send(request).await?;
136        if !response.status().is_success() {
137            bail!("{context}: {}", response.text().await.unwrap_or_default());
138        }
139
140        Ok(())
141    }
142
143    pub async fn create_server(
144        &self,
145        region: Region,
146        billing_type: BillingType,
147        name: impl Into<String>,
148        password: Option<impl Into<String>>,
149        max_players: u32,
150        product: impl Into<Uuid>,
151        active_mods: Vec<impl Into<String>>,
152        use_voice_chat: bool,
153        enable_io: bool,
154        enable_os: bool,
155        enable_lfs: bool,
156        allow_external_loading: bool,
157        partner_code: Option<String>,
158    ) -> Result<CreateInstanceResponse> {
159        let payload = CreateInstanceRequest {
160            product_id: product.into(),
161            region,
162            billing_type,
163            partner_code,
164            settings: DcsSettingsPayload {
165                initial_server_name: name.into(),
166                initial_server_password: password.map(|p| p.into()).unwrap_or_default(),
167                initial_max_players: max_players,
168                initial_use_voice_chat: use_voice_chat,
169                enable_io,
170                enable_os,
171                enable_lfs,
172                allow_external_loading,
173            },
174            active_mods: active_mods.into_iter().map(|m| m.into()).collect(),
175        };
176
177        self.send_json(
178            self.reqwest_client
179                .post(format!("{}/game_servers", Self::BASE_URL))
180                .json(&payload),
181        )
182        .await
183    }
184
185    pub async fn get_runtime(&self, id: &Uuid) -> Result<DcsRuntime> {
186        let server = self.get_server(id).await?;
187
188        match server.runtime {
189            Some(GameRuntime::Dcs(runtime)) => Ok(runtime),
190            None => bail!("server runtime is not available"),
191        }
192    }
193
194    pub async fn get_server_resources(
195        &self,
196        id: &Uuid,
197        period: SystemResourcesPeriod,
198    ) -> Result<ServerResourcesResponse> {
199        self.send_json(self.reqwest_client.get(format!(
200            "{}/game_servers/{}/resources?periode={}",
201            Self::BASE_URL,
202            id,
203            period
204        )))
205        .await
206    }
207
208    pub async fn health(&self) -> Result<()> {
209        self.send_unit(
210            self.reqwest_client
211                .get(format!("{}/health", Self::BASE_URL)),
212            "health check failed",
213        )
214        .await
215    }
216
217    pub async fn get_servers(&self) -> Result<InstancesResponse> {
218        self.send_json(
219            self.reqwest_client
220                .get(format!("{}/game_servers", Self::BASE_URL)),
221        )
222        .await
223    }
224
225    pub async fn get_server(&self, id: &Uuid) -> Result<InstanceResource> {
226        self.send_json(
227            self.reqwest_client
228                .get(format!("{}/game_servers/{}", Self::BASE_URL, id)),
229        )
230        .await
231    }
232
233    pub async fn update_server(
234        &self,
235        id: &Uuid,
236        payload: &EditInstanceRequest,
237    ) -> Result<InstanceResource> {
238        self.send_json(
239            self.reqwest_client
240                .put(format!("{}/game_servers/{}", Self::BASE_URL, id))
241                .json(payload),
242        )
243        .await
244    }
245
246    pub async fn get_server_logs(
247        &self,
248        id: &Uuid,
249        request: &InstanceLogRequest,
250    ) -> Result<GameServerLogsResponse> {
251        self.send_json(
252            self.reqwest_client
253                .get(format!("{}/game_servers/{}/logs", Self::BASE_URL, id))
254                .query(request),
255        )
256        .await
257    }
258
259    pub async fn get_chat(&self, id: &Uuid) -> Result<Vec<DcsChat>> {
260        self.send_json(self.reqwest_client.get(format!(
261            "{}/game_servers/{}/chat",
262            Self::BASE_URL,
263            id
264        )))
265        .await
266    }
267
268    pub async fn start_server(&self, id: &Uuid) -> Result<Instance> {
269        self.send_json(self.reqwest_client.post(format!(
270            "{}/game_servers/{}/start",
271            Self::BASE_URL,
272            id
273        )))
274        .await
275    }
276
277    pub async fn stop_server(&self, id: &Uuid) -> Result<Instance> {
278        self.send_json(self.reqwest_client.post(format!(
279            "{}/game_servers/{}/stop",
280            Self::BASE_URL,
281            id
282        )))
283        .await
284    }
285
286    pub async fn full_restart_server(&self, id: &Uuid) -> Result<Instance> {
287        self.send_json(self.reqwest_client.post(format!(
288            "{}/game_servers/{}/full_restart",
289            Self::BASE_URL,
290            id
291        )))
292        .await
293    }
294
295    pub async fn restart_server(&self, id: &Uuid) -> Result<Instance> {
296        self.send_json(self.reqwest_client.post(format!(
297            "{}/game_servers/{}/restart",
298            Self::BASE_URL,
299            id
300        )))
301        .await
302    }
303
304    pub async fn update_game_server(&self, id: &Uuid) -> Result<Instance> {
305        self.send_json(self.reqwest_client.post(format!(
306            "{}/game_servers/{}/update",
307            Self::BASE_URL,
308            id
309        )))
310        .await
311    }
312
313    pub async fn delete_server(&self, id: &Uuid) -> Result<()> {
314        self.send_unit(
315            self.reqwest_client
316                .delete(format!("{}/game_servers/{}", Self::BASE_URL, id)),
317            "failed to delete server",
318        )
319        .await
320    }
321
322    pub async fn reactivate_server(&self, id: &Uuid) -> Result<()> {
323        self.send_unit(
324            self.reqwest_client
325                .post(format!("{}/game_servers/{}/reactivate", Self::BASE_URL, id)),
326            "failed to reactivate server",
327        )
328        .await
329    }
330
331    pub async fn change_mods(&self, id: &Uuid, request: &ChangeModsRequest) -> Result<()> {
332        self.send_unit(
333            self.reqwest_client
334                .put(format!(
335                    "{}/game_servers/{}/mods/change",
336                    Self::BASE_URL,
337                    id
338                ))
339                .json(request),
340            "failed to change mods",
341        )
342        .await
343    }
344
345    pub async fn get_mod(&self, id: &Uuid, mod_id: ModConfigType) -> Result<serde_json::Value> {
346        self.send_json(self.reqwest_client.get(format!(
347            "{}/game_servers/{}/mods/{}",
348            Self::BASE_URL,
349            id,
350            mod_id
351        )))
352        .await
353    }
354
355    pub async fn list_files(
356        &self,
357        id: &Uuid,
358        root: FmRoot,
359        path: impl Into<String>,
360    ) -> Result<FileListResponse> {
361        self.send_json(
362            self.reqwest_client
363                .get(format!("{}/game_servers/{}/files", Self::BASE_URL, id))
364                .query(&FilePathQuery {
365                    root,
366                    path: path.into(),
367                }),
368        )
369        .await
370    }
371
372    pub async fn create_directory(
373        &self,
374        id: &Uuid,
375        root: FmRoot,
376        path: impl Into<String>,
377    ) -> Result<()> {
378        self.send_unit(
379            self.reqwest_client
380                .post(format!(
381                    "{}/game_servers/{}/files/directory",
382                    Self::BASE_URL,
383                    id
384                ))
385                .query(&FilePathQuery {
386                    root,
387                    path: path.into(),
388                }),
389            "failed to create directory",
390        )
391        .await
392    }
393
394    pub async fn upload_file(
395        &self,
396        id: &Uuid,
397        root: FmRoot,
398        path: impl Into<String>,
399        file: Vec<u8>,
400    ) -> Result<()> {
401        let part = Part::bytes(file)
402            .file_name("upload.bin")
403            .mime_str("application/octet-stream")?;
404        let form = Form::new().part("file", part);
405
406        self.send_unit(
407            self.reqwest_client
408                .post(format!(
409                    "{}/game_servers/{}/files/upload",
410                    Self::BASE_URL,
411                    id
412                ))
413                .query(&FilePathQuery {
414                    root,
415                    path: path.into(),
416                })
417                .multipart(form),
418            "failed to upload file",
419        )
420        .await
421    }
422
423    pub async fn upload_file_from(
424        &self,
425        id: &Uuid,
426        root: FmRoot,
427        path: impl Into<String>,
428        file: impl Into<PathBuf>,
429    ) -> Result<()> {
430        let form = Form::new().file("file", file.into()).await?;
431
432        self.send_unit(
433            self.reqwest_client
434                .post(format!(
435                    "{}/game_servers/{}/files/upload",
436                    Self::BASE_URL,
437                    id
438                ))
439                .query(&FilePathQuery {
440                    root,
441                    path: path.into(),
442                })
443                .multipart(form),
444            "failed to upload file",
445        )
446        .await
447    }
448
449    pub async fn download_file(
450        &self,
451        id: &Uuid,
452        root: FmRoot,
453        path: impl Into<String>,
454    ) -> Result<Vec<u8>> {
455        let response = self
456            .send(
457                self.reqwest_client
458                    .get(format!(
459                        "{}/game_servers/{}/files/download",
460                        Self::BASE_URL,
461                        id
462                    ))
463                    .query(&FilePathQuery {
464                        root,
465                        path: path.into(),
466                    }),
467            )
468            .await?;
469
470        if !response.status().is_success() {
471            bail!(format!("Failed to download file: {:?}", response));
472        }
473
474        let bytes = response.bytes().await?;
475        Ok(bytes.to_vec())
476    }
477
478    pub async fn download_file_to(
479        &self,
480        id: &Uuid,
481        root: FmRoot,
482        path: impl Into<String>,
483        destination: impl Into<PathBuf>,
484    ) -> Result<()> {
485        let bytes = self.download_file(id, root, path).await?;
486        let mut file = File::create(destination.into()).await?;
487        file.write_all(&bytes).await?;
488
489        Ok(())
490    }
491
492    pub async fn delete_file(
493        &self,
494        id: &Uuid,
495        root: FmRoot,
496        path: impl Into<String>,
497    ) -> Result<()> {
498        self.send_unit(
499            self.reqwest_client
500                .delete(format!("{}/game_servers/{}/files", Self::BASE_URL, id))
501                .query(&FilePathQuery {
502                    root,
503                    path: path.into(),
504                }),
505            "failed to delete file",
506        )
507        .await
508    }
509
510    pub async fn move_file(&self, id: &Uuid, request: &MoveFileRequest) -> Result<()> {
511        self.send_unit(
512            self.reqwest_client
513                .put(format!("{}/game_servers/{}/files/move", Self::BASE_URL, id))
514                .json(request),
515            "failed to move file",
516        )
517        .await
518    }
519
520    pub async fn add_missions(
521        &self,
522        id: &Uuid,
523        missions: &[String],
524    ) -> Result<AddMissionsResponse> {
525        self.send_json(
526            self.reqwest_client
527                .post(format!(
528                    "{}/game_servers/{}/dcs-api/missions",
529                    Self::BASE_URL,
530                    id
531                ))
532                .json(missions),
533        )
534        .await
535    }
536
537    pub async fn delete_missions(
538        &self,
539        id: &Uuid,
540        missions: &[i32],
541    ) -> Result<DeleteMissionsResponse> {
542        self.send_json(
543            self.reqwest_client
544                .delete(format!(
545                    "{}/game_servers/{}/dcs-api/missions",
546                    Self::BASE_URL,
547                    id
548                ))
549                .json(missions),
550        )
551        .await
552    }
553
554    pub async fn select_mission(
555        &self,
556        id: &Uuid,
557        mission_idx: i32,
558    ) -> Result<StartMissionResponse> {
559        self.send_json(self.reqwest_client.post(format!(
560            "{}/game_servers/{}/dcs-api/missions/{}/select",
561            Self::BASE_URL,
562            id,
563            mission_idx
564        )))
565        .await
566    }
567
568    pub async fn start_mission(&self, id: &Uuid, mission_idx: i32) -> Result<StartServerResponse> {
569        self.send_json(self.reqwest_client.post(format!(
570            "{}/game_servers/{}/dcs-api/missions/{}/start",
571            Self::BASE_URL,
572            id,
573            mission_idx
574        )))
575        .await
576    }
577
578    pub async fn pause_server(&self, id: &Uuid) -> Result<GetPauseServerResponse> {
579        self.send_json(self.reqwest_client.post(format!(
580            "{}/game_servers/{}/dcs-api/pause",
581            Self::BASE_URL,
582            id
583        )))
584        .await
585    }
586
587    pub async fn resume_server(&self, id: &Uuid) -> Result<GetResumeServerResponse> {
588        self.send_json(self.reqwest_client.post(format!(
589            "{}/game_servers/{}/dcs-api/resume",
590            Self::BASE_URL,
591            id
592        )))
593        .await
594    }
595
596    pub async fn save_settings(
597        &self,
598        id: &Uuid,
599        request: &SetServerSettingsRequest,
600    ) -> Result<SetServerSettingsResponse> {
601        self.send_json(
602            self.reqwest_client
603                .post(format!(
604                    "{}/game_servers/{}/dcs-api/settings",
605                    Self::BASE_URL,
606                    id
607                ))
608                .json(request),
609        )
610        .await
611    }
612
613    pub async fn kick_player(
614        &self,
615        id: &Uuid,
616        request: &KickPlayerRequest,
617    ) -> Result<KickPlayerResponse> {
618        self.send_json(
619            self.reqwest_client
620                .post(format!(
621                    "{}/game_servers/{}/dcs-api/kick",
622                    Self::BASE_URL,
623                    id
624                ))
625                .json(request),
626        )
627        .await
628    }
629
630    pub async fn ban_player(
631        &self,
632        id: &Uuid,
633        request: &BanPlayerRequest,
634    ) -> Result<BanPlayerResponse> {
635        self.send_json(
636            self.reqwest_client
637                .post(format!(
638                    "{}/game_servers/{}/dcs-api/ban",
639                    Self::BASE_URL,
640                    id
641                ))
642                .json(request),
643        )
644        .await
645    }
646
647    pub async fn send_chat(
648        &self,
649        id: &Uuid,
650        request: &SendChatRequest,
651    ) -> Result<SendChatResponse> {
652        self.send_json(
653            self.reqwest_client
654                .post(format!(
655                    "{}/game_servers/{}/dcs-api/sendChat",
656                    Self::BASE_URL,
657                    id
658                ))
659                .json(request),
660        )
661        .await
662    }
663
664    pub async fn get_srs_clients(&self, id: &Uuid) -> Result<SrsServerInfo> {
665        self.send_json(self.reqwest_client.get(format!(
666            "{}/game_servers/{}/mods/srs/clients",
667            Self::BASE_URL,
668            id
669        )))
670        .await
671    }
672
673    pub async fn kick_srs_client(&self, id: &Uuid, request: &SrsModRequest) -> Result<()> {
674        self.send_unit(
675            self.reqwest_client
676                .post(format!(
677                    "{}/game_servers/{}/mods/srs/kick",
678                    Self::BASE_URL,
679                    id
680                ))
681                .json(request),
682            "failed to kick srs client",
683        )
684        .await
685    }
686
687    pub async fn ban_srs_client(&self, id: &Uuid, request: &SrsModRequest) -> Result<()> {
688        self.send_unit(
689            self.reqwest_client
690                .post(format!(
691                    "{}/game_servers/{}/mods/srs/ban",
692                    Self::BASE_URL,
693                    id
694                ))
695                .json(request),
696            "failed to ban srs client",
697        )
698        .await
699    }
700
701    pub async fn execute_webconsole(
702        &self,
703        id: &Uuid,
704        request: &WebConsoleExecuteRequest,
705    ) -> Result<String> {
706        self.send_json(
707            self.reqwest_client
708                .post(format!(
709                    "{}/game_servers/{}/mods/webconsole/execute",
710                    Self::BASE_URL,
711                    id
712                ))
713                .json(request),
714        )
715        .await
716    }
717
718    pub async fn create_trigger(
719        &self,
720        id: &Uuid,
721        request: &CreateTriggerRequest,
722    ) -> Result<Trigger> {
723        self.send_json(
724            self.reqwest_client
725                .post(format!("{}/game_servers/{}/triggers", Self::BASE_URL, id))
726                .json(request),
727        )
728        .await
729    }
730
731    pub async fn list_triggers(&self, id: &Uuid) -> Result<Vec<Trigger>> {
732        self.send_json(self.reqwest_client.get(format!(
733            "{}/game_servers/{}/triggers",
734            Self::BASE_URL,
735            id
736        )))
737        .await
738    }
739
740    pub async fn delete_trigger(&self, id: &Uuid, trigger_id: &Uuid) -> Result<()> {
741        self.send_unit(
742            self.reqwest_client.delete(format!(
743                "{}/game_servers/{}/triggers/{}",
744                Self::BASE_URL,
745                id,
746                trigger_id
747            )),
748            "failed to delete trigger",
749        )
750        .await
751    }
752}