feat(dispatcher): support chat member updates

This commit is contained in:
teidesu 2021-04-27 20:31:04 +03:00
parent 7e9f255fdc
commit fa3c719312
7 changed files with 413 additions and 4 deletions

View file

@ -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<Mod>(
filter: UpdateFilter<ChatMemberUpdate, Mod>,
handler: ChatMemberUpdateHandler<filters.Modify<ChatMemberUpdate, Mod>>['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,
}
}
}

View file

@ -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<Mod>(
filter: UpdateFilter<ChatMemberUpdate, Mod>,
handler: ChatMemberUpdateHandler<
filters.Modify<ChatMemberUpdate, Mod>
>['callback'],
group?: number
): void
/** @internal */
onChatMemberUpdate(filter: any, handler?: any, group?: number): void {
this._addKnownHandler('chatMemberUpdate', filter, handler, group)
}
}

View file

@ -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: {
<T extends ChatMemberUpdate.Type>(type: T): UpdateFilter<
ChatMemberUpdate,
{ type: T }
>
<T extends ChatMemberUpdate.Type[]>(types: T): UpdateFilter<
ChatMemberUpdate,
{ type: T[number] }
>
} = (
types: MaybeArray<ChatMemberUpdate.Type>
): UpdateFilter<ChatMemberUpdate> => {
if (Array.isArray(types)) {
const index: Partial<Record<ChatMemberUpdate.Type, true>> = {}
types.forEach((typ) => (index[typ] = true))
return (upd) => upd.type in index
}
return (upd) => upd.type === types
}
}

View file

@ -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, Handler, Checker> {
type: Type
@ -34,6 +35,16 @@ export type RawUpdateHandler = BaseUpdateHandler<
) => MaybeAsync<boolean>
>
export type NewMessageHandler<T = Message> = ParsedUpdateHandler<'new_message', T>
export type NewMessageHandler<T = Message> = ParsedUpdateHandler<
'new_message',
T
>
export type ChatMemberUpdateHandler<T = ChatMemberUpdate> = ParsedUpdateHandler<
'chat_member',
T
>
export type UpdateHandler = RawUpdateHandler | NewMessageHandler
export type UpdateHandler =
| RawUpdateHandler
| NewMessageHandler
| ChatMemberUpdateHandler

View file

@ -3,3 +3,4 @@ export * from './dispatcher'
export * from './filters'
export * from './handler'
export * from './propagation'
export * from './updates'

View file

@ -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<number, tl.TypeUser>
/** Map of chats in this message. Mainly for internal use */
readonly _chats: Record<number, tl.TypeChat>
constructor(
client: TelegramClient,
raw: tl.RawUpdateChatParticipant | tl.RawUpdateChannelParticipant,
users: Record<number, tl.TypeUser>,
chats: Record<number, tl.TypeChat>
) {
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)

View file

@ -0,0 +1 @@
export * from './chat-member-update'