@grom.js/effect-tg 0.8.0 → 0.9.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.
Files changed (99) hide show
  1. package/README.md +162 -5
  2. package/dist/BotApi.d.ts +10 -12
  3. package/dist/BotApi.d.ts.map +1 -1
  4. package/dist/BotApi.js +2 -2
  5. package/dist/BotApi.js.map +1 -1
  6. package/dist/BotApiError.d.ts +46 -71
  7. package/dist/BotApiError.d.ts.map +1 -1
  8. package/dist/BotApiError.js +59 -41
  9. package/dist/BotApiError.js.map +1 -1
  10. package/dist/BotApiTransport.d.ts +10 -17
  11. package/dist/BotApiTransport.d.ts.map +1 -1
  12. package/dist/BotApiTransport.js +2 -7
  13. package/dist/BotApiTransport.js.map +1 -1
  14. package/dist/BotApiUrl.d.ts +6 -8
  15. package/dist/BotApiUrl.d.ts.map +1 -1
  16. package/dist/BotApiUrl.js.map +1 -1
  17. package/dist/Content.d.ts.map +1 -1
  18. package/dist/Dialog.d.ts +96 -41
  19. package/dist/Dialog.d.ts.map +1 -1
  20. package/dist/Dialog.js +60 -25
  21. package/dist/Dialog.js.map +1 -1
  22. package/dist/File.d.ts +1 -2
  23. package/dist/File.d.ts.map +1 -1
  24. package/dist/File.js +1 -1
  25. package/dist/File.js.map +1 -1
  26. package/dist/Markup.d.ts.map +1 -1
  27. package/dist/Runner.d.ts +1 -1
  28. package/dist/Runner.d.ts.map +1 -1
  29. package/dist/Send.d.ts +57 -130
  30. package/dist/Send.d.ts.map +1 -1
  31. package/dist/Send.js +65 -128
  32. package/dist/Send.js.map +1 -1
  33. package/dist/Text.d.ts.map +1 -1
  34. package/dist/internal/botApi.d.ts +3 -1
  35. package/dist/internal/botApi.d.ts.map +1 -1
  36. package/dist/internal/botApi.gen.d.ts +6501 -0
  37. package/dist/internal/botApi.gen.d.ts.map +1 -0
  38. package/dist/internal/botApi.gen.js +2 -0
  39. package/dist/internal/botApi.gen.js.map +1 -0
  40. package/dist/internal/botApi.js +2 -6
  41. package/dist/internal/botApi.js.map +1 -1
  42. package/dist/internal/botApiError.d.ts +5 -2
  43. package/dist/internal/botApiError.d.ts.map +1 -1
  44. package/dist/internal/botApiError.js +8 -18
  45. package/dist/internal/botApiError.js.map +1 -1
  46. package/dist/internal/botApiTransport.d.ts +5 -2
  47. package/dist/internal/botApiTransport.d.ts.map +1 -1
  48. package/dist/internal/botApiTransport.js +14 -10
  49. package/dist/internal/botApiTransport.js.map +1 -1
  50. package/dist/internal/dialog.d.ts +27 -10
  51. package/dist/internal/dialog.d.ts.map +1 -1
  52. package/dist/internal/dialog.js +88 -14
  53. package/dist/internal/dialog.js.map +1 -1
  54. package/dist/internal/file.d.ts +1 -1
  55. package/dist/internal/file.d.ts.map +1 -1
  56. package/dist/internal/file.js +1 -1
  57. package/dist/internal/file.js.map +1 -1
  58. package/dist/internal/runner.d.ts +4 -5
  59. package/dist/internal/runner.d.ts.map +1 -1
  60. package/dist/internal/runner.js +14 -19
  61. package/dist/internal/runner.js.map +1 -1
  62. package/dist/internal/send.d.ts +4 -4
  63. package/dist/internal/send.d.ts.map +1 -1
  64. package/dist/internal/send.js +62 -51
  65. package/dist/internal/send.js.map +1 -1
  66. package/package.json +11 -9
  67. package/src/BotApi.ts +38 -31
  68. package/src/BotApiError.ts +109 -63
  69. package/src/BotApiTransport.ts +18 -20
  70. package/src/BotApiUrl.ts +6 -8
  71. package/src/Content.ts +14 -14
  72. package/src/Dialog.ts +164 -42
  73. package/src/File.ts +3 -4
  74. package/src/Markup.ts +5 -5
  75. package/src/Send.ts +114 -202
  76. package/src/Text.ts +5 -5
  77. package/src/internal/botApi.gen.ts +6783 -0
  78. package/src/internal/botApi.ts +7 -11
  79. package/src/internal/botApiError.ts +15 -20
  80. package/src/internal/botApiTransport.ts +25 -17
  81. package/src/internal/dialog.ts +109 -26
  82. package/src/internal/file.ts +1 -1
  83. package/src/internal/runner.ts +34 -38
  84. package/src/internal/send.ts +161 -132
  85. package/dist/internal/botApiMethods.gen.d.ts +0 -2110
  86. package/dist/internal/botApiMethods.gen.d.ts.map +0 -1
  87. package/dist/internal/botApiMethods.gen.js +0 -2
  88. package/dist/internal/botApiMethods.gen.js.map +0 -1
  89. package/dist/internal/botApiShape.gen.d.ts +0 -406
  90. package/dist/internal/botApiShape.gen.d.ts.map +0 -1
  91. package/dist/internal/botApiShape.gen.js +0 -2
  92. package/dist/internal/botApiShape.gen.js.map +0 -1
  93. package/dist/internal/botApiTypes.gen.d.ts +0 -3986
  94. package/dist/internal/botApiTypes.gen.d.ts.map +0 -1
  95. package/dist/internal/botApiTypes.gen.js +0 -2
  96. package/dist/internal/botApiTypes.gen.js.map +0 -1
  97. package/src/internal/botApiMethods.gen.ts +0 -2111
  98. package/src/internal/botApiShape.gen.ts +0 -406
  99. package/src/internal/botApiTypes.gen.ts +0 -4264
