From fa3c719312727e278e35590b1a73075efcba5969 Mon Sep 17 00:00:00 2001 From: teidesu Date: Tue, 27 Apr 2021 20:31:04 +0300 Subject: [PATCH] feat(dispatcher): support chat member updates --- packages/dispatcher/src/builders.ts | 41 ++- packages/dispatcher/src/dispatcher.ts | 60 +++- packages/dispatcher/src/filters.ts | 28 ++ packages/dispatcher/src/handler.ts | 15 +- packages/dispatcher/src/index.ts | 1 + .../src/updates/chat-member-update.ts | 271 ++++++++++++++++++ packages/dispatcher/src/updates/index.ts | 1 + 7 files changed, 413 insertions(+), 4 deletions(-) create mode 100644 packages/dispatcher/src/updates/chat-member-update.ts create mode 100644 packages/dispatcher/src/updates/index.ts diff --git a/packages/dispatcher/src/builders.ts b/packages/dispatcher/src/builders.ts index 7fa455d1..e0245600 100644 --- a/packages/dispatcher/src/builders.ts +++ b/packages/dispatcher/src/builders.ts @@ -1,6 +1,7 @@ -import { NewMessageHandler, RawUpdateHandler } from './handler' +import { ChatMemberUpdateHandler, NewMessageHandler, RawUpdateHandler } from './handler' import { filters, UpdateFilter } from './filters' import { Message } from '@mtcute/client' +import { ChatMemberUpdate } from './updates' export namespace handlers { /** @@ -75,4 +76,42 @@ export namespace handlers { callback: filter, } } + + /** + * Create a {@link ChatMemberUpdateHandler} + * + * @param handler Chat member update handler + */ + export function chatMemberUpdate( + handler: ChatMemberUpdateHandler['callback'] + ): ChatMemberUpdateHandler + + /** + * Create a {@link ChatMemberUpdateHandler} with a filter + * + * @param filter Chat member update filter + * @param handler Chat member update handler + */ + export function chatMemberUpdate( + filter: UpdateFilter, + handler: ChatMemberUpdateHandler>['callback'] + ): ChatMemberUpdateHandler + + export function chatMemberUpdate( + filter: any, + handler?: any + ): ChatMemberUpdateHandler { + if (handler) { + return { + type: 'chat_member', + check: filter, + callback: handler, + } + } + + return { + type: 'chat_member', + callback: filter, + } + } } diff --git a/packages/dispatcher/src/dispatcher.ts b/packages/dispatcher/src/dispatcher.ts index a7099ce2..9eeaba49 100644 --- a/packages/dispatcher/src/dispatcher.ts +++ b/packages/dispatcher/src/dispatcher.ts @@ -6,9 +6,15 @@ import { StopChildrenPropagation, StopPropagation, } from './propagation' -import { NewMessageHandler, RawUpdateHandler, UpdateHandler } from './handler' +import { + ChatMemberUpdateHandler, + NewMessageHandler, + RawUpdateHandler, + UpdateHandler, +} from './handler' import { filters, UpdateFilter } from './filters' import { handlers } from './builders' +import { ChatMemberUpdate } from './updates' const noop = () => {} @@ -127,6 +133,19 @@ export class Dispatcher { ) } + let chatMember: ChatMemberUpdate | null = null + if ( + update._ === 'updateChatParticipant' || + update._ === 'updateChannelParticipant' + ) { + chatMember = new ChatMemberUpdate( + this._client, + update, + users, + chats + ) + } + outer: for (const grp of this._groupsOrder) { for (const handler of this._groups[grp]) { let result: void | PropagationSymbol @@ -155,6 +174,13 @@ export class Dispatcher { (await handler.check(message, this._client))) ) { result = await handler.callback(message, this._client) + } else if ( + handler.type === 'chat_member' && + chatMember && + (!handler.check || + (await handler.check(chatMember, this._client))) + ) { + result = await handler.callback(chatMember, this._client) } else continue if (result === ContinuePropagation) continue @@ -379,4 +405,36 @@ export class Dispatcher { onNewMessage(filter: any, handler?: any, group?: number): void { this._addKnownHandler('newMessage', filter, handler, group) } + + /** + * Register a chat member update filter without any filters. + * + * @param handler Update handler + * @param group Handler group index + * @internal + */ + onChatMemberUpdate( + handler: ChatMemberUpdateHandler['callback'], + group?: number + ): void + + /** + * Register a message handler with a given filter + * + * @param filter Update filter + * @param handler Update handler + * @param group Handler group index + */ + onChatMemberUpdate( + filter: UpdateFilter, + handler: ChatMemberUpdateHandler< + filters.Modify + >['callback'], + group?: number + ): void + + /** @internal */ + onChatMemberUpdate(filter: any, handler?: any, group?: number): void { + this._addKnownHandler('chatMemberUpdate', filter, handler, group) + } } diff --git a/packages/dispatcher/src/filters.ts b/packages/dispatcher/src/filters.ts index 7c1b9a4b..4e73280f 100644 --- a/packages/dispatcher/src/filters.ts +++ b/packages/dispatcher/src/filters.ts @@ -19,6 +19,7 @@ import { import { Game } from '@mtcute/client/src/types/media/game' import { WebPage } from '@mtcute/client/src/types/media/web-page' import { MaybeArray } from '@mtcute/core' +import { ChatMemberUpdate } from './updates' /** * Type describing a primitive filter, which is a function taking some `Base` @@ -561,4 +562,31 @@ export namespace filters { return false } } + + /** + * Create a filter for {@link ChatMemberUpdate} by update type + * + * @param types Update type(s) + */ + export const chatMember: { + (type: T): UpdateFilter< + ChatMemberUpdate, + { type: T } + > + (types: T): UpdateFilter< + ChatMemberUpdate, + { type: T[number] } + > + } = ( + types: MaybeArray + ): UpdateFilter => { + if (Array.isArray(types)) { + const index: Partial> = {} + types.forEach((typ) => (index[typ] = true)) + + return (upd) => upd.type in index + } + + return (upd) => upd.type === types + } } diff --git a/packages/dispatcher/src/handler.ts b/packages/dispatcher/src/handler.ts index d4e2cc24..770b643e 100644 --- a/packages/dispatcher/src/handler.ts +++ b/packages/dispatcher/src/handler.ts @@ -1,6 +1,7 @@ import { MaybeAsync, Message, TelegramClient } from '@mtcute/client' import { tl } from '@mtcute/tl' import { PropagationSymbol } from './propagation' +import { ChatMemberUpdate } from './updates' interface BaseUpdateHandler { type: Type @@ -34,6 +35,16 @@ export type RawUpdateHandler = BaseUpdateHandler< ) => MaybeAsync > -export type NewMessageHandler = ParsedUpdateHandler<'new_message', T> +export type NewMessageHandler = ParsedUpdateHandler< + 'new_message', + T +> +export type ChatMemberUpdateHandler = ParsedUpdateHandler< + 'chat_member', + T +> -export type UpdateHandler = RawUpdateHandler | NewMessageHandler +export type UpdateHandler = + | RawUpdateHandler + | NewMessageHandler + | ChatMemberUpdateHandler diff --git a/packages/dispatcher/src/index.ts b/packages/dispatcher/src/index.ts index 83731a64..059be2f1 100644 --- a/packages/dispatcher/src/index.ts +++ b/packages/dispatcher/src/index.ts @@ -3,3 +3,4 @@ export * from './dispatcher' export * from './filters' export * from './handler' export * from './propagation' +export * from './updates' diff --git a/packages/dispatcher/src/updates/chat-member-update.ts b/packages/dispatcher/src/updates/chat-member-update.ts new file mode 100644 index 00000000..4ac69080 --- /dev/null +++ b/packages/dispatcher/src/updates/chat-member-update.ts @@ -0,0 +1,271 @@ +import { tl } from '@mtcute/tl' +import { + Chat, + ChatInviteLink, + ChatMember, + TelegramClient, + User, +} from '@mtcute/client' +import { makeInspectable } from '@mtcute/client/src/types/utils' + +export namespace ChatMemberUpdate { + /** + * Type of the event. Can be one of: + * - `joined`: User `user` joined the chat/channel on their own + * - `added`: User `actor` added another user `user` to the chat + * - `left`: User `user` left the channel on their own + * - `kicked`: User `user` was kicked from the chat by `actor` + * - `unkicked`: User `user` was removed from the list of kicked users by `actor` and can join the chat again + * - `restricted`: User `user` was restricted by `actor` + * - `unrestricted`: User `user` was unrestricted by `actor` + * - `promoted`: User `user` was promoted to admin by `actor` + * - `demoted`: User `user` was demoted from admin by `actor` + * - `old_owner`: User `user` transferred their own chat ownership + * - `new_owner`: User `actor` transferred their chat ownership to `user` + * - `other`: Some other event (e.g. change in restrictions, change in admin rights, etc.) + */ + export type Type = + | 'joined' + | 'added' + | 'left' + | 'kicked' + | 'unkicked' + | 'restricted' + | 'unrestricted' + | 'promoted' + | 'demoted' + | 'old_owner' + | 'new_owner' + | 'other' +} + +/** + * Update representing a change in the status + * of a chat/channel member. + */ +export class ChatMemberUpdate { + readonly client: TelegramClient + + readonly raw: tl.RawUpdateChatParticipant | tl.RawUpdateChannelParticipant + + /** Map of users in this message. Mainly for internal use */ + readonly _users: Record + /** Map of chats in this message. Mainly for internal use */ + readonly _chats: Record + + constructor( + client: TelegramClient, + raw: tl.RawUpdateChatParticipant | tl.RawUpdateChannelParticipant, + users: Record, + chats: Record + ) { + this.client = client + this.raw = raw + this._users = users + this._chats = chats + } + + /** + * Date of the event + */ + get date(): Date { + return new Date(this.raw.date * 1000) + } + + /** + * Whether this is an update about current user + */ + get isSelf(): boolean { + return this.user.isSelf + } + + private _type?: ChatMemberUpdate.Type + /** + * Type of the update + * + * @link ChatMemberUpdate.Type + */ + get type(): ChatMemberUpdate.Type { + if (!this._type) { + // we do not use `.actor`, `.newMember` and `.oldMember`, + // since using them would mean creating objects, + // which will probably be useless in case this property + // is used inside of a filter + // fortunately, all the info is available as-is and does not require + // additional parsing + + const old = this.raw.prevParticipant + const cur = this.raw.newParticipant + + const oldId = + (old && ((old as any).userId || (old as any).peer.userId)) || + null + const curId = + (cur && ((cur as any).userId || (cur as any).peer.userId)) || + null + + const actorId = this.raw.actorId + + if (!old && cur) { + // join or added + return (this._type = actorId === curId ? 'joined' : 'added') + } + + if (old && !cur) { + // left, kicked (for chats) or unkicked + if (actorId === oldId) return (this._type = 'left') + + if (old._ === 'channelParticipantBanned') { + return (this._type = 'unkicked') + } + + return (this._type = 'kicked') + } + + // in this case OR is the same as AND, but AND doesn't work well with typescript :shrug: + if (!old || !cur) return (this._type = 'other') + + if (old._ === 'chatParticipant' || old._ === 'channelParticipant') { + if ( + cur._ === 'chatParticipantAdmin' || + cur._ === 'channelParticipantAdmin' + ) { + return (this._type = 'promoted') + } + + if (cur._ === 'channelParticipantBanned') { + // kicked or restricted + if (cur.left) return (this._type = 'kicked') + + return (this._type = 'restricted') + } + } + + if ( + old._ === 'channelParticipantBanned' && + cur._ === 'channelParticipant' + ) { + return (this._type = 'unrestricted') + } + + if ( + old._ === 'channelParticipantAdmin' && + cur._ === 'channelParticipant' + ) { + return (this._type = 'demoted') + } + + if ( + old._ === 'chatParticipantCreator' || + old._ === 'channelParticipantCreator' + ) { + return (this._type = 'old_owner') + } + + if ( + cur._ === 'chatParticipantCreator' || + cur._ === 'channelParticipantCreator' + ) { + return (this._type = 'new_owner') + } + + return (this._type = 'other') + } + + return this._type + } + + private _chat?: Chat + /** + * Chat in which this event has occurred + */ + get chat(): Chat { + if (!this._chat) { + const id = + this.raw._ === 'updateChannelParticipant' + ? this.raw.channelId + : this.raw.chatId + this._chat = new Chat(this.client, this._chats[id]) + } + + return this._chat + } + + private _actor?: User + /** + * Performer of the action which resulted in this update. + * + * Can be chat/channel administrator or the {@link user} themself. + */ + get actor(): User { + if (!this._actor) { + this._actor = new User(this.client, this._users[this.raw.actorId]) + } + + return this._actor + } + + private _user?: User + /** + * User representing the chat member whose status was changed. + */ + get user(): User { + if (!this._user) { + this._user = new User(this.client, this._users[this.raw.userId]) + } + + return this._user + } + + private _oldMember?: ChatMember + /** + * Previous (old) information about chat member. + */ + get oldMember(): ChatMember | null { + if (!this.raw.prevParticipant) return null + + if (!this._oldMember) { + this._oldMember = new ChatMember( + this.client, + this.raw.prevParticipant, + this._users + ) + } + + return this._oldMember + } + + private _newMember?: ChatMember + /** + * Current (new) information about chat member. + */ + get newMember(): ChatMember | null { + if (!this.raw.newParticipant) return null + + if (!this._newMember) { + this._newMember = new ChatMember( + this.client, + this.raw.newParticipant, + this._users + ) + } + + return this._newMember + } + + private _inviteLink?: ChatInviteLink + /** + * In case this is a "join" event, invite link that was used to join (if any) + */ + get inviteLink(): ChatInviteLink | null { + if (!this.raw.invite) return null + + if (!this._inviteLink) { + this._inviteLink = new ChatInviteLink(this.client, this.raw.invite) + } + + return this._inviteLink + } +} + +makeInspectable(ChatMemberUpdate) diff --git a/packages/dispatcher/src/updates/index.ts b/packages/dispatcher/src/updates/index.ts new file mode 100644 index 00000000..37f2ddb6 --- /dev/null +++ b/packages/dispatcher/src/updates/index.ts @@ -0,0 +1 @@ +export * from './chat-member-update'