@grom.js/effect-tg 0.13.0 → 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.
Files changed (85) hide show
  1. package/README.md +11 -12
  2. package/dist/Bot.d.ts +2 -3
  3. package/dist/Bot.d.ts.map +1 -1
  4. package/dist/Bot.js +1 -2
  5. package/dist/Bot.js.map +1 -1
  6. package/dist/BotApi.d.ts +6 -6
  7. package/dist/BotApi.d.ts.map +1 -1
  8. package/dist/BotApi.js +1 -2
  9. package/dist/BotApi.js.map +1 -1
  10. package/dist/BotApiError.d.ts +15 -19
  11. package/dist/BotApiError.d.ts.map +1 -1
  12. package/dist/BotApiError.js +10 -12
  13. package/dist/BotApiError.js.map +1 -1
  14. package/dist/BotApiTransport.d.ts +5 -8
  15. package/dist/BotApiTransport.d.ts.map +1 -1
  16. package/dist/BotApiTransport.js +1 -2
  17. package/dist/BotApiTransport.js.map +1 -1
  18. package/dist/BotApiUrl.d.ts +6 -9
  19. package/dist/BotApiUrl.d.ts.map +1 -1
  20. package/dist/BotApiUrl.js +1 -2
  21. package/dist/BotApiUrl.js.map +1 -1
  22. package/dist/Content.d.ts +89 -155
  23. package/dist/Content.d.ts.map +1 -1
  24. package/dist/Content.js +26 -119
  25. package/dist/Content.js.map +1 -1
  26. package/dist/Dialog.d.ts +28 -58
  27. package/dist/Dialog.d.ts.map +1 -1
  28. package/dist/Dialog.js +29 -48
  29. package/dist/Dialog.js.map +1 -1
  30. package/dist/File.d.ts +11 -6
  31. package/dist/File.d.ts.map +1 -1
  32. package/dist/File.js +19 -3
  33. package/dist/File.js.map +1 -1
  34. package/dist/Markup.d.ts +8 -21
  35. package/dist/Markup.d.ts.map +1 -1
  36. package/dist/Markup.js +12 -13
  37. package/dist/Markup.js.map +1 -1
  38. package/dist/Runner.d.ts +3 -1
  39. package/dist/Runner.d.ts.map +1 -1
  40. package/dist/Runner.js.map +1 -1
  41. package/dist/Send.d.ts +14 -14
  42. package/dist/Send.d.ts.map +1 -1
  43. package/dist/Send.js +6 -13
  44. package/dist/Send.js.map +1 -1
  45. package/dist/Text.d.ts +13 -26
  46. package/dist/Text.d.ts.map +1 -1
  47. package/dist/Text.js +4 -13
  48. package/dist/Text.js.map +1 -1
  49. package/dist/internal/botApi.d.ts +3 -3
  50. package/dist/internal/botApi.d.ts.map +1 -1
  51. package/dist/internal/botApi.js +2 -1
  52. package/dist/internal/botApi.js.map +1 -1
  53. package/dist/internal/botApiTransport.d.ts +2 -2
  54. package/dist/internal/botApiTransport.d.ts.map +1 -1
  55. package/dist/internal/botApiTransport.js +3 -4
  56. package/dist/internal/botApiTransport.js.map +1 -1
  57. package/dist/internal/dialog.d.ts.map +1 -1
  58. package/dist/internal/dialog.js +8 -16
  59. package/dist/internal/dialog.js.map +1 -1
  60. package/dist/internal/file.d.ts +5 -2
  61. package/dist/internal/file.d.ts.map +1 -1
  62. package/dist/internal/file.js +3 -0
  63. package/dist/internal/file.js.map +1 -1
  64. package/dist/internal/send.d.ts +6 -4
  65. package/dist/internal/send.d.ts.map +1 -1
  66. package/dist/internal/send.js +18 -17
  67. package/dist/internal/send.js.map +1 -1
  68. package/package.json +1 -1
  69. package/src/Bot.ts +3 -4
  70. package/src/BotApi.ts +7 -8
  71. package/src/BotApiError.ts +19 -25
  72. package/src/BotApiTransport.ts +6 -9
  73. package/src/BotApiUrl.ts +7 -10
  74. package/src/Content.ts +128 -130
  75. package/src/Dialog.ts +79 -73
  76. package/src/File.ts +36 -5
  77. package/src/Markup.ts +37 -27
  78. package/src/Runner.ts +5 -1
  79. package/src/Send.ts +27 -26
  80. package/src/Text.ts +21 -18
  81. package/src/internal/botApi.ts +5 -6
  82. package/src/internal/botApiTransport.ts +5 -6
  83. package/src/internal/dialog.ts +8 -16
  84. package/src/internal/file.ts +5 -2
  85. package/src/internal/send.ts +23 -23
