ajour_core/repository/backend/
tukui.rs

1use super::*;
2use crate::config::Flavor;
3use crate::error::{DownloadError, RepositoryError};
4use crate::network::request_async;
5use crate::repository::{ReleaseChannel, RemotePackage, RepositoryKind, RepositoryPackage};
6use crate::utility::{regex_html_tags_to_newline, regex_html_tags_to_space, truncate};
7
8use async_trait::async_trait;
9use chrono::{NaiveDateTime, TimeZone, Utc};
10use futures::future::join_all;
11use isahc::AsyncReadResponseExt;
12use serde::Deserialize;
13
14use std::collections::HashMap;
15
16#[derive(Debug, Clone)]
17pub struct Tukui {
18    pub id: String,
19    pub flavor: Flavor,
20}
21
22#[async_trait]
23impl Backend for Tukui {
24    async fn get_metadata(&self) -> Result<RepositoryMetadata, RepositoryError> {
25        let (_, package) = fetch_remote_package(&self.id, &self.flavor).await?;
26
27        let metadata = metadata_from_tukui_package(package);
28
29        Ok(metadata)
30    }
31
32    async fn get_changelog(
33        &self,
34        _file_id: Option<i64>,
35        _tag_name: Option<String>,
36    ) -> Result<Option<String>, RepositoryError> {
37        let url = changelog_endpoint(&self.id, &self.flavor);
38
39        match self.flavor {
40            Flavor::Retail | Flavor::RetailBeta | Flavor::RetailPtr => {
41                // Only TukUI and ElvUI main addons has changelog which can be fetched.
42                // The others is embeded into a page.
43                if &self.id == "-1" || &self.id == "-2" {
44                    let mut resp = request_async(&url, vec![], None).await?;
45
46                    if resp.status().is_success() {
47                        let changelog: String = resp.text().await?;
48
49                        let c = regex_html_tags_to_newline()
50                            .replace_all(&changelog, "\n")
51                            .to_string();
52                        let c = regex_html_tags_to_space().replace_all(&c, "").to_string();
53                        let c = truncate(&c, 2500).to_string();
54
55                        return Ok(Some(c));
56                    }
57                }
58            }
59            Flavor::Classic | Flavor::ClassicPtr | Flavor::ClassicBeta => {}
60        }
61
62        Ok(None)
63    }
64}
65
66pub(crate) fn metadata_from_tukui_package(package: TukuiPackage) -> RepositoryMetadata {
67    let mut remote_packages = HashMap::new();
68
69    {
70        let version = package.version.clone();
71        let download_url = package.url.clone();
72
73        let date_time = NaiveDateTime::parse_from_str(&package.lastupdate, "%Y-%m-%d %H:%M:%S")
74            .map_or(
75                NaiveDateTime::parse_from_str(
76                    &format!("{} 00:00:00", &package.lastupdate),
77                    "%Y-%m-%d %T",
78                ),
79                std::result::Result::Ok,
80            )
81            .map(|d| Utc.from_utc_datetime(&d))
82            .ok();
83
84        let package = RemotePackage {
85            version,
86            download_url,
87            date_time,
88            file_id: None,
89            modules: vec![],
90        };
91
92        // Since Tukui does not support release channels, our default is 'stable'.
93        remote_packages.insert(ReleaseChannel::Stable, package);
94    }
95
96    let website_url = Some(package.web_url.clone());
97    let changelog_url = Some(format!("{}&changelog", package.web_url));
98    let game_version = package.patch;
99    let title = package.name;
100
101    let mut metadata = RepositoryMetadata::empty();
102    metadata.website_url = website_url;
103    metadata.changelog_url = changelog_url;
104    metadata.game_version = game_version;
105    metadata.remote_packages = remote_packages;
106    metadata.title = Some(title);
107
108    metadata
109}
110
111/// Returns flavor `String` in Tukui format
112fn format_flavor(flavor: &Flavor) -> String {
113    let base_flavor = flavor.base_flavor();
114    match base_flavor {
115        Flavor::Retail => "retail".to_owned(),
116        Flavor::Classic => "classic".to_owned(),
117        _ => panic!("Unknown base flavor {}", base_flavor),
118    }
119}
120
121/// Return the tukui API endpoint.
122fn api_endpoint(id: &str, flavor: &Flavor) -> String {
123    format!(
124        "https://hub.wowup.io/tukui/{}/{}",
125        format_flavor(flavor),
126        id
127    )
128}
129
130fn changelog_endpoint(id: &str, flavor: &Flavor) -> String {
131    match flavor {
132        Flavor::Retail | Flavor::RetailPtr | Flavor::RetailBeta => match id {
133            "-1" => "https://www.tukui.org/ui/tukui/changelog".to_owned(),
134            "-2" => "https://www.tukui.org/ui/elvui/changelog".to_owned(),
135            _ => format!("https://www.tukui.org/addons.php?id={}&changelog", id),
136        },
137        Flavor::Classic | Flavor::ClassicPtr | Flavor::ClassicBeta => format!(
138            "https://www.tukui.org/classic-addons.php?id={}&changelog",
139            id
140        ),
141    }
142}
143
144pub(crate) async fn batch_fetch_repo_packages(
145    flavor: Flavor,
146    tukui_ids: &[String],
147) -> Result<Vec<RepositoryPackage>, DownloadError> {
148    let mut tukui_repo_packages = vec![];
149
150    if tukui_ids.is_empty() {
151        return Ok(tukui_repo_packages);
152    }
153
154    let fetch_tasks: Vec<_> = tukui_ids
155        .iter()
156        .map(|id| tukui::fetch_remote_package(&id, &flavor))
157        .collect();
158
159    tukui_repo_packages.extend(
160        join_all(fetch_tasks)
161            .await
162            .into_iter()
163            .filter_map(Result::ok)
164            .map(|(id, package)| (id, tukui::metadata_from_tukui_package(package)))
165            .filter_map(|(id, metadata)| {
166                RepositoryPackage::from_repo_id(flavor, RepositoryKind::Tukui, id)
167                    .ok()
168                    .map(|r| r.with_metadata(metadata))
169            }),
170    );
171
172    Ok(tukui_repo_packages)
173}
174
175/// Function to fetch a remote addon package which contains
176/// information about the addon on the repository.
177pub(crate) async fn fetch_remote_package(
178    id: &str,
179    flavor: &Flavor,
180) -> Result<(String, TukuiPackage), DownloadError> {
181    let url = api_endpoint(id, flavor);
182
183    let timeout = Some(30);
184    let mut resp = request_async(&url, vec![], timeout).await?;
185
186    if resp.status().is_success() {
187        let package = resp.json().await?;
188        Ok((id.to_string(), package))
189    } else {
190        Err(DownloadError::InvalidStatusCode {
191            code: resp.status(),
192            url,
193        })
194    }
195}
196
197#[derive(Clone, Debug, Deserialize)]
198/// Struct for applying tukui details to an `Addon`.
199pub struct TukuiPackage {
200    pub name: String,
201    pub version: String,
202    pub url: String,
203    pub web_url: String,
204    pub lastupdate: String,
205    pub patch: Option<String>,
206    pub author: Option<String>,
207    pub small_desc: Option<String>,
208}