Skip to content

Commit d05d150

Browse files
committed
feat: add peer user ID to DM conversation responses
- Introduced a new field `peer_user_id` in the API documentation and conversation responses for direct messages (DMs), allowing clients to identify the counterpart user in a conversation. - Updated the HTTP dispatch logic to include `peer_user_id` when handling DM conversations. - Enhanced unit tests to verify the presence of `peer_user_id` in DM responses and ensure it is omitted for group conversations.
1 parent 900861c commit d05d150

3 files changed

Lines changed: 73 additions & 1 deletion

File tree

API.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1190,6 +1190,7 @@ Each element of `conversations`:
11901190
| `type` | integer | `0` DM, `1` group, `2` channel |
11911191
| `created_by` | string | User id |
11921192
| `created_by_username` | string or null | Canonical username for `created_by` (non-disabled); `null` if not available |
1193+
| `peer_user_id` | string | Present only for DM (`type = 0`): counterpart user id relative to caller; for self-DM equals caller `user_id` |
11931194
| `created_at` | integer | Unix seconds (conversation created) |
11941195
| `membership_version` | integer | Bumps on membership changes |
11951196
| `last_activity_at` | integer or null | Unix seconds of the latest stored envelope in this conversation (`MAX(server_timestamp)`); `null` if there are no envelopes yet |
@@ -1204,6 +1205,7 @@ Each element of `conversations`:
12041205
"type": 0,
12051206
"created_by": "usr_alice",
12061207
"created_by_username": "alice",
1208+
"peer_user_id": "usr_bob",
12071209
"created_at": 1710000000,
12081210
"membership_version": 3,
12091211
"last_activity_at": 1710000123
@@ -1240,6 +1242,7 @@ Common fields:
12401242
| `type` | integer | `0` DM, `1` group, `2` channel |
12411243
| `created_by` | string | |
12421244
| `created_by_username` | string or null | Canonical username for `created_by` |
1245+
| `peer_user_id` | string | Present only for DM (`type = 0`): counterpart user id relative to caller; for self-DM equals caller `user_id` |
12431246
| `created_at` | integer | Unix seconds |
12441247
| `membership_version` | integer | |
12451248
| `my_role` | string | `"owner"`, `"admin"`, or `"member"` |
@@ -1251,7 +1254,8 @@ Additional fields for **channel** (`type == 2`) only:
12511254
| `title` | string | Channel title (may be empty) |
12521255
| `channel_post_policy` | string | e.g. `admins_only` or policy serialized by server |
12531256

1254-
**200 (DM or group)**`title` and `channel_post_policy` are **not** returned; only `my_role` among the extra fields below.
1257+
**200 (DM or group)**`title` and `channel_post_policy` are **not** returned; only `my_role` among the extra fields below.
1258+
For DM (`type = 0`) `peer_user_id` is included; for group (`type = 1`) it is omitted.
12551259

