@kakao-bot/sdk 0.3.0 → 0.4.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/README.md CHANGED
@@ -29,6 +29,35 @@ Node.js 20 이상, ESM을 지원합니다. 서비스 키는 브라우저 번들
29
29
 
30
30
  `queued: true`는 접수이며 실제 전달 성공이 아닙니다. 서버는 서비스별 `requestId`로 중복을 방지합니다. 같은 논리적 발송에는 같은 ID를 사용하세요. **SDK는 POST 또는 GET을 자동 재시도하지 않습니다.** 타임아웃은 이미 접수된 요청일 수 있으므로 새 ID로 재시도하면 중복 발송됩니다. `KakaoTransportError.requestId`로 요청을 식별하고 대시보드 로그를 확인하세요.
31
31
 
32
+ ## 멘션
33
+
34
+ 받은 웹훅의 `mentions`에는 메시지가 실제로 태그한 계정이 `{ userId, name? }`로 들어옵니다. 이름은 바뀔 수 있으니 `userId`로 비교하세요.
35
+
36
+ 보낼 때는 본문에 `mention(userId)`를 넣으면 그 사람을 진짜 카톡 멘션(알림 포함)으로 태그합니다. 허브가 `<@userId>`를 `@표시이름`으로 바꿔 보냅니다.
37
+
38
+ ```ts
39
+ import { mention } from '@kakao-bot/sdk';
40
+
41
+ await kakao.sendMessage({
42
+ roomId: process.env.KAKAO_ROOM_ID!,
43
+ text: `${mention(event.senderId!)} 접수했어요`,
44
+ requestId: `ack:${event.event_id}`,
45
+ });
46
+ ```
47
+
48
+ 방에 누가 있는지는 `listRoomMembers`로 봅니다(`rooms:read` 권한). 카톡 방 정보에서 온 계정 ID와, 그 방에서 본 적 있는 표시 이름을 돌려줍니다.
49
+
50
+ ```ts
51
+ const members = await bot.listRoomMembers({ roomSlug: "tf" });
52
+ // [{ userId: "353148777", name: "김준민", mention: "<@353148777>" }, { userId: "22854332", name: null, ... }]
53
+ const target = members.find((m) => m.name === "김준민");
54
+ if (target) await bot.sendMessage({ roomSlug: "tf", text: `${target.mention} 확인 부탁해요` });
55
+ ```
56
+
57
+ `name`은 봇폰 카톡 앱에서 읽어 채웁니다(봇폰에 Frida 설정 필요). 설정이 없으면 그 방에서 말하거나 멘션된 적 있는 사람만 이름이 있고, 나머지는 `null`입니다. 이름을 모르면 태그 글자를 만들 수 없어 `mention_unresolved`로 발송되지 않습니다. 키워드 자동응답(rule) 서비스는 응답 문구에 `{보낸사람}`을 쓰면 말한 사람을 태그합니다.
58
+
59
+ 그 방에 없는 사람이면 `mention_unresolved`로 발송되지 않습니다. 멘션은 Iris 전송에서만 동작하며, 봇폰 설정에 따라 태그 없이 `@이름` 글자로만 나갈 수 있습니다.
60
+
32
61
  ## 오류 처리
33
62
 
34
63
  모든 오류는 `KakaoError`를 상속합니다. 한 번의 `catch`로 전부 받고, `name`이나 `instanceof`로 갈라 봅니다.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  export type { KakaoErrorCode, KakaoErrorCodeLike, KakaoErrorJSON } from "./errors.js";
2
2
  export { isKakaoError, KAKAO_ERROR_BRAND, KAKAO_ERROR_CODES, KakaoApiError, KakaoError, KakaoProtocolError, KakaoTimeoutError, KakaoTransportError, } from "./errors.js";
