Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
258 changes: 128 additions & 130 deletions src/Content.ts

Large diffs are not rendered by default.

142 changes: 74 additions & 68 deletions src/Dialog.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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
Expand All @@ -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<Peer>().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
Expand Down Expand Up @@ -110,11 +102,6 @@ export const GroupId: Brand.Brand.Constructor<GroupId> = Brand.refined<GroupId>(
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<ChannelId> = Brand.refined<ChannelId>(
n => Option.isSome(internal.encodePeerId('channel', n)),
Expand Down Expand Up @@ -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,
Expand Down
37 changes: 34 additions & 3 deletions src/File.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'>
Expand All @@ -16,11 +16,42 @@ export const FileId: Brand.Brand.Constructor<FileId> = Brand.nominal<FileId>()
export type External = URL & Brand.Brand<'External'>
export const External: Brand.Brand.Constructor<External> = Brand.nominal<External>()

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<Uint8Array>
readonly filename: string
readonly mimeType?: string
}

const InputFileProto = {
[InputFileTypeId]: InputFileTypeId,
}

export const make: (args: {
stream: Stream.Stream<Uint8Array>
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.
Expand Down
64 changes: 37 additions & 27 deletions src/Markup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as Data from 'effect/Data'
import * as Option from 'effect/Option'

// =============================================================================
Expand All @@ -14,35 +13,42 @@ export type Markup =
| ReplyKeyboardRemove
| ForceReply

export class InlineKeyboard extends Data.TaggedClass('InlineKeyboard')<{
export interface InlineKeyboard {
readonly _tag: 'InlineKeyboard'
readonly rows: ReadonlyArray<ReadonlyArray<InlineButton>>
}> {}
}

export class ReplyKeyboard extends Data.TaggedClass('ReplyKeyboard')<{
export interface ReplyKeyboard {
readonly _tag: 'ReplyKeyboard'
readonly rows: ReadonlyArray<ReadonlyArray<ReplyButton>>
readonly persistent: boolean
readonly resizable: boolean
readonly oneTime: boolean
readonly selective: boolean
readonly inputPlaceholder: Option.Option<string>
}> {}
}

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<string>
}> {}
}

// =============================================================================
// Constructors
// =============================================================================

export const inlineKeyboard = (
rows: ReadonlyArray<ReadonlyArray<InlineButton>>,
): InlineKeyboard => new InlineKeyboard({ rows })
): InlineKeyboard => ({
_tag: 'InlineKeyboard',
rows,
})

export const replyKeyboard = (
rows: ReadonlyArray<ReadonlyArray<ReplyButton>>,
Expand All @@ -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
Expand Down
Loading