Initial commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Provide all models that map to Reddit objects."""
|
||||
95
backend/venv/Lib/site-packages/praw/models/reddit/base.py
Normal file
95
backend/venv/Lib/site-packages/praw/models/reddit/base.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""Provide the RedditBase class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from ...endpoints import API_PATH
|
||||
from ...exceptions import InvalidURL
|
||||
from ..base import PRAWBase
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw
|
||||
|
||||
|
||||
class RedditBase(PRAWBase):
|
||||
"""Base class that represents actual Reddit objects."""
|
||||
|
||||
@staticmethod
|
||||
def _url_parts(url: str) -> list[str]:
|
||||
parsed = urlparse(url)
|
||||
if not parsed.netloc:
|
||||
raise InvalidURL(url)
|
||||
return parsed.path.rstrip("/").split("/")
|
||||
|
||||
def __eq__(self, other: Any | str) -> bool:
|
||||
"""Return whether the other instance equals the current."""
|
||||
if isinstance(other, str):
|
||||
return other.lower() == str(self).lower()
|
||||
return (
|
||||
isinstance(other, self.__class__)
|
||||
and str(self).lower() == str(other).lower()
|
||||
)
|
||||
|
||||
def __getattr__(self, attribute: str) -> Any:
|
||||
"""Return the value of ``attribute``."""
|
||||
if not attribute.startswith("_") and not self._fetched:
|
||||
self._fetch()
|
||||
return getattr(self, attribute)
|
||||
msg = f"{self.__class__.__name__!r} object has no attribute {attribute!r}"
|
||||
raise AttributeError(msg)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""Return the hash of the current instance."""
|
||||
return hash(self.__class__.__name__) ^ hash(str(self).lower())
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
_data: dict[str, Any] | None,
|
||||
_extra_attribute_to_check: str | None = None,
|
||||
_fetched: bool = False,
|
||||
_str_field: bool = True,
|
||||
):
|
||||
"""Initialize a :class:`.RedditBase` instance.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
|
||||
"""
|
||||
super().__init__(reddit, _data=_data)
|
||||
self._fetched = _fetched
|
||||
if _str_field and self.STR_FIELD not in self.__dict__:
|
||||
if (
|
||||
_extra_attribute_to_check is not None
|
||||
and _extra_attribute_to_check in self.__dict__
|
||||
):
|
||||
return
|
||||
msg = f"An invalid value was specified for {self.STR_FIELD}. Check that the argument for the {self.STR_FIELD} parameter is not empty."
|
||||
raise ValueError(msg)
|
||||
|
||||
def __ne__(self, other: object) -> bool:
|
||||
"""Return whether the other instance differs from the current."""
|
||||
return not self == other
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return an object initialization representation of the instance."""
|
||||
return f"{self.__class__.__name__}({self.STR_FIELD}={str(self)!r})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return a string representation of the instance."""
|
||||
return getattr(self, self.STR_FIELD)
|
||||
|
||||
def _fetch(self):
|
||||
self._fetched = True
|
||||
|
||||
def _fetch_data(self):
|
||||
name, fields, params = self._fetch_info()
|
||||
path = API_PATH[name].format(**fields)
|
||||
return self._reddit.request(method="GET", params=params, path=path)
|
||||
|
||||
def _reset_attributes(self, *attributes: str):
|
||||
for attribute in attributes:
|
||||
if attribute in self.__dict__:
|
||||
del self.__dict__[attribute]
|
||||
self._fetched = False
|
||||
586
backend/venv/Lib/site-packages/praw/models/reddit/collections.py
Normal file
586
backend/venv/Lib/site-packages/praw/models/reddit/collections.py
Normal file
@@ -0,0 +1,586 @@
|
||||
"""Provide Collections functionality."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ...const import API_PATH
|
||||
from ...exceptions import ClientException
|
||||
from ...util import _deprecate_args
|
||||
from ...util.cache import cachedproperty
|
||||
from ..base import PRAWBase
|
||||
from .base import RedditBase
|
||||
from .submission import Submission
|
||||
from .subreddit import Subreddit
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from collections.abc import Iterator
|
||||
|
||||
import praw.models
|
||||
|
||||
|
||||
class CollectionModeration(PRAWBase):
|
||||
"""Class to support moderation actions on a :class:`.Collection`.
|
||||
|
||||
Obtain an instance via:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").collections("some_uuid").mod
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, reddit: praw.Reddit, collection_id: str):
|
||||
"""Initialize a :class:`.CollectionModeration` instance.
|
||||
|
||||
:param collection_id: The ID of a :class:`.Collection`.
|
||||
|
||||
"""
|
||||
super().__init__(reddit, _data=None)
|
||||
self.collection_id = collection_id
|
||||
|
||||
def _post_fullname(self, post: str | praw.models.Submission) -> str:
|
||||
"""Get a post's fullname.
|
||||
|
||||
:param post: A fullname, a :class:`.Submission`, a permalink, or an ID.
|
||||
|
||||
:returns: The fullname of the post.
|
||||
|
||||
"""
|
||||
if isinstance(post, Submission):
|
||||
return post.fullname
|
||||
if not isinstance(post, str):
|
||||
msg = f"Cannot get fullname from object of type {type(post)}."
|
||||
raise TypeError(msg)
|
||||
if post.startswith(f"{self._reddit.config.kinds['submission']}_"):
|
||||
return post
|
||||
try:
|
||||
return self._reddit.submission(url=post).fullname
|
||||
except ClientException:
|
||||
return self._reddit.submission(post).fullname
|
||||
|
||||
def add_post(self, submission: praw.models.Submission):
|
||||
"""Add a post to the collection.
|
||||
|
||||
:param submission: The post to add, a :class:`.Submission`, its permalink as a
|
||||
``str``, its fullname as a ``str``, or its ID as a ``str``.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
collection = reddit.subreddit("test").collections("some_uuid")
|
||||
collection.mod.add_post("bgibu9")
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.remove_post`
|
||||
|
||||
"""
|
||||
link_fullname = self._post_fullname(submission)
|
||||
|
||||
self._reddit.post(
|
||||
API_PATH["collection_add_post"],
|
||||
data={"collection_id": self.collection_id, "link_fullname": link_fullname},
|
||||
)
|
||||
|
||||
def delete(self):
|
||||
"""Delete this collection.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").collections("some_uuid").mod.delete()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`~.SubredditCollectionsModeration.create`
|
||||
|
||||
"""
|
||||
self._reddit.post(
|
||||
API_PATH["collection_delete"], data={"collection_id": self.collection_id}
|
||||
)
|
||||
|
||||
def remove_post(self, submission: praw.models.Submission):
|
||||
"""Remove a post from the collection.
|
||||
|
||||
:param submission: The post to remove, a :class:`.Submission`, its permalink as
|
||||
a ``str``, its fullname as a ``str``, or its ID as a ``str``.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
collection = reddit.subreddit("test").collections("some_uuid")
|
||||
collection.mod.remove_post("bgibu9")
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.add_post`
|
||||
|
||||
"""
|
||||
link_fullname = self._post_fullname(submission)
|
||||
|
||||
self._reddit.post(
|
||||
API_PATH["collection_remove_post"],
|
||||
data={"collection_id": self.collection_id, "link_fullname": link_fullname},
|
||||
)
|
||||
|
||||
def reorder(self, links: list[str | praw.models.Submission]):
|
||||
r"""Reorder posts in the collection.
|
||||
|
||||
:param links: A list of :class:`.Submission`\ s or a ``str`` that is either a
|
||||
fullname or an ID.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
collection = reddit.subreddit("test").collections("some_uuid")
|
||||
current_order = collection.link_ids
|
||||
new_order = reversed(current_order)
|
||||
collection.mod.reorder(new_order)
|
||||
|
||||
"""
|
||||
link_ids = ",".join(self._post_fullname(post) for post in links)
|
||||
self._reddit.post(
|
||||
API_PATH["collection_reorder"],
|
||||
data={"collection_id": self.collection_id, "link_ids": link_ids},
|
||||
)
|
||||
|
||||
def update_description(self, description: str):
|
||||
"""Update the collection's description.
|
||||
|
||||
:param description: The new description.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
collection = reddit.subreddit("test").collections("some_uuid")
|
||||
collection.mod.update_description("Please enjoy these links!")
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.update_title`
|
||||
|
||||
"""
|
||||
self._reddit.post(
|
||||
API_PATH["collection_desc"],
|
||||
data={"collection_id": self.collection_id, "description": description},
|
||||
)
|
||||
|
||||
def update_display_layout(self, display_layout: str):
|
||||
"""Update the collection's display layout.
|
||||
|
||||
:param display_layout: Either ``"TIMELINE"`` for events or discussions or
|
||||
``"GALLERY"`` for images or memes. Passing ``None`` will clear the set
|
||||
layout and ``collection.display_layout`` will be ``None``, however, the
|
||||
collection will appear on Reddit as if ``display_layout`` is set to
|
||||
``"TIMELINE"``.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
collection = reddit.subreddit("test").collections("some_uuid")
|
||||
collection.mod.update_display_layout("GALLERY")
|
||||
|
||||
"""
|
||||
self._reddit.post(
|
||||
API_PATH["collection_layout"],
|
||||
data={
|
||||
"collection_id": self.collection_id,
|
||||
"display_layout": display_layout,
|
||||
},
|
||||
)
|
||||
|
||||
def update_title(self, title: str):
|
||||
"""Update the collection's title.
|
||||
|
||||
:param title: The new title.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
collection = reddit.subreddit("test").collections("some_uuid")
|
||||
collection.mod.update_title("Titley McTitleface")
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.update_description`
|
||||
|
||||
"""
|
||||
self._reddit.post(
|
||||
API_PATH["collection_title"],
|
||||
data={"collection_id": self.collection_id, "title": title},
|
||||
)
|
||||
|
||||
|
||||
class SubredditCollectionsModeration(PRAWBase):
|
||||
r"""Class to represent moderator actions on a :class:`.Subreddit`'s :class:`.Collection`\ s.
|
||||
|
||||
Obtain an instance via:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").collections.mod
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
sub_fullname: str,
|
||||
_data: dict[str, Any] | None = None,
|
||||
):
|
||||
"""Initialize a :class:`.SubredditCollectionsModeration` instance."""
|
||||
super().__init__(reddit, _data)
|
||||
self.subreddit_fullname = sub_fullname
|
||||
|
||||
@_deprecate_args("title", "description", "display_layout")
|
||||
def create(
|
||||
self, *, description: str, display_layout: str | None = None, title: str
|
||||
) -> Collection:
|
||||
"""Create a new :class:`.Collection`.
|
||||
|
||||
The authenticated account must have appropriate moderator permissions in the
|
||||
subreddit this collection belongs to.
|
||||
|
||||
:param description: The description, up to 500 characters.
|
||||
:param display_layout: Either ``"TIMELINE"`` for events or discussions or
|
||||
``"GALLERY"`` for images or memes. Passing ``""`` or ``None`` will make the
|
||||
collection appear on Reddit as if this is set to ``"TIMELINE"`` (default:
|
||||
``None``).
|
||||
:param title: The title of the collection, up to 300 characters.
|
||||
|
||||
:returns: The newly created :class:`.Collection`.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
my_sub = reddit.subreddit("test")
|
||||
new_collection = my_sub.collections.mod.create(title="Title", description="desc")
|
||||
new_collection.mod.add_post("bgibu9")
|
||||
|
||||
To specify the display layout as ``"GALLERY"`` when creating the collection:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
my_sub = reddit.subreddit("test")
|
||||
new_collection = my_sub.collections.mod.create(
|
||||
title="Title", description="desc", display_layout="GALLERY"
|
||||
)
|
||||
new_collection.mod.add_post("bgibu9")
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`~.CollectionModeration.delete`
|
||||
|
||||
"""
|
||||
data = {
|
||||
"sr_fullname": self.subreddit_fullname,
|
||||
"title": title,
|
||||
"description": description,
|
||||
}
|
||||
if display_layout:
|
||||
data["display_layout"] = display_layout
|
||||
return self._reddit.post(
|
||||
API_PATH["collection_create"],
|
||||
data=data,
|
||||
)
|
||||
|
||||
|
||||
class SubredditCollections(PRAWBase):
|
||||
r"""Class to represent a :class:`.Subreddit`'s :class:`.Collection`\ s.
|
||||
|
||||
Obtain an instance via:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").collections
|
||||
|
||||
"""
|
||||
|
||||
@cachedproperty
|
||||
def mod(self) -> SubredditCollectionsModeration:
|
||||
"""Get an instance of :class:`.SubredditCollectionsModeration`.
|
||||
|
||||
Provides :meth:`~SubredditCollectionsModeration.create`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
my_sub = reddit.subreddit("test")
|
||||
new_collection = my_sub.collections.mod.create(title="Title", description="desc")
|
||||
|
||||
"""
|
||||
return SubredditCollectionsModeration(self._reddit, self.subreddit.fullname)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
collection_id: str | None = None,
|
||||
permalink: str | None = None,
|
||||
) -> Collection:
|
||||
"""Return the :class:`.Collection` with the specified ID.
|
||||
|
||||
:param collection_id: The ID of a :class:`.Collection` (default: ``None``).
|
||||
:param permalink: The permalink of a collection (default: ``None``).
|
||||
|
||||
:returns: The specified :class:`.Collection`.
|
||||
|
||||
Exactly one of ``collection_id`` or ``permalink`` is required.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit = reddit.subreddit("test")
|
||||
|
||||
uuid = "847e4548-a3b5-4ad7-afb4-edbfc2ed0a6b"
|
||||
collection = subreddit.collections(uuid)
|
||||
print(collection.title)
|
||||
print(collection.description)
|
||||
|
||||
permalink = "https://www.reddit.com/r/SUBREDDIT/collection/" + uuid
|
||||
collection = subreddit.collections(permalink=permalink)
|
||||
print(collection.title)
|
||||
print(collection.description)
|
||||
|
||||
"""
|
||||
if (collection_id is None) == (permalink is None):
|
||||
msg = "Exactly one of 'collection_id' or 'permalink' must be provided."
|
||||
raise TypeError(msg)
|
||||
return Collection(
|
||||
self._reddit, collection_id=collection_id, permalink=permalink
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
subreddit: praw.models.Subreddit,
|
||||
_data: dict[str, Any] | None = None,
|
||||
):
|
||||
"""Initialize a :class:`.SubredditCollections` instance."""
|
||||
super().__init__(reddit, _data)
|
||||
self.subreddit = subreddit
|
||||
|
||||
def __iter__(self):
|
||||
r"""Iterate over the :class:`.Subreddit`'s :class:`.Collection`\ s.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for collection in reddit.subreddit("test").collections:
|
||||
print(collection.permalink)
|
||||
|
||||
"""
|
||||
request = self._reddit.get(
|
||||
API_PATH["collection_subreddit"],
|
||||
params={"sr_fullname": self.subreddit.fullname},
|
||||
)
|
||||
yield from request
|
||||
|
||||
|
||||
class Collection(RedditBase):
|
||||
"""Class to represent a :class:`.Collection`.
|
||||
|
||||
Obtain an instance via:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
collection = reddit.subreddit("test").collections("some_uuid")
|
||||
|
||||
or
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
collection = reddit.subreddit("test").collections(
|
||||
permalink="https://reddit.com/r/SUBREDDIT/collection/some_uuid"
|
||||
)
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
=================== =============================================================
|
||||
Attribute Description
|
||||
=================== =============================================================
|
||||
``author`` The :class:`.Redditor` who created the collection.
|
||||
``collection_id`` The UUID of the collection.
|
||||
``created_at_utc`` Time the collection was created, represented in `Unix Time`_.
|
||||
``description`` The collection description.
|
||||
``display_layout`` The collection display layout.
|
||||
``last_update_utc`` Time the collection was last updated, represented in `Unix
|
||||
Time`_.
|
||||
``link_ids`` A list of :class:`.Submission` fullnames.
|
||||
``permalink`` The collection's permalink (to view on the web).
|
||||
``sorted_links`` An iterable listing of the posts in this collection.
|
||||
``title`` The title of the collection.
|
||||
=================== =============================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
STR_FIELD = "collection_id"
|
||||
|
||||
@cachedproperty
|
||||
def mod(self) -> CollectionModeration:
|
||||
"""Get an instance of :class:`.CollectionModeration`.
|
||||
|
||||
Provides access to various methods, including
|
||||
:meth:`~.CollectionModeration.add_post`, :meth:`~.CollectionModeration.delete`,
|
||||
:meth:`~.CollectionModeration.reorder`, and
|
||||
:meth:`~.CollectionModeration.update_title`.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
collection = reddit.subreddit("test").collections("some_uuid")
|
||||
collection.mod.update_title("My new title!")
|
||||
|
||||
"""
|
||||
return CollectionModeration(self._reddit, self.collection_id)
|
||||
|
||||
@cachedproperty
|
||||
def subreddit(self) -> praw.models.Subreddit:
|
||||
"""Get the subreddit that this collection belongs to.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
collection = reddit.subreddit("test").collections("some_uuid")
|
||||
subreddit = collection.subreddit
|
||||
|
||||
"""
|
||||
return next(self._reddit.info(fullnames=[self.subreddit_id]))
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
_data: dict[str, Any] = None,
|
||||
collection_id: str | None = None,
|
||||
permalink: str | None = None,
|
||||
):
|
||||
"""Initialize a :class:`.Collection` instance.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
:param _data: Any data associated with the :class:`.Collection`.
|
||||
:param collection_id: The ID of the :class:`.Collection`.
|
||||
:param permalink: The permalink of the :class:`.Collection`.
|
||||
|
||||
"""
|
||||
if (_data, collection_id, permalink).count(None) != 2:
|
||||
msg = "Exactly one of '_data', 'collection_id', or 'permalink' must be provided."
|
||||
raise TypeError(msg)
|
||||
|
||||
if permalink:
|
||||
collection_id = self._url_parts(permalink)[4]
|
||||
|
||||
if collection_id:
|
||||
self.collection_id = collection_id # set from _data otherwise
|
||||
|
||||
super().__init__(reddit, _data)
|
||||
|
||||
self._info_params = {
|
||||
"collection_id": self.collection_id,
|
||||
"include_links": True,
|
||||
}
|
||||
|
||||
def __iter__(self) -> Iterator:
|
||||
"""Provide a way to iterate over the posts in this :class:`.Collection`.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
collection = reddit.subreddit("test").collections("some_uuid")
|
||||
for submission in collection:
|
||||
print(submission.title, submission.permalink)
|
||||
|
||||
"""
|
||||
yield from self.sorted_links
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Get the number of posts in this :class:`.Collection`.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
collection = reddit.subreddit("test").collections("some_uuid")
|
||||
print(len(collection))
|
||||
|
||||
"""
|
||||
return len(self.link_ids)
|
||||
|
||||
def __setattr__(self, attribute: str, value: Any):
|
||||
"""Objectify author, subreddit, and sorted_links attributes."""
|
||||
if attribute == "author_name":
|
||||
self.author = self._reddit.redditor(value)
|
||||
elif attribute == "sorted_links":
|
||||
value = self._reddit._objector.objectify(value)
|
||||
super().__setattr__(attribute, value)
|
||||
|
||||
def _fetch(self):
|
||||
data = self._fetch_data()
|
||||
try:
|
||||
self._reddit._objector.check_error(data)
|
||||
except ClientException:
|
||||
# A well-formed but invalid Collections ID during fetch time
|
||||
# causes Reddit to return something that looks like an error
|
||||
# but with no content.
|
||||
msg = f"Error during fetch. Check collection ID {self.collection_id!r} is correct."
|
||||
raise ClientException(msg) from None
|
||||
|
||||
other = type(self)(self._reddit, _data=data)
|
||||
self.__dict__.update(other.__dict__)
|
||||
super()._fetch()
|
||||
|
||||
def _fetch_info(self):
|
||||
return "collection", {}, self._info_params
|
||||
|
||||
def follow(self):
|
||||
"""Follow this :class:`.Collection`.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").collections("some_uuid").follow()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.unfollow`
|
||||
|
||||
"""
|
||||
self._reddit.post(
|
||||
API_PATH["collection_follow"],
|
||||
data={"collection_id": self.collection_id, "follow": True},
|
||||
)
|
||||
|
||||
def unfollow(self):
|
||||
"""Unfollow this :class:`.Collection`.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").collections("some_uuid").unfollow()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.follow`
|
||||
|
||||
"""
|
||||
self._reddit.post(
|
||||
API_PATH["collection_follow"],
|
||||
data={"collection_id": self.collection_id, "follow": False},
|
||||
)
|
||||
|
||||
|
||||
Subreddit._subreddit_collections_class = SubredditCollections
|
||||
355
backend/venv/Lib/site-packages/praw/models/reddit/comment.py
Normal file
355
backend/venv/Lib/site-packages/praw/models/reddit/comment.py
Normal file
@@ -0,0 +1,355 @@
|
||||
"""Provide the Comment class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ...const import API_PATH
|
||||
from ...exceptions import ClientException, InvalidURL
|
||||
from ...util.cache import cachedproperty
|
||||
from ..comment_forest import CommentForest
|
||||
from .base import RedditBase
|
||||
from .mixins import (
|
||||
FullnameMixin,
|
||||
InboxableMixin,
|
||||
ThingModerationMixin,
|
||||
UserContentMixin,
|
||||
)
|
||||
from .redditor import Redditor
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class Comment(InboxableMixin, UserContentMixin, FullnameMixin, RedditBase):
|
||||
"""A class that represents a Reddit comment.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
================= =================================================================
|
||||
Attribute Description
|
||||
================= =================================================================
|
||||
``author`` Provides an instance of :class:`.Redditor`.
|
||||
``body`` The body of the comment, as Markdown.
|
||||
``body_html`` The body of the comment, as HTML.
|
||||
``created_utc`` Time the comment was created, represented in `Unix Time`_.
|
||||
``distinguished`` Whether or not the comment is distinguished.
|
||||
``edited`` Whether or not the comment has been edited.
|
||||
``id`` The ID of the comment.
|
||||
``is_submitter`` Whether or not the comment author is also the author of the
|
||||
submission.
|
||||
``link_id`` The submission ID that the comment belongs to.
|
||||
``parent_id`` The ID of the parent comment (prefixed with ``t1_``). If it is a
|
||||
top-level comment, this returns the submission ID instead
|
||||
(prefixed with ``t3_``).
|
||||
``permalink`` A permalink for the comment. :class:`.Comment` objects from the
|
||||
inbox have a ``context`` attribute instead.
|
||||
``replies`` Provides an instance of :class:`.CommentForest`.
|
||||
``saved`` Whether or not the comment is saved.
|
||||
``score`` The number of upvotes for the comment.
|
||||
``stickied`` Whether or not the comment is stickied.
|
||||
``submission`` Provides an instance of :class:`.Submission`. The submission that
|
||||
the comment belongs to.
|
||||
``subreddit`` Provides an instance of :class:`.Subreddit`. The subreddit that
|
||||
the comment belongs to.
|
||||
``subreddit_id`` The subreddit ID that the comment belongs to.
|
||||
================= =================================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
MISSING_COMMENT_MESSAGE = "This comment does not appear to be in the comment tree"
|
||||
STR_FIELD = "id"
|
||||
|
||||
@staticmethod
|
||||
def id_from_url(url: str) -> str:
|
||||
"""Get the ID of a comment from the full URL."""
|
||||
parts = RedditBase._url_parts(url)
|
||||
try:
|
||||
comment_index = parts.index("comments")
|
||||
except ValueError:
|
||||
raise InvalidURL(url) from None
|
||||
|
||||
if len(parts) - 4 != comment_index:
|
||||
raise InvalidURL(url)
|
||||
return parts[-1]
|
||||
|
||||
@cachedproperty
|
||||
def mod(self) -> praw.models.reddit.comment.CommentModeration:
|
||||
"""Provide an instance of :class:`.CommentModeration`.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.mod.approve()
|
||||
|
||||
"""
|
||||
return CommentModeration(self)
|
||||
|
||||
@property
|
||||
def _kind(self):
|
||||
"""Return the class's kind."""
|
||||
return self._reddit.config.kinds["comment"]
|
||||
|
||||
@property
|
||||
def is_root(self) -> bool:
|
||||
"""Return ``True`` when the comment is a top-level comment."""
|
||||
parent_type = self.parent_id.split("_", 1)[0]
|
||||
return parent_type == self._reddit.config.kinds["submission"]
|
||||
|
||||
@property
|
||||
def replies(self) -> CommentForest:
|
||||
"""Provide an instance of :class:`.CommentForest`.
|
||||
|
||||
This property may return an empty list if the comment has not been refreshed
|
||||
with :meth:`.refresh`
|
||||
|
||||
Sort order and reply limit can be set with the ``reply_sort`` and
|
||||
``reply_limit`` attributes before replies are fetched, including any call to
|
||||
:meth:`.refresh`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment.reply_sort = "new"
|
||||
comment.refresh()
|
||||
replies = comment.replies
|
||||
|
||||
.. note::
|
||||
|
||||
The appropriate values for ``reply_sort`` include ``"confidence"``,
|
||||
``"controversial"``, ``"new"``, ``"old"``, ``"q&a"``, and ``"top"``.
|
||||
|
||||
"""
|
||||
if isinstance(self._replies, list):
|
||||
self._replies = CommentForest(self.submission, self._replies)
|
||||
return self._replies
|
||||
|
||||
@property
|
||||
def submission(self) -> praw.models.Submission:
|
||||
"""Return the :class:`.Submission` object this comment belongs to."""
|
||||
if not self._submission: # Comment not from submission
|
||||
self._submission = self._reddit.submission(self._extract_submission_id())
|
||||
return self._submission
|
||||
|
||||
@submission.setter
|
||||
def submission(self, submission: praw.models.Submission):
|
||||
"""Update the :class:`.Submission` associated with the :class:`.Comment`."""
|
||||
submission._comments_by_id[self.fullname] = self
|
||||
self._submission = submission
|
||||
for reply in getattr(self, "replies", []):
|
||||
reply.submission = submission
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
id: str | None = None,
|
||||
url: str | None = None,
|
||||
_data: dict[str, Any] | None = None,
|
||||
):
|
||||
"""Initialize a :class:`.Comment` instance."""
|
||||
if (id, url, _data).count(None) != 2:
|
||||
msg = "Exactly one of 'id', 'url', or '_data' must be provided."
|
||||
raise TypeError(msg)
|
||||
fetched = False
|
||||
self._replies = []
|
||||
self._submission = None
|
||||
if id:
|
||||
self.id = id
|
||||
elif url:
|
||||
self.id = self.id_from_url(url)
|
||||
else:
|
||||
fetched = True
|
||||
super().__init__(reddit, _data=_data, _fetched=fetched)
|
||||
|
||||
def __setattr__(
|
||||
self,
|
||||
attribute: str,
|
||||
value: str | Redditor | CommentForest | praw.models.Subreddit,
|
||||
):
|
||||
"""Objectify author, replies, and subreddit."""
|
||||
if attribute == "author":
|
||||
value = Redditor.from_data(self._reddit, value)
|
||||
elif attribute == "replies":
|
||||
if value == "":
|
||||
value = []
|
||||
else:
|
||||
value = self._reddit._objector.objectify(value).children
|
||||
attribute = "_replies"
|
||||
elif attribute == "subreddit":
|
||||
value = self._reddit.subreddit(value)
|
||||
super().__setattr__(attribute, value)
|
||||
|
||||
def _extract_submission_id(self):
|
||||
if "context" in self.__dict__:
|
||||
return self.context.rsplit("/", 4)[1]
|
||||
return self.link_id.split("_", 1)[1]
|
||||
|
||||
def _fetch(self):
|
||||
data = self._fetch_data()
|
||||
data = data["data"]
|
||||
|
||||
if not data["children"]:
|
||||
msg = f"No data returned for comment {self.fullname}"
|
||||
raise ClientException(msg)
|
||||
|
||||
comment_data = data["children"][0]["data"]
|
||||
other = type(self)(self._reddit, _data=comment_data)
|
||||
self.__dict__.update(other.__dict__)
|
||||
super()._fetch()
|
||||
|
||||
def _fetch_info(self):
|
||||
return "info", {}, {"id": self.fullname}
|
||||
|
||||
def parent(
|
||||
self,
|
||||
) -> Comment | praw.models.Submission:
|
||||
"""Return the parent of the comment.
|
||||
|
||||
The returned parent will be an instance of either :class:`.Comment`, or
|
||||
:class:`.Submission`.
|
||||
|
||||
If this comment was obtained through a :class:`.Submission`, then its entire
|
||||
ancestry should be immediately available, requiring no extra network requests.
|
||||
However, if this comment was obtained through other means, e.g.,
|
||||
``reddit.comment("COMMENT_ID")``, or ``reddit.inbox.comment_replies``, then the
|
||||
returned parent may be a lazy instance of either :class:`.Comment`, or
|
||||
:class:`.Submission`.
|
||||
|
||||
Lazy comment example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment = reddit.comment("cklhv0f")
|
||||
parent = comment.parent()
|
||||
# 'replies' is empty until the comment is refreshed
|
||||
print(parent.replies) # Output: []
|
||||
parent.refresh()
|
||||
print(parent.replies) # Output is at least: [Comment(id="cklhv0f")]
|
||||
|
||||
.. warning::
|
||||
|
||||
Successive calls to :meth:`.parent` may result in a network request per call
|
||||
when the comment is not obtained through a :class:`.Submission`. See below
|
||||
for an example of how to minimize requests.
|
||||
|
||||
If you have a deeply nested comment and wish to most efficiently discover its
|
||||
top-most :class:`.Comment` ancestor you can chain successive calls to
|
||||
:meth:`.parent` with calls to :meth:`.refresh` at every 9 levels. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
ancestor = comment
|
||||
refresh_counter = 0
|
||||
while not ancestor.is_root:
|
||||
ancestor = ancestor.parent()
|
||||
if refresh_counter % 9 == 0:
|
||||
ancestor.refresh()
|
||||
refresh_counter += 1
|
||||
print(f"Top-most Ancestor: {ancestor}")
|
||||
|
||||
The above code should result in 5 network requests to Reddit. Without the calls
|
||||
to :meth:`.refresh` it would make at least 31 network requests.
|
||||
|
||||
"""
|
||||
if self.parent_id == self.submission.fullname:
|
||||
return self.submission
|
||||
|
||||
if self.parent_id in self.submission._comments_by_id:
|
||||
# The Comment already exists, so simply return it
|
||||
return self.submission._comments_by_id[self.parent_id]
|
||||
|
||||
parent = Comment(self._reddit, self.parent_id.split("_", 1)[1])
|
||||
parent._submission = self.submission
|
||||
return parent
|
||||
|
||||
def refresh(self) -> Comment:
|
||||
"""Refresh the comment's attributes.
|
||||
|
||||
If using :meth:`.Reddit.comment` this method must be called in order to obtain
|
||||
the comment's replies.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.refresh()
|
||||
|
||||
"""
|
||||
if "context" in self.__dict__: # Using hasattr triggers a fetch
|
||||
comment_path = self.context.split("?", 1)[0]
|
||||
else:
|
||||
path = API_PATH["submission"].format(id=self.submission.id)
|
||||
comment_path = f"{path}_/{self.id}"
|
||||
|
||||
# The context limit appears to be 8, but let's ask for more anyway.
|
||||
params = {"context": 100}
|
||||
if "reply_limit" in self.__dict__:
|
||||
params["limit"] = self.reply_limit
|
||||
if "reply_sort" in self.__dict__:
|
||||
params["sort"] = self.reply_sort
|
||||
comment_list = self._reddit.get(comment_path, params=params)[1].children
|
||||
if not comment_list:
|
||||
raise ClientException(self.MISSING_COMMENT_MESSAGE)
|
||||
|
||||
# With context, the comment may be nested so we have to find it
|
||||
comment = None
|
||||
queue = comment_list[:]
|
||||
while queue and (comment is None or comment.id != self.id):
|
||||
comment = queue.pop()
|
||||
if isinstance(comment, Comment):
|
||||
queue.extend(comment._replies)
|
||||
|
||||
if comment.id != self.id:
|
||||
raise ClientException(self.MISSING_COMMENT_MESSAGE)
|
||||
|
||||
if self._submission is not None:
|
||||
del comment.__dict__["_submission"] # Don't replace if set
|
||||
self.__dict__.update(comment.__dict__)
|
||||
|
||||
for reply in comment_list:
|
||||
reply.submission = self.submission
|
||||
return self
|
||||
|
||||
|
||||
class CommentModeration(ThingModerationMixin):
|
||||
"""Provide a set of functions pertaining to :class:`.Comment` moderation.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.mod.approve()
|
||||
|
||||
"""
|
||||
|
||||
REMOVAL_MESSAGE_API = "removal_comment_message"
|
||||
|
||||
def __init__(self, comment: praw.models.Comment):
|
||||
"""Initialize a :class:`.CommentModeration` instance.
|
||||
|
||||
:param comment: The comment to moderate.
|
||||
|
||||
"""
|
||||
self.thing = comment
|
||||
|
||||
def show(self):
|
||||
"""Uncollapse a :class:`.Comment` that has been collapsed by Crowd Control.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Uncollapse a comment:
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.mod.show()
|
||||
|
||||
"""
|
||||
url = API_PATH["show_comment"]
|
||||
|
||||
self.thing._reddit.post(url, data={"id": self.thing.fullname})
|
||||
308
backend/venv/Lib/site-packages/praw/models/reddit/draft.py
Normal file
308
backend/venv/Lib/site-packages/praw/models/reddit/draft.py
Normal file
@@ -0,0 +1,308 @@
|
||||
"""Provide the draft class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ...const import API_PATH
|
||||
from ...exceptions import ClientException
|
||||
from .base import RedditBase
|
||||
from .subreddit import Subreddit
|
||||
from .user_subreddit import UserSubreddit
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class Draft(RedditBase):
|
||||
"""A class that represents a Reddit submission draft.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
========================== ======================================================
|
||||
Attribute Description
|
||||
========================== ======================================================
|
||||
``link_flair_template_id`` The link flair's ID.
|
||||
``link_flair_text`` The link flair's text content, or ``None`` if not
|
||||
flaired.
|
||||
``modified`` Time the submission draft was modified, represented in
|
||||
`Unix Time`_.
|
||||
``original_content`` Whether the submission draft will be set as original
|
||||
content.
|
||||
``selftext`` The submission draft's selftext. ``None`` if a link
|
||||
submission draft.
|
||||
``spoiler`` Whether the submission will be marked as a spoiler.
|
||||
``subreddit`` Provides an instance of :class:`.Subreddit` or
|
||||
:class:`.UserSubreddit` (if set).
|
||||
``title`` The title of the submission draft.
|
||||
``url`` The URL the submission draft links to.
|
||||
========================== ======================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
STR_FIELD = "id"
|
||||
|
||||
@classmethod
|
||||
def _prepare_data(
|
||||
cls,
|
||||
*,
|
||||
flair_id: str | None = None,
|
||||
flair_text: str | None = None,
|
||||
is_public_link: bool | None = None,
|
||||
nsfw: bool | None = None,
|
||||
original_content: bool | None = None,
|
||||
selftext: str | None = None,
|
||||
send_replies: bool | None = None,
|
||||
spoiler: bool | None = None,
|
||||
subreddit: praw.models.Subreddit | praw.models.UserSubreddit | None = None,
|
||||
title: str | None = None,
|
||||
url: str | None = None,
|
||||
**draft_kwargs: Any,
|
||||
) -> dict[str, Any]:
|
||||
data = {
|
||||
"body": selftext or url,
|
||||
"flair_id": flair_id,
|
||||
"flair_text": flair_text,
|
||||
"is_public_link": is_public_link,
|
||||
"kind": "markdown" if selftext is not None else "link",
|
||||
"nsfw": nsfw,
|
||||
"original_content": original_content,
|
||||
"send_replies": send_replies,
|
||||
"spoiler": spoiler,
|
||||
"title": title,
|
||||
}
|
||||
if subreddit:
|
||||
data.update(
|
||||
{
|
||||
"subreddit": subreddit.fullname,
|
||||
"target": (
|
||||
"profile"
|
||||
if subreddit.display_name.startswith("u_")
|
||||
else "subreddit"
|
||||
),
|
||||
}
|
||||
)
|
||||
data.update(draft_kwargs)
|
||||
return data
|
||||
|
||||
def __init__(
|
||||
self, reddit: praw.Reddit, id: str | None = None, _data: dict[str, Any] = None
|
||||
):
|
||||
"""Initialize a :class:`.Draft` instance."""
|
||||
if (id, _data).count(None) != 1:
|
||||
msg = "Exactly one of 'id' or '_data' must be provided."
|
||||
raise TypeError(msg)
|
||||
fetched = False
|
||||
if id:
|
||||
self.id = id
|
||||
elif len(_data) > 1:
|
||||
if _data["kind"] == "markdown":
|
||||
_data["selftext"] = _data.pop("body")
|
||||
elif _data["kind"] == "link":
|
||||
_data["url"] = _data.pop("body")
|
||||
fetched = True
|
||||
super().__init__(reddit, _data=_data, _fetched=fetched)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return an object initialization representation of the instance."""
|
||||
if self._fetched:
|
||||
subreddit = (
|
||||
f" subreddit={self.subreddit.display_name!r}" if self.subreddit else ""
|
||||
)
|
||||
title = f" title={self.title!r}" if self.title else ""
|
||||
return f"{self.__class__.__name__}(id={self.id!r}{subreddit}{title})"
|
||||
return f"{self.__class__.__name__}(id={self.id!r})"
|
||||
|
||||
def _fetch(self):
|
||||
for draft in self._reddit.drafts():
|
||||
if draft.id == self.id:
|
||||
self.__dict__.update(draft.__dict__)
|
||||
super()._fetch()
|
||||
return
|
||||
msg = (
|
||||
f"The currently authenticated user not have a draft with an ID of {self.id}"
|
||||
)
|
||||
raise ClientException(msg)
|
||||
|
||||
def delete(self):
|
||||
"""Delete the :class:`.Draft`.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
draft = reddit.drafts("124862bc-e1e9-11eb-aa4f-e68667a77cbb")
|
||||
draft.delete()
|
||||
|
||||
"""
|
||||
self._reddit.delete(API_PATH["draft"], params={"draft_id": self.id})
|
||||
|
||||
def submit(
|
||||
self,
|
||||
*,
|
||||
flair_id: str | None = None,
|
||||
flair_text: str | None = None,
|
||||
nsfw: bool | None = None,
|
||||
selftext: str | None = None,
|
||||
spoiler: bool | None = None,
|
||||
subreddit: (
|
||||
str | praw.models.Subreddit | praw.models.UserSubreddit | None
|
||||
) = None,
|
||||
title: str | None = None,
|
||||
url: str | None = None,
|
||||
**submit_kwargs: Any,
|
||||
) -> praw.models.Submission:
|
||||
"""Submit a draft.
|
||||
|
||||
:param flair_id: The flair template to select (default: ``None``).
|
||||
:param flair_text: If the template's ``flair_text_editable`` value is ``True``,
|
||||
this value will set a custom text (default: ``None``). ``flair_id`` is
|
||||
required when ``flair_text`` is provided.
|
||||
:param nsfw: Whether or not the submission should be marked NSFW (default:
|
||||
``None``).
|
||||
:param selftext: The Markdown formatted content for a ``text`` submission. Use
|
||||
an empty string, ``""``, to make a title-only submission (default:
|
||||
``None``).
|
||||
:param spoiler: Whether or not the submission should be marked as a spoiler
|
||||
(default: ``None``).
|
||||
:param subreddit: The subreddit to submit the draft to. This accepts a subreddit
|
||||
display name, :class:`.Subreddit` object, or :class:`.UserSubreddit` object.
|
||||
:param title: The title of the submission (default: ``None``).
|
||||
:param url: The URL for a ``link`` submission (default: ``None``).
|
||||
|
||||
:returns: A :class:`.Submission` object for the newly created submission.
|
||||
|
||||
.. note::
|
||||
|
||||
Parameters set here will override their respective :class:`.Draft`
|
||||
attributes.
|
||||
|
||||
Additional keyword arguments are passed to the :meth:`.Subreddit.submit` method.
|
||||
|
||||
For example, to submit a draft as is:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
draft = reddit.drafts("5f87d55c-e4fb-11eb-8965-6aeb41b0880e")
|
||||
submission = draft.submit()
|
||||
|
||||
For example, to submit a draft but use a different title than what is set:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
draft = reddit.drafts("5f87d55c-e4fb-11eb-8965-6aeb41b0880e")
|
||||
submission = draft.submit(title="New Title")
|
||||
|
||||
.. seealso::
|
||||
|
||||
- :meth:`~.Subreddit.submit` to submit url posts and selftexts
|
||||
- :meth:`~.Subreddit.submit_gallery`. to submit more than one image in the
|
||||
same post
|
||||
- :meth:`~.Subreddit.submit_image` to submit images
|
||||
- :meth:`~.Subreddit.submit_poll` to submit polls
|
||||
- :meth:`~.Subreddit.submit_video` to submit videos and videogifs
|
||||
|
||||
"""
|
||||
submit_kwargs["draft_id"] = self.id
|
||||
if not (self.subreddit or subreddit):
|
||||
msg = "'subreddit' must be set on the Draft instance or passed as a keyword argument."
|
||||
raise ValueError(msg)
|
||||
for key, attribute in [
|
||||
("flair_id", flair_id),
|
||||
("flair_text", flair_text),
|
||||
("nsfw", nsfw),
|
||||
("selftext", selftext),
|
||||
("spoiler", spoiler),
|
||||
("title", title),
|
||||
("url", url),
|
||||
]:
|
||||
value = attribute or getattr(self, key, None)
|
||||
if value is not None:
|
||||
submit_kwargs[key] = value
|
||||
if isinstance(subreddit, str):
|
||||
_subreddit = self._reddit.subreddit(subreddit)
|
||||
elif isinstance(subreddit, (Subreddit, UserSubreddit)):
|
||||
_subreddit = subreddit
|
||||
else:
|
||||
_subreddit = self.subreddit
|
||||
return _subreddit.submit(**submit_kwargs)
|
||||
|
||||
def update(
|
||||
self,
|
||||
*,
|
||||
flair_id: str | None = None,
|
||||
flair_text: str | None = None,
|
||||
is_public_link: bool | None = None,
|
||||
nsfw: bool | None = None,
|
||||
original_content: bool | None = None,
|
||||
selftext: str | None = None,
|
||||
send_replies: bool | None = None,
|
||||
spoiler: bool | None = None,
|
||||
subreddit: (
|
||||
str | praw.models.Subreddit | praw.models.UserSubreddit | None
|
||||
) = None,
|
||||
title: str | None = None,
|
||||
url: str | None = None,
|
||||
**draft_kwargs: Any,
|
||||
):
|
||||
"""Update the :class:`.Draft`.
|
||||
|
||||
.. note::
|
||||
|
||||
Only provided values will be updated.
|
||||
|
||||
:param flair_id: The flair template to select.
|
||||
:param flair_text: If the template's ``flair_text_editable`` value is ``True``,
|
||||
this value will set a custom text. ``flair_id`` is required when
|
||||
``flair_text`` is provided.
|
||||
:param is_public_link: Whether to enable public viewing of the draft before it
|
||||
is submitted.
|
||||
:param nsfw: Whether the draft should be marked NSFW.
|
||||
:param original_content: Whether the submission should be marked as original
|
||||
content.
|
||||
:param selftext: The Markdown formatted content for a text submission draft. Use
|
||||
``None`` to make a title-only submission draft. ``selftext`` can not be
|
||||
provided if ``url`` is provided.
|
||||
:param send_replies: When ``True``, messages will be sent to the submission
|
||||
author when comments are made to the submission.
|
||||
:param spoiler: Whether the submission should be marked as a spoiler.
|
||||
:param subreddit: The subreddit to create the draft for. This accepts a
|
||||
subreddit display name, :class:`.Subreddit` object, or
|
||||
:class:`.UserSubreddit` object.
|
||||
:param title: The title of the draft.
|
||||
:param url: The URL for a ``link`` submission draft. ``url`` can not be provided
|
||||
if ``selftext`` is provided.
|
||||
|
||||
Additional keyword arguments can be provided to handle new parameters as Reddit
|
||||
introduces them.
|
||||
|
||||
For example, to update the title of a draft do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
draft = reddit.drafts("5f87d55c-e4fb-11eb-8965-6aeb41b0880e")
|
||||
draft.update(title="New title")
|
||||
|
||||
"""
|
||||
if isinstance(subreddit, str):
|
||||
subreddit = self._reddit.subreddit(subreddit)
|
||||
data = self._prepare_data(
|
||||
flair_id=flair_id,
|
||||
flair_text=flair_text,
|
||||
is_public_link=is_public_link,
|
||||
nsfw=nsfw,
|
||||
original_content=original_content,
|
||||
selftext=selftext,
|
||||
send_replies=send_replies,
|
||||
spoiler=spoiler,
|
||||
subreddit=subreddit,
|
||||
title=title,
|
||||
url=url,
|
||||
**draft_kwargs,
|
||||
)
|
||||
data["id"] = self.id
|
||||
_new_draft = self._reddit.put(API_PATH["draft"], data=data)
|
||||
_new_draft._fetch()
|
||||
self.__dict__.update(_new_draft.__dict__)
|
||||
246
backend/venv/Lib/site-packages/praw/models/reddit/emoji.py
Normal file
246
backend/venv/Lib/site-packages/praw/models/reddit/emoji.py
Normal file
@@ -0,0 +1,246 @@
|
||||
"""Provide the Emoji class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ...const import API_PATH
|
||||
from ...exceptions import ClientException
|
||||
from ...util import _deprecate_args
|
||||
from .base import RedditBase
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw
|
||||
|
||||
|
||||
class Emoji(RedditBase):
|
||||
"""An individual :class:`.Emoji` object.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
====================== =================================================
|
||||
Attribute Description
|
||||
====================== =================================================
|
||||
``mod_flair_only`` Whether the emoji is restricted for mod use only.
|
||||
``name`` The name of the emoji.
|
||||
``post_flair_allowed`` Whether the emoji may appear in post flair.
|
||||
``url`` The URL of the emoji image.
|
||||
``user_flair_allowed`` Whether the emoji may appear in user flair.
|
||||
====================== =================================================
|
||||
|
||||
"""
|
||||
|
||||
STR_FIELD = "name"
|
||||
|
||||
def __eq__(self, other: str | Emoji) -> bool:
|
||||
"""Return whether the other instance equals the current."""
|
||||
if isinstance(other, str):
|
||||
return other == str(self)
|
||||
if isinstance(other, self.__class__):
|
||||
return str(self) == str(other) and other.subreddit == self.subreddit
|
||||
return super().__eq__(other)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""Return the hash of the current instance."""
|
||||
return hash(self.__class__.__name__) ^ hash(str(self)) ^ hash(self.subreddit)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
subreddit: praw.models.Subreddit,
|
||||
name: str,
|
||||
_data: dict[str, Any] | None = None,
|
||||
):
|
||||
"""Initialize an :class:`.Emoji` instance."""
|
||||
self.name = name
|
||||
self.subreddit = subreddit
|
||||
super().__init__(reddit, _data=_data)
|
||||
|
||||
def _fetch(self):
|
||||
for emoji in self.subreddit.emoji:
|
||||
if emoji.name == self.name:
|
||||
self.__dict__.update(emoji.__dict__)
|
||||
super()._fetch()
|
||||
return
|
||||
msg = f"r/{self.subreddit} does not have the emoji {self.name}"
|
||||
raise ClientException(msg)
|
||||
|
||||
def delete(self):
|
||||
"""Delete an emoji from this subreddit by :class:`.Emoji`.
|
||||
|
||||
To delete ``"emoji"`` as an emoji on r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").emoji["emoji"].delete()
|
||||
|
||||
"""
|
||||
url = API_PATH["emoji_delete"].format(
|
||||
emoji_name=self.name, subreddit=self.subreddit
|
||||
)
|
||||
self._reddit.delete(url)
|
||||
|
||||
@_deprecate_args("mod_flair_only", "post_flair_allowed", "user_flair_allowed")
|
||||
def update(
|
||||
self,
|
||||
*,
|
||||
mod_flair_only: bool | None = None,
|
||||
post_flair_allowed: bool | None = None,
|
||||
user_flair_allowed: bool | None = None,
|
||||
):
|
||||
"""Update the permissions of an emoji in this subreddit.
|
||||
|
||||
:param mod_flair_only: Indicate whether the emoji is restricted to mod use only.
|
||||
Respects pre-existing settings if not provided.
|
||||
:param post_flair_allowed: Indicate whether the emoji may appear in post flair.
|
||||
Respects pre-existing settings if not provided.
|
||||
:param user_flair_allowed: Indicate whether the emoji may appear in user flair.
|
||||
Respects pre-existing settings if not provided.
|
||||
|
||||
.. note::
|
||||
|
||||
In order to retain pre-existing values for those that are not explicitly
|
||||
passed, a network request is issued. To avoid that network request,
|
||||
explicitly provide all values.
|
||||
|
||||
To restrict the emoji ``"emoji"`` in r/test to mod use only, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").emoji["emoji"].update(mod_flair_only=True)
|
||||
|
||||
"""
|
||||
locals_reference = locals()
|
||||
mapping = {
|
||||
attribute: locals_reference[attribute]
|
||||
for attribute in (
|
||||
"mod_flair_only",
|
||||
"post_flair_allowed",
|
||||
"user_flair_allowed",
|
||||
)
|
||||
}
|
||||
if all(value is None for value in mapping.values()):
|
||||
msg = "At least one attribute must be provided"
|
||||
raise TypeError(msg)
|
||||
|
||||
data = {"name": self.name}
|
||||
for attribute, value in mapping.items():
|
||||
if value is None:
|
||||
value = getattr(self, attribute) # noqa: PLW2901
|
||||
data[attribute] = value
|
||||
url = API_PATH["emoji_update"].format(subreddit=self.subreddit)
|
||||
self._reddit.post(url, data=data)
|
||||
for attribute, value in data.items():
|
||||
setattr(self, attribute, value)
|
||||
|
||||
|
||||
class SubredditEmoji:
|
||||
"""Provides a set of functions to a :class:`.Subreddit` for emoji."""
|
||||
|
||||
def __getitem__(self, name: str) -> Emoji:
|
||||
"""Lazily return the :class:`.Emoji` for the subreddit named ``name``.
|
||||
|
||||
:param name: The name of the emoji.
|
||||
|
||||
This method is to be used to fetch a specific emoji url, like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
emoji = reddit.subreddit("test").emoji["emoji"]
|
||||
print(emoji)
|
||||
|
||||
"""
|
||||
return Emoji(self._reddit, self.subreddit, name)
|
||||
|
||||
def __init__(self, subreddit: praw.models.Subreddit):
|
||||
"""Initialize a :class:`.SubredditEmoji` instance.
|
||||
|
||||
:param subreddit: The subreddit whose emoji are affected.
|
||||
|
||||
"""
|
||||
self.subreddit = subreddit
|
||||
self._reddit = subreddit._reddit
|
||||
|
||||
def __iter__(self) -> list[Emoji]:
|
||||
"""Return a list of :class:`.Emoji` for the subreddit.
|
||||
|
||||
This method is to be used to discover all emoji for a subreddit:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for emoji in reddit.subreddit("test").emoji:
|
||||
print(emoji)
|
||||
|
||||
"""
|
||||
response = self._reddit.get(
|
||||
API_PATH["emoji_list"].format(subreddit=self.subreddit)
|
||||
)
|
||||
subreddit_keys = [
|
||||
key
|
||||
for key in response
|
||||
if key.startswith(self._reddit.config.kinds["subreddit"])
|
||||
]
|
||||
assert len(subreddit_keys) == 1
|
||||
for emoji_name, emoji_data in response[subreddit_keys[0]].items():
|
||||
yield Emoji(self._reddit, self.subreddit, emoji_name, _data=emoji_data)
|
||||
|
||||
def add(
|
||||
self,
|
||||
*,
|
||||
image_path: str,
|
||||
mod_flair_only: bool | None = None,
|
||||
name: str,
|
||||
post_flair_allowed: bool | None = None,
|
||||
user_flair_allowed: bool | None = None,
|
||||
) -> Emoji:
|
||||
"""Add an emoji to this subreddit.
|
||||
|
||||
:param image_path: A path to a jpeg or png image.
|
||||
:param mod_flair_only: When provided, indicate whether the emoji is restricted
|
||||
to mod use only (default: ``None``).
|
||||
:param name: The name of the emoji.
|
||||
:param post_flair_allowed: When provided, indicate whether the emoji may appear
|
||||
in post flair (default: ``None``).
|
||||
:param user_flair_allowed: When provided, indicate whether the emoji may appear
|
||||
in user flair (default: ``None``).
|
||||
|
||||
:returns: The :class:`.Emoji` added.
|
||||
|
||||
To add ``"emoji"`` to r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").emoji.add(name="emoji", image_path="emoji.png")
|
||||
|
||||
"""
|
||||
file = Path(image_path)
|
||||
data = {
|
||||
"filepath": file.name,
|
||||
"mimetype": "image/jpeg",
|
||||
}
|
||||
if image_path.lower().endswith(".png"):
|
||||
data["mimetype"] = "image/png"
|
||||
url = API_PATH["emoji_lease"].format(subreddit=self.subreddit)
|
||||
|
||||
# until we learn otherwise, assume this request always succeeds
|
||||
upload_lease = self._reddit.post(url, data=data)["s3UploadLease"]
|
||||
upload_data = {item["name"]: item["value"] for item in upload_lease["fields"]}
|
||||
upload_url = f"https:{upload_lease['action']}"
|
||||
|
||||
with file.open("rb") as image:
|
||||
response = self._reddit._core._requestor._http.post(
|
||||
upload_url, data=upload_data, files={"file": image}
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
data = {
|
||||
"mod_flair_only": mod_flair_only,
|
||||
"name": name,
|
||||
"post_flair_allowed": post_flair_allowed,
|
||||
"s3_key": upload_data["key"],
|
||||
"user_flair_allowed": user_flair_allowed,
|
||||
}
|
||||
url = API_PATH["emoji_upload"].format(subreddit=self.subreddit)
|
||||
self._reddit.post(url, data=data)
|
||||
return Emoji(self._reddit, self.subreddit, name)
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Provide classes related to inline media."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ..util import _deprecate_args
|
||||
|
||||
|
||||
class InlineMedia:
|
||||
"""Provides a way to embed media in self posts."""
|
||||
|
||||
TYPE = None
|
||||
|
||||
def __eq__(self, other: InlineMedia) -> bool:
|
||||
"""Return whether the other instance equals the current."""
|
||||
return all(
|
||||
getattr(self, attr) == getattr(other, attr)
|
||||
for attr in ["TYPE", "path", "caption", "media_id"]
|
||||
)
|
||||
|
||||
@_deprecate_args("path", "caption")
|
||||
def __init__(self, *, caption: str = None, path: str):
|
||||
"""Initialize an :class:`.InlineMedia` instance.
|
||||
|
||||
:param caption: An optional caption to add to the image (default: ``None``).
|
||||
:param path: The path to a media file.
|
||||
|
||||
"""
|
||||
self.path = path
|
||||
self.caption = caption
|
||||
self.media_id = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return an object initialization representation of the instance."""
|
||||
return f"<{self.__class__.__name__} caption={self.caption!r}>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return a string representation of the media in Markdown format."""
|
||||
return f'\n\n\n\n'
|
||||
|
||||
|
||||
class InlineGif(InlineMedia):
|
||||
"""Class to provide a gif to embed in text."""
|
||||
|
||||
TYPE = "gif"
|
||||
|
||||
|
||||
class InlineImage(InlineMedia):
|
||||
"""Class to provide am image to embed in text."""
|
||||
|
||||
TYPE = "img"
|
||||
|
||||
|
||||
class InlineVideo(InlineMedia):
|
||||
"""Class to provide a video to embed in text."""
|
||||
|
||||
TYPE = "video"
|
||||
804
backend/venv/Lib/site-packages/praw/models/reddit/live.py
Normal file
804
backend/venv/Lib/site-packages/praw/models/reddit/live.py
Normal file
@@ -0,0 +1,804 @@
|
||||
"""Provide the LiveThread class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Iterable, Iterator
|
||||
|
||||
from ...const import API_PATH
|
||||
from ...util import _deprecate_args
|
||||
from ...util.cache import cachedproperty
|
||||
from ..list.redditor import RedditorList
|
||||
from ..listing.generator import ListingGenerator
|
||||
from ..util import stream_generator
|
||||
from .base import RedditBase
|
||||
from .mixins import FullnameMixin
|
||||
from .redditor import Redditor
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class LiveContributorRelationship:
|
||||
"""Provide methods to interact with live threads' contributors."""
|
||||
|
||||
@staticmethod
|
||||
def _handle_permissions(permissions: Iterable[str]) -> str:
|
||||
permissions = {"all"} if permissions is None else set(permissions)
|
||||
return ",".join(f"+{x}" for x in permissions)
|
||||
|
||||
def __call__(self) -> list[praw.models.Redditor]:
|
||||
"""Return a :class:`.RedditorList` for live threads' contributors.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
for contributor in thread.contributor():
|
||||
print(contributor)
|
||||
|
||||
"""
|
||||
url = API_PATH["live_contributors"].format(id=self.thread.id)
|
||||
temp = self.thread._reddit.get(url)
|
||||
return temp if isinstance(temp, RedditorList) else temp[0]
|
||||
|
||||
def __init__(self, thread: praw.models.LiveThread):
|
||||
"""Initialize a :class:`.LiveContributorRelationship` instance.
|
||||
|
||||
:param thread: An instance of :class:`.LiveThread`.
|
||||
|
||||
.. note::
|
||||
|
||||
This class should not be initialized directly. Instead, obtain an instance
|
||||
via: :meth:`.LiveThread.contributor`.
|
||||
|
||||
"""
|
||||
self.thread = thread
|
||||
|
||||
def accept_invite(self):
|
||||
"""Accept an invite to contribute the live thread.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ydwwxneu7vsa")
|
||||
thread.contributor.accept_invite()
|
||||
|
||||
"""
|
||||
url = API_PATH["live_accept_invite"].format(id=self.thread.id)
|
||||
self.thread._reddit.post(url)
|
||||
|
||||
@_deprecate_args("redditor", "permissions")
|
||||
def invite(
|
||||
self,
|
||||
redditor: str | praw.models.Redditor,
|
||||
*,
|
||||
permissions: list[str] | None = None,
|
||||
):
|
||||
"""Invite a redditor to be a contributor of the live thread.
|
||||
|
||||
:param redditor: A redditor name or :class:`.Redditor` instance.
|
||||
:param permissions: When provided (not ``None``), permissions should be a list
|
||||
of strings specifying which subset of permissions to grant. An empty list
|
||||
``[]`` indicates no permissions, and when not provided (``None``), indicates
|
||||
full permissions.
|
||||
|
||||
:raises: :class:`.RedditAPIException` if the invitation already exists.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
redditor = reddit.redditor("spez")
|
||||
|
||||
# "manage" and "settings" permissions
|
||||
thread.contributor.invite(redditor, permissions=["manage", "settings"])
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.LiveContributorRelationship.remove_invite` to remove the invite for
|
||||
redditor.
|
||||
|
||||
"""
|
||||
url = API_PATH["live_invite"].format(id=self.thread.id)
|
||||
data = {
|
||||
"name": str(redditor),
|
||||
"type": "liveupdate_contributor_invite",
|
||||
"permissions": self._handle_permissions(permissions),
|
||||
}
|
||||
self.thread._reddit.post(url, data=data)
|
||||
|
||||
def leave(self):
|
||||
"""Abdicate the live thread contributor position (use with care).
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ydwwxneu7vsa")
|
||||
thread.contributor.leave()
|
||||
|
||||
"""
|
||||
url = API_PATH["live_leave"].format(id=self.thread.id)
|
||||
self.thread._reddit.post(url)
|
||||
|
||||
def remove(self, redditor: str | praw.models.Redditor):
|
||||
"""Remove the redditor from the live thread contributors.
|
||||
|
||||
:param redditor: A redditor fullname (e.g., ``"t2_1w72"``) or :class:`.Redditor`
|
||||
instance.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
redditor = reddit.redditor("spez")
|
||||
thread.contributor.remove(redditor)
|
||||
thread.contributor.remove("t2_1w72") # with fullname
|
||||
|
||||
"""
|
||||
fullname = redditor.fullname if isinstance(redditor, Redditor) else redditor
|
||||
data = {"id": fullname}
|
||||
url = API_PATH["live_remove_contrib"].format(id=self.thread.id)
|
||||
self.thread._reddit.post(url, data=data)
|
||||
|
||||
def remove_invite(self, redditor: str | praw.models.Redditor):
|
||||
"""Remove the invite for redditor.
|
||||
|
||||
:param redditor: A redditor fullname (e.g., ``"t2_1w72"``) or :class:`.Redditor`
|
||||
instance.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
redditor = reddit.redditor("spez")
|
||||
thread.contributor.remove_invite(redditor)
|
||||
thread.contributor.remove_invite("t2_1w72") # with fullname
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.LiveContributorRelationship.invite` to invite a redditor to be a
|
||||
contributor of the live thread.
|
||||
|
||||
"""
|
||||
fullname = redditor.fullname if isinstance(redditor, Redditor) else redditor
|
||||
data = {"id": fullname}
|
||||
url = API_PATH["live_remove_invite"].format(id=self.thread.id)
|
||||
self.thread._reddit.post(url, data=data)
|
||||
|
||||
@_deprecate_args("redditor", "permissions")
|
||||
def update(
|
||||
self,
|
||||
redditor: str | praw.models.Redditor,
|
||||
*,
|
||||
permissions: list[str] | None = None,
|
||||
):
|
||||
"""Update the contributor permissions for ``redditor``.
|
||||
|
||||
:param redditor: A redditor name or :class:`.Redditor` instance.
|
||||
:param permissions: When provided (not ``None``), permissions should be a list
|
||||
of strings specifying which subset of permissions to grant (other
|
||||
permissions are removed). An empty list ``[]`` indicates no permissions, and
|
||||
when not provided (``None``), indicates full permissions.
|
||||
|
||||
For example, to grant all permissions to the contributor, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
thread.contributor.update("spez")
|
||||
|
||||
To grant ``"access"`` and ``"edit"`` permissions (and to remove other
|
||||
permissions), try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread.contributor.update("spez", permissions=["access", "edit"])
|
||||
|
||||
To remove all permissions from the contributor, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit.moderator.update("spez", permissions=[])
|
||||
|
||||
"""
|
||||
url = API_PATH["live_update_perms"].format(id=self.thread.id)
|
||||
data = {
|
||||
"name": str(redditor),
|
||||
"type": "liveupdate_contributor",
|
||||
"permissions": self._handle_permissions(permissions),
|
||||
}
|
||||
self.thread._reddit.post(url, data=data)
|
||||
|
||||
@_deprecate_args("redditor", "permissions")
|
||||
def update_invite(
|
||||
self,
|
||||
redditor: str | praw.models.Redditor,
|
||||
*,
|
||||
permissions: list[str] | None = None,
|
||||
):
|
||||
"""Update the contributor invite permissions for ``redditor``.
|
||||
|
||||
:param redditor: A redditor name or :class:`.Redditor` instance.
|
||||
:param permissions: When provided (not ``None``), permissions should be a list
|
||||
of strings specifying which subset of permissions to grant (other
|
||||
permissions are removed). An empty list ``[]`` indicates no permissions, and
|
||||
when not provided (``None``), indicates full permissions.
|
||||
|
||||
For example, to set all permissions to the invitation, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
thread.contributor.update_invite("spez")
|
||||
|
||||
To set ``"access"`` and ``"edit"`` permissions (and to remove other permissions)
|
||||
to the invitation, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread.contributor.update_invite("spez", permissions=["access", "edit"])
|
||||
|
||||
To remove all permissions from the invitation, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread.contributor.update_invite("spez", permissions=[])
|
||||
|
||||
"""
|
||||
url = API_PATH["live_update_perms"].format(id=self.thread.id)
|
||||
data = {
|
||||
"name": str(redditor),
|
||||
"type": "liveupdate_contributor_invite",
|
||||
"permissions": self._handle_permissions(permissions),
|
||||
}
|
||||
self.thread._reddit.post(url, data=data)
|
||||
|
||||
|
||||
class LiveThread(RedditBase):
|
||||
"""An individual :class:`.LiveThread` object.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
==================== =========================================================
|
||||
Attribute Description
|
||||
==================== =========================================================
|
||||
``created_utc`` The creation time of the live thread, in `Unix Time`_.
|
||||
``description`` Description of the live thread, as Markdown.
|
||||
``description_html`` Description of the live thread, as HTML.
|
||||
``id`` The ID of the live thread.
|
||||
``nsfw`` A ``bool`` representing whether or not the live thread is
|
||||
marked as NSFW.
|
||||
==================== =========================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
STR_FIELD = "id"
|
||||
|
||||
@cachedproperty
|
||||
def contrib(self) -> praw.models.reddit.live.LiveThreadContribution:
|
||||
"""Provide an instance of :class:`.LiveThreadContribution`.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
thread.contrib.add("### update")
|
||||
|
||||
"""
|
||||
return LiveThreadContribution(self)
|
||||
|
||||
@cachedproperty
|
||||
def contributor(self) -> praw.models.reddit.live.LiveContributorRelationship:
|
||||
"""Provide an instance of :class:`.LiveContributorRelationship`.
|
||||
|
||||
You can call the instance to get a list of contributors which is represented as
|
||||
:class:`.RedditorList` instance consists of :class:`.Redditor` instances. Those
|
||||
:class:`.Redditor` instances have ``permissions`` attributes as contributors:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
for contributor in thread.contributor():
|
||||
# prints `Redditor(name="Acidtwist") ["all"]`
|
||||
print(contributor, contributor.permissions)
|
||||
|
||||
"""
|
||||
return LiveContributorRelationship(self)
|
||||
|
||||
@cachedproperty
|
||||
def stream(self) -> praw.models.reddit.live.LiveThreadStream:
|
||||
"""Provide an instance of :class:`.LiveThreadStream`.
|
||||
|
||||
Streams are used to indefinitely retrieve new updates made to a live thread,
|
||||
like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for live_update in reddit.live("ta535s1hq2je").stream.updates():
|
||||
print(live_update.body)
|
||||
|
||||
Updates are yielded oldest first as :class:`.LiveUpdate`. Up to 100 historical
|
||||
updates will initially be returned. To only retrieve new updates starting from
|
||||
when the stream is created, pass ``skip_existing=True``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
live_thread = reddit.live("ta535s1hq2je")
|
||||
for live_update in live_thread.stream.updates(skip_existing=True):
|
||||
print(live_update.author)
|
||||
|
||||
"""
|
||||
return LiveThreadStream(self)
|
||||
|
||||
def __eq__(self, other: str | praw.models.LiveThread) -> bool:
|
||||
"""Return whether the other instance equals the current.
|
||||
|
||||
.. note::
|
||||
|
||||
This comparison is case sensitive.
|
||||
|
||||
"""
|
||||
if isinstance(other, str):
|
||||
return other == str(self)
|
||||
return isinstance(other, self.__class__) and str(self) == str(other)
|
||||
|
||||
def __getitem__(self, update_id: str) -> praw.models.LiveUpdate:
|
||||
"""Return a lazy :class:`.LiveUpdate` instance.
|
||||
|
||||
:param update_id: A live update ID, e.g.,
|
||||
``"7827987a-c998-11e4-a0b9-22000b6a88d2"``.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
update = thread["7827987a-c998-11e4-a0b9-22000b6a88d2"]
|
||||
update.thread # LiveThread(id="ukaeu1ik4sw5")
|
||||
update.id # "7827987a-c998-11e4-a0b9-22000b6a88d2"
|
||||
update.author # "umbrae"
|
||||
|
||||
"""
|
||||
return LiveUpdate(self._reddit, self.id, update_id)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""Return the hash of the current instance."""
|
||||
return hash(self.__class__.__name__) ^ hash(str(self))
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
id: str | None = None,
|
||||
_data: dict[str, Any] | None = None,
|
||||
):
|
||||
"""Initialize a :class:`.LiveThread` instance.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
:param id: A live thread ID, e.g., ``"ukaeu1ik4sw5"``
|
||||
|
||||
"""
|
||||
if (id, _data).count(None) != 1:
|
||||
msg = "Either 'id' or '_data' must be provided."
|
||||
raise TypeError(msg)
|
||||
if id:
|
||||
self.id = id
|
||||
super().__init__(reddit, _data=_data)
|
||||
|
||||
def _fetch(self):
|
||||
data = self._fetch_data()
|
||||
data = data["data"]
|
||||
other = type(self)(self._reddit, _data=data)
|
||||
self.__dict__.update(other.__dict__)
|
||||
super()._fetch()
|
||||
|
||||
def _fetch_info(self):
|
||||
return "liveabout", {"id": self.id}, None
|
||||
|
||||
def discussions(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Submission]:
|
||||
"""Get submissions linking to the thread.
|
||||
|
||||
:param generator_kwargs: keyword arguments passed to :class:`.ListingGenerator`
|
||||
constructor.
|
||||
|
||||
:returns: A :class:`.ListingGenerator` object which yields :class:`.Submission`
|
||||
objects.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
for submission in thread.discussions(limit=None):
|
||||
print(submission.title)
|
||||
|
||||
"""
|
||||
url = API_PATH["live_discussions"].format(id=self.id)
|
||||
return ListingGenerator(self._reddit, url, **generator_kwargs)
|
||||
|
||||
def report(self, type: str):
|
||||
"""Report the thread violating the Reddit rules.
|
||||
|
||||
:param type: One of ``"spam"``, ``"vote-manipulation"``,
|
||||
``"personal-information"``, ``"sexualizing-minors"``, or
|
||||
``"site-breaking"``.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("xyu8kmjvfrww")
|
||||
thread.report("spam")
|
||||
|
||||
"""
|
||||
url = API_PATH["live_report"].format(id=self.id)
|
||||
self._reddit.post(url, data={"type": type})
|
||||
|
||||
def updates(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.LiveUpdate]:
|
||||
"""Return a :class:`.ListingGenerator` yields :class:`.LiveUpdate` s.
|
||||
|
||||
:param generator_kwargs: keyword arguments passed to :class:`.ListingGenerator`
|
||||
constructor.
|
||||
|
||||
:returns: A :class:`.ListingGenerator` object which yields :class:`.LiveUpdate`
|
||||
objects.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
after = "LiveUpdate_fefb3dae-7534-11e6-b259-0ef8c7233633"
|
||||
for submission in thread.updates(limit=5, params={"after": after}):
|
||||
print(submission.body)
|
||||
|
||||
"""
|
||||
url = API_PATH["live_updates"].format(id=self.id)
|
||||
for update in ListingGenerator(self._reddit, url, **generator_kwargs):
|
||||
update._thread = self
|
||||
yield update
|
||||
|
||||
|
||||
class LiveThreadContribution:
|
||||
"""Provides a set of contribution functions to a :class:`.LiveThread`."""
|
||||
|
||||
def __init__(self, thread: praw.models.LiveThread):
|
||||
"""Initialize a :class:`.LiveThreadContribution` instance.
|
||||
|
||||
:param thread: An instance of :class:`.LiveThread`.
|
||||
|
||||
This instance can be retrieved through ``thread.contrib`` where thread is a
|
||||
:class:`.LiveThread` instance. E.g.,
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
thread.contrib.add("### update")
|
||||
|
||||
"""
|
||||
self.thread = thread
|
||||
|
||||
def add(self, body: str):
|
||||
"""Add an update to the live thread.
|
||||
|
||||
:param body: The Markdown formatted content for the update.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ydwwxneu7vsa")
|
||||
thread.contrib.add("test `LiveThreadContribution.add()`")
|
||||
|
||||
"""
|
||||
url = API_PATH["live_add_update"].format(id=self.thread.id)
|
||||
self.thread._reddit.post(url, data={"body": body})
|
||||
|
||||
def close(self):
|
||||
"""Close the live thread permanently (cannot be undone).
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
thread.contrib.close()
|
||||
|
||||
"""
|
||||
url = API_PATH["live_close"].format(id=self.thread.id)
|
||||
self.thread._reddit.post(url)
|
||||
|
||||
@_deprecate_args("title", "description", "nsfw", "resources")
|
||||
def update(
|
||||
self,
|
||||
*,
|
||||
description: str | None = None,
|
||||
nsfw: bool | None = None,
|
||||
resources: str | None = None,
|
||||
title: str | None = None,
|
||||
**other_settings: str | None,
|
||||
):
|
||||
"""Update settings of the live thread.
|
||||
|
||||
:param description: The live thread's description (default: ``None``).
|
||||
:param nsfw: Indicate whether this thread is not safe for work (default:
|
||||
``None``).
|
||||
:param resources: Markdown formatted information that is useful for the live
|
||||
thread (default: ``None``).
|
||||
:param title: The title of the live thread (default: ``None``).
|
||||
|
||||
Does nothing if no arguments are provided.
|
||||
|
||||
Each setting will maintain its current value if ``None`` is specified.
|
||||
|
||||
Additional keyword arguments can be provided to handle new settings as Reddit
|
||||
introduces them.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("xyu8kmjvfrww")
|
||||
|
||||
# update 'title' and 'nsfw'
|
||||
updated_thread = thread.contrib.update(title=new_title, nsfw=True)
|
||||
|
||||
If Reddit introduces new settings, you must specify ``None`` for the setting you
|
||||
want to maintain:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# update 'nsfw' and maintain new setting 'foo'
|
||||
thread.contrib.update(nsfw=True, foo=None)
|
||||
|
||||
"""
|
||||
settings = {
|
||||
"title": title,
|
||||
"description": description,
|
||||
"nsfw": nsfw,
|
||||
"resources": resources,
|
||||
}
|
||||
settings.update(other_settings)
|
||||
if all(value is None for value in settings.values()):
|
||||
return
|
||||
# get settings from Reddit (not cache)
|
||||
thread = LiveThread(self.thread._reddit, self.thread.id)
|
||||
data = {
|
||||
key: getattr(thread, key) if value is None else value
|
||||
for key, value in settings.items()
|
||||
}
|
||||
|
||||
url = API_PATH["live_update_thread"].format(id=self.thread.id)
|
||||
# prawcore (0.7.0) Session.request() modifies `data` kwarg
|
||||
self.thread._reddit.post(url, data=data.copy())
|
||||
self.thread._reset_attributes(*data.keys())
|
||||
|
||||
|
||||
class LiveThreadStream:
|
||||
"""Provides a :class:`.LiveThread` stream.
|
||||
|
||||
Usually used via:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for live_update in reddit.live("ta535s1hq2je").stream.updates():
|
||||
print(live_update.body)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, live_thread: praw.models.LiveThread):
|
||||
"""Initialize a :class:`.LiveThreadStream` instance.
|
||||
|
||||
:param live_thread: The live thread associated with the stream.
|
||||
|
||||
"""
|
||||
self.live_thread = live_thread
|
||||
|
||||
def updates(
|
||||
self, **stream_options: dict[str, Any]
|
||||
) -> Iterator[praw.models.LiveUpdate]:
|
||||
"""Yield new updates to the live thread as they become available.
|
||||
|
||||
:param skip_existing: Set to ``True`` to only fetch items created after the
|
||||
stream (default: ``False``).
|
||||
|
||||
As with :meth:`.LiveThread.updates()`, updates are yielded as
|
||||
:class:`.LiveUpdate`.
|
||||
|
||||
Updates are yielded oldest first. Up to 100 historical updates will initially be
|
||||
returned.
|
||||
|
||||
Keyword arguments are passed to :func:`.stream_generator`.
|
||||
|
||||
For example, to retrieve all new updates made to the ``"ta535s1hq2je"`` live
|
||||
thread, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for live_update in reddit.live("ta535s1hq2je").stream.updates():
|
||||
print(live_update.body)
|
||||
|
||||
To only retrieve new updates starting from when the stream is created, pass
|
||||
``skip_existing=True``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
live_thread = reddit.live("ta535s1hq2je")
|
||||
for live_update in live_thread.stream.updates(skip_existing=True):
|
||||
print(live_update.author)
|
||||
|
||||
"""
|
||||
return stream_generator(self.live_thread.updates, **stream_options)
|
||||
|
||||
|
||||
class LiveUpdateContribution:
|
||||
"""Provides a set of contribution functions to :class:`.LiveUpdate`."""
|
||||
|
||||
def __init__(self, update: praw.models.LiveUpdate):
|
||||
"""Initialize a :class:`.LiveUpdateContribution` instance.
|
||||
|
||||
:param update: An instance of :class:`.LiveUpdate`.
|
||||
|
||||
This instance can be retrieved through ``update.contrib`` where update is a
|
||||
:class:`.LiveUpdate` instance. E.g.,
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
update = thread["7827987a-c998-11e4-a0b9-22000b6a88d2"]
|
||||
update.contrib # LiveUpdateContribution instance
|
||||
update.contrib.remove()
|
||||
|
||||
"""
|
||||
self.update = update
|
||||
|
||||
def remove(self):
|
||||
"""Remove a live update.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ydwwxneu7vsa")
|
||||
update = thread["6854605a-efec-11e6-b0c7-0eafac4ff094"]
|
||||
update.contrib.remove()
|
||||
|
||||
"""
|
||||
url = API_PATH["live_remove_update"].format(id=self.update.thread.id)
|
||||
data = {"id": self.update.fullname}
|
||||
self.update.thread._reddit.post(url, data=data)
|
||||
|
||||
def strike(self):
|
||||
"""Strike a content of a live update.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("xyu8kmjvfrww")
|
||||
update = thread["cb5fe532-dbee-11e6-9a91-0e6d74fabcc4"]
|
||||
update.contrib.strike()
|
||||
|
||||
To check whether the update is stricken or not, use ``update.stricken``
|
||||
attribute.
|
||||
|
||||
.. note::
|
||||
|
||||
Accessing lazy attributes on updates (includes ``update.stricken``) may
|
||||
raise :py:class:`AttributeError`. See :class:`.LiveUpdate` for details.
|
||||
|
||||
"""
|
||||
url = API_PATH["live_strike"].format(id=self.update.thread.id)
|
||||
data = {"id": self.update.fullname}
|
||||
self.update.thread._reddit.post(url, data=data)
|
||||
|
||||
|
||||
class LiveUpdate(FullnameMixin, RedditBase):
|
||||
"""An individual :class:`.LiveUpdate` object.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
=============== ===================================================================
|
||||
Attribute Description
|
||||
=============== ===================================================================
|
||||
``author`` The :class:`.Redditor` who made the update.
|
||||
``body`` Body of the update, as Markdown.
|
||||
``body_html`` Body of the update, as HTML.
|
||||
``created_utc`` The time the update was created, as `Unix Time`_.
|
||||
``stricken`` A ``bool`` representing whether or not the update was stricken (see
|
||||
:meth:`.strike`).
|
||||
=============== ===================================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
STR_FIELD = "id"
|
||||
_kind = "LiveUpdate"
|
||||
|
||||
@cachedproperty
|
||||
def contrib(self) -> praw.models.reddit.live.LiveUpdateContribution:
|
||||
"""Provide an instance of :class:`.LiveUpdateContribution`.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
update = thread["7827987a-c998-11e4-a0b9-22000b6a88d2"]
|
||||
update.contrib # LiveUpdateContribution instance
|
||||
|
||||
"""
|
||||
return LiveUpdateContribution(self)
|
||||
|
||||
@property
|
||||
def thread(self) -> LiveThread:
|
||||
"""Return :class:`.LiveThread` object the update object belongs to."""
|
||||
return self._thread
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
thread_id: str | None = None,
|
||||
update_id: str | None = None,
|
||||
_data: dict[str, Any] | None = None,
|
||||
):
|
||||
"""Initialize a :class:`.LiveUpdate` instance.
|
||||
|
||||
Either ``thread_id`` and ``update_id``, or ``_data`` must be provided.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
:param thread_id: A live thread ID, e.g., ``"ukaeu1ik4sw5"``.
|
||||
:param update_id: A live update ID, e.g.,
|
||||
``"7827987a-c998-11e4-a0b9-22000b6a88d2"``.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
update = LiveUpdate(reddit, "ukaeu1ik4sw5", "7827987a-c998-11e4-a0b9-22000b6a88d2")
|
||||
update.thread # LiveThread(id="ukaeu1ik4sw5")
|
||||
update.id # "7827987a-c998-11e4-a0b9-22000b6a88d2"
|
||||
update.author # "umbrae"
|
||||
|
||||
"""
|
||||
if _data is not None:
|
||||
# Since _data (part of JSON returned from reddit) have no thread ID,
|
||||
# self._thread must be set by the caller of LiveUpdate(). See the code of
|
||||
# LiveThread.updates() for example.
|
||||
super().__init__(reddit, _data=_data, _fetched=True)
|
||||
elif thread_id and update_id:
|
||||
self.id = update_id
|
||||
super().__init__(reddit, _data=None)
|
||||
self._thread = LiveThread(self._reddit, thread_id)
|
||||
else:
|
||||
msg = "Either 'thread_id' and 'update_id', or '_data' must be provided."
|
||||
raise TypeError(msg)
|
||||
|
||||
def __setattr__(self, attribute: str, value: Any):
|
||||
"""Objectify author."""
|
||||
if attribute == "author":
|
||||
value = Redditor(self._reddit, name=value)
|
||||
super().__setattr__(attribute, value)
|
||||
|
||||
def _fetch(self):
|
||||
url = API_PATH["live_focus"].format(thread_id=self.thread.id, update_id=self.id)
|
||||
other = self._reddit.get(url)[0]
|
||||
self.__dict__.update(other.__dict__)
|
||||
super()._fetch()
|
||||
175
backend/venv/Lib/site-packages/praw/models/reddit/message.py
Normal file
175
backend/venv/Lib/site-packages/praw/models/reddit/message.py
Normal file
@@ -0,0 +1,175 @@
|
||||
"""Provide the Message class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ...const import API_PATH
|
||||
from .base import RedditBase
|
||||
from .mixins import FullnameMixin, InboxableMixin, ReplyableMixin
|
||||
from .redditor import Redditor
|
||||
from .subreddit import Subreddit
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class Message(InboxableMixin, ReplyableMixin, FullnameMixin, RedditBase):
|
||||
"""A class for private messages.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
=============== ================================================================
|
||||
Attribute Description
|
||||
=============== ================================================================
|
||||
``author`` Provides an instance of :class:`.Redditor`.
|
||||
``body`` The body of the message, as Markdown.
|
||||
``body_html`` The body of the message, as HTML.
|
||||
``created_utc`` Time the message was created, represented in `Unix Time`_.
|
||||
``dest`` Provides an instance of :class:`.Redditor`. The recipient of the
|
||||
message.
|
||||
``id`` The ID of the message.
|
||||
``name`` The full ID of the message, prefixed with ``t4_``.
|
||||
``subject`` The subject of the message.
|
||||
``was_comment`` Whether or not the message was a comment reply.
|
||||
=============== ================================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
STR_FIELD = "id"
|
||||
|
||||
@classmethod
|
||||
def parse(
|
||||
cls, data: dict[str, Any], reddit: praw.Reddit
|
||||
) -> Message | SubredditMessage:
|
||||
"""Return an instance of :class:`.Message` or :class:`.SubredditMessage` from ``data``.
|
||||
|
||||
:param data: The structured data.
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
|
||||
"""
|
||||
if data["author"]:
|
||||
data["author"] = Redditor(reddit, data["author"])
|
||||
|
||||
if data["dest"].startswith("#"):
|
||||
data["dest"] = Subreddit(reddit, data["dest"][1:])
|
||||
else:
|
||||
data["dest"] = Redditor(reddit, data["dest"])
|
||||
|
||||
if data["replies"]:
|
||||
replies = data["replies"]
|
||||
data["replies"] = reddit._objector.objectify(replies["data"]["children"])
|
||||
else:
|
||||
data["replies"] = []
|
||||
|
||||
if data["subreddit"]:
|
||||
data["subreddit"] = Subreddit(reddit, data["subreddit"])
|
||||
return SubredditMessage(reddit, _data=data)
|
||||
|
||||
return cls(reddit, _data=data)
|
||||
|
||||
@property
|
||||
def _kind(self) -> str:
|
||||
"""Return the class's kind."""
|
||||
return self._reddit.config.kinds["message"]
|
||||
|
||||
@property
|
||||
def parent(self) -> praw.models.Message | None:
|
||||
"""Return the parent of the message if it exists."""
|
||||
if not self._parent and self.parent_id:
|
||||
self._parent = self._reddit.inbox.message(self.parent_id.split("_")[1])
|
||||
return self._parent
|
||||
|
||||
@parent.setter
|
||||
def parent(self, value: praw.models.Message | None):
|
||||
self._parent = value
|
||||
|
||||
def __init__(self, reddit: praw.Reddit, _data: dict[str, Any]):
|
||||
"""Initialize a :class:`.Message` instance."""
|
||||
super().__init__(reddit, _data=_data, _fetched=True)
|
||||
self._parent = None
|
||||
for reply in _data.get("replies", []):
|
||||
if reply.parent_id == self.fullname:
|
||||
reply.parent = self
|
||||
|
||||
def delete(self):
|
||||
"""Delete the message.
|
||||
|
||||
.. note::
|
||||
|
||||
Reddit does not return an indication of whether or not the message was
|
||||
successfully deleted.
|
||||
|
||||
For example, to delete the most recent message in your inbox:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
next(reddit.inbox.all()).delete()
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["delete_message"], data={"id": self.fullname})
|
||||
|
||||
|
||||
class SubredditMessage(Message):
|
||||
"""A class for messages to a subreddit.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
=============== =================================================================
|
||||
Attribute Description
|
||||
=============== =================================================================
|
||||
``author`` Provides an instance of :class:`.Redditor`.
|
||||
``body`` The body of the message, as Markdown.
|
||||
``body_html`` The body of the message, as HTML.
|
||||
``created_utc`` Time the message was created, represented in `Unix Time`_.
|
||||
``dest`` Provides an instance of :class:`.Redditor`. The recipient of the
|
||||
message.
|
||||
``id`` The ID of the message.
|
||||
``name`` The full ID of the message, prefixed with ``t4_``.
|
||||
``subject`` The subject of the message.
|
||||
``subreddit`` If the message was sent from a subreddit, provides an instance of
|
||||
:class:`.Subreddit`.
|
||||
``was_comment`` Whether or not the message was a comment reply.
|
||||
=============== =================================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
def mute(self):
|
||||
"""Mute the sender of this :class:`.SubredditMessage`.
|
||||
|
||||
For example, to mute the sender of the first :class:`.SubredditMessage` in the
|
||||
authenticated users' inbox:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from praw.models import SubredditMessage
|
||||
|
||||
msg = next(
|
||||
message for message in reddit.inbox.all() if isinstance(message, SubredditMessage)
|
||||
)
|
||||
msg.mute()
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["mute_sender"], data={"id": self.fullname})
|
||||
|
||||
def unmute(self):
|
||||
"""Unmute the sender of this :class:`.SubredditMessage`.
|
||||
|
||||
For example, to unmute the sender of the first :class:`.SubredditMessage` in the
|
||||
authenticated users' inbox:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from praw.models import SubredditMessage
|
||||
|
||||
msg = next(
|
||||
message for message in reddit.inbox.all() if isinstance(message, SubredditMessage)
|
||||
)
|
||||
msg.unmute()
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["unmute_sender"], data={"id": self.fullname})
|
||||
@@ -0,0 +1,322 @@
|
||||
"""Package providing reddit class mixins."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from json import dumps
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from ....const import API_PATH
|
||||
from ....util import _deprecate_args
|
||||
from .editable import EditableMixin
|
||||
from .fullname import FullnameMixin
|
||||
from .gildable import GildableMixin
|
||||
from .inboxable import InboxableMixin
|
||||
from .inboxtoggleable import InboxToggleableMixin
|
||||
from .messageable import MessageableMixin
|
||||
from .modnote import ModNoteMixin
|
||||
from .replyable import ReplyableMixin
|
||||
from .reportable import ReportableMixin
|
||||
from .savable import SavableMixin
|
||||
from .votable import VotableMixin
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class ThingModerationMixin(ModNoteMixin):
|
||||
r"""Provides moderation methods for :class:`.Comment`\ s and :class:`.Submission`\ s."""
|
||||
|
||||
REMOVAL_MESSAGE_API = None
|
||||
|
||||
def _add_removal_reason(self, *, mod_note: str = "", reason_id: str | None = None):
|
||||
"""Add a removal reason for a :class:`.Comment` or :class:`.Submission`.
|
||||
|
||||
:param mod_note: A message for the other moderators.
|
||||
:param reason_id: The removal reason ID.
|
||||
|
||||
It is necessary to first call :meth:`.remove` on the :class:`.Comment` or
|
||||
:class:`.Submission`.
|
||||
|
||||
If ``reason_id`` is not specified, ``mod_note`` cannot be blank.
|
||||
|
||||
"""
|
||||
if not reason_id and not mod_note:
|
||||
msg = "mod_note cannot be blank if reason_id is not specified"
|
||||
raise ValueError(msg)
|
||||
# Only the first element of the item_id list is used.
|
||||
data = {
|
||||
"item_ids": [self.thing.fullname],
|
||||
"mod_note": mod_note,
|
||||
"reason_id": reason_id,
|
||||
}
|
||||
self.thing._reddit.post(API_PATH["removal_reasons"], data={"json": dumps(data)})
|
||||
|
||||
def approve(self):
|
||||
"""Approve a :class:`.Comment` or :class:`.Submission`.
|
||||
|
||||
Approving a comment or submission reverts a removal, resets the report counter,
|
||||
adds a green check mark indicator (only visible to other moderators) on the
|
||||
website view, and sets the ``approved_by`` attribute to the authenticated user.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# approve a comment:
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.mod.approve()
|
||||
# approve a submission:
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.approve()
|
||||
|
||||
"""
|
||||
self.thing._reddit.post(API_PATH["approve"], data={"id": self.thing.fullname})
|
||||
|
||||
@_deprecate_args("how", "sticky")
|
||||
def distinguish(self, *, how: str = "yes", sticky: bool = False):
|
||||
"""Distinguish a :class:`.Comment` or :class:`.Submission`.
|
||||
|
||||
:param how: One of ``"yes"``, ``"no"``, ``"admin"``, or ``"special"``. ``"yes"``
|
||||
adds a moderator level distinguish. ``"no"`` removes any distinction.
|
||||
``"admin"`` and ``"special"`` require special user privileges to use
|
||||
(default ``"yes"``).
|
||||
:param sticky: :class:`.Comment` is stickied if ``True``, placing it at the top
|
||||
of the comment page regardless of score. If thing is not a top-level
|
||||
comment, this parameter is silently ignored (default ``False``).
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# distinguish and sticky a comment:
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.mod.distinguish(sticky=True)
|
||||
# undistinguish a submission:
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.distinguish(how="no")
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.undistinguish`
|
||||
|
||||
"""
|
||||
data = {"how": how, "id": self.thing.fullname}
|
||||
if sticky and getattr(self.thing, "is_root", False):
|
||||
data["sticky"] = True
|
||||
self.thing._reddit.post(API_PATH["distinguish"], data=data)
|
||||
|
||||
def ignore_reports(self):
|
||||
"""Ignore future reports on a :class:`.Comment` or :class:`.Submission`.
|
||||
|
||||
Calling this method will prevent future reports on this :class:`.Comment` or
|
||||
:class:`.Submission` from both triggering notifications and appearing in the
|
||||
various moderation listings. The report count will still increment on the
|
||||
:class:`.Comment` or :class:`.Submission`.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# ignore future reports on a comment:
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.mod.ignore_reports()
|
||||
# ignore future reports on a submission:
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.ignore_reports()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.unignore_reports`
|
||||
|
||||
"""
|
||||
self.thing._reddit.post(
|
||||
API_PATH["ignore_reports"], data={"id": self.thing.fullname}
|
||||
)
|
||||
|
||||
def lock(self):
|
||||
"""Lock a :class:`.Comment` or :class:`.Submission`.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# lock a comment:
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.mod.lock()
|
||||
# lock a submission:
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.lock()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.unlock`
|
||||
|
||||
"""
|
||||
self.thing._reddit.post(API_PATH["lock"], data={"id": self.thing.fullname})
|
||||
|
||||
@_deprecate_args("spam", "mod_note", "reason_id")
|
||||
def remove(
|
||||
self, *, mod_note: str = "", spam: bool = False, reason_id: str | None = None
|
||||
):
|
||||
"""Remove a :class:`.Comment` or :class:`.Submission`.
|
||||
|
||||
:param mod_note: A message for the other moderators.
|
||||
:param spam: When ``True``, use the removal to help train the
|
||||
:class:`.Subreddit`'s spam filter (default: ``False``).
|
||||
:param reason_id: The removal reason ID.
|
||||
|
||||
If either ``reason_id`` or ``mod_note`` are provided, a second API call is made
|
||||
to add the removal reason.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# remove a comment and mark as spam:
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.mod.remove(spam=True)
|
||||
# remove a submission
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.remove()
|
||||
# remove a submission with a removal reason
|
||||
reason = reddit.subreddit.mod.removal_reasons["110ni21zo23ql"]
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.remove(reason_id=reason.id)
|
||||
|
||||
"""
|
||||
data = {"id": self.thing.fullname, "spam": bool(spam)}
|
||||
self.thing._reddit.post(API_PATH["remove"], data=data)
|
||||
if any([reason_id, mod_note]):
|
||||
self._add_removal_reason(mod_note=mod_note, reason_id=reason_id)
|
||||
|
||||
@_deprecate_args("message", "title", "type")
|
||||
def send_removal_message(
|
||||
self,
|
||||
*,
|
||||
message: str,
|
||||
title: str = "ignored",
|
||||
type: str = "public",
|
||||
) -> praw.models.Comment | None:
|
||||
"""Send a removal message for a :class:`.Comment` or :class:`.Submission`.
|
||||
|
||||
.. warning::
|
||||
|
||||
The object has to be removed before giving it a removal reason. Remove the
|
||||
object with :meth:`.remove`. Trying to add a removal reason without removing
|
||||
the object will result in :class:`.RedditAPIException` being thrown with an
|
||||
``INVALID_ID`` error_type.
|
||||
|
||||
Reddit adds human-readable information about the object to the message.
|
||||
|
||||
:param type: One of ``"public"``, ``"public_as_subreddit"``, ``"private"``, or
|
||||
``"private_exposed"``. ``"public"`` leaves a stickied comment on the post.
|
||||
``"public_as_subreddit"`` leaves a stickied comment on the post with the
|
||||
u/subreddit-ModTeam account. ``"private"`` sends a modmail message with
|
||||
hidden username. ``"private_exposed"`` sends a modmail message without
|
||||
hidden username (default: ``"public"``).
|
||||
:param title: The short reason given in the message. Ignored if type is
|
||||
``"public"`` or ``"public_as_subreddit"``.
|
||||
:param message: The body of the message.
|
||||
|
||||
:returns: The new :class:`.Comment` if ``type`` is ``"public"`` or
|
||||
``"public_as_subreddit"``.
|
||||
|
||||
"""
|
||||
# The API endpoint used to send removal messages is different for posts and
|
||||
# comments, so the derived classes specify which one.
|
||||
if self.REMOVAL_MESSAGE_API is None:
|
||||
msg = "ThingModerationMixin must be extended."
|
||||
raise NotImplementedError(msg)
|
||||
url = API_PATH[self.REMOVAL_MESSAGE_API]
|
||||
|
||||
# Only the first element of the item_id list is used.
|
||||
data = {
|
||||
"item_id": [self.thing.fullname],
|
||||
"message": message,
|
||||
"title": title,
|
||||
"type": type,
|
||||
}
|
||||
|
||||
return self.thing._reddit.post(url, data={"json": dumps(data)}) or None
|
||||
|
||||
def undistinguish(self):
|
||||
"""Remove mod, admin, or special distinguishing from an object.
|
||||
|
||||
Also unstickies the object if applicable.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# undistinguish a comment:
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.mod.undistinguish()
|
||||
# undistinguish a submission:
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.undistinguish()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.distinguish`
|
||||
|
||||
"""
|
||||
self.distinguish(how="no")
|
||||
|
||||
def unignore_reports(self):
|
||||
"""Resume receiving future reports on a :class:`.Comment` or :class:`.Submission`.
|
||||
|
||||
Future reports on this :class:`.Comment` or :class:`.Submission` will cause
|
||||
notifications, and appear in the various moderation listings.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# accept future reports on a comment:
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.mod.unignore_reports()
|
||||
# accept future reports on a submission:
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.unignore_reports()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.ignore_reports`
|
||||
|
||||
"""
|
||||
self.thing._reddit.post(
|
||||
API_PATH["unignore_reports"], data={"id": self.thing.fullname}
|
||||
)
|
||||
|
||||
def unlock(self):
|
||||
"""Unlock a :class:`.Comment` or :class:`.Submission`.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# unlock a comment:
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.mod.unlock()
|
||||
# unlock a submission:
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.unlock()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.lock`
|
||||
|
||||
"""
|
||||
self.thing._reddit.post(API_PATH["unlock"], data={"id": self.thing.fullname})
|
||||
|
||||
|
||||
class UserContentMixin(
|
||||
EditableMixin,
|
||||
GildableMixin,
|
||||
InboxToggleableMixin,
|
||||
ReplyableMixin,
|
||||
ReportableMixin,
|
||||
SavableMixin,
|
||||
VotableMixin,
|
||||
):
|
||||
"""A convenience mixin that applies to both Comments and Submissions."""
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Provide the EditableMixin class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ....const import API_PATH
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class EditableMixin:
|
||||
"""Interface for classes that can be edited and deleted."""
|
||||
|
||||
def delete(self):
|
||||
"""Delete the object.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.delete()
|
||||
|
||||
submission = reddit.submission("8dmv8z")
|
||||
submission.delete()
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["del"], data={"id": self.fullname})
|
||||
|
||||
def edit(self, body: str) -> praw.models.Comment | praw.models.Submission:
|
||||
"""Replace the body of the object with ``body``.
|
||||
|
||||
:param body: The Markdown formatted content for the updated object.
|
||||
|
||||
:returns: The current instance after updating its attributes.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
|
||||
# construct the text of an edited comment
|
||||
# by appending to the old body:
|
||||
edited_body = comment.body + "Edit: thanks for the gold!"
|
||||
comment.edit(edited_body)
|
||||
|
||||
"""
|
||||
data = {
|
||||
"text": body,
|
||||
"thing_id": self.fullname,
|
||||
"validate_on_submit": self._reddit.validate_on_submit,
|
||||
}
|
||||
updated = self._reddit.post(API_PATH["edit"], data=data)[0]
|
||||
for attribute in [
|
||||
"_fetched",
|
||||
"_reddit",
|
||||
"_submission",
|
||||
"replies",
|
||||
"subreddit",
|
||||
]:
|
||||
if attribute in updated.__dict__:
|
||||
delattr(updated, attribute)
|
||||
self.__dict__.update(updated.__dict__)
|
||||
return self
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Provide the FullnameMixin class."""
|
||||
|
||||
|
||||
class FullnameMixin:
|
||||
"""Interface for classes that have a fullname."""
|
||||
|
||||
_kind = None
|
||||
|
||||
@property
|
||||
def fullname(self) -> str:
|
||||
"""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``.
|
||||
|
||||
"""
|
||||
if "_" in self.id:
|
||||
return self.id
|
||||
return f"{self._kind}_{self.id}"
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Provide the GildableMixin class."""
|
||||
|
||||
from warnings import warn
|
||||
|
||||
from ....const import API_PATH
|
||||
from ....util import _deprecate_args
|
||||
|
||||
|
||||
class GildableMixin:
|
||||
"""Interface for classes that can be gilded."""
|
||||
|
||||
@_deprecate_args("gild_type", "is_anonymous", "message")
|
||||
def award(
|
||||
self,
|
||||
*,
|
||||
gild_type: str = "gid_2",
|
||||
is_anonymous: bool = True,
|
||||
message: str = None,
|
||||
) -> dict:
|
||||
"""Award the author of the item.
|
||||
|
||||
:param gild_type: Type of award to give. See table below for currently know
|
||||
global award types.
|
||||
:param is_anonymous: If ``True``, the authenticated user's username will not be
|
||||
revealed to the recipient.
|
||||
:param message: Message to include with the award.
|
||||
|
||||
:returns: A dict containing info similar to what is shown below:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
{
|
||||
"subreddit_balance": 85260,
|
||||
"treatment_tags": [],
|
||||
"coins": 8760,
|
||||
"gildings": {"gid_1": 0, "gid_2": 1, "gid_3": 0},
|
||||
"awarder_karma_received": 4,
|
||||
"all_awardings": [
|
||||
{
|
||||
"giver_coin_reward": 0,
|
||||
"subreddit_id": None,
|
||||
"is_new": False,
|
||||
"days_of_drip_extension": 0,
|
||||
"coin_price": 75,
|
||||
"id": "award_9663243a-e77f-44cf-abc6-850ead2cd18d",
|
||||
"penny_donate": 0,
|
||||
"coin_reward": 0,
|
||||
"icon_url": "https://www.redditstatic.com/gold/awards/icon/SnooClappingPremium_512.png",
|
||||
"days_of_premium": 0,
|
||||
"icon_height": 512,
|
||||
"tiers_by_required_awardings": None,
|
||||
"icon_width": 512,
|
||||
"static_icon_width": 512,
|
||||
"start_date": None,
|
||||
"is_enabled": True,
|
||||
"awardings_required_to_grant_benefits": None,
|
||||
"description": "For an especially amazing showing.",
|
||||
"end_date": None,
|
||||
"subreddit_coin_reward": 0,
|
||||
"count": 1,
|
||||
"static_icon_height": 512,
|
||||
"name": "Bravo Grande!",
|
||||
"icon_format": "APNG",
|
||||
"award_sub_type": "PREMIUM",
|
||||
"penny_price": 0,
|
||||
"award_type": "global",
|
||||
"static_icon_url": "https://i.redd.it/award_images/t5_q0gj4/59e02tmkl4451_BravoGrande-Static.png",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
.. warning::
|
||||
|
||||
Requires the authenticated user to own Reddit Coins. Calling this method
|
||||
will consume Reddit Coins.
|
||||
|
||||
To award the gold award anonymously do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.award()
|
||||
|
||||
submission = reddit.submission("8dmv8z")
|
||||
submission.award()
|
||||
|
||||
To award the platinum award with the message 'Nice!' and reveal your username to
|
||||
the recipient do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.award(gild_type="gild_3", message="Nice!", is_anonymous=False)
|
||||
|
||||
submission = reddit.submission("8dmv8z")
|
||||
submission.award(gild_type="gild_3", message="Nice!", is_anonymous=False)
|
||||
|
||||
.. include:: awards.txt
|
||||
|
||||
"""
|
||||
params = {
|
||||
"api_type": "json",
|
||||
"gild_type": gild_type,
|
||||
"is_anonymous": is_anonymous,
|
||||
"thing_id": self.fullname,
|
||||
"message": message,
|
||||
}
|
||||
return self._reddit.post(API_PATH["award_thing"], params=params)
|
||||
|
||||
def gild(self) -> dict:
|
||||
"""Alias for :meth:`.award` to maintain backwards compatibility."""
|
||||
warn(
|
||||
"'.gild' has been renamed to '.award'.",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.award()
|
||||
@@ -0,0 +1,153 @@
|
||||
"""Provide the InboxableMixin class."""
|
||||
|
||||
from ....const import API_PATH
|
||||
|
||||
|
||||
class InboxableMixin:
|
||||
"""Interface for :class:`.RedditBase` subclasses that originate from the inbox."""
|
||||
|
||||
def block(self):
|
||||
"""Block the user who sent the item.
|
||||
|
||||
.. note::
|
||||
|
||||
This method pertains only to objects which were retrieved via the inbox.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.block()
|
||||
|
||||
# or, identically:
|
||||
|
||||
comment.author.block()
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["block"], data={"id": self.fullname})
|
||||
|
||||
def collapse(self):
|
||||
"""Mark the item as collapsed.
|
||||
|
||||
.. note::
|
||||
|
||||
This method pertains only to objects which were retrieved via the inbox.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
inbox = reddit.inbox()
|
||||
|
||||
# select first inbox item and collapse it message = next(inbox)
|
||||
message.collapse()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.uncollapse`
|
||||
|
||||
"""
|
||||
self._reddit.inbox.collapse([self])
|
||||
|
||||
def mark_read(self):
|
||||
"""Mark a single inbox item as read.
|
||||
|
||||
.. note::
|
||||
|
||||
This method pertains only to objects which were retrieved via the inbox.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
inbox = reddit.inbox.unread()
|
||||
|
||||
for message in inbox:
|
||||
# process unread messages
|
||||
...
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.mark_unread`
|
||||
|
||||
To mark the whole inbox as read with a single network request, use
|
||||
:meth:`.Inbox.mark_all_read`
|
||||
|
||||
"""
|
||||
self._reddit.inbox.mark_read([self])
|
||||
|
||||
def mark_unread(self):
|
||||
"""Mark the item as unread.
|
||||
|
||||
.. note::
|
||||
|
||||
This method pertains only to objects which were retrieved via the inbox.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
inbox = reddit.inbox(limit=10)
|
||||
|
||||
for message in inbox:
|
||||
# process messages
|
||||
...
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.mark_read`
|
||||
|
||||
"""
|
||||
self._reddit.inbox.mark_unread([self])
|
||||
|
||||
def unblock_subreddit(self):
|
||||
"""Unblock a subreddit.
|
||||
|
||||
.. note::
|
||||
|
||||
This method pertains only to objects which were retrieved via the inbox.
|
||||
|
||||
For example, to unblock all blocked subreddits that you can find by going
|
||||
through your inbox:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from praw.models import SubredditMessage
|
||||
|
||||
subs = set()
|
||||
for item in reddit.inbox.messages(limit=None):
|
||||
if isinstance(item, SubredditMessage):
|
||||
if (
|
||||
item.subject == "[message from blocked subreddit]"
|
||||
and str(item.subreddit) not in subs
|
||||
):
|
||||
item.unblock_subreddit()
|
||||
subs.add(str(item.subreddit))
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["unblock_subreddit"], data={"id": self.fullname})
|
||||
|
||||
def uncollapse(self):
|
||||
"""Mark the item as uncollapsed.
|
||||
|
||||
.. note::
|
||||
|
||||
This method pertains only to objects which were retrieved via the inbox.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
inbox = reddit.inbox()
|
||||
|
||||
# select first inbox item and uncollapse it
|
||||
message = next(inbox)
|
||||
message.uncollapse()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.collapse`
|
||||
|
||||
"""
|
||||
self._reddit.inbox.uncollapse([self])
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Provide the InboxToggleableMixin class."""
|
||||
|
||||
from ....const import API_PATH
|
||||
|
||||
|
||||
class InboxToggleableMixin:
|
||||
"""Interface for classes that can optionally receive inbox replies."""
|
||||
|
||||
def disable_inbox_replies(self):
|
||||
"""Disable inbox replies for the item.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only apply to items created by the authenticated user.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.disable_inbox_replies()
|
||||
|
||||
submission = reddit.submission("8dmv8z")
|
||||
submission.disable_inbox_replies()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.enable_inbox_replies`
|
||||
|
||||
"""
|
||||
self._reddit.post(
|
||||
API_PATH["sendreplies"], data={"id": self.fullname, "state": False}
|
||||
)
|
||||
|
||||
def enable_inbox_replies(self):
|
||||
"""Enable inbox replies for the item.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only apply to items created by the authenticated user.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.enable_inbox_replies()
|
||||
|
||||
submission = reddit.submission("8dmv8z")
|
||||
submission.enable_inbox_replies()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.disable_inbox_replies`
|
||||
|
||||
"""
|
||||
self._reddit.post(
|
||||
API_PATH["sendreplies"], data={"id": self.fullname, "state": True}
|
||||
)
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Provide the MessageableMixin class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ....const import API_PATH
|
||||
from ....util import _deprecate_args
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw
|
||||
|
||||
|
||||
class MessageableMixin:
|
||||
"""Interface for classes that can be messaged."""
|
||||
|
||||
@_deprecate_args("subject", "message", "from_subreddit")
|
||||
def message(
|
||||
self,
|
||||
*,
|
||||
from_subreddit: praw.models.Subreddit | str | None = None,
|
||||
message: str,
|
||||
subject: str,
|
||||
):
|
||||
"""Send a message to a :class:`.Redditor` or a :class:`.Subreddit`'s moderators (modmail).
|
||||
|
||||
:param from_subreddit: A :class:`.Subreddit` instance or string to send the
|
||||
message from. When provided, messages are sent from the subreddit rather
|
||||
than from the authenticated user.
|
||||
|
||||
.. note::
|
||||
|
||||
The authenticated user must be a moderator of the subreddit and have the
|
||||
``mail`` moderator permission.
|
||||
|
||||
:param message: The message content.
|
||||
:param subject: The subject of the message.
|
||||
|
||||
For example, to send a private message to u/spez, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.redditor("spez").message(subject="TEST", message="test message from PRAW")
|
||||
|
||||
To send a message to u/spez from the moderators of r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.redditor("spez").message(
|
||||
subject="TEST", message="test message from r/test", from_subreddit="test"
|
||||
)
|
||||
|
||||
To send a message to the moderators of r/test, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").message(subject="TEST", message="test PM from PRAW")
|
||||
|
||||
"""
|
||||
data = {
|
||||
"subject": subject,
|
||||
"text": message,
|
||||
"to": f"{getattr(self.__class__, 'MESSAGE_PREFIX', '')}{self}",
|
||||
}
|
||||
if from_subreddit:
|
||||
data["from_sr"] = str(from_subreddit)
|
||||
self._reddit.post(API_PATH["compose"], data=data)
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Provide the ModNoteMixin class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Generator
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class ModNoteMixin:
|
||||
"""Interface for classes that can have a moderator note set on them."""
|
||||
|
||||
def author_notes(
|
||||
self, **generator_kwargs: Any
|
||||
) -> Generator[praw.models.ModNote, None, None]:
|
||||
"""Get the moderator notes for the author of this object in the subreddit it's posted in.
|
||||
|
||||
:param generator_kwargs: Additional keyword arguments are passed in the
|
||||
initialization of the moderator note generator.
|
||||
|
||||
:returns: A generator of :class:`.ModNote`.
|
||||
|
||||
For example, to list all notes the author of a submission, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for note in reddit.submission("92dd8").mod.author_notes():
|
||||
print(f"{note.label}: {note.note}")
|
||||
|
||||
"""
|
||||
return self.thing.subreddit.mod.notes.redditors(
|
||||
self.thing.author, **generator_kwargs
|
||||
)
|
||||
|
||||
def create_note(
|
||||
self, *, label: str | None = None, note: str, **other_settings: Any
|
||||
) -> praw.models.ModNote:
|
||||
"""Create a moderator note on the author of this object in the subreddit it's posted in.
|
||||
|
||||
:param label: The label for the note. As of this writing, this can be one of the
|
||||
following: ``"ABUSE_WARNING"``, ``"BAN"``, ``"BOT_BAN"``,
|
||||
``"HELPFUL_USER"``, ``"PERMA_BAN"``, ``"SOLID_CONTRIBUTOR"``,
|
||||
``"SPAM_WARNING"``, ``"SPAM_WATCH"``, or ``None`` (default: ``None``).
|
||||
:param note: The content of the note. As of this writing, this is limited to 250
|
||||
characters.
|
||||
:param other_settings: Additional keyword arguments are passed to
|
||||
:meth:`~.BaseModNotes.create`.
|
||||
|
||||
:returns: The new :class:`.ModNote` object.
|
||||
|
||||
For example, to create a note on a :class:`.Submission`, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.submission("92dd8").mod.create_note(label="HELPFUL_USER", note="Test note")
|
||||
|
||||
"""
|
||||
return self.thing.subreddit.mod.notes.create(
|
||||
label=label, note=note, thing=self.thing, **other_settings
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Provide the ReplyableMixin class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ....const import API_PATH
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class ReplyableMixin:
|
||||
"""Interface for :class:`.RedditBase` classes that can be replied to."""
|
||||
|
||||
def reply(self, body: str) -> praw.models.Comment | praw.models.Message | None:
|
||||
"""Reply to the object.
|
||||
|
||||
:param body: The Markdown formatted content for a comment.
|
||||
|
||||
:returns: A :class:`.Comment` or :class:`.Message` object for the newly created
|
||||
comment or message or ``None`` if Reddit doesn't provide one.
|
||||
|
||||
:raises: ``prawcore.exceptions.Forbidden`` when attempting to reply to some
|
||||
items, such as locked submissions/comments or non-replyable messages.
|
||||
|
||||
A ``None`` value can be returned if the target is a comment or submission in a
|
||||
quarantined subreddit and the authenticated user has not opt-ed into viewing the
|
||||
content. When this happens the comment will be successfully created on Reddit
|
||||
and can be retried by drawing the comment from the user's comment history.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.reply("reply")
|
||||
|
||||
comment = reddit.comment("dxolpyc")
|
||||
comment.reply("reply")
|
||||
|
||||
"""
|
||||
data = {"text": body, "thing_id": self.fullname}
|
||||
comments = self._reddit.post(API_PATH["comment"], data=data)
|
||||
try:
|
||||
return comments[0]
|
||||
except IndexError:
|
||||
return None
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Provide the ReportableMixin class."""
|
||||
|
||||
from ....const import API_PATH
|
||||
|
||||
|
||||
class ReportableMixin:
|
||||
"""Interface for :class:`.RedditBase` classes that can be reported."""
|
||||
|
||||
def report(self, reason: str):
|
||||
"""Report this object to the moderators of its subreddit.
|
||||
|
||||
:param reason: The reason for reporting.
|
||||
|
||||
:raises: :class:`.RedditAPIException` if ``reason`` is longer than 100
|
||||
characters.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.report("report reason")
|
||||
|
||||
comment = reddit.comment("dxolpyc")
|
||||
comment.report("report reason")
|
||||
|
||||
"""
|
||||
self._reddit.post(
|
||||
API_PATH["report"], data={"id": self.fullname, "reason": reason}
|
||||
)
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Provide the SavableMixin class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ....const import API_PATH
|
||||
from ....util import _deprecate_args
|
||||
|
||||
|
||||
class SavableMixin:
|
||||
"""Interface for :class:`.RedditBase` classes that can be saved."""
|
||||
|
||||
@_deprecate_args("category")
|
||||
def save(self, *, category: str | None = None):
|
||||
"""Save the object.
|
||||
|
||||
:param category: The category to save to. If the authenticated user does not
|
||||
have Reddit Premium this value is ignored by Reddit (default: ``None``).
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.save(category="view later")
|
||||
|
||||
comment = reddit.comment("dxolpyc")
|
||||
comment.save()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.unsave`
|
||||
|
||||
"""
|
||||
self._reddit.post(
|
||||
API_PATH["save"], data={"category": category, "id": self.fullname}
|
||||
)
|
||||
|
||||
def unsave(self):
|
||||
"""Unsave the object.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.unsave()
|
||||
|
||||
comment = reddit.comment("dxolpyc")
|
||||
comment.unsave()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.save`
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["unsave"], data={"id": self.fullname})
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Provide the VotableMixin class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ....const import API_PATH
|
||||
|
||||
|
||||
class VotableMixin:
|
||||
"""Interface for :class:`.RedditBase` classes that can be voted on."""
|
||||
|
||||
def _vote(self, direction: int):
|
||||
self._reddit.post(
|
||||
API_PATH["vote"], data={"dir": str(direction), "id": self.fullname}
|
||||
)
|
||||
|
||||
def clear_vote(self):
|
||||
"""Clear the authenticated user's vote on the object.
|
||||
|
||||
.. 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 manipulation. [`Ref
|
||||
<https://www.reddit.com/dev/api#POST_api_vote>`_]
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.clear_vote()
|
||||
|
||||
comment = reddit.comment("dxolpyc")
|
||||
comment.clear_vote()
|
||||
|
||||
"""
|
||||
self._vote(direction=0)
|
||||
|
||||
def downvote(self):
|
||||
"""Downvote the object.
|
||||
|
||||
.. 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 manipulation. [`Ref
|
||||
<https://www.reddit.com/dev/api#POST_api_vote>`_]
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.downvote()
|
||||
|
||||
comment = reddit.comment("dxolpyc")
|
||||
comment.downvote()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.upvote`
|
||||
|
||||
"""
|
||||
self._vote(direction=-1)
|
||||
|
||||
def upvote(self):
|
||||
"""Upvote the object.
|
||||
|
||||
.. 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 manipulation. [`Ref
|
||||
<https://www.reddit.com/dev/api#POST_api_vote>`_]
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.upvote()
|
||||
|
||||
comment = reddit.comment("dxolpyc")
|
||||
comment.upvote()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.downvote`
|
||||
|
||||
"""
|
||||
self._vote(direction=1)
|
||||
344
backend/venv/Lib/site-packages/praw/models/reddit/modmail.py
Normal file
344
backend/venv/Lib/site-packages/praw/models/reddit/modmail.py
Normal file
@@ -0,0 +1,344 @@
|
||||
"""Provide models for new modmail."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ...const import API_PATH
|
||||
from ...util import _deprecate_args, snake_case_keys
|
||||
from .base import RedditBase
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw
|
||||
|
||||
|
||||
class ModmailObject(RedditBase):
|
||||
"""A base class for objects within a modmail conversation."""
|
||||
|
||||
AUTHOR_ATTRIBUTE = "author"
|
||||
STR_FIELD = "id"
|
||||
|
||||
def __setattr__(self, attribute: str, value: Any):
|
||||
"""Objectify the AUTHOR_ATTRIBUTE attribute."""
|
||||
if attribute == self.AUTHOR_ATTRIBUTE:
|
||||
value = self._reddit._objector.objectify(value)
|
||||
super().__setattr__(attribute, value)
|
||||
|
||||
|
||||
class ModmailConversation(RedditBase):
|
||||
"""A class for modmail conversations.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
==================== ===============================================================
|
||||
Attribute Description
|
||||
==================== ===============================================================
|
||||
``authors`` Provides an ordered list of :class:`.Redditor` instances. The
|
||||
authors of each message in the modmail conversation.
|
||||
``id`` The ID of the :class:`.ModmailConversation`.
|
||||
``is_highlighted`` Whether or not the :class:`.ModmailConversation` is
|
||||
highlighted.
|
||||
``is_internal`` Whether or not the :class:`.ModmailConversation` is a private
|
||||
mod conversation.
|
||||
``last_mod_update`` Time of the last mod message reply, represented in the `ISO
|
||||
8601`_ standard with timezone.
|
||||
``last_updated`` Time of the last message reply, represented in the `ISO 8601`_
|
||||
standard with timezone.
|
||||
``last_user_update`` Time of the last user message reply, represented in the `ISO
|
||||
8601`_ standard with timezone.
|
||||
``num_messages`` The number of messages in the :class:`.ModmailConversation`.
|
||||
``obj_ids`` Provides a list of dictionaries representing mod actions on the
|
||||
:class:`.ModmailConversation`. Each dict contains attributes of
|
||||
``"key"`` and ``"id"``. The key can be either ``""messages"``
|
||||
or ``"ModAction"``. ``"ModAction"`` represents
|
||||
archiving/highlighting etc.
|
||||
``owner`` Provides an instance of :class:`.Subreddit`. The subreddit that
|
||||
the :class:`.ModmailConversation` belongs to.
|
||||
``participant`` Provides an instance of :class:`.Redditor`. The participating
|
||||
user in the :class:`.ModmailConversation`.
|
||||
``subject`` The subject of the :class:`.ModmailConversation`.
|
||||
==================== ===============================================================
|
||||
|
||||
.. _iso 8601: https://en.wikipedia.org/wiki/ISO_8601
|
||||
|
||||
"""
|
||||
|
||||
STR_FIELD = "id"
|
||||
|
||||
@staticmethod
|
||||
def _convert_conversation_objects(data: dict[str, Any], reddit: praw.Reddit):
|
||||
"""Convert messages and mod actions to PRAW objects."""
|
||||
result = {"messages": [], "modActions": []}
|
||||
for thing in data["objIds"]:
|
||||
key = thing["key"]
|
||||
thing_data = data[key][thing["id"]]
|
||||
result[key].append(reddit._objector.objectify(thing_data))
|
||||
data.update(result)
|
||||
|
||||
@staticmethod
|
||||
def _convert_user_summary(data: dict[str, Any], reddit: praw.Reddit):
|
||||
"""Convert dictionaries of recent user history to PRAW objects."""
|
||||
parsers = {
|
||||
"recentComments": reddit._objector.parsers[reddit.config.kinds["comment"]],
|
||||
"recentConvos": ModmailConversation,
|
||||
"recentPosts": reddit._objector.parsers[reddit.config.kinds["submission"]],
|
||||
}
|
||||
for kind, parser in parsers.items():
|
||||
objects = []
|
||||
for thing_id, summary in data[kind].items():
|
||||
thing = parser(reddit, id=thing_id.rsplit("_", 1)[-1])
|
||||
if parser is not ModmailConversation:
|
||||
del summary["permalink"]
|
||||
for key, value in summary.items():
|
||||
setattr(thing, key, value)
|
||||
objects.append(thing)
|
||||
# Sort by id, oldest to newest
|
||||
data[kind] = sorted(objects, key=lambda x: int(x.id, base=36), reverse=True)
|
||||
|
||||
@classmethod
|
||||
def parse(
|
||||
cls,
|
||||
data: dict[str, Any],
|
||||
reddit: praw.Reddit,
|
||||
) -> ModmailConversation:
|
||||
"""Return an instance of :class:`.ModmailConversation` from ``data``.
|
||||
|
||||
:param data: The structured data.
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
|
||||
"""
|
||||
data["authors"] = [
|
||||
reddit._objector.objectify(author) for author in data["authors"]
|
||||
]
|
||||
for entity in "owner", "participant":
|
||||
data[entity] = reddit._objector.objectify(data[entity])
|
||||
|
||||
if data.get("user"):
|
||||
cls._convert_user_summary(data["user"], reddit)
|
||||
data["user"] = reddit._objector.objectify(data["user"])
|
||||
|
||||
data = snake_case_keys(data)
|
||||
|
||||
return cls(reddit, _data=data)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
id: str | None = None,
|
||||
mark_read: bool = False,
|
||||
_data: dict[str, Any] | None = None,
|
||||
):
|
||||
"""Initialize a :class:`.ModmailConversation` instance.
|
||||
|
||||
:param mark_read: If ``True``, conversation is marked as read (default:
|
||||
``False``).
|
||||
|
||||
"""
|
||||
if bool(id) == bool(_data):
|
||||
msg = "Either 'id' or '_data' must be provided."
|
||||
raise TypeError(msg)
|
||||
|
||||
if id:
|
||||
self.id = id
|
||||
|
||||
super().__init__(reddit, _data=_data)
|
||||
|
||||
self._info_params = {"markRead": True} if mark_read else None
|
||||
|
||||
def _build_conversation_list(
|
||||
self, other_conversations: list[ModmailConversation]
|
||||
) -> str:
|
||||
"""Return a comma-separated list of conversation IDs."""
|
||||
conversations = [self] + (other_conversations or [])
|
||||
return ",".join(conversation.id for conversation in conversations)
|
||||
|
||||
def _fetch(self):
|
||||
data = self._fetch_data()
|
||||
other = self._reddit._objector.objectify(data)
|
||||
self.__dict__.update(other.__dict__)
|
||||
super()._fetch()
|
||||
|
||||
def _fetch_info(self):
|
||||
return "modmail_conversation", {"id": self.id}, self._info_params
|
||||
|
||||
def archive(self):
|
||||
"""Archive the conversation.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").modmail("2gmz").archive()
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["modmail_archive"].format(id=self.id))
|
||||
|
||||
def highlight(self):
|
||||
"""Highlight the conversation.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").modmail("2gmz").highlight()
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["modmail_highlight"].format(id=self.id))
|
||||
|
||||
@_deprecate_args("num_days")
|
||||
def mute(self, *, num_days: int = 3):
|
||||
"""Mute the non-mod user associated with the conversation.
|
||||
|
||||
:param num_days: Duration of mute in days. Valid options are ``3``, ``7``, or
|
||||
``28`` (default: ``3``).
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").modmail("2gmz").mute()
|
||||
|
||||
To mute for 7 days:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").modmail("2gmz").mute(num_days=7)
|
||||
|
||||
"""
|
||||
params = {"num_hours": num_days * 24} if num_days != 3 else {}
|
||||
self._reddit.request(
|
||||
method="POST",
|
||||
params=params,
|
||||
path=API_PATH["modmail_mute"].format(id=self.id),
|
||||
)
|
||||
|
||||
@_deprecate_args("other_conversations")
|
||||
def read(self, *, other_conversations: list[ModmailConversation] | None = None):
|
||||
"""Mark the conversation(s) as read.
|
||||
|
||||
:param other_conversations: A list of other conversations to mark (default:
|
||||
``None``).
|
||||
|
||||
For example, to mark the conversation as read along with other recent
|
||||
conversations from the same user:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit = reddit.subreddit("test")
|
||||
conversation = subreddit.modmail.conversation("2gmz")
|
||||
conversation.read(other_conversations=conversation.user.recent_convos)
|
||||
|
||||
"""
|
||||
data = {"conversationIds": self._build_conversation_list(other_conversations)}
|
||||
self._reddit.post(API_PATH["modmail_read"], data=data)
|
||||
|
||||
@_deprecate_args("body", "author_hidden", "internal")
|
||||
def reply(
|
||||
self, *, author_hidden: bool = False, body: str, internal: bool = False
|
||||
) -> ModmailMessage:
|
||||
"""Reply to the conversation.
|
||||
|
||||
:param author_hidden: When ``True``, author is hidden from non-moderators
|
||||
(default: ``False``).
|
||||
:param body: The Markdown formatted content for a message.
|
||||
:param internal: When ``True``, message is a private moderator note, hidden from
|
||||
non-moderators (default: ``False``).
|
||||
|
||||
:returns: A :class:`.ModmailMessage` object for the newly created message.
|
||||
|
||||
For example, to reply to the non-mod user while hiding your username:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
conversation = reddit.subreddit("test").modmail("2gmz")
|
||||
conversation.reply(body="Message body", author_hidden=True)
|
||||
|
||||
To create a private moderator note on the conversation:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
conversation.reply(body="Message body", internal=True)
|
||||
|
||||
"""
|
||||
data = {
|
||||
"body": body,
|
||||
"isAuthorHidden": author_hidden,
|
||||
"isInternal": internal,
|
||||
}
|
||||
response = self._reddit.post(
|
||||
API_PATH["modmail_conversation"].format(id=self.id), data=data
|
||||
)
|
||||
if isinstance(response, dict):
|
||||
# Reddit recently changed the response format, so we need to handle both in case they change it back
|
||||
message_id = response["conversation"]["objIds"][-1]["id"]
|
||||
message_data = response["messages"][message_id]
|
||||
return self._reddit._objector.objectify(message_data)
|
||||
for message in response.messages: # noqa: RET503
|
||||
if message.id == response.obj_ids[-1]["id"]:
|
||||
return message
|
||||
|
||||
def unarchive(self):
|
||||
"""Unarchive the conversation.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").modmail("2gmz").unarchive()
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["modmail_unarchive"].format(id=self.id))
|
||||
|
||||
def unhighlight(self):
|
||||
"""Un-highlight the conversation.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").modmail("2gmz").unhighlight()
|
||||
|
||||
"""
|
||||
self._reddit.delete(API_PATH["modmail_highlight"].format(id=self.id))
|
||||
|
||||
def unmute(self):
|
||||
"""Unmute the non-mod user associated with the conversation.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").modmail("2gmz").unmute()
|
||||
|
||||
"""
|
||||
self._reddit.request(
|
||||
method="POST", path=API_PATH["modmail_unmute"].format(id=self.id)
|
||||
)
|
||||
|
||||
@_deprecate_args("other_conversations")
|
||||
def unread(self, *, other_conversations: list[ModmailConversation] | None = None):
|
||||
"""Mark the conversation(s) as unread.
|
||||
|
||||
:param other_conversations: A list of other conversations to mark (default:
|
||||
``None``).
|
||||
|
||||
For example, to mark the conversation as unread along with other recent
|
||||
conversations from the same user:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit = reddit.subreddit("test")
|
||||
conversation = subreddit.modmail.conversation("2gmz")
|
||||
conversation.unread(other_conversations=conversation.user.recent_convos)
|
||||
|
||||
"""
|
||||
data = {"conversationIds": self._build_conversation_list(other_conversations)}
|
||||
self._reddit.post(API_PATH["modmail_unread"], data=data)
|
||||
|
||||
|
||||
class ModmailAction(ModmailObject):
|
||||
"""A class for moderator actions on modmail conversations."""
|
||||
|
||||
|
||||
class ModmailMessage(ModmailObject):
|
||||
"""A class for modmail messages."""
|
||||
83
backend/venv/Lib/site-packages/praw/models/reddit/more.py
Normal file
83
backend/venv/Lib/site-packages/praw/models/reddit/more.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""Provide the MoreComments class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ...const import API_PATH
|
||||
from ...util import _deprecate_args
|
||||
from ..base import PRAWBase
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class MoreComments(PRAWBase):
|
||||
"""A class indicating there are more comments."""
|
||||
|
||||
def __eq__(self, other: str | MoreComments) -> bool:
|
||||
"""Return ``True`` if these :class:`.MoreComments` instances are the same."""
|
||||
if isinstance(other, self.__class__):
|
||||
return self.count == other.count and self.children == other.children
|
||||
return super().__eq__(other)
|
||||
|
||||
def __init__(self, reddit: praw.Reddit, _data: dict[str, Any]):
|
||||
"""Initialize a :class:`.MoreComments` instance."""
|
||||
self.count = self.parent_id = None
|
||||
self.children = []
|
||||
super().__init__(reddit, _data=_data)
|
||||
self._comments = None
|
||||
self.submission = None
|
||||
|
||||
def __lt__(self, other: MoreComments) -> bool:
|
||||
"""Provide a sort order on the :class:`.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.
|
||||
return self.count > other.count
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return an object initialization representation of the instance."""
|
||||
children = self.children[:4]
|
||||
if len(self.children) > 4:
|
||||
children[-1] = "..."
|
||||
return f"<{self.__class__.__name__} count={self.count}, children={children!r}>"
|
||||
|
||||
def _continue_comments(self, update: bool):
|
||||
assert not self.children, "Please file a bug report with PRAW."
|
||||
parent = self._load_comment(self.parent_id.split("_", 1)[1])
|
||||
self._comments = parent.replies
|
||||
if update:
|
||||
for comment in self._comments:
|
||||
comment.submission = self.submission
|
||||
return self._comments
|
||||
|
||||
def _load_comment(self, comment_id: str):
|
||||
path = f"{API_PATH['submission'].format(id=self.submission.id)}_/{comment_id}"
|
||||
_, comments = self._reddit.get(
|
||||
path,
|
||||
params={
|
||||
"limit": self.submission.comment_limit,
|
||||
"sort": self.submission.comment_sort,
|
||||
},
|
||||
)
|
||||
assert len(comments.children) == 1, "Please file a bug report with PRAW."
|
||||
return comments.children[0]
|
||||
|
||||
@_deprecate_args("update")
|
||||
def comments(self, *, update: bool = True) -> list[praw.models.Comment]:
|
||||
"""Fetch and return the comments for a single :class:`.MoreComments` object."""
|
||||
if self._comments is None:
|
||||
if self.count == 0: # Handle "continue this thread"
|
||||
return self._continue_comments(update)
|
||||
assert self.children, "Please file a bug report with PRAW."
|
||||
data = {
|
||||
"children": ",".join(self.children),
|
||||
"link_id": self.submission.fullname,
|
||||
"sort": self.submission.comment_sort,
|
||||
}
|
||||
self._comments = self._reddit.post(API_PATH["morechildren"], data=data)
|
||||
if update:
|
||||
for comment in self._comments:
|
||||
comment.submission = self.submission
|
||||
return self._comments
|
||||
240
backend/venv/Lib/site-packages/praw/models/reddit/multi.py
Normal file
240
backend/venv/Lib/site-packages/praw/models/reddit/multi.py
Normal file
@@ -0,0 +1,240 @@
|
||||
"""Provide the Multireddit class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from json import dumps
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ...const import API_PATH
|
||||
from ...util import _deprecate_args, cachedproperty
|
||||
from ..listing.mixins import SubredditListingMixin
|
||||
from .base import RedditBase
|
||||
from .redditor import Redditor
|
||||
from .subreddit import Subreddit, SubredditStream
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class Multireddit(SubredditListingMixin, RedditBase):
|
||||
r"""A class for users' multireddits.
|
||||
|
||||
This is referred to as a "Custom Feed" on the Reddit UI.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
==================== ==============================================================
|
||||
Attribute Description
|
||||
==================== ==============================================================
|
||||
``can_edit`` A ``bool`` representing whether or not the authenticated user
|
||||
may edit the multireddit.
|
||||
``copied_from`` The multireddit that the multireddit was copied from, if it
|
||||
exists, otherwise ``None``.
|
||||
``created_utc`` When the multireddit was created, in `Unix Time`_.
|
||||
``description_html`` The description of the multireddit, as HTML.
|
||||
``description_md`` The description of the multireddit, as Markdown.
|
||||
``display_name`` The display name of the multireddit.
|
||||
``name`` The name of the multireddit.
|
||||
``over_18`` A ``bool`` representing whether or not the multireddit is
|
||||
restricted for users over 18.
|
||||
``subreddits`` A list of :class:`.Subreddit`\ s that make up the multireddit.
|
||||
``visibility`` The visibility of the multireddit, either ``"private"``,
|
||||
``"public"``, or ``"hidden"``.
|
||||
==================== ==============================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
STR_FIELD = "path"
|
||||
RE_INVALID = re.compile(r"[\W_]+", re.UNICODE)
|
||||
|
||||
@staticmethod
|
||||
def sluggify(title: str) -> str:
|
||||
"""Return a slug version of the title.
|
||||
|
||||
:param title: The title to make a slug of.
|
||||
|
||||
Adapted from Reddit's utils.py.
|
||||
|
||||
"""
|
||||
title = Multireddit.RE_INVALID.sub("_", title).strip("_").lower()
|
||||
if len(title) > 21: # truncate to nearest word
|
||||
title = title[:21]
|
||||
last_word = title.rfind("_")
|
||||
if last_word > 0:
|
||||
title = title[:last_word]
|
||||
return title or "_"
|
||||
|
||||
@cachedproperty
|
||||
def stream(self) -> SubredditStream:
|
||||
"""Provide an instance of :class:`.SubredditStream`.
|
||||
|
||||
Streams can be used to indefinitely retrieve new comments made to a multireddit,
|
||||
like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for comment in reddit.multireddit(redditor="spez", name="fun").stream.comments():
|
||||
print(comment)
|
||||
|
||||
Additionally, new submissions can be retrieved via the stream. In the following
|
||||
example all new submissions to the multireddit are fetched:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for submission in reddit.multireddit(
|
||||
redditor="bboe", name="games"
|
||||
).stream.submissions():
|
||||
print(submission)
|
||||
|
||||
"""
|
||||
return SubredditStream(self)
|
||||
|
||||
def __init__(self, reddit: praw.Reddit, _data: dict[str, Any]):
|
||||
"""Initialize a :class:`.Multireddit` instance."""
|
||||
self.path = None
|
||||
super().__init__(reddit, _data=_data)
|
||||
self._author = Redditor(reddit, self.path.split("/", 3)[2])
|
||||
self._path = API_PATH["multireddit"].format(multi=self.name, user=self._author)
|
||||
self.path = f"/{self._path[:-1]}" # Prevent requests for path
|
||||
if "subreddits" in self.__dict__:
|
||||
self.subreddits = [Subreddit(reddit, x["name"]) for x in self.subreddits]
|
||||
|
||||
def _fetch(self):
|
||||
data = self._fetch_data()
|
||||
data = data["data"]
|
||||
other = type(self)(self._reddit, _data=data)
|
||||
self.__dict__.update(other.__dict__)
|
||||
super()._fetch()
|
||||
|
||||
def _fetch_info(self):
|
||||
return (
|
||||
"multireddit_api",
|
||||
{"multi": self.name, "user": self._author.name},
|
||||
None,
|
||||
)
|
||||
|
||||
def add(self, subreddit: praw.models.Subreddit):
|
||||
"""Add a subreddit to this multireddit.
|
||||
|
||||
:param subreddit: The subreddit to add to this multi.
|
||||
|
||||
For example, to add r/test to multireddit ``bboe/test``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit = reddit.subreddit("test")
|
||||
reddit.multireddit(redditor="bboe", name="test").add(subreddit)
|
||||
|
||||
"""
|
||||
url = API_PATH["multireddit_update"].format(
|
||||
multi=self.name, user=self._author, subreddit=subreddit
|
||||
)
|
||||
self._reddit.put(url, data={"model": dumps({"name": str(subreddit)})})
|
||||
self._reset_attributes("subreddits")
|
||||
|
||||
@_deprecate_args("display_name")
|
||||
def copy(self, *, display_name: str | None = None) -> praw.models.Multireddit:
|
||||
"""Copy this multireddit and return the new multireddit.
|
||||
|
||||
:param display_name: The display name for the copied multireddit. Reddit will
|
||||
generate the ``name`` field from this display name. When not provided the
|
||||
copy will use the same display name and name as this multireddit.
|
||||
|
||||
To copy the multireddit ``bboe/test`` with a name of ``"testing"``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.multireddit(redditor="bboe", name="test").copy(display_name="testing")
|
||||
|
||||
"""
|
||||
if display_name:
|
||||
name = self.sluggify(display_name)
|
||||
else:
|
||||
display_name = self.display_name
|
||||
name = self.name
|
||||
data = {
|
||||
"display_name": display_name,
|
||||
"from": self.path,
|
||||
"to": API_PATH["multireddit"].format(
|
||||
multi=name, user=self._reddit.user.me()
|
||||
),
|
||||
}
|
||||
return self._reddit.post(API_PATH["multireddit_copy"], data=data)
|
||||
|
||||
def delete(self):
|
||||
"""Delete this multireddit.
|
||||
|
||||
For example, to delete multireddit ``bboe/test``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.multireddit(redditor="bboe", name="test").delete()
|
||||
|
||||
"""
|
||||
path = API_PATH["multireddit_api"].format(
|
||||
multi=self.name, user=self._author.name
|
||||
)
|
||||
self._reddit.delete(path)
|
||||
|
||||
def remove(self, subreddit: praw.models.Subreddit):
|
||||
"""Remove a subreddit from this multireddit.
|
||||
|
||||
:param subreddit: The subreddit to remove from this multi.
|
||||
|
||||
For example, to remove r/test from multireddit ``bboe/test``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit = reddit.subreddit("test")
|
||||
reddit.multireddit(redditor="bboe", name="test").remove(subreddit)
|
||||
|
||||
"""
|
||||
url = API_PATH["multireddit_update"].format(
|
||||
multi=self.name, user=self._author, subreddit=subreddit
|
||||
)
|
||||
self._reddit.delete(url, data={"model": dumps({"name": str(subreddit)})})
|
||||
self._reset_attributes("subreddits")
|
||||
|
||||
def update(
|
||||
self,
|
||||
**updated_settings: str | list[str | praw.models.Subreddit | dict[str, str]],
|
||||
):
|
||||
"""Update this multireddit.
|
||||
|
||||
Keyword arguments are passed for settings that should be updated. They can any
|
||||
of:
|
||||
|
||||
:param display_name: The display name for this multireddit. Must be no longer
|
||||
than 50 characters.
|
||||
:param subreddits: Subreddits for this multireddit.
|
||||
:param description_md: Description for this multireddit, formatted in Markdown.
|
||||
:param icon_name: Can be one of: ``"art and design"``, ``"ask"``, ``"books"``,
|
||||
``"business"``, ``"cars"``, ``"comics"``, ``"cute animals"``, ``"diy"``,
|
||||
``"entertainment"``, ``"food and drink"``, ``"funny"``, ``"games"``,
|
||||
``"grooming"``, ``"health"``, ``"life advice"``, ``"military"``, ``"models
|
||||
pinup"``, ``"music"``, ``"news"``, ``"philosophy"``, ``"pictures and
|
||||
gifs"``, ``"science"``, ``"shopping"``, ``"sports"``, ``"style"``,
|
||||
``"tech"``, ``"travel"``, ``"unusual stories"``, ``"video"``, or ``None``.
|
||||
:param key_color: RGB hex color code of the form ``"#FFFFFF"``.
|
||||
:param visibility: Can be one of: ``"hidden"``, ``"private"``, or ``"public"``.
|
||||
:param weighting_scheme: Can be one of: ``"classic"`` or ``"fresh"``.
|
||||
|
||||
For example, to rename multireddit ``"bboe/test"`` to ``"bboe/testing"``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.multireddit(redditor="bboe", name="test").update(display_name="testing")
|
||||
|
||||
"""
|
||||
if "subreddits" in updated_settings:
|
||||
updated_settings["subreddits"] = [
|
||||
{"name": str(sub)} for sub in updated_settings["subreddits"]
|
||||
]
|
||||
path = API_PATH["multireddit_api"].format(
|
||||
multi=self.name, user=self._author.name
|
||||
)
|
||||
new = self._reddit.put(path, data={"model": dumps(updated_settings)})
|
||||
self.__dict__.update(new.__dict__)
|
||||
112
backend/venv/Lib/site-packages/praw/models/reddit/poll.py
Normal file
112
backend/venv/Lib/site-packages/praw/models/reddit/poll.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""Provide poll-related classes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from ...util import cachedproperty
|
||||
from ..base import PRAWBase
|
||||
|
||||
|
||||
class PollOption(PRAWBase):
|
||||
"""Class to represent one option of a poll.
|
||||
|
||||
If ``submission`` is a poll :class:`.Submission`, access the poll's options like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
poll_data = submission.poll_data
|
||||
|
||||
# By index -- print the first option
|
||||
print(poll_data.options[0])
|
||||
|
||||
# By ID -- print the option with ID "576797"
|
||||
print(poll_data.option("576797"))
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
============== =================================================
|
||||
Attribute Description
|
||||
============== =================================================
|
||||
``id`` ID of the poll option.
|
||||
``text`` The text of the poll option.
|
||||
``vote_count`` The number of votes the poll option has received.
|
||||
============== =================================================
|
||||
|
||||
"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return an object initialization representation of the instance."""
|
||||
return f"PollOption(id={self.id!r})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return a string version of the PollData, its text."""
|
||||
return self.text
|
||||
|
||||
|
||||
class PollData(PRAWBase):
|
||||
"""Class to represent poll data on a poll submission.
|
||||
|
||||
If ``submission`` is a poll :class:`.Submission`, access the poll data like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
poll_data = submission.poll_data
|
||||
print(f"There are {poll_data.total_vote_count} votes total.")
|
||||
print("The options are:")
|
||||
for option in poll_data.options:
|
||||
print(f"{option} ({option.vote_count} votes)")
|
||||
print(f"I voted for {poll_data.user_selection}.")
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
======================== =========================================================
|
||||
Attribute Description
|
||||
======================== =========================================================
|
||||
``options`` A list of :class:`.PollOption` of the poll.
|
||||
``total_vote_count`` The total number of votes cast in the poll.
|
||||
``user_selection`` The poll option selected by the authenticated user
|
||||
(possibly ``None``).
|
||||
``voting_end_timestamp`` Time the poll voting closes, represented in `Unix Time`_.
|
||||
======================== =========================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
@cachedproperty
|
||||
def user_selection(self) -> PollOption | None:
|
||||
"""Get the user's selection in this poll, if any.
|
||||
|
||||
:returns: The user's selection as a :class:`.PollOption`, or ``None`` if there
|
||||
is no choice.
|
||||
|
||||
"""
|
||||
if self._user_selection is None:
|
||||
return None
|
||||
return self.option(self._user_selection)
|
||||
|
||||
def __setattr__(self, attribute: str, value: Any):
|
||||
"""Objectify the options attribute, and save user_selection."""
|
||||
if attribute == "options" and isinstance(value, list):
|
||||
value = [PollOption(self._reddit, option) for option in value]
|
||||
elif attribute == "user_selection":
|
||||
attribute = "_user_selection"
|
||||
super().__setattr__(attribute, value)
|
||||
|
||||
def option(self, option_id: str) -> PollOption:
|
||||
"""Get the option with the specified ID.
|
||||
|
||||
:param option_id: The ID of a poll option, as a ``str``.
|
||||
|
||||
:returns: The specified :class:`.PollOption`.
|
||||
|
||||
:raises: :py:class:`KeyError` if no option exists with the specified ID.
|
||||
|
||||
"""
|
||||
for option in self.options:
|
||||
if option.id == option_id:
|
||||
return option
|
||||
|
||||
msg = f"No poll option with ID {option_id!r}."
|
||||
raise KeyError(msg)
|
||||
494
backend/venv/Lib/site-packages/praw/models/reddit/redditor.py
Normal file
494
backend/venv/Lib/site-packages/praw/models/reddit/redditor.py
Normal file
@@ -0,0 +1,494 @@
|
||||
"""Provide the Redditor class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from json import dumps
|
||||
from typing import TYPE_CHECKING, Any, Generator
|
||||
|
||||
from ...const import API_PATH
|
||||
from ...util import _deprecate_args
|
||||
from ...util.cache import cachedproperty
|
||||
from ..listing.mixins import RedditorListingMixin
|
||||
from ..util import stream_generator
|
||||
from .base import RedditBase
|
||||
from .mixins import FullnameMixin, MessageableMixin
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class Redditor(MessageableMixin, RedditorListingMixin, FullnameMixin, RedditBase):
|
||||
"""A class representing the users of Reddit.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
.. note::
|
||||
|
||||
Shadowbanned accounts are treated the same as non-existent accounts, meaning
|
||||
that they will not have any attributes.
|
||||
|
||||
.. note::
|
||||
|
||||
Suspended/banned accounts will only return the ``name`` and ``is_suspended``
|
||||
attributes.
|
||||
|
||||
=================================== ================================================
|
||||
Attribute Description
|
||||
=================================== ================================================
|
||||
``comment_karma`` The comment karma for the :class:`.Redditor`.
|
||||
``comments`` Provide an instance of :class:`.SubListing` for
|
||||
comment access.
|
||||
``submissions`` Provide an instance of :class:`.SubListing` for
|
||||
submission access.
|
||||
``created_utc`` Time the account was created, represented in
|
||||
`Unix Time`_.
|
||||
``has_verified_email`` Whether or not the :class:`.Redditor` has
|
||||
verified their email.
|
||||
``icon_img`` The url of the Redditors' avatar.
|
||||
``id`` The ID of the :class:`.Redditor`.
|
||||
``is_employee`` Whether or not the :class:`.Redditor` is a
|
||||
Reddit employee.
|
||||
``is_friend`` Whether or not the :class:`.Redditor` is friends
|
||||
with the authenticated user.
|
||||
``is_mod`` Whether or not the :class:`.Redditor` mods any
|
||||
subreddits.
|
||||
``is_gold`` Whether or not the :class:`.Redditor` has active
|
||||
Reddit Premium status.
|
||||
``is_suspended`` Whether or not the :class:`.Redditor` is
|
||||
currently suspended.
|
||||
``link_karma`` The link karma for the :class:`.Redditor`.
|
||||
``name`` The Redditor's username.
|
||||
``subreddit`` If the :class:`.Redditor` has created a
|
||||
user-subreddit, provides a dictionary of
|
||||
additional attributes. See below.
|
||||
``subreddit["banner_img"]`` The URL of the user-subreddit banner.
|
||||
``subreddit["name"]`` The fullname of the user-subreddit.
|
||||
``subreddit["over_18"]`` Whether or not the user-subreddit is NSFW.
|
||||
``subreddit["public_description"]`` The public description of the user-subreddit.
|
||||
``subreddit["subscribers"]`` The number of users subscribed to the
|
||||
user-subreddit.
|
||||
``subreddit["title"]`` The title of the user-subreddit.
|
||||
=================================== ================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
STR_FIELD = "name"
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, reddit: praw.Reddit, data: dict[str, Any]) -> Redditor | None:
|
||||
"""Return an instance of :class:`.Redditor`, or ``None`` from ``data``."""
|
||||
if data == "[deleted]":
|
||||
return None
|
||||
return cls(reddit, data)
|
||||
|
||||
@cachedproperty
|
||||
def notes(self) -> praw.models.RedditorModNotes:
|
||||
"""Provide an instance of :class:`.RedditorModNotes`.
|
||||
|
||||
This provides an interface for managing moderator notes for a redditor.
|
||||
|
||||
.. note::
|
||||
|
||||
The authenticated user must be a moderator of the provided subreddit(s).
|
||||
|
||||
For example, all the notes for u/spez in r/test can be iterated through like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
redditor = reddit.redditor("spez")
|
||||
|
||||
for note in redditor.notes.subreddits("test"):
|
||||
print(f"{note.label}: {note.note}")
|
||||
|
||||
"""
|
||||
from praw.models.mod_notes import RedditorModNotes
|
||||
|
||||
return RedditorModNotes(self._reddit, self)
|
||||
|
||||
@cachedproperty
|
||||
def stream(self) -> praw.models.reddit.redditor.RedditorStream:
|
||||
"""Provide an instance of :class:`.RedditorStream`.
|
||||
|
||||
Streams can be used to indefinitely retrieve new comments made by a redditor,
|
||||
like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for comment in reddit.redditor("spez").stream.comments():
|
||||
print(comment)
|
||||
|
||||
Additionally, new submissions can be retrieved via the stream. In the following
|
||||
example all submissions are fetched via the redditor u/spez:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for submission in reddit.redditor("spez").stream.submissions():
|
||||
print(submission)
|
||||
|
||||
"""
|
||||
return RedditorStream(self)
|
||||
|
||||
@property
|
||||
def _kind(self) -> str:
|
||||
"""Return the class's kind."""
|
||||
return self._reddit.config.kinds["redditor"]
|
||||
|
||||
@property
|
||||
def _path(self) -> str:
|
||||
return API_PATH["user"].format(user=self)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
name: str | None = None,
|
||||
fullname: str | None = None,
|
||||
_data: dict[str, Any] | None = None,
|
||||
):
|
||||
"""Initialize a :class:`.Redditor` instance.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
:param name: The name of the redditor.
|
||||
:param fullname: The fullname of the redditor, starting with ``t2_``.
|
||||
|
||||
Exactly one of ``name``, ``fullname`` or ``_data`` must be provided.
|
||||
|
||||
"""
|
||||
if (name, fullname, _data).count(None) != 2:
|
||||
msg = "Exactly one of 'name', 'fullname', or '_data' must be provided."
|
||||
raise TypeError(msg)
|
||||
if _data:
|
||||
assert ( # noqa: PT018
|
||||
isinstance(_data, dict) and "name" in _data
|
||||
), "Please file a bug with PRAW."
|
||||
self._listing_use_sort = True
|
||||
if name:
|
||||
self.name = name
|
||||
elif fullname:
|
||||
self._fullname = fullname
|
||||
super().__init__(reddit, _data=_data, _extra_attribute_to_check="_fullname")
|
||||
|
||||
def __setattr__(self, name: str, value: Any):
|
||||
"""Objectify the subreddit attribute."""
|
||||
if name == "subreddit" and value:
|
||||
from .user_subreddit import UserSubreddit
|
||||
|
||||
value = UserSubreddit(reddit=self._reddit, _data=value)
|
||||
super().__setattr__(name, value)
|
||||
|
||||
def _fetch(self):
|
||||
data = self._fetch_data()
|
||||
data = data["data"]
|
||||
other = type(self)(self._reddit, _data=data)
|
||||
self.__dict__.update(other.__dict__)
|
||||
super()._fetch()
|
||||
|
||||
def _fetch_info(self):
|
||||
if hasattr(self, "_fullname"):
|
||||
self.name = self._fetch_username(self._fullname)
|
||||
return "user_about", {"user": self.name}, None
|
||||
|
||||
def _fetch_username(self, fullname: str):
|
||||
return self._reddit.get(API_PATH["user_by_fullname"], params={"ids": fullname})[
|
||||
fullname
|
||||
]["name"]
|
||||
|
||||
def _friend(self, *, data: dict[str, Any], method: str):
|
||||
url = API_PATH["friend_v1"].format(user=self)
|
||||
self._reddit.request(data=dumps(data), method=method, path=url)
|
||||
|
||||
def block(self):
|
||||
"""Block the :class:`.Redditor`.
|
||||
|
||||
For example, to block :class:`.Redditor` u/spez:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.redditor("spez").block()
|
||||
|
||||
.. note::
|
||||
|
||||
Blocking a trusted user will remove that user from your trusted list.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.trust`
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["block_user"], params={"name": self.name})
|
||||
|
||||
def distrust(self):
|
||||
"""Remove the :class:`.Redditor` from your whitelist of trusted users.
|
||||
|
||||
For example, to remove :class:`.Redditor` u/spez from your whitelist:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.redditor("spez").distrust()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.trust`
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["remove_whitelisted"], data={"name": self.name})
|
||||
|
||||
@_deprecate_args("note")
|
||||
def friend(self, *, note: str = None):
|
||||
"""Friend the :class:`.Redditor`.
|
||||
|
||||
:param note: A note to save along with the relationship. Requires Reddit Premium
|
||||
(default: ``None``).
|
||||
|
||||
Calling this method subsequent times will update the note.
|
||||
|
||||
For example, to friend u/spez:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.redditor("spez").friend()
|
||||
|
||||
To add a note to the friendship (requires Reddit Premium):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.redditor("spez").friend(note="My favorite admin")
|
||||
|
||||
"""
|
||||
self._friend(data={"note": note} if note else {}, method="PUT")
|
||||
|
||||
def friend_info(self) -> praw.models.Redditor:
|
||||
"""Return a :class:`.Redditor` instance with specific friend-related attributes.
|
||||
|
||||
:returns: A :class:`.Redditor` instance with fields ``date``, ``id``, and
|
||||
possibly ``note`` if the authenticated user has Reddit Premium.
|
||||
|
||||
For example, to get the friendship information of :class:`.Redditor` u/spez:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
info = reddit.redditor("spez").friend_info
|
||||
friend_data = info.date
|
||||
|
||||
"""
|
||||
return self._reddit.get(API_PATH["friend_v1"].format(user=self))
|
||||
|
||||
@_deprecate_args("months")
|
||||
def gild(self, *, months: int = 1):
|
||||
"""Gild the :class:`.Redditor`.
|
||||
|
||||
:param months: Specifies the number of months to gild up to 36 (default: ``1``).
|
||||
|
||||
For example, to gild :class:`.Redditor` u/spez for 1 month:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.redditor("spez").gild(months=1)
|
||||
|
||||
"""
|
||||
if months < 1 or months > 36:
|
||||
msg = "months must be between 1 and 36"
|
||||
raise TypeError(msg)
|
||||
self._reddit.post(
|
||||
API_PATH["gild_user"].format(username=self), data={"months": months}
|
||||
)
|
||||
|
||||
def moderated(self) -> list[praw.models.Subreddit]:
|
||||
"""Return a list of the redditor's moderated subreddits.
|
||||
|
||||
:returns: A list of :class:`.Subreddit` objects. Return ``[]`` if the redditor
|
||||
has no moderated subreddits.
|
||||
|
||||
:raises: ``prawcore.ServerError`` in certain circumstances. See the note below.
|
||||
|
||||
.. note::
|
||||
|
||||
The redditor's own user profile subreddit will not be returned, but other
|
||||
user profile subreddits they moderate will be returned.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for subreddit in reddit.redditor("spez").moderated():
|
||||
print(subreddit.display_name)
|
||||
print(subreddit.title)
|
||||
|
||||
.. note::
|
||||
|
||||
A ``prawcore.ServerError`` exception may be raised if the redditor moderates
|
||||
a large number of subreddits. If that happens, try switching to
|
||||
:ref:`read-only mode <read_only_application>`. For example,
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.read_only = True
|
||||
for subreddit in reddit.redditor("reddit").moderated():
|
||||
print(str(subreddit))
|
||||
|
||||
It is possible that requests made in read-only mode will also raise a
|
||||
``prawcore.ServerError`` exception.
|
||||
|
||||
When used in read-only mode, this method does not retrieve information about
|
||||
subreddits that require certain special permissions to access, e.g., private
|
||||
subreddits and premium-only subreddits.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.User.moderator_subreddits`
|
||||
|
||||
"""
|
||||
return self._reddit.get(API_PATH["moderated"].format(user=self)) or []
|
||||
|
||||
def multireddits(self) -> list[praw.models.Multireddit]:
|
||||
"""Return a list of the redditor's public multireddits.
|
||||
|
||||
For example, to to get :class:`.Redditor` u/spez's multireddits:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
multireddits = reddit.redditor("spez").multireddits()
|
||||
|
||||
"""
|
||||
return self._reddit.get(API_PATH["multireddit_user"].format(user=self))
|
||||
|
||||
def trophies(self) -> list[praw.models.Trophy]:
|
||||
"""Return a list of the redditor's trophies.
|
||||
|
||||
:returns: A list of :class:`.Trophy` objects. Return ``[]`` if the redditor has
|
||||
no trophies.
|
||||
|
||||
:raises: :class:`.RedditAPIException` if the redditor doesn't exist.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for trophy in reddit.redditor("spez").trophies():
|
||||
print(trophy.name)
|
||||
print(trophy.description)
|
||||
|
||||
"""
|
||||
return list(self._reddit.get(API_PATH["trophies"].format(user=self)))
|
||||
|
||||
def trust(self):
|
||||
"""Add the :class:`.Redditor` to your whitelist of trusted users.
|
||||
|
||||
Trusted users will always be able to send you PMs.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.redditor("AaronSw").trust()
|
||||
|
||||
Use the ``accept_pms`` parameter of :meth:`.Preferences.update` to toggle your
|
||||
``accept_pms`` setting between ``"everyone"`` and ``"whitelisted"``. For
|
||||
example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Accept private messages from everyone:
|
||||
reddit.user.preferences.update(accept_pms="everyone")
|
||||
# Only accept private messages from trusted users:
|
||||
reddit.user.preferences.update(accept_pms="whitelisted")
|
||||
|
||||
You may trust a user even if your ``accept_pms`` setting is switched to
|
||||
``"everyone"``.
|
||||
|
||||
.. note::
|
||||
|
||||
You are allowed to have a user on your blocked list and your friends list at
|
||||
the same time. However, you cannot trust a user who is on your blocked list.
|
||||
|
||||
.. seealso::
|
||||
|
||||
- :meth:`.distrust`
|
||||
- :meth:`.Preferences.update`
|
||||
- :meth:`.trusted`
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["add_whitelisted"], data={"name": self.name})
|
||||
|
||||
def unblock(self):
|
||||
"""Unblock the :class:`.Redditor`.
|
||||
|
||||
For example, to unblock :class:`.Redditor` u/spez:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.redditor("spez").unblock()
|
||||
|
||||
"""
|
||||
data = {
|
||||
"container": self._reddit.user.me().fullname,
|
||||
"name": str(self),
|
||||
"type": "enemy",
|
||||
}
|
||||
url = API_PATH["unfriend"].format(subreddit="all")
|
||||
self._reddit.post(url, data=data)
|
||||
|
||||
def unfriend(self):
|
||||
"""Unfriend the :class:`.Redditor`.
|
||||
|
||||
For example, to unfriend :class:`.Redditor` u/spez:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.redditor("spez").unfriend()
|
||||
|
||||
"""
|
||||
self._friend(data={"id": str(self)}, method="DELETE")
|
||||
|
||||
|
||||
class RedditorStream:
|
||||
"""Provides submission and comment streams."""
|
||||
|
||||
def __init__(self, redditor: praw.models.Redditor):
|
||||
"""Initialize a :class:`.RedditorStream` instance.
|
||||
|
||||
:param redditor: The redditor associated with the streams.
|
||||
|
||||
"""
|
||||
self.redditor = redditor
|
||||
|
||||
def comments(
|
||||
self, **stream_options: str | int | dict[str, str]
|
||||
) -> Generator[praw.models.Comment, None, None]:
|
||||
"""Yield new comments as they become available.
|
||||
|
||||
Comments are yielded oldest first. Up to 100 historical comments will initially
|
||||
be returned.
|
||||
|
||||
Keyword arguments are passed to :func:`.stream_generator`.
|
||||
|
||||
For example, to retrieve all new comments made by redditor u/spez, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for comment in reddit.redditor("spez").stream.comments():
|
||||
print(comment)
|
||||
|
||||
"""
|
||||
return stream_generator(self.redditor.comments.new, **stream_options)
|
||||
|
||||
def submissions(
|
||||
self, **stream_options: str | int | dict[str, str]
|
||||
) -> Generator[praw.models.Submission, None, None]:
|
||||
"""Yield new submissions as they become available.
|
||||
|
||||
Submissions are yielded oldest first. Up to 100 historical submissions will
|
||||
initially be returned.
|
||||
|
||||
Keyword arguments are passed to :func:`.stream_generator`.
|
||||
|
||||
For example, to retrieve all new submissions made by redditor u/spez, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for submission in reddit.redditor("spez").stream.submissions():
|
||||
print(submission)
|
||||
|
||||
"""
|
||||
return stream_generator(self.redditor.submissions.new, **stream_options)
|
||||
@@ -0,0 +1,255 @@
|
||||
"""Provide the Removal Reason class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Iterator
|
||||
from warnings import warn
|
||||
|
||||
from ...const import API_PATH
|
||||
from ...exceptions import ClientException
|
||||
from ...util import _deprecate_args, cachedproperty
|
||||
from .base import RedditBase
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw
|
||||
|
||||
|
||||
class RemovalReason(RedditBase):
|
||||
"""An individual Removal Reason object.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
=========== ==================================
|
||||
Attribute Description
|
||||
=========== ==================================
|
||||
``id`` The ID of the removal reason.
|
||||
``message`` The message of the removal reason.
|
||||
``title`` The title of the removal reason.
|
||||
=========== ==================================
|
||||
|
||||
"""
|
||||
|
||||
STR_FIELD = "id"
|
||||
|
||||
@staticmethod
|
||||
def _warn_reason_id(
|
||||
*, id_value: str | None, reason_id_value: str | None
|
||||
) -> str | None:
|
||||
"""Reason ID param is deprecated. Warns if it's used.
|
||||
|
||||
:param id_value: Returns the actual value of parameter ``id`` is parameter
|
||||
``reason_id`` is not used.
|
||||
:param reason_id_value: The value passed as parameter ``reason_id``.
|
||||
|
||||
"""
|
||||
if reason_id_value is not None:
|
||||
warn(
|
||||
"Parameter 'reason_id' is deprecated. Either use positional arguments"
|
||||
' (e.g., reason_id="x" -> "x") or change the parameter name to \'id\''
|
||||
' (e.g., reason_id="x" -> id="x"). This parameter will be removed in'
|
||||
" PRAW 8.",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
return reason_id_value
|
||||
return id_value
|
||||
|
||||
def __eq__(self, other: str | RemovalReason) -> bool:
|
||||
"""Return whether the other instance equals the current."""
|
||||
if isinstance(other, str):
|
||||
return other == str(self)
|
||||
return isinstance(other, self.__class__) and str(self) == str(other)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""Return the hash of the current instance."""
|
||||
return hash(self.__class__.__name__) ^ hash(str(self))
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
subreddit: praw.models.Subreddit,
|
||||
id: str | None = None,
|
||||
reason_id: str | None = None,
|
||||
_data: dict[str, Any] | None = None,
|
||||
):
|
||||
"""Initialize a :class:`.RemovalReason` instance.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
:param subreddit: An instance of :class:`.Subreddit`.
|
||||
:param id: The ID of the removal reason.
|
||||
:param reason_id: The original name of the ``id`` parameter. Used for backwards
|
||||
compatibility. This parameter should not be used.
|
||||
|
||||
"""
|
||||
reason_id = self._warn_reason_id(id_value=id, reason_id_value=reason_id)
|
||||
if (reason_id, _data).count(None) != 1:
|
||||
msg = "Either id or _data needs to be given."
|
||||
raise ValueError(msg)
|
||||
|
||||
if reason_id:
|
||||
self.id = reason_id
|
||||
self.subreddit = subreddit
|
||||
super().__init__(reddit, _data=_data)
|
||||
|
||||
def _fetch(self):
|
||||
for removal_reason in self.subreddit.mod.removal_reasons:
|
||||
if removal_reason.id == self.id:
|
||||
self.__dict__.update(removal_reason.__dict__)
|
||||
super()._fetch()
|
||||
return
|
||||
msg = f"Subreddit {self.subreddit} does not have the removal reason {self.id}"
|
||||
raise ClientException(msg)
|
||||
|
||||
def delete(self):
|
||||
"""Delete a removal reason from this subreddit.
|
||||
|
||||
To delete ``"141vv5c16py7d"`` from r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").mod.removal_reasons["141vv5c16py7d"].delete()
|
||||
|
||||
"""
|
||||
url = API_PATH["removal_reason"].format(subreddit=self.subreddit, id=self.id)
|
||||
self._reddit.delete(url)
|
||||
|
||||
@_deprecate_args("message", "title")
|
||||
def update(self, *, message: str | None = None, title: str | None = None):
|
||||
"""Update the removal reason from this subreddit.
|
||||
|
||||
.. note::
|
||||
|
||||
Existing values will be used for any unspecified arguments.
|
||||
|
||||
:param message: The removal reason's new message.
|
||||
:param title: The removal reason's new title.
|
||||
|
||||
To update ``"141vv5c16py7d"`` from r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").mod.removal_reasons["141vv5c16py7d"].update(
|
||||
title="New title", message="New message"
|
||||
)
|
||||
|
||||
"""
|
||||
url = API_PATH["removal_reason"].format(subreddit=self.subreddit, id=self.id)
|
||||
data = {
|
||||
name: getattr(self, name) if value is None else value
|
||||
for name, value in {"message": message, "title": title}.items()
|
||||
}
|
||||
self._reddit.put(url, data=data)
|
||||
|
||||
|
||||
class SubredditRemovalReasons:
|
||||
"""Provide a set of functions to a :class:`.Subreddit`'s removal reasons."""
|
||||
|
||||
@cachedproperty
|
||||
def _removal_reason_list(self) -> list[RemovalReason]:
|
||||
"""Get a list of Removal Reason objects.
|
||||
|
||||
:returns: A list of instances of :class:`.RemovalReason`.
|
||||
|
||||
"""
|
||||
response = self._reddit.get(
|
||||
API_PATH["removal_reasons_list"].format(subreddit=self.subreddit)
|
||||
)
|
||||
return [
|
||||
RemovalReason(
|
||||
self._reddit, self.subreddit, _data=response["data"][reason_id]
|
||||
)
|
||||
for reason_id in response["order"]
|
||||
]
|
||||
|
||||
def __getitem__(self, reason_id: str | int | slice) -> RemovalReason:
|
||||
"""Return the Removal Reason with the ID/number/slice ``reason_id``.
|
||||
|
||||
:param reason_id: The ID or index of the removal reason
|
||||
|
||||
.. note::
|
||||
|
||||
Removal reasons fetched using a specific rule name are lazily loaded, so you
|
||||
might have to access an attribute to get all the expected attributes.
|
||||
|
||||
This method is to be used to fetch a specific removal reason, like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reason_id = "141vv5c16py7d"
|
||||
reason = reddit.subreddit("test").mod.removal_reasons[reason_id]
|
||||
print(reason)
|
||||
|
||||
You can also use indices to get a numbered removal reason. Since Python uses
|
||||
0-indexing, the first removal reason is index 0, and so on.
|
||||
|
||||
.. note::
|
||||
|
||||
Both negative indices and slices can be used to interact with the removal
|
||||
reasons.
|
||||
|
||||
:raises: :py:class:`IndexError` if a removal reason of a specific number does
|
||||
not exist.
|
||||
|
||||
For example, to get the second removal reason of r/test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reason = reddit.subreddit("test").mod.removal_reasons[1]
|
||||
|
||||
To get the last three removal reasons in a subreddit:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reasons = reddit.subreddit("test").mod.removal_reasons[-3:]
|
||||
for reason in reasons:
|
||||
print(reason)
|
||||
|
||||
"""
|
||||
if not isinstance(reason_id, str):
|
||||
return self._removal_reason_list[reason_id]
|
||||
return RemovalReason(self._reddit, self.subreddit, reason_id)
|
||||
|
||||
def __init__(self, subreddit: praw.models.Subreddit):
|
||||
"""Initialize a :class:`.SubredditRemovalReasons` instance.
|
||||
|
||||
:param subreddit: The subreddit whose removal reasons to work with.
|
||||
|
||||
"""
|
||||
self.subreddit = subreddit
|
||||
self._reddit = subreddit._reddit
|
||||
|
||||
def __iter__(self) -> Iterator[RemovalReason]:
|
||||
"""Return a list of Removal Reasons for the subreddit.
|
||||
|
||||
This method is used to discover all removal reasons for a subreddit:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for removal_reason in reddit.subreddit("test").mod.removal_reasons:
|
||||
print(removal_reason)
|
||||
|
||||
"""
|
||||
return iter(self._removal_reason_list)
|
||||
|
||||
@_deprecate_args("message", "title")
|
||||
def add(self, *, message: str, title: str) -> RemovalReason:
|
||||
"""Add a removal reason to this subreddit.
|
||||
|
||||
:param message: The message associated with the removal reason.
|
||||
:param title: The title of the removal reason.
|
||||
|
||||
:returns: The :class:`.RemovalReason` added.
|
||||
|
||||
The message will be prepended with ``Hi u/username,`` automatically.
|
||||
|
||||
To add ``"Test"`` to r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").mod.removal_reasons.add(title="Test", message="Foobar")
|
||||
|
||||
"""
|
||||
data = {"message": message, "title": title}
|
||||
url = API_PATH["removal_reasons_list"].format(subreddit=self.subreddit)
|
||||
reason_id = self._reddit.post(url, data=data)
|
||||
return RemovalReason(self._reddit, self.subreddit, reason_id)
|
||||
452
backend/venv/Lib/site-packages/praw/models/reddit/rules.py
Normal file
452
backend/venv/Lib/site-packages/praw/models/reddit/rules.py
Normal file
@@ -0,0 +1,452 @@
|
||||
"""Provide the Rule class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Iterator
|
||||
from urllib.parse import quote
|
||||
from warnings import warn
|
||||
|
||||
from ...const import API_PATH
|
||||
from ...exceptions import ClientException
|
||||
from ...util import _deprecate_args, cachedproperty
|
||||
from .base import RedditBase
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class Rule(RedditBase):
|
||||
"""An individual :class:`.Rule` object.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
==================== =============================================================
|
||||
Attribute Description
|
||||
==================== =============================================================
|
||||
``created_utc`` Time the rule was created, represented in `Unix Time`_.
|
||||
``description`` The description of the rule, if provided, otherwise a blank
|
||||
string.
|
||||
``kind`` The kind of rule. Can be ``"link"``, ``comment"``, or
|
||||
``"all"``.
|
||||
``priority`` Represents where the rule is ranked. For example, the first
|
||||
rule is at priority ``0``. Serves as an index number on the
|
||||
list of rules.
|
||||
``short_name`` The name of the rule.
|
||||
``violation_reason`` The reason that is displayed on the report menu for the rule.
|
||||
==================== =============================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
STR_FIELD = "short_name"
|
||||
|
||||
@cachedproperty
|
||||
def mod(self) -> praw.models.reddit.rules.RuleModeration:
|
||||
"""Contain methods used to moderate rules.
|
||||
|
||||
To delete ``"No spam"`` from r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").rules["No spam"].mod.delete()
|
||||
|
||||
To update ``"No spam"`` from r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").removal_reasons["No spam"].mod.update(
|
||||
description="Don't do this!", violation_reason="Spam post"
|
||||
)
|
||||
|
||||
"""
|
||||
return RuleModeration(self)
|
||||
|
||||
def __getattribute__(self, attribute: str) -> Any:
|
||||
"""Get the value of an attribute."""
|
||||
value = super().__getattribute__(attribute)
|
||||
if attribute == "subreddit" and value is None:
|
||||
msg = "The Rule is missing a subreddit. File a bug report at PRAW."
|
||||
raise ValueError(msg)
|
||||
return value
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
subreddit: praw.models.Subreddit | None = None,
|
||||
short_name: str | None = None,
|
||||
_data: dict[str, str] | None = None,
|
||||
):
|
||||
"""Initialize a :class:`.Rule` instance."""
|
||||
if (short_name, _data).count(None) != 1:
|
||||
msg = "Either short_name or _data needs to be given."
|
||||
raise ValueError(msg)
|
||||
if short_name:
|
||||
self.short_name = short_name
|
||||
# Note: The subreddit parameter can be None, because the objector does not know
|
||||
# this info. In that case, it is the responsibility of the caller to set the
|
||||
# `subreddit` property on the returned value.
|
||||
self.subreddit = subreddit
|
||||
super().__init__(reddit, _data=_data)
|
||||
|
||||
def _fetch(self):
|
||||
for rule in self.subreddit.rules:
|
||||
if rule.short_name == self.short_name:
|
||||
self.__dict__.update(rule.__dict__)
|
||||
super()._fetch()
|
||||
return
|
||||
msg = f"Subreddit {self.subreddit} does not have the rule {self.short_name}"
|
||||
raise ClientException(msg)
|
||||
|
||||
|
||||
class RuleModeration:
|
||||
"""Contain methods used to moderate rules.
|
||||
|
||||
To delete ``"No spam"`` from r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").rules["No spam"].mod.delete()
|
||||
|
||||
To update ``"No spam"`` from r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").removal_reasons["No spam"].mod.update(
|
||||
description="Don't do this!", violation_reason="Spam post"
|
||||
)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, rule: praw.models.Rule):
|
||||
"""Initialize a :class:`.RuleModeration` instance."""
|
||||
self.rule = rule
|
||||
|
||||
def delete(self):
|
||||
"""Delete a rule from this subreddit.
|
||||
|
||||
To delete ``"No spam"`` from r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").rules["No spam"].mod.delete()
|
||||
|
||||
"""
|
||||
data = {
|
||||
"r": str(self.rule.subreddit),
|
||||
"short_name": self.rule.short_name,
|
||||
}
|
||||
self.rule._reddit.post(API_PATH["remove_subreddit_rule"], data=data)
|
||||
|
||||
@_deprecate_args("description", "kind", "short_name", "violation_reason")
|
||||
def update(
|
||||
self,
|
||||
*,
|
||||
description: str | None = None,
|
||||
kind: str | None = None,
|
||||
short_name: str | None = None,
|
||||
violation_reason: str | None = None,
|
||||
) -> praw.models.Rule:
|
||||
"""Update the rule from this subreddit.
|
||||
|
||||
.. note::
|
||||
|
||||
Existing values will be used for any unspecified arguments.
|
||||
|
||||
:param description: The new description for the rule. Can be empty.
|
||||
:param kind: The kind of item that the rule applies to. One of ``"link"``,
|
||||
``"comment"``, or ``"all"``.
|
||||
:param short_name: The name of the rule.
|
||||
:param violation_reason: The reason that is shown on the report menu.
|
||||
|
||||
:returns: A Rule object containing the updated values.
|
||||
|
||||
To update ``"No spam"`` from r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").removal_reasons["No spam"].mod.update(
|
||||
description="Don't do this!", violation_reason="Spam post"
|
||||
)
|
||||
|
||||
"""
|
||||
data = {
|
||||
"r": str(self.rule.subreddit),
|
||||
"old_short_name": self.rule.short_name,
|
||||
}
|
||||
for name, value in {
|
||||
"description": description,
|
||||
"kind": kind,
|
||||
"short_name": short_name,
|
||||
"violation_reason": violation_reason,
|
||||
}.items():
|
||||
data[name] = getattr(self.rule, name) if value is None else value
|
||||
updated_rule = self.rule._reddit.post(
|
||||
API_PATH["update_subreddit_rule"], data=data
|
||||
)[0]
|
||||
updated_rule.subreddit = self.rule.subreddit
|
||||
return updated_rule
|
||||
|
||||
|
||||
class SubredditRules:
|
||||
"""Provide a set of functions to access a :class:`.Subreddit`'s rules.
|
||||
|
||||
For example, to list all the rules for a subreddit:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for rule in reddit.subreddit("test").rules:
|
||||
print(rule)
|
||||
|
||||
Moderators can also add rules to the subreddit. For example, to make a rule called
|
||||
``"No spam"`` in r/test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").rules.mod.add(
|
||||
short_name="No spam", kind="all", description="Do not spam. Spam bad"
|
||||
)
|
||||
|
||||
"""
|
||||
|
||||
@cachedproperty
|
||||
def _rule_list(self) -> list[Rule]:
|
||||
"""Get a list of :class:`.Rule` objects.
|
||||
|
||||
:returns: A list of instances of :class:`.Rule`.
|
||||
|
||||
"""
|
||||
rule_list = self._reddit.get(API_PATH["rules"].format(subreddit=self.subreddit))
|
||||
for rule in rule_list:
|
||||
rule.subreddit = self.subreddit
|
||||
return rule_list
|
||||
|
||||
@cachedproperty
|
||||
def mod(self) -> SubredditRulesModeration:
|
||||
"""Contain methods to moderate subreddit rules as a whole.
|
||||
|
||||
To add rule ``"No spam"`` to r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").rules.mod.add(
|
||||
short_name="No spam", kind="all", description="Do not spam. Spam bad"
|
||||
)
|
||||
|
||||
To move the fourth rule to the first position, and then to move the prior first
|
||||
rule to where the third rule originally was in r/test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit = reddit.subreddit("test")
|
||||
rules = list(subreddit.rules)
|
||||
new_rules = rules[3:4] + rules[1:3] + rules[0:1] + rules[4:]
|
||||
# Alternate: [rules[3]] + rules[1:3] + [rules[0]] + rules[4:]
|
||||
new_rule_list = subreddit.rules.mod.reorder(new_rules)
|
||||
|
||||
"""
|
||||
return SubredditRulesModeration(self)
|
||||
|
||||
def __call__(self) -> list[praw.models.Rule]:
|
||||
r"""Return a list of :class:`.Rule`\ s (Deprecated).
|
||||
|
||||
:returns: A list of instances of :class:`.Rule`.
|
||||
|
||||
.. deprecated:: 7.1
|
||||
|
||||
Use the iterator by removing the call to :class:`.SubredditRules`. For
|
||||
example, in order to use the iterator:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for rule in reddit.subreddit("test").rules:
|
||||
print(rule)
|
||||
|
||||
"""
|
||||
warn(
|
||||
"Calling SubredditRules to get a list of rules is deprecated. Remove the"
|
||||
" parentheses to use the iterator. View the PRAW documentation on how to"
|
||||
" change the code in order to use the iterator"
|
||||
" (https://praw.readthedocs.io/en/latest/code_overview/other/subredditrules.html#praw.models.reddit.rules.SubredditRules.__call__).",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self._reddit.request(
|
||||
method="GET", path=API_PATH["rules"].format(subreddit=self.subreddit)
|
||||
)
|
||||
|
||||
def __getitem__(self, short_name: str | int | slice) -> praw.models.Rule:
|
||||
"""Return the :class:`.Rule` for the subreddit with short_name ``short_name``.
|
||||
|
||||
:param short_name: The short_name of the rule, or the rule number.
|
||||
|
||||
.. note::
|
||||
|
||||
Rules fetched using a specific rule name are lazily loaded, so you might
|
||||
have to access an attribute to get all the expected attributes.
|
||||
|
||||
This method is to be used to fetch a specific rule, like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
rule_name = "No spam"
|
||||
rule = reddit.subreddit("test").rules[rule_name]
|
||||
print(rule)
|
||||
|
||||
You can also fetch a numbered rule of a subreddit.
|
||||
|
||||
Rule numbers start at ``0``, so the first rule is at index ``0``, and the second
|
||||
rule is at index ``1``, and so on.
|
||||
|
||||
:raises: :py:class:`IndexError` if a rule of a specific number does not exist.
|
||||
|
||||
.. note::
|
||||
|
||||
You can use negative indexes, such as ``-1``, to get the last rule. You can
|
||||
also use slices, to get a subset of rules, such as the last three rules with
|
||||
``rules[-3:]``.
|
||||
|
||||
For example, to fetch the second rule of r/test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
rule = reddit.subreddit("test").rules[1]
|
||||
|
||||
"""
|
||||
if not isinstance(short_name, str):
|
||||
return self._rule_list[short_name]
|
||||
return Rule(self._reddit, subreddit=self.subreddit, short_name=short_name)
|
||||
|
||||
def __init__(self, subreddit: praw.models.Subreddit):
|
||||
"""Initialize a :class:`.SubredditRules` instance.
|
||||
|
||||
:param subreddit: The subreddit whose rules to work with.
|
||||
|
||||
"""
|
||||
self.subreddit = subreddit
|
||||
self._reddit = subreddit._reddit
|
||||
|
||||
def __iter__(self) -> Iterator[praw.models.Rule]:
|
||||
"""Iterate through the rules of the subreddit.
|
||||
|
||||
:returns: An iterator containing all the rules of a subreddit.
|
||||
|
||||
This method is used to discover all rules for a subreddit.
|
||||
|
||||
For example, to get the rules for r/test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for rule in reddit.subreddit("test").rules:
|
||||
print(rule)
|
||||
|
||||
"""
|
||||
return iter(self._rule_list)
|
||||
|
||||
|
||||
class SubredditRulesModeration:
|
||||
"""Contain methods to moderate subreddit rules as a whole.
|
||||
|
||||
To add rule ``"No spam"`` to r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").rules.mod.add(
|
||||
short_name="No spam", kind="all", description="Do not spam. Spam bad"
|
||||
)
|
||||
|
||||
To move the fourth rule to the first position, and then to move the prior first rule
|
||||
to where the third rule originally was in r/test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit = reddit.subreddit("test")
|
||||
rules = list(subreddit.rules)
|
||||
new_rules = rules[3:4] + rules[1:3] + rules[0:1] + rules[4:]
|
||||
# Alternate: [rules[3]] + rules[1:3] + [rules[0]] + rules[4:]
|
||||
new_rule_list = subreddit.rules.mod.reorder(new_rules)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, subreddit_rules: SubredditRules):
|
||||
"""Initialize a :class:`.SubredditRulesModeration` instance."""
|
||||
self.subreddit_rules = subreddit_rules
|
||||
|
||||
@_deprecate_args("short_name", "kind", "description", "violation_reason")
|
||||
def add(
|
||||
self,
|
||||
*,
|
||||
description: str = "",
|
||||
kind: str,
|
||||
short_name: str,
|
||||
violation_reason: str | None = None,
|
||||
) -> praw.models.Rule:
|
||||
"""Add a removal reason to this subreddit.
|
||||
|
||||
:param description: The description for the rule.
|
||||
:param kind: The kind of item that the rule applies to. One of ``"link"``,
|
||||
``"comment"``, or ``"all"``.
|
||||
:param short_name: The name of the rule.
|
||||
:param violation_reason: The reason that is shown on the report menu. If a
|
||||
violation reason is not specified, the short name will be used as the
|
||||
violation reason.
|
||||
|
||||
:returns: The added :class:`.Rule`.
|
||||
|
||||
To add rule ``"No spam"`` to r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").rules.mod.add(
|
||||
short_name="No spam", kind="all", description="Do not spam. Spam bad"
|
||||
)
|
||||
|
||||
"""
|
||||
data = {
|
||||
"r": str(self.subreddit_rules.subreddit),
|
||||
"description": description,
|
||||
"kind": kind,
|
||||
"short_name": short_name,
|
||||
"violation_reason": (
|
||||
short_name if violation_reason is None else violation_reason
|
||||
),
|
||||
}
|
||||
new_rule = self.subreddit_rules._reddit.post(
|
||||
API_PATH["add_subreddit_rule"], data=data
|
||||
)[0]
|
||||
new_rule.subreddit = self.subreddit_rules.subreddit
|
||||
return new_rule
|
||||
|
||||
def reorder(self, rule_list: list[praw.models.Rule]) -> list[praw.models.Rule]:
|
||||
"""Reorder the rules of a subreddit.
|
||||
|
||||
:param rule_list: The list of rules, in the wanted order. Each index of the list
|
||||
indicates the position of the rule.
|
||||
|
||||
:returns: A list containing the rules in the specified order.
|
||||
|
||||
For example, to move the fourth rule to the first position, and then to move the
|
||||
prior first rule to where the third rule originally was in r/test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit = reddit.subreddit("test")
|
||||
rules = list(subreddit.rules)
|
||||
new_rules = rules[3:4] + rules[1:3] + rules[0:1] + rules[4:]
|
||||
# Alternate: [rules[3]] + rules[1:3] + [rules[0]] + rules[4:]
|
||||
new_rule_list = subreddit.rules.mod.reorder(new_rules)
|
||||
|
||||
"""
|
||||
order_string = quote(
|
||||
",".join([rule.short_name for rule in rule_list]), safe=","
|
||||
)
|
||||
data = {
|
||||
"r": str(self.subreddit_rules.subreddit),
|
||||
"new_rule_order": order_string,
|
||||
}
|
||||
response = self.subreddit_rules._reddit.post(
|
||||
API_PATH["reorder_subreddit_rules"], data=data
|
||||
)
|
||||
for rule in response:
|
||||
rule.subreddit = self.subreddit_rules.subreddit
|
||||
return response
|
||||
951
backend/venv/Lib/site-packages/praw/models/reddit/submission.py
Normal file
951
backend/venv/Lib/site-packages/praw/models/reddit/submission.py
Normal file
@@ -0,0 +1,951 @@
|
||||
"""Provide the Submission class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from json import dumps
|
||||
from typing import TYPE_CHECKING, Any, Generator
|
||||
from urllib.parse import urljoin
|
||||
from warnings import warn
|
||||
|
||||
from prawcore import Conflict
|
||||
|
||||
from ...const import API_PATH
|
||||
from ...exceptions import InvalidURL
|
||||
from ...util import _deprecate_args, cachedproperty
|
||||
from ..comment_forest import CommentForest
|
||||
from ..listing.listing import Listing
|
||||
from ..listing.mixins import SubmissionListingMixin
|
||||
from .base import RedditBase
|
||||
from .mixins import FullnameMixin, ModNoteMixin, ThingModerationMixin, UserContentMixin
|
||||
from .poll import PollData
|
||||
from .redditor import Redditor
|
||||
from .subreddit import Subreddit
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
INLINE_MEDIA_PATTERN = re.compile(
|
||||
r"\n\n!?(\[.*?])?\(?((https://((preview|i)\.redd\.it|reddit.com/link).*?)|(?!https)([a-zA-Z0-9]+( \".*?\")?))\)?"
|
||||
)
|
||||
MEDIA_TYPE_MAPPING = {
|
||||
"Image": "img",
|
||||
"RedditVideo": "video",
|
||||
"AnimatedImage": "gif",
|
||||
}
|
||||
|
||||
|
||||
class SubmissionFlair:
|
||||
"""Provide a set of functions pertaining to :class:`.Submission` flair."""
|
||||
|
||||
def __init__(self, submission: praw.models.Submission):
|
||||
"""Initialize a :class:`.SubmissionFlair` instance.
|
||||
|
||||
:param submission: The :class:`.Submission` associated with the flair functions.
|
||||
|
||||
"""
|
||||
self.submission = submission
|
||||
|
||||
def choices(self) -> list[dict[str, bool | list | str]]:
|
||||
"""Return list of available flair choices.
|
||||
|
||||
Choices are required in order to use :meth:`.select`.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
choices = submission.flair.choices()
|
||||
|
||||
"""
|
||||
url = API_PATH["flairselector"].format(subreddit=self.submission.subreddit)
|
||||
return self.submission._reddit.post(
|
||||
url, data={"link": self.submission.fullname}
|
||||
)["choices"]
|
||||
|
||||
@_deprecate_args("flair_template_id", "text")
|
||||
def select(self, flair_template_id: str, *, text: str | None = None):
|
||||
"""Select flair for submission.
|
||||
|
||||
:param flair_template_id: The flair template to select. The possible values can
|
||||
be discovered through :meth:`.choices`.
|
||||
:param text: If the template's ``flair_text_editable`` value is ``True``, this
|
||||
value will set a custom text (default: ``None``).
|
||||
|
||||
For example, to select an arbitrary editable flair text (assuming there is one)
|
||||
and set a custom value try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
choices = submission.flair.choices()
|
||||
template_id = next(x for x in choices if x["flair_text_editable"])["flair_template_id"]
|
||||
submission.flair.select(template_id, text="my custom value")
|
||||
|
||||
"""
|
||||
data = {
|
||||
"flair_template_id": flair_template_id,
|
||||
"link": self.submission.fullname,
|
||||
"text": text,
|
||||
}
|
||||
url = API_PATH["select_flair"].format(subreddit=self.submission.subreddit)
|
||||
self.submission._reddit.post(url, data=data)
|
||||
|
||||
|
||||
class SubmissionModeration(ThingModerationMixin, ModNoteMixin):
|
||||
"""Provide a set of functions pertaining to :class:`.Submission` moderation.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("8dmv8z")
|
||||
submission.mod.approve()
|
||||
|
||||
"""
|
||||
|
||||
REMOVAL_MESSAGE_API = "removal_link_message"
|
||||
|
||||
def __init__(self, submission: praw.models.Submission):
|
||||
"""Initialize a :class:`.SubmissionModeration` instance.
|
||||
|
||||
:param submission: The submission to moderate.
|
||||
|
||||
"""
|
||||
self.thing = submission
|
||||
|
||||
@_deprecate_args("state")
|
||||
def contest_mode(self, *, state: bool = True):
|
||||
"""Set contest mode for the comments of this submission.
|
||||
|
||||
:param state: ``True`` enables contest mode and ``False`` disables (default:
|
||||
``True``).
|
||||
|
||||
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.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.contest_mode()
|
||||
|
||||
"""
|
||||
self.thing._reddit.post(
|
||||
API_PATH["contest_mode"], data={"id": self.thing.fullname, "state": state}
|
||||
)
|
||||
|
||||
@_deprecate_args("text", "css_class", "flair_template_id")
|
||||
def flair(
|
||||
self,
|
||||
*,
|
||||
css_class: str = "",
|
||||
flair_template_id: str | None = None,
|
||||
text: str = "",
|
||||
):
|
||||
"""Set flair for the submission.
|
||||
|
||||
:param css_class: The css class to associate with the flair html (default:
|
||||
``""``).
|
||||
:param flair_template_id: The flair template ID to use when flairing.
|
||||
:param text: The flair text to associate with the :class:`.Submission` (default:
|
||||
``""``).
|
||||
|
||||
This method can only be used by an authenticated user who is a moderator of the
|
||||
submission's :class:`.Subreddit`.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.flair(text="PRAW", css_class="bot")
|
||||
|
||||
"""
|
||||
data = {
|
||||
"css_class": css_class,
|
||||
"link": self.thing.fullname,
|
||||
"text": text,
|
||||
}
|
||||
url = API_PATH["flair"].format(subreddit=self.thing.subreddit)
|
||||
if flair_template_id is not None:
|
||||
data["flair_template_id"] = flair_template_id
|
||||
url = API_PATH["select_flair"].format(subreddit=self.thing.subreddit)
|
||||
self.thing._reddit.post(url, data=data)
|
||||
|
||||
def nsfw(self):
|
||||
"""Mark as not safe for work.
|
||||
|
||||
This method can be used both by the submission author and moderators of the
|
||||
subreddit that the submission belongs to.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.subreddit("test").submit("nsfw test", selftext="nsfw")
|
||||
submission.mod.nsfw()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.sfw`
|
||||
|
||||
"""
|
||||
self.thing._reddit.post(API_PATH["marknsfw"], data={"id": self.thing.fullname})
|
||||
|
||||
def set_original_content(self):
|
||||
"""Mark as original content.
|
||||
|
||||
This method can be used by moderators of the subreddit that the submission
|
||||
belongs to. If the subreddit has enabled the Original Content beta feature in
|
||||
settings, then the submission's author can use it as well.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.subreddit("test").submit("oc test", selftext="original")
|
||||
submission.mod.set_original_content()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.unset_original_content`
|
||||
|
||||
"""
|
||||
data = {
|
||||
"id": self.thing.id,
|
||||
"fullname": self.thing.fullname,
|
||||
"should_set_oc": True,
|
||||
"executed": False,
|
||||
"r": self.thing.subreddit,
|
||||
}
|
||||
self.thing._reddit.post(API_PATH["set_original_content"], data=data)
|
||||
|
||||
def sfw(self):
|
||||
"""Mark as safe for work.
|
||||
|
||||
This method can be used both by the submission author and moderators of the
|
||||
subreddit that the submission belongs to.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.sfw()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.nsfw`
|
||||
|
||||
"""
|
||||
self.thing._reddit.post(
|
||||
API_PATH["unmarknsfw"], data={"id": self.thing.fullname}
|
||||
)
|
||||
|
||||
def spoiler(self):
|
||||
"""Indicate that the submission contains spoilers.
|
||||
|
||||
This method can be used both by the submission author and moderators of the
|
||||
subreddit that the submission belongs to.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.spoiler()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.unspoiler`
|
||||
|
||||
"""
|
||||
self.thing._reddit.post(API_PATH["spoiler"], data={"id": self.thing.fullname})
|
||||
|
||||
@_deprecate_args("state", "bottom")
|
||||
def sticky(
|
||||
self, *, bottom: bool = True, state: bool = True
|
||||
) -> praw.models.Submission:
|
||||
"""Set the submission's sticky state in its subreddit.
|
||||
|
||||
:param bottom: When ``True``, set the submission as the bottom sticky. If no top
|
||||
sticky exists, this submission will become the top sticky regardless
|
||||
(default: ``True``).
|
||||
:param state: ``True`` sets the sticky for the submission and ``False`` unsets
|
||||
(default: ``True``).
|
||||
|
||||
:returns: The stickied submission object.
|
||||
|
||||
.. note::
|
||||
|
||||
When a submission is stickied two or more times, the Reddit API responds
|
||||
with a 409 error that is raised as a ``Conflict`` by prawcore. This method
|
||||
suppresses these ``Conflict`` errors.
|
||||
|
||||
This submission will replace the second stickied submission if one exists.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.sticky()
|
||||
|
||||
"""
|
||||
data = {"id": self.thing.fullname, "state": state}
|
||||
if not bottom:
|
||||
data["num"] = 1
|
||||
try:
|
||||
return self.thing._reddit.post(API_PATH["sticky_submission"], data=data)
|
||||
except Conflict:
|
||||
pass
|
||||
|
||||
@_deprecate_args("sort")
|
||||
def suggested_sort(self, *, sort: str = "blank"):
|
||||
"""Set the suggested sort for the comments of the submission.
|
||||
|
||||
:param sort: Can be one of: ``"confidence"``, ``"top"``, ``"new"``,
|
||||
``"controversial"``, ``"old"``, ``"random"``, ``"qa"``, or ``"blank"``
|
||||
(default: ``"blank"``).
|
||||
|
||||
"""
|
||||
self.thing._reddit.post(
|
||||
API_PATH["suggested_sort"], data={"id": self.thing.fullname, "sort": sort}
|
||||
)
|
||||
|
||||
def unset_original_content(self):
|
||||
"""Indicate that the submission is not original content.
|
||||
|
||||
This method can be used by moderators of the subreddit that the submission
|
||||
belongs to. If the subreddit has enabled the Original Content beta feature in
|
||||
settings, then the submission's author can use it as well.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.subreddit("test").submit("oc test", selftext="original")
|
||||
submission.mod.unset_original_content()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.set_original_content`
|
||||
|
||||
"""
|
||||
data = {
|
||||
"id": self.thing.id,
|
||||
"fullname": self.thing.fullname,
|
||||
"should_set_oc": False,
|
||||
"executed": False,
|
||||
"r": self.thing.subreddit,
|
||||
}
|
||||
self.thing._reddit.post(API_PATH["set_original_content"], data=data)
|
||||
|
||||
def unspoiler(self):
|
||||
"""Indicate that the submission does not contain spoilers.
|
||||
|
||||
This method can be used both by the submission author and moderators of the
|
||||
subreddit that the submission belongs to.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.subreddit("test").submit("not spoiler", selftext="spoiler")
|
||||
submission.mod.unspoiler()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.spoiler`
|
||||
|
||||
"""
|
||||
self.thing._reddit.post(API_PATH["unspoiler"], data={"id": self.thing.fullname})
|
||||
|
||||
def update_crowd_control_level(self, level: int):
|
||||
"""Change the Crowd Control level of the submission.
|
||||
|
||||
:param level: An integer between 0 and 3.
|
||||
|
||||
**Level Descriptions**
|
||||
|
||||
===== ======== ================================================================
|
||||
Level Name Description
|
||||
===== ======== ================================================================
|
||||
0 Off Crowd Control will not action any of the submission's comments.
|
||||
1 Lenient Comments from users who have negative karma in the subreddit are
|
||||
automatically collapsed.
|
||||
2 Moderate Comments from new users and users with negative karma in the
|
||||
subreddit are automatically collapsed.
|
||||
3 Strict Comments from users who haven’t joined the subreddit, new users,
|
||||
and users with negative karma in the subreddit are automatically
|
||||
collapsed.
|
||||
===== ======== ================================================================
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("745ryj")
|
||||
submission.mod.update_crowd_control_level(2)
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`~.CommentModeration.show`
|
||||
|
||||
"""
|
||||
self.thing._reddit.post(
|
||||
API_PATH["update_crowd_control"],
|
||||
data={"id": self.thing.fullname, "level": level},
|
||||
)
|
||||
|
||||
|
||||
class Submission(SubmissionListingMixin, UserContentMixin, FullnameMixin, RedditBase):
|
||||
"""A class for submissions to Reddit.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
========================== =========================================================
|
||||
Attribute Description
|
||||
========================== =========================================================
|
||||
``author`` Provides an instance of :class:`.Redditor`.
|
||||
``author_flair_text`` The text content of the author's flair, or ``None`` if
|
||||
not flaired.
|
||||
``clicked`` Whether or not the submission has been clicked by the
|
||||
client.
|
||||
``comments`` Provides an instance of :class:`.CommentForest`.
|
||||
``created_utc`` Time the submission was created, represented in `Unix
|
||||
Time`_.
|
||||
``distinguished`` Whether or not the submission is distinguished.
|
||||
``edited`` Whether or not the submission has been edited.
|
||||
``id`` ID of the submission.
|
||||
``is_original_content`` Whether or not the submission has been set as original
|
||||
content.
|
||||
``is_self`` Whether or not the submission is a selfpost (text-only).
|
||||
``link_flair_template_id`` The link flair's ID.
|
||||
``link_flair_text`` The link flair's text content, or ``None`` if not
|
||||
flaired.
|
||||
``locked`` Whether or not the submission has been locked.
|
||||
``name`` Fullname of the submission.
|
||||
``num_comments`` The number of comments on the submission.
|
||||
``over_18`` Whether or not the submission has been marked as NSFW.
|
||||
``permalink`` A permalink for the submission.
|
||||
``poll_data`` A :class:`.PollData` object representing the data of this
|
||||
submission, if it is a poll submission.
|
||||
``saved`` Whether or not the submission is saved.
|
||||
``score`` The number of upvotes for the submission.
|
||||
``selftext`` The submissions' selftext - an empty string if a link
|
||||
post.
|
||||
``spoiler`` Whether or not the submission has been marked as a
|
||||
spoiler.
|
||||
``stickied`` Whether or not the submission is stickied.
|
||||
``subreddit`` Provides an instance of :class:`.Subreddit`.
|
||||
``title`` The title of the submission.
|
||||
``upvote_ratio`` The percentage of upvotes from all votes on the
|
||||
submission.
|
||||
``url`` The URL the submission links to, or the permalink if a
|
||||
selfpost.
|
||||
========================== =========================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
STR_FIELD = "id"
|
||||
|
||||
@staticmethod
|
||||
def id_from_url(url: str) -> str:
|
||||
"""Return the ID contained within a submission URL.
|
||||
|
||||
:param url: A url to a submission in one of the following formats (http urls
|
||||
will also work):
|
||||
|
||||
- ``"https://redd.it/2gmzqe"``
|
||||
- ``"https://reddit.com/comments/2gmzqe/"``
|
||||
- ``"https://www.reddit.com/r/redditdev/comments/2gmzqe/praw_https/"``
|
||||
- ``"https://www.reddit.com/gallery/2gmzqe"``
|
||||
|
||||
:raises: :class:`.InvalidURL` if ``url`` is not a valid submission URL.
|
||||
|
||||
"""
|
||||
parts = RedditBase._url_parts(url)
|
||||
if "comments" not in parts and "gallery" not in parts:
|
||||
submission_id = parts[-1]
|
||||
if "r" in parts:
|
||||
raise InvalidURL(
|
||||
url, message="Invalid URL (subreddit, not submission): {}"
|
||||
)
|
||||
|
||||
elif "gallery" in parts:
|
||||
submission_id = parts[parts.index("gallery") + 1]
|
||||
|
||||
elif parts[-1] == "comments":
|
||||
raise InvalidURL(url, message="Invalid URL (submission ID not present): {}")
|
||||
|
||||
else:
|
||||
submission_id = parts[parts.index("comments") + 1]
|
||||
|
||||
if not submission_id.isalnum():
|
||||
raise InvalidURL(url)
|
||||
return submission_id
|
||||
|
||||
@cachedproperty
|
||||
def flair(self) -> SubmissionFlair:
|
||||
"""Provide an instance of :class:`.SubmissionFlair`.
|
||||
|
||||
This attribute is used to work with flair as a regular user of the subreddit the
|
||||
submission belongs to. Moderators can directly use :meth:`.flair`.
|
||||
|
||||
For example, to select an arbitrary editable flair text (assuming there is one)
|
||||
and set a custom value try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
choices = submission.flair.choices()
|
||||
template_id = next(x for x in choices if x["flair_text_editable"])["flair_template_id"]
|
||||
submission.flair.select(template_id, text="my custom value")
|
||||
|
||||
"""
|
||||
return SubmissionFlair(self)
|
||||
|
||||
@cachedproperty
|
||||
def mod(self) -> SubmissionModeration:
|
||||
"""Provide an instance of :class:`.SubmissionModeration`.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("8dmv8z")
|
||||
submission.mod.approve()
|
||||
|
||||
"""
|
||||
return SubmissionModeration(self)
|
||||
|
||||
@property
|
||||
def _kind(self) -> str:
|
||||
"""Return the class's kind."""
|
||||
return self._reddit.config.kinds["submission"]
|
||||
|
||||
@property
|
||||
def comments(self) -> CommentForest:
|
||||
"""Provide an instance of :class:`.CommentForest`.
|
||||
|
||||
This attribute can be used, for example, to obtain a flat list of comments, with
|
||||
any :class:`.MoreComments` removed:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission.comments.replace_more(limit=0)
|
||||
comments = submission.comments.list()
|
||||
|
||||
Sort order and comment limit can be set with the ``comment_sort`` and
|
||||
``comment_limit`` attributes before comments are fetched, including any call to
|
||||
:meth:`.replace_more`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission.comment_sort = "new"
|
||||
comments = submission.comments.list()
|
||||
|
||||
.. note::
|
||||
|
||||
The appropriate values for ``"comment_sort"`` include ``"confidence"``,
|
||||
``"controversial"``, ``"new"``, ``"old"``, ``"q&a"``, and ``"top"``
|
||||
|
||||
See :ref:`extracting_comments` for more on working with a
|
||||
:class:`.CommentForest`.
|
||||
|
||||
"""
|
||||
# This assumes _comments is set so that _fetch is called when it's not.
|
||||
return self._comments
|
||||
|
||||
@property
|
||||
def shortlink(self) -> str:
|
||||
"""Return a shortlink to the submission.
|
||||
|
||||
For example, https://redd.it/eorhm is a shortlink for
|
||||
https://www.reddit.com/r/announcements/comments/eorhm/reddit_30_less_typing/.
|
||||
|
||||
"""
|
||||
return urljoin(self._reddit.config.short_url, self.id)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
id: str | None = None,
|
||||
url: str | None = None,
|
||||
_data: dict[str, Any] | None = None,
|
||||
):
|
||||
"""Initialize a :class:`.Submission` instance.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
:param id: A reddit base36 submission ID, e.g., ``"2gmzqe"``.
|
||||
:param url: A URL supported by :meth:`.id_from_url`.
|
||||
|
||||
Either ``id`` or ``url`` can be provided, but not both.
|
||||
|
||||
"""
|
||||
if (id, url, _data).count(None) != 2:
|
||||
msg = "Exactly one of 'id', 'url', or '_data' must be provided."
|
||||
raise TypeError(msg)
|
||||
self.comment_limit = 2048
|
||||
|
||||
# Specify the sort order for ``comments``
|
||||
self.comment_sort = "confidence"
|
||||
|
||||
if id:
|
||||
self.id = id
|
||||
elif url:
|
||||
self.id = self.id_from_url(url)
|
||||
|
||||
super().__init__(reddit, _data=_data)
|
||||
|
||||
self._additional_fetch_params = {}
|
||||
self._comments_by_id = {}
|
||||
|
||||
def __setattr__(self, attribute: str, value: Any):
|
||||
"""Objectify author, subreddit, and poll data attributes."""
|
||||
if attribute == "author":
|
||||
value = Redditor.from_data(self._reddit, value)
|
||||
elif attribute == "subreddit":
|
||||
value = Subreddit(self._reddit, value)
|
||||
elif attribute == "poll_data":
|
||||
value = PollData(self._reddit, value)
|
||||
elif (
|
||||
attribute == "comment_sort"
|
||||
and hasattr(self, "_fetched")
|
||||
and self._fetched
|
||||
and hasattr(self, "_reddit")
|
||||
and self._reddit.config.warn_comment_sort
|
||||
):
|
||||
warn(
|
||||
"The comments for this submission have already been fetched, so the"
|
||||
" updated comment_sort will not have any effect.",
|
||||
stacklevel=2,
|
||||
)
|
||||
super().__setattr__(attribute, value)
|
||||
|
||||
def _chunk(
|
||||
self,
|
||||
*,
|
||||
chunk_size: int,
|
||||
other_submissions: list[praw.models.Submission] | None,
|
||||
) -> Generator[str, None, None]:
|
||||
all_submissions = [self.fullname]
|
||||
if other_submissions:
|
||||
all_submissions += [x.fullname for x in other_submissions]
|
||||
|
||||
for position in range(0, len(all_submissions), chunk_size):
|
||||
yield ",".join(all_submissions[position : position + 50])
|
||||
|
||||
def _edit_experimental(
|
||||
self,
|
||||
body: str,
|
||||
*,
|
||||
preserve_inline_media: bool = False,
|
||||
inline_media: dict[str, praw.models.InlineMedia] | None = None,
|
||||
) -> praw.models.Submission:
|
||||
"""Replace the body of the object with ``body``.
|
||||
|
||||
:param body: The Markdown formatted content for the updated object.
|
||||
:param preserve_inline_media: Attempt to preserve inline media in ``body``.
|
||||
|
||||
.. danger::
|
||||
|
||||
This method is experimental. It is reliant on undocumented API endpoints
|
||||
and may result in existing inline media not displaying correctly and/or
|
||||
creating a malformed body. Use at your own risk. This method may be
|
||||
removed in the future without warning.
|
||||
|
||||
:param inline_media: A dict of :class:`.InlineMedia` objects where the key is
|
||||
the placeholder name in ``body``.
|
||||
|
||||
:returns: The current instance after updating its attributes.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from praw.models import InlineGif, InlineImage, InlineVideo
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
gif = InlineGif(path="path/to/image.gif", caption="optional caption")
|
||||
image = InlineImage(path="path/to/image.jpg", caption="optional caption")
|
||||
video = InlineVideo(path="path/to/video.mp4", caption="optional caption")
|
||||
body = "New body with a gif {gif1} an image {image1} and a video {video1} inline"
|
||||
media = {"gif1": gif, "image1": image, "video1": video}
|
||||
submission._edit_experimental(submission.selftext + body, inline_media=media)
|
||||
|
||||
"""
|
||||
data = {
|
||||
"thing_id": self.fullname,
|
||||
"validate_on_submit": self._reddit.validate_on_submit,
|
||||
}
|
||||
is_richtext_json = False
|
||||
if INLINE_MEDIA_PATTERN.search(body) and self.media_metadata:
|
||||
is_richtext_json = True
|
||||
if inline_media:
|
||||
body = body.format(
|
||||
**{
|
||||
placeholder: self.subreddit._upload_inline_media(media)
|
||||
for placeholder, media in inline_media.items()
|
||||
}
|
||||
)
|
||||
is_richtext_json = True
|
||||
if is_richtext_json:
|
||||
richtext_json = self.subreddit._convert_to_fancypants(body)
|
||||
if preserve_inline_media:
|
||||
self._replace_richtext_links(richtext_json)
|
||||
data["richtext_json"] = dumps(richtext_json)
|
||||
else:
|
||||
data["text"] = body
|
||||
updated = self._reddit.post(API_PATH["edit"], data=data)
|
||||
if not is_richtext_json:
|
||||
updated = updated[0]
|
||||
for attribute in [
|
||||
"_fetched",
|
||||
"_reddit",
|
||||
"_submission",
|
||||
"replies",
|
||||
"subreddit",
|
||||
]:
|
||||
if attribute in updated.__dict__:
|
||||
delattr(updated, attribute)
|
||||
self.__dict__.update(updated.__dict__)
|
||||
else:
|
||||
self.__dict__.update(updated)
|
||||
return self
|
||||
|
||||
def _fetch(self):
|
||||
data = self._fetch_data()
|
||||
submission_listing, comment_listing = data
|
||||
comment_listing = Listing(self._reddit, _data=comment_listing["data"])
|
||||
|
||||
submission_data = submission_listing["data"]["children"][0]["data"]
|
||||
submission = type(self)(self._reddit, _data=submission_data)
|
||||
delattr(submission, "comment_limit")
|
||||
delattr(submission, "comment_sort")
|
||||
submission._comments = CommentForest(self)
|
||||
|
||||
self.__dict__.update(submission.__dict__)
|
||||
self.comments._update(comment_listing.children)
|
||||
super()._fetch()
|
||||
|
||||
def _fetch_data(self):
|
||||
name, fields, params = self._fetch_info()
|
||||
params.update(self._additional_fetch_params.copy())
|
||||
path = API_PATH[name].format(**fields)
|
||||
return self._reddit.request(method="GET", params=params, path=path)
|
||||
|
||||
def _fetch_info(self):
|
||||
return (
|
||||
"submission",
|
||||
{"id": self.id},
|
||||
{"limit": self.comment_limit, "sort": self.comment_sort},
|
||||
)
|
||||
|
||||
def _replace_richtext_links(self, richtext_json: dict):
|
||||
parsed_media_types = {
|
||||
media_id: MEDIA_TYPE_MAPPING[value["e"]]
|
||||
for media_id, value in self.media_metadata.items()
|
||||
}
|
||||
|
||||
for index, element in enumerate(richtext_json["document"][:]):
|
||||
element_items = element.get("c")
|
||||
if isinstance(element_items, str):
|
||||
assert element.get("e") in ["gif", "img", "video"], (
|
||||
"Unexpected richtext JSON schema. Please file a bug report with"
|
||||
" PRAW."
|
||||
) # make sure this is an inline element
|
||||
continue # pragma: no cover
|
||||
for item in element.get("c"):
|
||||
if item.get("e") == "link":
|
||||
ids = set(parsed_media_types)
|
||||
# remove extra bits from the url
|
||||
url = item["u"].split("https://")[1].split("?")[0]
|
||||
# the id is in the url somewhere, so we split by '/' and '.'
|
||||
matched_id = ids.intersection(re.split(r"[./]", url))
|
||||
if matched_id:
|
||||
matched_id = matched_id.pop()
|
||||
correct_element = {
|
||||
"e": parsed_media_types[matched_id],
|
||||
"id": matched_id,
|
||||
}
|
||||
if item.get("t") != item.get("u"): # add caption if it exists
|
||||
correct_element["c"] = item["t"]
|
||||
richtext_json["document"][index] = correct_element
|
||||
|
||||
def add_fetch_param(self, key: str, value: str):
|
||||
"""Add a parameter to be used for the next fetch.
|
||||
|
||||
:param key: The key of the fetch parameter.
|
||||
:param value: The value of the fetch parameter.
|
||||
|
||||
For example, to fetch a submission with the ``rtjson`` attribute populated:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("mcqjl8")
|
||||
submission.add_fetch_param("rtj", "all")
|
||||
print(submission.rtjson)
|
||||
|
||||
"""
|
||||
if (
|
||||
hasattr(self, "_fetched")
|
||||
and self._fetched
|
||||
and hasattr(self, "_reddit")
|
||||
and self._reddit.config.warn_additional_fetch_params
|
||||
):
|
||||
warn(
|
||||
f"This {self.__class__.__name__.lower()} has already been fetched, so"
|
||||
" adding additional fetch parameters will not have any effect.",
|
||||
stacklevel=2,
|
||||
)
|
||||
self._additional_fetch_params[key] = value
|
||||
|
||||
@_deprecate_args(
|
||||
"subreddit",
|
||||
"title",
|
||||
"send_replies",
|
||||
"flair_id",
|
||||
"flair_text",
|
||||
"nsfw",
|
||||
"spoiler",
|
||||
)
|
||||
def crosspost(
|
||||
self,
|
||||
subreddit: praw.models.Subreddit,
|
||||
*,
|
||||
flair_id: str | None = None,
|
||||
flair_text: str | None = None,
|
||||
nsfw: bool = False,
|
||||
send_replies: bool = True,
|
||||
spoiler: bool = False,
|
||||
title: str | None = None,
|
||||
) -> praw.models.Submission:
|
||||
"""Crosspost the submission to a subreddit.
|
||||
|
||||
.. note::
|
||||
|
||||
Be aware you have to be subscribed to the target subreddit.
|
||||
|
||||
:param subreddit: Name of the subreddit or :class:`.Subreddit` object to
|
||||
crosspost into.
|
||||
:param flair_id: The flair template to select (default: ``None``).
|
||||
:param flair_text: If the template's ``flair_text_editable`` value is ``True``,
|
||||
this value will set a custom text (default: ``None``).
|
||||
:param nsfw: Whether the submission should be marked NSFW (default: ``False``).
|
||||
:param send_replies: When ``True``, messages will be sent to the created
|
||||
submission's author when comments are made to the submission (default:
|
||||
``True``).
|
||||
:param spoiler: Whether the submission should be marked as a spoiler (default:
|
||||
``False``).
|
||||
:param title: Title of the submission. Will use this submission's title if
|
||||
``None`` (default: ``None``).
|
||||
|
||||
:returns: A :class:`.Submission` object for the newly created submission.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
cross_post = submission.crosspost("learnprogramming", send_replies=False)
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.hide`
|
||||
|
||||
"""
|
||||
if title is None:
|
||||
title = self.title
|
||||
|
||||
data = {
|
||||
"sr": str(subreddit),
|
||||
"title": title,
|
||||
"sendreplies": bool(send_replies),
|
||||
"kind": "crosspost",
|
||||
"crosspost_fullname": self.fullname,
|
||||
"nsfw": bool(nsfw),
|
||||
"spoiler": bool(spoiler),
|
||||
}
|
||||
for key, value in (("flair_id", flair_id), ("flair_text", flair_text)):
|
||||
if value is not None:
|
||||
data[key] = value
|
||||
|
||||
return self._reddit.post(API_PATH["submit"], data=data)
|
||||
|
||||
@_deprecate_args("other_submissions")
|
||||
def hide(self, *, other_submissions: list[praw.models.Submission] | None = None):
|
||||
"""Hide :class:`.Submission`.
|
||||
|
||||
:param other_submissions: When provided, additionally hide this list of
|
||||
:class:`.Submission` instances as part of a single request (default:
|
||||
``None``).
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.hide()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.unhide`
|
||||
|
||||
"""
|
||||
for submissions in self._chunk(
|
||||
chunk_size=50, other_submissions=other_submissions
|
||||
):
|
||||
self._reddit.post(API_PATH["hide"], data={"id": submissions})
|
||||
|
||||
def mark_visited(self):
|
||||
"""Mark submission as visited.
|
||||
|
||||
This method requires a subscription to reddit premium.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mark_visited()
|
||||
|
||||
"""
|
||||
data = {"links": self.fullname}
|
||||
self._reddit.post(API_PATH["store_visits"], data=data)
|
||||
|
||||
@_deprecate_args("other_submissions")
|
||||
def unhide(self, *, other_submissions: list[praw.models.Submission] | None = None):
|
||||
"""Unhide :class:`.Submission`.
|
||||
|
||||
:param other_submissions: When provided, additionally unhide this list of
|
||||
:class:`.Submission` instances as part of a single request (default:
|
||||
``None``).
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.unhide()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.hide`
|
||||
|
||||
"""
|
||||
for submissions in self._chunk(
|
||||
chunk_size=50, other_submissions=other_submissions
|
||||
):
|
||||
self._reddit.post(API_PATH["unhide"], data={"id": submissions})
|
||||
|
||||
|
||||
Subreddit._submission_class = Submission
|
||||
4255
backend/venv/Lib/site-packages/praw/models/reddit/subreddit.py
Normal file
4255
backend/venv/Lib/site-packages/praw/models/reddit/subreddit.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,242 @@
|
||||
"""Provide the :class:`.UserSubreddit` class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
from warnings import warn
|
||||
|
||||
from ...util.cache import cachedproperty
|
||||
from .subreddit import Subreddit, SubredditModeration
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class UserSubreddit(Subreddit):
|
||||
"""A class for :class:`.User` Subreddits.
|
||||
|
||||
To obtain an instance of this class execute:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit = reddit.user.me().subreddit
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
========================= ==========================================================
|
||||
Attribute Description
|
||||
========================= ==========================================================
|
||||
``can_assign_link_flair`` Whether users can assign their own link flair.
|
||||
``can_assign_user_flair`` Whether users can assign their own user flair.
|
||||
``created_utc`` Time the subreddit was created, represented in `Unix
|
||||
Time`_.
|
||||
``description`` Subreddit description, in Markdown.
|
||||
``description_html`` Subreddit description, in HTML.
|
||||
``display_name`` Name of the subreddit.
|
||||
``id`` ID of the subreddit.
|
||||
``name`` Fullname of the subreddit.
|
||||
``over18`` Whether the subreddit is NSFW.
|
||||
``public_description`` Description of the subreddit, shown in searches and on the
|
||||
"You must be invited to visit this community" page (if
|
||||
applicable).
|
||||
``spoilers_enabled`` Whether the spoiler tag feature is enabled.
|
||||
``subscribers`` Count of subscribers. This will be ``0`` unless unless the
|
||||
authenticated user is a moderator.
|
||||
``user_is_banned`` Whether the authenticated user is banned.
|
||||
``user_is_moderator`` Whether the authenticated user is a moderator.
|
||||
``user_is_subscriber`` Whether the authenticated user is subscribed.
|
||||
========================= ==========================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _dict_deprecated_wrapper(func: Callable) -> Callable:
|
||||
"""Show deprecation notice for dict only methods."""
|
||||
|
||||
def wrapper(*args: Any, **kwargs: Any):
|
||||
warn(
|
||||
"'Redditor.subreddit' is no longer a dict and is now an UserSubreddit"
|
||||
f" object. Using '{func.__name__}' is deprecated and will be removed in"
|
||||
" PRAW 8.",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
@cachedproperty
|
||||
def mod(self) -> praw.models.reddit.user_subreddit.UserSubredditModeration:
|
||||
"""Provide an instance of :class:`.UserSubredditModeration`.
|
||||
|
||||
For example, to update the authenticated user's display name:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.user.me().subreddit.mod.update(title="New display name")
|
||||
|
||||
"""
|
||||
return UserSubredditModeration(self)
|
||||
|
||||
def __getitem__(self, item: str) -> Any:
|
||||
"""Show deprecation notice for dict method ``__getitem__``."""
|
||||
warn(
|
||||
"'Redditor.subreddit' is no longer a dict and is now an UserSubreddit"
|
||||
" object. Accessing attributes using string indices is deprecated.",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return getattr(self, item)
|
||||
|
||||
def __init__(self, reddit: praw.Reddit, *args: Any, **kwargs: Any):
|
||||
"""Initialize an :class:`.UserSubreddit` instance.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
|
||||
.. note::
|
||||
|
||||
This class should not be initialized directly. Instead, obtain an instance
|
||||
via: ``reddit.user.me().subreddit`` or
|
||||
``reddit.redditor("redditor_name").subreddit``.
|
||||
|
||||
"""
|
||||
|
||||
def predicate(item: str):
|
||||
name = getattr(item, "__name__", None)
|
||||
return name not in dir(object) + dir(Subreddit) and name in dir(dict)
|
||||
|
||||
for name, _member in inspect.getmembers(dict, predicate=predicate):
|
||||
if name != "__getitem__":
|
||||
setattr(
|
||||
self,
|
||||
name,
|
||||
self._dict_deprecated_wrapper(getattr(self.__dict__, name)),
|
||||
)
|
||||
|
||||
super().__init__(reddit, *args, **kwargs)
|
||||
|
||||
|
||||
# noinspection PyIncorrectDocstring
|
||||
class UserSubredditModeration(SubredditModeration):
|
||||
"""Provides a set of moderation functions to a :class:`.UserSubreddit`.
|
||||
|
||||
For example, to accept a moderation invite from the user subreddit of u/spez:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").mod.accept_invite()
|
||||
|
||||
"""
|
||||
|
||||
def update(self, **settings: str | int | bool) -> dict[str, str | int | bool]:
|
||||
"""Update the :class:`.Subreddit`'s settings.
|
||||
|
||||
:param all_original_content: Mandate all submissions to be original content
|
||||
only.
|
||||
:param allow_chat_post_creation: Allow users to create chat submissions.
|
||||
:param allow_images: Allow users to upload images using the native image
|
||||
hosting.
|
||||
:param allow_polls: Allow users to post polls to the subreddit.
|
||||
:param allow_post_crossposts: Allow users to crosspost submissions from other
|
||||
subreddits.
|
||||
:param allow_top: Allow the subreddit to appear on r/all as well as the default
|
||||
and trending lists.
|
||||
:param allow_videos: Allow users to upload videos using the native image
|
||||
hosting.
|
||||
:param collapse_deleted_comments: Collapse deleted and removed comments on
|
||||
comments pages by default.
|
||||
:param crowd_control_chat_level: Controls the crowd control level for chat
|
||||
rooms. Goes from 0-3.
|
||||
:param crowd_control_level: Controls the crowd control level for submissions.
|
||||
Goes from 0-3.
|
||||
:param crowd_control_mode: Enables/disables crowd control.
|
||||
:param comment_score_hide_mins: The number of minutes to hide comment scores.
|
||||
:param description: Shown in the sidebar of your subreddit.
|
||||
:param disable_contributor_requests: Specifies whether redditors may send
|
||||
automated modmail messages requesting approval as a submitter.
|
||||
:param exclude_banned_modqueue: Exclude posts by site-wide banned users from
|
||||
modqueue/unmoderated.
|
||||
:param free_form_reports: Allow users to specify custom reasons in the report
|
||||
menu.
|
||||
:param header_hover_text: The text seen when hovering over the snoo.
|
||||
:param hide_ads: Don't show ads within this subreddit. Only applies to
|
||||
Premium-user only subreddits.
|
||||
:param key_color: A 6-digit rgb hex color (e.g., ``"#AABBCC"``), used as a
|
||||
thematic color for your subreddit on mobile.
|
||||
:param lang: A valid IETF language tag (underscore separated).
|
||||
:param link_type: The types of submissions users can make. One of ``"any"``,
|
||||
``"link"``, or ``"self"``.
|
||||
:param original_content_tag_enabled: Enables the use of the ``original content``
|
||||
label for submissions.
|
||||
:param over_18: Viewers must be over 18 years old (i.e., NSFW).
|
||||
:param public_description: Public description blurb. Appears in search results
|
||||
and on the landing page for private subreddits.
|
||||
:param public_traffic: Make the traffic stats page public.
|
||||
:param restrict_commenting: Specifies whether approved users have the ability to
|
||||
comment.
|
||||
:param restrict_posting: Specifies whether approved users have the ability to
|
||||
submit posts.
|
||||
:param show_media: Show thumbnails on submissions.
|
||||
:param show_media_preview: Expand media previews on comments pages.
|
||||
:param spam_comments: Spam filter strength for comments. One of ``"all"``,
|
||||
``"low"``, or ``"high"``.
|
||||
:param spam_links: Spam filter strength for links. One of ``"all"``, ``"low"``,
|
||||
or ``"high"``.
|
||||
:param spam_selfposts: Spam filter strength for selfposts. One of ``"all"``,
|
||||
``"low"``, or ``"high"``.
|
||||
:param spoilers_enabled: Enable marking posts as containing spoilers.
|
||||
:param submit_link_label: Custom label for submit link button (None for
|
||||
default).
|
||||
:param submit_text: Text to show on submission page.
|
||||
:param submit_text_label: Custom label for submit text post button (None for
|
||||
default).
|
||||
:param subreddit_type: The string ``"user"``.
|
||||
:param suggested_comment_sort: All comment threads will use this sorting method
|
||||
by default. Leave ``None``, or choose one of ``confidence``,
|
||||
``"controversial"``, ``"live"``, ``"new"``, ``"old"``, ``"qa"``,
|
||||
``"random"``, or ``"top"``.
|
||||
:param title: The title of the subreddit.
|
||||
:param welcome_message_enabled: Enables the subreddit welcome message.
|
||||
:param welcome_message_text: The text to be used as a welcome message. A welcome
|
||||
message is sent to all new subscribers by a Reddit bot.
|
||||
:param wiki_edit_age: Account age, in days, required to edit and create wiki
|
||||
pages.
|
||||
:param wiki_edit_karma: Subreddit karma required to edit and create wiki pages.
|
||||
:param wikimode: One of ``"anyone"``, ``"disabled"``, or ``"modonly"``.
|
||||
|
||||
Additional keyword arguments can be provided to handle new settings as Reddit
|
||||
introduces them.
|
||||
|
||||
Settings that are documented here and aren't explicitly set by you in a call to
|
||||
:meth:`.SubredditModeration.update` should retain their current value. If they
|
||||
do not please file a bug.
|
||||
|
||||
.. warning::
|
||||
|
||||
Undocumented settings, or settings that were very recently documented, may
|
||||
not retain their current value when updating. This often occurs when Reddit
|
||||
adds a new setting but forgets to add that setting to the API endpoint that
|
||||
is used to fetch the current settings.
|
||||
|
||||
"""
|
||||
current_settings = self.settings()
|
||||
|
||||
# These attributes come out using different names than they go in.
|
||||
remap = {
|
||||
"allow_top": "default_set",
|
||||
"header_title": "header_hover_text",
|
||||
"lang": "language",
|
||||
"link_type": "content_options",
|
||||
"sr": "subreddit_id",
|
||||
"type": "subreddit_type",
|
||||
}
|
||||
for new, old in remap.items():
|
||||
current_settings[new] = current_settings.pop(old)
|
||||
|
||||
current_settings.update(settings)
|
||||
return UserSubreddit._create_or_update(
|
||||
_reddit=self.subreddit._reddit, **current_settings
|
||||
)
|
||||
1871
backend/venv/Lib/site-packages/praw/models/reddit/widgets.py
Normal file
1871
backend/venv/Lib/site-packages/praw/models/reddit/widgets.py
Normal file
File diff suppressed because it is too large
Load Diff
346
backend/venv/Lib/site-packages/praw/models/reddit/wikipage.py
Normal file
346
backend/venv/Lib/site-packages/praw/models/reddit/wikipage.py
Normal file
@@ -0,0 +1,346 @@
|
||||
"""Provide the WikiPage class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Generator, Iterator
|
||||
|
||||
from ...const import API_PATH
|
||||
from ...util import _deprecate_args
|
||||
from ...util.cache import cachedproperty
|
||||
from ..listing.generator import ListingGenerator
|
||||
from .base import RedditBase
|
||||
from .redditor import Redditor
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class WikiPageModeration:
|
||||
"""Provides a set of moderation functions for a :class:`.WikiPage`.
|
||||
|
||||
For example, to add u/spez as an editor on the wikipage ``"praw_test"`` try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").wiki["praw_test"].mod.add("spez")
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, wikipage: WikiPage):
|
||||
"""Initialize a :class:`.WikiPageModeration` instance.
|
||||
|
||||
:param wikipage: The wikipage to moderate.
|
||||
|
||||
"""
|
||||
self.wikipage = wikipage
|
||||
|
||||
def add(self, redditor: praw.models.Redditor):
|
||||
"""Add an editor to this :class:`.WikiPage`.
|
||||
|
||||
:param redditor: A redditor name or :class:`.Redditor` instance.
|
||||
|
||||
To add u/spez as an editor on the wikipage ``"praw_test"`` try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").wiki["praw_test"].mod.add("spez")
|
||||
|
||||
"""
|
||||
data = {"page": self.wikipage.name, "username": str(redditor)}
|
||||
url = API_PATH["wiki_page_editor"].format(
|
||||
subreddit=self.wikipage.subreddit, method="add"
|
||||
)
|
||||
self.wikipage._reddit.post(url, data=data)
|
||||
|
||||
def remove(self, redditor: praw.models.Redditor):
|
||||
"""Remove an editor from this :class:`.WikiPage`.
|
||||
|
||||
:param redditor: A redditor name or :class:`.Redditor` instance.
|
||||
|
||||
To remove u/spez as an editor on the wikipage ``"praw_test"`` try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").wiki["praw_test"].mod.remove("spez")
|
||||
|
||||
"""
|
||||
data = {"page": self.wikipage.name, "username": str(redditor)}
|
||||
url = API_PATH["wiki_page_editor"].format(
|
||||
subreddit=self.wikipage.subreddit, method="del"
|
||||
)
|
||||
self.wikipage._reddit.post(url, data=data)
|
||||
|
||||
def revert(self):
|
||||
"""Revert a wikipage back to a specific revision.
|
||||
|
||||
To revert the page ``"praw_test"`` in r/test to revision ``"1234abc"``, try
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").wiki["praw_test"].revision("1234abc").mod.revert()
|
||||
|
||||
.. note::
|
||||
|
||||
When you attempt to revert the page ``config/stylesheet``, Reddit checks to
|
||||
see if the revision being reverted to passes the CSS filter. If the check
|
||||
fails, then the revision attempt will also fail, and a
|
||||
``prawcore.Forbidden`` exception will be raised. For example, you can't
|
||||
revert to a revision that contains a link to ``url(%%PRAW%%)`` if there is
|
||||
no image named ``PRAW`` on the current stylesheet.
|
||||
|
||||
Here is an example of how to look for this type of error:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from prawcore.exceptions import Forbidden
|
||||
|
||||
try:
|
||||
reddit.subreddit("test").wiki["config/stylesheet"].revision("1234abc").mod.revert()
|
||||
except Forbidden as exception:
|
||||
try:
|
||||
exception.response.json()
|
||||
except ValueError:
|
||||
exception.response.text
|
||||
|
||||
If the error occurs, the output will look something like
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
{"reason": "INVALID_CSS", "message": "Forbidden", "explanation": "%(css_error)s"}
|
||||
|
||||
"""
|
||||
self.wikipage._reddit.post(
|
||||
API_PATH["wiki_revert"].format(subreddit=self.wikipage.subreddit),
|
||||
data={
|
||||
"page": self.wikipage.name,
|
||||
"revision": self.wikipage._revision,
|
||||
},
|
||||
)
|
||||
|
||||
def settings(self) -> dict[str, Any]:
|
||||
"""Return the settings for this :class:`.WikiPage`."""
|
||||
url = API_PATH["wiki_page_settings"].format(
|
||||
subreddit=self.wikipage.subreddit, page=self.wikipage.name
|
||||
)
|
||||
return self.wikipage._reddit.get(url)["data"]
|
||||
|
||||
@_deprecate_args("listed", "permlevel")
|
||||
def update(
|
||||
self, *, listed: bool, permlevel: int, **other_settings: Any
|
||||
) -> dict[str, Any]:
|
||||
"""Update the settings for this :class:`.WikiPage`.
|
||||
|
||||
:param listed: Show this page on page list.
|
||||
:param permlevel: Who can edit this page? ``0`` use subreddit wiki permissions,
|
||||
``1`` only approved wiki contributors for this page may edit (see
|
||||
:meth:`.WikiPageModeration.add`), ``2`` only mods may edit and view.
|
||||
:param other_settings: Additional keyword arguments to pass.
|
||||
|
||||
:returns: The updated WikiPage settings.
|
||||
|
||||
To set the wikipage ``"praw_test"`` in r/test to mod only and disable it from
|
||||
showing in the page list, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").wiki["praw_test"].mod.update(listed=False, permlevel=2)
|
||||
|
||||
"""
|
||||
other_settings.update({"listed": listed, "permlevel": permlevel})
|
||||
url = API_PATH["wiki_page_settings"].format(
|
||||
subreddit=self.wikipage.subreddit, page=self.wikipage.name
|
||||
)
|
||||
return self.wikipage._reddit.post(url, data=other_settings)["data"]
|
||||
|
||||
|
||||
class WikiPage(RedditBase):
|
||||
"""An individual :class:`.WikiPage` object.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
================= =================================================================
|
||||
Attribute Description
|
||||
================= =================================================================
|
||||
``content_html`` The contents of the wiki page, as HTML.
|
||||
``content_md`` The contents of the wiki page, as Markdown.
|
||||
``may_revise`` A ``bool`` representing whether or not the authenticated user may
|
||||
edit the wiki page.
|
||||
``name`` The name of the wiki page.
|
||||
``revision_by`` The :class:`.Redditor` who authored this revision of the wiki
|
||||
page.
|
||||
``revision_date`` The time of this revision, in `Unix Time`_.
|
||||
``subreddit`` The :class:`.Subreddit` this wiki page belongs to.
|
||||
================= =================================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
__hash__ = RedditBase.__hash__
|
||||
|
||||
@staticmethod
|
||||
def _revision_generator(
|
||||
*,
|
||||
generator_kwargs: dict[str, Any],
|
||||
subreddit: praw.models.Subreddit,
|
||||
url: str,
|
||||
) -> Generator[
|
||||
dict[str, Redditor | WikiPage | str | int | bool | None], None, None
|
||||
]:
|
||||
for revision in ListingGenerator(subreddit._reddit, url, **generator_kwargs):
|
||||
if revision["author"] is not None:
|
||||
revision["author"] = Redditor(
|
||||
subreddit._reddit, _data=revision["author"]["data"]
|
||||
)
|
||||
revision["page"] = WikiPage(
|
||||
subreddit._reddit, subreddit, revision["page"], revision["id"]
|
||||
)
|
||||
yield revision
|
||||
|
||||
@cachedproperty
|
||||
def mod(self) -> WikiPageModeration:
|
||||
"""Provide an instance of :class:`.WikiPageModeration`.
|
||||
|
||||
For example, to add u/spez as an editor on the wikipage ``"praw_test"`` try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").wiki["praw_test"].mod.add("spez")
|
||||
|
||||
"""
|
||||
return WikiPageModeration(self)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
subreddit: praw.models.Subreddit,
|
||||
name: str,
|
||||
revision: str | None = None,
|
||||
_data: dict[str, Any] | None = None,
|
||||
):
|
||||
"""Initialize a :class:`.WikiPage` instance.
|
||||
|
||||
:param revision: A specific revision ID to fetch. By default, fetches the most
|
||||
recent revision.
|
||||
|
||||
"""
|
||||
self.name = name
|
||||
self._revision = revision
|
||||
self.subreddit = subreddit
|
||||
super().__init__(reddit, _data=_data, _str_field=False)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return an object initialization representation of the instance."""
|
||||
return (
|
||||
f"{self.__class__.__name__}(subreddit={self.subreddit!r},"
|
||||
f" name={self.name!r})"
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return a string representation of the instance."""
|
||||
return f"{self.subreddit}/{self.name}"
|
||||
|
||||
def _fetch(self):
|
||||
data = self._fetch_data()
|
||||
data = data["data"]
|
||||
if data["revision_by"] is not None:
|
||||
data["revision_by"] = Redditor(
|
||||
self._reddit, _data=data["revision_by"]["data"]
|
||||
)
|
||||
self.__dict__.update(data)
|
||||
super()._fetch()
|
||||
|
||||
def _fetch_info(self):
|
||||
return (
|
||||
"wiki_page",
|
||||
{"subreddit": self.subreddit, "page": self.name},
|
||||
{"v": self._revision} if self._revision else None,
|
||||
)
|
||||
|
||||
def discussions(self, **generator_kwargs: Any) -> Iterator[praw.models.Submission]:
|
||||
"""Return a :class:`.ListingGenerator` for discussions of a wiki page.
|
||||
|
||||
Discussions are site-wide links to a wiki page.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
To view the titles of discussions of the page ``"praw_test"`` in r/test, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for submission in reddit.subreddit("test").wiki["praw_test"].discussions():
|
||||
print(submission.title)
|
||||
|
||||
"""
|
||||
return ListingGenerator(
|
||||
self._reddit,
|
||||
API_PATH["wiki_discussions"].format(
|
||||
subreddit=self.subreddit, page=self.name
|
||||
),
|
||||
**generator_kwargs,
|
||||
)
|
||||
|
||||
@_deprecate_args("content", "reason")
|
||||
def edit(self, *, content: str, reason: str | None = None, **other_settings: Any):
|
||||
"""Edit this wiki page's contents.
|
||||
|
||||
:param content: The updated Markdown content of the page.
|
||||
:param reason: The reason for the revision.
|
||||
:param other_settings: Additional keyword arguments to pass.
|
||||
|
||||
For example, to replace the first wiki page of r/test with the phrase ``"test
|
||||
wiki page"``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
page = next(iter(reddit.subreddit("test").wiki))
|
||||
page.edit(content="test wiki page")
|
||||
|
||||
"""
|
||||
other_settings.update({"content": content, "page": self.name, "reason": reason})
|
||||
self._reddit.post(
|
||||
API_PATH["wiki_edit"].format(subreddit=self.subreddit), data=other_settings
|
||||
)
|
||||
|
||||
def revision(self, revision: str) -> WikiPage:
|
||||
"""Return a specific version of this page by revision ID.
|
||||
|
||||
To view revision ``"1234abc"`` of ``"praw_test"`` in r/test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
page = reddit.subreddit("test").wiki["praw_test"].revision("1234abc")
|
||||
|
||||
"""
|
||||
return WikiPage(self.subreddit._reddit, self.subreddit, self.name, revision)
|
||||
|
||||
def revisions(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Generator[WikiPage, None, None]:
|
||||
"""Return a :class:`.ListingGenerator` for page revisions.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
To view the wiki revisions for ``"praw_test"`` in r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for item in reddit.subreddit("test").wiki["praw_test"].revisions():
|
||||
print(item)
|
||||
|
||||
To get :class:`.WikiPage` objects for each revision:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for item in reddit.subreddit("test").wiki["praw_test"].revisions():
|
||||
print(item["page"])
|
||||
|
||||
"""
|
||||
url = API_PATH["wiki_page_revisions"].format(
|
||||
subreddit=self.subreddit, page=self.name
|
||||
)
|
||||
return self._revision_generator(
|
||||
generator_kwargs=generator_kwargs, subreddit=self.subreddit, url=url
|
||||
)
|
||||
Reference in New Issue
Block a user