// use bytes::Bytes; use kxio::{ fs::FileHandle, net::{Net, ReqBuilder}, }; use reqwest::multipart; use serde_json::{json, Value}; use tracing::instrument; use crate::nextcloud::model::NextcloudOrder; use crate::{ api_result::APIResult, f, nextcloud::model::{ Attachment, Board, Card, Label, NextcloudBoardId, NextcloudCardDescription, NextcloudCardId, NextcloudCardTitle, NextcloudHostname, NextcloudLabelColour, NextcloudLabelId, NextcloudLabelTitle, NextcloudPassword, NextcloudStackId, NextcloudStackTitle, NextcloudUsername, Stack, }, with_exponential_backoff, FullCtx, }; pub(crate) struct DeckClient<'ctx> { ctx: &'ctx FullCtx, hostname: &'ctx NextcloudHostname, username: &'ctx NextcloudUsername, password: &'ctx NextcloudPassword, } // Uses the API described here: https://deck.readthedocs.io/en/stable/API/#cards impl<'ctx> DeckClient<'ctx> { pub const fn new(ctx: &'ctx FullCtx) -> Self { Self { ctx, hostname: &ctx.cfg.nextcloud.hostname, username: &ctx.cfg.nextcloud.username, password: &ctx.cfg.nextcloud.password, } } fn url(&self, path: impl Into) -> String { f!( "{}/index.php/apps/deck/api/v1.0/{}", self.hostname, path.into() ) } #[instrument(skip_all)] async fn request serde::Deserialize<'a>>( &self, url: impl Into, custom: fn(&Net, String) -> ReqBuilder, ) -> APIResult { let url = url.into(); tracing::trace!(?url); APIResult::new( with_exponential_backoff!( &self.ctx, custom(&self.ctx.net, self.url(url.clone())) .basic_auth(self.username.as_str(), Some(self.password.as_str())) .header("accept", "application/json") .header("content-type", "application/json") .send() .await ), &self.ctx.prt, ) .await } #[instrument(skip_all)] async fn request_with_body serde::Deserialize<'a>>( &self, url: impl Into, body: Value, custom: fn(&Net, String) -> ReqBuilder, ) -> APIResult { let url = self.url(url.into()); tracing::trace!(?url, %body); let body: Bytes = body.to_string().into(); APIResult::new( with_exponential_backoff!( &self.ctx, custom(&self.ctx.net, url.clone()) .basic_auth(self.username.as_str(), Some(self.password.as_str())) .header("accept", "application/json") .header("content-type", "application/json") .body(body.clone()) .send() .await ), &self.ctx.prt, ) .await } #[instrument(skip(self))] pub(crate) async fn get_boards(&self) -> APIResult> { self.request("boards", |net, url| net.get(url)).await } #[instrument(skip(self))] pub(crate) async fn get_board(&self, board_id: NextcloudBoardId) -> APIResult { self.request(f!("boards/{board_id}"), |net, url| net.get(url)) .await } #[instrument(skip(self))] pub(crate) async fn get_stacks(&self, board_id: NextcloudBoardId) -> APIResult> { self.request(f!("boards/{board_id}/stacks"), |net, url| net.get(url)) .await } #[instrument(skip(self))] pub(crate) async fn get_stack( &self, board_id: NextcloudBoardId, stack_id: NextcloudStackId, ) -> APIResult { self.request(f!("boards/{board_id}/stacks/{stack_id}"), |net, url| { net.get(url) }) .await } #[instrument(skip(self))] pub(crate) async fn create_stack( &self, board_id: NextcloudBoardId, stack_title: NextcloudStackTitle, stack_order: NextcloudOrder, ) -> APIResult { self.request_with_body( f!("boards/{board_id}/stacks"), json!({ "title": stack_title, "order": stack_order, }), |net, url| net.post(url), ) .await } #[instrument(skip(self))] pub(crate) async fn create_card( &self, board_id: NextcloudBoardId, stack_id: NextcloudStackId, title: &NextcloudCardTitle, description: Option<&NextcloudCardDescription>, ) -> APIResult { let mut body = json!({ "title": title, }); if let Some(desc) = &description { body["description"] = serde_json::Value::String(desc.to_string()); } self.request_with_body( f!("boards/{board_id}/stacks/{stack_id}/cards"), body, |net, url| net.post(url), ) .await } #[instrument(skip(self))] pub(crate) async fn create_label( &self, board_id: NextcloudBoardId, name: &NextcloudLabelTitle, colour: &NextcloudLabelColour, ) -> APIResult