@noverachat/sdk-web 0.0.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -27,6 +27,224 @@ __export(index_exports, {
27
27
  });
28
28
  module.exports = __toCommonJS(index_exports);
29
29
 
30
+ // src/generated/rest.ts
31
+ function parseAppPolicy(j) {
32
+ return {
33
+ appId: j["app_id"],
34
+ allowMemberBulkDelete: j["allow_member_bulk_delete"],
35
+ readReceiptsEnabled: j["read_receipts_enabled"],
36
+ typingIndicatorsEnabled: j["typing_indicators_enabled"],
37
+ allowMemberDeleteAnyMessage: j["allow_member_delete_any_message"]
38
+ };
39
+ }
40
+ function parseUnreadSummary(j) {
41
+ return {
42
+ total: j["total"],
43
+ rooms: j["rooms"].map((e) => parseUnreadRoomItem(e))
44
+ };
45
+ }
46
+ function parseUnreadRoomItem(j) {
47
+ return {
48
+ roomId: j["room_id"],
49
+ name: j["name"],
50
+ roomType: j["room_type"],
51
+ unreadCount: j["unread_count"],
52
+ lastMessageId: j["last_message_id"],
53
+ lastReadMessageId: j["last_read_message_id"],
54
+ lastMessagePreview: j["last_message_preview"],
55
+ memberCount: j["member_count"],
56
+ membersPreview: j["members_preview"] == null ? void 0 : j["members_preview"].map((e) => parseMemberPreview(e))
57
+ };
58
+ }
59
+ function parseMemberPreview(j) {
60
+ return {
61
+ userId: j["user_id"],
62
+ nickname: j["nickname"],
63
+ profileImageUrl: j["profile_image_url"],
64
+ isOnline: j["is_online"]
65
+ };
66
+ }
67
+ function parseRoomUnread(j) {
68
+ return {
69
+ roomId: j["room_id"],
70
+ unreadCount: j["unread_count"],
71
+ lastReadMessageId: j["last_read_message_id"],
72
+ lastMessageId: j["last_message_id"]
73
+ };
74
+ }
75
+ function parseMessageOut(j) {
76
+ return {
77
+ appId: j["app_id"],
78
+ roomId: j["room_id"],
79
+ messageId: j["message_id"],
80
+ sender: j["sender"] == null ? null : parseSenderMini(j["sender"]),
81
+ senderId: j["sender_id"],
82
+ messageType: j["message_type"],
83
+ customType: j["custom_type"],
84
+ content: j["content"],
85
+ data: j["data"],
86
+ meta: j["meta"],
87
+ fileId: j["file_id"],
88
+ file: j["file"],
89
+ files: j["files"],
90
+ replyToId: j["reply_to_id"],
91
+ reactions: j["reactions"] == null ? void 0 : j["reactions"].map((e) => parseReactionEntry(e)),
92
+ createdAt: j["created_at"],
93
+ updatedAt: j["updated_at"],
94
+ deletedAt: j["deleted_at"],
95
+ deletedByUserId: j["deleted_by_user_id"],
96
+ deleteScope: j["delete_scope"],
97
+ editedCount: j["edited_count"],
98
+ createdAtMs: j["created_at_ms"],
99
+ updatedAtMs: j["updated_at_ms"]
100
+ };
101
+ }
102
+ function parseAnnouncementOut(j) {
103
+ return {
104
+ id: j["id"],
105
+ roomId: j["room_id"],
106
+ content: j["content"],
107
+ createdBy: j["created_by"],
108
+ createdAt: j["created_at"],
109
+ updatedAt: j["updated_at"]
110
+ };
111
+ }
112
+ function parseInviteTokenOut(j) {
113
+ return {
114
+ token: j["token"],
115
+ roomId: j["room_id"],
116
+ createdBy: j["created_by"],
117
+ createdAt: j["created_at"],
118
+ expiresAt: j["expires_at"],
119
+ maxUses: j["max_uses"],
120
+ usedCount: j["used_count"],
121
+ revokedAt: j["revoked_at"]
122
+ };
123
+ }
124
+ function parseInviteAcceptResponse(j) {
125
+ return {
126
+ roomId: j["room_id"],
127
+ roomName: j["room_name"],
128
+ wasAlreadyMember: j["was_already_member"]
129
+ };
130
+ }
131
+ function parseDeviceOut(j) {
132
+ return {
133
+ deviceId: j["device_id"],
134
+ tokenType: j["token_type"],
135
+ tokenPreview: j["token_preview"],
136
+ os: j["os"],
137
+ appVersion: j["app_version"],
138
+ pushEnabled: j["push_enabled"],
139
+ lastActiveAt: j["last_active_at"],
140
+ createdAt: j["created_at"]
141
+ };
142
+ }
143
+ function parseDeviceListResponse(j) {
144
+ return {
145
+ items: j["items"].map((e) => parseDeviceOut(e)),
146
+ total: j["total"]
147
+ };
148
+ }
149
+ function parsePushPreferencesOut(j) {
150
+ return {
151
+ pushEnabled: j["push_enabled"]
152
+ };
153
+ }
154
+ function parseFileOut(j) {
155
+ return {
156
+ id: j["id"],
157
+ kind: j["kind"],
158
+ mime: j["mime"],
159
+ sizeBytes: j["size_bytes"],
160
+ originalName: j["original_name"],
161
+ width: j["width"],
162
+ height: j["height"],
163
+ durationSec: j["duration_sec"],
164
+ thumbnailStatus: j["thumbnail_status"],
165
+ uploadStatus: j["upload_status"],
166
+ createdAt: j["created_at"],
167
+ committedAt: j["committed_at"],
168
+ forwardBlocked: j["forward_blocked"]
169
+ };
170
+ }
171
+ function parseFileCreateResponse(j) {
172
+ return {
173
+ fileId: j["file_id"],
174
+ uploadUrl: j["upload_url"],
175
+ headers: j["headers"],
176
+ expiresAtMs: j["expires_at_ms"]
177
+ };
178
+ }
179
+ function parseBlockOut(j) {
180
+ return {
181
+ blockedUserId: j["blocked_user_id"],
182
+ reason: j["reason"],
183
+ createdAt: j["created_at"]
184
+ };
185
+ }
186
+ function parseRoomMemberOut(j) {
187
+ return {
188
+ userId: j["user_id"],
189
+ nickname: j["nickname"],
190
+ role: j["role"],
191
+ pushTrigger: j["push_trigger"],
192
+ lastReadMessageId: j["last_read_message_id"],
193
+ joinedAt: j["joined_at"],
194
+ isOnline: j["is_online"],
195
+ mutedUntil: j["muted_until"]
196
+ };
197
+ }
198
+ function parseBulkDeleteResult(j) {
199
+ return {
200
+ cleared: j["cleared"],
201
+ upToMessageId: j["up_to_message_id"]
202
+ };
203
+ }
204
+ function parseBulkDeleteBySenderResult(j) {
205
+ return {
206
+ cleared: j["cleared"]
207
+ };
208
+ }
209
+ function parseJoinRequestOut(j) {
210
+ return {
211
+ userId: j["user_id"],
212
+ status: j["status"],
213
+ reason: j["reason"],
214
+ requestedAt: j["requested_at"],
215
+ processedAt: j["processed_at"],
216
+ processedByUserId: j["processed_by_user_id"]
217
+ };
218
+ }
219
+ function parseJoinRequestListResponse(j) {
220
+ return {
221
+ items: j["items"].map((e) => parseJoinRequestOut(e)),
222
+ total: j["total"],
223
+ limit: j["limit"],
224
+ offset: j["offset"]
225
+ };
226
+ }
227
+ function parseJoinRequestCreateResponse(j) {
228
+ return {
229
+ status: j["status"],
230
+ roomId: j["room_id"]
231
+ };
232
+ }
233
+ function parseSenderMini(j) {
234
+ return {
235
+ userId: j["user_id"],
236
+ nickname: j["nickname"],
237
+ profileImageUrl: j["profile_image_url"]
238
+ };
239
+ }
240
+ function parseReactionEntry(j) {
241
+ return {
242
+ key: j["key"],
243
+ userIds: j["user_ids"],
244
+ updatedAt: j["updated_at"]
245
+ };
246
+ }
247
+
30
248
  // src/errors.ts
