@kakao-bot/sdk 0.2.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 +42 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +26 -0
- package/package.json +1 -1
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`로 갈라 봅니다.
|
|
@@ -82,3 +111,16 @@ pnpm --filter @kakao-bot/sdk pack --pack-destination /tmp/kakao-sdk-release
|
|
|
82
111
|
테스트 요청 ID는 저장하지 않습니다. 같은 ID의 테스트를 반복해도 매번 검증하며, 이후 운영 키에서 같은 ID로 실제 발송할 수 있습니다. 운영 전환은 운영 키를 새로 발급해 서버 환경변수를 교체하세요. 기존 키는 운영 키로 유지됩니다.
|
|
83
112
|
|
|
84
113
|
동일 서비스에서 이미 접수한 `requestId`의 방 ID 또는 본문을 바꾸면 `409 idempotency_conflict`를 반환합니다. 동일한 방·본문은 중복으로 처리하며 추가 큐/사용량을 만들지 않습니다. slug/방 표시명 변경은 고정 방 ID가 같으면 충돌로 보지 않습니다. 기존 기록 중 본문 검증값이 없는 건은 방 변경만 검출하고 재발송하지 않습니다. 검증값은 서버 비밀키로 보호하므로 비밀키 교체 후 이전 요청의 재시도는 충돌할 수 있습니다.
|
|
114
|
+
|
|
115
|
+
### Iris migration
|
|
116
|
+
|
|
117
|
+
`sendMessage` and `listRooms` retain their existing contracts. Prefer a `roomId` or
|
|
118
|
+
`roomSlug` to a display name. The server resolves the stable Kakao ID internally;
|
|
119
|
+
SDK clients must not call the phone directly. `queued: true` means accepted by the
|
|
120
|
+
hub, not delivered to a recipient, and uncertain sends must not be retried blindly.
|
|
121
|
+
|
|
122
|
+
`InboundWebhookEvent` describes signed webhook events, including optional
|
|
123
|
+
`roomExternalId`, `senderId`, and `imageBase64` from Iris. Treat IDs as strings;
|
|
124
|
+
Kakao IDs may exceed JavaScript's safe integer range. Check `X-Kakao-Hub-Signature`
|
|
125
|
+
against the raw request body before parsing. Images are only included for rooms
|
|
126
|
+
explicitly enabled by the owner.
|
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,36 @@ 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
|
+
}
|
|
60
|
+
/** Hub-signed inbound webhook. Validate the HMAC on the raw body before parsing. */
|
|
61
|
+
export interface InboundWebhookEvent {
|
|
62
|
+
event_id: string;
|
|
63
|
+
room: string;
|
|
64
|
+
roomExternalId?: string;
|
|
65
|
+
sender: string;
|
|
66
|
+
senderId?: string;
|
|
67
|
+
msg: string;
|
|
68
|
+
command: string;
|
|
69
|
+
isMention: boolean;
|
|
70
|
+
isGroupChat: boolean;
|
|
71
|
+
received_at: number;
|
|
72
|
+
/** Present only for rooms explicitly enabled by the owner. Never log image bytes. */
|
|
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
|
+
}[];
|
|
79
|
+
test?: boolean;
|
|
80
|
+
}
|
|
44
81
|
export interface RequestOptions {
|
|
45
82
|
signal?: AbortSignal;
|
|
46
83
|
}
|
|
@@ -52,5 +89,11 @@ export declare class KakaoBot {
|
|
|
52
89
|
constructor(options: KakaoBotOptions);
|
|
53
90
|
sendMessage(input: SendMessageInput, options?: RequestOptions): Promise<SendMessageResult>;
|
|
54
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[]>;
|
|
55
98
|
private request;
|
|
56
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;
|