12561260
```json
12571261
{

lib/vox_net/http_dispatch.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,21 @@ void SetProfileField(boost::json::object& o,
109109
}
110110
}
111111

112+
std::optional<vox::common::UserId> DmPeerUserId(const std::vector<vox::store::MemberRecord>& members,
113+
const vox::common::UserId& self_user_id) {
114+
for (const auto& member : members) {
115+
if (member.user_id != self_user_id) {
116+
return member.user_id;
117+
}
118+
}
119+
for (const auto& member : members) {
120+
if (member.user_id == self_user_id) {
121+
return member.user_id;
122+
}
123+
}
124+
return std::nullopt;
125+
}
126+
112127
common::Timestamp NowSeconds() {
113128
return std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
114129
}
@@ -1113,6 +1128,14 @@ OptRes HandleAuthenticated(ServerContext& ctx,
11131128
}
11141129
out["created_at"] = conv->created_at;
11151130
out["membership_version"] = conv->membership_version;
1131+
if (conv->type == common::ConversationType::kDm) {
1132+
const auto members = ctx.conversations_store.GetMembers(*conv_only);
1133+
if (const auto peer = DmPeerUserId(members, sess.user_id)) {
1134+
out["peer_user_id"] = *peer;
1135+
} else {
1136+
out["peer_user_id"] = nullptr;
1137+
}
1138+
}
11161139
if (conv->type == common::ConversationType::kChannel) {
11171140
if (pol.contains("title")) {
11181141
out["title"] = pol.at("title");
@@ -1349,6 +1372,14 @@ OptRes HandleAuthenticated(ServerContext& ctx,
13491372
SetProfileField(co, creator_names, c.created_by, "created_by_username");
13501373
co["created_at"] = c.created_at;
13511374
co["membership_version"] = c.membership_version;
1375+
if (c.type == common::ConversationType::kDm) {
1376+
const auto members = ctx.conversations_store.GetMembers(c.conversation_id);
1377+
if (const auto peer = DmPeerUserId(members, sess.user_id)) {
1378+
co["peer_user_id"] = *peer;
1379+
} else {
1380+
co["peer_user_id"] = nullptr;
1381+
}
1382+
}
13521383
if (c.last_activity_at) {
13531384
co["last_activity_at"] = *c.last_activity_at;
13541385
} else {

tests/net_api_tests.cpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ TEST_F(NetApiTestSuite, ListConversationsIncludesCreatedByUsername) {
206206
const auto& o = item.as_object();
207207
if (o.at("conversation_id").as_string() == conv_id) {
208208
ASSERT_EQ(o.at("created_by_username").as_string(), "lst_b");
209+
ASSERT_TRUE(o.contains("peer_user_id"));
210+
ASSERT_EQ(o.at("peer_user_id").as_string(), a.user_id);
209211
found = true;
210212
break;
211213
}
@@ -226,6 +228,36 @@ TEST_F(NetApiTestSuite, GetConversationIncludesCreatedByUsername) {
226228
ASSERT_EQ(gst, 200u);
227229
auto o = ParseObj(gbody);
228230
ASSERT_EQ(o["created_by_username"].as_string(), "gone_b");
231+
ASSERT_TRUE(o.contains("peer_user_id"));
232+
ASSERT_EQ(o["peer_user_id"].as_string(), a.user_id);
233+
}
234+
235+
TEST_F(NetApiTestSuite, ListConversationsOmitsPeerUserIdForGroup) {
236+
auto a = RegisterUser("grp_l_a", "grp_lda");
237+
auto b = RegisterUser("grp_l_b", "grp_ldb");
238+
boost::json::object conv;
239+
conv["type"] = "group";
240+
boost::json::array members;
241+
members.emplace_back(a.user_id);
242+
members.emplace_back(b.user_id);
243+
conv["members"] = members;
244+
auto [cst, cbody] = HttpPost("/v1/conversations", boost::json::serialize(conv), a.access_token);
245+
ASSERT_EQ(cst, 200u);
246+
std::string conv_id = JsonString(cbody, "conversation_id");
247+
248+
auto [gst, gbody] = HttpGet("/v1/conversations", a.access_token);
249+
ASSERT_EQ(gst, 200u);
250+
bool found = false;
251+
auto list_root = ParseObj(gbody);
252+
for (const auto& item : list_root["conversations"].as_array()) {
253+
const auto& o = item.as_object();
254+
if (o.at("conversation_id").as_string() == conv_id) {
255+
ASSERT_FALSE(o.contains("peer_user_id"));
256+
found = true;
257+
break;
258+
}
259+
}
260+
ASSERT_TRUE(found);
229261
}
230262

231263
TEST_F(NetApiTestSuite, LogoutRevokesBearer) {
@@ -691,6 +723,11 @@ TEST_F(NetApiTestSuite, SelfDmCreateAndSendMessage) {
691723
auto [cst, cbody] = HttpPost("/v1/conversations", boost::json::serialize(conv), u.access_token);
692724
ASSERT_EQ(cst, 200u);
693725
std::string conv_id = JsonString(cbody, "conversation_id");
726+
auto [gst, gbody] = HttpGet(std::string("/v1/conversations/") + conv_id, u.access_token);
727+
ASSERT_EQ(gst, 200u);
728+
auto conv_obj = ParseObj(gbody);
729+
ASSERT_TRUE(conv_obj.contains("peer_user_id"));
730+
ASSERT_EQ(conv_obj["peer_user_id"].as_string(), u.user_id);
694731

695732
boost::json::object send;
696733
send["device_id"] = "dev_self";

0 commit comments

Comments
 (0)