@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,66 @@
|
|
|
1
|
+
import { Array, Data, Duration, Effect, Redacted, Result, Schema, pipe } from "effect";
|
|
2
|
+
import { HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
3
|
+
import * as MessengerError from "@effect-uai/core/MessengerError";
|
|
4
|
+
import * as Multipart from "@effect-uai/core/Multipart";
|
|
5
|
+
//#region src/internal/api.ts
|
|
6
|
+
const provider = "telegram";
|
|
7
|
+
/**
|
|
8
|
+
* A Bot API call that came back `ok: false` or never came back at all.
|
|
9
|
+
* `code` mirrors the HTTP status Telegram puts in `error_code`; each verb
|
|
10
|
+
* maps this onto its own `MessengerError`.
|
|
11
|
+
*/
|
|
12
|
+
var ApiFailure = class extends Data.TaggedError("TelegramApiFailure") {};
|
|
13
|
+
const Envelope = Schema.Struct({
|
|
14
|
+
ok: Schema.Boolean,
|
|
15
|
+
result: Schema.optional(Schema.Unknown),
|
|
16
|
+
description: Schema.optional(Schema.String),
|
|
17
|
+
error_code: Schema.optional(Schema.Number),
|
|
18
|
+
parameters: Schema.optional(Schema.Struct({ retry_after: Schema.optional(Schema.Number) }))
|
|
19
|
+
});
|
|
20
|
+
const endpoint = (cfg, method) => `${cfg.baseUrl ?? "https://api.telegram.org"}/bot${Redacted.value(cfg.token)}/${method}`;
|
|
21
|
+
const transportFailure = (method) => (raw) => new ApiFailure({
|
|
22
|
+
method,
|
|
23
|
+
description: "transport failure",
|
|
24
|
+
raw
|
|
25
|
+
});
|
|
26
|
+
const rejected = (method, envelope) => envelope.parameters?.retry_after !== void 0 ? new MessengerError.MessengerRateLimited({
|
|
27
|
+
provider,
|
|
28
|
+
retryAfter: Duration.seconds(envelope.parameters.retry_after),
|
|
29
|
+
raw: envelope
|
|
30
|
+
}) : new ApiFailure({
|
|
31
|
+
method,
|
|
32
|
+
...envelope.error_code !== void 0 && { code: envelope.error_code },
|
|
33
|
+
description: envelope.description ?? "unknown error",
|
|
34
|
+
raw: envelope
|
|
35
|
+
});
|
|
36
|
+
const send = (method, request) => Effect.gen(function* () {
|
|
37
|
+
const json = yield* (yield* (yield* HttpClient.HttpClient).execute(request).pipe(Effect.mapError(transportFailure(method)))).json.pipe(Effect.mapError(transportFailure(method)));
|
|
38
|
+
const envelope = yield* Schema.decodeUnknownEffect(Envelope)(json).pipe(Effect.mapError(() => new ApiFailure({
|
|
39
|
+
method,
|
|
40
|
+
description: "malformed envelope",
|
|
41
|
+
raw: json
|
|
42
|
+
})));
|
|
43
|
+
return envelope.ok ? envelope.result : yield* rejected(method, envelope);
|
|
44
|
+
});
|
|
45
|
+
/** One JSON-bodied method call. `result` comes back undecoded. */
|
|
46
|
+
const call = (cfg) => (method, params = {}) => send(method, HttpClientRequest.post(endpoint(cfg, method)).pipe(HttpClientRequest.bodyJsonUnsafe(params)));
|
|
47
|
+
const entries = (params, file) => pipe(Object.entries(params), Array.filterMap(([name, value]) => value === void 0 ? Result.failVoid : Result.succeed([name, String(value)])), Array.append([file.field, new File([file.bytes], file.filename, { type: file.mimeType })]));
|
|
48
|
+
const formData = (fields) => fields.reduce((form, [name, value]) => (form.append(name, value), form), new FormData());
|
|
49
|
+
/** One multipart method call: string fields plus a single file part. */
|
|
50
|
+
const upload = (cfg) => (method, params, file) => Effect.gen(function* () {
|
|
51
|
+
const body = yield* Multipart.bodyMultipart(formData(entries(params, file))).pipe(Effect.mapError(transportFailure(method)));
|
|
52
|
+
return yield* send(method, HttpClientRequest.post(endpoint(cfg, method)).pipe(body));
|
|
53
|
+
});
|
|
54
|
+
/** Decode a call's `result`; a shape mismatch is a malformed response, not a defect. */
|
|
55
|
+
const decoded = (method, schema) => (result) => Effect.gen(function* () {
|
|
56
|
+
const raw = yield* result;
|
|
57
|
+
return yield* Schema.decodeUnknownEffect(schema)(raw).pipe(Effect.mapError(() => new ApiFailure({
|
|
58
|
+
method,
|
|
59
|
+
description: "malformed result",
|
|
60
|
+
raw
|
|
61
|
+
})));
|
|
62
|
+
});
|
|
63
|
+
//#endregion
|
|
64
|
+
export { ApiFailure, call, decoded, provider, upload };
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=api.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.mjs","names":["Arr"],"sources":["../../src/internal/api.ts"],"sourcesContent":["import { Array as Arr, Data, Duration, Effect, Redacted, Result, Schema, pipe } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as MessengerError from \"@effect-uai/core/MessengerError\"\nimport * as Multipart from \"@effect-uai/core/Multipart\"\n\nexport const provider = \"telegram\"\n\nexport type Config = {\n readonly token: Redacted.Redacted\n /** Defaults to `https://api.telegram.org`. */\n readonly baseUrl?: string\n}\n\n/**\n * A Bot API call that came back `ok: false` or never came back at all.\n * `code` mirrors the HTTP status Telegram puts in `error_code`; each verb\n * maps this onto its own `MessengerError`.\n */\nexport class ApiFailure extends Data.TaggedError(\"TelegramApiFailure\")<{\n readonly method: string\n readonly code?: number\n readonly description: string\n readonly raw: unknown\n}> {}\n\nexport type ApiError = ApiFailure | MessengerError.MessengerRateLimited\n\nconst Envelope = Schema.Struct({\n ok: Schema.Boolean,\n result: Schema.optional(Schema.Unknown),\n description: Schema.optional(Schema.String),\n error_code: Schema.optional(Schema.Number),\n parameters: Schema.optional(Schema.Struct({ retry_after: Schema.optional(Schema.Number) })),\n})\n\nexport type Params = Readonly<Record<string, unknown>>\n\n/** The file part of a multipart upload. */\nexport type Upload = {\n readonly field: string\n readonly bytes: Uint8Array\n readonly filename: string\n readonly mimeType: string\n}\n\nconst endpoint = (cfg: Config, method: string): string =>\n `${cfg.baseUrl ?? \"https://api.telegram.org\"}/bot${Redacted.value(cfg.token)}/${method}`\n\nconst transportFailure = (method: string) => (raw: unknown) =>\n new ApiFailure({ method, description: \"transport failure\", raw })\n\nconst rejected = (method: string, envelope: typeof Envelope.Type): ApiError =>\n envelope.parameters?.retry_after !== undefined\n ? new MessengerError.MessengerRateLimited({\n provider,\n retryAfter: Duration.seconds(envelope.parameters.retry_after),\n raw: envelope,\n })\n : new ApiFailure({\n method,\n ...(envelope.error_code !== undefined && { code: envelope.error_code }),\n description: envelope.description ?? \"unknown error\",\n raw: envelope,\n })\n\nconst send = (\n method: string,\n request: HttpClientRequest.HttpClientRequest,\n): Effect.Effect<unknown, ApiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const response = yield* client.execute(request).pipe(Effect.mapError(transportFailure(method)))\n const json = yield* response.json.pipe(Effect.mapError(transportFailure(method)))\n const envelope = yield* Schema.decodeUnknownEffect(Envelope)(json).pipe(\n Effect.mapError(\n () => new ApiFailure({ method, description: \"malformed envelope\", raw: json }),\n ),\n )\n return envelope.ok ? envelope.result : yield* rejected(method, envelope)\n })\n\n/** One JSON-bodied method call. `result` comes back undecoded. */\nexport const call =\n (cfg: Config) =>\n (method: string, params: Params = {}): Effect.Effect<unknown, ApiError, HttpClient.HttpClient> =>\n send(\n method,\n HttpClientRequest.post(endpoint(cfg, method)).pipe(HttpClientRequest.bodyJsonUnsafe(params)),\n )\n\ntype Entry = readonly [name: string, value: string | File]\n\n// A `File` carries its own filename, so one entry shape covers both kinds.\nconst entries = (params: Params, file: Upload): ReadonlyArray<Entry> =>\n pipe(\n Object.entries(params),\n Arr.filterMap(([name, value]) =>\n value === undefined ? Result.failVoid : Result.succeed([name, String(value)] as const),\n ),\n Arr.append([\n file.field,\n new File([file.bytes as Uint8Array<ArrayBuffer>], file.filename, { type: file.mimeType }),\n ] as const),\n )\n\nconst formData = (fields: ReadonlyArray<Entry>): FormData =>\n fields.reduce((form, [name, value]) => (form.append(name, value), form), new FormData())\n\n/** One multipart method call: string fields plus a single file part. */\nexport const upload =\n (cfg: Config) =>\n (\n method: string,\n params: Params,\n file: Upload,\n ): Effect.Effect<unknown, ApiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const body = yield* Multipart.bodyMultipart(formData(entries(params, file))).pipe(\n Effect.mapError(transportFailure(method)),\n )\n return yield* send(method, HttpClientRequest.post(endpoint(cfg, method)).pipe(body))\n })\n\n/** Decode a call's `result`; a shape mismatch is a malformed response, not a defect. */\nexport const decoded =\n <A>(method: string, schema: Schema.Decoder<A>) =>\n <E, R>(result: Effect.Effect<unknown, E, R>): Effect.Effect<A, E | ApiFailure, R> =>\n Effect.gen(function* () {\n const raw = yield* result\n return yield* Schema.decodeUnknownEffect(schema)(raw).pipe(\n Effect.mapError(() => new ApiFailure({ method, description: \"malformed result\", raw })),\n )\n })\n"],"mappings":";;;;;AAKA,MAAa,WAAW;;;;;;AAaxB,IAAa,aAAb,cAAgC,KAAK,YAAY,oBAAoB,CAAC,CAKnE,CAAC;AAIJ,MAAM,WAAW,OAAO,OAAO;CAC7B,IAAI,OAAO;CACX,QAAQ,OAAO,SAAS,OAAO,OAAO;CACtC,aAAa,OAAO,SAAS,OAAO,MAAM;CAC1C,YAAY,OAAO,SAAS,OAAO,MAAM;CACzC,YAAY,OAAO,SAAS,OAAO,OAAO,EAAE,aAAa,OAAO,SAAS,OAAO,MAAM,EAAE,CAAC,CAAC;AAC5F,CAAC;AAYD,MAAM,YAAY,KAAa,WAC7B,GAAG,IAAI,WAAW,2BAA2B,MAAM,SAAS,MAAM,IAAI,KAAK,EAAE,GAAG;AAElF,MAAM,oBAAoB,YAAoB,QAC5C,IAAI,WAAW;CAAE;CAAQ,aAAa;CAAqB;AAAI,CAAC;AAElE,MAAM,YAAY,QAAgB,aAChC,SAAS,YAAY,gBAAgB,KAAA,IACjC,IAAI,eAAe,qBAAqB;CACtC;CACA,YAAY,SAAS,QAAQ,SAAS,WAAW,WAAW;CAC5D,KAAK;AACP,CAAC,IACD,IAAI,WAAW;CACb;CACA,GAAI,SAAS,eAAe,KAAA,KAAa,EAAE,MAAM,SAAS,WAAW;CACrE,aAAa,SAAS,eAAe;CACrC,KAAK;AACP,CAAC;AAEP,MAAM,QACJ,QACA,YAEA,OAAO,IAAI,aAAa;CAGtB,MAAM,OAAO,QAAO,QADI,OADF,WAAW,WAAA,CACF,QAAQ,OAAO,CAAC,CAAC,KAAK,OAAO,SAAS,iBAAiB,MAAM,CAAC,CAAC,EAAA,CACjE,KAAK,KAAK,OAAO,SAAS,iBAAiB,MAAM,CAAC,CAAC;CAChF,MAAM,WAAW,OAAO,OAAO,oBAAoB,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,KACjE,OAAO,eACC,IAAI,WAAW;EAAE;EAAQ,aAAa;EAAsB,KAAK;CAAK,CAAC,CAC/E,CACF;CACA,OAAO,SAAS,KAAK,SAAS,SAAS,OAAO,SAAS,QAAQ,QAAQ;AACzE,CAAC;;AAGH,MAAa,QACV,SACA,QAAgB,SAAiB,CAAC,MACjC,KACE,QACA,kBAAkB,KAAK,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,kBAAkB,eAAe,MAAM,CAAC,CAC7F;AAKJ,MAAM,WAAW,QAAgB,SAC/B,KACE,OAAO,QAAQ,MAAM,GACrBA,MAAI,WAAW,CAAC,MAAM,WACpB,UAAU,KAAA,IAAY,OAAO,WAAW,OAAO,QAAQ,CAAC,MAAM,OAAO,KAAK,CAAC,CAAU,CACvF,GACAA,MAAI,OAAO,CACT,KAAK,OACL,IAAI,KAAK,CAAC,KAAK,KAAgC,GAAG,KAAK,UAAU,EAAE,MAAM,KAAK,SAAS,CAAC,CAC1F,CAAU,CACZ;AAEF,MAAM,YAAY,WAChB,OAAO,QAAQ,MAAM,CAAC,MAAM,YAAY,KAAK,OAAO,MAAM,KAAK,GAAG,OAAO,IAAI,SAAS,CAAC;;AAGzF,MAAa,UACV,SAEC,QACA,QACA,SAEA,OAAO,IAAI,aAAa;CACtB,MAAM,OAAO,OAAO,UAAU,cAAc,SAAS,QAAQ,QAAQ,IAAI,CAAC,CAAC,CAAC,CAAC,KAC3E,OAAO,SAAS,iBAAiB,MAAM,CAAC,CAC1C;CACA,OAAO,OAAO,KAAK,QAAQ,kBAAkB,KAAK,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;AACrF,CAAC;;AAGL,MAAa,WACP,QAAgB,YACb,WACL,OAAO,IAAI,aAAa;CACtB,MAAM,MAAM,OAAO;CACnB,OAAO,OAAO,OAAO,oBAAoB,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,KACpD,OAAO,eAAe,IAAI,WAAW;EAAE;EAAQ,aAAa;EAAoB;CAAI,CAAC,CAAC,CACxF;AACF,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as Message, c as Reaction, d as User, f as allowedUpdates, i as Entity, l as ReplyTarget, n as CallbackQuery, o as MessageReaction, p as toEvents, r as Chat, s as Placement, t as BotIdentity, u as Update } from "../events-BkljYfG6.mjs";
|
|
2
|
+
export { BotIdentity, CallbackQuery, Chat, Entity, Message, MessageReaction, Placement, Reaction, ReplyTarget, Update, User, allowedUpdates, toEvents };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Array, Match, Option, Schema } from "effect";
|
|
2
|
+
import { ChannelId, InboundEvent, MessageId, ThreadId, UserId } from "@effect-uai/core/Messenger";
|
|
3
|
+
//#region src/internal/events.ts
|
|
4
|
+
const User = Schema.Struct({
|
|
5
|
+
id: Schema.Number,
|
|
6
|
+
is_bot: Schema.Boolean,
|
|
7
|
+
username: Schema.optional(Schema.String)
|
|
8
|
+
});
|
|
9
|
+
const Chat = Schema.Struct({
|
|
10
|
+
id: Schema.Number,
|
|
11
|
+
type: Schema.String
|
|
12
|
+
});
|
|
13
|
+
const Entity = Schema.Struct({
|
|
14
|
+
type: Schema.String,
|
|
15
|
+
offset: Schema.Number,
|
|
16
|
+
length: Schema.Number
|
|
17
|
+
});
|
|
18
|
+
/** Where a message sits: its chat, and its forum topic when it is in one. */
|
|
19
|
+
const Placement = Schema.Struct({
|
|
20
|
+
chat: Chat,
|
|
21
|
+
message_thread_id: Schema.optional(Schema.Number),
|
|
22
|
+
is_topic_message: Schema.optional(Schema.Boolean)
|
|
23
|
+
});
|
|
24
|
+
const ReplyTarget = Schema.Struct({
|
|
25
|
+
message_id: Schema.Number,
|
|
26
|
+
from: Schema.optional(User)
|
|
27
|
+
});
|
|
28
|
+
const Message = Schema.Struct({
|
|
29
|
+
...Placement.fields,
|
|
30
|
+
message_id: Schema.Number,
|
|
31
|
+
from: Schema.optional(User),
|
|
32
|
+
text: Schema.optional(Schema.String),
|
|
33
|
+
caption: Schema.optional(Schema.String),
|
|
34
|
+
entities: Schema.optional(Schema.Array(Entity)),
|
|
35
|
+
reply_to_message: Schema.optional(ReplyTarget)
|
|
36
|
+
});
|
|
37
|
+
const Reaction = Schema.Struct({
|
|
38
|
+
type: Schema.String,
|
|
39
|
+
emoji: Schema.optional(Schema.String)
|
|
40
|
+
});
|
|
41
|
+
const MessageReaction = Schema.Struct({
|
|
42
|
+
chat: Chat,
|
|
43
|
+
message_id: Schema.Number,
|
|
44
|
+
user: Schema.optional(User),
|
|
45
|
+
new_reaction: Schema.Array(Reaction)
|
|
46
|
+
});
|
|
47
|
+
const CallbackQuery = Schema.Struct({
|
|
48
|
+
id: Schema.String,
|
|
49
|
+
from: User,
|
|
50
|
+
data: Schema.optional(Schema.String),
|
|
51
|
+
message: Schema.optional(Schema.Struct({
|
|
52
|
+
...Placement.fields,
|
|
53
|
+
message_id: Schema.Number
|
|
54
|
+
}))
|
|
55
|
+
});
|
|
56
|
+
const Update = Schema.Struct({
|
|
57
|
+
update_id: Schema.Number,
|
|
58
|
+
message: Schema.optional(Message),
|
|
59
|
+
edited_message: Schema.optional(Message),
|
|
60
|
+
message_reaction: Schema.optional(MessageReaction),
|
|
61
|
+
callback_query: Schema.optional(CallbackQuery)
|
|
62
|
+
});
|
|
63
|
+
/** Requested on every `getUpdates`; without it Telegram never delivers reactions. */
|
|
64
|
+
const allowedUpdates = [
|
|
65
|
+
"message",
|
|
66
|
+
"edited_message",
|
|
67
|
+
"callback_query",
|
|
68
|
+
"message_reaction"
|
|
69
|
+
];
|
|
70
|
+
const conversation = (at) => ({
|
|
71
|
+
channel: ChannelId(String(at.chat.id)),
|
|
72
|
+
...at.is_topic_message === true && at.message_thread_id !== void 0 && { thread: ThreadId(String(at.message_thread_id)) }
|
|
73
|
+
});
|
|
74
|
+
const slice = (text, entity) => text.slice(entity.offset, entity.offset + entity.length);
|
|
75
|
+
const isOwnHandle = (bot, handle) => handle.toLowerCase() === `@${bot.username}`.toLowerCase();
|
|
76
|
+
const entitiesOf = (message) => message.entities ?? [];
|
|
77
|
+
const ownMentions = (bot, message, text) => entitiesOf(message).filter((entity) => entity.type === "mention" && isOwnHandle(bot, slice(text, entity)));
|
|
78
|
+
/** `/name@bot args` at offset 0 is a command; a mid-text `/word` is just text. */
|
|
79
|
+
const command = (bot, message, text) => Array.findFirst(entitiesOf(message), (entity) => entity.type === "bot_command" && entity.offset === 0).pipe(Option.flatMap((entity) => {
|
|
80
|
+
const [name = "", suffix] = slice(text, entity).slice(1).split("@");
|
|
81
|
+
return suffix !== void 0 && !isOwnHandle(bot, `@${suffix}`) ? Option.none() : Option.some({
|
|
82
|
+
name,
|
|
83
|
+
args: text.slice(entity.length).trim()
|
|
84
|
+
});
|
|
85
|
+
}));
|
|
86
|
+
/** The bot's own `@handle` removed, right to left so earlier offsets stay valid. */
|
|
87
|
+
const withoutMention = (bot, message, text) => Array.reverse(ownMentions(bot, message, text)).reduce((acc, entity) => acc.slice(0, entity.offset) + acc.slice(entity.offset + entity.length), text).trim();
|
|
88
|
+
const isAddressed = (bot, message, text) => message.chat.type === "private" || ownMentions(bot, message, text).length > 0 || message.reply_to_message?.from?.id === bot.id;
|
|
89
|
+
const fromMessage = (bot, message, raw) => Option.map(Option.fromNullishOr(message.from), (from) => {
|
|
90
|
+
const text = message.text ?? message.caption ?? "";
|
|
91
|
+
const author = UserId(String(from.id));
|
|
92
|
+
const at = conversation(message);
|
|
93
|
+
return Option.match(command(bot, message, text), {
|
|
94
|
+
onSome: ({ name, args }) => InboundEvent.Command({
|
|
95
|
+
conversation: at,
|
|
96
|
+
name,
|
|
97
|
+
args,
|
|
98
|
+
author,
|
|
99
|
+
raw
|
|
100
|
+
}),
|
|
101
|
+
onNone: () => InboundEvent.Message({
|
|
102
|
+
conversation: at,
|
|
103
|
+
id: MessageId(String(message.message_id)),
|
|
104
|
+
author,
|
|
105
|
+
text: withoutMention(bot, message, text),
|
|
106
|
+
addressed: isAddressed(bot, message, text),
|
|
107
|
+
...message.reply_to_message !== void 0 && { replyTo: MessageId(String(message.reply_to_message.message_id)) },
|
|
108
|
+
raw
|
|
109
|
+
})
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
const fromReaction = (reaction, raw) => Option.match(Option.fromNullishOr(reaction.user), {
|
|
113
|
+
onNone: () => [],
|
|
114
|
+
onSome: (user) => Array.flatMap(reaction.new_reaction, (r) => Array.fromNullishOr(r.emoji).map((emoji) => InboundEvent.Reaction({
|
|
115
|
+
conversation: conversation({ chat: reaction.chat }),
|
|
116
|
+
message: MessageId(String(reaction.message_id)),
|
|
117
|
+
emoji,
|
|
118
|
+
author: UserId(String(user.id)),
|
|
119
|
+
raw
|
|
120
|
+
})))
|
|
121
|
+
});
|
|
122
|
+
const fromCallback = (query, raw) => Option.map(Option.fromNullishOr(query.message), (message) => InboundEvent.Action({
|
|
123
|
+
conversation: conversation(message),
|
|
124
|
+
actionId: query.data ?? "",
|
|
125
|
+
author: UserId(String(query.from.id)),
|
|
126
|
+
raw
|
|
127
|
+
}));
|
|
128
|
+
/**
|
|
129
|
+
* Everything one `Update` says to the agent. Edited messages are received
|
|
130
|
+
* (so the offset advances past them) but not re-delivered: a turn already
|
|
131
|
+
* answered them.
|
|
132
|
+
*/
|
|
133
|
+
const toEvents = (bot) => (update) => Match.value(update).pipe(Match.when({ message: Match.defined }, ({ message }) => Array.fromOption(fromMessage(bot, message, update))), Match.when({ message_reaction: Match.defined }, ({ message_reaction }) => fromReaction(message_reaction, update)), Match.when({ callback_query: Match.defined }, ({ callback_query }) => Array.fromOption(fromCallback(callback_query, update))), Match.orElse(() => []));
|
|
134
|
+
//#endregion
|
|
135
|
+
export { CallbackQuery, Chat, Entity, Message, MessageReaction, Placement, Reaction, ReplyTarget, Update, User, allowedUpdates, toEvents };
|
|
136
|
+
|
|
137
|
+
//# sourceMappingURL=events.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.mjs","names":["Arr"],"sources":["../../src/internal/events.ts"],"sourcesContent":["import { Array as Arr, Match, Option, Schema } from \"effect\"\nimport {\n ChannelId,\n type ConversationRef,\n InboundEvent,\n MessageId,\n ThreadId,\n UserId,\n} from \"@effect-uai/core/Messenger\"\n\n// ---------------------------------------------------------------------------\n// Wire shapes: the fields this adapter reads. The rest survives on `raw`.\n// ---------------------------------------------------------------------------\n\nexport const User = Schema.Struct({\n id: Schema.Number,\n is_bot: Schema.Boolean,\n username: Schema.optional(Schema.String),\n})\nexport type User = typeof User.Type\n\nexport const Chat = Schema.Struct({\n id: Schema.Number,\n type: Schema.String,\n})\nexport type Chat = typeof Chat.Type\n\nexport const Entity = Schema.Struct({\n type: Schema.String,\n offset: Schema.Number,\n length: Schema.Number,\n})\nexport type Entity = typeof Entity.Type\n\n/** Where a message sits: its chat, and its forum topic when it is in one. */\nexport const Placement = Schema.Struct({\n chat: Chat,\n message_thread_id: Schema.optional(Schema.Number),\n is_topic_message: Schema.optional(Schema.Boolean),\n})\nexport type Placement = typeof Placement.Type\n\nexport const ReplyTarget = Schema.Struct({\n message_id: Schema.Number,\n from: Schema.optional(User),\n})\nexport type ReplyTarget = typeof ReplyTarget.Type\n\nexport const Message = Schema.Struct({\n ...Placement.fields,\n message_id: Schema.Number,\n from: Schema.optional(User),\n text: Schema.optional(Schema.String),\n caption: Schema.optional(Schema.String),\n entities: Schema.optional(Schema.Array(Entity)),\n reply_to_message: Schema.optional(ReplyTarget),\n})\nexport type Message = typeof Message.Type\n\nexport const Reaction = Schema.Struct({\n type: Schema.String,\n emoji: Schema.optional(Schema.String),\n})\nexport type Reaction = typeof Reaction.Type\n\nexport const MessageReaction = Schema.Struct({\n chat: Chat,\n message_id: Schema.Number,\n user: Schema.optional(User),\n new_reaction: Schema.Array(Reaction),\n})\nexport type MessageReaction = typeof MessageReaction.Type\n\nexport const CallbackQuery = Schema.Struct({\n id: Schema.String,\n from: User,\n data: Schema.optional(Schema.String),\n message: Schema.optional(Schema.Struct({ ...Placement.fields, message_id: Schema.Number })),\n})\nexport type CallbackQuery = typeof CallbackQuery.Type\n\nexport const Update = Schema.Struct({\n update_id: Schema.Number,\n message: Schema.optional(Message),\n edited_message: Schema.optional(Message),\n message_reaction: Schema.optional(MessageReaction),\n callback_query: Schema.optional(CallbackQuery),\n})\nexport type Update = typeof Update.Type\n\n/** Requested on every `getUpdates`; without it Telegram never delivers reactions. */\nexport const allowedUpdates = [\n \"message\",\n \"edited_message\",\n \"callback_query\",\n \"message_reaction\",\n] as const\n\nexport type BotIdentity = {\n readonly id: number\n readonly username: string\n}\n\n// ---------------------------------------------------------------------------\n// Mapping\n// ---------------------------------------------------------------------------\n\nconst conversation = (at: Placement): ConversationRef => ({\n channel: ChannelId(String(at.chat.id)),\n // Forum topics are threads; reply chains are not (they carry no thread id).\n ...(at.is_topic_message === true &&\n at.message_thread_id !== undefined && { thread: ThreadId(String(at.message_thread_id)) }),\n})\n\nconst slice = (text: string, entity: Entity): string =>\n text.slice(entity.offset, entity.offset + entity.length)\n\nconst isOwnHandle = (bot: BotIdentity, handle: string): boolean =>\n handle.toLowerCase() === `@${bot.username}`.toLowerCase()\n\nconst entitiesOf = (message: Message): ReadonlyArray<Entity> => message.entities ?? []\n\nconst ownMentions = (bot: BotIdentity, message: Message, text: string): ReadonlyArray<Entity> =>\n entitiesOf(message).filter(\n (entity) => entity.type === \"mention\" && isOwnHandle(bot, slice(text, entity)),\n )\n\ntype Command = { readonly name: string; readonly args: string }\n\n/** `/name@bot args` at offset 0 is a command; a mid-text `/word` is just text. */\nconst command = (bot: BotIdentity, message: Message, text: string): Option.Option<Command> =>\n Arr.findFirst(\n entitiesOf(message),\n (entity) => entity.type === \"bot_command\" && entity.offset === 0,\n ).pipe(\n Option.flatMap((entity) => {\n const [name = \"\", suffix] = slice(text, entity).slice(1).split(\"@\")\n // A command aimed at another bot arrives under privacy-off, but is not ours.\n return suffix !== undefined && !isOwnHandle(bot, `@${suffix}`)\n ? Option.none()\n : Option.some({ name, args: text.slice(entity.length).trim() })\n }),\n )\n\n/** The bot's own `@handle` removed, right to left so earlier offsets stay valid. */\nconst withoutMention = (bot: BotIdentity, message: Message, text: string): string =>\n Arr.reverse(ownMentions(bot, message, text))\n .reduce(\n (acc, entity) => acc.slice(0, entity.offset) + acc.slice(entity.offset + entity.length),\n text,\n )\n .trim()\n\nconst isAddressed = (bot: BotIdentity, message: Message, text: string): boolean =>\n message.chat.type === \"private\" ||\n ownMentions(bot, message, text).length > 0 ||\n message.reply_to_message?.from?.id === bot.id\n\nconst fromMessage = (\n bot: BotIdentity,\n message: Message,\n raw: unknown,\n): Option.Option<InboundEvent> =>\n Option.map(Option.fromNullishOr(message.from), (from) => {\n const text = message.text ?? message.caption ?? \"\"\n const author = UserId(String(from.id))\n const at = conversation(message)\n return Option.match(command(bot, message, text), {\n onSome: ({ name, args }) =>\n InboundEvent.Command({ conversation: at, name, args, author, raw }),\n onNone: () =>\n InboundEvent.Message({\n conversation: at,\n id: MessageId(String(message.message_id)),\n author,\n text: withoutMention(bot, message, text),\n addressed: isAddressed(bot, message, text),\n ...(message.reply_to_message !== undefined && {\n replyTo: MessageId(String(message.reply_to_message.message_id)),\n }),\n raw,\n }),\n })\n })\n\nconst fromReaction = (reaction: MessageReaction, raw: unknown): ReadonlyArray<InboundEvent> =>\n Option.match(Option.fromNullishOr(reaction.user), {\n onNone: () => [],\n onSome: (user) =>\n Arr.flatMap(reaction.new_reaction, (r) =>\n Arr.fromNullishOr(r.emoji).map((emoji) =>\n InboundEvent.Reaction({\n conversation: conversation({ chat: reaction.chat }),\n message: MessageId(String(reaction.message_id)),\n emoji,\n author: UserId(String(user.id)),\n raw,\n }),\n ),\n ),\n })\n\nconst fromCallback = (query: CallbackQuery, raw: unknown): Option.Option<InboundEvent> =>\n Option.map(Option.fromNullishOr(query.message), (message) =>\n InboundEvent.Action({\n conversation: conversation(message),\n actionId: query.data ?? \"\",\n author: UserId(String(query.from.id)),\n raw,\n }),\n )\n\n/**\n * Everything one `Update` says to the agent. Edited messages are received\n * (so the offset advances past them) but not re-delivered: a turn already\n * answered them.\n */\nexport const toEvents =\n (bot: BotIdentity) =>\n (update: Update): ReadonlyArray<InboundEvent> =>\n Match.value(update).pipe(\n Match.when({ message: Match.defined }, ({ message }) =>\n Arr.fromOption(fromMessage(bot, message, update)),\n ),\n Match.when({ message_reaction: Match.defined }, ({ message_reaction }) =>\n fromReaction(message_reaction, update),\n ),\n Match.when({ callback_query: Match.defined }, ({ callback_query }) =>\n Arr.fromOption(fromCallback(callback_query, update)),\n ),\n Match.orElse(() => []),\n )\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|