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 = self.parsers["ModmailConversation"]
76             parser._convert_conversation_objects(data, self._reddit)
77         elif {"messages", "modActions"}.issubset(data) or {
78             "legacyFirstMessageId",
79             "state",
80         }.issubset(data):
81             parser = self.parsers["ModmailConversation"]
82         elif {"conversationIds", "conversations", "messages"}.issubset(data):
83             data["conversations"] = [
84                 data["conversations"][conversation_id]
85                 for conversation_id in data["conversationIds"]
86             ]
87             data = snake_case_keys(data)
88             parser = self.parsers["ModmailConversations-list"]
89         elif {"actionTypeId", "author", "date"}.issubset(data):
90             # Modmail mod action
91             data = snake_case_keys(data)
92             parser = self.parsers["ModmailAction"]
93         elif {"bodyMarkdown", "isInternal"}.issubset(data):
94             # Modmail message
95             data = snake_case_keys(data)
96             parser = self.parsers["ModmailMessage"]
97         elif {"kind", "short_name", "violation_reason"}.issubset(data):
98             # This is a Rule
99             parser = self.parsers["rule"]
100         elif {"isAdmin", "isDeleted"}.issubset(data):
101             # Modmail author
102             data = snake_case_keys(data)
103             # Prevent clobbering base-36 id
104             del data["id"]
105             data["is_subreddit_mod"] = data.pop("is_mod")
106             parser = self.parsers[self._reddit.config.kinds["redditor"]]
107         elif {"banStatus", "muteStatus", "recentComments"}.issubset(data):
108             # Modmail user
109             data = snake_case_keys(data)
110             data["created_string"] = data.pop("created")
111             parser = self.parsers[self._reddit.config.kinds["redditor"]]
112         elif {"displayName", "id", "type"}.issubset(data):
113             # Modmail subreddit
114             data = snake_case_keys(data)
115             parser = self.parsers[self._reddit.config.kinds[data["type"]]]
116         elif {"date", "id", "name"}.issubset(data) or {
117             "id",
118             "name",
119             "permissions",
120         }.issubset(data):
121             parser = self.parsers[self._reddit.config.kinds["redditor"]]
122         elif {"text", "url"}.issubset(data):
123             if "color" in data or "linkUrl" in data:
124                 parser = self.parsers["Button"]
125             else:
126                 parser = self.parsers["MenuLink"]
127         elif {"children", "text"}.issubset(data):
128             parser = self.parsers["Submenu"]
129         elif {"height", "url", "width"}.issubset(data):
130             parser = self.parsers["Image"]
131         elif {"isSubscribed", "name", "subscribers"}.issubset(data):
132             # discards icon and subscribed information
133             return self._reddit.subreddit(data["name"])
134         elif {"authorFlairType", "name"}.issubset(data):
135             # discards flair information
136             return self._reddit.redditor(data["name"])
137         elif {"parent_id"}.issubset(data):
138             parser = self.parsers[self._reddit.config.kinds["comment"]]
139         elif "collection_id" in data.keys():
140             parser = self.parsers["Collection"]
141         elif {"moderators", "moderatorIds", "allUsersLoaded", "subredditId"}.issubset(
142             data
143         ):
144             data = snake_case_keys(data)
145             moderators = []
146             for mod_id in data["moderator_ids"]:
147                 mod = snake_case_keys(data["moderators"][mod_id])
148                 mod["mod_permissions"] = list(mod["mod_permissions"].keys())
149                 moderators.append(mod)
150             data["moderators"] = moderators
151             parser = self.parsers["moderator-list"]
152         elif "username" in data.keys():
153             data["name"] = data.pop("username")
154             parser = self.parsers[self._reddit.config.kinds["redditor"]]
155         elif {"mod_permissions", "name", "sr", "subscribers"}.issubset(data):
156             data["display_name"] = data["sr"]
157             parser = self.parsers[self._reddit.config.kinds["subreddit"]]
158         elif {"drafts", "subreddits"}.issubset(data):  # Draft list
159             subreddit_parser = self.parsers[self._reddit.config.kinds["subreddit"]]
160             user_subreddit_parser = self.parsers["UserSubreddit"]
161             subreddits = {
162                 subreddit["name"]: user_subreddit_parser.parse(subreddit, self._reddit)
163                 if subreddit["display_name_prefixed"].startswith("u/")
164                 else subreddit_parser.parse(subreddit, self._reddit)
165                 for subreddit in data.pop("subreddits")
166             }
167             for draft in data["drafts"]:
168                 if draft["subreddit"]:
169                     draft["subreddit"] = subreddits[draft["subreddit"]]
170                 draft["modified"] = datetime.fromtimestamp(
171                     draft["modified"] / 1000
172                 ).astimezone()
173             parser = self.parsers["DraftList"]
174         elif {"mod_action_data", "user_note_data"}.issubset(data):
175             data["moderator"] = self._reddit.redditor(data["operator"])
176             data["subreddit"] = self._reddit.subreddit(data["subreddit"])
177             data["user"] = self._reddit.redditor(data["user"])
178             # move these sub dict values into the main dict for simplicity
179             data.update(data["mod_action_data"])
180             del data["mod_action_data"]
181             data.update(data["user_note_data"])
182             del data["user_note_data"]
183             parser = self.parsers["mod_note"]
184         elif (
185             "created" in data
186             and isinstance(data["created"], dict)
187             and {"mod_action_data", "user_note_data"}.issubset(data["created"])
188         ):
189             data = data["created"]
190             return self._objectify_dict(data)
191         else:
192             if "user" in data:
193                 parser = self.parsers[self._reddit.config.kinds["redditor"]]
194                 data["user"] = parser.parse({"name": data["user"]}, self._reddit)
195             return data
196         return parser.parse(data, self._reddit)
197 
198     def objectify(
199         self, data: Optional[Union[Dict[str, Any], List[Any], bool]]
200     ) -> Optional[Union[RedditBase, Dict[str, Any], List[Any], bool]]:
201         """Create :class:`.RedditBase` objects from data.
202 
203         :param data: The structured data.
204 
205         :returns: An instance of :class:`.RedditBase`, or ``None`` if given ``data`` is
206             ``None``.
207 
208         """
209         # pylint: disable=too-many-return-statements
210         if data is None:  # 204 no content
211             return None
212         if isinstance(data, list):
213             return [self.objectify(item) for item in data]
214         if isinstance(data, bool):  # Reddit.username_available
215             return data
216         if "json" in data and "errors" in data["json"]:
217             errors = data["json"]["errors"]
218             if len(errors) > 0:
219                 raise RedditAPIException(errors)
220         if "kind" in data and (
221             "shortName" in data or data["kind"] in ("menu", "moderators")
222         ):
223             # This is a widget
224             parser = self.parsers.get(data["kind"], self.parsers["widget"])
225             return parser.parse(data, self._reddit)
226         if {"kind", "data"}.issubset(data) and data["kind"] in self.parsers:
227             parser = self.parsers[data["kind"]]
228             if data["kind"] == "ModeratedList":
229                 return parser.parse(data, self._reddit)
230             else:
231                 return parser.parse(data["data"], self._reddit)
232         if "json" in data and "data" in data["json"]:
233             if "websocket_url" in data["json"]["data"]:
234                 return data
235             if "things" in data["json"]["data"]:  # Submission.reply
236                 return self.objectify(data["json"]["data"]["things"])
237             if "rules" in data["json"]["data"]:
238                 return self.objectify(loads(data["json"]["data"]["rules"]))
239             if "drafts_count" in data["json"]["data"] and all(
240                 [key not in data["json"]["data"] for key in ["name", "url"]]
241             ):  # Draft
242                 data["json"]["data"].pop("drafts_count")
243                 return self.parsers["Draft"].parse(data["json"]["data"], self._reddit)
244             if "url" in data["json"]["data"]:  # Subreddit.submit
245                 # The URL is the URL to the submission, so it's removed.
246                 del data["json"]["data"]["url"]
247                 parser = self.parsers[self._reddit.config.kinds["submission"]]
248                 if data["json"]["data"]["id"].startswith(
249                     f"{self._reddit.config.kinds['submission']}_"
250                 ):
251                     # With polls, Reddit returns a fullname but calls it an "id". This
252                     # fixes this by coercing the fullname into an id.
253                     data["json"]["data"]["id"] = data["json"]["data"]["id"].split(
254                         "_", 1
255                     )[1]
256             else:
257                 parser = self.parsers["LiveUpdateEvent"]
258             return parser.parse(data["json"]["data"], self._reddit)
259         if {"is_public_link", "title", "body"}.issubset(data):
260             parser = self.parsers["Draft"]
261             return parser.parse(data, self._reddit)
262         if "rules" in data:
263             return self.objectify(data["rules"])
264         elif isinstance(data, dict):
265             return self._objectify_dict(data)
266 
267         return data
268
```
--- 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
@@ -67,6 +67,7 @@
             "conversations",
             "conversation",
         }.intersection(data):
+            # fetched conversation
             data.update(
                 data.pop("conversation")
                 if "conversation" in data
@@ -78,12 +79,24 @@
             "legacyFirstMessageId",
             "state",
         }.issubset(data):
+            # not fetched conversation i.e., from conversations()
+            del data["objIds"]  # delete objIds since it could be missing data
             parser = self.parsers["ModmailConversation"]
         elif {"conversationIds", "conversations", "messages"}.issubset(data):
-            data["conversations"] = [
-                data["conversations"][conversation_id]
-                for conversation_id in data["conversationIds"]
-            ]
+            # modmail conversations
+            conversations = []
+            for conversation_id in data["conversationIds"]:
+                conversation = data["conversations"][conversation_id]
+                # set if the numMessages is same as number of messages in objIds
+                if conversation["numMessages"] == len(
+                    [obj for obj in conversation["objIds"] if obj["key"] == "messages"]
+                ):
+                    conversation["messages"] = [
+                        self.objectify(data["messages"][obj_id["id"]])
+                        for obj_id in conversation["objIds"]
+                    ]
+                conversations.append(conversation)
+            data["conversations"] = conversations
             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@@ -67,6 +67,7 @@\n             \"conversations\",\n             \"conversation\",\n         }.intersection(data):\n+            # fetched conversation\n             data.update(\n                 data.pop(\"conversation\")\n                 if \"conversation\" in data\n@@ -78,12 +79,24 @@\n             \"legacyFirstMessageId\",\n             \"state\",\n         }.issubset(data):\n+            # not fetched conversation i.e., from conversations()\n+            del data[\"objIds\"]  # delete objIds since it could be missing 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+            # modmail conversations\n+            conversations = []\n+            for conversation_id in data[\"conversationIds\"]:\n+                conversation = data[\"conversations\"][conversation_id]\n+                # set if the numMessages is same as number of messages in objIds\n+                if conversation[\"numMessages\"] == len(\n+                    [obj for obj in conversation[\"objIds\"] if obj[\"key\"] == \"messages\"]\n+                ):\n+                    conversation[\"messages\"] = [\n+                        self.objectify(data[\"messages\"][obj_id[\"id\"]])\n+                        for obj_id in conversation[\"objIds\"]\n+                    ]\n+                conversations.append(conversation)\n+            data[\"conversations\"] = conversations\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 = 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"}], "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            # fetched conversation\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            # not fetched conversation i.e., from conversations()\n            del data[\"objIds\"]  # delete objIds since it could be missing data\n            parser = self.parsers[\"ModmailConversation\"]\n        elif {\"conversationIds\", \"conversations\", \"messages\"}.issubset(data):\n            # modmail conversations\n            conversations = []\n            for conversation_id in data[\"conversationIds\"]:\n                conversation = data[\"conversations\"][conversation_id]\n                # set if the numMessages is same as number of messages in objIds\n                if conversation[\"numMessages\"] == len(\n                    [obj for obj in conversation[\"objIds\"] if obj[\"key\"] == \"messages\"]\n                ):\n                    conversation[\"messages\"] = [\n                        self.objectify(data[\"messages\"][obj_id[\"id\"]])\n                        for obj_id in conversation[\"objIds\"]\n                    ]\n                conversations.append(conversation)\n            data[\"conversations\"] = conversations\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"}]}