package/src/Dialog.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type * as BotApi from './BotApi.ts'
2
2
  import * as Brand from 'effect/Brand'
3
- import * as Data from 'effect/Data'
3
+ import * as Match from 'effect/Match'
4
4
  import * as Option from 'effect/Option'
5
5
  import * as internal from './internal/dialog.ts'
6
6
 
@@ -14,20 +14,23 @@ export type Dialog =
14
14
  | ForumTopic
15
15
  | ChannelDm
16
16
 
17
- export class PrivateTopic extends Data.TaggedClass('PrivateTopic')<{
18
- user: User
19
- topicId: number
20
- }> {}
17
+ export interface PrivateTopic {
18
+ readonly _tag: 'PrivateTopic'
19
+ readonly user: User
20
+ readonly topicId: number
21
+ }
21
22
 
22
- export class ForumTopic extends Data.TaggedClass('ForumTopic')<{
23
- supergroup: Supergroup
24
- topicId: number
25
- }> {}
23
+ export interface ForumTopic {
24
+ readonly _tag: 'ForumTopic'
25
+ readonly supergroup: Supergroup
26
+ readonly topicId: number
27
+ }
26
28
 
27
- export class ChannelDm extends Data.TaggedClass('ChannelDm')<{
28
- channel: Channel
29
- topicId: number
30
- }> {}
29
+ export interface ChannelDm {
30
+ readonly _tag: 'ChannelDm'
31
+ readonly channel: Channel
32
+ readonly topicId: number
33
+ }
31
34
 
32
35
  // =============================================================================
33
36
  // Peer
@@ -39,49 +42,38 @@ export type Peer =
39
42
  | Channel
40
43
  | Supergroup
41
44
 
