Repository: praw-dev/praw

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_


Discussion:
Isn't that the same as the UI though? If you load the modmail view, it shows the most recent message in each thread and you have to click on one to get the whole thing. That's just what the API returns.
Previous versions of PRAW intentionally left `messages` unset when building conversation objects via the `subreddit.modmail.conversations()` API (`_fetched` would be False). When attempting to access `messages`, the messages list would then be populated by fetching the specific conversation (then `_fetched` would be True). That was the behavior for PRAW versions 7.3.0, 7.4.0, and 7.5.0.

What is happening now is the `messages` list is populated with an incomplete messages list (with `_fetched` False). Because `messages` is set, the rest of the messages list is never populated when accessing the messages list.

`messages` should be left unset when initially building modmail conversation objects through the `subreddit.modmail.conversations()` API. (A small optimization would be to set `messages` when `num_messages` is 1 since it's possible to get one message essentially for free.)

To illustrate this another way, here is the previous behavior with PRAW 7.5.0:

    >>> for thread in r.subreddit("all").modmail.conversations(limit=1):
    ...   print(thread.user)
    ...   print(len(thread.messages))
    ... 
    SomeUsernameHere
    5
    >>> for thread in r.subreddit("all").modmail.conversations(limit=1):
    ...   print(len(thread.messages))
    ...   print(thread.user)
    ... 
    5
    SomeUsernameHere

compared to PRAW 7.6.0:

    >>> for thread in r.subreddit("all").modmail.conversations(limit=1):
    ...   print(thread.user)
    ...   print(len(thread.messages))
    ... 
    SomeUsernameHere
    5
    >>> for thread in r.subreddit("all").modmail.conversations(limit=1):
    ...   print(len(thread.messages))
    ...   print(thread.user)
    ... 
    1
    SomeUsernameHere

That is unexpected and unlike pretty much any other PRAW object.

P.S. The same issue exists for the `obj_ids` list. It should also be left unset until the conversation is fetched.
While this isn't a bug with how PRAW is parsing the data from reddit, it is however an issue there is not a way for PRAW to automatically refetch the individual conversation if `len(convo.messages) != convo.num_messages)`. This bug was introduced because of this change where the messages is constructed from the `objIds` in the conversation data:
https://github.com/praw-dev/praw/blob/af494cfc37d352ea97a0cb4d1b589b3a3ea42ef6/praw/objector.py#L87-L99

I will work on a fix to have PRAW refetch when there is a mismatch.
It looks like `obj_ids` has a similar issue. Unfortunately, there's no equivalent to the `num_messages` attribute storing the count for `obj_ids`.

    >>> for thread in r.subreddit("BotDefense").modmail.conversations(limit=100, state="archived"):
    ...   if thread.id == "11uub4":
    ...     print(thread.id, thread.num_messages, len(thread.obj_ids), len(thread.messages))
    ... 
    11uub4 1 1 1
    >>> thread = r.subreddit("BotDefense").modmail("11uub4")
    >>> print(thread.id, thread.num_messages, len(thread.obj_ids), len(thread.messages))
    11uub4 1 2 1


Resolving patch:
diff --git a/praw/objector.py b/praw/objector.py
--- a/praw/objector.py
+++ b/praw/objector.py
@@ -72,33 +72,18 @@ def _objectify_dict(self, data):
                 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):


Test patch:
diff --git a/tests/unit/models/reddit/test_modmail.py b/tests/unit/models/reddit/test_modmail.py
--- a/tests/unit/models/reddit/test_modmail.py
+++ b/tests/unit/models/reddit/test_modmail.py
@@ -18,6 +18,7 @@ def test_construct_failure(self):
 
     def test_str(self):
         conversation = ModmailConversation(self.reddit, _data={"id": "ik72"})
+        assert str(conversation) == "ik72"
 
         conversation = ModmailConversation(self.reddit, "ik72")
         assert str(conversation) == "ik72"