@satorijs/adapter-lark 3.1.3 → 3.1.5

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.
@@ -0,0 +1,233 @@
1
+ import { Internal, Lark } from '..'
2
+ import { Paginated, Pagination } from '../utils'
3
+
4
+ import { MessageContent } from './content'
5
+
6
+ export * from './content'
7
+ export * from './asset'
8
+
9
+ export type MessageType = 'text' | 'post' | 'image' | 'file' | 'audio' | 'media' | 'sticker' | 'interactive' | 'share_chat' | 'share_user'
10
+
11
+ export interface MessageContentMap {
12
+ 'text': MessageContent.Text
13
+ 'post': MessageContent.RichText
14
+ 'image': MessageContent.Image
15
+ 'file': MessageContent.File
16
+ 'audio': MessageContent.Audio
17
+ 'media': MessageContent.Media
18
+ 'sticker': MessageContent.Sticker
19
+ 'share_chat': MessageContent.ShareChat
20
+ 'share_user': MessageContent.ShareUser
21
+ }
22
+
23
+ export type MessageContentType<T extends MessageType> = T extends keyof MessageContentMap ? MessageContentMap[T] : any
24
+
25
+ export interface Sender extends Lark.UserIdentifiers {
26
+ sender_type: string
27
+ tenant_key: string
28
+ }
29
+ export interface Mention extends Lark.UserIdentifiers {
30
+ key: string
31
+ name: string
32
+ tenant_key: string
33
+ }
34
+
35
+ declare module '../event' {
36
+ export interface Events {
37
+ /**
38
+ * Receive message event.
39
+ * @see https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/events/receive
40
+ */
41
+ 'im.message.receive_v1': EventSkeleton<'im.message.receive_v1', {
42
+ sender: {
43
+ sender_id: Lark.UserIds
44
+ sender_type?: string
45
+ tenant_key: string
46
+ }
47
+ message: {
48
+ message_id: string
49
+ root_id: string
50
+ parent_id: string
51
+ create_time: string
52
+ chat_id: string
53
+ chat_type: string
54
+ message_type: MessageType
55
+ content: string
56
+ mentions: {
57
+ key: string
58
+ id: Lark.UserIds
59
+ name: string
60
+ tenant_key: string
61
+ }[]
62
+ }
63
+ }>
64
+ /**
65
+ * Message read event.
66
+ * @see https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/events/message_read
67
+ */
68
+ 'im.message.message_read_v1': EventSkeleton<'im.message.message_read_v1', {
69
+ reader: {
70
+ reader_id: Lark.UserIds
71
+ read_time: string
72
+ tenant_key: string
73
+ }
74
+ message_id_list: string[]
75
+ }>
76
+ }
77
+ }
78
+
79
+ export interface MessagePayload {
80
+ receive_id: string
81
+ content: string
82
+ msg_type: string
83
+ }
84
+
85
+ export interface Message {
86
+ /**
87
+ * The id of current message
88
+ *
89
+ * Should be started with `om_`
90
+ */
91
+ message_id: string
92
+ /**
93
+ * The id of the *root* message in reply chains
94
+ * @see https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/intro#ac79c1c2
95
+ */
96
+ root_id: string
97
+
98
+ /**
99
+ * The id of the direct *parent* message in reply chains
100
+ * @see https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/intro#ac79c1c2
101
+ */
102
+ parent_id: string
103
+
104
+ /**
105
+ * The message type.
106
+ * @see https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/im-v1/message/create_json
107
+ */
108
+ msg_type: MessageType
109
+
110
+ /**
111
+ * The timestamp when the message is created in milliseconds.
112
+ */
113
+ create_time: string
114
+
115
+ /**
116
+ * The timestamp when the message is last updated in milliseconds.
117
+ */
118
+ update_time: string
119
+
120
+ /**
121
+ * Whether the message is deleted.
122
+ */
123
+ deleted: boolean
124
+
125
+ /**
126
+ * Whether the message is updated.
127
+ */
128
+ updated: boolean
129
+
130
+ /**
131
+ * The id of the group / channel the message is sent to.
132
+ */
133
+ chat_id: string
134
+
135
+ /**
136
+ * The sender of the message.
137
+ * Can be a user or an app.
138
+ */
139
+ sender: Sender
140
+
141
+ /**
142
+ * The body of the message.
143
+ */
144
+ body: {
145
+ /**
146
+ * The content of the message.
147
+ * Should be a string that represents the JSON object contains the message content.
148
+ */
149
+ content: string
150
+ }
151
+
152
+ /**
153
+ * Users mentioned in the message.
154
+ */
155
+ mentions: Mention[]
156
+
157
+ /**
158
+ * The id of the direct *parent* message in `merge and repost` chains.
159
+ */
160
+ upper_message_id: string
161
+ }
162
+
163
+ export interface ReadUser {
164
+ user_id_type: Lark.UserIdType
165
+ user_id: string
166
+ timestamp: string
167
+ tenant_key: string
168
+ }
169
+
170
+ export interface GetMessageListParams {
171
+ /**
172
+ * Currently there is only 'chat' available
173
+ * @see https://open.larksuite.com/document/server-docs/im-v1/message/list
174
+ */
175
+ container_id_type: 'p2p' | 'chat'
176
+ /**
177
+ * Should be in the format like `oc_234jsi43d3ssi993d43545f`
178
+ */
179
+ container_id: string
180
+ /** Timestamp in seconds */
181
+ start_time?: string | number
182
+ /** Timestamp in seconds */
183
+ end_time?: string | number
184
+ /** @default 'ByCreateTimeAsc' */
185
+ sort_type?: 'ByCreateTimeAsc' | 'ByCreateTimeDesc'
186
+ /** Range from 1 to 50 */
187
+ page_size?: number
188
+ /**
189
+ * If the current page is the first page, this field should be omitted.
190
+ * Otherwise you could use the `page_token` from the previous response to
191
+ * get the next page.
192
+ */
193
+ page_token?: string
194
+ }
195
+
196
+ declare module '../internal' {
197
+ export interface Internal {
198
+ /** @see https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/create */
199
+ sendMessage(receive_id_type: Lark.ReceiveIdType, message: MessagePayload): Promise<BaseResponse & { data: Message }>
200
+ /** @see https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/reply */
201
+ replyMessage(message_id: string, message: MessagePayload): Promise<BaseResponse & { data: Message }>
202
+ /** @see https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/update */
203
+ updateMessage(message_id: string, message: Omit<MessagePayload, 'receive_id'>): Promise<BaseResponse & { data: Message }>
204
+ /** @see https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/get */
205
+ getMessage(message_id: string): Promise<BaseResponse & { data: Message }>
206
+ /** @see https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/delete */
207
+ deleteMessage(message_id: string): Promise<BaseResponse>
208
+ /** @see https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/read_users */
209
+ getMessageReadUsers(message_id: string, params: Pagination<{ user_id_type: Lark.UserIdType }>): Promise<BaseResponse & { data: Paginated<ReadUser> }>
210
+ /** @see https://open.larksuite.com/document/server-docs/im-v1/message/list */
211
+ getMessageList(params: GetMessageListParams): Promise<BaseResponse & { data: Paginated<Message> }>
212
+ }
213
+ }
214
+
215
+ Internal.define({
216
+ '/im/v1/messages': {
217
+ GET: 'getMessageList',
218
+ },
219
+ '/im/v1/messages?receive_id_type={receive_id_type}': {
220
+ POST: 'sendMessage',
221
+ },
222
+ '/im/v1/messages/{message_id}/reply': {
223
+ POST: 'replyMessage',
224
+ },
225
+ '/im/v1/messages/{message_id}': {
226
+ GET: 'getMessage',
227
+ PUT: 'updateMessage',
228
+ DELETE: 'deleteMessage',
229
+ },
230
+ '/im/v1/messages/{message_id}/read_users': {
231
+ GET: 'getMessageReadUsers',
232
+ },
233
+ })
@@ -0,0 +1,91 @@
1
+ import { Lark } from '.'
2
+ import { Internal } from './internal'
3
+
4
+ declare module '.' {
5
+ export namespace Lark {
6
+ export interface User {
7
+ union_id: string
8
+ user_id?: string
9
+ open_id: string
10
+ name?: string
11
+ en_name?: string
12
+ nickname?: string
13
+ email?: string
14
+ mobile?: string
15
+ mobile_visible: boolean
16
+ gender?: Gender
17
+ avatar?: AvatarInfo
18
+ status?: UserStatus
19
+ department_ids?: string[]
20
+ leader_user_id?: string
21
+ city?: string
22
+ country?: string
23
+ work_station?: string
24
+ join_time?: number
25
+ is_tenant_manager?: boolean
26
+ employee_no?: string
27
+ employee_type?: number
28
+ orders?: UserOrder[]
29
+ custom_attrs?: any // TODO
30
+ enterprise_email?: string
31
+ job_title?: string
32
+ geo?: string
33
+ job_level_id?: string
34
+ job_family_id?: string
35
+ assign_info?: any // TODO
36
+ department_path?: DepartmentDetail[]
37
+ }
38
+
39
+ export enum Gender {
40
+ SECRET = 0,
41
+ MALE = 1,
42
+ FEMALE = 2,
43
+ }
44
+
45
+ export interface AvatarInfo {
46
+ avatar_72: string
47
+ avatar_240: string
48
+ avatar_640: string
49
+ avatar_origin: string
50
+ }
51
+
52
+ export interface UserStatus {
53
+ is_frozen: boolean
54
+ is_resigned: boolean
55
+ is_activated: boolean
56
+ is_exited: boolean
57
+ is_unjoin: boolean
58
+ }
59
+
60
+ export interface UserOrder {
61
+ department_id: string
62
+ user_order: number
63
+ department_order: number
64
+ is_primary_dept: boolean
65
+ }
66
+
67
+ export interface DepartmentDetail {
68
+ dotted_line_leader_user_ids: string[]
69
+ }
70
+ }
71
+ }
72
+
73
+ export interface GuildMember {
74
+ member_id_type: Lark.UserIdType
75
+ member_id: string
76
+ name: string
77
+ tenant_key: string
78
+ }
79
+
80
+ declare module './internal' {
81
+ export interface Internal {
82
+ /** @see https://open.larksuite.com/document/server-docs/contact-v3/user/get */
83
+ getUserInfo(user_id: string, user_id_type?: Lark.UserIdType, department_id_type?: Lark.DepartmentIdType): Promise<BaseResponse & { data: Lark.User }>
84
+ }
85
+ }
86
+
87
+ Internal.define({
88
+ 'contact/v3/users/{user_id}': {
89
+ GET: 'getUserInfo',
90
+ },
91
+ })
@@ -0,0 +1,7 @@
1
+ export type Pagination<T> = T & { page_size?: number; page_token?: string }
2
+
3
+ export interface Paginated<T> {
4
+ items: T[]
5
+ has_more: boolean
6
+ page_token: string
7
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,199 @@
1
+ import crypto from 'crypto'
2
+ import { Channel, Guild, Message, User } from '@satorijs/protocol'
3
+ import { Context, h, Session, trimSlash } from '@satorijs/satori'
4
+ import { FeishuBot, LarkBot } from './bot'
5
+ import { AllEvents, Events, Lark, Message as LarkMessage, MessageContentType, MessageType } from './types'
6
+
7
+ export type Sender =
8
+ | {
9
+ sender_id: Lark.UserIds
10
+ sender_type?: string
11
+ tenant_key: string
12
+ }
13
+ | (Lark.UserIdentifiers & { sender_type?: string; tenant_key: string })
14
+
15
+ export function adaptSender(sender: Sender, session: Session): Session {
16
+ let userId: string | undefined
17
+ if ('sender_id' in sender) {
18
+ userId = sender.sender_id.open_id
19
+ } else {
20
+ userId = sender.id
21
+ }
22
+ session.userId = userId
23
+ return session
24
+ }
25
+
26
+ export function adaptMessage(bot: FeishuBot, data: Events['im.message.receive_v1']['event'], session: Session): Session {
27
+ const json = JSON.parse(data.message.content) as MessageContentType<MessageType>
28
+ const assetEndpoint = trimSlash(bot.config.selfUrl ?? bot.ctx.router.config.selfUrl) + bot.config.path + '/assets'
29
+ const content: (string | h)[] = []
30
+ switch (data.message.message_type) {
31
+ case 'text': {
32
+ const text = json.text as string
33
+ if (!data.message.mentions?.length) {
34
+ content.push(text)
35
+ break
36
+ }
37
+
38
+ // Lark's `at` Element would be `@user_id` in text
39
+ text.split(' ').forEach((word) => {
40
+ if (word.startsWith('@')) {
41
+ const mention = data.message.mentions.find((mention) => mention.key === word)
42
+ content.push(h.at(mention.id.open_id, { name: mention.name }))
43
+ } else {
44
+ content.push(word)
45
+ }
46
+ })
47
+ break
48
+ }
49
+ case 'image':
50
+ content.push(h.image(`${assetEndpoint}/image/${data.message.message_id}/${json.image_key}?self_id=${bot.selfId}`))
51
+ break
52
+ case 'audio':
53
+ content.push(h.audio(`${assetEndpoint}/file/${data.message.message_id}/${json.file_key}?self_id=${bot.selfId}`))
54
+ break
55
+ case 'media':
56
+ content.push(h.video(`${assetEndpoint}/file/${data.message.message_id}/${json.file_key}?self_id=${bot.selfId}`, json.image_key))
57
+ break
58
+ case 'file':
59
+ content.push(h.file(`${assetEndpoint}/file/${data.message.message_id}/${json.file_key}?self_id=${bot.selfId}`))
60
+ break
61
+ }
62
+
63
+ session.timestamp = +data.message.create_time
64
+ session.messageId = data.message.message_id
65
+ session.channelId = data.message.chat_id
66
+ session.content = content.map((c) => c.toString()).join(' ')
67
+
68
+ return session
69
+ }
70
+
71
+ export function adaptSession<C extends Context>(bot: FeishuBot<C>, body: AllEvents) {
72
+ const session = bot.session()
73
+ session.setInternal('lark', body)
74
+
75
+ switch (body.type) {
76
+ case 'im.message.receive_v1':
77
+ session.type = 'message'
78
+ session.subtype = body.event.message.chat_type
79
+ if (session.subtype === 'p2p') session.subtype = 'private'
80
+ session.isDirect = session.subtype === 'private'
81
+ adaptSender(body.event.sender, session)
82
+ adaptMessage(bot, body.event, session)
83
+ break
84
+ }
85
+ return session
86
+ }
87
+
88
+ // TODO: This function has many duplicated code with `adaptMessage`, should refactor them
89
+ export async function decodeMessage(bot: LarkBot, body: LarkMessage): Promise<Message> {
90
+ const json = JSON.parse(body.body.content) as MessageContentType<MessageType>
91
+ const assetEndpoint = trimSlash(bot.config.selfUrl ?? bot.ctx.router.config.selfUrl) + bot.config.path + '/assets'
92
+ const content: h[] = []
93
+ switch (body.msg_type) {
94
+ case 'text': {
95
+ const text = json.text as string
96
+ if (!body.mentions?.length) {
97
+ content.push(h.text(text))
98
+ break
99
+ }
100
+
101
+ // Lark's `at` Element would be `@user_id` in text
102
+ text.split(' ').forEach((word) => {
103
+ if (word.startsWith('@')) {
104
+ const mention = body.mentions.find((mention) => mention.key === word)
105
+ content.push(h.at(mention.id, { name: mention.name }))
106
+ } else {
107
+ content.push(h.text(word))
108
+ }
109
+ })
110
+ break
111
+ }
112
+ case 'image':
113
+ content.push(h.image(`${assetEndpoint}/image/${body.message_id}/${json.image_key}?self_id=${bot.selfId}`))
114
+ break
115
+ case 'audio':
116
+ content.push(h.audio(`${assetEndpoint}/file/${body.message_id}/${json.file_key}?self_id=${bot.selfId}`))
117
+ break
118
+ case 'media':
119
+ content.push(h.video(`${assetEndpoint}/file/${body.message_id}/${json.file_key}?self_id=${bot.selfId}`, json.image_key))
120
+ break
121
+ case 'file':
122
+ content.push(h.file(`${assetEndpoint}/file/${body.message_id}/${json.file_key}?self_id=${bot.selfId}`))
123
+ break
124
+ }
125
+
126
+ return {
127
+ timestamp: +body.update_time,
128
+ createdAt: +body.create_time,
129
+ updatedAt: +body.update_time,
130
+ id: body.message_id,
131
+ content: content.map((c) => c.toString()).join(' '),
132
+ elements: content,
133
+ quote: body.upper_message_id ? await bot.getMessage(body.chat_id, body.upper_message_id) : undefined,
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Get ID type from id string
139
+ * @see https://open.larksuite.com/document/home/user-identity-introduction/introduction
140
+ */
141
+ export function extractIdType(id: string): Lark.ReceiveIdType {
142
+ if (id.startsWith('ou')) return 'open_id'
143
+ if (id.startsWith('on')) return 'union_id'
144
+ if (id.startsWith('oc')) return 'chat_id'
145
+ if (id.includes('@')) return 'email'
146
+ return 'user_id'
147
+ }
148
+
149
+ export function decodeChannel(guild: Lark.Guild): Channel {
150
+ return {
151
+ id: guild.chat_id,
152
+ type: Channel.Type.TEXT,
153
+ name: guild.name,
154
+ parentId: guild.chat_id,
155
+ }
156
+ }
157
+
158
+ export function decodeGuild(guild: Lark.Guild): Guild {
159
+ return {
160
+ id: guild.chat_id,
161
+ name: guild.name,
162
+ avatar: guild.avatar,
163
+ }
164
+ }
165
+
166
+ export function decodeUser(user: Lark.User): User {
167
+ return {
168
+ id: user.open_id,
169
+ avatar: user.avatar?.avatar_origin,
170
+ isBot: false,
171
+ name: user.name,
172
+ }
173
+ }
174
+
175
+ export class Cipher {
176
+ encryptKey: string
177
+ key: Buffer
178
+
179
+ constructor(key: string) {
180
+ this.encryptKey = key
181
+ const hash = crypto.createHash('sha256')
182
+ hash.update(key)
183
+ this.key = hash.digest()
184
+ }
185
+
186
+ decrypt(encrypt: string) {
187
+ const encryptBuffer = Buffer.from(encrypt, 'base64')
188
+ const decipher = crypto.createDecipheriv('aes-256-cbc', this.key, encryptBuffer.slice(0, 16))
189
+ let decrypted = decipher.update(encryptBuffer.slice(16).toString('hex'), 'hex', 'utf8')
190
+ decrypted += decipher.final('utf8')
191
+ return decrypted
192
+ }
193
+
194
+ calculateSignature(timestamp: string, nonce: string, body: string): string {
195
+ const content = timestamp + nonce + this.encryptKey + body
196
+ const sign = crypto.createHash('sha256').update(content).digest('hex')
197
+ return sign
198
+ }
199
+ }