31
249
  var ErrorCode = {
32
250
  INTERNAL: "NC-GEN-001",
@@ -322,64 +540,31 @@ function tempId() {
322
540
  }
323
541
 
324
542
  // src/features/room.ts
325
- function toMessageOut(r) {
326
- const dataObj = r.data ?? null;
543
+ function toFileInline(f) {
544
+ return {
545
+ id: f.id,
546
+ kind: f.kind,
547
+ mime: f.mime,
548
+ size: f.size,
549
+ name: f.name ?? null,
550
+ width: f.width ?? null,
551
+ height: f.height ?? null,
552
+ durationSec: f.duration_sec ?? null,
553
+ thumbnailStatus: f.thumbnail_status,
554
+ forwardBlocked: Boolean(f.forward_blocked)
555
+ };
556
+ }
557
+ function toMessageOut(j) {
558
+ const base = parseMessageOut(j);
559
+ const raw = j;
560
+ const dataObj = base.data ?? null;
327
561
  const customMeta = dataObj && typeof dataObj === "object" && "_customMeta" in dataObj ? dataObj._customMeta : null;
328
562
  return {
329
- appId: r.app_id,
330
- roomId: r.room_id,
331
- messageId: r.message_id,
332
- sender: r.sender ? {
333
- userId: r.sender.user_id,
334
- nickname: r.sender.nickname ?? null,
335
- profileImageUrl: r.sender.profile_image_url ?? null
336
- } : null,
337
- senderId: r.sender_id ?? null,
338
- messageType: r.message_type,
339
- customType: r.custom_type ?? null,
340
- content: r.content ?? null,
341
- data: dataObj,
342
- meta: r.meta ?? null,
343
- customMeta,
344
- fileId: r.file_id ?? null,
345
- file: r.file ? {
346
- id: r.file.id,
347
- kind: r.file.kind,
348
- mime: r.file.mime,
349
- size: r.file.size,
350
- name: r.file.name ?? null,
351
- width: r.file.width ?? null,
352
- height: r.file.height ?? null,
353
- durationSec: r.file.duration_sec ?? null,
354
- thumbnailStatus: r.file.thumbnail_status,
355
- forwardBlocked: Boolean(r.file.forward_blocked)
356
- } : null,
357
- files: r.files && r.files.length > 0 ? r.files.map((f) => ({
358
- id: f.id,
359
- kind: f.kind,
360
- mime: f.mime,
361
- size: f.size,
362
- name: f.name ?? null,
363
- width: f.width ?? null,
364
- height: f.height ?? null,
365
- durationSec: f.duration_sec ?? null,
366
- thumbnailStatus: f.thumbnail_status,
367
- forwardBlocked: Boolean(f.forward_blocked)
368
- })) : null,
369
- replyToId: r.reply_to_id ?? null,
370
- reactions: (r.reactions ?? []).map((x) => ({
371
- key: x.key,
372
- userIds: x.user_ids,
373
- updatedAt: x.updated_at
374
- })),
375
- createdAt: r.created_at,
376
- createdAtMs: r.created_at_ms,
377
- updatedAt: r.updated_at ?? null,
378
- updatedAtMs: r.updated_at_ms ?? null,
379
- deletedAt: r.deleted_at ?? null,
380
- deletedByUserId: r.deleted_by_user_id ?? null,
381
- deleteScope: r.delete_scope ?? null,
382
- editedCount: r.edited_count ?? 0
563
+ ...base,
564
+ file: raw.file ? toFileInline(raw.file) : null,
565
+ files: raw.files && raw.files.length > 0 ? raw.files.map(toFileInline) : null,
566
+ reactions: base.reactions ?? [],
567
+ customMeta
383
568
  };
384
569
  }
385
570
  var Room = class _Room {
@@ -565,20 +750,24 @@ var Room = class _Room {
565
750
  async sendFile(file, opts = {}) {
566
751
  const kind = opts.kind ?? (file.type.startsWith("image/") ? "image" : file.type.startsWith("video/") ? "video" : "file");
567
752
  const name = opts.name ?? file.name;
568
- const presign = await this.http.request("POST", "/api/v1/files", {
569
- kind,
570
- mime: file.type || "application/octet-stream",
571
- size: file.size,
572
- original_name: name,
573
- // 파일 전달 제한 — uploader-set at presign time. When true,
574
- // other users can't forward this file to a different room.
575
- forward_blocked: Boolean(opts.forwardBlocked)
576
- });
753
+ const created = parseFileCreateResponse(
754
+ await this.http.request("POST", "/api/v1/files", {
755
+ kind,
756
+ mime: file.type || "application/octet-stream",
757
+ size: file.size,
758
+ original_name: name,
759
+ // 파일 전달 제한 uploader-set at presign time. When true,
760
+ // other users can't forward this file to a different room.
761
+ forward_blocked: Boolean(opts.forwardBlocked)
762
+ })
763
+ );
577
764
  const presigned = {
578
- fileId: presign.file_id,
579
- uploadUrl: presign.upload_url,
580
- headers: presign.headers,
581
- expiresAtMs: presign.expires_at_ms
765
+ fileId: created.fileId,
766
+ uploadUrl: created.uploadUrl,
767
+ // Presigned S3 headers are always string-valued; the generated
768
+ // model types the JSON object loosely as Record<string, unknown>.
769
+ headers: created.headers,
770
+ expiresAtMs: created.expiresAtMs
582
771
  };
583
772
  await new Promise((resolve, reject) => {
584
773
  const xhr = new XMLHttpRequest();
@@ -633,17 +822,19 @@ var Room = class _Room {
633
822
  async uploadFileOnly(file, opts = {}) {
634
823
  const kind = opts.kind ?? (file.type.startsWith("image/") ? "image" : file.type.startsWith("video/") ? "video" : "file");
635
824
  const name = opts.name ?? file.name;
636
- const presign = await this.http.request("POST", "/api/v1/files", {
637
- kind,
638
- mime: file.type || "application/octet-stream",
639
- size: file.size,
640
- original_name: name,
641
- forward_blocked: Boolean(opts.forwardBlocked)
642
- });
825
+ const created = parseFileCreateResponse(
826
+ await this.http.request("POST", "/api/v1/files", {
827
+ kind,
828
+ mime: file.type || "application/octet-stream",
829
+ size: file.size,
830
+ original_name: name,
831
+ forward_blocked: Boolean(opts.forwardBlocked)
832
+ })
833
+ );
643
834
  await new Promise((resolve, reject) => {
644
835
  const xhr = new XMLHttpRequest();
645
- xhr.open("PUT", presign.upload_url);
646
- for (const [k, v] of Object.entries(presign.headers)) {
836
+ xhr.open("PUT", created.uploadUrl);
837
+ for (const [k, v] of Object.entries(created.headers)) {
647
838
  xhr.setRequestHeader(k, v);
648
839
  }
649
840
  if (opts.onProgress) {
@@ -670,10 +861,10 @@ var Room = class _Room {
670
861
  });
671
862
  await this.http.request(
672
863
  "POST",
673
- `/api/v1/files/${presign.file_id}/commit`,
864
+ `/api/v1/files/${created.fileId}/commit`,
674
865
  {}
675
866
  );
676
- return presign.file_id;
867
+ return created.fileId;
677
868
  }
678
869
  /**
679
870
  * List files shared in this room — newest-first, paginated.
@@ -698,23 +889,26 @@ var Room = class _Room {
698
889
  }
699
890
  );
700
891
  return {
701
- items: raw.items.map((r) => ({
702
- messageId: r.message_id,
703
- senderId: r.sender_id,
704
- createdAtMs: r.created_at_ms,
705
- file: {
706
- id: r.file.id,
707
- kind: r.file.kind,
708
- mime: r.file.mime,
709
- size: r.file.size_bytes,
710
- name: r.file.original_name ?? null,
711
- width: r.file.width ?? null,
712
- height: r.file.height ?? null,
713
- durationSec: r.file.duration_sec ?? null,
714
- thumbnailStatus: r.file.thumbnail_status,
715
- forwardBlocked: Boolean(r.file.forward_blocked)
716
- }
717
- })),
892
+ items: raw.items.map((r) => {
893
+ const f = parseFileOut(r.file);
894
+ return {
895
+ messageId: r.message_id,
896
+ senderId: r.sender_id,
897
+ createdAtMs: r.created_at_ms,
898
+ file: {
899
+ id: f.id,
900
+ kind: f.kind,
901
+ mime: f.mime,
902
+ size: f.sizeBytes,
903
+ name: f.originalName ?? null,
904
+ width: f.width ?? null,
905
+ height: f.height ?? null,
906
+ durationSec: f.durationSec ?? null,
907
+ thumbnailStatus: f.thumbnailStatus,
908
+ forwardBlocked: Boolean(f.forwardBlocked)
909
+ }
910
+ };
911
+ }),
718
912
  nextCursor: raw.next_cursor,
719
913
  hasMore: raw.has_more
720
914
  };
@@ -752,11 +946,12 @@ var Room = class _Room {
752
946
  this.ws.send({ type: "typing", room_id: this.roomId, is_typing: isTyping });
753
947
  }
754
948
  async edit(messageId, patch) {
755
- return this.http.request(
949
+ const raw = await this.http.request(
756
950
  "PUT",
757
951
  `/api/v1/rooms/${this.roomId}/messages/${messageId}`,
758
952
  patch
759
953
  );
954
+ return toMessageOut(raw);
760
955
  }
761
956
  async delete(messageId, scope = "ALL") {
762
957
  await this.http.request(
@@ -781,12 +976,39 @@ var Room = class _Room {
781
976
  * Other members get a `MEMBER_LEFT` [`roomEvent`](../reference/noverachat.md#events).
782
977
  * Idempotent if already a non-member; throws 409 if the caller was KICKED
783
978
  * (ask an operator to restore first). For GROUP/SUPER_GROUP, re-entry needs
784
- * an operator add; for ONE, re-creating the 1:1 rejoins with history kept. */
785
- async leave() {
979
+ * an operator add; for ONE, re-creating the 1:1 rejoins with history kept.
980
+ *
981
+ * Optional `deleteMessages` — atomically clean up chat history as part
982
+ * of leaving. Default is `"none"` (messages survive so the room's
983
+ * history keeps making sense to remaining members).
984
+ * - `"none"` — leave only.
985
+ * - `"mine"` — soft-delete every message YOU sent, then leave. Other
986
+ * members' messages are untouched. Broadcasts
987
+ * `messages_cleared_by_sender` so connected clients hide
988
+ * them locally.
989
+ * - `"all"` — wipe every message in the room, then leave. Subject to
990
+ * the app's `allow_member_bulk_delete` gate and per-room
991
+ * OPERATOR check — a plain member gets 403 back. */
992
+ async leave(options) {
993
+ const mode = options?.deleteMessages ?? "none";
994
+ const query = mode === "none" ? "" : `?delete_messages=${encodeURIComponent(mode)}`;
786
995
  await this.http.request(
787
996
  "POST",
788
- `/api/v1/rooms/${this.roomId}/leave`
997
+ `/api/v1/rooms/${this.roomId}/leave${query}`
998
+ );
999
+ }
1000
+ /** Delete every message YOU sent in this room. Standalone version of
1001
+ * `leave({ deleteMessages: "mine" })` — useful when a user wants to
1002
+ * scrub their history without actually leaving. Other members'
1003
+ * messages are untouched; broadcasts `messages_cleared_by_sender`
1004
+ * so connected clients hide the vanished ones locally.
1005
+ * No OPERATOR gate — you own your messages. */
1006
+ async deleteMyMessages() {
1007
+ const raw = await this.http.request(
1008
+ "DELETE",
1009
+ `/api/v1/rooms/${this.roomId}/messages/mine`
789
1010
  );
1011
+ return parseBulkDeleteBySenderResult(raw);
790
1012
  }
791
1013
  /**
792
1014
  * Set the caller's per-room push preference — like long-pressing a
@@ -812,11 +1034,11 @@ var Room = class _Room {
812
1034
  * hide rendered messages up to (and including) that id.
813
1035
  */
814
1036
  async deleteAllMessages() {
815
- const raw = await this.http.request("DELETE", `/api/v1/rooms/${this.roomId}/messages`);
816
- return {
817
- cleared: raw.cleared,
818
- upToMessageId: raw.up_to_message_id
819
- };
1037
+ const raw = await this.http.request(
1038
+ "DELETE",
1039
+ `/api/v1/rooms/${this.roomId}/messages`
1040
+ );
1041
+ return parseBulkDeleteResult(raw);
820
1042
  }
821
1043
  async react(messageId, key) {
822
1044
  await this.http.request(
@@ -903,17 +1125,11 @@ var Room = class _Room {
903
1125
  * rooms, so the caller must already be a member to read this.
904
1126
  */
905
1127
  async listMembers() {
906
- const raw = await this.http.request("GET", `/api/v1/rooms/${this.roomId}/members`);
907
- return raw.map((m) => ({
908
- userId: m.user_id,
909
- nickname: m.nickname ?? null,
910
- role: m.role,
911
- pushTrigger: m.push_trigger,
912
- lastReadMessageId: m.last_read_message_id,
913
- joinedAt: m.joined_at,
914
- isOnline: m.is_online ?? null,
915
- mutedUntil: m.muted_until ?? null
916
- }));
1128
+ const raw = await this.http.request(
1129
+ "GET",
1130
+ `/api/v1/rooms/${this.roomId}/members`
1131
+ );
1132
+ return raw.map((m) => parseRoomMemberOut(m));
917
1133
  }
918
1134
  // -------- mute --------
919
1135
  /** OPERATOR-only: mute a member so they can't send messages in this
@@ -941,13 +1157,11 @@ var Room = class _Room {
941
1157
  * newer than the caller's `read_watermark`, plus the room's
942
1158
  * `last_message_id` so the UI can show a "•" dot regardless of count. */
943
1159
  async unread() {
944
- const raw = await this.http.request("GET", `/api/v1/rooms/${this.roomId}/unread`);
945
- return {
946
- roomId: raw.room_id,
947
- unreadCount: raw.unread_count,
948
- lastReadMessageId: raw.last_read_message_id,
949
- lastMessageId: raw.last_message_id
950
- };
1160
+ const raw = await this.http.request(
1161
+ "GET",
1162
+ `/api/v1/rooms/${this.roomId}/unread`
1163
+ );
1164
+ return parseRoomUnread(raw);
951
1165
  }
952
1166
  // -------- freeze --------
953
1167
  /** OPERATOR-only moderation: freeze this room. Non-admin messages are
@@ -982,49 +1196,35 @@ var Room = class _Room {
982
1196
  }
983
1197
  // -------- announcements --------
984
1198
  async listAnnouncements() {
985
- const raw = await this.http.request("GET", `/api/v1/rooms/${this.roomId}/announcements`);
986
- return raw.map((a) => ({
987
- id: a.id,
988
- roomId: a.room_id,
989
- content: a.content,
990
- createdBy: a.created_by,
991
- createdAt: a.created_at,
992
- updatedAt: a.updated_at
993
- }));
1199
+ const raw = await this.http.request(
1200
+ "GET",
1201
+ `/api/v1/rooms/${this.roomId}/announcements`
1202
+ );
1203
+ return raw.map((a) => parseAnnouncementOut(a));
994
1204
  }
995
1205
  async getCurrentAnnouncement() {
996
- const raw = await this.http.request("GET", `/api/v1/rooms/${this.roomId}/announcements/current`);
1206
+ const raw = await this.http.request(
1207
+ "GET",
1208
+ `/api/v1/rooms/${this.roomId}/announcements/current`
1209
+ );
997
1210
  if (!raw) return null;
998
- return {
999
- id: raw.id,
1000
- roomId: raw.room_id,
1001
- content: raw.content,
1002
- createdBy: raw.created_by,
1003
- createdAt: raw.created_at,
1004
- updatedAt: raw.updated_at
1005
- };
1211
+ return parseAnnouncementOut(raw);
1006
1212
  }
1007
1213
  async postAnnouncement(content) {
1008
- const raw = await this.http.request("POST", `/api/v1/rooms/${this.roomId}/announcements`, { content });
1009
- return {
1010
- id: raw.id,
1011
- roomId: raw.room_id,
1012
- content: raw.content,
1013
- createdBy: raw.created_by,
1014
- createdAt: raw.created_at,
1015
- updatedAt: raw.updated_at
1016
- };
1214
+ const raw = await this.http.request(
1215
+ "POST",
1216
+ `/api/v1/rooms/${this.roomId}/announcements`,
1217
+ { content }
1218
+ );
1219
+ return parseAnnouncementOut(raw);
1017
1220
  }
1018
1221
  async updateAnnouncement(id, content) {
1019
- const raw = await this.http.request("PATCH", `/api/v1/rooms/${this.roomId}/announcements/${id}`, { content });
1020
- return {
1021
- id: raw.id,
1022
- roomId: raw.room_id,
1023
- content: raw.content,
1024
- createdBy: raw.created_by,
1025
- createdAt: raw.created_at,
1026
- updatedAt: raw.updated_at
1027
- };
1222
+ const raw = await this.http.request(
1223
+ "PATCH",
1224
+ `/api/v1/rooms/${this.roomId}/announcements/${id}`,
1225
+ { content }
1226
+ );
1227
+ return parseAnnouncementOut(raw);
1028
1228
  }
1029
1229
  async deleteAnnouncement(id) {
1030
1230
  await this.http.request(
@@ -1046,32 +1246,21 @@ var Room = class _Room {
1046
1246
  const body = {};
1047
1247
  if (opts && "expiresInHours" in opts) body.expires_in_hours = opts.expiresInHours;
1048
1248
  if (opts && "maxUses" in opts) body.max_uses = opts.maxUses;
1049
- const raw = await this.http.request("POST", `/api/v1/rooms/${this.roomId}/invites`, body);
1050
- return {
1051
- token: raw.token,
1052
- roomId: raw.room_id,
1053
- createdBy: raw.created_by,
1054
- createdAt: raw.created_at,
1055
- expiresAt: raw.expires_at,
1056
- maxUses: raw.max_uses,
1057
- usedCount: raw.used_count,
1058
- revokedAt: raw.revoked_at
1059
- };
1249
+ const raw = await this.http.request(
1250
+ "POST",
1251
+ `/api/v1/rooms/${this.roomId}/invites`,
1252
+ body
1253
+ );
1254
+ return parseInviteTokenOut(raw);
1060
1255
  }
1061
1256
  /** OPERATOR-only: list all invite tokens for this room (including
1062
1257
  * revoked / expired / exhausted), newest first. */
1063
1258
  async listInvites() {
1064
- const raw = await this.http.request("GET", `/api/v1/rooms/${this.roomId}/invites`);
1065
- return raw.map((r) => ({
1066
- token: r.token,
1067
- roomId: r.room_id,
1068
- createdBy: r.created_by,
1069
- createdAt: r.created_at,
1070
- expiresAt: r.expires_at,
1071
- maxUses: r.max_uses,
1072
- usedCount: r.used_count,
1073
- revokedAt: r.revoked_at
1074
- }));
1259
+ const raw = await this.http.request(
1260
+ "GET",
1261
+ `/api/v1/rooms/${this.roomId}/invites`
1262
+ );
1263
+ return raw.map((r) => parseInviteTokenOut(r));
1075
1264
  }
1076
1265
  /** OPERATOR-only: revoke a token immediately. Idempotent — revoking
1077
1266
  * an already-revoked token returns 204 as well. */
@@ -1081,6 +1270,81 @@ var Room = class _Room {
1081
1270
  `/api/v1/rooms/${this.roomId}/invites/${encodeURIComponent(token)}`
1082
1271
  );
1083
1272
  }
1273
+ // -------- join requests --------
1274
+ /**
1275
+ * Ask to join this room (self). Behavior depends on the room's
1276
+ * `joinPolicy`:
1277
+ * - `OPEN` → the caller is added immediately (`status: "joined"`);
1278
+ * - `APPROVAL_REQUIRED` → the request lands in the operators' inbox
1279
+ * (`status: "pending"`) until [`approveJoinRequest`] / reject.
1280
+ * Idempotent for members (`status: "already_member"`).
1281
+ *
1282
+ * @param reason Optional note shown to operators ("하은이 반 친구 엄마예요").
1283
+ */
1284
+ async requestJoin(reason) {
1285
+ const raw = await this.http.request(
1286
+ "POST",
1287
+ `/api/v1/rooms/${this.roomId}/join-request`,
1288
+ reason ? { reason } : {}
1289
+ );
1290
+ return parseJoinRequestCreateResponse(raw);
1291
+ }
1292
+ /** Cancel the caller's own pending join request. */
1293
+ async cancelJoinRequest() {
1294
+ await this.http.request(
1295
+ "DELETE",
1296
+ `/api/v1/rooms/${this.roomId}/join-request`
1297
+ );
1298
+ }
1299
+ /** OPERATOR-only: list join requests for this room (the approval inbox).
1300
+ * Defaults to pending only — pass a status to see processed history. */
1301
+ async listJoinRequests(opts) {
1302
+ const raw = await this.http.request(
1303
+ "GET",
1304
+ `/api/v1/rooms/${this.roomId}/join-requests`,
1305
+ void 0,
1306
+ {
1307
+ ...opts?.status ? { status: opts.status } : {},
1308
+ ...opts?.limit !== void 0 ? { limit: opts.limit } : {},
1309
+ ...opts?.offset !== void 0 ? { offset: opts.offset } : {}
1310
+ }
1311
+ );
1312
+ return parseJoinRequestListResponse(raw);
1313
+ }
1314
+ /** OPERATOR-only: approve a pending join request — the requester becomes
1315
+ * a MEMBER. [reason] is echoed back to them in later list calls. */
1316
+ async approveJoinRequest(userId, reason) {
1317
+ await this.http.request(
1318
+ "POST",
1319
+ `/api/v1/rooms/${this.roomId}/join-requests/${encodeURIComponent(userId)}/approve`,
1320
+ reason ? { reason } : {}
1321
+ );
1322
+ }
1323
+ /** OPERATOR-only: reject a pending join request. */
1324
+ async rejectJoinRequest(userId, reason) {
1325
+ await this.http.request(
1326
+ "POST",
1327
+ `/api/v1/rooms/${this.roomId}/join-requests/${encodeURIComponent(userId)}/reject`,
1328
+ reason ? { reason } : {}
1329
+ );
1330
+ }
1331
+ // -------- hide / unhide --------
1332
+ /** Hide this room from the caller's own room list (self, reversible —
1333
+ * the KakaoTalk "숨기기"). The room typically reappears via [unhide]
1334
+ * or on new activity, per server policy. Messages are untouched. */
1335
+ async hide() {
1336
+ await this.http.request(
1337
+ "POST",
1338
+ `/api/v1/rooms/${this.roomId}/hide`
1339
+ );
1340
+ }
1341
+ /** Undo [hide] — the room shows up in the caller's list again. */
1342
+ async unhide() {
1343
+ await this.http.request(
1344
+ "POST",
1345
+ `/api/v1/rooms/${this.roomId}/unhide`
1346
+ );
1347
+ }
1084
1348
  // -------- internal dispatch --------
1085
1349
  on(event, fn) {
1086
1350
  return this.bus.on(event, fn);
@@ -1329,13 +1593,7 @@ var NoveraChat = class {
1329
1593
  * destructive buttons to expose. */
1330
1594
  async appPolicy() {
1331
1595
  const raw = await this.http.request("GET", "/api/v1/app/policy");
1332
- return {
1333
- appId: raw.app_id,
1334
- allowMemberBulkDelete: raw.allow_member_bulk_delete,
1335
- readReceiptsEnabled: raw.read_receipts_enabled ?? true,
1336
- typingIndicatorsEnabled: raw.typing_indicators_enabled ?? true,
1337
- deleteAnyMessageEnabled: raw.allow_member_delete_any_message ?? false
1338
- };
1596
+ return parseAppPolicy(raw);
1339
1597
  }
1340
1598
  // ---- device tokens (push) ----
1341
1599
  /**
@@ -1345,19 +1603,23 @@ var NoveraChat = class {
1345
1603
  * safe. Returns the stored device record (token shown only as a prefix).
1346
1604
  */
1347
1605
  async registerDevice(params) {
1348
- const raw = await this.http.request("POST", "/api/v1/users/me/devices", {
1349
- device_id: params.deviceId,
1350
- token: params.token,
1351
- token_type: params.tokenType,
1352
- os: params.os ?? null,
1353
- app_version: params.appVersion ?? null
1354
- });
1355
- return this._toDeviceInfo(raw);
1606
+ const raw = await this.http.request(
1607
+ "POST",
1608
+ "/api/v1/users/me/devices",
1609
+ {
1610
+ device_id: params.deviceId,
1611
+ token: params.token,
1612
+ token_type: params.tokenType,
1613
+ os: params.os ?? null,
1614
+ app_version: params.appVersion ?? null
1615
+ }
1616
+ );
1617
+ return parseDeviceOut(raw);
1356
1618
  }
1357
1619
  /** List the devices registered for the authenticated user. */
1358
1620
  async listDevices() {
1359
1621
  const raw = await this.http.request("GET", "/api/v1/users/me/devices");
1360
- return raw.items.map((d) => this._toDeviceInfo(d));
1622
+ return parseDeviceListResponse(raw).items;
1361
1623
  }
1362
1624
  /**
1363
1625
  * Turn push delivery on/off for one device without unregistering it —
@@ -1395,7 +1657,7 @@ var NoveraChat = class {
1395
1657
  "GET",
1396
1658
  "/api/v1/users/me/push-preferences"
1397
1659
  );
1398
- return { pushEnabled: Boolean(raw.push_enabled) };
1660
+ return parsePushPreferencesOut(raw);
1399
1661
  }
1400
1662
  /**
1401
1663
  * Flip the caller's global push kill switch. Pass `false` to mute
@@ -1408,7 +1670,7 @@ var NoveraChat = class {
1408
1670
  "/api/v1/users/me/push-preferences",
1409
1671
  { push_enabled: enabled }
1410
1672
  );
1411
- return { pushEnabled: Boolean(raw.push_enabled) };
1673
+ return parsePushPreferencesOut(raw);
1412
1674
  }
1413
1675
  // -------------------------------------------------------------------------
1414
1676
  // User blocks — KakaoTalk-style one-way block.
@@ -1432,11 +1694,7 @@ var NoveraChat = class {
1432
1694
  `/api/v1/users/me/blocks/${encodeURIComponent(userId)}`,
1433
1695
  { reason: reason ?? null }
1434
1696
  );
1435
- return {
1436
- blockedUserId: raw.blocked_user_id,
1437
- reason: raw.reason,
1438
- createdAt: raw.created_at
1439
- };
1697
+ return parseBlockOut(raw);
1440
1698
  }
1441
1699
  /** Unblock a user. Idempotent — resolves either way. */
1442
1700
  async unblockUser(userId) {
@@ -1448,23 +1706,7 @@ var NoveraChat = class {
1448
1706
  /** List every user the caller has blocked, newest first. */
1449
1707
  async listBlocks() {
1450
1708
  const raw = await this.http.request("GET", "/api/v1/users/me/blocks");
1451
- return raw.map((r) => ({
1452
- blockedUserId: r.blocked_user_id,
1453
- reason: r.reason,
1454
- createdAt: r.created_at
1455
- }));
1456
- }
1457
- _toDeviceInfo(raw) {
1458
- return {
1459
- deviceId: raw.device_id,
1460
- tokenType: raw.token_type,
1461
- tokenPreview: raw.token_preview,
1462
- os: raw.os,
1463
- appVersion: raw.app_version,
1464
- pushEnabled: raw.push_enabled,
1465
- lastActiveAt: raw.last_active_at,
1466
- createdAt: raw.created_at
1467
- };
1709
+ return raw.map((r) => parseBlockOut(r));
1468
1710
  }
1469
1711
  /**
1470
1712
  * Authorization-gated URL for downloading a file attachment.
@@ -1522,41 +1764,27 @@ var NoveraChat = class {
1522
1764
  * shipping date.
1523
1765
  */
1524
1766
  async getFileMeta(fileId) {
1525
- const raw = await this.http.request("GET", `/api/v1/files/${encodeURIComponent(fileId)}/meta`);
1767
+ const raw = await this.http.request(
1768
+ "GET",
1769
+ `/api/v1/files/${encodeURIComponent(fileId)}/meta`
1770
+ );
1771
+ const f = parseFileOut(raw);
1526
1772
  return {
1527
- id: raw.id,
1528
- kind: raw.kind,
1529
- mime: raw.mime,
1530
- size: raw.size_bytes,
1531
- name: raw.original_name ?? null,
1532
- width: raw.width ?? null,
1533
- height: raw.height ?? null,
1534
- durationSec: raw.duration_sec ?? null,
1535
- thumbnailStatus: raw.thumbnail_status,
1536
- forwardBlocked: Boolean(raw.forward_blocked)
1773
+ id: f.id,
1774
+ kind: f.kind,
1775
+ mime: f.mime,
1776
+ size: f.sizeBytes,
1777
+ name: f.originalName ?? null,
1778
+ width: f.width ?? null,
1779
+ height: f.height ?? null,
1780
+ durationSec: f.durationSec ?? null,
1781
+ thumbnailStatus: f.thumbnailStatus,
1782
+ forwardBlocked: Boolean(f.forwardBlocked)
1537
1783
  };
1538
1784
  }
1539
1785
  async unreadSummary() {
1540
1786
  const raw = await this.http.request("GET", "/api/v1/users/me/unread");
1541
- return {
1542
- total: raw.total,
1543
- rooms: raw.rooms.map((r) => ({
1544
- roomId: r.room_id,
1545
- name: r.name,
1546
- roomType: r.room_type ?? null,
1547
- unreadCount: r.unread_count,
1548
- lastMessageId: r.last_message_id,
1549
- lastMessagePreview: r.last_message_preview ?? null,
1550
- lastReadMessageId: r.last_read_message_id,
1551
- memberCount: r.member_count ?? 0,
1552
- members: (r.members_preview ?? []).map((m) => ({
1553
- userId: m.user_id,
1554
- nickname: m.nickname ?? null,
1555
- profileImageUrl: m.profile_image_url ?? null,
1556
- isOnline: m.is_online ?? null
1557
- }))
1558
- }))
1559
- };
1787
+ return parseUnreadSummary(raw);
1560
1788
  }
1561
1789
  /**
1562
1790
  * Accept an invite link — call this in flows where you receive a
@@ -1574,12 +1802,11 @@ var NoveraChat = class {
1574
1802
  * kicked from the target room.
1575
1803
  */
1576
1804
  async acceptInvite(token) {
1577
- const raw = await this.http.request("POST", `/api/v1/invites/${encodeURIComponent(token)}/accept`);
1578
- return {
1579
- roomId: raw.room_id,
1580
- roomName: raw.room_name,
1581
- wasAlreadyMember: raw.was_already_member
1582
- };
1805
+ const raw = await this.http.request(
1806
+ "POST",
1807
+ `/api/v1/invites/${encodeURIComponent(token)}/accept`
1808
+ );
1809
+ return parseInviteAcceptResponse(raw);
1583
1810
  }
1584
1811
  // ---- internal ----
1585
1812
  dispatch(msg) {