1use std::time::Duration;
3
4use reqwest::{
5 RequestBuilder,
6 header::{ACCEPT, CONTENT_LENGTH, CONTENT_TYPE, COOKIE},
7};
8use secrecy::{ExposeSecret, SecretString};
9use serde::{Deserialize, Serialize};
10use tokio::io::AsyncRead;
11use tokio_util::io::ReaderStream;
12
13use super::import::BoardImportRequest;
14use crate::client::{
15 ClientError, WekanClient, imports::PreparedImportRequest, transport::decode_success,
16};
17
18#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
19#[serde(rename_all = "lowercase")]
20pub enum TrelloImportFormat {
21 Json,
22 Zip,
23}
24
25#[derive(Clone, Copy, Debug)]
26pub enum TrelloImportAuth {
27 Bearer,
28 XAuthToken,
29 MeteorCookie,
30 WekanCookie,
31}
32
33#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
36#[serde(untagged)]
37pub enum TrelloImportBoardName {
38 String(String),
39 Number(serde_json::Number),
40 Boolean(bool),
41}
42
43#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
44#[serde(deny_unknown_fields)]
45pub struct TrelloImportBoardError {
46 pub board: TrelloImportBoardName,
47 pub error: String,
48}
49
50#[derive(Debug, Deserialize)]
51#[serde(deny_unknown_fields)]
52struct JsonResponse {
53 #[serde(rename = "boardIds")]
54 board_ids: Vec<String>,
55}
56
57#[derive(Debug, Deserialize)]
58#[serde(deny_unknown_fields)]
59struct ZipResponse {
60 #[serde(rename = "boardIds")]
61 board_ids: Vec<String>,
62 errors: Vec<TrelloImportBoardError>,
63}
64
65#[derive(Deserialize)]
66#[serde(deny_unknown_fields)]
67struct ErrorResponse {
68 #[serde(rename = "error")]
69 _error: String,
70}
71
72#[derive(Debug)]
73pub struct TrelloImportResponse {
74 pub board_ids: Vec<String>,
75 pub errors: Option<Vec<TrelloImportBoardError>>,
76}
77
78impl WekanClient {
79 pub async fn import_trello_json(
80 &self,
81 body: PreparedImportRequest<BoardImportRequest>,
82 token: &SecretString,
83 auth: TrelloImportAuth,
84 timeout: Option<Duration>,
85 ) -> Result<TrelloImportResponse, ClientError> {
86 let request = self
87 .http
88 .post(self.boards_api_endpoint("import-trello", "Trello import")?)
89 .header(CONTENT_TYPE, "application/json")
90 .body(body.into_body());
91 self.send_trello_import(request, TrelloImportFormat::Json, token, auth, timeout)
92 .await
93 }
94
95 pub async fn import_trello_zip(
96 &self,
97 reader: impl AsyncRead + Send + Unpin + 'static,
98 length: u64,
99 token: &SecretString,
100 auth: TrelloImportAuth,
101 timeout: Option<Duration>,
102 ) -> Result<TrelloImportResponse, ClientError> {
103 let request = self
104 .http
105 .post(self.boards_api_endpoint("import-trello", "Trello import")?)
106 .header(CONTENT_TYPE, "application/zip")
107 .header(CONTENT_LENGTH, length)
108 .body(reqwest::Body::wrap_stream(ReaderStream::new(reader)));
109 self.send_trello_import(request, TrelloImportFormat::Zip, token, auth, timeout)
110 .await
111 }
112
113 async fn send_trello_import(
114 &self,
115 request: RequestBuilder,
116 format: TrelloImportFormat,
117 token: &SecretString,
118 auth: TrelloImportAuth,
119 timeout: Option<Duration>,
120 ) -> Result<TrelloImportResponse, ClientError> {
121 let mut request = request.header(ACCEPT, "application/json");
122 request = match auth {
123 TrelloImportAuth::Bearer => request.bearer_auth(token.expose_secret()),
124 TrelloImportAuth::XAuthToken => request.header("X-Auth-Token", token.expose_secret()),
125 TrelloImportAuth::MeteorCookie | TrelloImportAuth::WekanCookie => {
126 let name = if matches!(auth, TrelloImportAuth::MeteorCookie) {
127 "meteor_login_token"
128 } else {
129 "wekan_login_token"
130 };
131 let encoded =
132 url::form_urlencoded::byte_serialize(token.expose_secret().as_bytes())
133 .collect::<String>()
134 .replace('+', "%20");
135 request.header(COOKIE, format!("{name}={encoded}"))
136 }
137 };
138 if let Some(timeout) = timeout {
139 request = request.timeout(timeout);
140 }
141 let body = self
142 .execute_success_request_with_error::<ClientError>(request, |_, body| {
143 if let Ok(value) = serde_json::from_slice(body) {
146 let _: ErrorResponse =
147 crate::client::decoding::decode_value(value, "Trello import error", false)?;
148 }
149 Ok(None)
150 })
151 .await?;
152 match format {
153 TrelloImportFormat::Json => {
154 let response: JsonResponse = decode_success(&body, "Trello JSON import", true)?;
155 Ok(TrelloImportResponse {
156 board_ids: response.board_ids,
157 errors: None,
158 })
159 }
160 TrelloImportFormat::Zip => {
161 let response: ZipResponse = decode_success(&body, "Trello ZIP import", true)?;
162 Ok(TrelloImportResponse {
163 board_ids: response.board_ids,
164 errors: Some(response.errors),
165 })
166 }
167 }
168 }
169}