2004 lines
78 KiB
Python
2004 lines
78 KiB
Python
# This file is part of PRAW.
|
|
#
|
|
# PRAW is free software: you can redistribute it and/or modify it under the
|
|
# terms of the GNU General Public License as published by the Free Software
|
|
# Foundation, either version 3 of the License, or (at your option) any later
|
|
# version.
|
|
#
|
|
# PRAW is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along with
|
|
# PRAW. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
"""
|
|
Contains code about objects such as Submissions, Redditors or Commments.
|
|
|
|
There are two main groups of objects in this file. The first are objects that
|
|
correspond to a Thing or part of a Thing as specified in reddit's API overview,
|
|
https://github.com/reddit/reddit/wiki/API. The second gives functionality that
|
|
extends over multiple Things. An object that extends from Saveable indicates
|
|
that it can be saved and unsaved in the context of a logged in user.
|
|
"""
|
|
|
|
from __future__ import print_function, unicode_literals
|
|
import six
|
|
from six.moves.urllib.parse import ( # pylint: disable=F0401
|
|
parse_qs, urlparse, urlunparse)
|
|
from heapq import heappop, heappush
|
|
from json import dumps
|
|
from requests.compat import urljoin
|
|
from warnings import warn, warn_explicit
|
|
from praw import (AuthenticatedReddit as AR, ModConfigMixin as MCMix,
|
|
ModFlairMixin as MFMix, ModLogMixin as MLMix,
|
|
ModOnlyMixin as MOMix, ModSelfMixin as MSMix,
|
|
MultiredditMixin as MultiMix, PrivateMessagesMixin as PMMix,
|
|
SubmitMixin, SubscribeMixin, UnauthenticatedReddit as UR)
|
|
from praw.decorators import (alias_function, limit_chars, restrict_access,
|
|
deprecated)
|
|
from praw.errors import ClientException
|
|
from praw.internal import (_get_redditor_listing, _get_sorter,
|
|
_modify_relationship)
|
|
|
|
|
|
REDDITOR_KEYS = ('approved_by', 'author', 'banned_by', 'redditor',
|
|
'revision_by')
|
|
|
|
|
|
class RedditContentObject(object):
|
|
"""Base class that represents actual reddit objects."""
|
|
|
|
@classmethod
|
|
def from_api_response(cls, reddit_session, json_dict):
|
|
"""Return an instance of the appropriate class from the json_dict."""
|
|
return cls(reddit_session, json_dict=json_dict)
|
|
|
|
def __init__(self, reddit_session, json_dict=None, fetch=True,
|
|
info_url=None, underscore_names=None, uniq=None):
|
|
"""Create a new object from the dict of attributes returned by the API.
|
|
|
|
The fetch parameter specifies whether to retrieve the object's
|
|
information from the API (only matters when it isn't provided using
|
|
json_dict).
|
|
|
|
"""
|
|
self._info_url = info_url or reddit_session.config['info']
|
|
self.reddit_session = reddit_session
|
|
self._underscore_names = underscore_names
|
|
self._uniq = uniq
|
|
self._has_fetched = self._populate(json_dict, fetch)
|
|
|
|
def __eq__(self, other):
|
|
"""Return whether the other instance equals the current."""
|
|
return (isinstance(other, RedditContentObject) and
|
|
self.fullname == other.fullname)
|
|
|
|
def __hash__(self):
|
|
"""Return the hash of the current instance."""
|
|
return hash(self.fullname)
|
|
|
|
def __getattr__(self, attr):
|
|
"""Return the value of the `attr` attribute."""
|
|
# Because this method may perform web requests, there are certain
|
|
# attributes we must blacklist to prevent accidental requests:
|
|
# __members__, __methods__: Caused by `dir(obj)` in Python 2.
|
|
# __setstate__: Caused by Pickle deserialization.
|
|
blacklist = ('__members__', '__methods__', '__setstate__')
|
|
if attr not in blacklist and not self._has_fetched:
|
|
self._has_fetched = self._populate(None, True)
|
|
return getattr(self, attr)
|
|
msg = '\'{0}\' has no attribute \'{1}\''.format(type(self), attr)
|
|
raise AttributeError(msg)
|
|
|
|
def __getstate__(self):
|
|
"""Needed for `pickle`.
|
|
|
|
Without this, pickle protocol version 0 will make HTTP requests
|
|
upon serialization, hence slowing it down significantly.
|
|
"""
|
|
return self.__dict__
|
|
|
|
def __ne__(self, other):
|
|
"""Return whether the other instance differs from the current."""
|
|
return not self == other
|
|
|
|
def __reduce_ex__(self, _):
|
|
"""Needed for `pickle`.
|
|
|
|
Without this, `pickle` protocol version 2 will make HTTP requests
|
|
upon serialization, hence slowing it down significantly.
|
|
"""
|
|
return self.__reduce__()
|
|
|
|
def __setattr__(self, name, value):
|
|
"""Set the `name` attribute to `value."""
|
|
if value and name == 'subreddit':
|
|
value = Subreddit(self.reddit_session, value, fetch=False)
|
|
elif value and name in REDDITOR_KEYS:
|
|
if isinstance(value, bool):
|
|
pass
|
|
elif isinstance(value, dict):
|
|
value = Redditor(self.reddit_session, json_dict=value['data'])
|
|
elif not value or value == '[deleted]':
|
|
value = None
|
|
else:
|
|
value = Redditor(self.reddit_session, value, fetch=False)
|
|
object.__setattr__(self, name, value)
|
|
|
|
def __str__(self):
|
|
"""Return a string representation of the RedditContentObject."""
|
|
retval = self.__unicode__()
|
|
if not six.PY3:
|
|
retval = retval.encode('utf-8')
|
|
return retval
|
|
|
|
def _get_json_dict(self):
|
|
# (disabled for entire function) pylint: disable=W0212
|
|
|
|
# OAuth handling needs to be special cased here. For instance, the user
|
|
# might be calling a method on a Subreddit object that requires first
|
|
# loading the information about the subreddit. This method should try
|
|
# to obtain the information in a scope-less manner unless either:
|
|
# a) The object is a WikiPage and the reddit_session has the `wikiread`
|
|
# scope.
|
|
# b) The object is not a WikiPage and the reddit_session has the
|
|
# `read` scope.
|
|
prev_use_oauth = self.reddit_session._use_oauth
|
|
|
|
wiki_page = isinstance(self, WikiPage)
|
|
scope = self.reddit_session.has_scope
|
|
|
|
self.reddit_session._use_oauth = wiki_page and scope('wikiread') or \
|
|
not wiki_page and scope('read')
|
|
|
|
try:
|
|
params = {'uniq': self._uniq} if self._uniq else {}
|
|
response = self.reddit_session.request_json(
|
|
self._info_url, params=params, as_objects=False)
|
|
finally:
|
|
self.reddit_session._use_oauth = prev_use_oauth
|
|
return response['data']
|
|
|
|
def _populate(self, json_dict, fetch):
|
|
if json_dict is None:
|
|
json_dict = self._get_json_dict() if fetch else {}
|
|
|
|
if self.reddit_session.config.store_json_result is True:
|
|
self.json_dict = json_dict
|
|
else:
|
|
self.json_dict = None
|
|
|
|
# TODO: Remove this wikipagelisting hack
|
|
if isinstance(json_dict, list):
|
|
json_dict = {'_tmp': json_dict}
|
|
|
|
for name, value in six.iteritems(json_dict):
|
|
if self._underscore_names and name in self._underscore_names:
|
|
name = '_' + name
|
|
setattr(self, name, value)
|
|
|
|
self._post_populate(fetch)
|
|
return bool(json_dict) or fetch
|
|
|
|
def _post_populate(self, fetch):
|
|
"""Called after populating the attributes of the instance."""
|
|
|
|
@property
|
|
def fullname(self):
|
|
"""Return the object's fullname.
|
|
|
|
A fullname is an object's kind mapping like `t3` followed by an
|
|
underscore and the object's base36 id, e.g., `t1_c5s96e0`.
|
|
|
|
"""
|
|
by_object = self.reddit_session.config.by_object
|
|
return '{0}_{1}'.format(by_object[self.__class__], self.id)
|
|
|
|
@property
|
|
@deprecated('``has_fetched`` will not be a public attribute in PRAW4.')
|
|
def has_fetched(self):
|
|
"""Return whether the object has been fully fetched from reddit."""
|
|
return self._has_fetched
|
|
|
|
|
|
class Moderatable(RedditContentObject):
|
|
"""Interface for Reddit content objects that have can be moderated."""
|
|
|
|
@restrict_access(scope='modposts')
|
|
def approve(self):
|
|
"""Approve object.
|
|
|
|
This reverts a removal, resets the report counter, marks it with a
|
|
green check mark (only visible to other moderators) on the website view
|
|
and sets the approved_by attribute to the logged in user.
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
url = self.reddit_session.config['approve']
|
|
data = {'id': self.fullname}
|
|
response = self.reddit_session.request_json(url, data=data)
|
|
urls = [self.reddit_session.config[x] for x in ['modqueue', 'spam']]
|
|
if isinstance(self, Submission):
|
|
urls += self.subreddit._listing_urls # pylint: disable=W0212
|
|
self.reddit_session.evict(urls)
|
|
return response
|
|
|
|
@restrict_access(scope='modposts')
|
|
def distinguish(self, as_made_by='mod', sticky=False):
|
|
"""Distinguish object as made by mod, admin or special.
|
|
|
|
Distinguished objects have a different author color. With Reddit
|
|
Enhancement Suite it is the background color that changes.
|
|
|
|
`sticky` argument only used for top-level Comments.
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
url = self.reddit_session.config['distinguish']
|
|
data = {'id': self.fullname,
|
|
'how': 'yes' if as_made_by == 'mod' else as_made_by}
|
|
if isinstance(self, Comment) and self.is_root:
|
|
data['sticky'] = sticky
|
|
return self.reddit_session.request_json(url, data=data)
|
|
|
|
@restrict_access(scope='modposts')
|
|
def ignore_reports(self):
|
|
"""Ignore future reports on this object.
|
|
|
|
This prevents future reports from causing notifications or appearing
|
|
in the various moderation listing. The report count will still
|
|
increment.
|
|
|
|
"""
|
|
url = self.reddit_session.config['ignore_reports']
|
|
data = {'id': self.fullname}
|
|
return self.reddit_session.request_json(url, data=data)
|
|
|
|
@restrict_access(scope='modposts')
|
|
def remove(self, spam=False):
|
|
"""Remove object. This is the moderator version of delete.
|
|
|
|
The object is removed from the subreddit listings and placed into the
|
|
spam listing. If spam is set to True, then the automatic spam filter
|
|
will try to remove objects with similar attributes in the future.
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
url = self.reddit_session.config['remove']
|
|
data = {'id': self.fullname,
|
|
'spam': 'True' if spam else 'False'}
|
|
response = self.reddit_session.request_json(url, data=data)
|
|
urls = [self.reddit_session.config[x] for x in ['modqueue', 'spam']]
|
|
if isinstance(self, Submission) and hasattr(self, 'subreddit'):
|
|
urls += self.subreddit._listing_urls # pylint: disable=W0212
|
|
self.reddit_session.evict(urls)
|
|
return response
|
|
|
|
def undistinguish(self):
|
|
"""Remove mod, admin or special distinguishing on object.
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
return self.distinguish(as_made_by='no')
|
|
|
|
@restrict_access(scope='modposts')
|
|
def unignore_reports(self):
|
|
"""Remove ignoring of future reports on this object.
|
|
|
|
Undoes 'ignore_reports'. Future reports will now cause notifications
|
|
and appear in the various moderation listings.
|
|
|
|
"""
|
|
url = self.reddit_session.config['unignore_reports']
|
|
data = {'id': self.fullname}
|
|
return self.reddit_session.request_json(url, data=data)
|
|
|
|
|
|
class Editable(RedditContentObject):
|
|
"""Interface for Reddit content objects that can be edited and deleted."""
|
|
|
|
@restrict_access(scope='edit')
|
|
def delete(self):
|
|
"""Delete this object.
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
url = self.reddit_session.config['del']
|
|
data = {'id': self.fullname}
|
|
response = self.reddit_session.request_json(url, data=data)
|
|
self.reddit_session.evict(self.reddit_session.config['user'])
|
|
return response
|
|
|
|
@restrict_access(scope='edit')
|
|
def edit(self, text):
|
|
"""Replace the body of the object with `text`.
|
|
|
|
:returns: The updated object.
|
|
|
|
"""
|
|
url = self.reddit_session.config['edit']
|
|
data = {'thing_id': self.fullname,
|
|
'text': text}
|
|
response = self.reddit_session.request_json(url, data=data)
|
|
self.reddit_session.evict(self.reddit_session.config['user'])
|
|
return response['data']['things'][0]
|
|
|
|
|
|
class Gildable(RedditContentObject):
|
|
"""Interface for RedditContentObjects that can be gilded."""
|
|
|
|
@restrict_access(scope='creddits', oauth_only=True)
|
|
def gild(self, months=None):
|
|
"""Gild the Redditor or author of the content.
|
|
|
|
:param months: Specifies the number of months to gild. This parameter
|
|
is Only valid when the instance called upon is of type
|
|
Redditor. When not provided, the value defaults to 1.
|
|
:returns: True on success, otherwise raises an exception.
|
|
|
|
"""
|
|
if isinstance(self, Redditor):
|
|
months = int(months) if months is not None else 1
|
|
if months < 1:
|
|
raise TypeError('months must be at least 1')
|
|
if months > 36:
|
|
raise TypeError('months must be no more than 36')
|
|
response = self.reddit_session.request(
|
|
self.reddit_session.config['gild_user'].format(
|
|
username=six.text_type(self)), data={'months': months})
|
|
elif months is not None:
|
|
raise TypeError('months is not a valid parameter for {0}'
|
|
.format(type(self)))
|
|
else:
|
|
response = self.reddit_session.request(
|
|
self.reddit_session.config['gild_thing']
|
|
.format(fullname=self.fullname), data=True)
|
|
return response.status_code == 200
|
|
|
|
|
|
class Hideable(RedditContentObject):
|
|
"""Interface for objects that can be hidden."""
|
|
|
|
def hide(self, _unhide=False):
|
|
"""Hide object in the context of the logged in user.
|
|
|
|
:param _unhide: If True, unhide the item instead. Use
|
|
:meth:`~praw.objects.Hideable.unhide` instead of setting this
|
|
manually.
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
return self.reddit_session.hide(self.fullname, _unhide=_unhide)
|
|
|
|
def unhide(self):
|
|
"""Unhide object in the context of the logged in user.
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
return self.hide(_unhide=True)
|
|
|
|
|
|
class Inboxable(RedditContentObject):
|
|
"""Interface for objects that appear in the inbox (orangereds)."""
|
|
|
|
def mark_as_read(self):
|
|
"""Mark object as read.
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
return self.reddit_session._mark_as_read([self.fullname])
|
|
|
|
def mark_as_unread(self):
|
|
"""Mark object as unread.
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
return self.reddit_session._mark_as_read([self.fullname], unread=True)
|
|
|
|
def reply(self, text):
|
|
"""Reply to object with the specified text.
|
|
|
|
:returns: A Comment object for the newly created comment (reply).
|
|
|
|
"""
|
|
# pylint: disable=W0212
|
|
response = self.reddit_session._add_comment(self.fullname, text)
|
|
# pylint: enable=W0212
|
|
urls = [self.reddit_session.config['inbox']]
|
|
if isinstance(self, Comment):
|
|
urls.append(self.submission._api_link) # pylint: disable=W0212
|
|
elif isinstance(self, Message):
|
|
urls.append(self.reddit_session.config['sent'])
|
|
self.reddit_session.evict(urls)
|
|
return response
|
|
|
|
|
|
class Messageable(RedditContentObject):
|
|
"""Interface for RedditContentObjects that can be messaged."""
|
|
|
|
_methods = (('send_message', PMMix),)
|
|
|
|
|
|
class Refreshable(RedditContentObject):
|
|
"""Interface for objects that can be refreshed."""
|
|
|
|
def refresh(self):
|
|
"""Re-query to update object with latest values. Return the object.
|
|
|
|
Any listing, such as the submissions on a subreddits top page, will
|
|
automatically be refreshed serverside. Refreshing a submission will
|
|
also refresh all its comments.
|
|
|
|
In the rare case of a comment being deleted or removed when it had
|
|
no replies, a second request will be made, not all information will
|
|
be updated and a warning will list the attributes that could not be
|
|
retrieved if there were any.
|
|
|
|
"""
|
|
unique = self.reddit_session._unique_count # pylint: disable=W0212
|
|
self.reddit_session._unique_count += 1 # pylint: disable=W0212
|
|
|
|
if isinstance(self, Redditor):
|
|
other = Redditor(self.reddit_session, self._case_name, fetch=True,
|
|
uniq=unique)
|
|
elif isinstance(self, Comment):
|
|
sub = Submission.from_url(self.reddit_session, self.permalink,
|
|
params={'uniq': unique})
|
|
if sub.comments:
|
|
other = sub.comments[0]
|
|
else:
|
|
# comment is "specially deleted", a reddit inconsistency;
|
|
# see #519, #524, #535, #537, and #552 it needs to be
|
|
# retreived via /api/info, but that's okay since these
|
|
# specially deleted comments always have the same json
|
|
# structure. The unique count needs to be updated
|
|
# in case the comment originally came from /api/info
|
|
msg = ("Comment {0} was deleted or removed, and had "
|
|
"no replies when such happened, so a second "
|
|
"request was made to /api/info.".format(self.name))
|
|
unique = self.reddit_session._unique_count
|
|
self.reddit_session._unique_count += 1
|
|
other = self.reddit_session.get_info(thing_id=self.name,
|
|
params={'uniq': unique})
|
|
oldkeys = set(self.__dict__.keys())
|
|
newkeys = set(other.__dict__.keys())
|
|
keydiff = ", ".join(oldkeys - newkeys)
|
|
if keydiff:
|
|
msg += "\nCould not retrieve:\n{0}".format(keydiff)
|
|
self.__dict__.update(other.__dict__) # pylint: disable=W0201
|
|
warn(msg, RuntimeWarning)
|
|
return self
|
|
elif isinstance(self, Multireddit):
|
|
other = Multireddit(self.reddit_session, author=self._author,
|
|
name=self.name, uniq=unique, fetch=True)
|
|
elif isinstance(self, Submission):
|
|
params = self._params.copy()
|
|
params['uniq'] = unique
|
|
other = Submission.from_url(self.reddit_session, self.permalink,
|
|
comment_sort=self._comment_sort,
|
|
params=params)
|
|
elif isinstance(self, Subreddit):
|
|
other = Subreddit(self.reddit_session, self._case_name, fetch=True,
|
|
uniq=unique)
|
|
elif isinstance(self, WikiPage):
|
|
other = WikiPage(self.reddit_session,
|
|
six.text_type(self.subreddit), self.page,
|
|
fetch=True, uniq=unique)
|
|
|
|
self.__dict__ = other.__dict__ # pylint: disable=W0201
|
|
return self
|
|
|
|
|
|
class Reportable(RedditContentObject):
|
|
"""Interface for RedditContentObjects that can be reported."""
|
|
|
|
@restrict_access(scope='report')
|
|
def report(self, reason=None):
|
|
"""Report this object to the moderators.
|
|
|
|
:param reason: The user-supplied reason for reporting a comment
|
|
or submission. Default: None (blank reason)
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
url = self.reddit_session.config['report']
|
|
data = {'id': self.fullname}
|
|
if reason:
|
|
data['reason'] = reason
|
|
response = self.reddit_session.request_json(url, data=data)
|
|
# Reported objects are automatically hidden as well
|
|
# pylint: disable=W0212
|
|
self.reddit_session.evict(
|
|
[self.reddit_session.config['user'],
|
|
urljoin(self.reddit_session.user._url, 'hidden')])
|
|
# pylint: enable=W0212
|
|
return response
|
|
|
|
|
|
class Saveable(RedditContentObject):
|
|
"""Interface for RedditContentObjects that can be saved."""
|
|
|
|
@restrict_access(scope='save')
|
|
def save(self, unsave=False):
|
|
"""Save the object.
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
url = self.reddit_session.config['unsave' if unsave else 'save']
|
|
data = {'id': self.fullname,
|
|
'executed': 'unsaved' if unsave else 'saved'}
|
|
response = self.reddit_session.request_json(url, data=data)
|
|
self.reddit_session.evict(self.reddit_session.config['saved'])
|
|
return response
|
|
|
|
def unsave(self):
|
|
"""Unsave the object.
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
return self.save(unsave=True)
|
|
|
|
|
|
class Voteable(RedditContentObject):
|
|
"""Interface for RedditContentObjects that can be voted on."""
|
|
|
|
def clear_vote(self):
|
|
"""Remove the logged in user's vote on the object.
|
|
|
|
Running this on an object with no existing vote has no adverse effects.
|
|
|
|
Note: votes must be cast by humans. That is, API clients proxying a
|
|
human's action one-for-one are OK, but bots deciding how to vote on
|
|
content or amplifying a human's vote are not. See the reddit rules for
|
|
more details on what constitutes vote cheating.
|
|
|
|
Source for note: http://www.reddit.com/dev/api#POST_api_vote
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
return self.vote()
|
|
|
|
def downvote(self):
|
|
"""Downvote object. If there already is a vote, replace it.
|
|
|
|
Note: votes must be cast by humans. That is, API clients proxying a
|
|
human's action one-for-one are OK, but bots deciding how to vote on
|
|
content or amplifying a human's vote are not. See the reddit rules for
|
|
more details on what constitutes vote cheating.
|
|
|
|
Source for note: http://www.reddit.com/dev/api#POST_api_vote
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
return self.vote(direction=-1)
|
|
|
|
def upvote(self):
|
|
"""Upvote object. If there already is a vote, replace it.
|
|
|
|
Note: votes must be cast by humans. That is, API clients proxying a
|
|
human's action one-for-one are OK, but bots deciding how to vote on
|
|
content or amplifying a human's vote are not. See the reddit rules for
|
|
more details on what constitutes vote cheating.
|
|
|
|
Source for note: http://www.reddit.com/dev/api#POST_api_vote
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
return self.vote(direction=1)
|
|
|
|
@restrict_access(scope='vote')
|
|
def vote(self, direction=0):
|
|
"""Vote for the given item in the direction specified.
|
|
|
|
Note: votes must be cast by humans. That is, API clients proxying a
|
|
human's action one-for-one are OK, but bots deciding how to vote on
|
|
content or amplifying a human's vote are not. See the reddit rules for
|
|
more details on what constitutes vote cheating.
|
|
|
|
Source for note: http://www.reddit.com/dev/api#POST_api_vote
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
url = self.reddit_session.config['vote']
|
|
data = {'id': self.fullname,
|
|
'dir': six.text_type(direction)}
|
|
if self.reddit_session.user:
|
|
# pylint: disable=W0212
|
|
urls = [urljoin(self.reddit_session.user._url, 'disliked'),
|
|
urljoin(self.reddit_session.user._url, 'liked')]
|
|
# pylint: enable=W0212
|
|
self.reddit_session.evict(urls)
|
|
return self.reddit_session.request_json(url, data=data)
|
|
|
|
|
|
class Comment(Editable, Gildable, Inboxable, Moderatable, Refreshable,
|
|
Reportable, Saveable, Voteable):
|
|
"""A class that represents a reddit comments."""
|
|
|
|
def __init__(self, reddit_session, json_dict):
|
|
"""Construct an instance of the Comment object."""
|
|
super(Comment, self).__init__(reddit_session, json_dict,
|
|
underscore_names=['replies'])
|
|
self._has_fetched_replies = not hasattr(self, 'was_comment')
|
|
if self._replies:
|
|
self._replies = self._replies['data']['children']
|
|
elif self._replies == '': # Comment tree was built and there are none
|
|
self._replies = []
|
|
else:
|
|
self._replies = None
|
|
self._submission = None
|
|
|
|
@limit_chars
|
|
def __unicode__(self):
|
|
"""Return a string representation of the comment."""
|
|
return getattr(self, 'body', '[Unloaded Comment]')
|
|
|
|
@property
|
|
def _fast_permalink(self):
|
|
"""Return the short permalink to the comment."""
|
|
if hasattr(self, 'link_id'): # from /r or /u comments page
|
|
sid = self.link_id.split('_')[1]
|
|
else: # from user's /message page
|
|
sid = self.context.split('/')[4]
|
|
return urljoin(self.reddit_session.config['comments'], '{0}/_/{1}'
|
|
.format(sid, self.id))
|
|
|
|
def _update_submission(self, submission):
|
|
"""Submission isn't set on __init__ thus we need to update it."""
|
|
submission._comments_by_id[self.name] = self # pylint: disable=W0212
|
|
self._submission = submission
|
|
if self._replies:
|
|
for reply in self._replies:
|
|
reply._update_submission(submission) # pylint: disable=W0212
|
|
|
|
@property
|
|
def is_root(self):
|
|
"""Return True when the comment is a top level comment."""
|
|
sub_prefix = self.reddit_session.config.by_object[Submission]
|
|
return self.parent_id.startswith(sub_prefix)
|
|
|
|
@property
|
|
def permalink(self):
|
|
"""Return a permalink to the comment."""
|
|
return urljoin(self.submission.permalink, self.id)
|
|
|
|
@property
|
|
def replies(self):
|
|
"""Return a list of the comment replies to this comment.
|
|
|
|
If the comment is not from a submission, :meth:`replies` will
|
|
always be an empty list unless you call :meth:`refresh()
|
|
before calling :meth:`replies` due to a limitation in
|
|
reddit's API.
|
|
|
|
"""
|
|
if self._replies is None or not self._has_fetched_replies:
|
|
response = self.reddit_session.request_json(self._fast_permalink)
|
|
if response[1]['data']['children']:
|
|
# pylint: disable=W0212
|
|
self._replies = response[1]['data']['children'][0]._replies
|
|
else:
|
|
# comment is "specially deleted", a reddit inconsistency;
|
|
# see #519, #524, #535, #537, and #552 it needs to be
|
|
# retreived via /api/info, but that's okay since these
|
|
# specially deleted comments always have the same json
|
|
# structure.
|
|
msg = ("Comment {0} was deleted or removed, and had "
|
|
"no replies when such happened, so it still "
|
|
"has no replies".format(self.name))
|
|
warn(msg, RuntimeWarning)
|
|
self._replies = []
|
|
# pylint: enable=W0212
|
|
self._has_fetched_replies = True
|
|
# Set the submission object if it is not set.
|
|
if not self._submission:
|
|
self._submission = response[0]['data']['children'][0]
|
|
return self._replies
|
|
|
|
@property
|
|
def submission(self):
|
|
"""Return the Submission object this comment belongs to."""
|
|
if not self._submission: # Comment not from submission
|
|
self._submission = self.reddit_session.get_submission(
|
|
url=self._fast_permalink)
|
|
return self._submission
|
|
|
|
|
|
class Message(Inboxable):
|
|
"""A class for private messages."""
|
|
|
|
@staticmethod
|
|
@restrict_access(scope='privatemessages')
|
|
def from_id(reddit_session, message_id, *args, **kwargs):
|
|
"""Request the url for a Message and return a Message object.
|
|
|
|
:param reddit_session: The session to make the request with.
|
|
:param message_id: The ID of the message to request.
|
|
|
|
The additional parameters are passed directly into
|
|
:meth:`.request_json`.
|
|
|
|
"""
|
|
# Reduce fullname to ID if necessary
|
|
message_id = message_id.split('_', 1)[-1]
|
|
url = reddit_session.config['message'].format(messageid=message_id)
|
|
message_info = reddit_session.request_json(url, *args, **kwargs)
|
|
message = message_info['data']['children'][0]
|
|
|
|
# Messages are received as a listing such that
|
|
# the first item is always the thread's root.
|
|
# The ID requested by the user may be a child.
|
|
if message.id == message_id:
|
|
return message
|
|
for child in message.replies:
|
|
if child.id == message_id:
|
|
return child
|
|
|
|
def __init__(self, reddit_session, json_dict):
|
|
"""Construct an instance of the Message object."""
|
|
super(Message, self).__init__(reddit_session, json_dict)
|
|
if self.replies: # pylint: disable=E0203
|
|
self.replies = self.replies['data']['children']
|
|
else:
|
|
self.replies = []
|
|
|
|
@limit_chars
|
|
def __unicode__(self):
|
|
"""Return a string representation of the Message."""
|
|
return 'From: {0}\nSubject: {1}\n\n{2}'.format(self.author,
|
|
self.subject, self.body)
|
|
|
|
@restrict_access(scope='privatemessages')
|
|
def collapse(self):
|
|
"""Collapse a private message or modmail."""
|
|
url = self.reddit_session.config['collapse_message']
|
|
self.reddit_session.request_json(url, data={'id': self.name})
|
|
|
|
@restrict_access(scope='modcontributors')
|
|
def mute_modmail_author(self, _unmute=False):
|
|
"""Mute the sender of this modmail message.
|
|
|
|
:param _unmute: Unmute the user instead. Please use
|
|
:meth:`unmute_modmail_author` instead of setting this directly.
|
|
|
|
"""
|
|
path = 'unmute_sender' if _unmute else 'mute_sender'
|
|
return self.reddit_session.request_json(
|
|
self.reddit_session.config[path], data={'id': self.fullname})
|
|
|
|
@restrict_access(scope='privatemessages')
|
|
def uncollapse(self):
|
|
"""Uncollapse a private message or modmail."""
|
|
url = self.reddit_session.config['uncollapse_message']
|
|
self.reddit_session.request_json(url, data={'id': self.name})
|
|
|
|
def unmute_modmail_author(self):
|
|
"""Unmute the sender of this modmail message."""
|
|
return self.mute_modmail_author(_unmute=True)
|
|
|
|
|
|
class MoreComments(RedditContentObject):
|
|
"""A class indicating there are more comments."""
|
|
|
|
def __init__(self, reddit_session, json_dict):
|
|
"""Construct an instance of the MoreComment object."""
|
|
super(MoreComments, self).__init__(reddit_session, json_dict)
|
|
self.submission = None
|
|
self._comments = None
|
|
|
|
def __lt__(self, other):
|
|
"""Proide a sort order on the MoreComments object."""
|
|
# To work with heapq a "smaller" item is the one with the most comments
|
|
# We are intentionally making the biggest element the smallest element
|
|
# to turn the min-heap implementation in heapq into a max-heap
|
|
# implementation for Submission.replace_more_comments()
|
|
return self.count > other.count
|
|
|
|
def __unicode__(self):
|
|
"""Return a string representation of the MoreComments object."""
|
|
return '[More Comments: {0}]'.format(self.count)
|
|
|
|
def _continue_comments(self, update):
|
|
assert len(self.children) > 0
|
|
tmp = self.reddit_session.get_submission(urljoin(
|
|
self.submission.permalink, self.parent_id.split('_', 1)[1]))
|
|
assert len(tmp.comments) == 1
|
|
self._comments = tmp.comments[0].replies
|
|
if update:
|
|
for comment in self._comments:
|
|
# pylint: disable=W0212
|
|
comment._update_submission(self.submission)
|
|
# pylint: enable=W0212
|
|
return self._comments
|
|
|
|
def _update_submission(self, submission):
|
|
self.submission = submission
|
|
|
|
def comments(self, update=True):
|
|
"""Fetch and return the comments for a single MoreComments object."""
|
|
if not self._comments:
|
|
if self.count == 0: # Handle 'continue this thread' type
|
|
return self._continue_comments(update)
|
|
# pylint: disable=W0212
|
|
children = [x for x in self.children if 't1_{0}'.format(x)
|
|
not in self.submission._comments_by_id]
|
|
# pylint: enable=W0212
|
|
if not children:
|
|
return None
|
|
data = {'children': ','.join(children),
|
|
'link_id': self.submission.fullname,
|
|
'r': str(self.submission.subreddit)}
|
|
|
|
# pylint: disable=W0212
|
|
if self.submission._comment_sort:
|
|
data['where'] = self.submission._comment_sort
|
|
# pylint: enable=W0212
|
|
url = self.reddit_session.config['morechildren']
|
|
response = self.reddit_session.request_json(url, data=data)
|
|
self._comments = response['data']['things']
|
|
if update:
|
|
for comment in self._comments:
|
|
# pylint: disable=W0212
|
|
comment._update_submission(self.submission)
|
|
# pylint: enable=W0212
|
|
return self._comments
|
|
|
|
|
|
class Redditor(Gildable, Messageable, Refreshable):
|
|
"""A class representing the users of reddit."""
|
|
|
|
_methods = (('get_multireddit', MultiMix), ('get_multireddits', MultiMix))
|
|
|
|
get_comments = _get_redditor_listing('comments')
|
|
get_overview = _get_redditor_listing('')
|
|
get_submitted = _get_redditor_listing('submitted')
|
|
|
|
def __init__(self, reddit_session, user_name=None, json_dict=None,
|
|
fetch=False, **kwargs):
|
|
"""Construct an instance of the Redditor object."""
|
|
if not user_name:
|
|
user_name = json_dict['name']
|
|
info_url = reddit_session.config['user_about'].format(user=user_name)
|
|
# name is set before calling the parent constructor so that the
|
|
# json_dict 'name' attribute (if available) has precedence
|
|
self._case_name = user_name
|
|
super(Redditor, self).__init__(reddit_session, json_dict,
|
|
fetch, info_url, **kwargs)
|
|
self.name = self._case_name
|
|
self._url = reddit_session.config['user'].format(user=self.name)
|
|
self._mod_subs = None
|
|
|
|
def __repr__(self):
|
|
"""Return a code representation of the Redditor."""
|
|
return 'Redditor(user_name=\'{0}\')'.format(self.name)
|
|
|
|
def __unicode__(self):
|
|
"""Return a string representation of the Redditor."""
|
|
return self.name
|
|
|
|
def _post_populate(self, fetch):
|
|
if fetch:
|
|
# Maintain a consistent `name` until the user
|
|
# explicitly calls `redditor.refresh()`
|
|
tmp = self._case_name
|
|
self._case_name = self.name
|
|
self.name = tmp
|
|
|
|
@restrict_access(scope='subscribe')
|
|
def friend(self, note=None, _unfriend=False):
|
|
"""Friend the user.
|
|
|
|
:param note: A personal note about the user. Requires reddit Gold.
|
|
:param _unfriend: Unfriend the user. Please use :meth:`unfriend`
|
|
instead of setting this parameter manually.
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
self.reddit_session.evict(self.reddit_session.config['friends'])
|
|
|
|
# Requests through password auth use /api/friend
|
|
# Requests through oauth use /api/v1/me/friends/{username}
|
|
if not self.reddit_session.is_oauth_session():
|
|
modifier = _modify_relationship('friend', unlink=_unfriend)
|
|
data = {'note': note} if note else {}
|
|
return modifier(self.reddit_session.user, self, **data)
|
|
|
|
url = self.reddit_session.config['friend_v1'].format(user=self.name)
|
|
# This endpoint wants the data to be a string instead of an actual
|
|
# dictionary, although it is not required to have any content for adds.
|
|
# Unfriending does require the 'id' key.
|
|
if _unfriend:
|
|
data = {'id': self.name}
|
|
else:
|
|
# We cannot send a null or empty note string.
|
|
data = {'note': note} if note else {}
|
|
data = dumps(data)
|
|
method = 'DELETE' if _unfriend else 'PUT'
|
|
return self.reddit_session.request_json(url, data=data, method=method)
|
|
|
|
def get_disliked(self, *args, **kwargs):
|
|
"""Return a listing of the Submissions the user has downvoted.
|
|
|
|
This method points to :meth:`get_downvoted`, as the "disliked" name
|
|
is being phased out.
|
|
"""
|
|
return self.get_downvoted(*args, **kwargs)
|
|
|
|
def get_downvoted(self, *args, **kwargs):
|
|
"""Return a listing of the Submissions the user has downvoted.
|
|
|
|
:returns: get_content generator of Submission items.
|
|
|
|
The additional parameters are passed directly into
|
|
:meth:`.get_content`. Note: the `url` parameter cannot be altered.
|
|
|
|
As a default, this listing is only accessible by the user. Thereby
|
|
requiring either user/pswd authentication or OAuth authentication with
|
|
the 'history' scope. Users may choose to make their voting record
|
|
public by changing a user preference. In this case, no authentication
|
|
will be needed to access this listing.
|
|
|
|
"""
|
|
# Sending an OAuth authenticated request for a redditor, who isn't the
|
|
# authenticated user. But who has a public voting record will be
|
|
# successful.
|
|
kwargs['_use_oauth'] = self.reddit_session.is_oauth_session()
|
|
return _get_redditor_listing('downvoted')(self, *args, **kwargs)
|
|
|
|
@restrict_access(scope='mysubreddits')
|
|
def get_friend_info(self):
|
|
"""Return information about this friend, including personal notes.
|
|
|
|
The personal note can be added or overwritten with :meth:friend, but
|
|
only if the user has reddit Gold.
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
url = self.reddit_session.config['friend_v1'].format(user=self.name)
|
|
data = {'id': self.name}
|
|
return self.reddit_session.request_json(url, data=data, method='GET')
|
|
|
|
def get_liked(self, *args, **kwargs):
|
|
"""Return a listing of the Submissions the user has upvoted.
|
|
|
|
This method points to :meth:`get_upvoted`, as the "liked" name
|
|
is being phased out.
|
|
"""
|
|
return self.get_upvoted(*args, **kwargs)
|
|
|
|
def get_upvoted(self, *args, **kwargs):
|
|
"""Return a listing of the Submissions the user has upvoted.
|
|
|
|
:returns: get_content generator of Submission items.
|
|
|
|
The additional parameters are passed directly into
|
|
:meth:`.get_content`. Note: the `url` parameter cannot be altered.
|
|
|
|
As a default, this listing is only accessible by the user. Thereby
|
|
requirering either user/pswd authentication or OAuth authentication
|
|
with the 'history' scope. Users may choose to make their voting record
|
|
public by changing a user preference. In this case, no authentication
|
|
will be needed to access this listing.
|
|
|
|
"""
|
|
kwargs['_use_oauth'] = self.reddit_session.is_oauth_session()
|
|
return _get_redditor_listing('upvoted')(self, *args, **kwargs)
|
|
|
|
def mark_as_read(self, messages, unread=False):
|
|
"""Mark message(s) as read or unread.
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
ids = []
|
|
if isinstance(messages, Inboxable):
|
|
ids.append(messages.fullname)
|
|
elif hasattr(messages, '__iter__'):
|
|
for msg in messages:
|
|
if not isinstance(msg, Inboxable):
|
|
msg = 'Invalid message type: {0}'.format(type(msg))
|
|
raise ClientException(msg)
|
|
ids.append(msg.fullname)
|
|
else:
|
|
msg = 'Invalid message type: {0}'.format(type(messages))
|
|
raise ClientException(msg)
|
|
# pylint: disable=W0212
|
|
retval = self.reddit_session._mark_as_read(ids, unread=unread)
|
|
# pylint: enable=W0212
|
|
return retval
|
|
|
|
def unfriend(self):
|
|
"""Unfriend the user.
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
return self.friend(_unfriend=True)
|
|
|
|
|
|
class LoggedInRedditor(Redditor):
|
|
"""A class representing a currently logged in Redditor."""
|
|
|
|
get_hidden = restrict_access('history')(_get_redditor_listing('hidden'))
|
|
get_saved = restrict_access('history')(_get_redditor_listing('saved'))
|
|
|
|
def get_blocked(self):
|
|
"""Return a UserList of Redditors with whom the user has blocked."""
|
|
url = self.reddit_session.config['blocked']
|
|
return self.reddit_session.request_json(url)
|
|
|
|
def get_cached_moderated_reddits(self):
|
|
"""Return a cached dictionary of the user's moderated reddits.
|
|
|
|
This list is used internally. Consider using the `get_my_moderation`
|
|
function instead.
|
|
|
|
"""
|
|
if self._mod_subs is None:
|
|
self._mod_subs = {'mod': self.reddit_session.get_subreddit('mod')}
|
|
for sub in self.reddit_session.get_my_moderation(limit=None):
|
|
self._mod_subs[six.text_type(sub).lower()] = sub
|
|
return self._mod_subs
|
|
|
|
@deprecated('``get_friends`` has been moved to '
|
|
':class:`praw.AuthenticatedReddit` and will be removed from '
|
|
':class:`objects.LoggedInRedditor` in PRAW v4.0.0')
|
|
def get_friends(self, **params):
|
|
"""Return a UserList of Redditors with whom the user is friends.
|
|
|
|
This method has been moved to :class:`praw.AuthenticatedReddit`.
|
|
|
|
"""
|
|
return self.reddit_session.get_friends(**params)
|
|
|
|
|
|
class ModAction(RedditContentObject):
|
|
"""A moderator action."""
|
|
|
|
def __init__(self, reddit_session, json_dict=None, fetch=False):
|
|
"""Construct an instance of the ModAction object."""
|
|
super(ModAction, self).__init__(reddit_session, json_dict, fetch)
|
|
|
|
def __unicode__(self):
|
|
"""Return a string reprsentation of the moderator action."""
|
|
return 'Action: {0}'.format(self.action)
|
|
|
|
|
|
class Submission(Editable, Gildable, Hideable, Moderatable, Refreshable,
|
|
Reportable, Saveable, Voteable):
|
|
"""A class for submissions to reddit."""
|
|
|
|
_methods = (('select_flair', AR),)
|
|
|
|
@staticmethod
|
|
def _extract_more_comments(tree):
|
|
"""Return a list of MoreComments objects removed from tree."""
|
|
more_comments = []
|
|
queue = [(None, x) for x in tree]
|
|
while len(queue) > 0:
|
|
parent, comm = queue.pop(0)
|
|
if isinstance(comm, MoreComments):
|
|
heappush(more_comments, comm)
|
|
if parent:
|
|
parent.replies.remove(comm)
|
|
else:
|
|
tree.remove(comm)
|
|
else:
|
|
for item in comm.replies:
|
|
queue.append((comm, item))
|
|
return more_comments
|
|
|
|
@staticmethod
|
|
def from_id(reddit_session, subreddit_id):
|
|
"""Return an edit-only submission object based on the id."""
|
|
pseudo_data = {'id': subreddit_id,
|
|
'permalink': '/comments/{0}'.format(subreddit_id)}
|
|
return Submission(reddit_session, pseudo_data)
|
|
|
|
@staticmethod
|
|
def from_json(json_response):
|
|
"""Return a submission object from the json response."""
|
|
submission = json_response[0]['data']['children'][0]
|
|
submission.comments = json_response[1]['data']['children']
|
|
return submission
|
|
|
|
@staticmethod
|
|
@restrict_access(scope='read')
|
|
def from_url(reddit_session, url, comment_limit=0, comment_sort=None,
|
|
comments_only=False, params=None):
|
|
"""Request the url and return a Submission object.
|
|
|
|
:param reddit_session: The session to make the request with.
|
|
:param url: The url to build the Submission object from.
|
|
:param comment_limit: The desired number of comments to fetch. If <= 0
|
|
fetch the default number for the session's user. If None, fetch the
|
|
maximum possible.
|
|
:param comment_sort: The sort order for retrieved comments. When None
|
|
use the default for the session's user.
|
|
:param comments_only: Return only the list of comments.
|
|
:param params: dictionary containing extra GET data to put in the url.
|
|
|
|
"""
|
|
if params is None:
|
|
params = {}
|
|
|
|
parsed = urlparse(url)
|
|
query_pairs = parse_qs(parsed.query)
|
|
get_params = dict((k, ",".join(v)) for k, v in query_pairs.items())
|
|
params.update(get_params)
|
|
url = urlunparse(parsed[:3] + ("", "", ""))
|
|
if comment_limit is None: # Fetch MAX
|
|
params['limit'] = 2048 # Just use a big number
|
|
elif comment_limit > 0: # Use value
|
|
params['limit'] = comment_limit
|
|
if comment_sort:
|
|
params['sort'] = comment_sort
|
|
|
|
response = reddit_session.request_json(url, params=params)
|
|
if comments_only:
|
|
return response[1]['data']['children']
|
|
submission = Submission.from_json(response)
|
|
submission._comment_sort = comment_sort # pylint: disable=W0212
|
|
submission._params = params # pylint: disable=W0212
|
|
return submission
|
|
|
|
def __init__(self, reddit_session, json_dict):
|
|
"""Construct an instance of the Subreddit object."""
|
|
super(Submission, self).__init__(reddit_session, json_dict)
|
|
# pylint: disable=E0203
|
|
self._api_link = urljoin(reddit_session.config.api_url, self.permalink)
|
|
# pylint: enable=E0203
|
|
self.permalink = urljoin(reddit_session.config.permalink_url,
|
|
self.permalink)
|
|
self._comment_sort = None
|
|
self._comments_by_id = {}
|
|
self._comments = None
|
|
self._orphaned = {}
|
|
self._replaced_more = False
|
|
self._params = {}
|
|
|
|
@limit_chars
|
|
def __unicode__(self):
|
|
"""Return a string representation of the Subreddit.
|
|
|
|
Note: The representation is truncated to a fix number of characters.
|
|
"""
|
|
title = self.title.replace('\r\n', ' ')
|
|
return six.text_type('{0} :: {1}').format(self.score, title)
|
|
|
|
def _insert_comment(self, comment):
|
|
if comment.name in self._comments_by_id: # Skip existing comments
|
|
return
|
|
|
|
comment._update_submission(self) # pylint: disable=W0212
|
|
|
|
if comment.name in self._orphaned: # Reunite children with parent
|
|
comment.replies.extend(self._orphaned[comment.name])
|
|
del self._orphaned[comment.name]
|
|
|
|
if comment.is_root:
|
|
self._comments.append(comment)
|
|
elif comment.parent_id in self._comments_by_id:
|
|
self._comments_by_id[comment.parent_id].replies.append(comment)
|
|
else: # Orphan
|
|
if comment.parent_id in self._orphaned:
|
|
self._orphaned[comment.parent_id].append(comment)
|
|
else:
|
|
self._orphaned[comment.parent_id] = [comment]
|
|
|
|
def _update_comments(self, comments):
|
|
self._comments = comments
|
|
for comment in self._comments:
|
|
comment._update_submission(self) # pylint: disable=W0212
|
|
|
|
def add_comment(self, text):
|
|
"""Comment on the submission using the specified text.
|
|
|
|
:returns: A Comment object for the newly created comment.
|
|
|
|
"""
|
|
# pylint: disable=W0212
|
|
response = self.reddit_session._add_comment(self.fullname, text)
|
|
# pylint: enable=W0212
|
|
self.reddit_session.evict(self._api_link) # pylint: disable=W0212
|
|
return response
|
|
|
|
@property
|
|
def comments(self): # pylint: disable=E0202
|
|
"""Return forest of comments, with top-level comments as tree roots.
|
|
|
|
May contain instances of MoreComment objects. To easily replace these
|
|
objects with Comment objects, use the replace_more_comments method then
|
|
fetch this attribute. Use comment replies to walk down the tree. To get
|
|
an unnested, flat list of comments from this attribute use
|
|
helpers.flatten_tree.
|
|
|
|
"""
|
|
if self._comments is None:
|
|
self.comments = Submission.from_url( # pylint: disable=W0212
|
|
self.reddit_session, self._api_link, comments_only=True)
|
|
return self._comments
|
|
|
|
@comments.setter # NOQA
|
|
def comments(self, new_comments): # pylint: disable=E0202
|
|
"""Update the list of comments with the provided nested list."""
|
|
self._update_comments(new_comments)
|
|
self._orphaned = {}
|
|
|
|
def get_duplicates(self, *args, **kwargs):
|
|
"""Return a get_content generator for the submission's duplicates.
|
|
|
|
:returns: get_content generator iterating over Submission objects.
|
|
|
|
The additional parameters are passed directly into
|
|
:meth:`.get_content`. Note: the `url` and `object_filter` parameters
|
|
cannot be altered.
|
|
|
|
"""
|
|
url = self.reddit_session.config['duplicates'].format(
|
|
submissionid=self.id)
|
|
return self.reddit_session.get_content(url, *args, object_filter=1,
|
|
**kwargs)
|
|
|
|
def get_flair_choices(self, *args, **kwargs):
|
|
"""Return available link flair choices and current flair.
|
|
|
|
Convenience function for
|
|
:meth:`~.AuthenticatedReddit.get_flair_choices` populating both the
|
|
`subreddit` and `link` parameters.
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
return self.subreddit.get_flair_choices(self.fullname, *args, **kwargs)
|
|
|
|
@restrict_access(scope='modposts')
|
|
def lock(self):
|
|
"""Lock thread.
|
|
|
|
Requires that the currently authenticated user has the modposts oauth
|
|
scope or has user/password authentication as a mod of the subreddit.
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
url = self.reddit_session.config['lock']
|
|
data = {'id': self.fullname}
|
|
return self.reddit_session.request_json(url, data=data)
|
|
|
|
def mark_as_nsfw(self, unmark_nsfw=False):
|
|
"""Mark as Not Safe For Work.
|
|
|
|
Requires that the currently authenticated user is the author of the
|
|
submission, has the modposts oauth scope or has user/password
|
|
authentication as a mod of the subreddit.
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
def mark_as_nsfw_helper(self): # pylint: disable=W0613
|
|
# It is necessary to have the 'self' argument as it's needed in
|
|
# restrict_access to determine what class the decorator is
|
|
# operating on.
|
|
url = self.reddit_session.config['unmarknsfw' if unmark_nsfw else
|
|
'marknsfw']
|
|
data = {'id': self.fullname}
|
|
return self.reddit_session.request_json(url, data=data)
|
|
|
|
is_author = (self.reddit_session.is_logged_in() and self.author ==
|
|
self.reddit_session.user)
|
|
if is_author:
|
|
return mark_as_nsfw_helper(self)
|
|
else:
|
|
return restrict_access('modposts')(mark_as_nsfw_helper)(self)
|
|
|
|
def replace_more_comments(self, limit=32, threshold=1):
|
|
"""Update the comment tree by replacing instances of MoreComments.
|
|
|
|
:param limit: The maximum number of MoreComments objects to
|
|
replace. Each replacement requires 1 API request. Set to None to
|
|
have no limit, or to 0 to make no extra requests. Default: 32
|
|
:param threshold: The minimum number of children comments a
|
|
MoreComments object must have in order to be replaced. Default: 1
|
|
:returns: A list of MoreComments objects that were not replaced.
|
|
|
|
Note that after making this call, the `comments` attribute of the
|
|
submission will no longer contain any MoreComments objects. Items that
|
|
weren't replaced are still removed from the tree, and will be included
|
|
in the returned list.
|
|
|
|
"""
|
|
if self._replaced_more:
|
|
return []
|
|
|
|
remaining = limit
|
|
more_comments = self._extract_more_comments(self.comments)
|
|
skipped = []
|
|
|
|
# Fetch largest more_comments until reaching the limit or the threshold
|
|
while more_comments:
|
|
item = heappop(more_comments)
|
|
if remaining == 0: # We're not going to replace any more
|
|
heappush(more_comments, item) # It wasn't replaced
|
|
break
|
|
elif len(item.children) == 0 or 0 < item.count < threshold:
|
|
heappush(skipped, item) # It wasn't replaced
|
|
continue
|
|
|
|
# Fetch new comments and decrease remaining if a request was made
|
|
new_comments = item.comments(update=False)
|
|
if new_comments is not None and remaining is not None:
|
|
remaining -= 1
|
|
elif new_comments is None:
|
|
continue
|
|
|
|
# Re-add new MoreComment objects to the heap of more_comments
|
|
for more in self._extract_more_comments(new_comments):
|
|
more._update_submission(self) # pylint: disable=W0212
|
|
heappush(more_comments, more)
|
|
# Insert the new comments into the tree
|
|
for comment in new_comments:
|
|
self._insert_comment(comment)
|
|
|
|
self._replaced_more = True
|
|
return more_comments + skipped
|
|
|
|
def set_flair(self, *args, **kwargs):
|
|
"""Set flair for this submission.
|
|
|
|
Convenience function that utilizes :meth:`.ModFlairMixin.set_flair`
|
|
populating both the `subreddit` and `item` parameters.
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
return self.subreddit.set_flair(self, *args, **kwargs)
|
|
|
|
@restrict_access(scope='modposts')
|
|
def set_contest_mode(self, state=True):
|
|
"""Set 'Contest Mode' for the comments of this submission.
|
|
|
|
Contest mode have the following effects:
|
|
* The comment thread will default to being sorted randomly.
|
|
* Replies to top-level comments will be hidden behind
|
|
"[show replies]" buttons.
|
|
* Scores will be hidden from non-moderators.
|
|
* Scores accessed through the API (mobile apps, bots) will be
|
|
obscured to "1" for non-moderators.
|
|
|
|
Source for effects: https://www.reddit.com/159bww/
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
# TODO: Whether a submission is in contest mode is not exposed via the
|
|
# API. Adding a test of this method is thus currently impossible.
|
|
# Add a test when it becomes possible.
|
|
url = self.reddit_session.config['contest_mode']
|
|
data = {'id': self.fullname, 'state': state}
|
|
return self.reddit_session.request_json(url, data=data)
|
|
|
|
@restrict_access(scope='modposts')
|
|
def set_suggested_sort(self, sort='blank'):
|
|
"""Set 'Suggested Sort' for the comments of the submission.
|
|
|
|
Comments can be sorted in one of (confidence, top, new, hot,
|
|
controversial, old, random, qa, blank).
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
url = self.reddit_session.config['suggested_sort']
|
|
data = {'id': self.fullname, 'sort': sort}
|
|
return self.reddit_session.request_json(url, data=data)
|
|
|
|
@property
|
|
def short_link(self):
|
|
"""Return a short link to the submission.
|
|
|
|
The short link points to a page on the short_domain that redirects to
|
|
the main. For example http://redd.it/eorhm is a short link for
|
|
https://www.reddit.com/r/announcements/comments/eorhm/reddit_30_less_typing/.
|
|
|
|
"""
|
|
return urljoin(self.reddit_session.config.short_domain, self.id)
|
|
|
|
@restrict_access(scope='modposts')
|
|
def sticky(self, bottom=True):
|
|
"""Sticky a post in its subreddit.
|
|
|
|
If there is already a stickied post in the designated slot it will be
|
|
unstickied.
|
|
|
|
:param bottom: Set this as the top or bottom sticky. If no top sticky
|
|
exists, this submission will become the top sticky regardless.
|
|
|
|
:returns: The json response from the server
|
|
|
|
"""
|
|
url = self.reddit_session.config['sticky_submission']
|
|
data = {'id': self.fullname, 'state': True}
|
|
if not bottom:
|
|
data['num'] = 1
|
|
return self.reddit_session.request_json(url, data=data)
|
|
|
|
@restrict_access(scope='modposts')
|
|
def unlock(self):
|
|
"""Lock thread.
|
|
|
|
Requires that the currently authenticated user has the modposts oauth
|
|
scope or has user/password authentication as a mod of the subreddit.
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
url = self.reddit_session.config['unlock']
|
|
data = {'id': self.fullname}
|
|
return self.reddit_session.request_json(url, data=data)
|
|
|
|
def unmark_as_nsfw(self):
|
|
"""Mark as Safe For Work.
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
return self.mark_as_nsfw(unmark_nsfw=True)
|
|
|
|
@restrict_access(scope='modposts')
|
|
def unset_contest_mode(self):
|
|
"""Unset 'Contest Mode' for the comments of this submission.
|
|
|
|
Contest mode have the following effects:
|
|
* The comment thread will default to being sorted randomly.
|
|
* Replies to top-level comments will be hidden behind
|
|
"[show replies]" buttons.
|
|
* Scores will be hidden from non-moderators.
|
|
* Scores accessed through the API (mobile apps, bots) will be
|
|
obscured to "1" for non-moderators.
|
|
|
|
Source for effects: http://www.reddit.com/159bww/
|
|
|
|
:returns: The json response from the server.
|
|
|
|
"""
|
|
return self.set_contest_mode(False)
|
|
|
|
@restrict_access(scope='modposts')
|
|
def unsticky(self):
|
|
"""Unsticky this post.
|
|
|
|
:returns: The json response from the server
|
|
|
|
"""
|
|
url = self.reddit_session.config['sticky_submission']
|
|
data = {'id': self.fullname, 'state': False}
|
|
return self.reddit_session.request_json(url, data=data)
|
|
|
|
|
|
class Subreddit(Messageable, Refreshable):
|
|
"""A class for Subreddits."""
|
|
|
|
_methods = (('accept_moderator_invite', AR),
|
|
('add_flair_template', MFMix),
|
|
('clear_flair_templates', MFMix),
|
|
('configure_flair', MFMix),
|
|
('delete_flair', MFMix),
|
|
('delete_image', MCMix),
|
|
('edit_wiki_page', AR),
|
|
('get_banned', MOMix),
|
|
('get_comments', UR),
|
|
('get_contributors', MOMix),
|
|
('get_edited', MOMix),
|
|
('get_flair', UR),
|
|
('get_flair_choices', AR),
|
|
('get_flair_list', MFMix),
|
|
('get_moderators', UR),
|
|
('get_mod_log', MLMix),
|
|
('get_mod_queue', MOMix),
|
|
('get_mod_mail', MOMix),
|
|
('get_muted', MOMix),
|
|
('get_random_submission', UR),
|
|
('get_reports', MOMix),
|
|
('get_rules', UR),
|
|
('get_settings', MCMix),
|
|
('get_spam', MOMix),
|
|
('get_sticky', UR),
|
|
('get_stylesheet', MOMix),
|
|
('get_traffic', UR),
|
|
('get_unmoderated', MOMix),
|
|
('get_wiki_banned', MOMix),
|
|
('get_wiki_contributors', MOMix),
|
|
('get_wiki_page', UR),
|
|
('get_wiki_pages', UR),
|
|
('leave_contributor', MSMix),
|
|
('leave_moderator', MSMix),
|
|
('search', UR),
|
|
('select_flair', AR),
|
|
('set_flair', MFMix),
|
|
('set_flair_csv', MFMix),
|
|
('set_settings', MCMix),
|
|
('set_stylesheet', MCMix),
|
|
('submit', SubmitMixin),
|
|
('subscribe', SubscribeMixin),
|
|
('unsubscribe', SubscribeMixin),
|
|
('update_settings', MCMix),
|
|
('upload_image', MCMix))
|
|
|
|
# Subreddit banned
|
|
add_ban = _modify_relationship('banned', is_sub=True)
|
|
remove_ban = _modify_relationship('banned', unlink=True, is_sub=True)
|
|
|
|
# Subreddit contributors
|
|
add_contributor = _modify_relationship('contributor', is_sub=True)
|
|
remove_contributor = _modify_relationship('contributor', unlink=True,
|
|
is_sub=True)
|
|
# Subreddit moderators
|
|
add_moderator = _modify_relationship('moderator', is_sub=True)
|
|
remove_moderator = _modify_relationship('moderator', unlink=True,
|
|
is_sub=True)
|
|
# Subreddit muted
|
|
add_mute = _modify_relationship('muted', is_sub=True)
|
|
remove_mute = _modify_relationship('muted', is_sub=True, unlink=True)
|
|
|
|
# Subreddit wiki banned
|
|
add_wiki_ban = _modify_relationship('wikibanned', is_sub=True)
|
|
remove_wiki_ban = _modify_relationship('wikibanned', unlink=True,
|
|
is_sub=True)
|
|
# Subreddit wiki contributors
|
|
add_wiki_contributor = _modify_relationship('wikicontributor', is_sub=True)
|
|
remove_wiki_contributor = _modify_relationship('wikicontributor',
|
|
unlink=True, is_sub=True)
|
|
|
|
# Generic listing selectors
|
|
get_controversial = _get_sorter('controversial')
|
|
get_hot = _get_sorter('')
|
|
get_new = _get_sorter('new')
|
|
get_top = _get_sorter('top')
|
|
|
|
# Explicit listing selectors
|
|
get_controversial_from_all = _get_sorter('controversial', t='all')
|
|
get_controversial_from_day = _get_sorter('controversial', t='day')
|
|
get_controversial_from_hour = _get_sorter('controversial', t='hour')
|
|
get_controversial_from_month = _get_sorter('controversial', t='month')
|
|
get_controversial_from_week = _get_sorter('controversial', t='week')
|
|
get_controversial_from_year = _get_sorter('controversial', t='year')
|
|
get_rising = _get_sorter('rising')
|
|
get_top_from_all = _get_sorter('top', t='all')
|
|
get_top_from_day = _get_sorter('top', t='day')
|
|
get_top_from_hour = _get_sorter('top', t='hour')
|
|
get_top_from_month = _get_sorter('top', t='month')
|
|
get_top_from_week = _get_sorter('top', t='week')
|
|
get_top_from_year = _get_sorter('top', t='year')
|
|
|
|
def __init__(self, reddit_session, subreddit_name=None, json_dict=None,
|
|
fetch=False, **kwargs):
|
|
"""Construct an instance of the Subreddit object."""
|
|
# Special case for when my_subreddits is called as no name is returned
|
|
# so we have to extract the name from the URL. The URLs are returned
|
|
# as: /r/reddit_name/
|
|
if subreddit_name is None:
|
|
subreddit_name = json_dict['url'].split('/')[2]
|
|
|
|
if not isinstance(subreddit_name, six.string_types) \
|
|
or not subreddit_name:
|
|
raise TypeError('subreddit_name must be a non-empty string.')
|
|
|
|
if fetch and ('+' in subreddit_name or '-' in subreddit_name):
|
|
fetch = False
|
|
warn_explicit('fetch=True has no effect on multireddits',
|
|
UserWarning, '', 0)
|
|
|
|
info_url = reddit_session.config['subreddit_about'].format(
|
|
subreddit=subreddit_name)
|
|
self._case_name = subreddit_name
|
|
super(Subreddit, self).__init__(reddit_session, json_dict, fetch,
|
|
info_url, **kwargs)
|
|
self.display_name = self._case_name
|
|
self._url = reddit_session.config['subreddit'].format(
|
|
subreddit=self.display_name)
|
|
# '' is the hot listing
|
|
listings = ['new/', '', 'top/', 'controversial/', 'rising/']
|
|
base = reddit_session.config['subreddit'].format(
|
|
subreddit=self.display_name)
|
|
self._listing_urls = [base + x + '.json' for x in listings]
|
|
|
|
def __repr__(self):
|
|
"""Return a code representation of the Subreddit."""
|
|
return 'Subreddit(subreddit_name=\'{0}\')'.format(self.display_name)
|
|
|
|
def __unicode__(self):
|
|
"""Return a string representation of the Subreddit."""
|
|
return self.display_name
|
|
|
|
def _post_populate(self, fetch):
|
|
if fetch:
|
|
# Maintain a consistent `display_name` until the user
|
|
# explicitly calls `subreddit.refresh()`
|
|
tmp = self._case_name
|
|
self._case_name = self.display_name
|
|
self.display_name = tmp
|
|
|
|
def clear_all_flair(self):
|
|
"""Remove all user flair on this subreddit.
|
|
|
|
:returns: The json response from the server when there is flair to
|
|
clear, otherwise returns None.
|
|
|
|
"""
|
|
csv = [{'user': x['user']} for x in self.get_flair_list(limit=None)]
|
|
if csv:
|
|
return self.set_flair_csv(csv)
|
|
else:
|
|
return
|
|
|
|
|
|
class Multireddit(Refreshable):
|
|
"""A class for users' Multireddits."""
|
|
|
|
# Generic listing selectors
|
|
get_controversial = _get_sorter('controversial')
|
|
get_hot = _get_sorter('')
|
|
get_new = _get_sorter('new')
|
|
get_top = _get_sorter('top')
|
|
|
|
# Explicit listing selectors
|
|
get_controversial_from_all = _get_sorter('controversial', t='all')
|
|
get_controversial_from_day = _get_sorter('controversial', t='day')
|
|
get_controversial_from_hour = _get_sorter('controversial', t='hour')
|
|
get_controversial_from_month = _get_sorter('controversial', t='month')
|
|
get_controversial_from_week = _get_sorter('controversial', t='week')
|
|
get_controversial_from_year = _get_sorter('controversial', t='year')
|
|
get_rising = _get_sorter('rising')
|
|
get_top_from_all = _get_sorter('top', t='all')
|
|
get_top_from_day = _get_sorter('top', t='day')
|
|
get_top_from_hour = _get_sorter('top', t='hour')
|
|
get_top_from_month = _get_sorter('top', t='month')
|
|
get_top_from_week = _get_sorter('top', t='week')
|
|
get_top_from_year = _get_sorter('top', t='year')
|
|
|
|
@classmethod
|
|
def from_api_response(cls, reddit_session, json_dict):
|
|
"""Return an instance of the appropriate class from the json dict."""
|
|
# The Multireddit response contains the Subreddits attribute as a list
|
|
# of dicts of the form {'name': 'subredditname'}.
|
|
# We must convert each of these into a Subreddit object.
|
|
json_dict['subreddits'] = [Subreddit(reddit_session, item['name'])
|
|
for item in json_dict['subreddits']]
|
|
return cls(reddit_session, None, None, json_dict)
|
|
|
|
def __init__(self, reddit_session, author=None, name=None,
|
|
json_dict=None, fetch=False, **kwargs):
|
|
"""Construct an instance of the Multireddit object."""
|
|
author = six.text_type(author) if author \
|
|
else json_dict['path'].split('/')[-3]
|
|
if not name:
|
|
name = json_dict['path'].split('/')[-1]
|
|
|
|
info_url = reddit_session.config['multireddit_about'].format(
|
|
user=author, multi=name)
|
|
self.name = name
|
|
self._author = author
|
|
super(Multireddit, self).__init__(reddit_session, json_dict, fetch,
|
|
info_url, **kwargs)
|
|
self._url = reddit_session.config['multireddit'].format(
|
|
user=author, multi=name)
|
|
|
|
def __repr__(self):
|
|
"""Return a code representation of the Multireddit."""
|
|
return 'Multireddit(author=\'{0}\', name=\'{1}\')'.format(
|
|
self._author, self.name)
|
|
|
|
def __unicode__(self):
|
|
"""Return a string representation of the Multireddit."""
|
|
return self.name
|
|
|
|
def _post_populate(self, fetch):
|
|
if fetch:
|
|
# Subreddits are returned as dictionaries in the form
|
|
# {'name': 'subredditname'}. Convert them to Subreddit objects.
|
|
self.subreddits = [Subreddit(self.reddit_session, item['name'])
|
|
for item in self.subreddits]
|
|
|
|
# paths are of the form "/user/{USERNAME}/m/{MULTINAME}"
|
|
author = self.path.split('/')[2]
|
|
self.author = Redditor(self.reddit_session, author)
|
|
|
|
@restrict_access(scope='subscribe')
|
|
def add_subreddit(self, subreddit, _delete=False, *args, **kwargs):
|
|
"""Add a subreddit to the multireddit.
|
|
|
|
:param subreddit: The subreddit name or Subreddit object to add
|
|
|
|
The additional parameters are passed directly into
|
|
:meth:`~praw.__init__.BaseReddit.request_json`.
|
|
|
|
"""
|
|
subreddit = six.text_type(subreddit)
|
|
url = self.reddit_session.config['multireddit_add'].format(
|
|
user=self._author, multi=self.name, subreddit=subreddit)
|
|
method = 'DELETE' if _delete else 'PUT'
|
|
self.reddit_session.http.headers['x-modhash'] = \
|
|
self.reddit_session.modhash
|
|
data = {'model': dumps({'name': subreddit})}
|
|
try:
|
|
self.reddit_session.request(url, data=data, method=method,
|
|
*args, **kwargs)
|
|
finally:
|
|
del self.reddit_session.http.headers['x-modhash']
|
|
|
|
@restrict_access(scope='subscribe')
|
|
def copy(self, to_name):
|
|
"""Copy this multireddit.
|
|
|
|
Convenience function that utilizes
|
|
:meth:`.MultiredditMixin.copy_multireddit` populating both
|
|
the `from_redditor` and `from_name` parameters.
|
|
|
|
"""
|
|
return self.reddit_session.copy_multireddit(self._author, self.name,
|
|
to_name)
|
|
|
|
@restrict_access(scope='subscribe')
|
|
def delete(self):
|
|
"""Delete this multireddit.
|
|
|
|
Convenience function that utilizes
|
|
:meth:`.MultiredditMixin.delete_multireddit` populating the `name`
|
|
parameter.
|
|
|
|
"""
|
|
return self.reddit_session.delete_multireddit(self.name)
|
|
|
|
@restrict_access(scope='subscribe')
|
|
def edit(self, *args, **kwargs):
|
|
"""Edit this multireddit.
|
|
|
|
Convenience function that utilizes
|
|
:meth:`.MultiredditMixin.edit_multireddit` populating the `name`
|
|
parameter.
|
|
|
|
"""
|
|
return self.reddit_session.edit_multireddit(name=self.name, *args,
|
|
**kwargs)
|
|
|
|
@restrict_access(scope='subscribe')
|
|
def remove_subreddit(self, subreddit, *args, **kwargs):
|
|
"""Remove a subreddit from the user's multireddit."""
|
|
return self.add_subreddit(subreddit, True, *args, **kwargs)
|
|
|
|
@restrict_access(scope='subscribe')
|
|
def rename(self, new_name, *args, **kwargs):
|
|
"""Rename this multireddit.
|
|
|
|
This function is a handy shortcut to
|
|
:meth:`rename_multireddit` of the reddit_session.
|
|
|
|
"""
|
|
new = self.reddit_session.rename_multireddit(self.name, new_name,
|
|
*args, **kwargs)
|
|
self.__dict__ = new.__dict__ # pylint: disable=W0201
|
|
return self
|
|
|
|
|
|
class PRAWListing(RedditContentObject):
|
|
"""An abstract class to coerce a listing into RedditContentObjects."""
|
|
|
|
CHILD_ATTRIBUTE = None
|
|
|
|
def __init__(self, reddit_session, json_dict=None, fetch=False):
|
|
"""Construct an instance of the PRAWListing object."""
|
|
super(PRAWListing, self).__init__(reddit_session, json_dict, fetch)
|
|
|
|
if not self.CHILD_ATTRIBUTE:
|
|
raise NotImplementedError('PRAWListing must be extended.')
|
|
|
|
child_list = getattr(self, self.CHILD_ATTRIBUTE)
|
|
for i in range(len(child_list)):
|
|
child_list[i] = self._convert(reddit_session, child_list[i])
|
|
|
|
def __contains__(self, item):
|
|
"""Test if item exists in the listing."""
|
|
return item in getattr(self, self.CHILD_ATTRIBUTE)
|
|
|
|
def __delitem__(self, index):
|
|
"""Remove the item at position index from the listing."""
|
|
del getattr(self, self.CHILD_ATTRIBUTE)[index]
|
|
|
|
def __getitem__(self, index):
|
|
"""Return the item at position index in the listing."""
|
|
return getattr(self, self.CHILD_ATTRIBUTE)[index]
|
|
|
|
def __iter__(self):
|
|
"""Return an iterator to the listing."""
|
|
return getattr(self, self.CHILD_ATTRIBUTE).__iter__()
|
|
|
|
def __len__(self):
|
|
"""Return the number of items in the listing."""
|
|
return len(getattr(self, self.CHILD_ATTRIBUTE))
|
|
|
|
def __setitem__(self, index, item):
|
|
"""Set item at position `index` in the listing."""
|
|
getattr(self, self.CHILD_ATTRIBUTE)[index] = item
|
|
|
|
def __unicode__(self):
|
|
"""Return a string representation of the listing."""
|
|
return six.text_type(getattr(self, self.CHILD_ATTRIBUTE))
|
|
|
|
|
|
class UserList(PRAWListing):
|
|
"""A list of Redditors. Works just like a regular list."""
|
|
|
|
CHILD_ATTRIBUTE = 'children'
|
|
|
|
@staticmethod
|
|
def _convert(reddit_session, data):
|
|
"""Return a Redditor object from the data."""
|
|
retval = Redditor(reddit_session, data['name'], fetch=False)
|
|
retval.id = data['id'].split('_')[1] # pylint: disable=C0103,W0201
|
|
return retval
|
|
|
|
|
|
class WikiPage(Refreshable):
|
|
"""An individual WikiPage object."""
|
|
|
|
@classmethod
|
|
def from_api_response(cls, reddit_session, json_dict):
|
|
"""Return an instance of the appropriate class from the json_dict."""
|
|
# The WikiPage response does not contain the necessary information
|
|
# in the JSON response to determine the name of the page nor the
|
|
# subreddit it belongs to. Thus we must extract this information
|
|
# from the request URL.
|
|
# pylint: disable=W0212
|
|
parts = reddit_session._request_url.split('/', 6)
|
|
# pylint: enable=W0212
|
|
subreddit = parts[4]
|
|
page = parts[6].split('.', 1)[0]
|
|
return cls(reddit_session, subreddit, page, json_dict=json_dict)
|
|
|
|
def __init__(self, reddit_session, subreddit=None, page=None,
|
|
json_dict=None, fetch=False, **kwargs):
|
|
"""Construct an instance of the WikiPage object."""
|
|
if not subreddit and not page:
|
|
subreddit = json_dict['sr']
|
|
page = json_dict['page']
|
|
info_url = reddit_session.config['wiki_page'].format(
|
|
subreddit=six.text_type(subreddit), page=page)
|
|
super(WikiPage, self).__init__(reddit_session, json_dict, fetch,
|
|
info_url, **kwargs)
|
|
self.page = page
|
|
self.subreddit = subreddit
|
|
|
|
def __unicode__(self):
|
|
"""Return a string representation of the page."""
|
|
return six.text_type('{0}:{1}').format(self.subreddit, self.page)
|
|
|
|
@restrict_access(scope='modwiki')
|
|
def add_editor(self, username, _delete=False, *args, **kwargs):
|
|
"""Add an editor to this wiki page.
|
|
|
|
:param username: The name or Redditor object of the user to add.
|
|
:param _delete: If True, remove the user as an editor instead.
|
|
Please use :meth:`remove_editor` rather than setting it manually.
|
|
|
|
Additional parameters are passed into
|
|
:meth:`~praw.__init__.BaseReddit.request_json`.
|
|
"""
|
|
url = self.reddit_session.config['wiki_page_editor']
|
|
url = url.format(subreddit=six.text_type(self.subreddit),
|
|
method='del' if _delete else 'add')
|
|
|
|
data = {'page': self.page,
|
|
'username': six.text_type(username)}
|
|
return self.reddit_session.request_json(url, data=data, *args,
|
|
**kwargs)
|
|
|
|
@restrict_access(scope='modwiki')
|
|
def get_settings(self, *args, **kwargs):
|
|
"""Return the settings for this wiki page.
|
|
|
|
Includes permission level, names of editors, and whether
|
|
the page is listed on /wiki/pages.
|
|
|
|
Additional parameters are passed into
|
|
:meth:`~praw.__init__.BaseReddit.request_json`
|
|
"""
|
|
url = self.reddit_session.config['wiki_page_settings']
|
|
url = url.format(subreddit=six.text_type(self.subreddit),
|
|
page=self.page)
|
|
return self.reddit_session.request_json(url, *args, **kwargs)['data']
|
|
|
|
def edit(self, *args, **kwargs):
|
|
"""Edit the wiki page.
|
|
|
|
Convenience function that utilizes
|
|
:meth:`.AuthenticatedReddit.edit_wiki_page` populating both the
|
|
``subreddit`` and ``page`` parameters.
|
|
"""
|
|
return self.subreddit.edit_wiki_page(self.page, *args, **kwargs)
|
|
|
|
@restrict_access(scope='modwiki')
|
|
def edit_settings(self, permlevel, listed, *args, **kwargs):
|
|
"""Edit the settings for this individual wiki page.
|
|
|
|
:param permlevel: Who can edit this page?
|
|
(0) use subreddit wiki permissions, (1) only approved wiki
|
|
contributors for this page may edit (see
|
|
:meth:`~praw.objects.WikiPage.add_editor`), (2) only mods may edit
|
|
and view
|
|
:param listed: Show this page on the listing?
|
|
True - Appear in /wiki/pages
|
|
False - Do not appear in /wiki/pages
|
|
:returns: The updated settings data.
|
|
|
|
Additional parameters are passed into :meth:`request_json`.
|
|
|
|
"""
|
|
url = self.reddit_session.config['wiki_page_settings']
|
|
url = url.format(subreddit=six.text_type(self.subreddit),
|
|
page=self.page)
|
|
data = {'permlevel': permlevel,
|
|
'listed': 'on' if listed else 'off'}
|
|
|
|
return self.reddit_session.request_json(url, data=data, *args,
|
|
**kwargs)['data']
|
|
|
|
def remove_editor(self, username, *args, **kwargs):
|
|
"""Remove an editor from this wiki page.
|
|
|
|
:param username: The name or Redditor object of the user to remove.
|
|
|
|
This method points to :meth:`add_editor` with _delete=True.
|
|
|
|
Additional parameters are are passed to :meth:`add_editor` and
|
|
subsequently into :meth:`~praw.__init__.BaseReddit.request_json`.
|
|
"""
|
|
return self.add_editor(username=username, _delete=True, *args,
|
|
**kwargs)
|
|
|
|
|
|
class WikiPageListing(PRAWListing):
|
|
"""A list of WikiPages. Works just like a regular list."""
|
|
|
|
CHILD_ATTRIBUTE = '_tmp'
|
|
|
|
@staticmethod
|
|
def _convert(reddit_session, data):
|
|
"""Return a WikiPage object from the data."""
|
|
# TODO: The _request_url hack shouldn't be necessary
|
|
# pylint: disable=W0212
|
|
subreddit = reddit_session._request_url.rsplit('/', 4)[1]
|
|
# pylint: enable=W0212
|
|
return WikiPage(reddit_session, subreddit, data, fetch=False)
|
|
|
|
|
|
def _add_aliases():
|
|
def predicate(obj):
|
|
return inspect.isclass(obj) and hasattr(obj, '_methods')
|
|
|
|
import inspect
|
|
import sys
|
|
|
|
for _, cls in inspect.getmembers(sys.modules[__name__], predicate):
|
|
for name, mixin in cls._methods: # pylint: disable=W0212
|
|
setattr(cls, name, alias_function(getattr(mixin, name),
|
|
mixin.__name__))
|
|
_add_aliases()
|