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"""
16Python Reddit API Wrapper.
17
18PRAW, an acronym for "Python Reddit API Wrapper", is a python package that
19allows for simple access to reddit's API. PRAW aims to be as easy to use as
20possible and is designed to follow all of reddit's API rules. You have to give
21a useragent, everything else is handled by PRAW so you needn't worry about
22violating them.
23
24More information about PRAW can be found at https://github.com/praw-dev/praw
25"""
26
27from __future__ import print_function, unicode_literals
28
29import json
30import os
31import platform
32import re
33import six
34import sys
35from . import decorators, errors
36from .handlers import DefaultHandler
37from .helpers import chunk_sequence, normalize_url
38from .internal import (_image_type, _prepare_request,
39                       _raise_redirect_exceptions,
40                       _raise_response_exceptions,
41                       _to_reddit_list, _warn_pyopenssl)
42from .settings import CONFIG
43from requests import Session
44from requests.compat import urljoin
45from requests.utils import to_native_string
46from requests import Request
47# pylint: disable=F0401
48from six.moves import html_entities, http_cookiejar
49from six.moves.urllib.parse import parse_qs, urlparse, urlunparse
50# pylint: enable=F0401
51from warnings import warn_explicit
52
53
54__version__ = '3.6.1'
55
56
57class Config(object):  # pylint: disable=R0903
58    """A class containing the configuration for a reddit site."""
59
60    API_PATHS = {'accept_mod_invite':   'api/accept_moderator_invite',
61                 'access_token_url':    'api/v1/access_token/',
62                 'approve':             'api/approve/',
63                 'authorize':           'api/v1/authorize/',
64                 'banned':              'r/{subreddit}/about/banned/',
65                 'blocked':             'prefs/blocked/',
66                 'by_id':               'by_id/',
67                 'captcha':             'captcha/',
68                 'clearflairtemplates': 'api/clearflairtemplates/',
69                 'collapse_message':    'api/collapse_message/',
70                 'comment':             'api/comment/',
71                 'comment_replies':     'message/comments/',
72                 'comments':            'comments/',
73                 'compose':             'api/compose/',
74                 'contest_mode':        'api/set_contest_mode/',
75                 'contributors':        'r/{subreddit}/about/contributors/',
76                 'controversial':       'controversial/',
77                 'default_subreddits':  'subreddits/default/',
78                 'del':                 'api/del/',
79                 'deleteflair':         'api/deleteflair',
80                 'delete_redditor':     'api/delete_user',
81                 'delete_sr_header':    'r/{subreddit}/api/delete_sr_header',
82                 'delete_sr_image':     'r/{subreddit}/api/delete_sr_img',
83                 'distinguish':         'api/distinguish/',
84                 'domain':              'domain/{domain}/',
85                 'duplicates':          'duplicates/{submissionid}/',
86                 'edit':                'api/editusertext/',
87                 'edited':              'r/{subreddit}/about/edited/',
88                 'flair':               'api/flair/',
89                 'flairconfig':         'api/flairconfig/',
90                 'flaircsv':            'api/flaircsv/',
91                 'flairlist':           'r/{subreddit}/api/flairlist/',
92                 'flairselector':       'api/flairselector/',
93                 'flairtemplate':       'api/flairtemplate/',
94                 'friend':              'api/friend/',
95                 'friend_v1':           'api/v1/me/friends/{user}',
96                 'friends':             'prefs/friends/',
97                 'gild_thing':          'api/v1/gold/gild/{fullname}/',
98                 'gild_user':           'api/v1/gold/give/{username}/',
99                 'gilded':              'gilded/',
100                 'help':                'help/',
101                 'hide':                'api/hide/',
102                 'ignore_reports':      'api/ignore_reports/',
103                 'inbox':               'message/inbox/',
104                 'info':                'api/info/',
105                 'leavecontributor':    'api/leavecontributor',
106                 'leavemoderator':      'api/leavemoderator',
107                 'lock':                'api/lock/',
108                 'login':               'api/login/',
109                 'me':                  'api/v1/me',
110                 'mentions':            'message/mentions',
111                 'message':             'message/messages/{messageid}/',
112                 'messages':            'message/messages/',
113                 'moderators':          'r/{subreddit}/about/moderators/',
114                 'modlog':              'r/{subreddit}/about/log/',
115                 'modqueue':            'r/{subreddit}/about/modqueue/',
116                 'mod_mail':            'r/{subreddit}/message/moderator/',
117                 'morechildren':        'api/morechildren/',
118                 'my_con_subreddits':   'subreddits/mine/contributor/',
119                 'my_mod_subreddits':   'subreddits/mine/moderator/',
120                 'my_multis':           'api/multi/mine/',
121                 'my_subreddits':       'subreddits/mine/subscriber/',
122                 'new':                 'new/',
123                 'new_subreddits':      'subreddits/new/',
124                 'marknsfw':            'api/marknsfw/',
125                 'multireddit':         'user/{user}/m/{multi}/',
126                 'multireddit_add':     ('api/multi/user/{user}/m/{multi}/r/'
127                                         '{subreddit}'),
128                 'multireddit_about':   'api/multi/user/{user}/m/{multi}/',
129                 'multireddit_copy':    'api/multi/copy/',
130                 'multireddit_mine':    'me/m/{multi}/',
131                 'multireddit_rename':  'api/multi/rename/',
132                 'multireddit_user':    'api/multi/user/{user}/',
133                 'mute_sender':         'api/mute_message_author/',
134                 'muted':               'r/{subreddit}/about/muted/',
135                 'popular_subreddits':  'subreddits/popular/',
136                 'post_replies':        'message/selfreply/',
137                 'read_message':        'api/read_message/',
138                 'reddit_url':          '/',
139                 'register':            'api/register/',
140                 'remove':              'api/remove/',
141                 'report':              'api/report/',
142                 'reports':             'r/{subreddit}/about/reports/',
143                 'rising':              'rising/',
144                 'rules':               'r/{subreddit}/about/rules/',
145                 'save':                'api/save/',
146                 'saved':               'saved/',
147                 'search':              'r/{subreddit}/search/',
148                 'search_reddit_names': 'api/search_reddit_names/',
149                 'select_flair':        'api/selectflair/',
150                 'sent':                'message/sent/',
151                 'sticky':              'r/{subreddit}/about/sticky/',
152                 'sticky_submission':   'api/set_subreddit_sticky/',
153                 'site_admin':          'api/site_admin/',
154                 'spam':                'r/{subreddit}/about/spam/',
155                 'stylesheet':          'r/{subreddit}/about/stylesheet/',
156                 'submit':              'api/submit/',
157                 'sub_comments_gilded': 'r/{subreddit}/comments/gilded/',
158                 'sub_recommendations': 'api/recommend/sr/{subreddits}',
159                 'subreddit':           'r/{subreddit}/',
160                 'subreddit_about':     'r/{subreddit}/about/',
161                 'subreddit_comments':  'r/{subreddit}/comments/',
162                 'subreddit_css':       'api/subreddit_stylesheet/',
163                 'subreddit_random':    'r/{subreddit}/random/',
164                 'subreddit_settings':  'r/{subreddit}/about/edit/',
165                 'subreddit_traffic':   'r/{subreddit}/about/traffic/',
166                 'subscribe':           'api/subscribe/',
167                 'suggested_sort':      'api/set_suggested_sort/',
168                 'top':                 'top/',
169                 'uncollapse_message':  'api/uncollapse_message/',
170                 'unfriend':            'api/unfriend/',
171                 'unhide':              'api/unhide/',
172                 'unlock':              'api/unlock/',
173                 'unmarknsfw':          'api/unmarknsfw/',
174                 'unmoderated':         'r/{subreddit}/about/unmoderated/',
175                 'unmute_sender':       'api/unmute_message_author/',
176                 'unignore_reports':    'api/unignore_reports/',
177                 'unread':              'message/unread/',
178                 'unread_message':      'api/unread_message/',
179                 'unsave':              'api/unsave/',
180                 'upload_image':        'api/upload_sr_img',
181                 'user':                'user/{user}/',
182                 'user_about':          'user/{user}/about/',
183                 'username_available':  'api/username_available/',
184                 'vote':                'api/vote/',
185                 'wiki_edit':           'api/wiki/edit/',
186                 'wiki_page':           'r/{subreddit}/wiki/{page}',  # No /
187                 'wiki_page_editor':    ('r/{subreddit}/api/wiki/alloweditor/'
188                                         '{method}'),
189                 'wiki_page_settings':  'r/{subreddit}/wiki/settings/{page}',
190                 'wiki_pages':          'r/{subreddit}/wiki/pages/',
191                 'wiki_banned':         'r/{subreddit}/about/wikibanned/',
192                 'wiki_contributors':   'r/{subreddit}/about/wikicontributors/'
193                 }
194    WWW_PATHS = set(['authorize'])
195
196    @staticmethod
197    def ua_string(praw_info):
198        """Return the user-agent string.
199
200        The user-agent string contains PRAW version and platform version info.
201
202        """
203        if os.environ.get('SERVER_SOFTWARE') is not None:
204            # Google App Engine information
205            # https://developers.google.com/appengine/docs/python/
206            info = os.environ.get('SERVER_SOFTWARE')
207        else:
208            # Standard platform information
209            info = platform.platform(True).encode('ascii', 'ignore')
210
211        return '{0} PRAW/{1} Python/{2} {3}'.format(
212            praw_info, __version__, sys.version.split()[0], info)
213
214    def __init__(self, site_name, **kwargs):
215        """Initialize PRAW's configuration."""
216        def config_boolean(item):
217            return item and item.lower() in ('1', 'yes', 'true', 'on')
218
219        obj = dict(CONFIG.items(site_name))
220        # Overwrite configuration file settings with those given during
221        # instantiation of the Reddit instance.
222        for key, value in kwargs.items():
223            obj[key] = value
224
225        self.api_url = 'https://' + obj['api_domain']
226        self.permalink_url = 'https://' + obj['permalink_domain']
227        self.oauth_url = ('https://' if config_boolean(obj['oauth_https'])
228                          else 'http://') + obj['oauth_domain']
229        self.api_request_delay = float(obj['api_request_delay'])
230        self.by_kind = {obj['comment_kind']:    objects.Comment,
231                        obj['message_kind']:    objects.Message,
232                        obj['redditor_kind']:   objects.Redditor,
233                        obj['submission_kind']: objects.Submission,
234                        obj['subreddit_kind']:  objects.Subreddit,
235                        'LabeledMulti':         objects.Multireddit,
236                        'modaction':            objects.ModAction,
237                        'more':                 objects.MoreComments,
238                        'wikipage':             objects.WikiPage,
239                        'wikipagelisting':      objects.WikiPageListing,
240                        'UserList':             objects.UserList}
241        self.by_object = dict((value, key) for (key, value) in
242                              six.iteritems(self.by_kind))
243        self.by_object[objects.LoggedInRedditor] = obj['redditor_kind']
244        self.cache_timeout = float(obj['cache_timeout'])
245        self.check_for_updates = config_boolean(obj['check_for_updates'])
246        self.domain = obj['permalink_domain']
247        self.output_chars_limit = int(obj['output_chars_limit'])
248        self.log_requests = int(obj['log_requests'])
249        self.http_proxy = (obj.get('http_proxy') or os.getenv('http_proxy') or
250                           None)
251        self.https_proxy = (obj.get('https_proxy') or
252                            os.getenv('https_proxy') or None)
253        # We use `get(...) or None` because `get` may return an empty string
254
255        self.validate_certs = config_boolean(obj.get('validate_certs'))
256
257        self.client_id = obj.get('oauth_client_id') or None
258        self.client_secret = obj.get('oauth_client_secret') or None
259        self.redirect_uri = obj.get('oauth_redirect_uri') or None
260        self.grant_type = obj.get('oauth_grant_type') or None
261        self.refresh_token = obj.get('oauth_refresh_token') or None
262        self.store_json_result = config_boolean(obj.get('store_json_result'))
263
264        if 'short_domain' in obj and obj['short_domain']:
265            self._short_domain = 'http://' + obj['short_domain']
266        else:
267            self._short_domain = None
268        self.timeout = float(obj['timeout'])
269        try:
270            self.user = obj['user'] if obj['user'] else None
271            self.pswd = obj['pswd']
272        except KeyError:
273            self.user = self.pswd = None
274
275    def __getitem__(self, key):
276        """Return the URL for key."""
277        prefix = self.permalink_url if key in self.WWW_PATHS else self.api_url
278        return urljoin(prefix, self.API_PATHS[key])
279
280    @property
281    def short_domain(self):
282        """Return the short domain of the reddit server.
283
284        Used to generate the shortlink. For reddit.com the short_domain is
285        redd.it.
286
287        """
288        if self._short_domain:
289            return self._short_domain
290        else:
291            raise errors.ClientException('No short domain specified.')
292
293
294class BaseReddit(object):
295    """A base class that allows access to reddit's API.
296
297    You should **not** directly instantiate instances of this class. Use
298    :class:`.Reddit` instead.
299
300    """
301
302    RETRY_CODES = [502, 503, 504]
303    update_checked = False
304    openssl_warned = False
305
306    def __init__(self, user_agent, site_name=None, handler=None,
307                 disable_update_check=False, **kwargs):
308        """Initialize our connection with a reddit server.
309
310        The user_agent is how your application identifies itself. Read the
311        official API guidelines for user_agents
312        https://github.com/reddit/reddit/wiki/API. Applications using default
313        user_agents such as "Python/urllib" are drastically limited.
314
315        site_name allows you to specify which reddit you want to connect to.
316        The installation defaults are reddit.com, if you only need to connect
317        to reddit.com then you can safely ignore this. If you want to connect
318        to another reddit, set site_name to the name of that reddit. This must
319        match with an entry in praw.ini. If site_name is None, then the site
320        name will be looked for in the environment variable REDDIT_SITE. If it
321        is not found there, the default site name reddit matching reddit.com
322        will be used.
323
324        disable_update_check allows you to prevent an update check from
325        occurring in spite of the check_for_updates setting in praw.ini.
326
327        All additional parameters specified via kwargs will be used to
328        initialize the Config object. This can be used to specify configuration
329        settings during instantiation of the Reddit instance. See
330        https://praw.readthedocs.io/en/latest/pages/configuration_files.html
331        for more details.
332
333        """
334        if not user_agent or not isinstance(user_agent, six.string_types):
335            raise TypeError('user_agent must be a non-empty string.')
336        if 'bot' in user_agent.lower():
337            warn_explicit(
338                'The keyword `bot` in your user_agent may be problematic.',
339                UserWarning, '', 0)
340
341        self.config = Config(site_name or os.getenv('REDDIT_SITE') or 'reddit',
342                             **kwargs)
343        self.handler = handler or DefaultHandler()
344        self.http = Session()
345        self.http.headers['User-Agent'] = self.config.ua_string(user_agent)
346        self.http.validate_certs = self.config.validate_certs
347
348        # This `Session` object is only used to store request information that
349        # is used to make prepared requests. It _should_ never be used to make
350        # a direct request, thus we raise an exception when it is used.
351
352        def _req_error(*_, **__):
353            raise errors.ClientException('Do not make direct requests.')
354        self.http.request = _req_error
355
356        if self.config.http_proxy or self.config.https_proxy:
357            self.http.proxies = {}
358            if self.config.http_proxy:
359                self.http.proxies['http'] = self.config.http_proxy
360            if self.config.https_proxy:
361                self.http.proxies['https'] = self.config.https_proxy
362        self.modhash = None
363
364        # Check for updates if permitted and this is the first Reddit instance
365        # if not disable_update_check and not BaseReddit.update_checked \
366        #         and self.config.check_for_updates:
367        #     update_check(__name__, __version__)
368        #     BaseReddit.update_checked = True
369
370        # Warn against a potentially incompatible version of pyOpenSSL
371        if not BaseReddit.openssl_warned and self.config.validate_certs:
372            _warn_pyopenssl()
373            BaseReddit.openssl_warned = True
374
375        # Initial values
376        self._use_oauth = False
377
378    def _request(self, url, params=None, data=None, files=None, auth=None,
379                 timeout=None, raw_response=False, retry_on_error=True,
380                 method=None):
381        """Given a page url and a dict of params, open and return the page.
382
383        :param url: the url to grab content from.
384        :param params: a dictionary containing the GET data to put in the url
385        :param data: a dictionary containing the extra data to submit
386        :param files: a dictionary specifying the files to upload
387        :param auth: Add the HTTP authentication headers (see requests)
388        :param timeout: Specifies the maximum time that the actual HTTP request
389            can take.
390        :param raw_response: return the response object rather than the
391            response body
392        :param retry_on_error: if True retry the request, if it fails, for up
393            to 3 attempts
394        :returns: either the response body or the response object
395
396        """
397        def build_key_items(url, params, data, auth, files, method):
398            request = _prepare_request(self, url, params, data, auth, files,
399                                       method)
400
401            # Prepare extra arguments
402            key_items = []
403            oauth = request.headers.get('Authorization', None)
404            for key_value in (params, data, request.cookies, auth, oauth):
405                if isinstance(key_value, dict):
406                    key_items.append(tuple(key_value.items()))
407                elif isinstance(key_value, http_cookiejar.CookieJar):
408                    key_items.append(tuple(key_value.get_dict().items()))
409                else:
410                    key_items.append(key_value)
411            kwargs = {'_rate_domain': self.config.domain,
412                      '_rate_delay': int(self.config.api_request_delay),
413                      '_cache_ignore': bool(files) or raw_response,
414                      '_cache_timeout': int(self.config.cache_timeout)}
415
416            return (request, key_items, kwargs)
417
418        def decode(match):
419            return six.unichr(html_entities.name2codepoint[match.group(1)])
420
421        def handle_redirect():
422            response = None
423            url = request.url
424            while url:  # Manually handle 302 redirects
425                request.url = url
426                kwargs['_cache_key'] = (normalize_url(request.url),
427                                        tuple(key_items))
428                response = self.handler.request(
429                    request=request.prepare(),
430                    proxies=self.http.proxies,
431                    timeout=timeout,
432                    verify=self.http.validate_certs, **kwargs)
433
434                if self.config.log_requests >= 2:
435                    msg = 'status: {0}\n'.format(response.status_code)
436                    sys.stderr.write(msg)
437                url = _raise_redirect_exceptions(response)
438                assert url != request.url
439            return response
440
441        timeout = self.config.timeout if timeout is None else timeout
442        request, key_items, kwargs = build_key_items(url, params, data,
443                                                     auth, files, method)
444
445        tempauth = self._use_oauth
446        remaining_attempts = 3 if retry_on_error else 1
447        attempt_oauth_refresh = bool(self.refresh_token)
448        while True:
449            try:
450                self._use_oauth = self.is_oauth_session()
451                response = handle_redirect()
452                _raise_response_exceptions(response)
453                self.http.cookies.update(response.cookies)
454                if raw_response:
455                    return response
456                else:
457                    return re.sub('&([^;]+);', decode, response.text)
458            except errors.OAuthInvalidToken as error:
459                if not attempt_oauth_refresh:
460                    raise
461                attempt_oauth_refresh = False
462                self._use_oauth = False
463                self.refresh_access_information()
464                self._use_oauth = tempauth
465                request, key_items, kwargs = build_key_items(url, params,
466                                                             data, auth, files,
467                                                             method)
468            except errors.HTTPException as error:
469                remaining_attempts -= 1
470                # pylint: disable=W0212
471                if error._raw.status_code not in self.RETRY_CODES or \
472                        remaining_attempts == 0:
473                    raise
474            finally:
475                self._use_oauth = tempauth
476
477    def _json_reddit_objecter(self, json_data):
478        """Return an appropriate RedditObject from json_data when possible."""
479        try:
480            object_class = self.config.by_kind[json_data['kind']]
481        except KeyError:
482            if 'json' in json_data:
483                if len(json_data) != 1:
484                    msg = 'Unknown object type: {0}'.format(json_data)
485                    warn_explicit(msg, UserWarning, '', 0)
486                return json_data['json']
487        else:
488            return object_class.from_api_response(self, json_data['data'])
489        return json_data
490
491    def evict(self, urls):
492        """Evict url(s) from the cache.
493
494        :param urls: An iterable containing normalized urls.
495        :returns: The number of items removed from the cache.
496
497        """
498        if isinstance(urls, six.string_types):
499            urls = (urls,)
500        return self.handler.evict(urls)
501
502    @decorators.oauth_generator
503    def get_content(self, url, params=None, limit=0, place_holder=None,
504                    root_field='data', thing_field='children',
505                    after_field='after', object_filter=None, **kwargs):
506        """A generator method to return reddit content from a URL.
507
508        Starts at the initial url, and fetches content using the `after`
509        JSON data until `limit` entries have been fetched, or the
510        `place_holder` has been reached.
511
512        :param url: the url to start fetching content from
513        :param params: dictionary containing extra GET data to put in the url
514        :param limit: the number of content entries to fetch. If limit <= 0,
515            fetch the default for your account (25 for unauthenticated
516            users). If limit is None, then fetch as many entries as possible
517            (reddit returns at most 100 per request, however, PRAW will
518            automatically make additional requests as necessary).
519        :param place_holder: if not None, the method will fetch `limit`
520            content, stopping if it finds content with `id` equal to
521            `place_holder`. The place_holder item is the last item to be
522            yielded from this generator. Note that the use of `place_holder` is
523            not 100% reliable as the place holder item may no longer exist due
524            to being removed or deleted.
525        :param root_field: indicates the field in the json response that holds
526            the data. Most objects use 'data', however some (flairlist) don't
527            have the 'data' object. Use None for the root object.
528        :param thing_field: indicates the field under the root_field which
529            contains the list of things. Most objects use 'children'.
530        :param after_field: indicates the field which holds the after item
531            element
532        :param object_filter: if set to an integer value, fetch content from
533            the corresponding list index in the JSON response. For example
534            the JSON response for submission duplicates is a list of objects,
535            and the object we want to fetch from is at index 1. So we set
536            object_filter=1 to filter out the other useless list elements.
537        :type place_holder: a string corresponding to a reddit base36 id
538            without prefix, e.g. 'asdfasdf'
539        :returns: a list of reddit content, of type Subreddit, Comment,
540            Submission or user flair.
541
542        """
543        _use_oauth = kwargs.get('_use_oauth', self.is_oauth_session())
544
545        objects_found = 0
546        params = params or {}
547        fetch_all = fetch_once = False
548        if limit is None:
549            fetch_all = True
550            params['limit'] = 1024  # Just use a big number
551        elif limit > 0:
552            params['limit'] = limit
553        else:
554            fetch_once = True
555
556        if hasattr(self, '_url_update'):
557            url = self._url_update(url)  # pylint: disable=E1101
558
559        # While we still need to fetch more content to reach our limit, do so.
560        while fetch_once or fetch_all or objects_found < limit:
561            if _use_oauth:  # Set the necessary _use_oauth value
562                assert self._use_oauth is False
563                self._use_oauth = _use_oauth
564            try:
565                page_data = self.request_json(url, params=params)
566                if object_filter:
567                    page_data = page_data[object_filter]
568            finally:  # Restore _use_oauth value
569                if _use_oauth:
570                    self._use_oauth = False
571            fetch_once = False
572            root = page_data.get(root_field, page_data)
573            for thing in root[thing_field]:
574                yield thing
575                objects_found += 1
576                # Terminate when we've reached the limit, or place holder
577                if objects_found == limit or (place_holder and
578                                              thing.id == place_holder):
579                    return
580            # Set/update the 'after' parameter for the next iteration
581            if root.get(after_field):
582                # We use `root.get` to also test if the value evaluates to True
583                params['after'] = root[after_field]
584            else:
585                return
586
587    @decorators.raise_api_exceptions
588    def request(self, url, params=None, data=None, retry_on_error=True,
589                method=None):
590        """Make a HTTP request and return the response.
591
592        :param url: the url to grab content from.
593        :param params: a dictionary containing the GET data to put in the url
594        :param data: a dictionary containing the extra data to submit
595        :param retry_on_error: if True retry the request, if it fails, for up
596            to 3 attempts
597        :param method: The HTTP method to use in the request.
598        :returns: The HTTP response.
599        """
600        return self._request(url, params, data, raw_response=True,
601                             retry_on_error=retry_on_error, method=method)
602
603    @decorators.raise_api_exceptions
604    def request_json(self, url, params=None, data=None, as_objects=True,
605                     retry_on_error=True, method=None):
606        """Get the JSON processed from a page.
607
608        :param url: the url to grab content from.
609        :param params: a dictionary containing the GET data to put in the url
610        :param data: a dictionary containing the extra data to submit
611        :param as_objects: if True return reddit objects else raw json dict.
612        :param retry_on_error: if True retry the request, if it fails, for up
613            to 3 attempts
614        :returns: JSON processed page
615
616        """
617        if not url.endswith('.json'):
618            url += '.json'
619        response = self._request(url, params, data, method=method,
620                                 retry_on_error=retry_on_error)
621        hook = self._json_reddit_objecter if as_objects else None
622        # Request url just needs to be available for the objecter to use
623        self._request_url = url  # pylint: disable=W0201
624
625        if response == '':
626            # Some of the v1 urls don't return anything, even when they're
627            # successful.
628            return response
629
630        data = json.loads(response, object_hook=hook)
631        delattr(self, '_request_url')
632        # Update the modhash
633        if isinstance(data, dict) and 'data' in data \
634                and 'modhash' in data['data']:
635            self.modhash = data['data']['modhash']
636        return data
637
638
639class OAuth2Reddit(BaseReddit):
640    """Provides functionality for obtaining reddit OAuth2 access tokens.
641
642    You should **not** directly instantiate instances of this class. Use
643    :class:`.Reddit` instead.
644
645    """
646
647    def __init__(self, *args, **kwargs):
648        """Initialize an OAuth2Reddit instance."""
649        super(OAuth2Reddit, self).__init__(*args, **kwargs)
650        self.client_id = self.config.client_id
651        self.client_secret = self.config.client_secret
652        self.redirect_uri = self.config.redirect_uri
653
654    def _handle_oauth_request(self, data):
655        auth = (self.client_id, self.client_secret)
656        url = self.config['access_token_url']
657        response = self._request(url, auth=auth, data=data, raw_response=True)
658        if not response.ok:
659            msg = 'Unexpected OAuthReturn: {0}'.format(response.status_code)
660            raise errors.OAuthException(msg, url)
661        retval = response.json()
662        if 'error' in retval:
663            error = retval['error']
664            if error == 'invalid_grant':
665                raise errors.OAuthInvalidGrant(error, url)
666            raise errors.OAuthException(retval['error'], url)
667        return retval
668
669    @decorators.require_oauth
670    def get_access_information(self, code):
671        """Return the access information for an OAuth2 authorization grant.
672
673        :param code: the code received in the request from the OAuth2 server
674        :returns: A dictionary with the key/value pairs for ``access_token``,
675            ``refresh_token`` and ``scope``. The ``refresh_token`` value will
676            be None when the OAuth2 grant is not refreshable. The ``scope``
677            value will be a set containing the scopes the tokens are valid for.
678
679        """
680        if self.config.grant_type == 'password':
681            data = {'grant_type': 'password',
682                    'username': self.config.user,
683                    'password': self.config.pswd}
684        else:
685            data = {'code': code, 'grant_type': 'authorization_code',
686                    'redirect_uri': self.redirect_uri}
687        retval = self._handle_oauth_request(data)
688        return {'access_token': retval['access_token'],
689                'refresh_token': retval.get('refresh_token'),
690                'scope': set(retval['scope'].split(' '))}
691
692    @decorators.require_oauth
693    def get_authorize_url(self, state, scope='identity', refreshable=False):
694        """Return the URL to send the user to for OAuth2 authorization.
695
696        :param state: a unique string of your choice that represents this
697            individual client
698        :param scope: the reddit scope to ask permissions for. Multiple scopes
699            can be enabled by passing in a container of strings.
700        :param refreshable: when True, a permanent "refreshable" token is
701            issued
702
703        """
704        params = {'client_id': self.client_id, 'response_type': 'code',
705                  'redirect_uri': self.redirect_uri, 'state': state,
706                  'scope': _to_reddit_list(scope)}
707        params['duration'] = 'permanent' if refreshable else 'temporary'
708        request = Request('GET', self.config['authorize'], params=params)
709        return request.prepare().url
710
711    @property
712    def has_oauth_app_info(self):
713        """Return True when OAuth credentials are associated with the instance.
714
715        The necessary credentials are: ``client_id``, ``client_secret`` and
716        ``redirect_uri``.
717
718        """
719        return all((self.client_id is not None,
720                    self.client_secret is not None,
721                    self.redirect_uri is not None))
722
723    @decorators.require_oauth
724    def refresh_access_information(self, refresh_token):
725        """Return updated access information for an OAuth2 authorization grant.
726
727        :param refresh_token: the refresh token used to obtain the updated
728            information
729        :returns: A dictionary with the key/value pairs for access_token,
730            refresh_token and scope. The refresh_token value will be done when
731            the OAuth2 grant is not refreshable. The scope value will be a set
732            containing the scopes the tokens are valid for.
733
734        Password grants aren't refreshable, so use `get_access_information()`
735        again, instead.
736        """
737        if self.config.grant_type == 'password':
738            data = {'grant_type': 'password',
739                    'username': self.config.user,
740                    'password': self.config.pswd}
741        else:
742            data = {'grant_type': 'refresh_token',
743                    'redirect_uri': self.redirect_uri,
744                    'refresh_token': refresh_token}
745        retval = self._handle_oauth_request(data)
746        return {'access_token': retval['access_token'],
747                'refresh_token': refresh_token,
748                'scope': set(retval['scope'].split(' '))}
749
750    def set_oauth_app_info(self, client_id, client_secret, redirect_uri):
751        """Set the app information to use with OAuth2.
752
753        This function need only be called if your praw.ini site configuration
754        does not already contain the necessary information.
755
756        Go to https://www.reddit.com/prefs/apps/ to discover the appropriate
757        values for your application.
758
759        :param client_id: the client_id of your application
760        :param client_secret: the client_secret of your application
761        :param redirect_uri: the redirect_uri of your application
762
763        """
764        self.client_id = client_id
765        self.client_secret = client_secret
766        self.redirect_uri = redirect_uri
767
768
769class UnauthenticatedReddit(BaseReddit):
770    """This mixin provides bindings for basic functions of reddit's API.
771
772    None of these functions require authenticated access to reddit's API.
773
774    You should **not** directly instantiate instances of this class. Use
775    :class:`.Reddit` instead.
776
777    """
778
779    def __init__(self, *args, **kwargs):
780        """Initialize an UnauthenticatedReddit instance."""
781        super(UnauthenticatedReddit, self).__init__(*args, **kwargs)
782        # initialize to 1 instead of 0, because 0 does not reliably make
783        # new requests.
784        self._unique_count = 1
785
786    def create_redditor(self, user_name, password, email=''):
787        """Register a new user.
788
789        :returns: The json response from the server.
790
791        """
792        data = {'email': email,
793                'passwd': password,
794                'passwd2': password,
795                'user': user_name}
796        return self.request_json(self.config['register'], data=data)
797
798    def default_subreddits(self, *args, **kwargs):
799        """Return a get_content generator for the default subreddits.
800
801        The additional parameters are passed directly into
802        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
803
804        """
805        url = self.config['default_subreddits']
806        return self.get_content(url, *args, **kwargs)
807
808    @decorators.restrict_access(scope='read')
809    def get_comments(self, subreddit, gilded_only=False, *args, **kwargs):
810        """Return a get_content generator for comments in the given subreddit.
811
812        :param gilded_only: If True only return gilded comments.
813
814        The additional parameters are passed directly into
815        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
816
817        """
818        key = 'sub_comments_gilded' if gilded_only else 'subreddit_comments'
819        url = self.config[key].format(subreddit=six.text_type(subreddit))
820        return self.get_content(url, *args, **kwargs)
821
822    @decorators.restrict_access(scope='read')
823    def get_controversial(self, *args, **kwargs):
824        """Return a get_content generator for controversial submissions.
825
826        Corresponds to submissions provided by
827        ``https://www.reddit.com/controversial/`` for the session.
828
829        The additional parameters are passed directly into
830        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
831
832        """
833        return self.get_content(self.config['controversial'], *args, **kwargs)
834
835    @decorators.restrict_access(scope='read')
836    def get_domain_listing(self, domain, sort='hot', period=None, *args,
837                           **kwargs):
838        """Return a get_content generator for submissions by domain.
839
840        Corresponds to the submissions provided by
841        ``https://www.reddit.com/domain/{domain}``.
842
843        :param domain: The domain to generate a submission listing for.
844        :param sort: When provided must be one of 'hot', 'new', 'rising',
845            'controversial, 'gilded', or 'top'. Defaults to 'hot'.
846        :param period: When sort is either 'controversial', or 'top' the period
847            can be either None (for account default), 'all', 'year', 'month',
848            'week', 'day', or 'hour'.
849
850        The additional parameters are passed directly into
851        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
852
853        """
854        # Verify arguments
855        if sort not in ('controversial', 'hot', 'new', 'rising', 'top',
856                        'gilded'):
857            raise TypeError('Invalid sort parameter.')
858        if period not in (None, 'all', 'day', 'hour', 'month', 'week', 'year'):
859            raise TypeError('Invalid period parameter.')
860        if sort not in ('controversial', 'top') and period:
861            raise TypeError('Period cannot be set for that sort argument.')
862
863        url = self.config['domain'].format(domain=domain)
864        if sort != 'hot':
865            url += sort
866        if period:  # Set or overwrite params 't' parameter
867            kwargs.setdefault('params', {})['t'] = period
868        return self.get_content(url, *args, **kwargs)
869
870    @decorators.restrict_access(scope='modflair')
871    def get_flair(self, subreddit, redditor, **params):
872        """Return the flair for a user on the given subreddit.
873
874        :param subreddit: Can be either a Subreddit object or the name of a
875            subreddit.
876        :param redditor: Can be either a Redditor object or the name of a
877            redditor.
878        :returns: None if the user doesn't exist, otherwise a dictionary
879            containing the keys `flair_css_class`, `flair_text`, and `user`.
880
881        """
882        name = six.text_type(redditor)
883        params.update(name=name)
884        url = self.config['flairlist'].format(
885            subreddit=six.text_type(subreddit))
886        data = self.request_json(url, params=params)
887        if not data['users'] or \
888                data['users'][0]['user'].lower() != name.lower():
889            return None
890        return data['users'][0]
891
892    @decorators.restrict_access(scope='read')
893    def get_front_page(self, *args, **kwargs):
894        """Return a get_content generator for the front page submissions.
895
896        Corresponds to the submissions provided by ``https://www.reddit.com/``
897        for the session.
898
899        The additional parameters are passed directly into
900        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
901
902        """
903        return self.get_content(self.config['reddit_url'], *args, **kwargs)
904
905    @decorators.restrict_access(scope='read', generator_called=True)
906    def get_info(self, url=None, thing_id=None, *args, **kwargs):
907        """Look up existing items by thing_id (fullname) or url.
908
909        :param url: A url to lookup.
910        :param thing_id: A single thing_id, or a list of thing_ids. A thing_id
911            can be any one of Comment (``t1_``), Link (``t3_``), or Subreddit
912            (``t5_``) to lookup by fullname.
913        :returns: When a single ``thing_id`` is provided, return the
914            corresponding thing object, or ``None`` if not found. When a list
915            of ``thing_id``s or a ``url`` is provided return a list of thing
916            objects (up to ``limit``). ``None`` is returned if all of the
917            thing_ids or the URL is invalid.
918
919        The additional parameters are passed into :meth:`.get_content` after
920        the `params` parameter is exctracted and used to update the dictionary
921        of url parameters this function sends. Note: the `url` parameter
922        cannot be altered.
923
924        Also, if using thing_id and the `limit` parameter passed to
925        :meth:`.get_content` is used to slice the list of retreived things
926        before returning it to the user, for when `limit > 100` and
927        `(limit % 100) > 0`, to ensure a maximum of `limit` thigns are
928        returned.
929
930        """
931        if bool(url) == bool(thing_id):
932            raise TypeError('Only one of url or thing_id is required!')
933
934        # In these cases, we will have a list of things to return.
935        # Otherwise, it will just be one item.
936        if isinstance(thing_id, six.string_types) and ',' in thing_id:
937            thing_id = thing_id.split(',')
938        return_list = bool(url) or not isinstance(thing_id, six.string_types)
939
940        if url:
941            param_groups = [{'url': url}]
942        else:
943            if isinstance(thing_id, six.string_types):
944                thing_id = [thing_id]
945            id_chunks = chunk_sequence(thing_id, 100)
946            param_groups = [{'id': ','.join(id_chunk)} for
947                            id_chunk in id_chunks]
948
949        items = []
950        update_with = kwargs.pop('params', {})
951        for param_group in param_groups:
952            param_group.update(update_with)
953            kwargs['params'] = param_group
954            chunk = self.get_content(self.config['info'], *args, **kwargs)
955            items.extend(list(chunk))
956
957        # if using ids, manually set the limit
958        if kwargs.get('limit'):
959            items = items[:kwargs['limit']]
960
961        if return_list:
962            return items if items else None
963        elif items:
964            return items[0]
965        else:
966            return None
967
968    @decorators.restrict_access(scope='read')
969    def get_moderators(self, subreddit, **kwargs):
970        """Return the list of moderators for the given subreddit."""
971        url = self.config['moderators'].format(
972            subreddit=six.text_type(subreddit))
973        return self.request_json(url, **kwargs)
974
975    @decorators.restrict_access(scope='read')
976    def get_new(self, *args, **kwargs):
977        """Return a get_content generator for new submissions.
978
979        Corresponds to the submissions provided by
980        ``https://www.reddit.com/new/`` for the session.
981
982        The additional parameters are passed directly into
983        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
984
985        """
986        return self.get_content(self.config['new'], *args, **kwargs)
987
988    def get_new_subreddits(self, *args, **kwargs):
989        """Return a get_content generator for the newest subreddits.
990
991        The additional parameters are passed directly into
992        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
993
994        """
995        url = self.config['new_subreddits']
996        return self.get_content(url, *args, **kwargs)
997
998    def get_popular_subreddits(self, *args, **kwargs):
999        """Return a get_content generator for the most active subreddits.
1000
1001        The additional parameters are passed directly into
1002        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
1003
1004        """
1005        url = self.config['popular_subreddits']
1006        return self.get_content(url, *args, **kwargs)
1007
1008    def get_random_subreddit(self, nsfw=False):
1009        """Return a random Subreddit object.
1010
1011        :param nsfw: When true, return a random NSFW Subreddit object. Calling
1012            in this manner will set the 'over18' cookie for the duration of the
1013            PRAW session.
1014
1015        """
1016        path = 'random'
1017        if nsfw:
1018            self.http.cookies.set('over18', '1')
1019            path = 'randnsfw'
1020        url = self.config['subreddit'].format(subreddit=path)
1021        response = self._request(url, params={'unique': self._unique_count},
1022                                 raw_response=True)
1023        self._unique_count += 1
1024        return self.get_subreddit(response.url.rsplit('/', 2)[-2])
1025
1026    def get_random_submission(self, subreddit='all'):
1027        """Return a random Submission object.
1028
1029        :param subreddit: Limit the submission to the specified
1030            subreddit(s). Default: all
1031
1032        """
1033        url = self.config['subreddit_random'].format(
1034            subreddit=six.text_type(subreddit))
1035        try:
1036            item = self.request_json(url,
1037                                     params={'unique': self._unique_count})
1038            self._unique_count += 1  # Avoid network-level caching
1039            return objects.Submission.from_json(item)
1040        except errors.RedirectException as exc:
1041            self._unique_count += 1
1042            return self.get_submission(exc.response_url)
1043        raise errors.ClientException('Expected exception not raised.')
1044
1045    def get_redditor(self, user_name, *args, **kwargs):
1046        """Return a Redditor instance for the user_name specified.
1047
1048        The additional parameters are passed directly into the
1049        :class:`.Redditor` constructor.
1050
1051        """
1052        return objects.Redditor(self, user_name, *args, **kwargs)
1053
1054    @decorators.restrict_access(scope='read')
1055    def get_rising(self, *args, **kwargs):
1056        """Return a get_content generator for rising submissions.
1057
1058        Corresponds to the submissions provided by
1059        ``https://www.reddit.com/rising/`` for the session.
1060
1061        The additional parameters are passed directly into
1062        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
1063
1064        """
1065        return self.get_content(self.config['rising'], *args, **kwargs)
1066
1067    @decorators.restrict_access(scope='read')
1068    def get_rules(self, subreddit, bottom=False):
1069        """Return the json dictionary containing rules for a subreddit.
1070
1071        :param subreddit: The subreddit whose rules we will return.
1072
1073        """
1074        url = self.config['rules'].format(subreddit=six.text_type(subreddit))
1075        return self.request_json(url)
1076
1077    @decorators.restrict_access(scope='read')
1078    def get_sticky(self, subreddit, bottom=False):
1079        """Return a Submission object for the sticky of the subreddit.
1080
1081        :param bottom: Get the top or bottom sticky. If the subreddit has only
1082            a single sticky, it is considered the top one.
1083
1084        """
1085        url = self.config['sticky'].format(subreddit=six.text_type(subreddit))
1086        param = {'num': 2} if bottom else None
1087        return objects.Submission.from_json(self.request_json(url,
1088                                                              params=param))
1089
1090    def get_submission(self, url=None, submission_id=None, comment_limit=0,
1091                       comment_sort=None, params=None):
1092        """Return a Submission object for the given url or submission_id.
1093
1094        :param comment_limit: The desired number of comments to fetch. If <= 0
1095            fetch the default number for the session's user. If None, fetch the
1096            maximum possible.
1097        :param comment_sort: The sort order for retrieved comments. When None
1098            use the default for the session's user.
1099        :param params: Dictionary containing extra GET data to put in the url.
1100
1101        """
1102        if bool(url) == bool(submission_id):
1103            raise TypeError('One (and only one) of id or url is required!')
1104        if submission_id:
1105            url = urljoin(self.config['comments'], submission_id)
1106        return objects.Submission.from_url(self, url,
1107                                           comment_limit=comment_limit,
1108                                           comment_sort=comment_sort,
1109                                           params=params)
1110
1111    def get_submissions(self, fullnames, *args, **kwargs):
1112        """Generate Submission objects for each item provided in `fullnames`.
1113
1114        A submission fullname looks like `t3_<base36_id>`. Submissions are
1115        yielded in the same order they appear in `fullnames`.
1116
1117        Up to 100 items are batched at a time -- this happens transparently.
1118
1119        The additional parameters are passed directly into
1120        :meth:`.get_content`. Note: the `url` and `limit` parameters cannot be
1121        altered.
1122
1123        """
1124        fullnames = fullnames[:]
1125        while fullnames:
1126            cur = fullnames[:100]
1127            fullnames[:100] = []
1128            url = self.config['by_id'] + ','.join(cur)
1129            for item in self.get_content(url, limit=len(cur), *args, **kwargs):
1130                yield item
1131
1132    def get_subreddit(self, subreddit_name, *args, **kwargs):
1133        """Return a Subreddit object for the subreddit_name specified.
1134
1135        The additional parameters are passed directly into the
1136        :class:`.Subreddit` constructor.
1137
1138        """
1139        sr_name_lower = subreddit_name.lower()
1140        if sr_name_lower == 'random':
1141            return self.get_random_subreddit()
1142        elif sr_name_lower == 'randnsfw':
1143            return self.get_random_subreddit(nsfw=True)
1144        return objects.Subreddit(self, subreddit_name, *args, **kwargs)
1145
1146    def get_subreddit_recommendations(self, subreddits, omit=None):
1147        """Return a list of recommended subreddits as Subreddit objects.
1148
1149        Subreddits with activity less than a certain threshold, will not have
1150        any recommendations due to lack of data.
1151
1152        :param subreddits: A list of subreddits (either names or Subreddit
1153            objects) to base the recommendations on.
1154        :param omit: A list of subreddits (either names or Subreddit
1155            objects) that will be filtered out of the result.
1156
1157        """
1158        params = {'omit': _to_reddit_list(omit or [])}
1159        url = self.config['sub_recommendations'].format(
1160            subreddits=_to_reddit_list(subreddits))
1161        result = self.request_json(url, params=params)
1162        return [objects.Subreddit(self, sub['sr_name']) for sub in result]
1163
1164    @decorators.restrict_access(scope='read')
1165    def get_top(self, *args, **kwargs):
1166        """Return a get_content generator for top submissions.
1167
1168        Corresponds to the submissions provided by
1169        ``https://www.reddit.com/top/`` for the session.
1170
1171        The additional parameters are passed directly into
1172        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
1173
1174        """
1175        return self.get_content(self.config['top'], *args, **kwargs)
1176
1177    @decorators.restrict_access(scope='read')
1178    def get_gilded(self, *args, **kwargs):
1179        """Return a get_content generator for gilded submissions.
1180
1181        Corresponds to the submissions provided by
1182        ``https://www.reddit.com/gilded/`` for the session.
1183
1184        The additional parameters are passed directly into
1185        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
1186
1187        """
1188        return self.get_content(self.config['gilded'], *args, **kwargs)
1189
1190    # There exists a `modtraffic` scope, but it is unused.
1191    @decorators.restrict_access(scope='modconfig')
1192    def get_traffic(self, subreddit):
1193        """Return the json dictionary containing traffic stats for a subreddit.
1194
1195        :param subreddit: The subreddit whose /about/traffic page we will
1196            collect.
1197
1198        """
1199        url = self.config['subreddit_traffic'].format(
1200            subreddit=six.text_type(subreddit))
1201        return self.request_json(url)
1202
1203    @decorators.restrict_access(scope='wikiread', login=False)
1204    def get_wiki_page(self, subreddit, page):
1205        """Return a WikiPage object for the subreddit and page provided."""
1206        return objects.WikiPage(self, six.text_type(subreddit), page.lower())
1207
1208    @decorators.restrict_access(scope='wikiread', login=False)
1209    def get_wiki_pages(self, subreddit):
1210        """Return a list of WikiPage objects for the subreddit."""
1211        url = self.config['wiki_pages'].format(
1212            subreddit=six.text_type(subreddit))
1213        return self.request_json(url)
1214
1215    def is_username_available(self, username):
1216        """Return True if username is valid and available, otherwise False."""
1217        params = {'user': username}
1218        try:
1219            result = self.request_json(self.config['username_available'],
1220                                       params=params)
1221        except errors.BadUsername:
1222            return False
1223        return result
1224
1225    def search(self, query, subreddit=None, sort=None, syntax=None,
1226               period=None, *args, **kwargs):
1227        """Return a generator for submissions that match the search query.
1228
1229        :param query: The query string to search for. If query is a URL only
1230            submissions which link to that URL will be returned.
1231        :param subreddit: Limit search results to the subreddit if provided.
1232        :param sort: The sort order of the results.
1233        :param syntax: The syntax of the search query.
1234        :param period: The time period of the results.
1235
1236        The additional parameters are passed directly into
1237        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
1238
1239        See https://www.reddit.com/wiki/search for more information on how to
1240        build a search query.
1241
1242        """
1243        params = {'q': query}
1244        if 'params' in kwargs:
1245            params.update(kwargs['params'])
1246            kwargs.pop('params')
1247        if sort:
1248            params['sort'] = sort
1249        if syntax:
1250            params['syntax'] = syntax
1251        if period:
1252            params['t'] = period
1253        if subreddit:
1254            params['restrict_sr'] = 'on'
1255            subreddit = six.text_type(subreddit)
1256        else:
1257            subreddit = 'all'
1258        url = self.config['search'].format(subreddit=subreddit)
1259
1260        depth = 2
1261        while depth > 0:
1262            depth -= 1
1263            try:
1264                for item in self.get_content(url, params=params, *args,
1265                                             **kwargs):
1266                    yield item
1267                break
1268            except errors.RedirectException as exc:
1269                parsed = urlparse(exc.response_url)
1270                params = dict((k, ",".join(v)) for k, v in
1271                              parse_qs(parsed.query).items())
1272                url = urlunparse(parsed[:3] + ("", "", ""))
1273                # Handle redirects from URL searches
1274                if 'already_submitted' in params:
1275                    yield self.get_submission(url)
1276                    break
1277
1278    def search_reddit_names(self, query):
1279        """Return subreddits whose display name contains the query."""
1280        data = {'query': query}
1281        results = self.request_json(self.config['search_reddit_names'],
1282                                    data=data)
1283        return [self.get_subreddit(name) for name in results['names']]
1284
1285
1286class AuthenticatedReddit(OAuth2Reddit, UnauthenticatedReddit):
1287    """This class adds the methods necessary for authenticating with reddit.
1288
1289    Authentication can either be login based
1290    (through :meth:`~praw.__init__.AuthenticatedReddit.login`), or OAuth2 based
1291    (via :meth:`~praw.__init__.AuthenticatedReddit.set_access_credentials`).
1292
1293    You should **not** directly instantiate instances of this class. Use
1294    :class:`.Reddit` instead.
1295
1296    """
1297
1298    def __init__(self, *args, **kwargs):
1299        """Initialize an AuthenticatedReddit instance."""
1300        super(AuthenticatedReddit, self).__init__(*args, **kwargs)
1301        # Add variable to distinguish between authentication type
1302        #  * None means unauthenticated
1303        #  * True mean login authenticated
1304        #  * set(...) means OAuth authenticated with the scopes in the set
1305        self._authentication = None
1306        self.access_token = None
1307        self.refresh_token = self.config.refresh_token or None
1308        self.user = None
1309
1310    def __str__(self):
1311        """Return a string representation of the AuthenticatedReddit."""
1312        if isinstance(self._authentication, set):
1313            return 'OAuth2 reddit session (scopes: {0})'.format(
1314                ', '.join(self._authentication))
1315        elif self._authentication:
1316            return 'LoggedIn reddit session (user: {0})'.format(self.user)
1317        else:
1318            return 'Unauthenticated reddit session'
1319
1320    def _url_update(self, url):
1321        # When getting posts from a multireddit owned by the authenticated
1322        # Redditor, we are redirected to me/m/multi/. Handle that now
1323        # instead of catching later.
1324        if re.search('user/.*/m/.*', url):
1325            redditor = url.split('/')[-4]
1326            if self.user and self.user.name.lower() == redditor.lower():
1327                url = url.replace("user/"+redditor, 'me')
1328        return url
1329
1330    @decorators.restrict_access(scope='modself', mod=False)
1331    def accept_moderator_invite(self, subreddit):
1332        """Accept a moderator invite to the given subreddit.
1333
1334        Callable upon an instance of Subreddit with no arguments.
1335
1336        :returns: The json response from the server.
1337
1338        """
1339        data = {'r': six.text_type(subreddit)}
1340        # Clear moderated subreddits and cache
1341        self.user._mod_subs = None  # pylint: disable=W0212
1342        self.evict(self.config['my_mod_subreddits'])
1343        return self.request_json(self.config['accept_mod_invite'], data=data)
1344
1345    def clear_authentication(self):
1346        """Clear any existing authentication on the reddit object.
1347
1348        This function is implicitly called on `login` and
1349        `set_access_credentials`.
1350
1351        """
1352        self._authentication = None
1353        self.access_token = None
1354        self.refresh_token = None
1355        self.http.cookies.clear()
1356        self.user = None
1357
1358    def delete(self, password, message=""):
1359        """Delete the currently authenticated redditor.
1360
1361        WARNING!
1362
1363        This action is IRREVERSIBLE. Use only if you're okay with NEVER
1364        accessing this reddit account again.
1365
1366        :param password: password for currently authenticated account
1367        :param message: optional 'reason for deletion' message.
1368        :returns: json response from the server.
1369
1370        """
1371        data = {'user': self.user.name,
1372                'passwd': password,
1373                'delete_message': message,
1374                'confirm': True}
1375        return self.request_json(self.config['delete_redditor'], data=data)
1376
1377    @decorators.restrict_access(scope='wikiedit')
1378    def edit_wiki_page(self, subreddit, page, content, reason=''):
1379        """Create or edit a wiki page with title `page` for `subreddit`.
1380
1381        :returns: The json response from the server.
1382
1383        """
1384        data = {'content': content,
1385                'page': page,
1386                'r': six.text_type(subreddit),
1387                'reason': reason}
1388        evict = self.config['wiki_page'].format(
1389            subreddit=six.text_type(subreddit), page=page.lower())
1390        self.evict(evict)
1391        return self.request_json(self.config['wiki_edit'], data=data)
1392
1393    def get_access_information(self, code,  # pylint: disable=W0221
1394                               update_session=True):
1395        """Return the access information for an OAuth2 authorization grant.
1396
1397        :param code: the code received in the request from the OAuth2 server
1398        :param update_session: Update the current session with the retrieved
1399            token(s).
1400        :returns: A dictionary with the key/value pairs for access_token,
1401            refresh_token and scope. The refresh_token value will be done when
1402            the OAuth2 grant is not refreshable.
1403
1404        """
1405        retval = super(AuthenticatedReddit, self).get_access_information(code)
1406        if update_session:
1407            self.set_access_credentials(**retval)
1408        return retval
1409
1410    @decorators.restrict_access(scope='flair')
1411    def get_flair_choices(self, subreddit, link=None):
1412        """Return available flair choices and current flair.
1413
1414        :param link: If link is given, return the flair options for this
1415            submission. Not normally given directly, but instead set by calling
1416            the flair_choices method for Submission objects.
1417            Use the default for the session's user.
1418
1419        :returns: A dictionary with 2 keys. 'current' containing current flair
1420            settings for the authenticated user and 'choices' containing a list
1421            of possible flair choices.
1422
1423        """
1424        data = {'r':  six.text_type(subreddit), 'link': link}
1425        return self.request_json(self.config['flairselector'], data=data)
1426
1427    @decorators.restrict_access(scope='read', login=True)
1428    def get_friends(self, **params):
1429        """Return a UserList of Redditors with whom the user is friends."""
1430        url = self.config['friends']
1431        return self.request_json(url, params=params)[0]
1432
1433    @decorators.restrict_access(scope='identity', oauth_only=True)
1434    def get_me(self):
1435        """Return a LoggedInRedditor object.
1436
1437        Note: This function is only intended to be used with an 'identity'
1438        providing OAuth2 grant.
1439        """
1440        response = self.request_json(self.config['me'])
1441        user = objects.Redditor(self, response['name'], response)
1442        user.__class__ = objects.LoggedInRedditor
1443        return user
1444
1445    def has_scope(self, scope):
1446        """Return True if OAuth2 authorized for the passed in scope(s)."""
1447        if not self.is_oauth_session():
1448            return False
1449        if '*' in self._authentication:
1450            return True
1451        if isinstance(scope, six.string_types):
1452            scope = [scope]
1453        return all(s in self._authentication for s in scope)
1454
1455    def is_logged_in(self):
1456        """Return True when the session is authenticated via username/password.
1457
1458        Username and passwords are provided via
1459        :meth:`~praw.__init__.AuthenticatedReddit.login`.
1460
1461        """
1462        return self._authentication is True
1463
1464    def is_oauth_session(self):
1465        """Return True when the current session is an OAuth2 session."""
1466        return isinstance(self._authentication, set)
1467
1468    @decorators.deprecated('reddit intends to disable password-based '
1469                           'authentication of API clients sometime in the '
1470                           'near future. As a result this method will be '
1471                           'removed in a future major version of PRAW.\n\n'
1472                           'For more information please see:\n\n'
1473                           '* Original reddit deprecation notice: '
1474                           'https://www.reddit.com/comments/2ujhkr/\n\n'
1475                           '* Updated delayed deprecation notice: '
1476                           'https://www.reddit.com/comments/37e2mv/\n\n'
1477                           'Pass ``disable_warning=True`` to ``login`` to '
1478                           'disable this warning.')
1479    def login(self, username=None, password=None, **kwargs):
1480        """Login to a reddit site.
1481
1482        **DEPRECATED**. Will be removed in a future version of PRAW.
1483
1484        https://www.reddit.com/comments/2ujhkr/
1485        https://www.reddit.com/comments/37e2mv/
1486
1487        Look for username first in parameter, then praw.ini and finally if both
1488        were empty get it from stdin. Look for password in parameter, then
1489        praw.ini (but only if username matches that in praw.ini) and finally
1490        if they both are empty get it with getpass. Add the variables ``user``
1491        (username) and ``pswd`` (password) to your praw.ini file to allow for
1492        auto-login.
1493
1494        A successful login will overwrite any existing authentication.
1495
1496        """
1497        if password and not username:
1498            raise Exception('Username must be provided when password is.')
1499        user = username or self.config.user
1500        if not user:
1501            sys.stdout.write('Username: ')
1502            sys.stdout.flush()
1503            user = sys.stdin.readline().strip()
1504            pswd = None
1505        else:
1506            pswd = password or self.config.pswd
1507        if not pswd:
1508            import getpass
1509            pswd = getpass.getpass('Password for {0}: '.format(user)
1510                                   .encode('ascii', 'ignore'))
1511
1512        data = {'passwd': pswd,
1513                'user': user}
1514        self.clear_authentication()
1515        self.request_json(self.config['login'], data=data)
1516        # Update authentication settings
1517        self._authentication = True
1518        self.user = self.get_redditor(user)
1519        self.user.__class__ = objects.LoggedInRedditor
1520
1521    def refresh_access_information(self,  # pylint: disable=W0221
1522                                   refresh_token=None,
1523                                   update_session=True):
1524        """Return updated access information for an OAuth2 authorization grant.
1525
1526        :param refresh_token: The refresh token used to obtain the updated
1527            information. When not provided, use the stored refresh_token.
1528        :param update_session: Update the session with the returned data.
1529        :returns: A dictionary with the key/value pairs for ``access_token``,
1530            ``refresh_token`` and ``scope``. The ``refresh_token`` value will
1531            be None when the OAuth2 grant is not refreshable. The ``scope``
1532            value will be a set containing the scopes the tokens are valid for.
1533
1534        """
1535        response = super(AuthenticatedReddit, self).refresh_access_information(
1536            refresh_token=refresh_token or self.refresh_token)
1537        if update_session:
1538            self.set_access_credentials(**response)
1539        return response
1540
1541    @decorators.restrict_access(scope='flair')
1542    def select_flair(self, item, flair_template_id='', flair_text=''):
1543        """Select user flair or link flair on subreddits.
1544
1545        This can only be used for assigning your own name flair or link flair
1546        on your own submissions. For assigning other's flairs using moderator
1547        access, see :meth:`~praw.__init__.ModFlairMixin.set_flair`.
1548
1549        :param item: A string, Subreddit object (for user flair), or
1550            Submission object (for link flair). If ``item`` is a string it
1551            will be treated as the name of a Subreddit.
1552        :param flair_template_id: The id for the desired flair template. Use
1553            the :meth:`~praw.objects.Subreddit.get_flair_choices` and
1554            :meth:`~praw.objects.Submission.get_flair_choices` methods to find
1555            the ids for the available user and link flair choices.
1556        :param flair_text: A string containing the custom flair text.
1557            Used on subreddits that allow it.
1558
1559        :returns: The json response from the server.
1560
1561        """
1562        data = {'flair_template_id': flair_template_id or '',
1563                'text':              flair_text or ''}
1564        if isinstance(item, objects.Submission):
1565            # Link flair
1566            data['link'] = item.fullname
1567            evict = item.permalink
1568        else:
1569            # User flair
1570            data['name'] = self.user.name
1571            data['r'] = six.text_type(item)
1572            evict = self.config['flairlist'].format(
1573                subreddit=six.text_type(item))
1574        response = self.request_json(self.config['select_flair'], data=data)
1575        self.evict(evict)
1576        return response
1577
1578    @decorators.require_oauth
1579    def set_access_credentials(self, scope, access_token, refresh_token=None,
1580                               update_user=True):
1581        """Set the credentials used for OAuth2 authentication.
1582
1583        Calling this function will overwrite any currently existing access
1584        credentials.
1585
1586        :param scope: A set of reddit scopes the tokens provide access to
1587        :param access_token: the access token of the authentication
1588        :param refresh_token: the refresh token of the authentication
1589        :param update_user: Whether or not to set the user attribute for
1590            identity scopes
1591
1592        """
1593        if isinstance(scope, (list, tuple)):
1594            scope = set(scope)
1595        elif isinstance(scope, six.string_types):
1596            scope = set(scope.split())
1597        if not isinstance(scope, set):
1598            raise TypeError('`scope` parameter must be a set')
1599        self.clear_authentication()
1600        # Update authentication settings
1601        self._authentication = scope
1602        self.access_token = access_token
1603        self.refresh_token = refresh_token
1604        # Update the user object
1605        if update_user and ('identity' in scope or '*' in scope):
1606            self.user = self.get_me()
1607
1608
1609class ModConfigMixin(AuthenticatedReddit):
1610    """Adds methods requiring the 'modconfig' scope (or mod access).
1611
1612    You should **not** directly instantiate instances of this class. Use
1613    :class:`.Reddit` instead.
1614
1615    """
1616
1617    @decorators.restrict_access(scope='modconfig', mod=False)
1618    @decorators.require_captcha
1619    def create_subreddit(self, name, title, description='', language='en',
1620                         subreddit_type='public', content_options='any',
1621                         over_18=False, default_set=True, show_media=False,
1622                         domain='', wikimode='disabled', captcha=None,
1623                         **kwargs):
1624        """Create a new subreddit.
1625
1626        :returns: The json response from the server.
1627
1628        This function may result in a captcha challenge. PRAW will
1629        automatically prompt you for a response. See :ref:`handling-captchas`
1630        if you want to manually handle captchas.
1631
1632        """
1633        data = {'name': name,
1634                'title': title,
1635                'description': description,
1636                'lang': language,
1637                'type': subreddit_type,
1638                'link_type': content_options,
1639                'over_18': 'on' if over_18 else 'off',
1640                'allow_top': 'on' if default_set else 'off',
1641                'show_media': 'on' if show_media else 'off',
1642                'wikimode': wikimode,
1643                'domain': domain}
1644        if captcha:
1645            data.update(captcha)
1646        return self.request_json(self.config['site_admin'], data=data)
1647
1648    @decorators.restrict_access(scope='modconfig')
1649    def delete_image(self, subreddit, name=None, header=False):
1650        """Delete an image from the subreddit.
1651
1652        :param name: The name of the image if removing a CSS image.
1653        :param header: When true, delete the subreddit header.
1654        :returns: The json response from the server.
1655
1656        """
1657        subreddit = six.text_type(subreddit)
1658        if name and header:
1659            raise TypeError('Both name and header cannot be set.')
1660        elif name:
1661            data = {'img_name': name}
1662            url = self.config['delete_sr_image']
1663            self.evict(self.config['stylesheet'].format(subreddit=subreddit))
1664        else:
1665            data = True
1666            url = self.config['delete_sr_header']
1667        url = url.format(subreddit=subreddit)
1668        return self.request_json(url, data=data)
1669
1670    @decorators.restrict_access(scope='modconfig')
1671    def get_settings(self, subreddit, **params):
1672        """Return the settings for the given subreddit."""
1673        url = self.config['subreddit_settings'].format(
1674            subreddit=six.text_type(subreddit))
1675        return self.request_json(url, params=params)['data']
1676
1677    @decorators.restrict_access(scope='modconfig')
1678    def set_settings(self, subreddit, title, public_description='',
1679                     description='', language='en', subreddit_type='public',
1680                     content_options='any', over_18=False, default_set=True,
1681                     show_media=False, domain='', domain_css=False,
1682                     domain_sidebar=False, header_hover_text='',
1683                     wikimode='disabled', wiki_edit_age=30,
1684                     wiki_edit_karma=100,
1685                     submit_link_label='', submit_text_label='',
1686                     exclude_banned_modqueue=False, comment_score_hide_mins=0,
1687                     public_traffic=False, collapse_deleted_comments=False,
1688                     spam_comments='low', spam_links='high',
1689                     spam_selfposts='high', submit_text='',
1690                     hide_ads=False, suggested_comment_sort='',
1691                     key_color='',
1692                     **kwargs):
1693        """Set the settings for the given subreddit.
1694
1695        :param subreddit: Must be a subreddit object.
1696        :returns: The json response from the server.
1697
1698        """
1699        data = {'sr': subreddit.fullname,
1700                'allow_top': default_set,
1701                'comment_score_hide_mins': comment_score_hide_mins,
1702                'collapse_deleted_comments': collapse_deleted_comments,
1703                'description': description,
1704                'domain': domain or '',
1705                'domain_css': domain_css,
1706                'domain_sidebar': domain_sidebar,
1707                'exclude_banned_modqueue': exclude_banned_modqueue,
1708                'header-title': header_hover_text or '',
1709                'hide_ads': hide_ads,
1710                'key_color': key_color,
1711                'lang': language,
1712                'link_type': content_options,
1713                'over_18': over_18,
1714                'public_description': public_description,
1715                'public_traffic': public_traffic,
1716                'show_media': show_media,
1717                'submit_link_label': submit_link_label or '',
1718                'submit_text': submit_text,
1719                'submit_text_label': submit_text_label or '',
1720                'suggested_comment_sort': suggested_comment_sort or '',
1721                'spam_comments': spam_comments,
1722                'spam_links': spam_links,
1723                'spam_selfposts': spam_selfposts,
1724                'title': title,
1725                'type': subreddit_type,
1726                'wiki_edit_age': six.text_type(wiki_edit_age),
1727                'wiki_edit_karma': six.text_type(wiki_edit_karma),
1728                'wikimode': wikimode}
1729
1730        if kwargs:
1731            msg = 'Extra settings fields: {0}'.format(kwargs.keys())
1732            warn_explicit(msg, UserWarning, '', 0)
1733            data.update(kwargs)
1734        evict = self.config['subreddit_settings'].format(
1735            subreddit=six.text_type(subreddit))
1736        self.evict(evict)
1737        return self.request_json(self.config['site_admin'], data=data)
1738
1739    @decorators.restrict_access(scope='modconfig')
1740    def set_stylesheet(self, subreddit, stylesheet):
1741        """Set stylesheet for the given subreddit.
1742
1743        :returns: The json response from the server.
1744
1745        """
1746        subreddit = six.text_type(subreddit)
1747        data = {'r': subreddit,
1748                'stylesheet_contents': stylesheet,
1749                'op': 'save'}  # Options: save / preview
1750        self.evict(self.config['stylesheet'].format(subreddit=subreddit))
1751        return self.request_json(self.config['subreddit_css'], data=data)
1752
1753    @decorators.restrict_access(scope='modconfig')
1754    def upload_image(self, subreddit, image_path, name=None,
1755                     header=False, upload_as=None):
1756        """Upload an image to the subreddit.
1757
1758        :param image_path: A path to the jpg or png image you want to upload.
1759        :param name: The name to provide the image. When None the name will be
1760            filename less any extension.
1761        :param header: When True, upload the image as the subreddit header.
1762        :param upload_as: Must be `'jpg'`, `'png'` or `None`. When None, this
1763            will match the format of the image itself. In all cases where both
1764            this value and the image format is not png, reddit will also
1765            convert  the image mode to RGBA. reddit optimizes the image
1766            according to this value.
1767        :returns: A link to the uploaded image. Raises an exception otherwise.
1768
1769        """
1770        if name and header:
1771            raise TypeError('Both name and header cannot be set.')
1772        if upload_as not in (None, 'png', 'jpg'):
1773            raise TypeError("upload_as must be 'jpg', 'png', or None.")
1774        with open(image_path, 'rb') as image:
1775            image_type = upload_as or _image_type(image)
1776            data = {'r': six.text_type(subreddit), 'img_type': image_type}
1777            if header:
1778                data['header'] = 1
1779            else:
1780                if not name:
1781                    name = os.path.splitext(os.path.basename(image.name))[0]
1782                data['name'] = name
1783
1784            response = json.loads(self._request(
1785                self.config['upload_image'], data=data, files={'file': image},
1786                method=to_native_string('POST'), retry_on_error=False))
1787
1788        if response['errors']:
1789            raise errors.APIException(response['errors'], None)
1790        return response['img_src']
1791
1792    def update_settings(self, subreddit, **kwargs):
1793        """Update only the given settings for the given subreddit.
1794
1795        The settings to update must be given by keyword and match one of the
1796        parameter names in `set_settings`.
1797
1798        :returns: The json response from the server.
1799
1800        """
1801        settings = self.get_settings(subreddit)
1802        settings.update(kwargs)
1803        del settings['subreddit_id']
1804        return self.set_settings(subreddit, **settings)
1805
1806
1807class ModFlairMixin(AuthenticatedReddit):
1808    """Adds methods requiring the 'modflair' scope (or mod access).
1809
1810    You should **not** directly instantiate instances of this class. Use
1811    :class:`.Reddit` instead.
1812
1813    """
1814
1815    @decorators.restrict_access(scope='modflair')
1816    def add_flair_template(self, subreddit, text='', css_class='',
1817                           text_editable=False, is_link=False):
1818        """Add a flair template to the given subreddit.
1819
1820        :returns: The json response from the server.
1821
1822        """
1823        data = {'r': six.text_type(subreddit),
1824                'text': text,
1825                'css_class': css_class,
1826                'text_editable': six.text_type(text_editable),
1827                'flair_type': 'LINK_FLAIR' if is_link else 'USER_FLAIR'}
1828        return self.request_json(self.config['flairtemplate'], data=data)
1829
1830    @decorators.restrict_access(scope='modflair')
1831    def clear_flair_templates(self, subreddit, is_link=False):
1832        """Clear flair templates for the given subreddit.
1833
1834        :returns: The json response from the server.
1835
1836        """
1837        data = {'r': six.text_type(subreddit),
1838                'flair_type': 'LINK_FLAIR' if is_link else 'USER_FLAIR'}
1839        return self.request_json(self.config['clearflairtemplates'], data=data)
1840
1841    @decorators.restrict_access(scope='modflair')
1842    def configure_flair(self, subreddit, flair_enabled=False,
1843                        flair_position='right',
1844                        flair_self_assign=False,
1845                        link_flair_enabled=False,
1846                        link_flair_position='left',
1847                        link_flair_self_assign=False):
1848        """Configure the flair setting for the given subreddit.
1849
1850        :returns: The json response from the server.
1851
1852        """
1853        flair_enabled = 'on' if flair_enabled else 'off'
1854        flair_self_assign = 'on' if flair_self_assign else 'off'
1855        if not link_flair_enabled:
1856            link_flair_position = ''
1857        link_flair_self_assign = 'on' if link_flair_self_assign else 'off'
1858        data = {'r': six.text_type(subreddit),
1859                'flair_enabled': flair_enabled,
1860                'flair_position': flair_position,
1861                'flair_self_assign_enabled': flair_self_assign,
1862                'link_flair_position': link_flair_position,
1863                'link_flair_self_assign_enabled': link_flair_self_assign}
1864        return self.request_json(self.config['flairconfig'], data=data)
1865
1866    @decorators.restrict_access(scope='modflair')
1867    def delete_flair(self, subreddit, user):
1868        """Delete the flair for the given user on the given subreddit.
1869
1870        :returns: The json response from the server.
1871
1872        """
1873        data = {'r': six.text_type(subreddit),
1874                'name': six.text_type(user)}
1875        return self.request_json(self.config['deleteflair'], data=data)
1876
1877    @decorators.restrict_access(scope='modflair')
1878    def get_flair_list(self, subreddit, *args, **kwargs):
1879        """Return a get_content generator of flair mappings.
1880
1881        :param subreddit: Either a Subreddit object or the name of the
1882            subreddit to return the flair list for.
1883
1884        The additional parameters are passed directly into
1885        :meth:`.get_content`. Note: the `url`, `root_field`, `thing_field`, and
1886        `after_field` parameters cannot be altered.
1887
1888        """
1889        url = self.config['flairlist'].format(
1890            subreddit=six.text_type(subreddit))
1891        return self.get_content(url, *args, root_field=None,
1892                                thing_field='users', after_field='next',
1893                                **kwargs)
1894
1895    @decorators.restrict_access(scope='modflair')
1896    def set_flair(self, subreddit, item, flair_text='', flair_css_class=''):
1897        """Set flair for the user in the given subreddit.
1898
1899        `item` can be a string, Redditor object, or Submission object.
1900        If `item` is a string it will be treated as the name of a Redditor.
1901
1902        This method can only be called by a subreddit moderator with flair
1903        permissions. To set flair on yourself or your own links use
1904        :meth:`~praw.__init__.AuthenticatedReddit.select_flair`.
1905
1906        :returns: The json response from the server.
1907
1908        """
1909        data = {'r': six.text_type(subreddit),
1910                'text': flair_text or '',
1911                'css_class': flair_css_class or ''}
1912        if isinstance(item, objects.Submission):
1913            data['link'] = item.fullname
1914            evict = item.permalink
1915        else:
1916            data['name'] = six.text_type(item)
1917            evict = self.config['flairlist'].format(
1918                subreddit=six.text_type(subreddit))
1919        response = self.request_json(self.config['flair'], data=data)
1920        self.evict(evict)
1921        return response
1922
1923    @decorators.restrict_access(scope='modflair')
1924    def set_flair_csv(self, subreddit, flair_mapping):
1925        """Set flair for a group of users in the given subreddit.
1926
1927        flair_mapping should be a list of dictionaries with the following keys:
1928          `user`: the user name,
1929          `flair_text`: the flair text for the user (optional),
1930          `flair_css_class`: the flair css class for the user (optional)
1931
1932        :returns: The json response from the server.
1933
1934        """
1935        if not flair_mapping:
1936            raise errors.ClientException('flair_mapping must be set')
1937        item_order = ['user', 'flair_text', 'flair_css_class']
1938        lines = []
1939        for mapping in flair_mapping:
1940            if 'user' not in mapping:
1941                raise errors.ClientException('flair_mapping must '
1942                                             'contain `user` key')
1943            lines.append(','.join([mapping.get(x, '') for x in item_order]))
1944        response = []
1945        while len(lines):
1946            data = {'r': six.text_type(subreddit),
1947                    'flair_csv': '\n'.join(lines[:100])}
1948            response.extend(self.request_json(self.config['flaircsv'],
1949                                              data=data))
1950            lines = lines[100:]
1951        evict = self.config['flairlist'].format(
1952            subreddit=six.text_type(subreddit))
1953        self.evict(evict)
1954        return response
1955
1956
1957class ModLogMixin(AuthenticatedReddit):
1958    """Adds methods requiring the 'modlog' scope (or mod access).
1959
1960    You should **not** directly instantiate instances of this class. Use
1961    :class:`.Reddit` instead.
1962
1963    """
1964
1965    @decorators.restrict_access(scope='modlog')
1966    def get_mod_log(self, subreddit, mod=None, action=None, *args, **kwargs):
1967        """Return a get_content generator for moderation log items.
1968
1969        :param subreddit: Either a Subreddit object or the name of the
1970            subreddit to return the modlog for.
1971        :param mod: If given, only return the actions made by this moderator.
1972            Both a moderator name or Redditor object can be used here.
1973        :param action: If given, only return entries for the specified action.
1974
1975        The additional parameters are passed directly into
1976        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
1977
1978        """
1979        params = kwargs.setdefault('params', {})
1980        if mod is not None:
1981            params['mod'] = six.text_type(mod)
1982        if action is not None:
1983            params['type'] = six.text_type(action)
1984        url = self.config['modlog'].format(subreddit=six.text_type(subreddit))
1985        return self.get_content(url, *args, **kwargs)
1986
1987
1988class ModOnlyMixin(AuthenticatedReddit):
1989    """Adds methods requiring the logged in moderator access.
1990
1991    You should **not** directly instantiate instances of this class. Use
1992    :class:`.Reddit` instead.
1993
1994    """
1995
1996    def _get_userlist(self, url, user_only, *args, **kwargs):
1997        content = self.get_content(url, *args, **kwargs)
1998        for data in content:
1999            user = objects.Redditor(self, data['name'], fetch=False)
2000            user.id = data['id'].split('_')[1]
2001            if user_only:
2002                yield user
2003            else:
2004                data['name'] = user
2005                yield data
2006
2007    @decorators.restrict_access(scope='read', mod=True)
2008    def get_banned(self, subreddit, user_only=True, *args, **kwargs):
2009        """Return a get_content generator of banned users for the subreddit.
2010
2011        :param subreddit: The subreddit to get the banned user list for.
2012        :param user_only: When False, the generator yields a dictionary of data
2013            associated with the server response for that user. In such cases,
2014            the Redditor will be in key 'name' (default: True).
2015
2016        """
2017        url = self.config['banned'].format(subreddit=six.text_type(subreddit))
2018        return self._get_userlist(url, user_only, *args, **kwargs)
2019
2020    def get_contributors(self, subreddit, *args, **kwargs):
2021        """
2022        Return a get_content generator of contributors for the given subreddit.
2023
2024        If it's a public subreddit, then authentication as a
2025        moderator of the subreddit is required. For protected/private
2026        subreddits only access is required. See issue #246.
2027
2028        """
2029        # pylint: disable=W0613
2030        def get_contributors_helper(self, subreddit):
2031            # It is necessary to have the 'self' argument as it's needed in
2032            # restrict_access to determine what class the decorator is
2033            # operating on.
2034            url = self.config['contributors'].format(
2035                subreddit=six.text_type(subreddit))
2036            return self._get_userlist(url, user_only=True, *args, **kwargs)
2037
2038        if self.is_logged_in():
2039            if not isinstance(subreddit, objects.Subreddit):
2040                subreddit = self.get_subreddit(subreddit)
2041            if subreddit.subreddit_type == "public":
2042                decorator = decorators.restrict_access(scope='read', mod=True)
2043                return decorator(get_contributors_helper)(self, subreddit)
2044        return get_contributors_helper(self, subreddit)
2045
2046    @decorators.restrict_access(scope='read', mod=True)
2047    def get_edited(self, subreddit='mod', *args, **kwargs):
2048        """Return a get_content generator of edited items.
2049
2050        :param subreddit: Either a Subreddit object or the name of the
2051            subreddit to return the edited items for. Defaults to `mod` which
2052            includes items for all the subreddits you moderate.
2053
2054        The additional parameters are passed directly into
2055        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
2056
2057        """
2058        url = self.config['edited'].format(subreddit=six.text_type(subreddit))
2059        return self.get_content(url, *args, **kwargs)
2060
2061    @decorators.restrict_access(scope='privatemessages', mod=True)
2062    def get_mod_mail(self, subreddit='mod', *args, **kwargs):
2063        """Return a get_content generator for moderator messages.
2064
2065        :param subreddit: Either a Subreddit object or the name of the
2066            subreddit to return the moderator mail from. Defaults to `mod`
2067            which includes items for all the subreddits you moderate.
2068
2069        The additional parameters are passed directly into
2070        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
2071
2072        """
2073        url = self.config['mod_mail'].format(
2074            subreddit=six.text_type(subreddit))
2075        return self.get_content(url, *args, **kwargs)
2076
2077    @decorators.restrict_access(scope='read', mod=True)
2078    def get_mod_queue(self, subreddit='mod', *args, **kwargs):
2079        """Return a get_content generator for the moderator queue.
2080
2081        :param subreddit: Either a Subreddit object or the name of the
2082            subreddit to return the modqueue for. Defaults to `mod` which
2083            includes items for all the subreddits you moderate.
2084
2085        The additional parameters are passed directly into
2086        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
2087
2088        """
2089        url = self.config['modqueue'].format(
2090            subreddit=six.text_type(subreddit))
2091        return self.get_content(url, *args, **kwargs)
2092
2093    @decorators.restrict_access(scope='read', mod=True)
2094    def get_muted(self, subreddit, user_only=True, *args, **kwargs):
2095        """Return a get_content generator for modmail-muted users.
2096
2097        :param subreddit: Either a Subreddit object or the name of a subreddit
2098            to get the list of muted users from.
2099
2100        The additional parameters are passed directly into
2101        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
2102
2103        """
2104        url = self.config['muted'].format(subreddit=six.text_type(subreddit))
2105        return self._get_userlist(url, user_only, *args, **kwargs)
2106
2107    @decorators.restrict_access(scope='read', mod=True)
2108    def get_reports(self, subreddit='mod', *args, **kwargs):
2109        """Return a get_content generator of reported items.
2110
2111        :param subreddit: Either a Subreddit object or the name of the
2112            subreddit to return the reported items. Defaults to `mod` which
2113            includes items for all the subreddits you moderate.
2114
2115        The additional parameters are passed directly into
2116        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
2117
2118        """
2119        url = self.config['reports'].format(subreddit=six.text_type(subreddit))
2120        return self.get_content(url, *args, **kwargs)
2121
2122    @decorators.restrict_access(scope='read', mod=True)
2123    def get_spam(self, subreddit='mod', *args, **kwargs):
2124        """Return a get_content generator of spam-filtered items.
2125
2126        :param subreddit: Either a Subreddit object or the name of the
2127            subreddit to return the spam-filtered items for. Defaults to `mod`
2128            which includes items for all the subreddits you moderate.
2129
2130        The additional parameters are passed directly into
2131        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
2132
2133        """
2134        url = self.config['spam'].format(subreddit=six.text_type(subreddit))
2135        return self.get_content(url, *args, **kwargs)
2136
2137    @decorators.restrict_access('modconfig', mod=False, login=False)
2138    def get_stylesheet(self, subreddit, **params):
2139        """Return the stylesheet and images for the given subreddit."""
2140        url = self.config['stylesheet'].format(
2141            subreddit=six.text_type(subreddit))
2142        return self.request_json(url, params=params)['data']
2143
2144    @decorators.restrict_access(scope='read', mod=True)
2145    def get_unmoderated(self, subreddit='mod', *args, **kwargs):
2146        """Return a get_content generator of unmoderated submissions.
2147
2148        :param subreddit: Either a Subreddit object or the name of the
2149            subreddit to return the unmoderated submissions for. Defaults to
2150            `mod` which includes items for all the subreddits you moderate.
2151
2152        The additional parameters are passed directly into
2153        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
2154
2155        """
2156        url = self.config['unmoderated'].format(
2157            subreddit=six.text_type(subreddit))
2158        return self.get_content(url, *args, **kwargs)
2159
2160    @decorators.restrict_access(scope='read', mod=True)
2161    def get_wiki_banned(self, subreddit, *args, **kwargs):
2162        """Return a get_content generator of users banned from the wiki."""
2163        url = self.config['wiki_banned'].format(
2164            subreddit=six.text_type(subreddit))
2165        return self._get_userlist(url, user_only=True, *args, **kwargs)
2166
2167    @decorators.restrict_access(scope='read', mod=True)
2168    def get_wiki_contributors(self, subreddit, *args, **kwargs):
2169        """Return a get_content generator of wiki contributors.
2170
2171        The returned users are those who have been approved as a wiki
2172        contributor by the moderators of the subreddit, Whether or not they've
2173        actually contributed to the wiki is irrellevant, their approval as wiki
2174        contributors is all that matters.
2175
2176        """
2177        url = self.config['wiki_contributors'].format(
2178            subreddit=six.text_type(subreddit))
2179        return self._get_userlist(url, user_only=True, *args, **kwargs)
2180
2181
2182class ModSelfMixin(AuthenticatedReddit):
2183    """Adds methods pertaining to the 'modself' OAuth scope (or login).
2184
2185    You should **not** directly instantiate instances of this class. Use
2186    :class:`.Reddit` instead.
2187
2188    """
2189
2190    def leave_contributor(self, subreddit):
2191        """Abdicate approved submitter status in a subreddit. Use with care.
2192
2193        :param subreddit: The name of the subreddit to leave `status` from.
2194
2195        :returns: the json response from the server.
2196        """
2197        return self._leave_status(subreddit, self.config['leavecontributor'])
2198
2199    def leave_moderator(self, subreddit):
2200        """Abdicate moderator status in a subreddit. Use with care.
2201
2202        :param subreddit: The name of the subreddit to leave `status` from.
2203
2204        :returns: the json response from the server.
2205        """
2206        self.evict(self.config['my_mod_subreddits'])
2207        return self._leave_status(subreddit, self.config['leavemoderator'])
2208
2209    @decorators.restrict_access(scope='modself', mod=False)
2210    def _leave_status(self, subreddit, statusurl):
2211        """Abdicate status in a subreddit.
2212
2213        :param subreddit: The name of the subreddit to leave `status` from.
2214        :param statusurl: The API URL which will be used in the leave request.
2215            Please use :meth:`leave_contributor` or :meth:`leave_moderator`
2216            rather than setting this directly.
2217
2218        :returns: the json response from the server.
2219        """
2220        if isinstance(subreddit, six.string_types):
2221            subreddit = self.get_subreddit(subreddit)
2222
2223        data = {'id': subreddit.fullname}
2224        return self.request_json(statusurl, data=data)
2225
2226
2227class MultiredditMixin(AuthenticatedReddit):
2228    """Adds methods pertaining to multireddits.
2229
2230    You should **not** directly instantiate instances of this class. Use
2231    :class:`.Reddit` instead.
2232
2233    """
2234
2235    MULTI_PATH = '/user/{0}/m/{1}'
2236
2237    @decorators.restrict_access(scope='subscribe')
2238    def copy_multireddit(self, from_redditor, from_name, to_name=None,
2239                         *args, **kwargs):
2240        """Copy a multireddit.
2241
2242        :param from_redditor: The username or Redditor object for the user
2243            who owns the original multireddit
2244        :param from_name: The name of the multireddit, belonging to
2245            from_redditor
2246        :param to_name: The name to copy the multireddit as. If None, uses
2247            the name of the original
2248
2249        The additional parameters are passed directly into
2250        :meth:`~praw.__init__.BaseReddit.request_json`
2251
2252        """
2253        if to_name is None:
2254            to_name = from_name
2255
2256        from_multipath = self.MULTI_PATH.format(from_redditor, from_name)
2257        to_multipath = self.MULTI_PATH.format(self.user.name, to_name)
2258        data = {'display_name': to_name,
2259                'from': from_multipath,
2260                'to': to_multipath}
2261        return self.request_json(self.config['multireddit_copy'], data=data,
2262                                 *args, **kwargs)
2263
2264    @decorators.restrict_access(scope='subscribe')
2265    def create_multireddit(self, name, description_md=None, icon_name=None,
2266                           key_color=None, subreddits=None, visibility=None,
2267                           weighting_scheme=None, overwrite=False,
2268                           *args, **kwargs):  # pylint: disable=W0613
2269        """Create a new multireddit.
2270
2271        :param name: The name of the new multireddit.
2272        :param description_md: Optional description for the multireddit,
2273            formatted in markdown.
2274        :param icon_name: Optional, choose an icon name from this list: ``art
2275            and design``, ``ask``, ``books``, ``business``, ``cars``,
2276            ``comics``, ``cute animals``, ``diy``, ``entertainment``, ``food
2277            and drink``, ``funny``, ``games``, ``grooming``, ``health``, ``life
2278            advice``, ``military``, ``models pinup``, ``music``, ``news``,
2279            ``philosophy``, ``pictures and gifs``, ``science``, ``shopping``,
2280            ``sports``, ``style``, ``tech``, ``travel``, ``unusual stories``,
2281            ``video``, or ``None``.
2282        :param key_color: Optional rgb hex color code of the form `#xxxxxx`.
2283        :param subreddits: Optional list of subreddit names or Subreddit
2284            objects to initialize the Multireddit with. You can always
2285            add more later with
2286            :meth:`~praw.objects.Multireddit.add_subreddit`.
2287        :param visibility: Choose a privacy setting from this list:
2288            ``public``, ``private``, ``hidden``. Defaults to private if blank.
2289        :param weighting_scheme: Choose a weighting scheme from this list:
2290            ``classic``, ``fresh``. Defaults to classic if blank.
2291        :param overwrite: Allow for overwriting / updating multireddits.
2292            If False, and the multi name already exists, throw 409 error.
2293            If True, and the multi name already exists, use the given
2294            properties to update that multi.
2295            If True, and the multi name does not exist, create it normally.
2296
2297        :returns: The newly created Multireddit object.
2298
2299        The additional parameters are passed directly into
2300        :meth:`~praw.__init__.BaseReddit.request_json`
2301
2302        """
2303        url = self.config['multireddit_about'].format(user=self.user.name,
2304                                                      multi=name)
2305        if subreddits:
2306            subreddits = [{'name': six.text_type(sr)} for sr in subreddits]
2307        model = {}
2308        for key in ('description_md', 'icon_name', 'key_color', 'subreddits',
2309                    'visibility', 'weighting_scheme'):
2310            value = locals()[key]
2311            if value:
2312                model[key] = value
2313
2314        method = 'PUT' if overwrite else 'POST'
2315        return self.request_json(url, data={'model': json.dumps(model)},
2316                                 method=method, *args, **kwargs)
2317
2318    @decorators.restrict_access(scope='subscribe')
2319    def delete_multireddit(self, name, *args, **kwargs):
2320        """Delete a Multireddit.
2321
2322        Any additional parameters are passed directly into
2323        :meth:`~praw.__init__.BaseReddit.request`
2324
2325        """
2326        url = self.config['multireddit_about'].format(user=self.user.name,
2327                                                      multi=name)
2328
2329        # The modhash isn't necessary for OAuth requests
2330        if not self._use_oauth:
2331            self.http.headers['x-modhash'] = self.modhash
2332
2333        try:
2334            self.request(url, data={}, method='DELETE', *args, **kwargs)
2335        finally:
2336            if not self._use_oauth:
2337                del self.http.headers['x-modhash']
2338
2339    def edit_multireddit(self, *args, **kwargs):
2340        """Edit a multireddit, or create one if it doesn't already exist.
2341
2342        See :meth:`create_multireddit` for accepted parameters.
2343
2344        """
2345        return self.create_multireddit(*args, overwrite=True, **kwargs)
2346
2347    def get_multireddit(self, redditor, multi, *args, **kwargs):
2348        """Return a Multireddit object for the author and name specified.
2349
2350        :param redditor: The username or Redditor object of the user
2351            who owns the multireddit.
2352        :param multi: The name of the multireddit to fetch.
2353
2354        The additional parameters are passed directly into the
2355        :class:`.Multireddit` constructor.
2356
2357        """
2358        return objects.Multireddit(self, six.text_type(redditor), multi,
2359                                   *args, **kwargs)
2360
2361    def get_multireddits(self, redditor, *args, **kwargs):
2362        """Return a list of multireddits belonging to a redditor.
2363
2364        :param redditor: The username or Redditor object to find multireddits
2365            from.
2366        :returns: The json response from the server
2367
2368        The additional parameters are passed directly into
2369        :meth:`~praw.__init__.BaseReddit.request_json`
2370
2371        If the requested redditor is the current user, all multireddits
2372        are visible. Otherwise, only public multireddits are returned.
2373
2374        """
2375        redditor = six.text_type(redditor)
2376        url = self.config['multireddit_user'].format(user=redditor)
2377        return self.request_json(url, *args, **kwargs)
2378
2379    @decorators.restrict_access(scope='subscribe')
2380    def rename_multireddit(self, current_name, new_name, *args, **kwargs):
2381        """Rename a Multireddit.
2382
2383        :param current_name: The name of the multireddit to rename
2384        :param new_name: The new name to assign to this multireddit
2385
2386        The additional parameters are passed directly into
2387        :meth:`~praw.__init__.BaseReddit.request_json`
2388
2389        """
2390        current_path = self.MULTI_PATH.format(self.user.name, current_name)
2391        new_path = self.MULTI_PATH.format(self.user.name, new_name)
2392        data = {'from': current_path,
2393                'to': new_path}
2394        return self.request_json(self.config['multireddit_rename'], data=data,
2395                                 *args, **kwargs)
2396
2397
2398class MySubredditsMixin(AuthenticatedReddit):
2399    """Adds methods requiring the 'mysubreddits' scope (or login).
2400
2401    You should **not** directly instantiate instances of this class. Use
2402    :class:`.Reddit` instead.
2403
2404    """
2405
2406    @decorators.restrict_access(scope='mysubreddits')
2407    def get_my_contributions(self, *args, **kwargs):
2408        """Return a get_content generator of subreddits.
2409
2410        The Subreddits generated are those where the session's user is a
2411        contributor.
2412
2413        The additional parameters are passed directly into
2414        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
2415
2416        """
2417        return self.get_content(self.config['my_con_subreddits'], *args,
2418                                **kwargs)
2419
2420    @decorators.restrict_access(scope='mysubreddits')
2421    def get_my_moderation(self, *args, **kwargs):
2422        """Return a get_content generator of subreddits.
2423
2424        The Subreddits generated are those where the session's user is a
2425        moderator.
2426
2427        The additional parameters are passed directly into
2428        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
2429
2430        """
2431        return self.get_content(self.config['my_mod_subreddits'], *args,
2432                                **kwargs)
2433
2434    @decorators.restrict_access(scope='mysubreddits')
2435    def get_my_multireddits(self):
2436        """Return a list of the authenticated Redditor's Multireddits."""
2437        # The JSON data for multireddits is returned from Reddit as a list
2438        # Therefore, we cannot use :meth:`get_content` to retrieve the objects
2439        return self.request_json(self.config['my_multis'])
2440
2441    @decorators.restrict_access(scope='mysubreddits')
2442    def get_my_subreddits(self, *args, **kwargs):
2443        """Return a get_content generator of subreddits.
2444
2445        The subreddits generated are those that hat the session's user is
2446        subscribed to.
2447
2448        The additional parameters are passed directly into
2449        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
2450
2451        """
2452        return self.get_content(self.config['my_subreddits'], *args, **kwargs)
2453
2454
2455class PrivateMessagesMixin(AuthenticatedReddit):
2456    """Adds methods requiring the 'privatemessages' scope (or login).
2457
2458    You should **not** directly instantiate instances of this class. Use
2459    :class:`.Reddit` instead.
2460
2461    """
2462
2463    @decorators.restrict_access(scope='privatemessages')
2464    def _mark_as_read(self, thing_ids, unread=False):
2465        """Mark each of the supplied thing_ids as (un)read.
2466
2467        :returns: The json response from the server.
2468
2469        """
2470        data = {'id': ','.join(thing_ids)}
2471        key = 'unread_message' if unread else 'read_message'
2472        response = self.request_json(self.config[key], data=data)
2473        self.evict([self.config[x] for x in ['inbox', 'messages',
2474                                             'mod_mail', 'unread']])
2475        return response
2476
2477    @decorators.restrict_access(scope='privatemessages')
2478    def get_comment_replies(self, *args, **kwargs):
2479        """Return a get_content generator for inboxed comment replies.
2480
2481        The additional parameters are passed directly into
2482        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
2483
2484        """
2485        return self.get_content(self.config['comment_replies'],
2486                                *args, **kwargs)
2487
2488    @decorators.restrict_access(scope='privatemessages')
2489    def get_inbox(self, *args, **kwargs):
2490        """Return a get_content generator for inbox (messages and comments).
2491
2492        The additional parameters are passed directly into
2493        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
2494
2495        """
2496        return self.get_content(self.config['inbox'], *args, **kwargs)
2497
2498    def get_message(self, message_id, *args, **kwargs):
2499        """Return a Message object corresponding to the given ID.
2500
2501        :param message_id: The ID or Fullname for a Message
2502
2503        The additional parameters are passed directly into
2504        :meth:`~praw.objects.Message.from_id` of Message, and subsequently into
2505        :meth:`.request_json`.
2506
2507        """
2508        return objects.Message.from_id(self, message_id, *args, **kwargs)
2509
2510    @decorators.restrict_access(scope='privatemessages')
2511    def get_messages(self, *args, **kwargs):
2512        """Return a get_content generator for inbox (messages only).
2513
2514        The additional parameters are passed directly into
2515        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
2516
2517        """
2518        return self.get_content(self.config['messages'], *args, **kwargs)
2519
2520    @decorators.restrict_access(scope='privatemessages')
2521    def get_post_replies(self, *args, **kwargs):
2522        """Return a get_content generator for inboxed submission replies.
2523
2524        The additional parameters are passed directly into
2525        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
2526
2527        """
2528        return self.get_content(self.config['post_replies'], *args, **kwargs)
2529
2530    @decorators.restrict_access(scope='privatemessages')
2531    def get_sent(self, *args, **kwargs):
2532        """Return a get_content generator for sent messages.
2533
2534        The additional parameters are passed directly into
2535        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
2536
2537        """
2538        return self.get_content(self.config['sent'], *args, **kwargs)
2539
2540    @decorators.restrict_access(scope='privatemessages')
2541    def get_unread(self, unset_has_mail=False, update_user=False, *args,
2542                   **kwargs):
2543        """Return a get_content generator for unread messages.
2544
2545        :param unset_has_mail: When True, clear the has_mail flag (orangered)
2546            for the user.
2547        :param update_user: If both `unset_has_mail` and `update user` is True,
2548            set the `has_mail` attribute of the logged-in user to False.
2549
2550        The additional parameters are passed directly into
2551        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
2552
2553        """
2554        params = kwargs.setdefault('params', {})
2555        if unset_has_mail:
2556            params['mark'] = 'true'
2557            if update_user:  # Update the user object
2558                # Use setattr to avoid pylint error
2559                setattr(self.user, 'has_mail', False)
2560        return self.get_content(self.config['unread'], *args, **kwargs)
2561
2562    @decorators.restrict_access(scope='privatemessages')
2563    def get_mentions(self, *args, **kwargs):
2564        """Return a get_content generator for username mentions.
2565
2566        The additional parameters are passed directly into
2567        :meth:`.get_content`. Note: the `url` parameter cannot be altered.
2568
2569        """
2570        return self.get_content(self.config['mentions'], *args, **kwargs)
2571
2572    @decorators.restrict_access(scope='privatemessages')
2573    @decorators.require_captcha
2574    def send_message(self, recipient, subject, message, from_sr=None,
2575                     captcha=None, **kwargs):
2576        """Send a message to a redditor or a subreddit's moderators (mod mail).
2577
2578        :param recipient: A Redditor or Subreddit instance to send a message
2579            to. A string can also be used in which case the string is treated
2580            as a redditor unless it is prefixed with either '/r/' or '#', in
2581            which case it will be treated as a subreddit.
2582        :param subject: The subject of the message to send.
2583        :param message: The actual message content.
2584        :param from_sr: A Subreddit instance or string to send the message
2585            from. When provided, messages are sent from the subreddit rather
2586            than from the authenticated user. Note that the authenticated user
2587            must be a moderator of the subreddit and have mail permissions.
2588
2589        :returns: The json response from the server.
2590
2591        This function may result in a captcha challenge. PRAW will
2592        automatically prompt you for a response. See :ref:`handling-captchas`
2593        if you want to manually handle captchas.
2594
2595        """
2596        if isinstance(recipient, objects.Subreddit):
2597            recipient = '/r/{0}'.format(six.text_type(recipient))
2598        else:
2599            recipient = six.text_type(recipient)
2600
2601        data = {'text': message,
2602                'subject': subject,
2603                'to': recipient}
2604        if from_sr:
2605            data['from_sr'] = six.text_type(from_sr)
2606        if captcha:
2607            data.update(captcha)
2608        response = self.request_json(self.config['compose'], data=data,
2609                                     retry_on_error=False)
2610        self.evict(self.config['sent'])
2611        return response
2612
2613
2614class ReportMixin(AuthenticatedReddit):
2615    """Adds methods requiring the 'report' scope (or login).
2616
2617    You should **not** directly instantiate instances of this class. Use
2618    :class:`.Reddit` instead.
2619
2620    """
2621
2622    @decorators.restrict_access(scope='report')
2623    def hide(self, thing_id, _unhide=False):
2624        """Hide one or multiple objects in the context of the logged in user.
2625
2626        :param thing_id: A single fullname or list of fullnames,
2627            representing objects which will be hidden.
2628        :param _unhide: If True, unhide the object(s) instead. Use
2629            :meth:`~praw.__init__.ReportMixin.unhide` rather than setting this
2630            manually.
2631
2632        :returns: The json response from the server.
2633
2634        """
2635        if isinstance(thing_id, six.string_types):
2636            thing_id = [thing_id]
2637        else:
2638            # Guarantee a subscriptable type.
2639            thing_id = list(thing_id)
2640
2641        if len(thing_id) == 0:
2642            raise ValueError('No fullnames provided')
2643
2644        # Will we return a list of server responses, or just one?
2645        # TODO: In future versions, change the threshold to 1 to get
2646        # list-in-list-out, single-in-single-out behavior. Threshold of 50
2647        # is to avoid a breaking change at this time.
2648        return_list = len(thing_id) > 50
2649
2650        id_chunks = chunk_sequence(thing_id, 50)
2651        responses = []
2652        for id_chunk in id_chunks:
2653            id_chunk = ','.join(id_chunk)
2654
2655            method = 'unhide' if _unhide else 'hide'
2656            data = {'id': id_chunk,
2657                    'executed': method}
2658
2659            response = self.request_json(self.config[method], data=data)
2660            responses.append(response)
2661
2662            if self.user is not None:
2663                self.evict(urljoin(self.user._url,  # pylint: disable=W0212
2664                                   'hidden'))
2665        if return_list:
2666            return responses
2667        else:
2668            return responses[0]
2669
2670    def unhide(self, thing_id):
2671        """Unhide up to 50 objects in the context of the logged in user.
2672
2673        :param thing_id: A single fullname or list of fullnames,
2674            representing objects which will be unhidden.
2675
2676        :returns: The json response from the server.
2677
2678        """
2679        return self.hide(thing_id, _unhide=True)
2680
2681
2682class SubmitMixin(AuthenticatedReddit):
2683    """Adds methods requiring the 'submit' scope (or login).
2684
2685    You should **not** directly instantiate instances of this class. Use
2686    :class:`.Reddit` instead.
2687
2688    """
2689
2690    def _add_comment(self, thing_id, text):
2691        """Comment on the given thing with the given text.
2692
2693        :returns: A Comment object for the newly created comment.
2694
2695        """
2696        def add_comment_helper(self, thing_id, text):
2697            data = {'thing_id': thing_id,
2698                    'text': text}
2699            retval = self.request_json(self.config['comment'], data=data,
2700                                       retry_on_error=False)
2701            return retval
2702
2703        if thing_id.startswith(self.config.by_object[objects.Message]):
2704            decorator = decorators.restrict_access(scope='privatemessages')
2705        else:
2706            decorator = decorators.restrict_access(scope='submit')
2707        retval = decorator(add_comment_helper)(self, thing_id, text)
2708        # REDDIT: reddit's end should only ever return a single comment
2709        return retval['data']['things'][0]
2710
2711    @decorators.restrict_access(scope='submit')
2712    @decorators.require_captcha
2713    def submit(self, subreddit, title, text=None, url=None, captcha=None,
2714               save=None, send_replies=None, resubmit=None, **kwargs):
2715        """Submit a new link to the given subreddit.
2716
2717        Accepts either a Subreddit object or a str containing the subreddit's
2718        display name.
2719
2720        :param resubmit: If True, submit the link even if it has already been
2721            submitted.
2722        :param save: If True the new Submission will be saved after creation.
2723        :param send_replies: If True, inbox replies will be received when
2724            people comment on the submission. If set to None, the default of
2725            True for text posts and False for link posts will be used.
2726
2727        :returns: The newly created Submission object if the reddit instance
2728            can access it. Otherwise, return the url to the submission.
2729
2730        This function may result in a captcha challenge. PRAW will
2731        automatically prompt you for a response. See :ref:`handling-captchas`
2732        if you want to manually handle captchas.
2733
2734        """
2735        if isinstance(text, six.string_types) == bool(url):
2736            raise TypeError('One (and only one) of text or url is required!')
2737        data = {'sr': six.text_type(subreddit),
2738                'title': title}
2739        if text or text == '':
2740            data['kind'] = 'self'
2741            data['text'] = text
2742        else:
2743            data['kind'] = 'link'
2744            data['url'] = url
2745        if captcha:
2746            data.update(captcha)
2747        if resubmit is not None:
2748            data['resubmit'] = resubmit
2749        if save is not None:
2750            data['save'] = save
2751        if send_replies is not None:
2752            data['sendreplies'] = send_replies
2753        result = self.request_json(self.config['submit'], data=data,
2754                                   retry_on_error=False)
2755        url = result['data']['url']
2756        # Clear the OAuth setting when attempting to fetch the submission
2757        if self._use_oauth:
2758            self._use_oauth = False
2759            if url.startswith(self.config.oauth_url):
2760                url = self.config.api_url + url[len(self.config.oauth_url):]
2761        try:
2762            return self.get_submission(url)
2763        except errors.Forbidden:
2764            # While the user may be able to submit to a subreddit,
2765            # that does not guarantee they have read access.
2766            return url
2767
2768
2769class SubscribeMixin(AuthenticatedReddit):
2770    """Adds methods requiring the 'subscribe' scope (or login).
2771
2772    You should **not** directly instantiate instances of this class. Use
2773    :class:`.Reddit` instead.
2774
2775    """
2776
2777    @decorators.restrict_access(scope='subscribe')
2778    def subscribe(self, subreddit, unsubscribe=False):
2779        """Subscribe to the given subreddit.
2780
2781        :param subreddit: Either the subreddit name or a subreddit object.
2782        :param unsubscribe: When True, unsubscribe.
2783        :returns: The json response from the server.
2784
2785        """
2786        data = {'action': 'unsub' if unsubscribe else 'sub',
2787                'sr_name': six.text_type(subreddit)}
2788        response = self.request_json(self.config['subscribe'], data=data)
2789        self.evict(self.config['my_subreddits'])
2790        return response
2791
2792    def unsubscribe(self, subreddit):
2793        """Unsubscribe from the given subreddit.
2794
2795        :param subreddit: Either the subreddit name or a subreddit object.
2796        :returns: The json response from the server.
2797
2798        """
2799        return self.subscribe(subreddit, unsubscribe=True)
2800
2801
2802class Reddit(ModConfigMixin, ModFlairMixin, ModLogMixin, ModOnlyMixin,
2803             ModSelfMixin, MultiredditMixin, MySubredditsMixin,
2804             PrivateMessagesMixin, ReportMixin, SubmitMixin, SubscribeMixin):
2805    """Provides access to reddit's API.
2806
2807    See :class:`.BaseReddit`'s documentation for descriptions of the
2808    initialization parameters.
2809
2810    """
2811
2812# Prevent recursive import
2813from . import objects  # NOQA
2814