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 members_mode: Option<&str>,
102 timeout: Option<Duration>,
103 ) -> Result<TrelloImportResponse, ClientError> {
104 let mut endpoint = self.boards_api_endpoint("import-trello", "Trello import")?;
105 if let Some(mode) = members_mode {
106 endpoint.query_pairs_mut().append_pair("membersMode", mode);
107 }
108 let request = self
109 .http
110 .post(endpoint)
111 .header(CONTENT_TYPE, "application/zip")
112 .header(CONTENT_LENGTH, length)
113 .body(reqwest::Body::wrap_stream(ReaderStream::new(reader)));
114 self.send_trello_import(request, TrelloImportFormat::Zip, token, auth, timeout)
115 .await
116 }
117
118 async fn send_trello_import(
119 &self,
120 request: RequestBuilder,
121 format: TrelloImportFormat,
122 token: &SecretString,
123 auth: TrelloImportAuth,
124 timeout: Option<Duration>,
125 ) -> Result<TrelloImportResponse, ClientError> {
126 let mut request = request.header(ACCEPT, "application/json");
127 request = match auth {
128 TrelloImportAuth::Bearer => request.bearer_auth(token.expose_secret()),
129 TrelloImportAuth::XAuthToken => request.header("X-Auth-Token", token.expose_secret()),
130 TrelloImportAuth::MeteorCookie | TrelloImportAuth::WekanCookie => {
131 let name = if matches!(auth, TrelloImportAuth::MeteorCookie) {
132 "meteor_login_token"
133 } else {
134 "wekan_login_token"
135 };
136 let encoded =
137 url::form_urlencoded::byte_serialize(token.expose_secret().as_bytes())
138 .collect::<String>()
139 .replace('+', "%20");
140 request.header(COOKIE, format!("{name}={encoded}"))
141 }
142 };
143 if let Some(timeout) = timeout {
144 request = request.timeout(timeout);
145 }
146 let body = self
147 .execute_success_request_with_error::<ClientError>(request, |_, body| {
148 if let Ok(value) = serde_json::from_slice(body) {
151 let _: ErrorResponse =
152 crate::client::decoding::decode_value(value, "Trello import error", false)?;
153 }
154 Ok(None)
155 })
156 .await?;
157 match format {
158 TrelloImportFormat::Json => {
159 let response: JsonResponse = decode_success(&body, "Trello JSON import", true)?;
160 Ok(TrelloImportResponse {
161 board_ids: response.board_ids,
162 errors: None,
163 })
164 }
165 TrelloImportFormat::Zip => {
166 let response: ZipResponse = decode_success(&body, "Trello ZIP import", true)?;
167 Ok(TrelloImportResponse {
168 board_ids: response.board_ids,
169 errors: Some(response.errors),
170 })
171 }
172 }
173 }
174}