diff --git a/src/Content.ts b/src/Content.ts index 8e5bc7d..161d72f 100644 --- a/src/Content.ts +++ b/src/Content.ts @@ -2,7 +2,6 @@ import type * as Duration from 'effect/Duration' import type * as File from './File.ts' import type * as LinkPreview from './LinkPreview.ts' import type * as Text_ from './Text.ts' -import * as Data from 'effect/Data' import * as Option from 'effect/Option' /** @@ -28,184 +27,171 @@ export type Content = | Sticker /** - * Content of a text message. - * * @see {@link https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1input_message_text.html TDLib • td_api.inputMessageText} * @see {@link https://core.telegram.org/bots/api#sendmessage Bot API • sendMessage} */ -export class Text extends Data.TaggedClass('Text')<{ - text: Text_.Text - linkPreview: Option.Option -}> {} +export interface Text { + readonly _tag: 'Text' + readonly text: Text_.Text + readonly linkPreview: Option.Option +} /** - * Content of a photo message. - * * @see {@link https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1input_message_photo.html TDLib • td_api.inputMessagePhoto} * @see {@link https://core.telegram.org/bots/api#sendphoto Bot API • sendPhoto} */ -export class Photo extends Data.TaggedClass('Photo')<{ - file: File.FileId | File.External | File.InputFile - caption: Option.Option - layout: 'caption-above' | 'caption-below' - spoiler: boolean -}> {} +export interface Photo { + readonly _tag: 'Photo' + readonly file: File.FileId | File.External | File.InputFile + readonly caption: Option.Option + readonly layout: 'caption-above' | 'caption-below' + readonly spoiler: boolean +} /** - * Content of an audio message. - * * @see {@link https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1input_message_audio.html TDLib • td_api.inputMessageAudio} * @see {@link https://core.telegram.org/bots/api#sendaudio Bot API • sendAudio} */ -export class Audio extends Data.TaggedClass('Audio')<{ - file: File.FileId | File.External | File.InputFile - caption: Option.Option - duration: Option.Option - performer: Option.Option - title: Option.Option - thumbnail: Option.Option -}> {} +export interface Audio { + readonly _tag: 'Audio' + readonly file: File.FileId | File.External | File.InputFile + readonly caption: Option.Option + readonly duration: Option.Option + readonly performer: Option.Option + readonly title: Option.Option + readonly thumbnail: Option.Option +} /** - * Content of a document message. - * * @see {@link https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1input_message_document.html TDLib • td_api.inputMessageDocument} * @see {@link https://core.telegram.org/bots/api#senddocument Bot API • sendDocument} */ -export class Document extends Data.TaggedClass('Document')<{ - file: File.FileId | File.External | File.InputFile - caption: Option.Option - thumbnail: Option.Option - contentTypeDetection: boolean -}> {} +export interface Document { + readonly _tag: 'Document' + readonly file: File.FileId | File.External | File.InputFile + readonly caption: Option.Option + readonly thumbnail: Option.Option + readonly contentTypeDetection: boolean +} /** - * Content of a video message. - * * @see {@link https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1input_message_video.html TDLib • td_api.inputMessageVideo} * @see {@link https://core.telegram.org/bots/api#sendvideo Bot API • sendVideo} */ -export class Video extends Data.TaggedClass('Video')<{ - file: File.FileId | File.External | File.InputFile - caption: Option.Option - layout: 'caption-above' | 'caption-below' - spoiler: boolean - duration: Option.Option - width: Option.Option - height: Option.Option - thumbnail: Option.Option - cover: Option.Option - startAt: Option.Option - supportsStreaming: boolean -}> {} +export interface Video { + readonly _tag: 'Video' + readonly file: File.FileId | File.External | File.InputFile + readonly caption: Option.Option + readonly layout: 'caption-above' | 'caption-below' + readonly spoiler: boolean + readonly duration: Option.Option + readonly width: Option.Option + readonly height: Option.Option + readonly thumbnail: Option.Option + readonly cover: Option.Option + readonly startAt: Option.Option + readonly supportsStreaming: boolean +} /** - * Content of an animation message (GIF or video without sound). - * * @see {@link https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1input_message_animation.html TDLib • td_api.inputMessageAnimation} * @see {@link https://core.telegram.org/bots/api#sendanimation Bot API • sendAnimation} */ -export class Animation extends Data.TaggedClass('Animation')<{ - file: File.FileId | File.External | File.InputFile - caption: Option.Option - layout: 'caption-above' | 'caption-below' - spoiler: boolean - duration: Option.Option - width: Option.Option - height: Option.Option - thumbnail: Option.Option -}> {} +export interface Animation { + readonly _tag: 'Animation' + readonly file: File.FileId | File.External | File.InputFile + readonly caption: Option.Option + readonly layout: 'caption-above' | 'caption-below' + readonly spoiler: boolean + readonly duration: Option.Option + readonly width: Option.Option + readonly height: Option.Option + readonly thumbnail: Option.Option +} /** - * Content of a voice note message. - * * @see {@link https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1input_message_voice_note.html TDLib • td_api.inputMessageVoiceNote} * @see {@link https://core.telegram.org/bots/api#sendvoice Bot API • sendVoice} */ -export class Voice extends Data.TaggedClass('Voice')<{ - file: File.FileId | File.External | File.InputFile - caption: Option.Option - duration: Option.Option -}> {} +export interface Voice { + readonly _tag: 'Voice' + readonly file: File.FileId | File.External | File.InputFile + readonly caption: Option.Option + readonly duration: Option.Option +} /** - * Content of a video note message (round video). - * * @see {@link https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1input_message_video_note.html TDLib • td_api.inputMessageVideoNote} * @see {@link https://core.telegram.org/bots/api#sendvideonote Bot API • sendVideoNote} */ -export class VideoNote extends Data.TaggedClass('VideoNote')<{ - file: File.FileId | File.InputFile - duration: Option.Option - diameter: Option.Option - thumbnail: Option.Option -}> {} +export interface VideoNote { + readonly _tag: 'VideoNote' + readonly file: File.FileId | File.InputFile + readonly duration: Option.Option + readonly diameter: Option.Option + readonly thumbnail: Option.Option +} /** - * Content of a location message. - * * @see {@link https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1input_message_location.html TDLib • td_api.inputMessageLocation} * @see {@link https://core.telegram.org/bots/api#sendlocation Bot API • sendLocation} */ -export class Location extends Data.TaggedClass('Location')<{ - latitude: number - longitude: number - uncertaintyRadius: Option.Option - livePeriod: Option.Option - heading: Option.Option - proximityAlertRadius: Option.Option -}> {} +export interface Location { + readonly _tag: 'Location' + readonly latitude: number + readonly longitude: number + readonly uncertaintyRadius: Option.Option + readonly livePeriod: Option.Option + readonly heading: Option.Option + readonly proximityAlertRadius: Option.Option +} /** - * Content of a venue message. - * * @see {@link https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1input_message_venue.html TDLib • td_api.inputMessageVenue} * @see {@link https://core.telegram.org/bots/api#sendvenue Bot API • sendVenue} */ -export class Venue extends Data.TaggedClass('Venue')<{ - latitude: number - longitude: number - title: string - address: string - foursquareId: Option.Option - foursquareType: Option.Option - googlePlaceId: Option.Option - googlePlaceType: Option.Option -}> {} +export interface Venue { + readonly _tag: 'Venue' + readonly latitude: number + readonly longitude: number + readonly title: string + readonly address: string + readonly foursquareId: Option.Option + readonly foursquareType: Option.Option + readonly googlePlaceId: Option.Option + readonly googlePlaceType: Option.Option +} /** - * Content of a contact message. - * * @see {@link https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1input_message_contact.html TDLib • td_api.inputMessageContact} * @see {@link https://core.telegram.org/bots/api#sendcontact Bot API • sendContact} */ -export class Contact extends Data.TaggedClass('Contact')<{ - phoneNumber: string - firstName: string - lastName: Option.Option - vcard: Option.Option -}> {} +export interface Contact { + readonly _tag: 'Contact' + readonly phoneNumber: string + readonly firstName: string + readonly lastName: Option.Option + readonly vcard: Option.Option +} /** - * Content of a dice message. - * * @see {@link https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1input_message_dice.html TDLib • td_api.inputMessageDice} * @see {@link https://core.telegram.org/bots/api#senddice Bot API • sendDice} */ -export class Dice extends Data.TaggedClass('Dice')<{ - emoji: '🎲' | '🎯' | '🏀' | '⚽' | '🎳' | '🎰' -}> {} +export interface Dice { + readonly _tag: 'Dice' + readonly emoji: '🎲' | '🎯' | '🏀' | '⚽' | '🎳' | '🎰' +} /** - * Content of a sticker message. - * * @see {@link https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1input_message_sticker.html TDLib • td_api.inputMessageSticker} * @see {@link https://core.telegram.org/bots/api#sendsticker Bot API • sendSticker} */ -export class Sticker extends Data.TaggedClass('Sticker')<{ - file: File.FileId | File.External | File.InputFile - emoji: Option.Option -}> {} +export interface Sticker { + readonly _tag: 'Sticker' + readonly file: File.FileId | File.External | File.InputFile + readonly emoji: Option.Option +} // ——— Constructors —————————————————————————————————————————————————————————— @@ -214,7 +200,8 @@ export const text = ( options?: { linkPreview?: LinkPreview.LinkPreview }, -): Text => new Text({ +): Text => ({ + _tag: 'Text', text, linkPreview: Option.fromNullable(options?.linkPreview), }) @@ -226,7 +213,8 @@ export const photo = ( layout?: 'caption-above' | 'caption-below' spoiler?: boolean }, -): Photo => new Photo({ +): Photo => ({ + _tag: 'Photo', file, caption: Option.fromNullable(options.caption), layout: options.layout ?? 'caption-below', @@ -242,7 +230,8 @@ export const audio = ( title?: string thumbnail?: File.InputFile }, -): Audio => new Audio({ +): Audio => ({ + _tag: 'Audio', file, caption: Option.fromNullable(options.caption), duration: Option.fromNullable(options.duration), @@ -258,7 +247,8 @@ export const document = ( thumbnail?: File.InputFile contentTypeDetection?: boolean }, -): Document => new Document({ +): Document => ({ + _tag: 'Document', file, caption: Option.fromNullable(options.caption), thumbnail: Option.fromNullable(options.thumbnail), @@ -279,7 +269,8 @@ export const video = ( startAt?: Duration.Duration supportsStreaming?: boolean }, -): Video => new Video({ +): Video => ({ + _tag: 'Video', file, caption: Option.fromNullable(options.caption), layout: options.layout ?? 'caption-below', @@ -304,7 +295,8 @@ export const animation = ( height?: number thumbnail?: File.InputFile }, -): Animation => new Animation({ +): Animation => ({ + _tag: 'Animation', file, caption: Option.fromNullable(options.caption), layout: options.layout ?? 'caption-below', @@ -321,7 +313,8 @@ export const voice = ( caption?: Text_.Text duration?: Duration.Duration }, -): Voice => new Voice({ +): Voice => ({ + _tag: 'Voice', file, caption: Option.fromNullable(options.caption), duration: Option.fromNullable(options.duration), @@ -334,7 +327,8 @@ export const videoNote = ( diameter?: number thumbnail?: File.InputFile }, -): VideoNote => new VideoNote({ +): VideoNote => ({ + _tag: 'VideoNote', file, duration: Option.fromNullable(options.duration), diameter: Option.fromNullable(options.diameter), @@ -345,7 +339,8 @@ export const location = (options: { latitude: number longitude: number uncertaintyRadius?: number -}): Location => new Location({ +}): Location => ({ + _tag: 'Location', latitude: options.latitude, longitude: options.longitude, uncertaintyRadius: Option.fromNullable(options.uncertaintyRadius), @@ -361,7 +356,8 @@ export const liveLocation = (options: { livePeriod: Duration.Duration heading?: number proximityAlertRadius?: number -}): Location => new Location({ +}): Location => ({ + _tag: 'Location', latitude: options.latitude, longitude: options.longitude, uncertaintyRadius: Option.fromNullable(options.uncertaintyRadius), @@ -379,7 +375,8 @@ export const venue = (options: { foursquareType?: string googlePlaceId?: string googlePlaceType?: string -}): Venue => new Venue({ +}): Venue => ({ + _tag: 'Venue', latitude: options.latitude, longitude: options.longitude, title: options.title, @@ -395,16 +392,17 @@ export const contact = (options: { firstName: string lastName?: string vcard?: string -}): Contact => new Contact({ +}): Contact => ({ + _tag: 'Contact', phoneNumber: options.phoneNumber, firstName: options.firstName, lastName: Option.fromNullable(options.lastName), vcard: Option.fromNullable(options.vcard), }) -export const dice = (emoji: Dice['emoji']): Dice => new Dice({ emoji }) +export const dice = (emoji: Dice['emoji']): Dice => ({ _tag: 'Dice', emoji }) export const sticker = ( file: File.FileId | File.External | File.InputFile, emoji?: string, -): Sticker => new Sticker({ file, emoji: Option.fromNullable(emoji) }) +): Sticker => ({ _tag: 'Sticker', file, emoji: Option.fromNullable(emoji) }) diff --git a/src/Dialog.ts b/src/Dialog.ts index 48b3d52..401e97b 100644 --- a/src/Dialog.ts +++ b/src/Dialog.ts @@ -1,6 +1,6 @@ import type * as BotApi from './BotApi.ts' import * as Brand from 'effect/Brand' -import * as Data from 'effect/Data' +import * as Match from 'effect/Match' import * as Option from 'effect/Option' import * as internal from './internal/dialog.ts' @@ -14,20 +14,23 @@ export type Dialog = | ForumTopic | ChannelDm -export class PrivateTopic extends Data.TaggedClass('PrivateTopic')<{ - user: User - topicId: number -}> {} +export interface PrivateTopic { + readonly _tag: 'PrivateTopic' + readonly user: User + readonly topicId: number +} -export class ForumTopic extends Data.TaggedClass('ForumTopic')<{ - supergroup: Supergroup - topicId: number -}> {} +export interface ForumTopic { + readonly _tag: 'ForumTopic' + readonly supergroup: Supergroup + readonly topicId: number +} -export class ChannelDm extends Data.TaggedClass('ChannelDm')<{ - channel: Channel - topicId: number -}> {} +export interface ChannelDm { + readonly _tag: 'ChannelDm' + readonly channel: Channel + readonly topicId: number +} // ============================================================================= // Peer @@ -39,49 +42,38 @@ export type Peer = | Channel | Supergroup -export class User extends Data.TaggedClass('User')<{ - id: UserId -}> { - public dialogId(): DialogId { - return Option.getOrThrow(internal.encodePeerId('user', this.id)) - } - - public topic(topicId: number): PrivateTopic { - return new PrivateTopic({ user: this, topicId }) - } +export interface User { + readonly _tag: 'User' + readonly id: UserId } -export class Group extends Data.TaggedClass('Group')<{ - id: GroupId -}> { - public dialogId(): DialogId { - return Option.getOrThrow(internal.encodePeerId('group', this.id)) - } +export interface Group { + readonly _tag: 'Group' + readonly id: GroupId } -export class Channel extends Data.TaggedClass('Channel')<{ - id: ChannelId -}> { - public dialogId(): DialogId { - return Option.getOrThrow(internal.encodePeerId('channel', this.id)) - } +export interface Channel { + readonly _tag: 'Channel' + readonly id: ChannelId +} - public directMessages(topicId: number): ChannelDm { - return new ChannelDm({ channel: this, topicId }) - } +export interface Supergroup { + readonly _tag: 'Supergroup' + readonly id: SupergroupId } -export class Supergroup extends Data.TaggedClass('Supergroup')<{ - id: SupergroupId -}> { - public dialogId(): DialogId { - return Option.getOrThrow(internal.encodePeerId('channel', this.id)) - } +// ============================================================================= +// Peer Functions +// ============================================================================= - public topic(topicId: number): ForumTopic { - return new ForumTopic({ supergroup: this, topicId }) - } -} +export const dialogId: (peer: Peer) => DialogId = Match.type().pipe( + Match.tagsExhaustive({ + User: u => Option.getOrThrow(internal.encodePeerId('user', u.id)), + Group: g => Option.getOrThrow(internal.encodePeerId('group', g.id)), + Channel: c => Option.getOrThrow(internal.encodePeerId('channel', c.id)), + Supergroup: s => Option.getOrThrow(internal.encodePeerId('channel', s.id)), + }), +) // ============================================================================= // Brands @@ -110,11 +102,6 @@ export const GroupId: Brand.Brand.Constructor = Brand.refined( n => Brand.error(`Invalid group ID: ${n}`), ) -/** - * ID for channels (including supergroups). - * - * @see {@link https://core.telegram.org/api/bots/ids Telegram API • Bot API dialog IDs} - */ export type ChannelId = number & Brand.Brand<'@grom.js/effect-tg/ChannelId'> export const ChannelId: Brand.Brand.Constructor = Brand.refined( n => Option.isSome(internal.encodePeerId('channel', n)), @@ -156,21 +143,40 @@ export const encodePeerId: ( // Constructors // ============================================================================= -export const user: (id: number) => User = (id) => { - return new User({ id: UserId(id) }) -} - -export const group: (id: number) => Group = (id) => { - return new Group({ id: GroupId(id) }) -} - -export const channel: (id: number) => Channel = (id) => { - return new Channel({ id: ChannelId(id) }) -} - -export const supergroup: (id: number) => Supergroup = (id) => { - return new Supergroup({ id: ChannelId(id) }) -} +export const user: (id: number) => User = id => ({ _tag: 'User', id: UserId(id) }) + +export const group: (id: number) => Group = id => ({ _tag: 'Group', id: GroupId(id) }) + +export const channel: (id: number) => Channel = id => ({ _tag: 'Channel', id: ChannelId(id) }) + +export const supergroup: (id: number) => Supergroup = id => ({ _tag: 'Supergroup', id: ChannelId(id) }) + +export const privateTopic: ( + user: User, + topicId: number, +) => PrivateTopic = (user, topicId) => ({ + _tag: 'PrivateTopic', + user, + topicId, +}) + +export const forumTopic: ( + supergroup: Supergroup, + topicId: number, +) => ForumTopic = (supergroup, topicId) => ({ + _tag: 'ForumTopic', + supergroup, + topicId, +}) + +export const channelDm: ( + channel: Channel, + topicId: number, +) => ChannelDm = (channel, topicId) => ({ + _tag: 'ChannelDm', + channel, + topicId, +}) export const ofMessage: ( message: BotApi.Types.Message, diff --git a/src/File.ts b/src/File.ts index 1332757..8b0070b 100644 --- a/src/File.ts +++ b/src/File.ts @@ -7,7 +7,7 @@ import type * as BotApi from './BotApi.ts' import type * as BotApiError from './BotApiError.ts' import type * as BotApiUrl from './BotApiUrl.ts' import * as Brand from 'effect/Brand' -import * as Data from 'effect/Data' +import * as Predicate from 'effect/Predicate' import * as internal from './internal/file.ts' export type FileId = string & Brand.Brand<'FileId'> @@ -16,11 +16,42 @@ export const FileId: Brand.Brand.Constructor = Brand.nominal() export type External = URL & Brand.Brand<'External'> export const External: Brand.Brand.Constructor = Brand.nominal() -export class InputFile extends Data.TaggedClass('InputFile')<{ +// ============================================================================= +// InputFile +// ============================================================================= + +const InputFileTypeId: unique symbol = Symbol.for('effect-tg/InputFile') as InputFileTypeId + +export type InputFileTypeId = typeof InputFileTypeId + +export interface InputFile { + readonly [InputFileTypeId]: InputFileTypeId + readonly stream: Stream.Stream + readonly filename: string + readonly mimeType?: string +} + +const InputFileProto = { + [InputFileTypeId]: InputFileTypeId, +} + +export const make: (args: { stream: Stream.Stream filename: string mimeType?: string -}> {} +}) => InputFile = ({ stream, filename, mimeType }) => { + const file = Object.create(InputFileProto) + file.stream = stream + file.filename = filename + file.mimeType = mimeType + return file +} + +export const isInputFile = (u: unknown): u is InputFile => Predicate.hasProperty(u, InputFileTypeId) + +// ============================================================================= +// Utilities +// ============================================================================= /** * Downloads a file from the Bot API server. diff --git a/src/Markup.ts b/src/Markup.ts index dc04d9a..cc25296 100644 --- a/src/Markup.ts +++ b/src/Markup.ts @@ -1,4 +1,3 @@ -import * as Data from 'effect/Data' import * as Option from 'effect/Option' // ============================================================================= @@ -14,27 +13,31 @@ export type Markup = | ReplyKeyboardRemove | ForceReply -export class InlineKeyboard extends Data.TaggedClass('InlineKeyboard')<{ +export interface InlineKeyboard { + readonly _tag: 'InlineKeyboard' readonly rows: ReadonlyArray> -}> {} +} -export class ReplyKeyboard extends Data.TaggedClass('ReplyKeyboard')<{ +export interface ReplyKeyboard { + readonly _tag: 'ReplyKeyboard' readonly rows: ReadonlyArray> readonly persistent: boolean readonly resizable: boolean readonly oneTime: boolean readonly selective: boolean readonly inputPlaceholder: Option.Option -}> {} +} -export class ReplyKeyboardRemove extends Data.TaggedClass('ReplyKeyboardRemove')<{ +export interface ReplyKeyboardRemove { + readonly _tag: 'ReplyKeyboardRemove' readonly selective: boolean -}> {} +} -export class ForceReply extends Data.TaggedClass('ForceReply')<{ +export interface ForceReply { + readonly _tag: 'ForceReply' readonly selective: boolean readonly inputPlaceholder: Option.Option -}> {} +} // ============================================================================= // Constructors @@ -42,7 +45,10 @@ export class ForceReply extends Data.TaggedClass('ForceReply')<{ export const inlineKeyboard = ( rows: ReadonlyArray>, -): InlineKeyboard => new InlineKeyboard({ rows }) +): InlineKeyboard => ({ + _tag: 'InlineKeyboard', + rows, +}) export const replyKeyboard = ( rows: ReadonlyArray>, @@ -53,27 +59,31 @@ export const replyKeyboard = ( readonly selective?: boolean readonly inputPlaceholder?: string }, -): ReplyKeyboard => - new ReplyKeyboard({ - rows, - persistent: options?.persistent ?? false, - resizable: options?.resizable ?? false, - oneTime: options?.oneTime ?? false, - selective: options?.selective ?? false, - inputPlaceholder: Option.fromNullable(options?.inputPlaceholder), - }) - -export const replyKeyboardRemove = (options?: { readonly selective?: boolean }): ReplyKeyboardRemove => - new ReplyKeyboardRemove({ selective: options?.selective ?? false }) +): ReplyKeyboard => ({ + _tag: 'ReplyKeyboard', + rows, + persistent: options?.persistent ?? false, + resizable: options?.resizable ?? false, + oneTime: options?.oneTime ?? false, + selective: options?.selective ?? false, + inputPlaceholder: Option.fromNullable(options?.inputPlaceholder), +}) + +export const replyKeyboardRemove = (options?: { + readonly selective?: boolean +}): ReplyKeyboardRemove => ({ + _tag: 'ReplyKeyboardRemove', + selective: options?.selective ?? false, +}) export const forceReply = (options?: { readonly selective?: boolean readonly inputPlaceholder?: string -}): ForceReply => - new ForceReply({ - selective: options?.selective ?? false, - inputPlaceholder: Option.fromNullable(options?.inputPlaceholder), - }) +}): ForceReply => ({ + _tag: 'ForceReply', + selective: options?.selective ?? false, + inputPlaceholder: Option.fromNullable(options?.inputPlaceholder), +}) // ============================================================================= // Inline button diff --git a/src/Text.ts b/src/Text.ts index 02a5dd3..3dfe141 100644 --- a/src/Text.ts +++ b/src/Text.ts @@ -1,6 +1,5 @@ import type { TgxElement } from '@grom.js/tgx' import type { Types } from './BotApi.ts' -import * as Data from 'effect/Data' /** * Formatted text. @@ -11,32 +10,36 @@ export type Text = | Markdown | Tgx -export class Plain extends Data.TaggedClass('Plain')<{ - text: string - entities?: Array -}> {} +export interface Plain { + readonly _tag: 'Plain' + readonly text: string + readonly entities?: Array +} -export class Html extends Data.TaggedClass('Html')<{ - html: string -}> {} +export interface Html { + readonly _tag: 'Html' + readonly html: string +} -export class Markdown extends Data.TaggedClass('Markdown')<{ - markdown: string -}> {} +export interface Markdown { + readonly _tag: 'Markdown' + readonly markdown: string +} -export class Tgx extends Data.TaggedClass('Tgx')<{ - tgx: TgxElement -}> {} +export interface Tgx { + readonly _tag: 'Tgx' + readonly tgx: TgxElement +} // ———— Constructors ——————————————————————————————————————————————————————————— export const plain = ( text: string, entities?: Array, -): Plain => new Plain({ text, entities }) +): Plain => ({ _tag: 'Plain', text, entities }) -export const html = (html: string): Html => new Html({ html }) +export const html = (html: string): Html => ({ _tag: 'Html', html }) -export const markdown = (markdown: string): Markdown => new Markdown({ markdown }) +export const markdown = (markdown: string): Markdown => ({ _tag: 'Markdown', markdown }) -export const tgx = (tgx: TgxElement): Tgx => new Tgx({ tgx }) +export const tgx = (tgx: TgxElement): Tgx => ({ _tag: 'Tgx', tgx }) diff --git a/src/internal/botApiTransport.ts b/src/internal/botApiTransport.ts index 9c74330..3603d48 100644 --- a/src/internal/botApiTransport.ts +++ b/src/internal/botApiTransport.ts @@ -18,7 +18,7 @@ interface ExtractedFile { * {@linkcode File.InputFile InputFile} instances. */ const hasInputFile = (value: unknown): boolean => { - if (value instanceof File.InputFile) { + if (File.isInputFile(value)) { return true } if (Array.isArray(value)) { @@ -34,7 +34,7 @@ const cloneAndExtract = ( value: unknown, files: ExtractedFile[], ): unknown => { - if (value instanceof File.InputFile) { + if (File.isInputFile(value)) { const attachId = String(files.length + 1) files.push({ attachId, file: value }) return `attach://${attachId}` diff --git a/src/internal/dialog.ts b/src/internal/dialog.ts index 10fdfcc..473f6da 100644 --- a/src/internal/dialog.ts +++ b/src/internal/dialog.ts @@ -80,34 +80,26 @@ export const ofMessage: ( ) => Dialog.Dialog = (m) => { switch (m.chat.type) { case 'private': { - const user = new Dialog.User({ - id: Option.getOrThrow(decodePeerId('user', m.chat.id)), - }) + const user = Dialog.user(Option.getOrThrow(decodePeerId('user', m.chat.id))) if (m.message_thread_id != null) { - return user.topic(m.message_thread_id) + return Dialog.privateTopic(user, m.message_thread_id) } return user } case 'group': { - return new Dialog.Group({ - id: Option.getOrThrow(decodePeerId('group', m.chat.id)), - }) + return Dialog.group(Option.getOrThrow(decodePeerId('group', m.chat.id))) } case 'channel': { - const channel = new Dialog.Channel({ - id: Option.getOrThrow(decodePeerId('channel', m.chat.id)), - }) + const channel = Dialog.channel(Option.getOrThrow(decodePeerId('channel', m.chat.id))) if (m.direct_messages_topic != null) { - return channel.directMessages(m.direct_messages_topic.topic_id) + return Dialog.channelDm(channel, m.direct_messages_topic.topic_id) } return channel } case 'supergroup': { - const supergroup = new Dialog.Supergroup({ - id: Option.getOrThrow(decodePeerId('channel', m.chat.id)), - }) + const supergroup = Dialog.supergroup(Option.getOrThrow(decodePeerId('channel', m.chat.id))) if (m.message_thread_id != null) { - return supergroup.topic(m.message_thread_id) + return Dialog.forumTopic(supergroup, m.message_thread_id) } return supergroup } diff --git a/src/internal/send.ts b/src/internal/send.ts index 7d613b5..76c6377 100644 --- a/src/internal/send.ts +++ b/src/internal/send.ts @@ -1,7 +1,6 @@ import type * as Effect from 'effect/Effect' import type * as BotApiError from '../BotApiError.ts' import type * as Content from '../Content.ts' -import type * as Dialog from '../Dialog.ts' import type * as Markup from '../Markup.ts' import type * as Reply from '../Reply.ts' import type * as Send from '../Send.ts' @@ -12,6 +11,7 @@ import * as Duration from 'effect/Duration' import * as Match from 'effect/Match' import * as Option from 'effect/Option' import * as BotApi from '../BotApi.ts' +import * as Dialog from '../Dialog.ts' import * as LinkPreview from '../LinkPreview.ts' // ============================================================================= @@ -246,27 +246,27 @@ const paramsDialog: ( })), Match.tagsExhaustive({ User: user => ({ - chat_id: user.dialogId(), + chat_id: Dialog.dialogId(user), }), Group: group => ({ - chat_id: group.dialogId(), + chat_id: Dialog.dialogId(group), }), Channel: channel => ({ - chat_id: channel.dialogId(), + chat_id: Dialog.dialogId(channel), }), Supergroup: supergroup => ({ - chat_id: supergroup.dialogId(), + chat_id: Dialog.dialogId(supergroup), }), PrivateTopic: topic => ({ - chat_id: topic.user.dialogId(), + chat_id: Dialog.dialogId(topic.user), message_thread_id: topic.topicId, }), ForumTopic: topic => ({ - chat_id: topic.supergroup.dialogId(), + chat_id: Dialog.dialogId(topic.supergroup), message_thread_id: topic.topicId, }), ChannelDm: dm => ({ - chat_id: dm.channel.dialogId(), + chat_id: Dialog.dialogId(dm.channel), direct_messages_topic_id: dm.topicId, }), }), @@ -466,7 +466,7 @@ const paramsReply = ( reply_parameters: { chat_id: Match.value(reply.dialog).pipe( Match.when(Match.number, id => id), - Match.orElse(peer => peer.dialogId()), + Match.orElse(peer => Dialog.dialogId(peer)), ), message_id: reply.messageId, checklist_task_id: Option.getOrUndefined(reply.taskId),