We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
modmail conversation messages list only includes the most recent message
### Describe the Bug

If you retrieve modmail through the `subreddit.modmail.conversations()` API, the `.messages` list only includes the most recent message rather than all of the messages for each conversation. The length is always 1 even if thread.num_messages is greater than 1.

Accessing a modmail thread's messages via `subreddit.modmail(thread_id).messages` works as expected.

The example code below will output the unexpected results using PRAW 7.6 (assuming the account's modmail has some threads with more than one message). On PRAW 7.3, the example code produces no output.

### Desired Result

The `.messages` attribute should include all of the messages for each thread.

### Relevant Logs

_No response_

### Code to reproduce the bug

```python
for thread in r.subreddit("all").modmail.conversations(limit=25):
    if len(thread.messages) != thread.num_messages:
        print(f"unexpected result: {thread.id} num_messages={thread.num_messages} length={len(thread.messages)}")
```


### My code example does not include the `Reddit()` initialization to prevent credential leakage.

Yes

### This code has previously worked as intended.

Yes

### Operating System/Environment

Linux

### Python Version

3.7.3

### PRAW Version

7.6.0

### Prawcore Version

2.2.0

### Anything else?

_No response_

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `praw/objector.py`
Content:
```
1 """Provides the Objector class."""
2 from datetime import datetime
3 from json import loads
4 from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
5 
6 from .exceptions import ClientException, RedditAPIException
7 from .models.reddit.base import RedditBase
8 from .util import snake_case_keys
9 
10 if TYPE_CHECKING:  # pragma: no cover
11     import praw
12 
13 
14 class Objector:
15     """The objector builds :class:`.RedditBase` objects."""
16 
17     @classmethod
18     def parse_error(
19         cls, data: Union[List[Any], Dict[str, Dict[str, str]]]
20     ) -> Optional[RedditAPIException]:
21         """Convert JSON response into an error object.
22 
23         :param data: The dict to be converted.
24 
25         :returns: An instance of :class:`.RedditAPIException`, or ``None`` if ``data``
26             doesn't fit this model.
27 
28         """
29         if isinstance(data, list):
30             # Fetching a Submission returns a list (of two items). Although it's handled
31             # manually in `Submission._fetch()`, assume it's a possibility here.
32             return None
33 
34         errors = data.get("json", {}).get("errors")
35         if errors is None:
36             return None
37         if len(errors) < 1:
38             # See `Collection._fetch()`.
39             raise ClientException("successful error response", data)
40         return RedditAPIException(errors)
41 
42     @classmethod
43     def check_error(cls, data: Union[List[Any], Dict[str, Dict[str, str]]]):
44         """Raise an error if the argument resolves to an error object."""
45         error = cls.parse_error(data)
46         if error:
47             raise error
48 
49     def __init__(self, reddit: "praw.Reddit", parsers: Optional[Dict[str, Any]] = None):
50         """Initialize an :class:`.Objector` instance.
51 
52         :param reddit: An instance of :class:`.Reddit`.
53 
54         """
55         self.parsers = {} if parsers is None else parsers
56         self._reddit = reddit
57 
58     def _objectify_dict(self, data):
59         """Create :class:`.RedditBase` objects from dicts.
60 
61         :param data: The structured data, assumed to be a dict.
62 
63         :returns: An instance of :class:`.RedditBase`.
64 
65         """
66         if {"messages", "modActions"}.issubset(data) and {
67             "conversations",
68             "conversation",
69         }.intersection(data):
70             data.update(
71                 data.pop("conversation")
72                 if "conversation" in data
73                 else data.pop("conversations")
74             )
75             parser: "praw.models.ModmailConversation" = self.parsers[
76                 "ModmailConversation"
77             ]
78             parser._convert_conversation_objects(data, self._reddit)
79         elif {"messages", "modActions"}.issubset(data):
80             parser = self.parsers["ModmailConversation"]
81         elif {"conversationIds", "conversations", "messages"}.issubset(data):
82             conversations = []
83             temp_data = {
84                 thing_type: data.pop(thing_type, {})
85                 for thing_type in ["messages", "modActions"]
86             }
87             for conversation_id in data["conversationIds"]:
88                 conversation_data = {
89                     **data["conversations"][conversation_id],
90                     "messages": [],
91                     "modActions": [],
92                 }
93                 for object_id in conversation_data["objIds"]:
94                     conversation_data[object_id["key"]].append(
95                         self._objectify_dict(
96                             temp_data[object_id["key"]].pop(object_id["id"])
97                         )
98                     )
99                 conversations.append(conversation_data)
100             data["conversations"] = conversations
101             del temp_data
102             data = snake_case_keys(data)
103             parser = self.parsers["ModmailConversations-list"]
104         elif {"actionTypeId", "author", "date"}.issubset(data):
105             # Modmail mod action
106             data = snake_case_keys(data)
107             parser = self.parsers["ModmailAction"]
108         elif {"bodyMarkdown", "isInternal"}.issubset(data):
109             # Modmail message
110             data = snake_case_keys(data)
111             parser = self.parsers["ModmailMessage"]
112         elif {"kind", "short_name", "violation_reason"}.issubset(data):
113             # This is a Rule
114             parser = self.parsers["rule"]
115         elif {"isAdmin", "isDeleted"}.issubset(data):
116             # Modmail author
117             data = snake_case_keys(data)
118             # Prevent clobbering base-36 id
119             del data["id"]
120             data["is_subreddit_mod"] = data.pop("is_mod")
121             parser = self.parsers[self._reddit.config.kinds["redditor"]]
122         elif {"banStatus", "muteStatus", "recentComments"}.issubset(data):
123             # Modmail user
124             data = snake_case_keys(data)
125             data["created_string"] = data.pop("created")
126             parser = self.parsers[self._reddit.config.kinds["redditor"]]
127         elif {"displayName", "id", "type"}.issubset(data):
128             # Modmail subreddit
129             data = snake_case_keys(data)
130             parser = self.parsers[self._reddit.config.kinds[data["type"]]]
131         elif {"date", "id", "name"}.issubset(data) or {
132             "id",
133             "name",
134             "permissions",
135         }.issubset(data):
136             parser = self.parsers[self._reddit.config.kinds["redditor"]]
137         elif {"text", "url"}.issubset(data):
138             if "color" in data or "linkUrl" in data:
139                 parser = self.parsers["Button"]
140             else:
141                 parser = self.parsers["MenuLink"]
142         elif {"children", "text"}.issubset(data):
143             parser = self.parsers["Submenu"]
144         elif {"height", "url", "width"}.issubset(data):
145             parser = self.parsers["Image"]
146         elif {"isSubscribed", "name", "subscribers"}.issubset(data):
147             # discards icon and subscribed information
148             return self._reddit.subreddit(data["name"])
149         elif {"authorFlairType", "name"}.issubset(data):
150             # discards flair information
151             return self._reddit.redditor(data["name"])
152         elif {"parent_id"}.issubset(data):
153             parser = self.parsers[self._reddit.config.kinds["comment"]]
154         elif "collection_id" in data.keys():
155             parser = self.parsers["Collection"]
156         elif {"moderators", "moderatorIds", "allUsersLoaded", "subredditId"}.issubset(
157             data
158         ):
159             data = snake_case_keys(data)
160             moderators = []
161             for mod_id in data["moderator_ids"]:
162                 mod = snake_case_keys(data["moderators"][mod_id])
163                 mod["mod_permissions"] = list(mod["mod_permissions"].keys())
164                 moderators.append(mod)
165             data["moderators"] = moderators
166             parser = self.parsers["moderator-list"]
167         elif "username" in data.keys():
168             data["name"] = data.pop("username")
169             parser = self.parsers[self._reddit.config.kinds["redditor"]]
170         elif {"mod_permissions", "name", "sr", "subscribers"}.issubset(data):
171             data["display_name"] = data["sr"]
172             parser = self.parsers[self._reddit.config.kinds["subreddit"]]
173         elif {"drafts", "subreddits"}.issubset(data):  # Draft list
174             subreddit_parser = self.parsers[self._reddit.config.kinds["subreddit"]]
175             user_subreddit_parser = self.parsers["UserSubreddit"]
176             subreddits = {
177                 subreddit["name"]: user_subreddit_parser.parse(subreddit, self._reddit)
178                 if subreddit["display_name_prefixed"].startswith("u/")
179                 else subreddit_parser.parse(subreddit, self._reddit)
180                 for subreddit in data.pop("subreddits")
181             }
182             for draft in data["drafts"]:
183                 if draft["subreddit"]:
184                     draft["subreddit"] = subreddits[draft["subreddit"]]
185                 draft["modified"] = datetime.fromtimestamp(
186                     draft["modified"] / 1000
187                 ).astimezone()
188             parser = self.parsers["DraftList"]
189         elif {"mod_action_data", "user_note_data"}.issubset(data):
190             data["moderator"] = self._reddit.redditor(data["operator"])
191             data["subreddit"] = self._reddit.subreddit(data["subreddit"])
192             data["user"] = self._reddit.redditor(data["user"])
193             # move these sub dict values into the main dict for simplicity
194             data.update(data["mod_action_data"])
195             del data["mod_action_data"]
196             data.update(data["user_note_data"])
197             del data["user_note_data"]
198             parser = self.parsers["mod_note"]
199         elif (
200             "created" in data
201             and isinstance(data["created"], dict)
202             and {"mod_action_data", "user_note_data"}.issubset(data["created"])
203         ):
204             data = data["created"]
205             return self._objectify_dict(data)
206         else:
207             if "user" in data:
208                 parser = self.parsers[self._reddit.config.kinds["redditor"]]
209                 data["user"] = parser.parse({"name": data["user"]}, self._reddit)
210             return data
211         return parser.parse(data, self._reddit)
212 
213     def objectify(
214         self, data: Optional[Union[Dict[str, Any], List[Any], bool]]
215     ) -> Optional[Union[RedditBase, Dict[str, Any], List[Any], bool]]:
216         """Create :class:`.RedditBase` objects from data.
217 
218         :param data: The structured data.
219 
220         :returns: An instance of :class:`.RedditBase`, or ``None`` if given ``data`` is
221             ``None``.
222 
223         """
224         # pylint: disable=too-many-return-statements
225         if data is None:  # 204 no content
226             return None
227         if isinstance(data, list):
228             return [self.objectify(item) for item in data]
229         if isinstance(data, bool):  # Reddit.username_available
230             return data
231         if "json" in data and "errors" in data["json"]:
232             errors = data["json"]["errors"]
233             if len(errors) > 0:
234                 raise RedditAPIException(errors)
235         if "kind" in data and (
236             "shortName" in data or data["kind"] in ("menu", "moderators")
237         ):
238             # This is a widget
239             parser = self.parsers.get(data["kind"], self.parsers["widget"])
240             return parser.parse(data, self._reddit)
241         if {"kind", "data"}.issubset(data) and data["kind"] in self.parsers:
242             parser = self.parsers[data["kind"]]
243             if data["kind"] == "ModeratedList":
244                 return parser.parse(data, self._reddit)
245             else:
246                 return parser.parse(data["data"], self._reddit)
247         if "json" in data and "data" in data["json"]:
248             if "websocket_url" in data["json"]["data"]:
249                 return data
250             if "things" in data["json"]["data"]:  # Submission.reply
251                 return self.objectify(data["json"]["data"]["things"])
252             if "rules" in data["json"]["data"]:
253                 return self.objectify(loads(data["json"]["data"]["rules"]))
254             if "drafts_count" in data["json"]["data"] and all(
255                 [key not in data["json"]["data"] for key in ["name", "url"]]
256             ):  # Draft
257                 data["json"]["data"].pop("drafts_count")
258                 return self.parsers["Draft"].parse(data["json"]["data"], self._reddit)
259             if "url" in data["json"]["data"]:  # Subreddit.submit
260                 # The URL is the URL to the submission, so it's removed.
261                 del data["json"]["data"]["url"]
262                 parser = self.parsers[self._reddit.config.kinds["submission"]]
263                 if data["json"]["data"]["id"].startswith(
264                     f"{self._reddit.config.kinds['submission']}_"
265                 ):
266                     # With polls, Reddit returns a fullname but calls it an "id". This
267                     # fixes this by coercing the fullname into an id.
268                     data["json"]["data"]["id"] = data["json"]["data"]["id"].split(
269                         "_", 1
270                     )[1]
271             else:
272                 parser = self.parsers["LiveUpdateEvent"]
273             return parser.parse(data["json"]["data"], self._reddit)
274         if {"is_public_link", "title", "body"}.issubset(data):
275             parser = self.parsers["Draft"]
276             return parser.parse(data, self._reddit)
277         if "rules" in data:
278             return self.objectify(data["rules"])
279         elif isinstance(data, dict):
280             return self._objectify_dict(data)
281 
282         return data
283
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/praw/objector.py b/praw/objector.py
--- a/praw/objector.py
+++ b/praw/objector.py
@@ -72,33 +72,18 @@
                 if "conversation" in data
                 else data.pop("conversations")
             )
-            parser: "praw.models.ModmailConversation" = self.parsers[
-                "ModmailConversation"
-            ]
+            parser = self.parsers["ModmailConversation"]
             parser._convert_conversation_objects(data, self._reddit)
-        elif {"messages", "modActions"}.issubset(data):
+        elif {"messages", "modActions"}.issubset(data) or {
+            "legacyFirstMessageId",
+            "state",
+        }.issubset(data):
             parser = self.parsers["ModmailConversation"]
         elif {"conversationIds", "conversations", "messages"}.issubset(data):
-            conversations = []
-            temp_data = {
-                thing_type: data.pop(thing_type, {})
-                for thing_type in ["messages", "modActions"]
-            }
-            for conversation_id in data["conversationIds"]:
-                conversation_data = {
-                    **data["conversations"][conversation_id],
-                    "messages": [],
-                    "modActions": [],
-                }
-                for object_id in conversation_data["objIds"]:
-                    conversation_data[object_id["key"]].append(
-                        self._objectify_dict(
-                            temp_data[object_id["key"]].pop(object_id["id"])
-                        )
-                    )
-                conversations.append(conversation_data)
-            data["conversations"] = conversations
-            del temp_data
+            data["conversations"] = [
+                data["conversations"][conversation_id]
+                for conversation_id in data["conversationIds"]
+            ]
             data = snake_case_keys(data)
             parser = self.parsers["ModmailConversations-list"]
         elif {"actionTypeId", "author", "date"}.issubset(data):

{"golden_diff": "diff --git a/praw/objector.py b/praw/objector.py\n--- a/praw/objector.py\n+++ b/praw/objector.py\n@@ -72,33 +72,18 @@\n                 if \"conversation\" in data\n                 else data.pop(\"conversations\")\n             )\n-            parser: \"praw.models.ModmailConversation\" = self.parsers[\n-                \"ModmailConversation\"\n-            ]\n+            parser = self.parsers[\"ModmailConversation\"]\n             parser._convert_conversation_objects(data, self._reddit)\n-        elif {\"messages\", \"modActions\"}.issubset(data):\n+        elif {\"messages\", \"modActions\"}.issubset(data) or {\n+            \"legacyFirstMessageId\",\n+            \"state\",\n+        }.issubset(data):\n             parser = self.parsers[\"ModmailConversation\"]\n         elif {\"conversationIds\", \"conversations\", \"messages\"}.issubset(data):\n-            conversations = []\n-            temp_data = {\n-                thing_type: data.pop(thing_type, {})\n-                for thing_type in [\"messages\", \"modActions\"]\n-            }\n-            for conversation_id in data[\"conversationIds\"]:\n-                conversation_data = {\n-                    **data[\"conversations\"][conversation_id],\n-                    \"messages\": [],\n-                    \"modActions\": [],\n-                }\n-                for object_id in conversation_data[\"objIds\"]:\n-                    conversation_data[object_id[\"key\"]].append(\n-                        self._objectify_dict(\n-                            temp_data[object_id[\"key\"]].pop(object_id[\"id\"])\n-                        )\n-                    )\n-                conversations.append(conversation_data)\n-            data[\"conversations\"] = conversations\n-            del temp_data\n+            data[\"conversations\"] = [\n+                data[\"conversations\"][conversation_id]\n+                for conversation_id in data[\"conversationIds\"]\n+            ]\n             data = snake_case_keys(data)\n             parser = self.parsers[\"ModmailConversations-list\"]\n         elif {\"actionTypeId\", \"author\", \"date\"}.issubset(data):\n", "issue": "modmail conversation messages list only includes the most recent message\n### Describe the Bug\n\nIf you retrieve modmail through the `subreddit.modmail.conversations()` API, the `.messages` list only includes the most recent message rather than all of the messages for each conversation. The length is always 1 even if thread.num_messages is greater than 1.\r\n\r\nAccessing a modmail thread's messages via `subreddit.modmail(thread_id).messages` works as expected.\r\n\r\nThe example code below will output the unexpected results using PRAW 7.6 (assuming the account's modmail has some threads with more than one message). On PRAW 7.3, the example code produces no output.\n\n### Desired Result\n\nThe `.messages` attribute should include all of the messages for each thread.\n\n### Relevant Logs\n\n_No response_\n\n### Code to reproduce the bug\n\n```python\nfor thread in r.subreddit(\"all\").modmail.conversations(limit=25):\r\n    if len(thread.messages) != thread.num_messages:\r\n        print(f\"unexpected result: {thread.id} num_messages={thread.num_messages} length={len(thread.messages)}\")\n```\n\n\n### My code example does not include the `Reddit()` initialization to prevent credential leakage.\n\nYes\n\n### This code has previously worked as intended.\n\nYes\n\n### Operating System/Environment\n\nLinux\n\n### Python Version\n\n3.7.3\n\n### PRAW Version\n\n7.6.0\n\n### Prawcore Version\n\n2.2.0\n\n### Anything else?\n\n_No response_\n", "before_files": [{"content": "\"\"\"Provides the Objector class.\"\"\"\nfrom datetime import datetime\nfrom json import loads\nfrom typing import TYPE_CHECKING, Any, Dict, List, Optional, Union\n\nfrom .exceptions import ClientException, RedditAPIException\nfrom .models.reddit.base import RedditBase\nfrom .util import snake_case_keys\n\nif TYPE_CHECKING:  # pragma: no cover\n    import praw\n\n\nclass Objector:\n    \"\"\"The objector builds :class:`.RedditBase` objects.\"\"\"\n\n    @classmethod\n    def parse_error(\n        cls, data: Union[List[Any], Dict[str, Dict[str, str]]]\n    ) -> Optional[RedditAPIException]:\n        \"\"\"Convert JSON response into an error object.\n\n        :param data: The dict to be converted.\n\n        :returns: An instance of :class:`.RedditAPIException`, or ``None`` if ``data``\n            doesn't fit this model.\n\n        \"\"\"\n        if isinstance(data, list):\n            # Fetching a Submission returns a list (of two items). Although it's handled\n            # manually in `Submission._fetch()`, assume it's a possibility here.\n            return None\n\n        errors = data.get(\"json\", {}).get(\"errors\")\n        if errors is None:\n            return None\n        if len(errors) < 1:\n            # See `Collection._fetch()`.\n            raise ClientException(\"successful error response\", data)\n        return RedditAPIException(errors)\n\n    @classmethod\n    def check_error(cls, data: Union[List[Any], Dict[str, Dict[str, str]]]):\n        \"\"\"Raise an error if the argument resolves to an error object.\"\"\"\n        error = cls.parse_error(data)\n        if error:\n            raise error\n\n    def __init__(self, reddit: \"praw.Reddit\", parsers: Optional[Dict[str, Any]] = None):\n        \"\"\"Initialize an :class:`.Objector` instance.\n\n        :param reddit: An instance of :class:`.Reddit`.\n\n        \"\"\"\n        self.parsers = {} if parsers is None else parsers\n        self._reddit = reddit\n\n    def _objectify_dict(self, data):\n        \"\"\"Create :class:`.RedditBase` objects from dicts.\n\n        :param data: The structured data, assumed to be a dict.\n\n        :returns: An instance of :class:`.RedditBase`.\n\n        \"\"\"\n        if {\"messages\", \"modActions\"}.issubset(data) and {\n            \"conversations\",\n            \"conversation\",\n        }.intersection(data):\n            data.update(\n                data.pop(\"conversation\")\n                if \"conversation\" in data\n                else data.pop(\"conversations\")\n            )\n            parser: \"praw.models.ModmailConversation\" = self.parsers[\n                \"ModmailConversation\"\n            ]\n            parser._convert_conversation_objects(data, self._reddit)\n        elif {\"messages\", \"modActions\"}.issubset(data):\n            parser = self.parsers[\"ModmailConversation\"]\n        elif {\"conversationIds\", \"conversations\", \"messages\"}.issubset(data):\n            conversations = []\n            temp_data = {\n                thing_type: data.pop(thing_type, {})\n                for thing_type in [\"messages\", \"modActions\"]\n            }\n            for conversation_id in data[\"conversationIds\"]:\n                conversation_data = {\n                    **data[\"conversations\"][conversation_id],\n                    \"messages\": [],\n                    \"modActions\": [],\n                }\n                for object_id in conversation_data[\"objIds\"]:\n                    conversation_data[object_id[\"key\"]].append(\n                        self._objectify_dict(\n                            temp_data[object_id[\"key\"]].pop(object_id[\"id\"])\n                        )\n                    )\n                conversations.append(conversation_data)\n            data[\"conversations\"] = conversations\n            del temp_data\n            data = snake_case_keys(data)\n            parser = self.parsers[\"ModmailConversations-list\"]\n        elif {\"actionTypeId\", \"author\", \"date\"}.issubset(data):\n            # Modmail mod action\n            data = snake_case_keys(data)\n            parser = self.parsers[\"ModmailAction\"]\n        elif {\"bodyMarkdown\", \"isInternal\"}.issubset(data):\n            # Modmail message\n            data = snake_case_keys(data)\n            parser = self.parsers[\"ModmailMessage\"]\n        elif {\"kind\", \"short_name\", \"violation_reason\"}.issubset(data):\n            # This is a Rule\n            parser = self.parsers[\"rule\"]\n        elif {\"isAdmin\", \"isDeleted\"}.issubset(data):\n            # Modmail author\n            data = snake_case_keys(data)\n            # Prevent clobbering base-36 id\n            del data[\"id\"]\n            data[\"is_subreddit_mod\"] = data.pop(\"is_mod\")\n            parser = self.parsers[self._reddit.config.kinds[\"redditor\"]]\n        elif {\"banStatus\", \"muteStatus\", \"recentComments\"}.issubset(data):\n            # Modmail user\n            data = snake_case_keys(data)\n            data[\"created_string\"] = data.pop(\"created\")\n            parser = self.parsers[self._reddit.config.kinds[\"redditor\"]]\n        elif {\"displayName\", \"id\", \"type\"}.issubset(data):\n            # Modmail subreddit\n            data = snake_case_keys(data)\n            parser = self.parsers[self._reddit.config.kinds[data[\"type\"]]]\n        elif {\"date\", \"id\", \"name\"}.issubset(data) or {\n            \"id\",\n            \"name\",\n            \"permissions\",\n        }.issubset(data):\n            parser = self.parsers[self._reddit.config.kinds[\"redditor\"]]\n        elif {\"text\", \"url\"}.issubset(data):\n            if \"color\" in data or \"linkUrl\" in data:\n                parser = self.parsers[\"Button\"]\n            else:\n                parser = self.parsers[\"MenuLink\"]\n        elif {\"children\", \"text\"}.issubset(data):\n            parser = self.parsers[\"Submenu\"]\n        elif {\"height\", \"url\", \"width\"}.issubset(data):\n            parser = self.parsers[\"Image\"]\n        elif {\"isSubscribed\", \"name\", \"subscribers\"}.issubset(data):\n            # discards icon and subscribed information\n            return self._reddit.subreddit(data[\"name\"])\n        elif {\"authorFlairType\", \"name\"}.issubset(data):\n            # discards flair information\n            return self._reddit.redditor(data[\"name\"])\n        elif {\"parent_id\"}.issubset(data):\n            parser = self.parsers[self._reddit.config.kinds[\"comment\"]]\n        elif \"collection_id\" in data.keys():\n            parser = self.parsers[\"Collection\"]\n        elif {\"moderators\", \"moderatorIds\", \"allUsersLoaded\", \"subredditId\"}.issubset(\n            data\n        ):\n            data = snake_case_keys(data)\n            moderators = []\n            for mod_id in data[\"moderator_ids\"]:\n                mod = snake_case_keys(data[\"moderators\"][mod_id])\n                mod[\"mod_permissions\"] = list(mod[\"mod_permissions\"].keys())\n                moderators.append(mod)\n            data[\"moderators\"] = moderators\n            parser = self.parsers[\"moderator-list\"]\n        elif \"username\" in data.keys():\n            data[\"name\"] = data.pop(\"username\")\n            parser = self.parsers[self._reddit.config.kinds[\"redditor\"]]\n        elif {\"mod_permissions\", \"name\", \"sr\", \"subscribers\"}.issubset(data):\n            data[\"display_name\"] = data[\"sr\"]\n            parser = self.parsers[self._reddit.config.kinds[\"subreddit\"]]\n        elif {\"drafts\", \"subreddits\"}.issubset(data):  # Draft list\n            subreddit_parser = self.parsers[self._reddit.config.kinds[\"subreddit\"]]\n            user_subreddit_parser = self.parsers[\"UserSubreddit\"]\n            subreddits = {\n                subreddit[\"name\"]: user_subreddit_parser.parse(subreddit, self._reddit)\n                if subreddit[\"display_name_prefixed\"].startswith(\"u/\")\n                else subreddit_parser.parse(subreddit, self._reddit)\n                for subreddit in data.pop(\"subreddits\")\n            }\n            for draft in data[\"drafts\"]:\n                if draft[\"subreddit\"]:\n                    draft[\"subreddit\"] = subreddits[draft[\"subreddit\"]]\n                draft[\"modified\"] = datetime.fromtimestamp(\n                    draft[\"modified\"] / 1000\n                ).astimezone()\n            parser = self.parsers[\"DraftList\"]\n        elif {\"mod_action_data\", \"user_note_data\"}.issubset(data):\n            data[\"moderator\"] = self._reddit.redditor(data[\"operator\"])\n            data[\"subreddit\"] = self._reddit.subreddit(data[\"subreddit\"])\n            data[\"user\"] = self._reddit.redditor(data[\"user\"])\n            # move these sub dict values into the main dict for simplicity\n            data.update(data[\"mod_action_data\"])\n            del data[\"mod_action_data\"]\n            data.update(data[\"user_note_data\"])\n            del data[\"user_note_data\"]\n            parser = self.parsers[\"mod_note\"]\n        elif (\n            \"created\" in data\n            and isinstance(data[\"created\"], dict)\n            and {\"mod_action_data\", \"user_note_data\"}.issubset(data[\"created\"])\n        ):\n            data = data[\"created\"]\n            return self._objectify_dict(data)\n        else:\n            if \"user\" in data:\n                parser = self.parsers[self._reddit.config.kinds[\"redditor\"]]\n                data[\"user\"] = parser.parse({\"name\": data[\"user\"]}, self._reddit)\n            return data\n        return parser.parse(data, self._reddit)\n\n    def objectify(\n        self, data: Optional[Union[Dict[str, Any], List[Any], bool]]\n    ) -> Optional[Union[RedditBase, Dict[str, Any], List[Any], bool]]:\n        \"\"\"Create :class:`.RedditBase` objects from data.\n\n        :param data: The structured data.\n\n        :returns: An instance of :class:`.RedditBase`, or ``None`` if given ``data`` is\n            ``None``.\n\n        \"\"\"\n        # pylint: disable=too-many-return-statements\n        if data is None:  # 204 no content\n            return None\n        if isinstance(data, list):\n            return [self.objectify(item) for item in data]\n        if isinstance(data, bool):  # Reddit.username_available\n            return data\n        if \"json\" in data and \"errors\" in data[\"json\"]:\n            errors = data[\"json\"][\"errors\"]\n            if len(errors) > 0:\n                raise RedditAPIException(errors)\n        if \"kind\" in data and (\n            \"shortName\" in data or data[\"kind\"] in (\"menu\", \"moderators\")\n        ):\n            # This is a widget\n            parser = self.parsers.get(data[\"kind\"], self.parsers[\"widget\"])\n            return parser.parse(data, self._reddit)\n        if {\"kind\", \"data\"}.issubset(data) and data[\"kind\"] in self.parsers:\n            parser = self.parsers[data[\"kind\"]]\n            if data[\"kind\"] == \"ModeratedList\":\n                return parser.parse(data, self._reddit)\n            else:\n                return parser.parse(data[\"data\"], self._reddit)\n        if \"json\" in data and \"data\" in data[\"json\"]:\n            if \"websocket_url\" in data[\"json\"][\"data\"]:\n                return data\n            if \"things\" in data[\"json\"][\"data\"]:  # Submission.reply\n                return self.objectify(data[\"json\"][\"data\"][\"things\"])\n            if \"rules\" in data[\"json\"][\"data\"]:\n                return self.objectify(loads(data[\"json\"][\"data\"][\"rules\"]))\n            if \"drafts_count\" in data[\"json\"][\"data\"] and all(\n                [key not in data[\"json\"][\"data\"] for key in [\"name\", \"url\"]]\n            ):  # Draft\n                data[\"json\"][\"data\"].pop(\"drafts_count\")\n                return self.parsers[\"Draft\"].parse(data[\"json\"][\"data\"], self._reddit)\n            if \"url\" in data[\"json\"][\"data\"]:  # Subreddit.submit\n                # The URL is the URL to the submission, so it's removed.\n                del data[\"json\"][\"data\"][\"url\"]\n                parser = self.parsers[self._reddit.config.kinds[\"submission\"]]\n                if data[\"json\"][\"data\"][\"id\"].startswith(\n                    f\"{self._reddit.config.kinds['submission']}_\"\n                ):\n                    # With polls, Reddit returns a fullname but calls it an \"id\". This\n                    # fixes this by coercing the fullname into an id.\n                    data[\"json\"][\"data\"][\"id\"] = data[\"json\"][\"data\"][\"id\"].split(\n                        \"_\", 1\n                    )[1]\n            else:\n                parser = self.parsers[\"LiveUpdateEvent\"]\n            return parser.parse(data[\"json\"][\"data\"], self._reddit)\n        if {\"is_public_link\", \"title\", \"body\"}.issubset(data):\n            parser = self.parsers[\"Draft\"]\n            return parser.parse(data, self._reddit)\n        if \"rules\" in data:\n            return self.objectify(data[\"rules\"])\n        elif isinstance(data, dict):\n            return self._objectify_dict(data)\n\n        return data\n", "path": "praw/objector.py"}], "after_files": [{"content": "\"\"\"Provides the Objector class.\"\"\"\nfrom datetime import datetime\nfrom json import loads\nfrom typing import TYPE_CHECKING, Any, Dict, List, Optional, Union\n\nfrom .exceptions import ClientException, RedditAPIException\nfrom .models.reddit.base import RedditBase\nfrom .util import snake_case_keys\n\nif TYPE_CHECKING:  # pragma: no cover\n    import praw\n\n\nclass Objector:\n    \"\"\"The objector builds :class:`.RedditBase` objects.\"\"\"\n\n    @classmethod\n    def parse_error(\n        cls, data: Union[List[Any], Dict[str, Dict[str, str]]]\n    ) -> Optional[RedditAPIException]:\n        \"\"\"Convert JSON response into an error object.\n\n        :param data: The dict to be converted.\n\n        :returns: An instance of :class:`.RedditAPIException`, or ``None`` if ``data``\n            doesn't fit this model.\n\n        \"\"\"\n        if isinstance(data, list):\n            # Fetching a Submission returns a list (of two items). Although it's handled\n            # manually in `Submission._fetch()`, assume it's a possibility here.\n            return None\n\n        errors = data.get(\"json\", {}).get(\"errors\")\n        if errors is None:\n            return None\n        if len(errors) < 1:\n            # See `Collection._fetch()`.\n            raise ClientException(\"successful error response\", data)\n        return RedditAPIException(errors)\n\n    @classmethod\n    def check_error(cls, data: Union[List[Any], Dict[str, Dict[str, str]]]):\n        \"\"\"Raise an error if the argument resolves to an error object.\"\"\"\n        error = cls.parse_error(data)\n        if error:\n            raise error\n\n    def __init__(self, reddit: \"praw.Reddit\", parsers: Optional[Dict[str, Any]] = None):\n        \"\"\"Initialize an :class:`.Objector` instance.\n\n        :param reddit: An instance of :class:`.Reddit`.\n\n        \"\"\"\n        self.parsers = {} if parsers is None else parsers\n        self._reddit = reddit\n\n    def _objectify_dict(self, data):\n        \"\"\"Create :class:`.RedditBase` objects from dicts.\n\n        :param data: The structured data, assumed to be a dict.\n\n        :returns: An instance of :class:`.RedditBase`.\n\n        \"\"\"\n        if {\"messages\", \"modActions\"}.issubset(data) and {\n            \"conversations\",\n            \"conversation\",\n        }.intersection(data):\n            data.update(\n                data.pop(\"conversation\")\n                if \"conversation\" in data\n                else data.pop(\"conversations\")\n            )\n            parser = self.parsers[\"ModmailConversation\"]\n            parser._convert_conversation_objects(data, self._reddit)\n        elif {\"messages\", \"modActions\"}.issubset(data) or {\n            \"legacyFirstMessageId\",\n            \"state\",\n        }.issubset(data):\n            parser = self.parsers[\"ModmailConversation\"]\n        elif {\"conversationIds\", \"conversations\", \"messages\"}.issubset(data):\n            data[\"conversations\"] = [\n                data[\"conversations\"][conversation_id]\n                for conversation_id in data[\"conversationIds\"]\n            ]\n            data = snake_case_keys(data)\n            parser = self.parsers[\"ModmailConversations-list\"]\n        elif {\"actionTypeId\", \"author\", \"date\"}.issubset(data):\n            # Modmail mod action\n            data = snake_case_keys(data)\n            parser = self.parsers[\"ModmailAction\"]\n        elif {\"bodyMarkdown\", \"isInternal\"}.issubset(data):\n            # Modmail message\n            data = snake_case_keys(data)\n            parser = self.parsers[\"ModmailMessage\"]\n        elif {\"kind\", \"short_name\", \"violation_reason\"}.issubset(data):\n            # This is a Rule\n            parser = self.parsers[\"rule\"]\n        elif {\"isAdmin\", \"isDeleted\"}.issubset(data):\n            # Modmail author\n            data = snake_case_keys(data)\n            # Prevent clobbering base-36 id\n            del data[\"id\"]\n            data[\"is_subreddit_mod\"] = data.pop(\"is_mod\")\n            parser = self.parsers[self._reddit.config.kinds[\"redditor\"]]\n        elif {\"banStatus\", \"muteStatus\", \"recentComments\"}.issubset(data):\n            # Modmail user\n            data = snake_case_keys(data)\n            data[\"created_string\"] = data.pop(\"created\")\n            parser = self.parsers[self._reddit.config.kinds[\"redditor\"]]\n        elif {\"displayName\", \"id\", \"type\"}.issubset(data):\n            # Modmail subreddit\n            data = snake_case_keys(data)\n            parser = self.parsers[self._reddit.config.kinds[data[\"type\"]]]\n        elif {\"date\", \"id\", \"name\"}.issubset(data) or {\n            \"id\",\n            \"name\",\n            \"permissions\",\n        }.issubset(data):\n            parser = self.parsers[self._reddit.config.kinds[\"redditor\"]]\n        elif {\"text\", \"url\"}.issubset(data):\n            if \"color\" in data or \"linkUrl\" in data:\n                parser = self.parsers[\"Button\"]\n            else:\n                parser = self.parsers[\"MenuLink\"]\n        elif {\"children\", \"text\"}.issubset(data):\n            parser = self.parsers[\"Submenu\"]\n        elif {\"height\", \"url\", \"width\"}.issubset(data):\n            parser = self.parsers[\"Image\"]\n        elif {\"isSubscribed\", \"name\", \"subscribers\"}.issubset(data):\n            # discards icon and subscribed information\n            return self._reddit.subreddit(data[\"name\"])\n        elif {\"authorFlairType\", \"name\"}.issubset(data):\n            # discards flair information\n            return self._reddit.redditor(data[\"name\"])\n        elif {\"parent_id\"}.issubset(data):\n            parser = self.parsers[self._reddit.config.kinds[\"comment\"]]\n        elif \"collection_id\" in data.keys():\n            parser = self.parsers[\"Collection\"]\n        elif {\"moderators\", \"moderatorIds\", \"allUsersLoaded\", \"subredditId\"}.issubset(\n            data\n        ):\n            data = snake_case_keys(data)\n            moderators = []\n            for mod_id in data[\"moderator_ids\"]:\n                mod = snake_case_keys(data[\"moderators\"][mod_id])\n                mod[\"mod_permissions\"] = list(mod[\"mod_permissions\"].keys())\n                moderators.append(mod)\n            data[\"moderators\"] = moderators\n            parser = self.parsers[\"moderator-list\"]\n        elif \"username\" in data.keys():\n            data[\"name\"] = data.pop(\"username\")\n            parser = self.parsers[self._reddit.config.kinds[\"redditor\"]]\n        elif {\"mod_permissions\", \"name\", \"sr\", \"subscribers\"}.issubset(data):\n            data[\"display_name\"] = data[\"sr\"]\n            parser = self.parsers[self._reddit.config.kinds[\"subreddit\"]]\n        elif {\"drafts\", \"subreddits\"}.issubset(data):  # Draft list\n            subreddit_parser = self.parsers[self._reddit.config.kinds[\"subreddit\"]]\n            user_subreddit_parser = self.parsers[\"UserSubreddit\"]\n            subreddits = {\n                subreddit[\"name\"]: user_subreddit_parser.parse(subreddit, self._reddit)\n                if subreddit[\"display_name_prefixed\"].startswith(\"u/\")\n                else subreddit_parser.parse(subreddit, self._reddit)\n                for subreddit in data.pop(\"subreddits\")\n            }\n            for draft in data[\"drafts\"]:\n                if draft[\"subreddit\"]:\n                    draft[\"subreddit\"] = subreddits[draft[\"subreddit\"]]\n                draft[\"modified\"] = datetime.fromtimestamp(\n                    draft[\"modified\"] / 1000\n                ).astimezone()\n            parser = self.parsers[\"DraftList\"]\n        elif {\"mod_action_data\", \"user_note_data\"}.issubset(data):\n            data[\"moderator\"] = self._reddit.redditor(data[\"operator\"])\n            data[\"subreddit\"] = self._reddit.subreddit(data[\"subreddit\"])\n            data[\"user\"] = self._reddit.redditor(data[\"user\"])\n            # move these sub dict values into the main dict for simplicity\n            data.update(data[\"mod_action_data\"])\n            del data[\"mod_action_data\"]\n            data.update(data[\"user_note_data\"])\n            del data[\"user_note_data\"]\n            parser = self.parsers[\"mod_note\"]\n        elif (\n            \"created\" in data\n            and isinstance(data[\"created\"], dict)\n            and {\"mod_action_data\", \"user_note_data\"}.issubset(data[\"created\"])\n        ):\n            data = data[\"created\"]\n            return self._objectify_dict(data)\n        else:\n            if \"user\" in data:\n                parser = self.parsers[self._reddit.config.kinds[\"redditor\"]]\n                data[\"user\"] = parser.parse({\"name\": data[\"user\"]}, self._reddit)\n            return data\n        return parser.parse(data, self._reddit)\n\n    def objectify(\n        self, data: Optional[Union[Dict[str, Any], List[Any], bool]]\n    ) -> Optional[Union[RedditBase, Dict[str, Any], List[Any], bool]]:\n        \"\"\"Create :class:`.RedditBase` objects from data.\n\n        :param data: The structured data.\n\n        :returns: An instance of :class:`.RedditBase`, or ``None`` if given ``data`` is\n            ``None``.\n\n        \"\"\"\n        # pylint: disable=too-many-return-statements\n        if data is None:  # 204 no content\n            return None\n        if isinstance(data, list):\n            return [self.objectify(item) for item in data]\n        if isinstance(data, bool):  # Reddit.username_available\n            return data\n        if \"json\" in data and \"errors\" in data[\"json\"]:\n            errors = data[\"json\"][\"errors\"]\n            if len(errors) > 0:\n                raise RedditAPIException(errors)\n        if \"kind\" in data and (\n            \"shortName\" in data or data[\"kind\"] in (\"menu\", \"moderators\")\n        ):\n            # This is a widget\n            parser = self.parsers.get(data[\"kind\"], self.parsers[\"widget\"])\n            return parser.parse(data, self._reddit)\n        if {\"kind\", \"data\"}.issubset(data) and data[\"kind\"] in self.parsers:\n            parser = self.parsers[data[\"kind\"]]\n            if data[\"kind\"] == \"ModeratedList\":\n                return parser.parse(data, self._reddit)\n            else:\n                return parser.parse(data[\"data\"], self._reddit)\n        if \"json\" in data and \"data\" in data[\"json\"]:\n            if \"websocket_url\" in data[\"json\"][\"data\"]:\n                return data\n            if \"things\" in data[\"json\"][\"data\"]:  # Submission.reply\n                return self.objectify(data[\"json\"][\"data\"][\"things\"])\n            if \"rules\" in data[\"json\"][\"data\"]:\n                return self.objectify(loads(data[\"json\"][\"data\"][\"rules\"]))\n            if \"drafts_count\" in data[\"json\"][\"data\"] and all(\n                [key not in data[\"json\"][\"data\"] for key in [\"name\", \"url\"]]\n            ):  # Draft\n                data[\"json\"][\"data\"].pop(\"drafts_count\")\n                return self.parsers[\"Draft\"].parse(data[\"json\"][\"data\"], self._reddit)\n            if \"url\" in data[\"json\"][\"data\"]:  # Subreddit.submit\n                # The URL is the URL to the submission, so it's removed.\n                del data[\"json\"][\"data\"][\"url\"]\n                parser = self.parsers[self._reddit.config.kinds[\"submission\"]]\n                if data[\"json\"][\"data\"][\"id\"].startswith(\n                    f\"{self._reddit.config.kinds['submission']}_\"\n                ):\n                    # With polls, Reddit returns a fullname but calls it an \"id\". This\n                    # fixes this by coercing the fullname into an id.\n                    data[\"json\"][\"data\"][\"id\"] = data[\"json\"][\"data\"][\"id\"].split(\n                        \"_\", 1\n                    )[1]\n            else:\n                parser = self.parsers[\"LiveUpdateEvent\"]\n            return parser.parse(data[\"json\"][\"data\"], self._reddit)\n        if {\"is_public_link\", \"title\", \"body\"}.issubset(data):\n            parser = self.parsers[\"Draft\"]\n            return parser.parse(data, self._reddit)\n        if \"rules\" in data:\n            return self.objectify(data[\"rules\"])\n        elif isinstance(data, dict):\n            return self._objectify_dict(data)\n\n        return data\n", "path": "praw/objector.py"}]}