feat: user status and typing related methods and updates

This commit is contained in:
teidesu 2021-05-08 16:35:25 +03:00
parent 8a0c9984b5
commit 002d949a13
11 changed files with 551 additions and 24 deletions

View file

@ -73,6 +73,7 @@ import { _parseEntities } from './methods/messages/parse-entities'
import { pinMessage } from './methods/messages/pin-message'
import { searchGlobal } from './methods/messages/search-global'
import { searchMessages } from './methods/messages/search-messages'
import { sendTyping } from './methods/messages/send-chat-action'
import { sendMediaGroup } from './methods/messages/send-media-group'
import { sendMedia } from './methods/messages/send-media'
import { sendText } from './methods/messages/send-text'
@ -105,6 +106,7 @@ import { getCommonChats } from './methods/users/get-common-chats'
import { getMe } from './methods/users/get-me'
import { getUsers } from './methods/users/get-users'
import { resolvePeer } from './methods/users/resolve-peer'
import { setOffline } from './methods/users/set-offline'
import { IMessageEntityParser } from './parser'
import { Readable } from 'stream'
import {
@ -1673,6 +1675,50 @@ export interface TelegramClient extends BaseTelegramClient {
chunkSize?: number
}
): AsyncIterableIterator<Message>
/**
* Sends a current user/bot typing event
* to a conversation partner or group.
*
* This status is set for 6 seconds, and is
* automatically cancelled if you send a
* message.
*
* @param chatId Chat ID
* @param action
* (default: `'typing'`)
* Chat action:
* - `typing` - user is typing
* - `cancel` to cancel previously sent event
* - `record_video` - user is recording a video
* - `upload_video` - user is uploading a video
* - `record_voice` - user is recording a voice note
* - `upload_voice` - user is uploading a voice note
* - `upload_photo` - user is uploading a photo
* - `upload_document` - user is sending a document
*
* @param progress For `upload_*` actions, progress of the upload (optional)
*/
sendChatAction(
chatId: InputPeerLike,
action?:
| 'typing'
| 'cancel'
| 'record_video'
| 'upload_video'
| 'record_voice'
| 'upload_voice'
| 'upload_photo'
| 'upload_document'
| 'geo'
| 'contact'
| 'game'
| 'record_round'
| 'upload_round'
| 'group_call'
| 'history_import'
| tl.TypeSendMessageAction,
progress?: number
): Promise<void>
/**
* Send a group of media.
*
@ -2169,6 +2215,12 @@ export interface TelegramClient extends BaseTelegramClient {
resolvePeer(
peerId: InputPeerLike
): Promise<tl.TypeInputPeer | tl.TypeInputUser | tl.TypeInputChannel>
/**
* Change user status to offline or online
*
* @param offline (default: `true`) Whether the user is currently offline
*/
setOffline(offline?: boolean): Promise<void>
}
/** @internal */
export class TelegramClient extends BaseTelegramClient {
@ -2273,6 +2325,7 @@ export class TelegramClient extends BaseTelegramClient {
pinMessage = pinMessage
searchGlobal = searchGlobal
searchMessages = searchMessages
sendChatAction = sendTyping
sendMediaGroup = sendMediaGroup
sendMedia = sendMedia
sendText = sendText
@ -2301,4 +2354,5 @@ export class TelegramClient extends BaseTelegramClient {
getMe = getMe
getUsers = getUsers
resolvePeer = resolvePeer
setOffline = setOffline
}

View file

@ -0,0 +1,81 @@
import { TelegramClient } from '../../client'
import { tl } from '@mtcute/tl'
import { InputPeerLike } from '../../types'
import { TypingStatus } from '../../types/peers/typing-status'
import { normalizeToInputPeer } from '../../utils/peer-utils'
/**
* Sends a current user/bot typing event
* to a conversation partner or group.
*
* This status is set for 6 seconds, and is
* automatically cancelled if you send a
* message.
*
* @param chatId Chat ID
* @param status Typing status
* @param progress For `upload_*` and actions, progress of the upload
* @internal
*/
export async function sendTyping(
this: TelegramClient,
chatId: InputPeerLike,
status: TypingStatus | tl.TypeSendMessageAction = 'typing',
progress = 0
): Promise<void> {
if (typeof status === 'string') {
switch (status) {
case 'typing':
status = { _: 'sendMessageTypingAction' }
break;
case 'cancel':
status = { _: 'sendMessageCancelAction' }
break;
case 'record_video':
status = { _: 'sendMessageRecordVideoAction' }
break;
case 'upload_video':
status = { _: 'sendMessageUploadVideoAction', progress }
break;
case 'record_voice':
status = { _: 'sendMessageRecordAudioAction' }
break;
case 'upload_voice':
status = { _: 'sendMessageUploadAudioAction', progress }
break;
case 'upload_photo':
status = { _: 'sendMessageUploadPhotoAction', progress }
break;
case 'upload_document':
status = { _: 'sendMessageUploadDocumentAction', progress }
break;
case 'geo':
status = { _: 'sendMessageGeoLocationAction' }
break;
case 'contact':
status = { _: 'sendMessageChooseContactAction' }
break;
case 'game':
status = { _: 'sendMessageGamePlayAction' }
break;
case 'record_round':
status = { _: 'sendMessageRecordRoundAction' }
break;
case 'upload_round':
status = { _: 'sendMessageUploadRoundAction', progress }
break;
case 'speak_call':
status = { _: 'speakingInGroupCallAction' }
break;
case 'history_import':
status = { _: 'sendMessageHistoryImportAction', progress }
break;
}
}
await this.call({
_: 'messages.setTyping',
peer: normalizeToInputPeer(await this.resolvePeer(chatId)),
action: status
})
}

View file

@ -0,0 +1,17 @@
import { TelegramClient } from '../../client'
/**
* Change user status to offline or online
*
* @param offline Whether the user is currently offline
* @internal
*/
export async function setOffline(
this: TelegramClient,
offline = true
): Promise<void> {
await this.call({
_: 'account.updateStatus',
offline
})
}

View file

@ -0,0 +1,38 @@
/**
* User typing status. Used to provide detailed
* information about chat partners' actions:
* typing messages, recording/uploading attachments, etc.
*
* Can be:
* - `typing`: User is typing
* - `cancel`: User is not doing anything (used to cancel previously sent status)
* - `record_video`: User is recording a video
* - `upload_video`: User is uploading a video
* - `record_voice`: User is recording a voice message
* - `upload_voice`: User is uploading a voice message
* - `upload_photo`: User is uploading a photo
* - `upload_document`: User is uploading a document
* - `geo`: User is choosing a geolocation to share
* - `contact`: User is choosing a contact to share
* - `game`: User is playing a game
* - `record_round`: User is recording a round video message
* - `upload_round`: User is uploading a round video message
* - `speak_call`: *undocumented* User is speaking in a group call
* - `history_import`: *undocumented* User is importing history
*/
export type TypingStatus =
| 'typing'
| 'cancel'
| 'record_video'
| 'upload_video'
| 'record_voice'
| 'upload_voice'
| 'upload_photo'
| 'upload_document'
| 'geo'
| 'contact'
| 'game'
| 'record_round'
| 'upload_round'
| 'speak_call'
| 'history_import'

View file

@ -25,12 +25,12 @@ export namespace User {
| 'within_month'
| 'long_time_ago'
| 'bot'
}
interface ParsedStatus {
status: User.Status
lastOnline: Date | null
nextOffline: Date | null
export interface ParsedStatus {
status: User.Status
lastOnline: Date | null
nextOffline: Date | null
}
}
export class User {
@ -117,40 +117,43 @@ export class User {
return this._user.lastName ?? null
}
private _parsedStatus?: ParsedStatus
private _parseStatus() {
let status: User.Status
static parseStatus(status: tl.TypeUserStatus, bot = false): User.ParsedStatus {
let ret: User.Status
let date: Date
const us = this._user.status
const us = status
if (!us) {
status = 'long_time_ago'
} else if (this._user.bot) {
status = 'bot'
ret = 'long_time_ago'
} else if (bot) {
ret = 'bot'
} else if (us._ === 'userStatusOnline') {
status = 'online'
ret = 'online'
date = new Date(us.expires * 1000)
} else if (us._ === 'userStatusOffline') {
status = 'offline'
ret = 'offline'
date = new Date(us.wasOnline * 1000)
} else if (us._ === 'userStatusRecently') {
status = 'recently'
ret = 'recently'
} else if (us._ === 'userStatusLastWeek') {
status = 'within_week'
ret = 'within_week'
} else if (us._ === 'userStatusLastMonth') {
status = 'within_month'
ret = 'within_month'
} else {
status = 'long_time_ago'
ret = 'long_time_ago'
}
this._parsedStatus = {
status,
lastOnline: status === 'offline' ? date! : null,
nextOffline: status === 'online' ? date! : null,
return {
status: ret,
lastOnline: ret === 'offline' ? date! : null,
nextOffline: ret === 'online' ? date! : null,
}
}
private _parsedStatus?: User.ParsedStatus
private _parseStatus() {
this._parsedStatus = User.parseStatus(this._user.status!, this._user.bot)
}
/** User's Last Seen & Online status */
get status(): User.Status {
if (!this._parsedStatus) this._parseStatus()

View file

@ -9,3 +9,5 @@ chosen_inline_result = ChosenInlineResult
callback_query = CallbackQuery
poll: PollUpdate = PollUpdate
poll_vote = PollVoteUpdate
user_status: UserStatusUpdate = UserStatusUpdate
user_typing = UserTypingUpdate

View file

@ -10,6 +10,8 @@ import {
CallbackQueryHandler,
PollUpdateHandler,
PollVoteHandler,
UserStatusUpdateHandler,
UserTypingHandler,
} from './handler'
// end-codegen-imports
import { filters, UpdateFilter } from './filters'
@ -18,6 +20,8 @@ import { ChatMemberUpdate } from './updates'
import { ChosenInlineResult } from './updates/chosen-inline-result'
import { PollUpdate } from './updates/poll-update'
import { PollVoteUpdate } from './updates/poll-vote'
import { UserStatusUpdate } from './updates/user-status-update'
import { UserTypingUpdate } from './updates/user-typing-update'
function _create<T extends UpdateHandler>(
type: T['type'],
@ -291,5 +295,62 @@ export namespace handlers {
return _create('poll_vote', filter, handler)
}
/**
* Create an user status update handler
*
* @param handler User status update handler
*/
export function userStatusUpdate(
handler: UserStatusUpdateHandler['callback']
): UserStatusUpdateHandler
/**
* Create an user status update handler with a filter
*
* @param filter Update filter
* @param handler User status update handler
*/
export function userStatusUpdate<Mod>(
filter: UpdateFilter<UserStatusUpdate, Mod>,
handler: UserStatusUpdateHandler<
filters.Modify<UserStatusUpdate, Mod>
>['callback']
): UserStatusUpdateHandler
/** @internal */
export function userStatusUpdate(
filter: any,
handler?: any
): UserStatusUpdateHandler {
return _create('user_status', filter, handler)
}
/**
* Create an user typing handler
*
* @param handler User typing handler
*/
export function userTyping(
handler: UserTypingHandler['callback']
): UserTypingHandler
/**
* Create an user typing handler with a filter
*
* @param filter Update filter
* @param handler User typing handler
*/
export function userTyping<Mod>(
filter: UpdateFilter<UserTypingUpdate, Mod>,
handler: UserTypingHandler<
filters.Modify<UserTypingUpdate, Mod>
>['callback']
): UserTypingHandler
/** @internal */
export function userTyping(filter: any, handler?: any): UserTypingHandler {
return _create('user_typing', filter, handler)
}
// end-codegen
}

View file

@ -24,6 +24,8 @@ import {
CallbackQueryHandler,
PollUpdateHandler,
PollVoteHandler,
UserStatusUpdateHandler,
UserTypingHandler,
} from './handler'
// end-codegen-imports
import { filters, UpdateFilter } from './filters'
@ -32,6 +34,8 @@ import { ChatMemberUpdate } from './updates'
import { ChosenInlineResult } from './updates/chosen-inline-result'
import { PollUpdate } from './updates/poll-update'
import { PollVoteUpdate } from './updates/poll-vote'
import { UserStatusUpdate } from './updates/user-status-update'
import { UserTypingUpdate } from './updates/user-typing-update'
const noop = () => {}
@ -67,6 +71,10 @@ const callbackQueryParser: UpdateParser = [
'callback_query',
(client, upd, users) => new CallbackQuery(client, upd as any, users),
]
const userTypingParser: UpdateParser = [
'user_typing',
(client, upd) => new UserTypingUpdate(client, upd as any)
]
const PARSERS: Partial<
Record<(tl.TypeUpdate | tl.TypeMessage)['_'], UpdateParser>
@ -100,10 +108,17 @@ const PARSERS: Partial<
'poll_vote',
(client, upd, users) => new PollVoteUpdate(client, upd as any, users),
],
updateUserStatus: [
'user_status',
(client, upd) => new UserStatusUpdate(client, upd as any),
],
updateChannelUserTyping: userTypingParser,
updateChatUserTyping: userTypingParser,
updateUserTyping: userTypingParser,
}
/**
* The dispatcher
* Updates dispatcher
*/
export class Dispatcher {
private _groups: Record<number, UpdateHandler[]> = {}
@ -667,5 +682,66 @@ export class Dispatcher {
this._addKnownHandler('pollVote', filter, handler, group)
}
/**
* Register an user status update handler without any filters
*
* @param handler User status update handler
* @param group Handler group index
* @internal
*/
onUserStatusUpdate(
handler: UserStatusUpdateHandler['callback'],
group?: number
): void
/**
* Register an user status update handler with a filter
*
* @param filter Update filter
* @param handler User status update handler
* @param group Handler group index
*/
onUserStatusUpdate<Mod>(
filter: UpdateFilter<UserStatusUpdate, Mod>,
handler: UserStatusUpdateHandler<
filters.Modify<UserStatusUpdate, Mod>
>['callback'],
group?: number
): void
/** @internal */
onUserStatusUpdate(filter: any, handler?: any, group?: number): void {
this._addKnownHandler('userStatusUpdate', filter, handler, group)
}
/**
* Register an user typing handler without any filters
*
* @param handler User typing handler
* @param group Handler group index
* @internal
*/
onUserTyping(handler: UserTypingHandler['callback'], group?: number): void
/**
* Register an user typing handler with a filter
*
* @param filter Update filter
* @param handler User typing handler
* @param group Handler group index
*/
onUserTyping<Mod>(
filter: UpdateFilter<UserTypingUpdate, Mod>,
handler: UserTypingHandler<
filters.Modify<UserTypingUpdate, Mod>
>['callback'],
group?: number
): void
/** @internal */
onUserTyping(filter: any, handler?: any, group?: number): void {
this._addKnownHandler('userTyping', filter, handler, group)
}
// end-codegen
}

View file

@ -11,6 +11,8 @@ import { ChatMemberUpdate } from './updates'
import { ChosenInlineResult } from './updates/chosen-inline-result'
import { PollUpdate } from './updates/poll-update'
import { PollVoteUpdate } from './updates/poll-vote'
import { UserStatusUpdate } from './updates/user-status-update'
import { UserTypingUpdate } from './updates/user-typing-update'
interface BaseUpdateHandler<Type, Handler, Checker> {
type: Type
@ -73,6 +75,14 @@ export type PollVoteHandler<T = PollVoteUpdate> = ParsedUpdateHandler<
'poll_vote',
T
>
export type UserStatusUpdateHandler<T = UserStatusUpdate> = ParsedUpdateHandler<
'user_status',
T
>
export type UserTypingHandler<T = UserTypingUpdate> = ParsedUpdateHandler<
'user_typing',
T
>
export type UpdateHandler =
| RawUpdateHandler
@ -84,5 +94,7 @@ export type UpdateHandler =
| CallbackQueryHandler
| PollUpdateHandler
| PollVoteHandler
| UserStatusUpdateHandler
| UserTypingHandler
// end-codegen

View file

@ -0,0 +1,66 @@
import { TelegramClient, User } from '@mtcute/client'
import { tl } from '@mtcute/tl'
import { makeInspectable } from '@mtcute/client/src/types/utils'
/**
* User status has changed
*/
export class UserStatusUpdate {
readonly client: TelegramClient
readonly raw: tl.RawUpdateUserStatus
constructor(
client: TelegramClient,
raw: tl.RawUpdateUserStatus
) {
this.client = client
this.raw = raw
}
/**
* ID of the user whose status has updated
*/
get userId(): number {
return this.raw.userId
}
private _parsedStatus?: User.ParsedStatus
private _parseStatus() {
this._parsedStatus = User.parseStatus(this.raw.status)
}
/**
* User's new Last Seen & Online status
*/
get status(): User.Status {
if (!this._parsedStatus) this._parseStatus()
return this._parsedStatus!.status
}
/**
* Last time this user was seen online.
* Only available if {@link status} is `offline`
*/
get lastOnline(): Date | null {
if (!this._parsedStatus) this._parseStatus()
return this._parsedStatus!.lastOnline
}
/**
* Time when this user will automatically go offline.
* Only available if {@link status} is `online`
*/
get nextOffline(): Date | null {
if (!this._parsedStatus) this._parseStatus()
return this._parsedStatus!.nextOffline
}
/**
* Fetch information about the user
*/
getUser(): Promise<User> {
return this.client.getUsers(this.raw.userId)
}
}
makeInspectable(UserStatusUpdate)

View file

@ -0,0 +1,117 @@
import { BasicPeerType, Chat, MtCuteUnsupportedError, TelegramClient, User } from '@mtcute/client'
import { tl } from '@mtcute/tl'
import { getBarePeerId, MAX_CHANNEL_ID } from '@mtcute/core'
import { TypingStatus } from '@mtcute/client/src/types/peers/typing-status'
import { makeInspectable } from '@mtcute/client/src/types/utils'
/**
* User's typing status has changed.
*
* This update is valid for 6 seconds.
*/
export class UserTypingUpdate {
readonly client: TelegramClient
readonly raw:
| tl.RawUpdateUserTyping
| tl.RawUpdateChatUserTyping
| tl.RawUpdateChannelUserTyping
constructor(client: TelegramClient, raw: UserTypingUpdate['raw']) {
this.client = client
this.raw = raw
}
/**
* ID of the user whose typing status changed
*/
get userId(): number {
return this.raw._ === 'updateUserTyping'
? this.raw.userId
: getBarePeerId(this.raw.fromId)
}
/**
* Marked ID of the chat where the user is typing,
*
* If the user is typing in PMs, this will
* equal to {@link userId}
*/
get chatId(): number {
switch (this.raw._) {
case 'updateUserTyping':
return this.raw.userId
case 'updateChatUserTyping':
return -this.raw.chatId
case 'updateChannelUserTyping':
return MAX_CHANNEL_ID - this.raw.channelId
}
}
/**
* Type of the chat where this event has occurred
*/
get chatType(): BasicPeerType {
switch (this.raw._) {
case 'updateUserTyping':
return 'user'
case 'updateChatUserTyping':
return 'chat'
case 'updateChannelUserTyping':
return 'channel'
}
}
/**
* Current typing status
*/
get status(): TypingStatus {
switch (this.raw.action._) {
case 'sendMessageTypingAction':
return 'typing'
case 'sendMessageCancelAction':
return 'cancel'
case 'sendMessageRecordVideoAction':
return 'record_video'
case 'sendMessageUploadVideoAction':
return 'upload_video'
case 'sendMessageRecordAudioAction':
return 'record_voice'
case 'sendMessageUploadAudioAction':
return 'upload_voice'
case 'sendMessageUploadPhotoAction':
return 'upload_photo'
case 'sendMessageUploadDocumentAction':
return 'upload_document'
case 'sendMessageGeoLocationAction':
return 'geo'
case 'sendMessageChooseContactAction':
return 'contact'
case 'sendMessageRecordRoundAction':
return 'record_round'
case 'sendMessageUploadRoundAction':
return 'upload_round'
case 'speakingInGroupCallAction':
return 'speak_call'
case 'sendMessageHistoryImportAction':
return 'history_import'
}
throw new MtCuteUnsupportedError()
}
/**
* Fetch the user whose typing status has changed
*/
getUser(): Promise<User> {
return this.client.getUsers(this.userId)
}
/**
* Fetch the chat where the update has happenned
*/
getChat(): Promise<Chat> {
return this.client.getChat(this.chatId)
}
}
makeInspectable(UserTypingUpdate)