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

345 lines
12 KiB
Python

"""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."""