3
+ /**
4
+ * Tag a member in `sendMessage` text: `` `${mention(ev.senderId)} 확인했어요` ``.
5
+ * The bridge swaps it for `@표시이름` and sends a real Kakao mention (notification to that person).
6
+ * Use the stable ID from `InboundWebhookEvent.senderId` or `mentions[].userId`, never a display name.
7
+ */
8
+ export declare function mention(kakaoUserId: string): string;
3
9
  export interface KakaoBotOptions {
4
10
  /** Hub origin, e.g. https://hub.example.com. Never use the phone's private endpoint. */
5
11
  baseUrl: string;
@@ -21,6 +27,7 @@ export type SendMessageInput = ({
21
27
  room?: never;
22
28
  roomSlug?: never;
23
29
  }) & {
30
+ /** `<@kakaoUserId>` (see {@link mention}) becomes a real Kakao tag on the Iris transport. */
24
31
  text: string;
25
32
  /** Reuse this ID for the same logical send. No automatic retries are performed. */
26
33
  requestId?: string;
@@ -41,6 +48,15 @@ export interface Room {
41
48
  status: "pending" | "active" | "suspended";
42
49
  canReply: boolean;
43
50
  }
51
+ /** A Kakao account in a room. Put `mention` in a message to tag them. */
52
+ export interface RoomMember {
53
+ /** Kakao account ID. */
54
+ userId: string;
55
+ /** Display name, or null until the person speaks or is mentioned in the room. */
56
+ name: string | null;
57
+ /** `<@userId>`; tags the member when included in message text. */
58
+ mention: string;
59
+ }
44
60
  /** Hub-signed inbound webhook. Validate the HMAC on the raw body before parsing. */
45
61
  export interface InboundWebhookEvent {
46
62
  event_id: string;
@@ -55,6 +71,11 @@ export interface InboundWebhookEvent {
55
71
  received_at: number;
56
72
  /** Present only for rooms explicitly enabled by the owner. Never log image bytes. */
57
73
  imageBase64?: string;
74
+ /** Accounts the message actually tagged (Iris transport only). Compare userId, not name. */
75
+ mentions?: {
76
+ userId: string;
77
+ name?: string;
78
+ }[];
58
79
  test?: boolean;
59
80
  }
60
81
  export interface RequestOptions {
@@ -68,5 +89,11 @@ export declare class KakaoBot {
68
89
  constructor(options: KakaoBotOptions);
69
90
  sendMessage(input: SendMessageInput, options?: RequestOptions): Promise<SendMessageResult>;
70
91
  listRooms(options?: RequestOptions): Promise<Room[]>;
92
+ /** Members of a room bound to this service. Requires the `rooms:read` scope. */
93
+ listRoomMembers(room: {
94
+ roomId: string;
95
+ } | {
96
+ roomSlug: string;
97
+ }, options?: RequestOptions): Promise<RoomMember[]>;
71
98
  private request;
72
99
  }
package/dist/index.js CHANGED
@@ -1,5 +1,15 @@
1
1
  export { isKakaoError, KAKAO_ERROR_BRAND, KAKAO_ERROR_CODES, KakaoApiError, KakaoError, KakaoProtocolError, KakaoTimeoutError, KakaoTransportError, } from "./errors.js";
2
2
  import { KakaoApiError, KakaoProtocolError, KakaoTimeoutError, KakaoTransportError, } from "./errors.js";
3
+ /**
4
+ * Tag a member in `sendMessage` text: `` `${mention(ev.senderId)} 확인했어요` ``.
5
+ * The bridge swaps it for `@표시이름` and sends a real Kakao mention (notification to that person).
6
+ * Use the stable ID from `InboundWebhookEvent.senderId` or `mentions[].userId`, never a display name.
7
+ */
8
+ export function mention(kakaoUserId) {
9
+ if (!/^\d{1,20}$/.test(kakaoUserId))
10
+ throw new TypeError("kakaoUserId must be a numeric Kakao user ID");
11
+ return `<@${kakaoUserId}>`;
12
+ }
3
13
  function object(value) {
4
14
  return !!value && typeof value === "object" && !Array.isArray(value);
5
15
  }
@@ -67,6 +77,22 @@ export class KakaoBot {
67
77
  throw new KakaoProtocolError();
68
78
  return data.rooms;
69
79
  }
80
+ /** Members of a room bound to this service. Requires the `rooms:read` scope. */
81
+ async listRoomMembers(room, options = {}) {
82
+ const path = "roomId" in room
83
+ ? `/v1/rooms/id/${encodeURIComponent(room.roomId)}/members`
84
+ : `/v1/rooms/${encodeURIComponent(room.roomSlug)}/members`;
85
+ const data = await this.request(path, "GET", undefined, options.signal);
86
+ if (!object(data) ||
87
+ !Array.isArray(data.members) ||
88
+ !data.members.every((member) => object(member) &&
89
+ typeof member.userId === "string" &&
90
+ /^\d+$/.test(member.userId) &&
91
+ (member.name === null || typeof member.name === "string") &&
92
+ member.mention === `<@${member.userId}>`))
93
+ throw new KakaoProtocolError();
94
+ return data.members;
95
+ }
70
96
  async request(path, method, body, signal, requestId) {
71
97
  const timeout = AbortSignal.timeout(this.timeoutMs);
72
98
  const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kakao-bot/sdk",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Typed server-side client for the Kakao Bot Hub API",
5
5
  "license": "MIT",
6
6
  "author": "Jeongmin Lee",