2024-12-17 07:09:49 +00:00
|
|
|
//
|
2024-12-08 17:23:54 +00:00
|
|
|
use bytes::Bytes;
|
2024-12-16 08:03:31 +00:00
|
|
|
use kxio::{
|
|
|
|
fs::FileHandle,
|
|
|
|
net::{Net, ReqBuilder},
|
|
|
|
};
|
|
|
|
use reqwest::multipart;
|
2024-12-08 19:31:10 +00:00
|
|
|
use serde_json::json;
|
2024-12-08 17:23:54 +00:00
|
|
|
|
2024-12-22 22:47:29 +00:00
|
|
|
use crate::nextcloud::model::NextcloudOrder;
|
2024-12-17 07:09:49 +00:00
|
|
|
use crate::{
|
|
|
|
api_result::APIResult,
|
|
|
|
f,
|
2024-12-17 07:19:31 +00:00
|
|
|
nextcloud::model::{
|
2024-12-22 14:14:53 +00:00
|
|
|
Attachment, Board, Card, Label, NextcloudBoardId, NextcloudCardDescription,
|
|
|
|
NextcloudCardId, NextcloudCardTitle, NextcloudHostname, NextcloudLabelColour,
|
|
|
|
NextcloudLabelId, NextcloudLabelTitle, NextcloudPassword, NextcloudStackId,
|
|
|
|
NextcloudStackTitle, NextcloudUsername, Stack,
|
2024-12-17 07:09:49 +00:00
|
|
|
},
|
2024-12-22 14:14:53 +00:00
|
|
|
with_exponential_backoff, FullCtx,
|
2024-12-17 07:09:49 +00:00
|
|
|
};
|
|
|
|
|
2024-12-08 17:23:54 +00:00
|
|
|
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> {
|
2024-12-20 19:56:44 +00:00
|
|
|
pub const fn new(ctx: &'ctx FullCtx) -> Self {
|
2024-12-08 17:23:54 +00:00
|
|
|
Self {
|
|
|
|
ctx,
|
|
|
|
hostname: &ctx.cfg.nextcloud.hostname,
|
|
|
|
username: &ctx.cfg.nextcloud.username,
|
|
|
|
password: &ctx.cfg.nextcloud.password,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn url(&self, path: impl Into<String>) -> String {
|
|
|
|
f!(
|
2024-12-16 08:03:31 +00:00
|
|
|
"{}/index.php/apps/deck/api/v1.0/{}",
|
2024-12-08 17:23:54 +00:00
|
|
|
self.hostname,
|
|
|
|
path.into()
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn request<T: for<'a> serde::Deserialize<'a>>(
|
|
|
|
&self,
|
|
|
|
url: impl Into<String>,
|
|
|
|
custom: fn(&Net, String) -> ReqBuilder,
|
|
|
|
) -> APIResult<T> {
|
2024-12-22 14:14:53 +00:00
|
|
|
let url = url.into();
|
2024-12-08 17:23:54 +00:00
|
|
|
APIResult::new(
|
2024-12-22 14:14:53 +00:00
|
|
|
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
|
|
|
|
),
|
2024-12-08 17:23:54 +00:00
|
|
|
&self.ctx.prt,
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn request_with_body<T: for<'a> serde::Deserialize<'a>>(
|
|
|
|
&self,
|
|
|
|
url: impl Into<String>,
|
|
|
|
body: impl Into<Bytes>,
|
|
|
|
custom: fn(&Net, String) -> ReqBuilder,
|
|
|
|
) -> APIResult<T> {
|
2024-12-22 14:14:53 +00:00
|
|
|
let url = self.url(url.into());
|
|
|
|
let body = body.into();
|
2024-12-08 17:23:54 +00:00
|
|
|
APIResult::new(
|
2024-12-22 14:14:53 +00:00
|
|
|
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
|
|
|
|
),
|
2024-12-16 08:03:31 +00:00
|
|
|
&self.ctx.prt,
|
2024-12-08 19:31:10 +00:00
|
|
|
)
|
|
|
|
.await
|
|
|
|
}
|
|
|
|
|
2024-12-08 17:23:54 +00:00
|
|
|
pub(crate) async fn get_boards(&self) -> APIResult<Vec<Board>> {
|
|
|
|
self.request("boards", |net, url| net.get(url)).await
|
|
|
|
}
|
|
|
|
|
2024-12-19 20:29:21 +00:00
|
|
|
pub(crate) async fn get_board(&self, board_id: NextcloudBoardId) -> APIResult<Board> {
|
|
|
|
self.request(f!("boards/{board_id}"), |net, url| net.get(url))
|
|
|
|
.await
|
|
|
|
}
|
|
|
|
|
2024-12-08 17:23:54 +00:00
|
|
|
pub(crate) async fn get_stacks(&self, board_id: NextcloudBoardId) -> APIResult<Vec<Stack>> {
|
|
|
|
self.request(f!("boards/{board_id}/stacks"), |net, url| net.get(url))
|
|
|
|
.await
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) async fn get_stack(
|
|
|
|
&self,
|
|
|
|
board_id: NextcloudBoardId,
|
|
|
|
stack_id: NextcloudStackId,
|
|
|
|
) -> APIResult<Stack> {
|
|
|
|
self.request(f!("boards/{board_id}/stacks/{stack_id}"), |net, url| {
|
|
|
|
net.get(url)
|
|
|
|
})
|
|
|
|
.await
|
|
|
|
}
|
|
|
|
|
2024-12-19 21:18:31 +00:00
|
|
|
pub(crate) async fn create_stack(
|
|
|
|
&self,
|
|
|
|
board_id: NextcloudBoardId,
|
2024-12-22 22:47:29 +00:00
|
|
|
stack_title: NextcloudStackTitle,
|
|
|
|
stack_order: NextcloudOrder,
|
2024-12-19 21:18:31 +00:00
|
|
|
) -> APIResult<Stack> {
|
|
|
|
self.request_with_body(
|
|
|
|
f!("boards/{board_id}/stacks"),
|
|
|
|
json!({
|
|
|
|
"title": stack_title,
|
2024-12-22 22:47:29 +00:00
|
|
|
"order": stack_order,
|
2024-12-19 21:18:31 +00:00
|
|
|
})
|
|
|
|
.to_string(),
|
|
|
|
|net, url| net.post(url),
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
}
|
|
|
|
|
2024-12-17 07:09:49 +00:00
|
|
|
pub(crate) async fn create_card(
|
|
|
|
&self,
|
|
|
|
board_id: NextcloudBoardId,
|
|
|
|
stack_id: NextcloudStackId,
|
2024-12-19 09:04:23 +00:00
|
|
|
title: &NextcloudCardTitle,
|
|
|
|
description: Option<&NextcloudCardDescription>,
|
2024-12-17 07:09:49 +00:00
|
|
|
) -> APIResult<Card> {
|
2024-12-08 19:31:10 +00:00
|
|
|
let mut body = json!({
|
2024-12-17 07:09:49 +00:00
|
|
|
"title": title,
|
2024-12-08 17:23:54 +00:00
|
|
|
});
|
|
|
|
|
2024-12-17 07:09:49 +00:00
|
|
|
if let Some(desc) = &description {
|
2024-12-08 17:23:54 +00:00
|
|
|
body["description"] = serde_json::Value::String(desc.to_string());
|
|
|
|
}
|
|
|
|
|
|
|
|
self.request_with_body(
|
2024-12-17 07:09:49 +00:00
|
|
|
f!("boards/{board_id}/stacks/{stack_id}/cards"),
|
2024-12-08 17:23:54 +00:00
|
|
|
body.to_string(),
|
|
|
|
|net, url| net.post(url),
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
}
|
|
|
|
|
2024-12-20 08:18:37 +00:00
|
|
|
pub(crate) async fn create_label(
|
|
|
|
&self,
|
|
|
|
board_id: NextcloudBoardId,
|
|
|
|
name: &NextcloudLabelTitle,
|
|
|
|
colour: &NextcloudLabelColour,
|
|
|
|
) -> APIResult<Label> {
|
|
|
|
self.request_with_body(
|
|
|
|
f!("boards/{board_id}/labels"),
|
|
|
|
json!({
|
|
|
|
"title": name,
|
|
|
|
"color": colour,
|
|
|
|
})
|
|
|
|
.to_string(),
|
|
|
|
|net, url| net.post(url),
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
}
|
|
|
|
|
2024-12-08 17:23:54 +00:00
|
|
|
pub(crate) async fn get_card(
|
|
|
|
&self,
|
|
|
|
board_id: NextcloudBoardId,
|
|
|
|
stack_id: NextcloudStackId,
|
|
|
|
card_id: NextcloudCardId,
|
|
|
|
) -> APIResult<Card> {
|
|
|
|
self.request(
|
|
|
|
f!("boards/{board_id}/stacks/{stack_id}/cards/{card_id}"),
|
|
|
|
|net, url| net.get(url),
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
}
|
2024-12-16 08:03:31 +00:00
|
|
|
|
|
|
|
pub(crate) async fn add_label_to_card(
|
|
|
|
&self,
|
|
|
|
board_id: NextcloudBoardId,
|
|
|
|
stack_id: NextcloudStackId,
|
|
|
|
card_id: NextcloudCardId,
|
|
|
|
label_id: NextcloudLabelId,
|
|
|
|
) -> APIResult<()> {
|
|
|
|
self.request_with_body(
|
|
|
|
f!("boards/{board_id}/stacks/{stack_id}/cards/{card_id}/assignLabel"),
|
|
|
|
json!({
|
|
|
|
"labelId": label_id
|
|
|
|
})
|
|
|
|
.to_string(),
|
|
|
|
|net, url| net.put(url),
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) async fn add_attachment_to_card(
|
|
|
|
&self,
|
|
|
|
board_id: NextcloudBoardId,
|
|
|
|
stack_id: NextcloudStackId,
|
|
|
|
card_id: NextcloudCardId,
|
2024-12-19 09:04:23 +00:00
|
|
|
file: &FileHandle,
|
2024-12-16 08:03:31 +00:00
|
|
|
) -> APIResult<Attachment> {
|
2024-12-22 14:14:53 +00:00
|
|
|
APIResult::new(
|
|
|
|
with_exponential_backoff!(&self.ctx, {
|
|
|
|
let form: multipart::Form = multipart::Form::new();
|
|
|
|
let form = form.text("type", "file");
|
|
|
|
let form = form
|
|
|
|
.file("file", file.as_pathbuf())
|
|
|
|
.await
|
|
|
|
.expect("read file");
|
|
|
|
self.ctx
|
|
|
|
.net
|
|
|
|
.send(
|
|
|
|
self.ctx
|
|
|
|
.net
|
|
|
|
.client()
|
|
|
|
.post(self.url(f!(
|
|
|
|
"boards/{board_id}/stacks/{stack_id}/cards/{card_id}/attachments"
|
|
|
|
)))
|
|
|
|
.basic_auth(self.username.as_str(), Some(self.password.as_str()))
|
|
|
|
.header("accept", "application/json")
|
|
|
|
.multipart(form),
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
}),
|
|
|
|
&self.ctx.prt,
|
2024-12-16 08:03:31 +00:00
|
|
|
)
|
|
|
|
.await
|
|
|
|
}
|
2024-12-08 17:23:54 +00:00
|
|
|
}
|