1# This file is part of PRAW.
2#
3# PRAW is free software: you can redistribute it and/or modify it under the
4# terms of the GNU General Public License as published by the Free Software
5# Foundation, either version 3 of the License, or (at your option) any later
6# version.
7#
8# PRAW is distributed in the hope that it will be useful, but WITHOUT ANY
9# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
10# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
11#
12# You should have received a copy of the GNU General Public License along with
13# PRAW.  If not, see <http://www.gnu.org/licenses/>.
14
15"""
16Error classes.
17
18Includes two main exceptions: ClientException, when something goes
19wrong on our end, and APIExeception for when something goes wrong on the
20server side. A number of classes extend these two main exceptions for more
21specific exceptions.
22"""
23
24from __future__ import print_function, unicode_literals
25
26import inspect
27import six
28import sys
29
30
31class PRAWException(Exception):
32    """The base PRAW Exception class.
33
34    Ideally, this can be caught to handle any exception from PRAW.
35
36    """
37
38
39class ClientException(PRAWException):
40    """Base exception class for errors that don't involve the remote API."""
41
42    def __init__(self, message=None):
43        """Construct a ClientException.
44
45        :param message: The error message to display.
46
47        """
48        if not message:
49            message = 'Clientside error'
50        super(ClientException, self).__init__()
51        self.message = message
52
53    def __str__(self):
54        """Return the message of the error."""
55        return self.message
56
57
58class OAuthScopeRequired(ClientException):
59    """Indicates that an OAuth2 scope is required to make the function call.
60
61    The attribute `scope` will contain the name of the necessary scope.
62
63    """
64
65    def __init__(self, function, scope, message=None):
66        """Contruct an OAuthScopeRequiredClientException.
67
68        :param function: The function that requires a scope.
69        :param scope: The scope required for the function.
70        :param message: A custom message to associate with the
71            exception. Default: `function` requires the OAuth2 scope `scope`
72
73        """
74        if not message:
75            message = '`{0}` requires the OAuth2 scope `{1}`'.format(function,
76                                                                     scope)
77        super(OAuthScopeRequired, self).__init__(message)
78        self.scope = scope
79
80
81class LoginRequired(ClientException):
82    """Indicates that a logged in session is required.
83
84    This exception is raised on a preemptive basis, whereas NotLoggedIn occurs
85    in response to a lack of credentials on a privileged API call.
86
87    """
88
89    def __init__(self, function, message=None):
90        """Construct a LoginRequired exception.
91
92        :param function: The function that requires login-based authentication.
93        :param message: A custom message to associate with the exception.
94            Default: `function` requires a logged in session
95
96        """
97        if not message:
98            message = '`{0}` requires a logged in session'.format(function)
99        super(LoginRequired, self).__init__(message)
100
101
102class LoginOrScopeRequired(OAuthScopeRequired, LoginRequired):
103    """Indicates that either a logged in session or OAuth2 scope is required.
104
105    The attribute `scope` will contain the name of the necessary scope.
106
107    """
108
109    def __init__(self, function, scope, message=None):
110        """Construct a LoginOrScopeRequired exception.
111
112        :param function: The function that requires authentication.
113        :param scope: The scope that is required if not logged in.
114        :param message: A custom message to associate with the exception.
115            Default: `function` requires a logged in session or the OAuth2
116            scope `scope`
117
118        """
119        if not message:
120            message = ('`{0}` requires a logged in session or the '
121                       'OAuth2 scope `{1}`').format(function, scope)
122        super(LoginOrScopeRequired, self).__init__(function, scope, message)
123
124
125class ModeratorRequired(LoginRequired):
126    """Indicates that a moderator of the subreddit is required."""
127
128    def __init__(self, function):
129        """Construct a ModeratorRequired exception.
130
131        :param function: The function that requires moderator access.
132
133        """
134        message = ('`{0}` requires a moderator '
135                   'of the subreddit').format(function)
136        super(ModeratorRequired, self).__init__(message)
137
138
139class ModeratorOrScopeRequired(LoginOrScopeRequired, ModeratorRequired):
140    """Indicates that a moderator of the sub or OAuth2 scope is required.
141
142    The attribute `scope` will contain the name of the necessary scope.
143
144    """
145
146    def __init__(self, function, scope):
147        """Construct a ModeratorOrScopeRequired exception.
148
149        :param function: The function that requires moderator authentication or
150            a moderator scope..
151        :param scope: The scope that is required if not logged in with
152            moderator access..
153
154        """
155        message = ('`{0}` requires a moderator of the subreddit or the '
156                   'OAuth2 scope `{1}`').format(function, scope)
157        super(ModeratorOrScopeRequired, self).__init__(function, scope,
158                                                       message)
159
160
161class OAuthAppRequired(ClientException):
162    """Raised when an OAuth client cannot be initialized.
163
164    This occurs when any one of the OAuth config values are not set.
165
166    """
167
168
169class HTTPException(PRAWException):
170    """Base class for HTTP related exceptions."""
171
172    def __init__(self, _raw, message=None):
173        """Construct a HTTPException.
174
175        :params _raw: The internal request library response object. This object
176            is mapped to attribute `_raw` whose format may change at any time.
177
178        """
179        if not message:
180            message = 'HTTP error'
181        super(HTTPException, self).__init__()
182        self._raw = _raw
183        self.message = message
184
185    def __str__(self):
186        """Return the message of the error."""
187        return self.message
188
189
190class Forbidden(HTTPException):
191    """Raised when the user does not have permission to the entity."""
192
193
194class NotFound(HTTPException):
195    """Raised when the requested entity is not found."""
196
197
198class InvalidComment(PRAWException):
199    """Indicate that the comment is no longer available on reddit."""
200
201    ERROR_TYPE = 'DELETED_COMMENT'
202
203    def __str__(self):
204        """Return the message of the error."""
205        return self.ERROR_TYPE
206
207
208class InvalidSubmission(PRAWException):
209    """Indicates that the submission is no longer available on reddit."""
210
211    ERROR_TYPE = 'DELETED_LINK'
212
213    def __str__(self):
214        """Return the message of the error."""
215        return self.ERROR_TYPE
216
217
218class InvalidSubreddit(PRAWException):
219    """Indicates that an invalid subreddit name was supplied."""
220
221    ERROR_TYPE = 'SUBREDDIT_NOEXIST'
222
223    def __str__(self):
224        """Return the message of the error."""
225        return self.ERROR_TYPE
226
227
228class RedirectException(PRAWException):
229    """Raised when a redirect response occurs that is not expected."""
230
231    def __init__(self, request_url, response_url, message=None):
232        """Construct a RedirectException.
233
234        :param request_url: The url requested.
235        :param response_url: The url being redirected to.
236        :param message: A custom message to associate with the exception.
237
238        """
239        if not message:
240            message = ('Unexpected redirect '
241                       'from {0} to {1}').format(request_url, response_url)
242        super(RedirectException, self).__init__()
243        self.request_url = request_url
244        self.response_url = response_url
245        self.message = message
246
247    def __str__(self):
248        """Return the message of the error."""
249        return self.message
250
251
252class OAuthException(PRAWException):
253    """Base exception class for OAuth API calls.
254
255    Attribute `message` contains the error message.
256    Attribute `url` contains the url that resulted in the error.
257
258    """
259
260    def __init__(self, message, url):
261        """Construct a OAuthException.
262
263        :param message: The message associated with the exception.
264        :param url: The url that resulted in error.
265
266        """
267        super(OAuthException, self).__init__()
268        self.message = message
269        self.url = url
270
271    def __str__(self):
272        """Return the message along with the url."""
273        return self.message + " on url {0}".format(self.url)
274
275
276class OAuthInsufficientScope(OAuthException):
277    """Raised when the current OAuth scope is not sufficient for the action.
278
279    This indicates the access token is valid, but not for the desired action.
280
281    """
282
283
284class OAuthInvalidGrant(OAuthException):
285    """Raised when the code to retrieve access information is not valid."""
286
287
288class OAuthInvalidToken(OAuthException):
289    """Raised when the current OAuth access token is not valid."""
290
291
292class APIException(PRAWException):
293    """Base exception class for the reddit API error message exceptions.
294
295    All exceptions of this type should have their own subclass.
296
297    """
298
299    def __init__(self, error_type, message, field='', response=None):
300        """Construct an APIException.
301
302        :param error_type: The error type set on reddit's end.
303        :param message: The associated message for the error.
304        :param field: The input field associated with the error, or ''.
305        :param response: The HTTP response that resulted in the exception.
306
307        """
308        super(APIException, self).__init__()
309        self.error_type = error_type
310        self.message = message
311        self.field = field
312        self.response = response
313
314    def __str__(self):
315        """Return a string containing the error message and field."""
316        if hasattr(self, 'ERROR_TYPE'):
317            return '`{0}` on field `{1}`'.format(self.message, self.field)
318        else:
319            return '({0}) `{1}` on field `{2}`'.format(self.error_type,
320                                                       self.message,
321                                                       self.field)
322
323
324class ExceptionList(APIException):
325    """Raised when more than one exception occurred."""
326
327    def __init__(self, errors):
328        """Construct an ExceptionList.
329
330        :param errors: The list of errors.
331
332        """
333        super(ExceptionList, self).__init__(None, None)
334        self.errors = errors
335
336    def __str__(self):
337        """Return a string representation for all the errors."""
338        ret = '\n'
339        for i, error in enumerate(self.errors):
340            ret += '\tError {0}) {1}\n'.format(i, six.text_type(error))
341        return ret
342
343
344class AlreadySubmitted(APIException):
345    """An exception to indicate that a URL was previously submitted."""
346
347    ERROR_TYPE = 'ALREADY_SUB'
348
349
350class AlreadyModerator(APIException):
351    """Used to indicate that a user is already a moderator of a subreddit."""
352
353    ERROR_TYPE = 'ALREADY_MODERATOR'
354
355
356class BadCSS(APIException):
357    """An exception to indicate bad CSS (such as invalid) was used."""
358
359    ERROR_TYPE = 'BAD_CSS'
360
361
362class BadCSSName(APIException):
363    """An exception to indicate a bad CSS name (such as invalid) was used."""
364
365    ERROR_TYPE = 'BAD_CSS_NAME'
366
367
368class BadUsername(APIException):
369    """An exception to indicate an invalid username was used."""
370
371    ERROR_TYPE = 'BAD_USERNAME'
372
373
374class InvalidCaptcha(APIException):
375    """An exception for when an incorrect captcha error is returned."""
376
377    ERROR_TYPE = 'BAD_CAPTCHA'
378
379
380class InvalidEmails(APIException):
381    """An exception for when invalid emails are provided."""
382
383    ERROR_TYPE = 'BAD_EMAILS'
384
385
386class InvalidFlairTarget(APIException):
387    """An exception raised when an invalid user is passed as a flair target."""
388
389    ERROR_TYPE = 'BAD_FLAIR_TARGET'
390
391
392class InvalidInvite(APIException):
393    """Raised when attempting to accept a nonexistent moderator invite."""
394
395    ERROR_TYPE = 'NO_INVITE_FOUND'
396
397
398class InvalidUser(APIException):
399    """An exception for when a user doesn't exist."""
400
401    ERROR_TYPE = 'USER_DOESNT_EXIST'
402
403
404class InvalidUserPass(APIException):
405    """An exception for failed logins."""
406
407    ERROR_TYPE = 'WRONG_PASSWORD'
408
409
410class InsufficientCreddits(APIException):
411    """Raised when there are not enough creddits to complete the action."""
412
413    ERROR_TYPE = 'INSUFFICIENT_CREDDITS'
414
415
416class NotLoggedIn(APIException):
417    """An exception for when a Reddit user isn't logged in."""
418
419    ERROR_TYPE = 'USER_REQUIRED'
420
421
422class NotModified(APIException):
423    """An exception raised when reddit returns {'error': 304}.
424
425    This error indicates that the requested content was not modified and is
426    being requested too frequently. Such an error usually occurs when multiple
427    instances of PRAW are running concurrently or in rapid succession.
428
429    """
430
431    def __init__(self, response):
432        """Construct an instance of the NotModified exception.
433
434        This error does not have an error_type, message, nor field.
435
436        """
437        super(NotModified, self).__init__(None, None, response=response)
438
439    def __str__(self):
440        """Return: That page has not been modified."""
441        return 'That page has not been modified.'
442
443
444class RateLimitExceeded(APIException):
445    """An exception for when something has happened too frequently.
446
447    Contains a `sleep_time` attribute for the number of seconds that must
448    transpire prior to the next request.
449
450    """
451
452    ERROR_TYPE = 'RATELIMIT'
453
454
455class SubredditExists(APIException):
456    """An exception to indicate that a subreddit name is not available."""
457
458    ERROR_TYPE = 'SUBREDDIT_EXISTS'
459
460
461class UsernameExists(APIException):
462    """An exception to indicate that a username is not available."""
463
464    ERROR_TYPE = 'USERNAME_TAKEN'
465
466
467def _build_error_mapping():
468    def predicate(obj):
469        return inspect.isclass(obj) and hasattr(obj, 'ERROR_TYPE')
470
471    tmp = {}
472    for _, obj in inspect.getmembers(sys.modules[__name__], predicate):
473        tmp[obj.ERROR_TYPE] = obj
474    return tmp
475ERROR_MAPPING = _build_error_mapping()
476