1
/*
2
Copyright (C) 2021-2023 Kunal Mehta <legoktm@debian.org>
3

            
4
This program is free software: you can redistribute it and/or modify
5
it under the terms of the GNU General Public License as published by
6
the Free Software Foundation, either version 3 of the License, or
7
(at your option) any later version.
8

            
9
This program is distributed in the hope that it will be useful,
10
but WITHOUT ANY WARRANTY; without even the implied warranty of
11
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
GNU General Public License for more details.
13

            
14
You should have received a copy of the GNU General Public License
15
along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
 */
17

            
18
use serde::Deserialize;
19
use serde_json::Value;
20
use std::fmt::{Display, Formatter};
21
use std::io;
22
use thiserror::Error as ThisError;
23

            
24
/// Represents a raw MediaWiki API error, with a error code and error message
25
/// (text). This is also used for warnings since they use the same format.
26
#[derive(Clone, Debug, Deserialize)]
27
pub struct ApiError {
28
    /// Error code
29
    pub code: String,
30
    /// Error message
31
    pub text: String,
32
    /// Extra data
33
    pub data: Option<Value>,
34
}
35

            
36
impl Display for ApiError {
37
24
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
38
24
        write!(f, "(code: {}): {}", self.code, self.text)
39
24
    }
40
}
41

            
42
/// Possible errors
43
#[non_exhaustive]
44
#[derive(ThisError, Debug)]
45
pub enum Error {
46
    /* Request related errors */
47
    /// A HTTP error like a 4XX or 5XX status code
48
    #[error("HTTP error: {0}")]
49
    HttpError(#[from] reqwest::Error),
50
    /// Invalid header value, likely if the provided OAuth2 token
51
    /// or User-agent are invalid
52
    #[error("Invalid header value: {0}")]
53
    InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue),
54
    /// Error when decoding the JSON response from the API
55
    #[error("JSON error: {0}")]
56
    InvalidJson(#[from] serde_json::Error),
57
    /// Error if unable to get request concurrency lock
58
    #[error("Unable to get request lock: {0}")]
59
    LockFailure(#[from] tokio::sync::AcquireError),
60
    #[error("I/O error: {0}")]
61
    IoError(#[from] io::Error),
62
    /// A HTTP error with 429 status code
63
    #[error("HTTP 429 Too Many Requests")]
64
    TooManyRequests { retry_after: Option<u64> },
65

            
66
    /* Token issues */
67
    /// Token invalid or expired
68
    #[error("Invalid CSRF token")]
69
    BadToken,
70
    /// Unable to fetch a CSRF token
71
    #[error("Unable to get token `{0}`")]
72
    TokenFailure(String),
73

            
74
    /* User-related issues */
75
    /// When expected to be logged in but aren't
76
    #[error("You're not logged in")]
77
    NotLoggedIn,
78
    /// When expected to be logged in but aren't
79
    #[error("You're not logged in as a bot account")]
80
    NotLoggedInAsBot,
81

            
82
    /* File-related issues */
83
    #[error("Upload warnings: {0:?}")]
84
    UploadWarning(Vec<String>),
85

            
86
    /* MediaWiki-side issues */
87
    #[error("maxlag tripped: {info}")]
88
    Maxlag {
89
        info: String,
90
        retry_after: Option<u64>,
91
    },
92
    /// When MediaWiki is in readonly mode
93
    #[error("MediaWiki is readonly: {info}")]
94
    Readonly {
95
        info: String,
96
        retry_after: Option<u64>,
97
    },
98
    /// An internal MediaWiki exception
99
    #[error("Internal MediaWiki exception: {0}")]
100
    InternalException(ApiError),
101

            
102
    /* Catchall/generic issues */
103
    /// Any arbitrary error returned by the MediaWiki API
104
    #[error("API error: {0}")]
105
    ApiError(ApiError),
106
    /// An error where we don't know what to do nor have
107
    /// information to report back
108
    #[error("Unknown error: {0}")]
109
    Unknown(String),
110
}
111

            
112
impl Error {
113
    /// Store the value of the retry-after header, if one was present, for
114
    /// error types that are safe to retry
115
8
    pub fn with_retry_after(self, value: u64) -> Self {
116
8
        match self {
117
            Error::TooManyRequests { .. } => Error::TooManyRequests {
118
                retry_after: Some(value),
119
            },
120
4
            Error::Maxlag { info, .. } => Error::Maxlag {
121
4
                info,
122
4
                retry_after: Some(value),
123
4
            },
124
            Error::Readonly { info, .. } => Error::Readonly {
125
                info,
126
                retry_after: Some(value),
127
            },
128
4
            err => err,
129
        }
130
8
    }
131

            
132
    /// If the error merits a retry, how long should we wait?
133
10
    pub fn retry_after(&self) -> Option<u64> {
134
10
        match self {
135
            Error::TooManyRequests { retry_after, .. } => {
136
                Some((*retry_after).unwrap_or(1))
137
            }
138
4
            Error::Maxlag { retry_after, .. } => {
139
4
                Some((*retry_after).unwrap_or(1))
140
            }
141
            Error::Readonly { retry_after, .. } => {
142
                Some((*retry_after).unwrap_or(1))
143
            }
144
6
            _ => None,
145
        }
146
10
    }
147
}
148

            
149
impl From<ApiError> for Error {
150
12
    fn from(apierr: ApiError) -> Self {
151
12
        match apierr.code.as_str() {
152
12
            "assertuserfailed" => Self::NotLoggedIn,
153
10
            "assertbotfailed" => Self::NotLoggedInAsBot,
154
8
            "badtoken" => Self::BadToken,
155
8
            "maxlag" => Self::Maxlag {
156
4
                info: apierr.text,
157
4
                retry_after: None,
158
4
            },
159
4
            "readonly" => Self::Readonly {
160
                info: apierr.text,
161
                retry_after: None,
162
            },
163
4
            code => {
164
4
                if code.starts_with("internal_api_error_") {
165
                    Self::InternalException(apierr)
166
                } else {
167
4
                    Self::ApiError(apierr)
168
                }
169
            }
170
        }
171
12
    }
172
}
173

            
174
#[cfg(test)]
175
mod tests {
176
    use super::*;
177

            
178
    #[test]
179
2
    fn test_from_apierror() {
180
2
        let apierr = ApiError {
181
2
            code: "assertbotfailed".to_string(),
182
2
            text: "Something something".to_string(),
183
2
            data: None,
184
2
        };
185
2
        let err = Error::from(apierr);
186
2
        assert!(matches!(err, Error::NotLoggedInAsBot));
187
2
    }
188

            
189
    #[test]
190
2
    fn test_to_string() {
191
2
        let apierr = ApiError {
192
2
            code: "errorcode".to_string(),
193
2
            text: "Some description".to_string(),
194
2
            data: None,
195
2
        };
196
2
        assert_eq!(&apierr.to_string(), "(code: errorcode): Some description");
197
2
    }
198
}