@@ -3,9 +3,11 @@ import type * as BotApiTransport from '../BotApiTransport.ts'
3
3
  import * as Effect from 'effect/Effect'
4
4
  import * as BotApiError from '../BotApiError.ts'
5
5
 
6
- export const make = (
7
- transport: BotApiTransport.BotApiTransport.Service,
8
- ): BotApi.BotApi.Service => (
6
+ export const make = ({
7
+ transport,
8
+ }: {
9
+ transport: BotApiTransport.Service
10
+ }): BotApi.Service => (
9
11
  new Proxy({}, {
10
12
  get: (_target, prop) => {
11
13
  if (typeof prop !== 'string') {
@@ -18,15 +20,9 @@ export const make = (
18
20
  if (response.ok) {
19
21
  return response.result
20
22
  }
21
- return yield* Effect.fail(
22
- new BotApiError.BotApiError({
23
- code: response.error_code,
24
- description: response.description,
25
- parameters: response.parameters,
26
- }),
27
- )
23
+ return yield* Effect.fail(BotApiError.fromResponse(response))
28
24
  },
29
25
  )
30
26
  },
31
- }) as BotApi.BotApi.Service
27
+ }) as BotApi.Service
32
28
  )
@@ -1,36 +1,31 @@
1
- import * as Duration from 'effect/Duration'
2
- import * as BotApiError from '../BotApiError.ts'
1
+ import type { MethodFailureReason } from '../BotApiError.ts'
3
2
 
