2026-02-01 09:31:38 +01:00

805 lines
26 KiB
Python

"""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()