rust_rcs_client/chat_bot/
mod.rs

1// Copyright 2023 宋昊文
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15pub mod chatbot_config;
16pub mod chatbot_sip_uri;
17pub mod cpim;
18pub mod ffi;
19
20use std::sync::Arc;
21
22use futures::{future::BoxFuture, AsyncReadExt, FutureExt};
23use rust_rcs_core::{
24    ffi::log::platform_log,
25    http::{
26        request::{Request, GET},
27        HttpClient,
28    },
29    internet::{header, headers::cache_control, Header},
30    security::{
31        authentication::digest::DigestAnswerParams,
32        gba::{self, GbaContext},
33        SecurityContext,
34    },
35};
36use url::Url;
37
38const LOG_TAG: &str = "chat_bot";
39
40pub enum RetrieveSpecificChatbotsSuccess {
41    Ok(String, Option<String>, u32),
42    NotModified(Option<String>, u32),
43}
44
45#[derive(Debug)]
46pub enum RetrieveSpecificChatbotsError {
47    Http(u16, String),
48    NetworkIO,
49    MalformedUrl,
50}
51
52impl RetrieveSpecificChatbotsError {
53    pub fn error_code(&self) -> u16 {
54        match self {
55            RetrieveSpecificChatbotsError::Http(status_code, _) => *status_code,
56            RetrieveSpecificChatbotsError::NetworkIO => 0,
57            RetrieveSpecificChatbotsError::MalformedUrl => 0,
58        }
59    }
60
61    pub fn error_string(&self) -> String {
62        match self {
63            RetrieveSpecificChatbotsError::Http(_, reason_phrase) => String::from(reason_phrase),
64            RetrieveSpecificChatbotsError::NetworkIO => String::from("NetworkIO"),
65            RetrieveSpecificChatbotsError::MalformedUrl => String::from("MalformedUrl"),
66        }
67    }
68}
69
70async fn retrieve_specific_chatbots_inner(
71    specific_chatbots_list_url: &str,
72    local_etag: Option<&str>,
73    msisdn: Option<&str>,
74    http_client: &Arc<HttpClient>,
75    gba_context: &Arc<GbaContext>,
76    security_context: &Arc<SecurityContext>,
77    digest_answer: Option<&DigestAnswerParams>,
78) -> Result<RetrieveSpecificChatbotsSuccess, RetrieveSpecificChatbotsError> {
79    if let Ok(url) = Url::parse(specific_chatbots_list_url) {
80        if let Ok(conn) = http_client.connect(&url, false).await {
81            let host = url.host_str().unwrap();
82
83            let mut req = Request::new_with_default_headers(GET, host, url.path(), url.query());
84
85            if let Some(msisdn) = msisdn {
86                req.headers.push(Header::new(
87                    b"X-3GPP-Intended-Identity",
88                    format!("tel:{}", msisdn),
89                ));
90            }
91
92            if let Some(local_etag) = local_etag {
93                req.headers
94                    .push(Header::new(b"If-None-Match", String::from(local_etag)));
95            }
96
97            let preloaded_answer = match digest_answer {
98                Some(_) => None,
99                None => {
100                    platform_log(LOG_TAG, "using stored authorization info");
101                    security_context.preload_auth(gba_context, host, conn.cipher_id(), GET, None)
102                }
103            };
104
105            let digest_answer = match digest_answer {
106                Some(digest_answer) => Some(digest_answer),
107                None => match &preloaded_answer {
108                    Some(preloaded_answer) => {
109                        platform_log(LOG_TAG, "using preloaded digest answer");
110                        Some(preloaded_answer)
111                    }
112                    None => None,
113                },
114            };
115
116            if let Some(digest_answer) = digest_answer {
117                if let Ok(authorization) = digest_answer.make_authorization_header(
118                    match &digest_answer.challenge {
119                        Some(challenge) => Some(&challenge.algorithm),
120                        None => None,
121                    },
122                    false,
123                    false,
124                ) {
125                    if let Ok(authorization) = String::from_utf8(authorization) {
126                        req.headers
127                            .push(Header::new(b"Authorization", String::from(authorization)));
128                    }
129                }
130            }
131
132            if let Ok((resp, resp_stream)) = conn.send(req, |_| {}).await {
133                platform_log(
134                    LOG_TAG,
135                    format!(
136                        "retrieve_specific_chatbots_inner resp.status_code = {}",
137                        resp.status_code
138                    ),
139                );
140
141                if (resp.status_code >= 200 && resp.status_code < 300) || resp.status_code == 304 {
142                    if let Some(authentication_info_header) =
143                        header::search(&resp.headers, b"Authentication-Info", false)
144                    {
145                        if let Some(digest_answer) = digest_answer {
146                            if let Some(challenge) = &digest_answer.challenge {
147                                security_context.update_auth_info(
148                                    authentication_info_header,
149                                    host,
150                                    b"\"",
151                                    challenge,
152                                    false,
153                                );
154                            }
155                        }
156                    }
157                }
158
159                if resp.status_code == 200 || resp.status_code == 304 {
160                    let response_etag =
161                        if let Some(etag_header) = header::search(&resp.headers, b"ETag", false) {
162                            match String::from_utf8(etag_header.get_value().to_vec()) {
163                                Ok(etag) => Some(etag),
164                                Err(_) => None,
165                            }
166                        } else {
167                            None
168                        };
169
170                    let expiry = if let Some(cache_control_header) =
171                        header::search(&resp.headers, b"Cache-Control", false)
172                    {
173                        let cc = cache_control::parse(cache_control_header.get_value());
174                        cc.max_age
175                    } else {
176                        0
177                    };
178
179                    if resp.status_code == 200 {
180                        if let Some(mut resp_stream) = resp_stream {
181                            let mut resp_data = Vec::new();
182                            match resp_stream.read_to_end(&mut resp_data).await {
183                                Ok(size) => {
184                                    platform_log(
185                                        LOG_TAG,
186                                        format!(
187                                            "retrieve_specific_chatbots_inner resp.data read {} bytes",
188                                            &size
189                                        ),
190                                    );
191
192                                    if let Ok(resp_string) = String::from_utf8(resp_data) {
193                                        platform_log(
194                                            LOG_TAG,
195                                            format!(
196                                                "retrieve_specific_chatbots_inner resp.string = {}",
197                                                &resp_string
198                                            ),
199                                        );
200
201                                        return Ok(RetrieveSpecificChatbotsSuccess::Ok(
202                                            resp_string,
203                                            response_etag,
204                                            expiry,
205                                        ));
206                                    }
207                                }
208
209                                Err(e) => {
210                                    platform_log(LOG_TAG, format!("retrieve_specific_chatbots_inner error reading response stream: {:?}", e));
211                                }
212                            }
213                        } else {
214                            platform_log(
215                                LOG_TAG,
216                                "retrieve_specific_chatbots_inner missing response body",
217                            );
218                        }
219                    } else {
220                        return Ok(RetrieveSpecificChatbotsSuccess::NotModified(
221                            response_etag,
222                            expiry,
223                        ));
224                    }
225                } else if resp.status_code == 401 {
226                    if digest_answer.is_none() {
227                        if let Some(www_authenticate_header) =
228                            header::search(&resp.headers, b"WWW-Authenticate", false)
229                        {
230                            if let Some(Ok(answer)) = gba::try_process_401_response(
231                                gba_context,
232                                host.as_bytes(),
233                                conn.cipher_id(),
234                                GET,
235                                b"\"/\"",
236                                None,
237                                www_authenticate_header,
238                                http_client,
239                                security_context,
240                            )
241                            .await
242                            {
243                                return retrieve_specific_chatbots(
244                                    specific_chatbots_list_url,
245                                    local_etag,
246                                    msisdn,
247                                    http_client,
248                                    gba_context,
249                                    security_context,
250                                    Some(&answer),
251                                )
252                                .await;
253                            }
254                        }
255                    }
256                } else {
257                    return Err(RetrieveSpecificChatbotsError::Http(
258                        resp.status_code,
259                        match String::from_utf8(resp.reason_phrase) {
260                            Ok(reason_phrase) => reason_phrase,
261                            Err(_) => String::from(""),
262                        },
263                    ));
264                }
265            }
266        }
267
268        Err(RetrieveSpecificChatbotsError::NetworkIO)
269    } else {
270        Err(RetrieveSpecificChatbotsError::MalformedUrl)
271    }
272}
273
274pub fn retrieve_specific_chatbots<'a, 'b: 'a>(
275    specific_chatbots_list_url: &'b str,
276    local_etag: Option<&'b str>,
277    msisdn: Option<&'b str>,
278    http_client: &'b Arc<HttpClient>,
279    gba_context: &'b Arc<GbaContext>,
280    security_context: &'b Arc<SecurityContext>,
281    digest_answer: Option<&'a DigestAnswerParams>,
282) -> BoxFuture<'a, Result<RetrieveSpecificChatbotsSuccess, RetrieveSpecificChatbotsError>> {
283    async move {
284        retrieve_specific_chatbots_inner(
285            specific_chatbots_list_url,
286            local_etag,
287            msisdn,
288            http_client,
289            gba_context,
290            security_context,
291            digest_answer,
292        )
293        .await
294    }
295    .boxed()
296}
297
298#[derive(Debug)]
299pub enum SearchChatbotError {
300    Http(u16, String),
301    NetworkIO,
302    MalformedUrl,
303}
304
305impl SearchChatbotError {
306    pub fn error_code(&self) -> u16 {
307        match self {
308            SearchChatbotError::Http(status_code, _) => *status_code,
309            SearchChatbotError::NetworkIO => 0,
310            SearchChatbotError::MalformedUrl => 0,
311        }
312    }
313
314    pub fn error_string(&self) -> String {
315        match self {
316            SearchChatbotError::Http(_, reason_phrase) => String::from(reason_phrase),
317            SearchChatbotError::NetworkIO => String::from("NetworkIO"),
318            SearchChatbotError::MalformedUrl => String::from("MalformedUrl"),
319        }
320    }
321}
322
323async fn search_chatbot_directory_inner(
324    chatbot_directory: &str,
325    query: &str,
326    start: u32,
327    num: u32,
328    home_operator: &str,
329    msisdn: Option<&str>,
330    http_client: &Arc<HttpClient>,
331    gba_context: &Arc<GbaContext>,
332    security_context: &Arc<SecurityContext>,
333    digest_answer: Option<&'_ DigestAnswerParams>,
334) -> Result<String, SearchChatbotError> {
335    let mut first_query = true; // to-do: chatbot_directory might already contain a query
336    let mut url_string = String::from(chatbot_directory);
337    if query.len() != 0 {
338        url_string = format!("{}?q={}", url_string, query);
339        first_query = false;
340    }
341
342    if start != 0 {
343        url_string = if first_query {
344            format!("{}?start={}", url_string, query)
345        } else {
346            format!("{}&start={}", url_string, query)
347        };
348        first_query = false;
349    }
350
351    url_string = if first_query {
352        format!(
353            "{}&num={}&ho={}&chatbot_version=1_2&client_vendor=Rusty&client_version=1.0.0",
354            url_string, num, home_operator
355        )
356    } else {
357        format!(
358            "{}&num={}&ho={}&chatbot_version=1_2&client_vendor=Rusty&client_version=1.0.0",
359            url_string, num, home_operator
360        )
361    };
362
363    platform_log(LOG_TAG, format!("url: {}", &url_string));
364
365    if let Ok(url) = Url::parse(&url_string) {
366        if let Ok(conn) = http_client.connect(&url, false).await {
367            let host = url.host_str().unwrap();
368
369            let mut req = Request::new_with_default_headers(GET, host, url.path(), url.query());
370
371            if let Some(msisdn) = msisdn {
372                req.headers.push(Header::new(
373                    b"X-3GPP-Intended-Identity",
374                    format!("tel:{}", msisdn),
375                ));
376            }
377
378            let preloaded_answer = match digest_answer {
379                Some(_) => None,
380                None => {
381                    platform_log(LOG_TAG, "using stored authorization info");
382                    security_context.preload_auth(gba_context, host, conn.cipher_id(), GET, None)
383                }
384            };
385
386            let digest_answer = match digest_answer {
387                Some(digest_answer) => Some(digest_answer),
388                None => match &preloaded_answer {
389                    Some(preloaded_answer) => {
390                        platform_log(LOG_TAG, "using preloaded digest answer");
391                        Some(preloaded_answer)
392                    }
393                    None => None,
394                },
395            };
396
397            if let Some(digest_answer) = digest_answer {
398                if let Ok(authorization) = digest_answer.make_authorization_header(
399                    match &digest_answer.challenge {
400                        Some(challenge) => Some(&challenge.algorithm),
401                        None => None,
402                    },
403                    false,
404                    false,
405                ) {
406                    if let Ok(authorization) = String::from_utf8(authorization) {
407                        req.headers
408                            .push(Header::new(b"Authorization", String::from(authorization)));
409                    }
410                }
411            }
412
413            if let Ok((resp, resp_stream)) = conn.send(req, |_| {}).await {
414                platform_log(
415                    LOG_TAG,
416                    format!(
417                        "search_chatbot_directory_inner resp.status_code = {}",
418                        resp.status_code
419                    ),
420                );
421
422                if resp.status_code == 200 {
423                    if let Some(authentication_info_header) =
424                        header::search(&resp.headers, b"Authentication-Info", false)
425                    {
426                        if let Some(digest_answer) = digest_answer {
427                            if let Some(challenge) = &digest_answer.challenge {
428                                security_context.update_auth_info(
429                                    authentication_info_header,
430                                    host,
431                                    b"\"",
432                                    challenge,
433                                    false,
434                                );
435                            }
436                        }
437                    }
438
439                    if let Some(mut resp_stream) = resp_stream {
440                        let mut resp_data = Vec::new();
441                        match resp_stream.read_to_end(&mut resp_data).await {
442                            Ok(size) => {
443                                platform_log(
444                                    LOG_TAG,
445                                    format!(
446                                        "search_chatbot_directory_inner resp.data read {} bytes",
447                                        &size
448                                    ),
449                                );
450
451                                if let Ok(resp_string) = String::from_utf8(resp_data) {
452                                    platform_log(
453                                        LOG_TAG,
454                                        format!(
455                                            "search_chatbot_directory_inner resp.string = {}",
456                                            &resp_string
457                                        ),
458                                    );
459
460                                    return Ok(resp_string);
461                                }
462                            }
463
464                            Err(e) => {
465                                platform_log(LOG_TAG, format!("search_chatbot_directory_inner error reading response stream: {:?}", e));
466                            }
467                        }
468                    } else {
469                        platform_log(
470                            LOG_TAG,
471                            "search_chatbot_directory_inner missing response body",
472                        );
473                    }
474                } else if resp.status_code == 401 {
475                    if digest_answer.is_none() {
476                        if let Some(www_authenticate_header) =
477                            header::search(&resp.headers, b"WWW-Authenticate", false)
478                        {
479                            if let Some(Ok(answer)) = gba::try_process_401_response(
480                                gba_context,
481                                host.as_bytes(),
482                                conn.cipher_id(),
483                                GET,
484                                b"\"/\"",
485                                None,
486                                www_authenticate_header,
487                                http_client,
488                                security_context,
489                            )
490                            .await
491                            {
492                                return search_chatbot_directory(
493                                    chatbot_directory,
494                                    query,
495                                    start,
496                                    num,
497                                    home_operator,
498                                    msisdn,
499                                    http_client,
500                                    gba_context,
501                                    security_context,
502                                    Some(&answer),
503                                )
504                                .await;
505                            }
506                        }
507                    }
508                } else {
509                    return Err(SearchChatbotError::Http(
510                        resp.status_code,
511                        match String::from_utf8(resp.reason_phrase) {
512                            Ok(reason_phrase) => reason_phrase,
513                            Err(_) => String::from(""),
514                        },
515                    ));
516                }
517            }
518        }
519
520        Err(SearchChatbotError::NetworkIO)
521    } else {
522        Err(SearchChatbotError::MalformedUrl)
523    }
524}
525
526pub fn search_chatbot_directory<'a, 'b: 'a>(
527    chatbot_directory: &'b str,
528    query: &'b str,
529    start: u32,
530    num: u32,
531    home_operator: &'b str,
532    msisdn: Option<&'b str>,
533    http_client: &'b Arc<HttpClient>,
534    gba_context: &'b Arc<GbaContext>,
535    security_context: &'b Arc<SecurityContext>,
536    digest_answer: Option<&'a DigestAnswerParams>,
537) -> BoxFuture<'a, Result<String, SearchChatbotError>> {
538    async move {
539        search_chatbot_directory_inner(
540            chatbot_directory,
541            query,
542            start,
543            num,
544            home_operator,
545            msisdn,
546            http_client,
547            gba_context,
548            security_context,
549            digest_answer,
550        )
551        .await
552    }
553    .boxed()
554}
555
556pub enum RetrieveChatbotInfoSuccess {
557    Ok(String, Option<String>, u32),
558    NotModified(Option<String>, u32),
559}
560
561#[derive(Debug)]
562pub enum RetrieveChatbotInfoError {
563    Http(u16, String),
564    NetworkIO,
565    MalformedUrl,
566}
567
568impl RetrieveChatbotInfoError {
569    pub fn error_code(&self) -> u16 {
570        match self {
571            RetrieveChatbotInfoError::Http(status_code, _) => *status_code,
572            RetrieveChatbotInfoError::NetworkIO => 0,
573            RetrieveChatbotInfoError::MalformedUrl => 0,
574        }
575    }
576
577    pub fn error_string(&self) -> String {
578        match self {
579            RetrieveChatbotInfoError::Http(_, reason_phrase) => String::from(reason_phrase),
580            RetrieveChatbotInfoError::NetworkIO => String::from("NetworkIO"),
581            RetrieveChatbotInfoError::MalformedUrl => String::from("MalformedUrl"),
582        }
583    }
584}
585
586async fn retrieve_chatbot_info_inner(
587    bot_domain: &str,
588    bot_id: &str,
589    local_etag: Option<&str>,
590    home_operator: &str,
591    home_language: &str,
592    msisdn: Option<&str>,
593    http_client: &Arc<HttpClient>,
594    gba_context: &Arc<GbaContext>,
595    security_context: &Arc<SecurityContext>,
596    digest_answer: Option<&DigestAnswerParams>,
597) -> Result<RetrieveChatbotInfoSuccess, RetrieveChatbotInfoError> {
598    let url_string = format!(
599        "https://{}/bot?id={}&hl={}&ho={}&v=3",
600        bot_domain,
601        urlencoding::encode(bot_id),
602        home_language,
603        home_operator
604    );
605
606    if let Ok(url) = Url::parse(&url_string) {
607        if let Ok(conn) = http_client.connect(&url, false).await {
608            let host = url.host_str().unwrap();
609
610            let mut req = Request::new_with_default_headers(GET, host, url.path(), url.query());
611
612            if let Some(msisdn) = msisdn {
613                req.headers.push(Header::new(
614                    b"X-3GPP-Intended-Identity",
615                    format!("tel:{}", msisdn),
616                ));
617            }
618
619            if let Some(local_etag) = local_etag {
620                req.headers
621                    .push(Header::new(b"If-None-Match", String::from(local_etag)));
622            }
623
624            let preloaded_answer = match digest_answer {
625                Some(_) => None,
626                None => {
627                    platform_log(LOG_TAG, "using stored authorization info");
628                    security_context.preload_auth(gba_context, host, conn.cipher_id(), GET, None)
629                }
630            };
631
632            let digest_answer = match digest_answer {
633                Some(digest_answer) => Some(digest_answer),
634                None => match &preloaded_answer {
635                    Some(preloaded_answer) => {
636                        platform_log(LOG_TAG, "using preloaded digest answer");
637                        Some(preloaded_answer)
638                    }
639                    None => None,
640                },
641            };
642
643            if let Some(digest_answer) = digest_answer {
644                if let Ok(authorization) = digest_answer.make_authorization_header(
645                    match &digest_answer.challenge {
646                        Some(challenge) => Some(&challenge.algorithm),
647                        None => None,
648                    },
649                    false,
650                    false,
651                ) {
652                    if let Ok(authorization) = String::from_utf8(authorization) {
653                        req.headers
654                            .push(Header::new(b"Authorization", String::from(authorization)));
655                    }
656                }
657            }
658
659            if let Ok((resp, resp_stream)) = conn.send(req, |_| {}).await {
660                platform_log(
661                    LOG_TAG,
662                    format!(
663                        "retrieve_chatbot_info_inner resp.status_code = {}",
664                        resp.status_code
665                    ),
666                );
667
668                if (resp.status_code >= 200 && resp.status_code < 300) || resp.status_code == 304 {
669                    if let Some(authentication_info_header) =
670                        header::search(&resp.headers, b"Authentication-Info", false)
671                    {
672                        if let Some(digest_answer) = digest_answer {
673                            if let Some(challenge) = &digest_answer.challenge {
674                                security_context.update_auth_info(
675                                    authentication_info_header,
676                                    host,
677                                    b"\"",
678                                    challenge,
679                                    false,
680                                );
681                            }
682                        }
683                    }
684                }
685
686                if resp.status_code == 200 || resp.status_code == 304 {
687                    let response_etag =
688                        if let Some(etag_header) = header::search(&resp.headers, b"ETag", false) {
689                            match String::from_utf8(etag_header.get_value().to_vec()) {
690                                Ok(etag) => Some(etag),
691                                Err(_) => None,
692                            }
693                        } else {
694                            None
695                        };
696
697                    let expiry = if let Some(cache_control_header) =
698                        header::search(&resp.headers, b"Cache-Control", false)
699                    {
700                        let cc = cache_control::parse(cache_control_header.get_value());
701                        cc.max_age
702                    } else {
703                        7 * 24 * 60 * 60
704                    };
705
706                    if resp.status_code == 200 {
707                        if let Some(mut resp_stream) = resp_stream {
708                            let mut resp_data = Vec::new();
709                            match resp_stream.read_to_end(&mut resp_data).await {
710                                Ok(size) => {
711                                    platform_log(
712                                        LOG_TAG,
713                                        format!(
714                                            "retrieve_chatbot_info_inner resp.data read {} bytes",
715                                            &size
716                                        ),
717                                    );
718
719                                    if let Ok(resp_string) = String::from_utf8(resp_data) {
720                                        platform_log(
721                                            LOG_TAG,
722                                            format!(
723                                                "retrieve_chatbot_info_inner resp.string = {}",
724                                                &resp_string
725                                            ),
726                                        );
727
728                                        return Ok(RetrieveChatbotInfoSuccess::Ok(
729                                            resp_string,
730                                            response_etag,
731                                            expiry,
732                                        ));
733                                    }
734                                }
735
736                                Err(e) => {
737                                    platform_log(LOG_TAG, format!("retrieve_chatbot_info_inner error reading response stream: {:?}", e));
738                                }
739                            }
740                        } else {
741                            platform_log(
742                                LOG_TAG,
743                                "retrieve_chatbot_info_inner missing response body",
744                            );
745                        }
746                    } else {
747                        return Ok(RetrieveChatbotInfoSuccess::NotModified(
748                            response_etag,
749                            expiry,
750                        ));
751                    }
752                } else if resp.status_code == 401 {
753                    if digest_answer.is_none() {
754                        if let Some(www_authenticate_header) =
755                            header::search(&resp.headers, b"WWW-Authenticate", false)
756                        {
757                            if let Some(Ok(answer)) = gba::try_process_401_response(
758                                gba_context,
759                                host.as_bytes(),
760                                conn.cipher_id(),
761                                GET,
762                                b"\"/\"",
763                                None,
764                                www_authenticate_header,
765                                http_client,
766                                security_context,
767                            )
768                            .await
769                            {
770                                return retrieve_chatbot_info(
771                                    bot_domain,
772                                    bot_id,
773                                    local_etag,
774                                    home_operator,
775                                    home_language,
776                                    msisdn,
777                                    http_client,
778                                    gba_context,
779                                    security_context,
780                                    Some(&answer),
781                                )
782                                .await;
783                            }
784                        }
785                    }
786                } else {
787                    return Err(RetrieveChatbotInfoError::Http(
788                        resp.status_code,
789                        match String::from_utf8(resp.reason_phrase) {
790                            Ok(reason_phrase) => reason_phrase,
791                            Err(_) => String::from(""),
792                        },
793                    ));
794                }
795            }
796        }
797
798        Err(RetrieveChatbotInfoError::NetworkIO)
799    } else {
800        Err(RetrieveChatbotInfoError::MalformedUrl)
801    }
802}
803
804pub fn retrieve_chatbot_info<'a, 'b: 'a>(
805    bot_domain: &'b str,
806    bot_id: &'b str,
807    local_etag: Option<&'b str>,
808    home_operator: &'b str,
809    home_language: &'b str,
810    msisdn: Option<&'b str>,
811    http_client: &'b Arc<HttpClient>,
812    gba_context: &'b Arc<GbaContext>,
813    security_context: &'b Arc<SecurityContext>,
814    digest_answer: Option<&'a DigestAnswerParams>,
815) -> BoxFuture<'a, Result<RetrieveChatbotInfoSuccess, RetrieveChatbotInfoError>> {
816    async move {
817        retrieve_chatbot_info_inner(
818            bot_domain,
819            bot_id,
820            local_etag,
821            home_operator,
822            home_language,
823            msisdn,
824            http_client,
825            gba_context,
826            security_context,
827            digest_answer,
828        )
829        .await
830    }
831    .boxed()
832}