4
- export const narrow = (
5
- error: BotApiError.BotApiError,
6
- ): BotApiError.KnownError | BotApiError.BotApiError => {
7
- const code = error.code
8
- const msg = error.message.toLowerCase()
9
- const params = error.parameters
10
- if (code === 429 && params?.retry_after != null) {
11
- return new BotApiError.TooManyRequests({
12
- cause: error,
13
- retryAfter: Duration.seconds(params.retry_after),
14
- })
15
- }
3
+ export const guessReason = ({
4
+ code,
5
+ description,
6
+ }: {
7
+ code: number
8
+ description: string
9
+ }): MethodFailureReason => {
10
+ const msg = description.toLowerCase()
16
11
  if (code === 403) {
17
12
  if (msg.includes('bot was blocked by the user')) {
18
- return new BotApiError.BotBlockedByUser({ cause: error })
13
+ return 'BotBlockedByUser'
19
14
  }
20
15
  }
21
16
  if (code === 400) {
22
17
  if (msg.includes('message is not modified')) {
23
- return new BotApiError.MessageNotModified({ cause: error })
18
+ return 'MessageNotModified'
24
19
  }
25
20
  if (msg.includes('reply markup too long')) {
26
- return new BotApiError.ReplyMarkupTooLong({ cause: error })
21
+ return 'ReplyMarkupTooLong'
27
22
  }
28
23
  if (msg.includes('query is too old') && msg.includes('query id is invalid')) {
29
- return new BotApiError.QueryIdInvalid({ cause: error })
24
+ return 'QueryIdInvalid'
30
25
  }
31
26
  if (msg.includes('can\'t use the media of the specified type in the album')) {
32
- return new BotApiError.MediaGroupedInvalid({ cause: error })
27
+ return 'MediaGroupedInvalid'
33
28
  }
34
29
  }
35
- return error
30
+ return 'Unknown'
36
31
  }
@@ -1,10 +1,11 @@
1
1
  import type * as HttpClient from '@effect/platform/HttpClient'
2
+ import type * as BotApiTransport from '../BotApiTransport.ts'
2
3
  import type * as BotApiUrl from '../BotApiUrl.ts'
3
4
  import * as HttpBody from '@effect/platform/HttpBody'
4
5
  import * as Chunk from 'effect/Chunk'
5
6
  import * as Effect from 'effect/Effect'
6
7
  import * as Stream from 'effect/Stream'
7
- import * as BotApiTransport from '../BotApiTransport.ts'
8
+ import * as BotApiError from '../BotApiError.ts'
8
9
  import * as File from '../File.ts'
9
10
 
10
11
  interface ExtractedFile {
@@ -13,13 +14,16 @@ interface ExtractedFile {
13
14
  }
14
15
 
15
16
  /**
16
- * Recursively checks whether a value contains {@linkcode File.InputFile} instances.
17
+ * Recursively checks whether a value contains
18
+ * {@linkcode File.InputFile InputFile} instances.
17
19
  */
18
20
  const hasInputFile = (value: unknown): boolean => {
19
- if (value instanceof File.InputFile)
21
+ if (value instanceof File.InputFile) {
20
22
  return true
21
- if (Array.isArray(value))
23
+ }
24
+ if (Array.isArray(value)) {
22
25
  return value.some(hasInputFile)
26
+ }
23
27
  if (typeof value === 'object' && value !== null) {
24
28
  return Object.values(value).some(hasInputFile)
25
29
  }
@@ -49,8 +53,9 @@ const cloneAndExtract = (
49
53
  }
50
54
 
51
55
  /**
52
- * Deep clones params, replacing {@linkcode InputFile} instances with
53
- * `attach://{id}` strings and collecting them into the files array.
56
+ * Clones parameters deeply, replacing {@linkcode File.InputFile InputFile}
57
+ * instances with `attach://{id}` strings and collecting them into
58
+ * the files array.
54
59
  */
55
60
  const extractFiles = (params: unknown): {
56
61
  params: Record<string, unknown>
@@ -81,8 +86,9 @@ const makeHttpBody = Effect.fnUntraced(function* (params: unknown) {
81
86
  const { params: processedParams, files } = extractFiles(params)
82
87
  const formData = new FormData()
83
88
  for (const [key, value] of Object.entries(processedParams)) {
84
- if (value == null)
89
+ if (value == null) {
85
90
  continue
91
+ }
86
92
  const serialized = typeof value === 'string' ? value : JSON.stringify(value)
87
93
  formData.append(key, serialized)
88
94
  }
@@ -97,10 +103,13 @@ const makeHttpBody = Effect.fnUntraced(function* (params: unknown) {
97
103
  return HttpBody.formData(formData)
98
104
  })
99
105
 
100
- export const make = (
101
- httpClient: HttpClient.HttpClient,
102
- botApiUrl: BotApiUrl.BotApiUrl.Service,
103
- ): BotApiTransport.BotApiTransport.Service => ({
106
+ export const make = ({
107
+ httpClient,
108
+ botApiUrl,
109
+ }: {
110
+ httpClient: HttpClient.HttpClient
111
+ botApiUrl: BotApiUrl.Service
112
+ }): BotApiTransport.Service => ({
104
113
  sendRequest: (method, params) => (
105
114
  Effect.gen(function* () {
106
115
  const body = yield* makeHttpBody(params)
@@ -108,11 +117,10 @@ export const make = (
108
117
  const responseJson = yield* response.json
109
118
  // We trust Bot API and don't want to introduce overhead with validation.
110
119
  return responseJson as BotApiTransport.BotApiResponse
111
- })
112
- .pipe(
113
- Effect.catchAll(cause => (
114
- Effect.fail(new BotApiTransport.BotApiTransportError({ cause }))
115
- )),
116
- )
120
+ }).pipe(
121
+ Effect.catchAll(cause => (
122
+ Effect.fail(new BotApiError.TransportError({ cause }))
123
+ )),
124
+ )
117
125
  ),
118
126
  })
@@ -1,33 +1,116 @@
1
- export function PeerId<Tag extends string>({
2
- tag,
3
- isValid,
4
- toDialogId,
5
- }: {
6
- tag: Tag
7
- isValid: (peerId: number) => boolean
8
- toDialogId: (peerId: number) => number
9
- }): {
10
- new (id: number): {
11
- readonly _tag: Tag
12
- readonly id: number
13
- readonly dialogId: number
1
+ import type * as BotApi from '../BotApi.ts'
2
+ import * as Option from 'effect/Option'
3
+ import * as Dialog from '../Dialog.ts'
4
+
5
+ // =============================================================================
6
+ // Dialog ID <-> Peer ID
7
+ // =============================================================================
8
+
9
+ export const decodeDialogId = (dialogId: number): Option.Option<
10
+ | { peer: 'user', id: Dialog.UserId }
11
+ | { peer: 'group', id: Dialog.GroupId }
12
+ | { peer: 'supergroup', id: Dialog.SupergroupId }
13
+ | { peer: 'monoforum', id: number }
14
+ | { peer: 'secret-chat', id: number }
15
+ > => {
16
+ if (Number.isSafeInteger(dialogId)) {
17
+ if (1 <= dialogId && dialogId <= 0xFFFFFFFFFF) {
18
+ return Option.some({ peer: 'user', id: +dialogId as Dialog.UserId })
19
+ }
20
+ if (-999999999999 <= dialogId && dialogId <= -1) {
21
+ return Option.some({ peer: 'group', id: -dialogId as Dialog.GroupId })
22
+ }
23
+ if (-1997852516352 <= dialogId && dialogId <= -1000000000001) {
24
+ return Option.some({ peer: 'supergroup', id: -(dialogId + 1000000000000) as Dialog.SupergroupId })
25
+ }
26
+ if (-4000000000000 <= dialogId && dialogId <= -2002147483649) {
27
+ return Option.some({ peer: 'monoforum', id: -(dialogId + 1000000000000) })
28
+ }
29
+ if (-2002147483648 <= dialogId && dialogId <= -1997852516353) {
30
+ return Option.some({ peer: 'secret-chat', id: dialogId + 2000000000000 })
31
+ }
32
+ }
33
+ return Option.none()
34
+ }
35
+
36
+ export const decodePeerId: {
37
+ (peer: 'user', dialogId: number): Option.Option<Dialog.UserId>
38
+ (peer: 'group', dialogId: number): Option.Option<Dialog.GroupId>
39
+ (peer: 'supergroup', dialogId: number): Option.Option<Dialog.SupergroupId>
40
+ (peer: 'monoforum', dialogId: number): Option.Option<number>
41
+ (peer: 'secret-chat', dialogId: number): Option.Option<number>
42
+ } = (peer, dialogId) => {
43
+ const decoded = decodeDialogId(dialogId)
44
+ if (Option.isSome(decoded) && decoded.value.peer === peer) {
45
+ return Option.some(decoded.value.id as any)
46
+ }
47
+ return Option.none()
48
+ }
49
+
50
+ export const encodePeerId = (
51
+ peer: 'user' | 'group' | 'supergroup' | 'monoforum' | 'secret-chat',
52
+ id: number,
53
+ ): Option.Option<Dialog.DialogId> => {
54
+ if (Number.isSafeInteger(id)) {
55
+ if (peer === 'user' && 1 <= id && id <= 0xFFFFFFFFFF) {
56
+ return Option.some(id as Dialog.DialogId)
57
+ }
58
+ if (peer === 'group' && 1 <= id && id <= 999999999999) {
59
+ return Option.some(-id as Dialog.DialogId)
60
+ }
61
+ if (peer === 'supergroup' && 1 <= id && id <= 997852516352) {
62
+ return Option.some(-(id + 1000000000000) as Dialog.DialogId)
63
+ }
64
+ if (peer === 'monoforum' && 1002147483649 <= id && id <= 3000000000000) {
65
+ return Option.some(-(id + 1000000000000) as Dialog.DialogId)
66
+ }
67
+ if (peer === 'secret-chat' && -2147483648 <= id && id <= 2147483647) {
68
+ return Option.some((id - 2000000000000) as Dialog.DialogId)
69
+ }
14
70
  }
15
- } {
16
- class Base {
17
- public readonly _tag = tag
18
- public readonly id: number
19
- public readonly dialogId: number
71
+ return Option.none()
72
+ }
73
+
74
+ // =============================================================================
75
+ // Constructors
76
+ // =============================================================================
20
77
 
21
- constructor(id: number) {
22
- if (!Number.isSafeInteger(id)) {
23
- throw new TypeError(`invalid integer: ${id}`)
78
+ export const ofMessage: (
79
+ message: BotApi.Types.Message,
80
+ ) => Dialog.Dialog = (m) => {
81
+ // TODO: Remove type assertion when bot-api-spec updates types.
82
+ switch (m.chat.type as 'private' | 'group' | 'supergroup' | 'channel') {
83
+ case 'private': {
84
+ const user = new Dialog.User({
85
+ id: Option.getOrThrow(decodePeerId('user', m.chat.id)),
86
+ })
87
+ if (m.message_thread_id != null) {
88
+ return user.topic(m.message_thread_id)
89
+ }
90
+ return user
91
+ }
92
+ case 'group': {
93
+ return new Dialog.Group({
94
+ id: Option.getOrThrow(decodePeerId('group', m.chat.id)),
95
+ })
96
+ }
97
+ case 'supergroup': {
98
+ const supergroup = new Dialog.Supergroup({
99
+ id: Option.getOrThrow(decodePeerId('supergroup', m.chat.id)),
100
+ })
101
+ if (m.message_thread_id != null) {
102
+ return supergroup.topic(m.message_thread_id)
24
103
  }
25
- if (!isValid(id)) {
26
- throw new Error(`invalid peer id: ${id}`)
104
+ return supergroup
105
+ }
106
+ case 'channel': {
107
+ const channel = new Dialog.Channel({
108
+ id: Option.getOrThrow(decodePeerId('supergroup', m.chat.id)),
109
+ })
110
+ if (m.direct_messages_topic != null) {
111
+ return channel.directMessages(m.direct_messages_topic.topic_id)
27
112
  }
28
- this.id = id
29
- this.dialogId = toDialogId(id)
113
+ return channel
30
114
  }
31
115
  }
32
- return Base
33
116
  }
@@ -4,7 +4,7 @@ import * as Effect from 'effect/Effect'
4
4
  import * as BotApi from '../BotApi.ts'
5
5
  import * as BotApiUrl from '../BotApiUrl.ts'
6
6
 
7
- export const get = Effect.fnUntraced(
7
+ export const download = Effect.fnUntraced(
8
8
  function* (fileId: FileId) {
9
9
  const file = yield* BotApi.callMethod('getFile', { file_id: fileId })
10
10
  if (file.file_path == null) {
@@ -1,54 +1,50 @@
1
- import type { BotApiError } from '../BotApiError.ts'
2
- import type { BotApiTransportError } from '../BotApiTransport.ts'
3
- import type { Runner } from '../Runner.ts'
1
+ import type * as BotApiError from '../BotApiError.ts'
2
+ import type * as Runner from '../Runner.ts'
4
3
  import * as Duration from 'effect/Duration'
5
4
  import * as Effect from 'effect/Effect'
6
5
  import * as Match from 'effect/Match'
7
6
  import * as Schedule from 'effect/Schedule'
8
- import { Update } from '../Bot.ts'
9
- import { BotApi } from '../BotApi.ts'
7
+ import * as Bot from '../Bot.ts'
8
+ import * as BotApi from '../BotApi.ts'
10
9
 
11
10
  export const makeSimple = (options?: {
12
11
  allowedUpdates?: string[]
13
- }): Runner<BotApiError | BotApiTransportError, BotApi> => ({
12
+ }): Runner.Runner<BotApiError.BotApiError, BotApi.BotApi> => ({
14
13
  run: Effect.fnUntraced(
15
14
  function* (bot) {
16
15
  const { allowedUpdates } = options ?? {}
17
- const api = yield* BotApi
16
+ const api = yield* BotApi.BotApi
18
17
  let lastUpdateId: undefined | number
19
18
  while (true) {
20
- const [update] = yield* api
21
- .getUpdates({
22
- offset: lastUpdateId == null ? undefined : lastUpdateId + 1,
23
- allowed_updates: allowedUpdates,
24
- timeout: 30,
25
- limit: 1,
26
- })
27
- .pipe(
28
- Effect.retry({
29
- schedule: Schedule.spaced(Duration.seconds(3)),
30
- while: error => Match.value(error).pipe(
31
- Match.tagsExhaustive({
32
- '@grom.js/effect-tg/BotApiError': error => Effect.succeed(
33
- error.code >= 500 || (
34
- error.code !== 401
35
- && error.code !== 403
36
- && error.code !== 404
37
- ),
38
- ),
39
- '@grom.js/effect-tg/BotApiTransport/BotApiTransportError': () => Effect.succeed(true),
40
- }),
41
- ),
42
- }),
43
- )
44
- if (update) {
45
- yield* Effect
46
- .provideService(bot, Update, update)
47
- .pipe(
48
- Effect.catchAll(error => (
49
- Effect.logError('Error in bot:', error)
19
+ const updates = yield* api.getUpdates({
20
+ offset: lastUpdateId == null ? undefined : lastUpdateId + 1,
21
+ allowed_updates: allowedUpdates,
22
+ timeout: 30,
23
+ limit: 1,
24
+ }).pipe(
25
+ Effect.retry({
26
+ schedule: Schedule.exponential(Duration.millis(100)),
27
+ while: e => Match.value(e).pipe(
28
+ Match.tag('InternalServerError', () => true),
29
+ Match.tag('TransportError', e => Match.value(e.cause).pipe(
30
+ Match.tag('RequestError', e => e.reason === 'Transport'),
31
+ Match.orElse(() => false),
50
32
  )),
51
- )
33
+ Match.orElse(() => false),
34
+ ),
35
+ }),
36
+ Effect.catchTag('RateLimited', e => Effect.gen(function* () {
37
+ yield* Effect.sleep(e.retryAfter)
38
+ return []
39
+ })),
40
+ )
41
+ if (updates.length > 0) {
42
+ const update = updates[0]!
43
+ yield* Effect.provideService(bot, Bot.Update, update).pipe(
44
+ Effect.catchAll(error => (
45
+ Effect.logError('Error in bot:', error)
46
+ )),
47
+ )
52
48
  lastUpdateId = update.update_id
53
49
  }
54
50
  }