42
- export class User extends Data.TaggedClass('User')<{
43
- id: UserId
44
- }> {
45
- public dialogId(): DialogId {
46
- return Option.getOrThrow(internal.encodePeerId('user', this.id))
47
- }
48
-
49
- public topic(topicId: number): PrivateTopic {
50
- return new PrivateTopic({ user: this, topicId })
51
- }
45
+ export interface User {
46
+ readonly _tag: 'User'
47
+ readonly id: UserId
52
48
  }
53
49
 
54
- export class Group extends Data.TaggedClass('Group')<{
55
- id: GroupId
56
- }> {
57
- public dialogId(): DialogId {
58
- return Option.getOrThrow(internal.encodePeerId('group', this.id))
59
- }
50
+ export interface Group {
51
+ readonly _tag: 'Group'
52
+ readonly id: GroupId
60
53
  }
61
54
 
62
- export class Channel extends Data.TaggedClass('Channel')<{
63
- id: ChannelId
64
- }> {
65
- public dialogId(): DialogId {
66
- return Option.getOrThrow(internal.encodePeerId('channel', this.id))
67
- }
55
+ export interface Channel {
56
+ readonly _tag: 'Channel'
57
+ readonly id: ChannelId
58
+ }
68
59
 
69
- public directMessages(topicId: number): ChannelDm {
70
- return new ChannelDm({ channel: this, topicId })
71
- }
60
+ export interface Supergroup {
61
+ readonly _tag: 'Supergroup'
62
+ readonly id: SupergroupId
72
63
  }
73
64
 
74
- export class Supergroup extends Data.TaggedClass('Supergroup')<{
75
- id: SupergroupId
76
- }> {
77
- public dialogId(): DialogId {
78
- return Option.getOrThrow(internal.encodePeerId('channel', this.id))
79
- }
65
+ // =============================================================================
66
+ // Peer Functions
67
+ // =============================================================================
80
68
 
81
- public topic(topicId: number): ForumTopic {
82
- return new ForumTopic({ supergroup: this, topicId })
83
- }
84
- }
69
+ export const dialogId: (peer: Peer) => DialogId = Match.type<Peer>().pipe(
70
+ Match.tagsExhaustive({
71
+ User: u => Option.getOrThrow(internal.encodePeerId('user', u.id)),
72
+ Group: g => Option.getOrThrow(internal.encodePeerId('group', g.id)),
73
+ Channel: c => Option.getOrThrow(internal.encodePeerId('channel', c.id)),
74
+ Supergroup: s => Option.getOrThrow(internal.encodePeerId('channel', s.id)),
75
+ }),
76
+ )
85
77
 
86
78
  // =============================================================================
87
79
  // Brands
@@ -93,30 +85,25 @@ export class Supergroup extends Data.TaggedClass('Supergroup')<{
93
85
  * @see {@link https://core.telegram.org/api/bots/ids Telegram API • Bot API dialog IDs}
94
86
  */
95
87
  export type DialogId = number & Brand.Brand<'@grom.js/effect-tg/DialogId'>
96
- export const DialogId = Brand.refined<DialogId>(
88
+ export const DialogId: Brand.Brand.Constructor<DialogId> = Brand.refined<DialogId>(
97
89
  n => Option.isSome(internal.decodeDialogId(n)),
98
90
  n => Brand.error(`Invalid dialog ID: ${n}`),
99
91
  )
100
92
 
101
93
  export type UserId = number & Brand.Brand<'@grom.js/effect-tg/UserId'>
102
- export const UserId = Brand.refined<UserId>(
94
+ export const UserId: Brand.Brand.Constructor<UserId> = Brand.refined<UserId>(
103
95
  n => Option.isSome(internal.encodePeerId('user', n)),
104
96
  n => Brand.error(`Invalid user ID: ${n}`),
105
97
  )
106
98
 
107
99
  export type GroupId = number & Brand.Brand<'@grom.js/effect-tg/GroupId'>
108
- export const GroupId = Brand.refined<GroupId>(
100
+ export const GroupId: Brand.Brand.Constructor<GroupId> = Brand.refined<GroupId>(
109
101
  n => Option.isSome(internal.encodePeerId('group', n)),
110
102
  n => Brand.error(`Invalid group ID: ${n}`),
111
103
  )
112
104
 
113
- /**
114
- * ID for channels (including supergroups).
115
- *
116
- * @see {@link https://core.telegram.org/api/bots/ids Telegram API • Bot API dialog IDs}
117
- */
118
105
  export type ChannelId = number & Brand.Brand<'@grom.js/effect-tg/ChannelId'>
119
- export const ChannelId = Brand.refined<ChannelId>(
106
+ export const ChannelId: Brand.Brand.Constructor<ChannelId> = Brand.refined<ChannelId>(
120
107
  n => Option.isSome(internal.encodePeerId('channel', n)),
121
108
  n => Brand.error(`Invalid channel or supergroup ID: ${n}`),
122
109
  )
@@ -125,7 +112,7 @@ export const ChannelId = Brand.refined<ChannelId>(
125
112
  export type SupergroupId = ChannelId
126
113
 
127
114
  /** @alias ChannelId */
128
- export const SupergroupId = ChannelId
115
+ export const SupergroupId: Brand.Brand.Constructor<ChannelId> = ChannelId
129
116
 
130
117
  // =============================================================================
131
118
  // Dialog ID <-> Peer ID
@@ -156,21 +143,40 @@ export const encodePeerId: (
156
143
  // Constructors
157
144
  // =============================================================================
158
145
 
159
- export const user: (id: number) => User = (id) => {
160
- return new User({ id: UserId(id) })
161
- }
162
-
163
- export const group: (id: number) => Group = (id) => {
164
- return new Group({ id: GroupId(id) })
165
- }
166
-
167
- export const channel: (id: number) => Channel = (id) => {
168
- return new Channel({ id: ChannelId(id) })
169
- }
170
-
171
- export const supergroup: (id: number) => Supergroup = (id) => {
172
- return new Supergroup({ id: ChannelId(id) })
173
- }
146
+ export const user: (id: number) => User = id => ({ _tag: 'User', id: UserId(id) })
147
+
148
+ export const group: (id: number) => Group = id => ({ _tag: 'Group', id: GroupId(id) })
149
+
150
+ export const channel: (id: number) => Channel = id => ({ _tag: 'Channel', id: ChannelId(id) })
151
+
152
+ export const supergroup: (id: number) => Supergroup = id => ({ _tag: 'Supergroup', id: ChannelId(id) })
153
+
154
+ export const privateTopic: (
155
+ user: User,
156
+ topicId: number,
157
+ ) => PrivateTopic = (user, topicId) => ({
158
+ _tag: 'PrivateTopic',
159
+ user,
160
+ topicId,
161
+ })
162
+
163
+ export const forumTopic: (
164
+ supergroup: Supergroup,
165
+ topicId: number,
166
+ ) => ForumTopic = (supergroup, topicId) => ({
167
+ _tag: 'ForumTopic',
168
+ supergroup,
169
+ topicId,
170
+ })
171
+
172
+ export const channelDm: (
173
+ channel: Channel,
174
+ topicId: number,
175
+ ) => ChannelDm = (channel, topicId) => ({
176
+ _tag: 'ChannelDm',
177
+ channel,
178
+ topicId,
179
+ })
174
180
 
175
181
  export const ofMessage: (
176
182
  message: BotApi.Types.Message,
package/src/File.ts CHANGED
@@ -7,20 +7,51 @@ import type * as BotApi from './BotApi.ts'
7
7
  import type * as BotApiError from './BotApiError.ts'
8
8
  import type * as BotApiUrl from './BotApiUrl.ts'
9
9
  import * as Brand from 'effect/Brand'
10
- import * as Data from 'effect/Data'
10
+ import * as Predicate from 'effect/Predicate'
11
11
  import * as internal from './internal/file.ts'
12
12
 
13
13
  export type FileId = string & Brand.Brand<'FileId'>
14
- export const FileId = Brand.nominal<FileId>()
14
+ export const FileId: Brand.Brand.Constructor<FileId> = Brand.nominal<FileId>()
15
15
 
16
16
  export type External = URL & Brand.Brand<'External'>
17
- export const External = Brand.nominal<External>()
17
+ export const External: Brand.Brand.Constructor<External> = Brand.nominal<External>()
18
18
 
19
- export class InputFile extends Data.TaggedClass('InputFile')<{
19
+ // =============================================================================
20
+ // InputFile
21
+ // =============================================================================
22
+
23
+ const InputFileTypeId: unique symbol = Symbol.for('effect-tg/InputFile')
24
+
25
+ export type InputFileTypeId = typeof InputFileTypeId
26
+
27
+ export interface InputFile {
28
+ readonly [InputFileTypeId]: InputFileTypeId
29
+ readonly stream: Stream.Stream<Uint8Array>
30
+ readonly filename: string
31
+ readonly mimeType?: string
32
+ }
33
+
34
+ const InputFileProto = {
35
+ [InputFileTypeId]: InputFileTypeId,
36
+ }
37
+
38
+ export const make: (args: {
20
39
  stream: Stream.Stream<Uint8Array>
21
40
  filename: string
22
41
  mimeType?: string
23
- }> {}
42
+ }) => InputFile = ({ stream, filename, mimeType }) => {
43
+ const file = Object.create(InputFileProto)
44
+ file.stream = stream
45
+ file.filename = filename
46
+ file.mimeType = mimeType
47
+ return file
48
+ }
49
+
50
+ export const isInputFile = (u: unknown): u is InputFile => Predicate.hasProperty(u, InputFileTypeId)
51
+
52
+ // =============================================================================
53
+ // Utilities
54
+ // =============================================================================
24
55
 
25
56
  /**
26
57
  * Downloads a file from the Bot API server.
package/src/Markup.ts CHANGED
@@ -1,4 +1,3 @@
1
- import * as Data from 'effect/Data'
2
1
  import * as Option from 'effect/Option'
3
2
 
4
3
  // =============================================================================
@@ -14,27 +13,31 @@ export type Markup =
14
13
  | ReplyKeyboardRemove
15
14
  | ForceReply
16
15
 
17
- export class InlineKeyboard extends Data.TaggedClass('InlineKeyboard')<{
16
+ export interface InlineKeyboard {
17
+ readonly _tag: 'InlineKeyboard'
18
18
  readonly rows: ReadonlyArray<ReadonlyArray<InlineButton>>
19
- }> {}
19
+ }
20
20
 
21
- export class ReplyKeyboard extends Data.TaggedClass('ReplyKeyboard')<{
21
+ export interface ReplyKeyboard {
22
+ readonly _tag: 'ReplyKeyboard'
22
23
  readonly rows: ReadonlyArray<ReadonlyArray<ReplyButton>>
23
24
  readonly persistent: boolean
24
25
  readonly resizable: boolean
25
26
  readonly oneTime: boolean
26
27
  readonly selective: boolean
27
28
  readonly inputPlaceholder: Option.Option<string>
28
- }> {}
29
+ }
29
30
 
30
- export class ReplyKeyboardRemove extends Data.TaggedClass('ReplyKeyboardRemove')<{
31
+ export interface ReplyKeyboardRemove {
32
+ readonly _tag: 'ReplyKeyboardRemove'
31
33
  readonly selective: boolean
32
- }> {}
34
+ }
33
35
 
34
- export class ForceReply extends Data.TaggedClass('ForceReply')<{
36
+ export interface ForceReply {
37
+ readonly _tag: 'ForceReply'
35
38
  readonly selective: boolean
36
39
  readonly inputPlaceholder: Option.Option<string>
37
- }> {}
40
+ }
38
41
 
39
42
  // =============================================================================
40
43
  // Constructors
@@ -42,7 +45,10 @@ export class ForceReply extends Data.TaggedClass('ForceReply')<{
42
45
 
43
46
  export const inlineKeyboard = (
44
47
  rows: ReadonlyArray<ReadonlyArray<InlineButton>>,
45
- ): InlineKeyboard => new InlineKeyboard({ rows })
48
+ ): InlineKeyboard => ({
49
+ _tag: 'InlineKeyboard',
50
+ rows,
51
+ })
46
52
 
47
53
  export const replyKeyboard = (
48
54
  rows: ReadonlyArray<ReadonlyArray<ReplyButton>>,
@@ -53,27 +59,31 @@ export const replyKeyboard = (
53
59
  readonly selective?: boolean
54
60
  readonly inputPlaceholder?: string
55
61
  },
56
- ): ReplyKeyboard =>
57
- new ReplyKeyboard({
58
- rows,
59
- persistent: options?.persistent ?? false,
60
- resizable: options?.resizable ?? false,
61
- oneTime: options?.oneTime ?? false,
62
- selective: options?.selective ?? false,
63
- inputPlaceholder: Option.fromNullable(options?.inputPlaceholder),
64
- })
65
-
66
- export const replyKeyboardRemove = (options?: { readonly selective?: boolean }): ReplyKeyboardRemove =>
67
- new ReplyKeyboardRemove({ selective: options?.selective ?? false })
62
+ ): ReplyKeyboard => ({
63
+ _tag: 'ReplyKeyboard',
64
+ rows,
65
+ persistent: options?.persistent ?? false,
66
+ resizable: options?.resizable ?? false,
67
+ oneTime: options?.oneTime ?? false,
68
+ selective: options?.selective ?? false,
69
+ inputPlaceholder: Option.fromNullable(options?.inputPlaceholder),
70
+ })
71
+
72
+ export const replyKeyboardRemove = (options?: {
73
+ readonly selective?: boolean
74
+ }): ReplyKeyboardRemove => ({
75
+ _tag: 'ReplyKeyboardRemove',
76
+ selective: options?.selective ?? false,
77
+ })
68
78
 
69
79
  export const forceReply = (options?: {
70
80
  readonly selective?: boolean
71
81
  readonly inputPlaceholder?: string
72
- }): ForceReply =>
73
- new ForceReply({
74
- selective: options?.selective ?? false,
75
- inputPlaceholder: Option.fromNullable(options?.inputPlaceholder),
76
- })
82
+ }): ForceReply => ({
83
+ _tag: 'ForceReply',
84
+ selective: options?.selective ?? false,
85
+ inputPlaceholder: Option.fromNullable(options?.inputPlaceholder),
86
+ })
77
87
 
78
88
  // =============================================================================
79
89
  // Inline button
package/src/Runner.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import type * as Effect from 'effect/Effect'
2
2
  import type * as Bot from './Bot.ts'
3
+ import type * as BotApi from './BotApi.ts'
4
+ import type * as BotApiError from './BotApiError.ts'
3
5
  import * as internal from './internal/runner.ts'
4
6
 
5
7
  /**
@@ -19,4 +21,6 @@ export interface Runner<E = never, R = never> {
19
21
  * Creates a simple runner that fetches updates by calling `BotApi.getUpdates`
20
22
  * method and handles them one by one.
21
23
  */
22
- export const makeSimple = internal.makeSimple
24
+ export const makeSimple: (options?: {
25
+ allowedUpdates?: string[]
26
+ }) => Runner<BotApiError.BotApiError, BotApi.BotApi> = internal.makeSimple
package/src/Send.ts CHANGED
@@ -36,6 +36,32 @@ export const sendMessage: (params: {
36
36
  BotApi.BotApi
37
37
  > = internal.sendMessage
38
38
 
39
+ // =============================================================================
40
+ // TargetDialog
41
+ // =============================================================================
42
+
43
+ /**
44
+ * Target dialog for sending messages.
45
+ */
46
+ export interface TargetDialog {
47
+ readonly dialog: Dialog.Dialog | Dialog.DialogId
48
+ }
49
+
50
+ export const TargetDialog: Context.Tag<TargetDialog, TargetDialog> = Context.GenericTag<TargetDialog>('@grom.js/effect-tg/Send/TargetDialog')
51
+
52
+ /**
53
+ * Provides the target dialog for sending messages.
54
+ */
55
+ export const to: {
56
+ (dialog: Dialog.Dialog | Dialog.DialogId): <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, TargetDialog>>
57
+ <A, E, R>(effect: Effect.Effect<A, E, R>, dialog: Dialog.Dialog | Dialog.DialogId): Effect.Effect<A, E, Exclude<R, TargetDialog>>
58
+ } = Function.dual(2, <A, E, R>(
59
+ effect: Effect.Effect<A, E, R>,
60
+ dialog: Dialog.Dialog | Dialog.DialogId,
61
+ ): Effect.Effect<A, E, Exclude<R, TargetDialog>> => (
62
+ Effect.provideService(effect, TargetDialog, { dialog })
63
+ ))
64
+
39
65
  // =============================================================================
40
66
  // MessageToSend
41
67
  // =============================================================================
@@ -71,7 +97,7 @@ const MessageToSendProto = {
71
97
  commit(this: MessageToSend) {
72
98
  return Effect.flatMap(
73
99
  TargetDialog,
74
- dialog => sendMessage({
100
+ ({ dialog }) => sendMessage({
75
101
  dialog,
76
102
  content: this.content,
77
103
  markup: this.markup,
@@ -109,31 +135,6 @@ export const message = (content: Content.Content, params?: {
109
135
  return self
110
136
  }
111
137
 
112
- // =============================================================================
113
- // TargetDialog
114
- // =============================================================================
115
-
116
- /**
117
- * Target dialog for sending messages.
118
- */
119
- export class TargetDialog extends Context.Tag('@grom.js/effect-tg/Send/TargetDialog')<
120
- TargetDialog,
121
- Dialog.Dialog | Dialog.DialogId
122
- >() {}
123
-
124
- /**
125
- * Provides the target dialog for sending messages.
126
- */
127
- export const to: {
128
- (dialog: Dialog.Dialog): <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, TargetDialog>>
129
- <A, E, R>(effect: Effect.Effect<A, E, R>, dialog: Dialog.Dialog): Effect.Effect<A, E, Exclude<R, TargetDialog>>
130
- } = Function.dual(2, <A, E, R>(
131
- effect: Effect.Effect<A, E, R>,
132
- dialog: Dialog.Dialog,
133
- ): Effect.Effect<A, E, Exclude<R, TargetDialog>> => (
134
- Effect.provideService(effect, TargetDialog, dialog)
135
- ))
136
-
137
138
  // =============================================================================
138
139
  // Reply Markup
139
140
  // =============================================================================
package/src/Text.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import type { TgxElement } from '@grom.js/tgx'
2
2
  import type { Types } from './BotApi.ts'
3
- import * as Data from 'effect/Data'
4
3
 
5
4
  /**
6
5
  * Formatted text.
@@ -11,32 +10,36 @@ export type Text =
11
10
  | Markdown
12
11
  | Tgx
13
12
 
14
- export class Plain extends Data.TaggedClass('Plain')<{
15
- text: string
16
- entities?: Array<Types.MessageEntity>
17
- }> {}
13
+ export interface Plain {
14
+ readonly _tag: 'Plain'
15
+ readonly text: string
16
+ readonly entities?: Array<Types.MessageEntity>
17
+ }
18
18
 
19
- export class Html extends Data.TaggedClass('Html')<{
20
- html: string
21
- }> {}
19
+ export interface Html {
20
+ readonly _tag: 'Html'
21
+ readonly html: string
22
+ }
22
23
 
23
- export class Markdown extends Data.TaggedClass('Markdown')<{
24
- markdown: string
25
- }> {}
24
+ export interface Markdown {
25
+ readonly _tag: 'Markdown'
26
+ readonly markdown: string
27
+ }
26
28
 
27
- export class Tgx extends Data.TaggedClass('Tgx')<{
28
- tgx: TgxElement
29
- }> {}
29
+ export interface Tgx {
30
+ readonly _tag: 'Tgx'
31
+ readonly tgx: TgxElement
32
+ }
30
33
 
31
34
  // ———— Constructors ———————————————————————————————————————————————————————————
32
35
 
33
36
  export const plain = (
34
37
  text: string,
35
38
  entities?: Array<Types.MessageEntity>,
36
- ): Plain => new Plain({ text, entities })
39
+ ): Plain => ({ _tag: 'Plain', text, entities })
37
40
 
38
- export const html = (html: string): Html => new Html({ html })
41
+ export const html = (html: string): Html => ({ _tag: 'Html', html })
39
42
 
40
- export const markdown = (markdown: string): Markdown => new Markdown({ markdown })
43
+ export const markdown = (markdown: string): Markdown => ({ _tag: 'Markdown', markdown })
41
44
 
42
- export const tgx = (tgx: TgxElement): Tgx => new Tgx({ tgx })
45
+ export const tgx = (tgx: TgxElement): Tgx => ({ _tag: 'Tgx', tgx })
@@ -3,17 +3,16 @@ 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,
8
- }: {
9
- transport: BotApiTransport.Service
10
- }): BotApi.Service => (
6
+ export const make = ({ transport }: {
7
+ transport: BotApiTransport.BotApiTransport
8
+ }): BotApi.BotApi => (
11
9
  new Proxy({}, {
12
10
  get: (_target, prop) => {
13
11
  if (typeof prop !== 'string') {
14
12
  return
15
13
  }
16
14
  const method = prop
15
+ // TODO: Shouldn't we cache effects not to create them on each call?
17
16
  return Effect.fnUntraced(
18
17
  function* (params: void | Record<string, unknown> = {}) {
19
18
  const response = yield* transport.sendRequest(method, params)
@@ -24,5 +23,5 @@ export const make = ({
24
23
  },
25
24
  )
26
25
  },
27
- }) as BotApi.Service
26
+ }) as BotApi.BotApi
28
27
  )
@@ -18,7 +18,7 @@ interface ExtractedFile {
18
18
  * {@linkcode File.InputFile InputFile} instances.
19
19
  */
20
20
  const hasInputFile = (value: unknown): boolean => {
21
- if (value instanceof File.InputFile) {
21
+ if (File.isInputFile(value)) {
22
22
  return true
23
23
  }
24
24
  if (Array.isArray(value)) {
@@ -34,7 +34,7 @@ const cloneAndExtract = (
34
34
  value: unknown,
35
35
  files: ExtractedFile[],
36
36
  ): unknown => {
37
- if (value instanceof File.InputFile) {
37
+ if (File.isInputFile(value)) {
38
38
  const attachId = String(files.length + 1)
39
39
  files.push({ attachId, file: value })
40
40
  return `attach://${attachId}`
@@ -108,15 +108,14 @@ export const make = ({
108
108
  botApiUrl,
109
109
  }: {
110
110
  httpClient: HttpClient.HttpClient
111
- botApiUrl: BotApiUrl.Service
112
- }): BotApiTransport.Service => ({
111
+ botApiUrl: BotApiUrl.BotApiUrl
112
+ }): BotApiTransport.BotApiTransport => ({
113
113
  sendRequest: (method, params) => (
114
114
  Effect.gen(function* () {
115
115
  const body = yield* makeHttpBody(params)
116
116
  const response = yield* httpClient.post(botApiUrl.toMethod(method), { body })
117
- const responseJson = yield* response.json
118
117
  // We trust Bot API and don't want to introduce overhead with validation.
119
- return responseJson as BotApiTransport.BotApiResponse
118
+ return (yield* response.json) as BotApiTransport.BotApiResponse
120
119
  }).pipe(
121
120
  Effect.catchAll(cause => (
122
121
  Effect.fail(new BotApiError.TransportError({ cause }))
@@ -15,7 +15,7 @@ export const decodeDialogId = (dialogId: number): Option.Option<
15
15
  > => {
16
16
  if (Number.isSafeInteger(dialogId)) {
17
17
  if (1 <= dialogId && dialogId <= 0xFFFFFFFFFF) {
18
- return Option.some({ peer: 'user', id: +dialogId as Dialog.UserId })
18
+ return Option.some({ peer: 'user', id: dialogId as Dialog.UserId })
19
19
  }
20
20
  if (-999999999999 <= dialogId && dialogId <= -1) {
21
21
  return Option.some({ peer: 'group', id: -dialogId as Dialog.GroupId })
@@ -80,34 +80,26 @@ export const ofMessage: (
80
80
  ) => Dialog.Dialog = (m) => {
81
81
  switch (m.chat.type) {
82
82
  case 'private': {
83
- const user = new Dialog.User({
84
- id: Option.getOrThrow(decodePeerId('user', m.chat.id)),
85
- })
83
+ const user = Dialog.user(Option.getOrThrow(decodePeerId('user', m.chat.id)))
86
84
  if (m.message_thread_id != null) {
87
- return user.topic(m.message_thread_id)
85
+ return Dialog.privateTopic(user, m.message_thread_id)
88
86
  }
89
87
  return user
90
88
  }
91
89
  case 'group': {
92
- return new Dialog.Group({
93
- id: Option.getOrThrow(decodePeerId('group', m.chat.id)),
94
- })
90
+ return Dialog.group(Option.getOrThrow(decodePeerId('group', m.chat.id)))
95
91
  }
96
92
  case 'channel': {
97
- const channel = new Dialog.Channel({
98
- id: Option.getOrThrow(decodePeerId('channel', m.chat.id)),
99
- })
93
+ const channel = Dialog.channel(Option.getOrThrow(decodePeerId('channel', m.chat.id)))
100
94
  if (m.direct_messages_topic != null) {
101
- return channel.directMessages(m.direct_messages_topic.topic_id)
95
+ return Dialog.channelDm(channel, m.direct_messages_topic.topic_id)
102
96
  }
103
97
  return channel
104
98
  }
105
99
  case 'supergroup': {
106
- const supergroup = new Dialog.Supergroup({
107
- id: Option.getOrThrow(decodePeerId('channel', m.chat.id)),
108
- })
100
+ const supergroup = Dialog.supergroup(Option.getOrThrow(decodePeerId('channel', m.chat.id)))
109
101
  if (m.message_thread_id != null) {
110
- return supergroup.topic(m.message_thread_id)
102
+ return Dialog.forumTopic(supergroup, m.message_thread_id)
111
103
  }
112
104
  return supergroup
113
105
  }
@@ -1,11 +1,14 @@
1
- import type { FileId } from '../File.ts'
1
+ import type * as File from '../File.ts'
2
2
  import * as HttpClient from '@effect/platform/HttpClient'
3
3
  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
+ /**
8
+ * @internal
9
+ */
7
10
  export const download = Effect.fnUntraced(
8
- function* (fileId: FileId) {
11
+ function* (fileId: File.FileId) {
9
12
  const file = yield* BotApi.callMethod('getFile', { file_id: fileId })
10
13
  if (file.file_path == null) {
11
14
  return yield* Effect.die(new Error(`Bot API returned no file path for file "${fileId}".`))