@effect-uai/telegram 0.14.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/LICENSE +21 -0
- package/README.md +45 -0
- package/dist/Telegram.d.mts +44 -0
- package/dist/Telegram.d.mts.map +1 -0
- package/dist/Telegram.mjs +208 -0
- package/dist/Telegram.mjs.map +1 -0
- package/dist/api-CnvvfWKq.d.mts +42 -0
- package/dist/api-CnvvfWKq.d.mts.map +1 -0
- package/dist/events-BkljYfG6.d.mts +221 -0
- package/dist/events-BkljYfG6.d.mts.map +1 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +2 -0
- package/dist/internal/api.d.mts +2 -0
- package/dist/internal/api.mjs +66 -0
- package/dist/internal/api.mjs.map +1 -0
- package/dist/internal/events.d.mts +2 -0
- package/dist/internal/events.mjs +137 -0
- package/dist/internal/events.mjs.map +1 -0
- package/dist/internal/events.test.d.mts +1 -0
- package/dist/internal/events.test.mjs +13818 -0
- package/dist/internal/events.test.mjs.map +1 -0
- package/dist/magic-string.es-CUAnjKYC.mjs +1014 -0
- package/dist/magic-string.es-CUAnjKYC.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +59 -0
- package/src/Telegram.ts +417 -0
- package/src/index.ts +1 -0
- package/src/internal/api.ts +133 -0
- package/src/internal/events.test.ts +112 -0
- package/src/internal/events.ts +232 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { Array as Arr, Match, Option, Schema } from "effect"
|
|
2
|
+
import {
|
|
3
|
+
ChannelId,
|
|
4
|
+
type ConversationRef,
|
|
5
|
+
InboundEvent,
|
|
6
|
+
MessageId,
|
|
7
|
+
ThreadId,
|
|
8
|
+
UserId,
|
|
9
|
+
} from "@effect-uai/core/Messenger"
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Wire shapes: the fields this adapter reads. The rest survives on `raw`.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export const User = Schema.Struct({
|
|
16
|
+
id: Schema.Number,
|
|
17
|
+
is_bot: Schema.Boolean,
|
|
18
|
+
username: Schema.optional(Schema.String),
|
|
19
|
+
})
|
|
20
|
+
export type User = typeof User.Type
|
|
21
|
+
|
|
22
|
+
export const Chat = Schema.Struct({
|
|
23
|
+
id: Schema.Number,
|
|
24
|
+
type: Schema.String,
|
|
25
|
+
})
|
|
26
|
+
export type Chat = typeof Chat.Type
|
|
27
|
+
|
|
28
|
+
export const Entity = Schema.Struct({
|
|
29
|
+
type: Schema.String,
|
|
30
|
+
offset: Schema.Number,
|
|
31
|
+
length: Schema.Number,
|
|
32
|
+
})
|
|
33
|
+
export type Entity = typeof Entity.Type
|
|
34
|
+
|
|
35
|
+
/** Where a message sits: its chat, and its forum topic when it is in one. */
|
|
36
|
+
export const Placement = Schema.Struct({
|
|
37
|
+
chat: Chat,
|
|
38
|
+
message_thread_id: Schema.optional(Schema.Number),
|
|
39
|
+
is_topic_message: Schema.optional(Schema.Boolean),
|
|
40
|
+
})
|
|
41
|
+
export type Placement = typeof Placement.Type
|
|
42
|
+
|
|
43
|
+
export const ReplyTarget = Schema.Struct({
|
|
44
|
+
message_id: Schema.Number,
|
|
45
|
+
from: Schema.optional(User),
|
|
46
|
+
})
|
|
47
|
+
export type ReplyTarget = typeof ReplyTarget.Type
|
|
48
|
+
|
|
49
|
+
export const Message = Schema.Struct({
|
|
50
|
+
...Placement.fields,
|
|
51
|
+
message_id: Schema.Number,
|
|
52
|
+
from: Schema.optional(User),
|
|
53
|
+
text: Schema.optional(Schema.String),
|
|
54
|
+
caption: Schema.optional(Schema.String),
|
|
55
|
+
entities: Schema.optional(Schema.Array(Entity)),
|
|
56
|
+
reply_to_message: Schema.optional(ReplyTarget),
|
|
57
|
+
})
|
|
58
|
+
export type Message = typeof Message.Type
|
|
59
|
+
|
|
60
|
+
export const Reaction = Schema.Struct({
|
|
61
|
+
type: Schema.String,
|
|
62
|
+
emoji: Schema.optional(Schema.String),
|
|
63
|
+
})
|
|
64
|
+
export type Reaction = typeof Reaction.Type
|
|
65
|
+
|
|
66
|
+
export const MessageReaction = Schema.Struct({
|
|
67
|
+
chat: Chat,
|
|
68
|
+
message_id: Schema.Number,
|
|
69
|
+
user: Schema.optional(User),
|
|
70
|
+
new_reaction: Schema.Array(Reaction),
|
|
71
|
+
})
|
|
72
|
+
export type MessageReaction = typeof MessageReaction.Type
|
|
73
|
+
|
|
74
|
+
export const CallbackQuery = Schema.Struct({
|
|
75
|
+
id: Schema.String,
|
|
76
|
+
from: User,
|
|
77
|
+
data: Schema.optional(Schema.String),
|
|
78
|
+
message: Schema.optional(Schema.Struct({ ...Placement.fields, message_id: Schema.Number })),
|
|
79
|
+
})
|
|
80
|
+
export type CallbackQuery = typeof CallbackQuery.Type
|
|
81
|
+
|
|
82
|
+
export const Update = Schema.Struct({
|
|
83
|
+
update_id: Schema.Number,
|
|
84
|
+
message: Schema.optional(Message),
|
|
85
|
+
edited_message: Schema.optional(Message),
|
|
86
|
+
message_reaction: Schema.optional(MessageReaction),
|
|
87
|
+
callback_query: Schema.optional(CallbackQuery),
|
|
88
|
+
})
|
|
89
|
+
export type Update = typeof Update.Type
|
|
90
|
+
|
|
91
|
+
/** Requested on every `getUpdates`; without it Telegram never delivers reactions. */
|
|
92
|
+
export const allowedUpdates = [
|
|
93
|
+
"message",
|
|
94
|
+
"edited_message",
|
|
95
|
+
"callback_query",
|
|
96
|
+
"message_reaction",
|
|
97
|
+
] as const
|
|
98
|
+
|
|
99
|
+
export type BotIdentity = {
|
|
100
|
+
readonly id: number
|
|
101
|
+
readonly username: string
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Mapping
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
const conversation = (at: Placement): ConversationRef => ({
|
|
109
|
+
channel: ChannelId(String(at.chat.id)),
|
|
110
|
+
// Forum topics are threads; reply chains are not (they carry no thread id).
|
|
111
|
+
...(at.is_topic_message === true &&
|
|
112
|
+
at.message_thread_id !== undefined && { thread: ThreadId(String(at.message_thread_id)) }),
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const slice = (text: string, entity: Entity): string =>
|
|
116
|
+
text.slice(entity.offset, entity.offset + entity.length)
|
|
117
|
+
|
|
118
|
+
const isOwnHandle = (bot: BotIdentity, handle: string): boolean =>
|
|
119
|
+
handle.toLowerCase() === `@${bot.username}`.toLowerCase()
|
|
120
|
+
|
|
121
|
+
const entitiesOf = (message: Message): ReadonlyArray<Entity> => message.entities ?? []
|
|
122
|
+
|
|
123
|
+
const ownMentions = (bot: BotIdentity, message: Message, text: string): ReadonlyArray<Entity> =>
|
|
124
|
+
entitiesOf(message).filter(
|
|
125
|
+
(entity) => entity.type === "mention" && isOwnHandle(bot, slice(text, entity)),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
type Command = { readonly name: string; readonly args: string }
|
|
129
|
+
|
|
130
|
+
/** `/name@bot args` at offset 0 is a command; a mid-text `/word` is just text. */
|
|
131
|
+
const command = (bot: BotIdentity, message: Message, text: string): Option.Option<Command> =>
|
|
132
|
+
Arr.findFirst(
|
|
133
|
+
entitiesOf(message),
|
|
134
|
+
(entity) => entity.type === "bot_command" && entity.offset === 0,
|
|
135
|
+
).pipe(
|
|
136
|
+
Option.flatMap((entity) => {
|
|
137
|
+
const [name = "", suffix] = slice(text, entity).slice(1).split("@")
|
|
138
|
+
// A command aimed at another bot arrives under privacy-off, but is not ours.
|
|
139
|
+
return suffix !== undefined && !isOwnHandle(bot, `@${suffix}`)
|
|
140
|
+
? Option.none()
|
|
141
|
+
: Option.some({ name, args: text.slice(entity.length).trim() })
|
|
142
|
+
}),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
/** The bot's own `@handle` removed, right to left so earlier offsets stay valid. */
|
|
146
|
+
const withoutMention = (bot: BotIdentity, message: Message, text: string): string =>
|
|
147
|
+
Arr.reverse(ownMentions(bot, message, text))
|
|
148
|
+
.reduce(
|
|
149
|
+
(acc, entity) => acc.slice(0, entity.offset) + acc.slice(entity.offset + entity.length),
|
|
150
|
+
text,
|
|
151
|
+
)
|
|
152
|
+
.trim()
|
|
153
|
+
|
|
154
|
+
const isAddressed = (bot: BotIdentity, message: Message, text: string): boolean =>
|
|
155
|
+
message.chat.type === "private" ||
|
|
156
|
+
ownMentions(bot, message, text).length > 0 ||
|
|
157
|
+
message.reply_to_message?.from?.id === bot.id
|
|
158
|
+
|
|
159
|
+
const fromMessage = (
|
|
160
|
+
bot: BotIdentity,
|
|
161
|
+
message: Message,
|
|
162
|
+
raw: unknown,
|
|
163
|
+
): Option.Option<InboundEvent> =>
|
|
164
|
+
Option.map(Option.fromNullishOr(message.from), (from) => {
|
|
165
|
+
const text = message.text ?? message.caption ?? ""
|
|
166
|
+
const author = UserId(String(from.id))
|
|
167
|
+
const at = conversation(message)
|
|
168
|
+
return Option.match(command(bot, message, text), {
|
|
169
|
+
onSome: ({ name, args }) =>
|
|
170
|
+
InboundEvent.Command({ conversation: at, name, args, author, raw }),
|
|
171
|
+
onNone: () =>
|
|
172
|
+
InboundEvent.Message({
|
|
173
|
+
conversation: at,
|
|
174
|
+
id: MessageId(String(message.message_id)),
|
|
175
|
+
author,
|
|
176
|
+
text: withoutMention(bot, message, text),
|
|
177
|
+
addressed: isAddressed(bot, message, text),
|
|
178
|
+
...(message.reply_to_message !== undefined && {
|
|
179
|
+
replyTo: MessageId(String(message.reply_to_message.message_id)),
|
|
180
|
+
}),
|
|
181
|
+
raw,
|
|
182
|
+
}),
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
const fromReaction = (reaction: MessageReaction, raw: unknown): ReadonlyArray<InboundEvent> =>
|
|
187
|
+
Option.match(Option.fromNullishOr(reaction.user), {
|
|
188
|
+
onNone: () => [],
|
|
189
|
+
onSome: (user) =>
|
|
190
|
+
Arr.flatMap(reaction.new_reaction, (r) =>
|
|
191
|
+
Arr.fromNullishOr(r.emoji).map((emoji) =>
|
|
192
|
+
InboundEvent.Reaction({
|
|
193
|
+
conversation: conversation({ chat: reaction.chat }),
|
|
194
|
+
message: MessageId(String(reaction.message_id)),
|
|
195
|
+
emoji,
|
|
196
|
+
author: UserId(String(user.id)),
|
|
197
|
+
raw,
|
|
198
|
+
}),
|
|
199
|
+
),
|
|
200
|
+
),
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
const fromCallback = (query: CallbackQuery, raw: unknown): Option.Option<InboundEvent> =>
|
|
204
|
+
Option.map(Option.fromNullishOr(query.message), (message) =>
|
|
205
|
+
InboundEvent.Action({
|
|
206
|
+
conversation: conversation(message),
|
|
207
|
+
actionId: query.data ?? "",
|
|
208
|
+
author: UserId(String(query.from.id)),
|
|
209
|
+
raw,
|
|
210
|
+
}),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Everything one `Update` says to the agent. Edited messages are received
|
|
215
|
+
* (so the offset advances past them) but not re-delivered: a turn already
|
|
216
|
+
* answered them.
|
|
217
|
+
*/
|
|
218
|
+
export const toEvents =
|
|
219
|
+
(bot: BotIdentity) =>
|
|
220
|
+
(update: Update): ReadonlyArray<InboundEvent> =>
|
|
221
|
+
Match.value(update).pipe(
|
|
222
|
+
Match.when({ message: Match.defined }, ({ message }) =>
|
|
223
|
+
Arr.fromOption(fromMessage(bot, message, update)),
|
|
224
|
+
),
|
|
225
|
+
Match.when({ message_reaction: Match.defined }, ({ message_reaction }) =>
|
|
226
|
+
fromReaction(message_reaction, update),
|
|
227
|
+
),
|
|
228
|
+
Match.when({ callback_query: Match.defined }, ({ callback_query }) =>
|
|
229
|
+
Arr.fromOption(fromCallback(callback_query, update)),
|
|
230
|
+
),
|
|
231
|
+
Match.orElse(() => []),
|
|
232
|
+
)
|