feat(client): chat event logs

also added support for chat locations and fixed ts errors
This commit is contained in:
teidesu 2021-05-10 21:51:25 +03:00
parent 4ad562bf06
commit f0eb95e4ba
8 changed files with 927 additions and 2 deletions

View file

@ -27,6 +27,7 @@ import { deleteChatPhoto } from './methods/chats/delete-chat-photo'
import { deleteGroup } from './methods/chats/delete-group'
import { deleteHistory } from './methods/chats/delete-history'
import { deleteUserHistory } from './methods/chats/delete-user-history'
import { getChatEventLog } from './methods/chats/get-chat-event-log'
import { getChatMember } from './methods/chats/get-chat-member'
import { getChatMembers } from './methods/chats/get-chat-members'
import { getChatPreview } from './methods/chats/get-chat-preview'
@ -137,6 +138,8 @@ import { IMessageEntityParser } from './parser'
import { Readable } from 'stream'
import {
Chat,
ChatEvent,
ChatInviteLink,
ChatMember,
ChatPreview,
Dialog,
@ -673,6 +676,74 @@ export interface TelegramClient extends BaseTelegramClient {
chatId: InputPeerLike,
userId: InputPeerLike
): Promise<void>
/**
* Get chat event log ("Recent actions" in official
* clients).
*
* Only available for supergroups and channels, and
* requires (any) administrator rights.
*
* Results are returned in reverse chronological
* order (i.e. newest first) and event IDs are
* in direct chronological order (i.e. newer
* events have bigger event ID)
*
* @param chatId Chat ID
* @param params
*/
getChatEventLog(
chatId: InputPeerLike,
params?: {
/**
* Search query
*/
query?: string
/**
* Minimum event ID to return
*/
minId?: tl.Long
/**
* Maximum event ID to return,
* can be used as a base offset
*/
maxId?: tl.Long
/**
* List of users whose actions to return
*/
users?: InputPeerLike[]
/**
* Event filters. Can be a TL object, or one or more
* action types.
*
* Note that some filters are grouped in TL
* (i.e. `info=true` will return `title_changed`,
* `username_changed` and many more),
* and when passing one or more action types,
* they will be filtered locally.
*/
filters?:
| tl.TypeChannelAdminLogEventsFilter
| MaybeArray<Exclude<ChatEvent.Action, null>['type']>
/**
* Limit the number of events returned.
*
* Defaults to `Infinity`, i.e. all events are returned
*/
limit?: number
/**
* Chunk size, usually not needed.
*
* Defaults to `100`
*/
chunkSize?: number
}
): AsyncIterableIterator<ChatEvent>
/**
* Get information about a single chat member
*
@ -2682,6 +2753,7 @@ export class TelegramClient extends BaseTelegramClient {
deleteGroup = deleteGroup
deleteHistory = deleteHistory
deleteUserHistory = deleteUserHistory
getChatEventLog = getChatEventLog
getChatMember = getChatMember
getChatMembers = getChatMembers
getChatPreview = getChatPreview

View file

@ -33,7 +33,9 @@ import {
StickerSet,
Poll,
TypingStatus,
Photo
Photo,
ChatEvent,
ChatInviteLink
} from '../types'
// @copy

View file

@ -0,0 +1,242 @@
import { TelegramClient } from '../../client'
import {
InputPeerLike,
MtCuteInvalidPeerTypeError,
ChatEvent,
} from '../../types'
import { tl } from '@mtcute/tl'
import { MaybeArray } from '@mtcute/core'
import bigInt from 'big-integer'
import {
createUsersChatsIndex,
normalizeToInputChannel,
normalizeToInputUser,
} from '../../utils/peer-utils'
/**
* Get chat event log ("Recent actions" in official
* clients).
*
* Only available for supergroups and channels, and
* requires (any) administrator rights.
*
* Results are returned in reverse chronological
* order (i.e. newest first) and event IDs are
* in direct chronological order (i.e. newer
* events have bigger event ID)
*
* @param chatId Chat ID
* @param params
* @internal
*/
export async function* getChatEventLog(
this: TelegramClient,
chatId: InputPeerLike,
params?: {
/**
* Search query
*/
query?: string
/**
* Minimum event ID to return
*/
minId?: tl.Long
/**
* Maximum event ID to return,
* can be used as a base offset
*/
maxId?: tl.Long
/**
* List of users whose actions to return
*/
users?: InputPeerLike[]
/**
* Event filters. Can be a TL object, or one or more
* action types.
*
* Note that some filters are grouped in TL
* (i.e. `info=true` will return `title_changed`,
* `username_changed` and many more),
* and when passing one or more action types,
* they will be filtered locally.
*/
filters?:
| tl.TypeChannelAdminLogEventsFilter
| MaybeArray<Exclude<ChatEvent.Action, null>['type']>
/**
* Limit the number of events returned.
*
* Defaults to `Infinity`, i.e. all events are returned
*/
limit?: number
/**
* Chunk size, usually not needed.
*
* Defaults to `100`
*/
chunkSize?: number
}
): AsyncIterableIterator<ChatEvent> {
if (!params) params = {}
const channel = normalizeToInputChannel(await this.resolvePeer(chatId))
if (!channel) throw new MtCuteInvalidPeerTypeError(chatId, 'channel')
let current = 0
let maxId = params.maxId ?? bigInt.zero
const minId = params.minId ?? bigInt.zero
const query = params.query ?? ''
const total = params.limit || Infinity
const chunkSize = Math.min(params.chunkSize ?? 100, total)
const admins: tl.TypeInputUser[] | undefined = params.users
? ((await Promise.all(
params.users
.map((u) => this.resolvePeer(u).then(normalizeToInputUser))
.filter(Boolean)
)) as tl.TypeInputUser[])
: undefined
let serverFilter:
| tl.Mutable<tl.TypeChannelAdminLogEventsFilter>
| undefined = undefined
let localFilter: Record<string, true> | undefined = undefined
if (params.filters) {
if (
typeof params.filters === 'string' ||
Array.isArray(params.filters)
) {
let input = params.filters
if (!Array.isArray(input)) input = [input]
serverFilter = {
_: 'channelAdminLogEventsFilter',
}
localFilter = {}
input.forEach((type) => {
localFilter![type] = true
switch (type) {
case 'user_joined':
serverFilter!.join = true
break
case 'user_left':
serverFilter!.leave = true
break
case 'user_invited':
serverFilter!.invite = true
break
case 'title_changed':
case 'description_changed':
case 'linked_chat_changed':
case 'location_changed':
case 'photo_changed':
case 'username_changed':
case 'stickerset_changed':
serverFilter!.info = true
break
case 'invites_toggled':
case 'history_toggled':
case 'signatures_toggled':
case 'def_perms_changed':
serverFilter!.settings = true
break
case 'msg_pinned':
serverFilter!.pinned = true
break
case 'msg_edited':
case 'poll_stopped':
serverFilter!.edit = true
break
case 'msg_deleted':
serverFilter!.delete = true
break
case 'user_perms_changed':
serverFilter!.ban = true
serverFilter!.unban = true
serverFilter!.kick = true
serverFilter!.unkick = true
break
case 'user_admin_perms_changed':
serverFilter!.promote = true
serverFilter!.demote = true
break
case 'slow_mode_changed':
case 'ttl_changed':
// not documented so idk, enable both
serverFilter!.settings = true
serverFilter!.info = true
break
case 'call_started':
case 'call_ended':
serverFilter!.groupCall = true
break
case 'call_setting_changed':
// not documented so idk, enable all
serverFilter!.groupCall = true
serverFilter!.settings = true
serverFilter!.info = true
break
case 'user_joined_invite':
// not documented so idk, enable all
serverFilter!.join = true
serverFilter!.invite = true
serverFilter!.invites = true
break
case 'invite_deleted':
case 'invite_edited':
case 'invite_revoked':
serverFilter!.invites = true
break
default: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _: never = type
}
}
})
} else {
serverFilter = params.filters
}
}
for (;;) {
const res = await this.call({
_: 'channels.getAdminLog',
channel,
q: query,
eventsFilter: serverFilter,
admins,
maxId,
minId,
limit: Math.min(chunkSize, total - current),
})
if (!res.events.length) break
const { users, chats } = createUsersChatsIndex(res)
const last = res.events[res.events.length - 1]
maxId = last.id
for (const evt of res.events) {
const parsed = new ChatEvent(this, evt, users, chats)
if (
localFilter &&
(!parsed.action || !localFilter[parsed.action.type])
)
continue
current += 1
yield parsed
if (current >= total) break
}
}
}

View file

@ -563,7 +563,7 @@ export class Message {
*
* @param revoke Whether to "revoke" (i.e. delete for both sides). Only used for chats and private chats.
*/
delete(revoke = false): Promise<boolean> {
delete(revoke = false): Promise<void> {
return this.client.deleteMessages(this.chat.inputPeer, this.id, revoke)
}

View file

@ -0,0 +1,553 @@
import { makeInspectable } from '../utils'
import { TelegramClient } from '../../client'
import { tl } from '@mtcute/tl'
import { User } from './user'
import { ChatMember } from './chat-member'
import { Photo } from '../media'
import { Message } from '../messages'
import { ChatPermissions } from './chat-permissions'
import { ChatLocation } from './chat-location'
import { ChatInviteLink } from './chat-invite-link'
export namespace ChatEvent {
/** A user has joined the group (in the case of big groups, info of the user that has joined isn't shown) */
export interface ActionUserJoined {
type: 'user_joined'
}
/** A user has joined the group using an invite link */
export interface ActionUserJoinedInvite {
type: 'user_joined_invite'
/** Invite link user to join */
link: ChatInviteLink
}
/** A user has left the group (in the case of big groups, info of the user that has joined isn't shown) */
export interface ActionUserLeft {
type: 'user_left'
}
/** A user was invited to the group */
export interface ActionUserInvited {
type: 'user_invited'
/** Member who has been invited */
member: ChatMember
}
/** Group title has been changed */
export interface ActionTitleChanged {
type: 'title_changed'
/** Old chat title */
old: string
/** New chat title */
new: string
}
/** Group description has been changed */
export interface ActionDescriptionChanged {
type: 'description_changed'
/** Old description */
old: string
/** New description */
new: string
}
/** Group username has been changed */
export interface ActionUsernameChanged {
type: 'username_changed'
/** Old username */
old: string
/** New username */
new: string
}
/** Group photo has been changed */
export interface ActionPhotoChanged {
type: 'photo_changed'
/** Old photo */
old: Photo
/** New photo */
new: Photo
}
/** Invites were enabled/disabled */
export interface ActionInvitesToggled {
type: 'invites_toggled'
/** Old value */
old: boolean
/** New value */
new: boolean
}
/** Signatures were enabled/disabled */
export interface ActionSignaturesToggled {
type: 'signatures_toggled'
/** Old value */
old: boolean
/** New value */
new: boolean
}
/** A message has been pinned */
export interface ActionMessagePinned {
type: 'msg_pinned'
/** Message which was pinned */
message: Message
}
/** A message has been edited */
export interface ActionMessageEdited {
type: 'msg_edited'
/** Old message */
old: Message
/** New message */
new: Message
}
/** A message has been deleted */
export interface ActionMessageDeleted {
type: 'msg_deleted'
/** Message which was deleted */
message: Message
}
/** User's permissions were changed */
export interface ActionUserPermissionsChanged {
type: 'user_perms_changed'
/** Information about member before change */
old: ChatMember
/** Information about member after change */
new: ChatMember
}
/** User's admin permissions were changed */
export interface ActionUserAdminPermissionsChanged {
type: 'user_admin_perms_changed'
/** Information about member before change */
old: ChatMember
/** Information about member after change */
new: ChatMember
}
/** Group stickerset has been changed */
export interface ActionStickersetChanged {
type: 'stickerset_changed'
/** Old stickerset */
old: tl.TypeInputStickerSet
/** New stickerset */
new: tl.TypeInputStickerSet
}
/** History visibility for new users has been toggled */
export interface ActionHistoryToggled {
type: 'history_toggled'
/** Old value (`false` if new users can see history) */
old: boolean
/** New value (`false` if new users can see history) */
new: boolean
}
/** Group default permissions have been changed */
export interface ActionDefaultPermissionsChanged {
type: 'def_perms_changed'
/** Old default permissions */
old: ChatPermissions
/** New default permissions */
new: ChatPermissions
}
/** Poll has been stopped */
export interface ActionPollStopped {
type: 'poll_stopped'
/** Message containing the poll */
message: Message
}
/** Linked chat has been changed */
export interface ActionLinkedChatChanged {
type: 'linked_chat_changed'
/** ID of the old linked chat */
old: number
/** ID of the new linked chat */
new: number
}
/** Group location has been changed */
export interface ActionLocationChanged {
type: 'location_changed'
/** Old location */
old: ChatLocation | null
/** New location */
new: ChatLocation | null
}
/** Group slow mode delay has been changed */
export interface ActionSlowModeChanged {
type: 'slow_mode_changed'
/** Old delay (can be 0) */
old: number
/** New delay (can be 0) */
new: number
}
/** Group call has been started */
export interface ActionCallStarted {
type: 'call_started'
/** TL object representing the call */
call: tl.TypeInputGroupCall
}
/** Group call has ended */
export interface ActionCallEnded {
type: 'call_ended'
/** TL object representing the call */
call: tl.TypeInputGroupCall
}
/** Group call "join muted" setting has been changed */
export interface ActionCallSettingChanged {
type: 'call_setting_changed'
/** Whether new call participants should join muted */
joinMuted: boolean
}
/** Invite link has been deleted */
export interface ActionInviteLinkDeleted {
type: 'invite_deleted'
/** Invite link which was deleted */
link: ChatInviteLink
}
/** Invite link has been edited */
export interface ActionInviteLinkEdited {
type: 'invite_edited'
/** Old invite link */
old: ChatInviteLink
/** New invite link */
new: ChatInviteLink
}
/** Invite link has been revoked */
export interface ActionInviteLinkRevoked {
type: 'invite_revoked'
/** Invite link which was revoked */
link: ChatInviteLink
}
/** History TTL has been changed */
export interface ActionTtlChanged {
type: 'ttl_changed'
/** Old TTL value (can be 0) */
old: number
/** New TTL value (can be 0) */
new: number
}
/** Chat event action (`null` if unsupported) */
export type Action =
| ActionUserJoined
| ActionUserLeft
| ActionUserInvited
| ActionTitleChanged
| ActionDescriptionChanged
| ActionUsernameChanged
| ActionPhotoChanged
| ActionInvitesToggled
| ActionSignaturesToggled
| ActionMessagePinned
| ActionMessageEdited
| ActionMessageDeleted
| ActionUserPermissionsChanged
| ActionUserAdminPermissionsChanged
| ActionStickersetChanged
| ActionHistoryToggled
| ActionDefaultPermissionsChanged
| ActionPollStopped
| ActionLinkedChatChanged
| ActionLocationChanged
| ActionSlowModeChanged
| ActionCallStarted
| ActionCallEnded
| ActionCallSettingChanged
| ActionUserJoinedInvite
| ActionInviteLinkDeleted
| ActionInviteLinkEdited
| ActionInviteLinkRevoked
| ActionTtlChanged
| null
}
function _actionFromTl(
this: ChatEvent,
e: tl.TypeChannelAdminLogEventAction
): ChatEvent.Action {
switch (e._) {
case 'channelAdminLogEventActionParticipantJoin':
return { type: 'user_joined' }
case 'channelAdminLogEventActionChangeTitle':
return {
type: 'title_changed',
old: e.prevValue,
new: e.newValue,
}
case 'channelAdminLogEventActionChangeAbout':
return {
type: 'description_changed',
old: e.prevValue,
new: e.newValue,
}
case 'channelAdminLogEventActionChangeUsername':
return {
type: 'username_changed',
old: e.prevValue,
new: e.newValue,
}
case 'channelAdminLogEventActionChangePhoto':
return {
type: 'photo_changed',
old: new Photo(this.client, e.prevPhoto as tl.RawPhoto),
new: new Photo(this.client, e.newPhoto as tl.RawPhoto)
}
case 'channelAdminLogEventActionToggleInvites':
return {
type: 'invites_toggled',
old: !e.newValue,
new: e.newValue
}
case 'channelAdminLogEventActionToggleSignatures':
return {
type: 'signatures_toggled',
old: !e.newValue,
new: e.newValue
}
case 'channelAdminLogEventActionUpdatePinned':
return {
type: 'msg_pinned',
message: new Message(this.client, e.message, this._users, this._chats)
}
case 'channelAdminLogEventActionEditMessage':
return {
type: 'msg_edited',
old: new Message(this.client, e.prevMessage, this._users, this._chats),
new: new Message(this.client, e.newMessage, this._users, this._chats)
}
case 'channelAdminLogEventActionDeleteMessage':
return {
type: 'msg_deleted',
message: new Message(this.client, e.message, this._users, this._chats)
}
case 'channelAdminLogEventActionParticipantLeave':
return { type: 'user_left' }
case 'channelAdminLogEventActionParticipantInvite':
return {
type: 'user_invited',
member: new ChatMember(this.client, e.participant, this._users),
}
case 'channelAdminLogEventActionParticipantToggleBan':
return {
type: 'user_perms_changed',
old: new ChatMember(this.client, e.prevParticipant, this._users),
new: new ChatMember(this.client, e.newParticipant, this._users)
}
case 'channelAdminLogEventActionParticipantToggleAdmin':
return {
type: 'user_admin_perms_changed',
old: new ChatMember(this.client, e.prevParticipant, this._users),
new: new ChatMember(this.client, e.newParticipant, this._users)
}
case 'channelAdminLogEventActionChangeStickerSet':
return {
type: 'stickerset_changed',
old: e.prevStickerset,
new: e.newStickerset
}
case 'channelAdminLogEventActionTogglePreHistoryHidden':
return {
type: 'history_toggled',
old: !e.newValue,
new: e.newValue
}
case 'channelAdminLogEventActionDefaultBannedRights':
return {
type: 'def_perms_changed',
old: new ChatPermissions(e.prevBannedRights),
new: new ChatPermissions(e.newBannedRights)
}
case 'channelAdminLogEventActionStopPoll':
return {
type: 'poll_stopped',
message: new Message(this.client, e.message, this._users, this._chats)
}
case 'channelAdminLogEventActionChangeLinkedChat':
return {
type: 'linked_chat_changed',
old: e.prevValue,
new: e.newValue
}
case 'channelAdminLogEventActionChangeLocation':
return {
type: 'location_changed',
old: e.prevValue._ === 'channelLocationEmpty' ? null : new ChatLocation(this.client, e.prevValue),
new: e.newValue._ === 'channelLocationEmpty' ? null : new ChatLocation(this.client, e.newValue),
}
case 'channelAdminLogEventActionToggleSlowMode':
return {
type: 'slow_mode_changed',
old: e.prevValue,
new: e.newValue
}
case 'channelAdminLogEventActionStartGroupCall':
return {
type: 'call_started',
call: e.call
}
case 'channelAdminLogEventActionDiscardGroupCall':
return {
type: 'call_ended',
call: e.call
}
case 'channelAdminLogEventActionParticipantMute':
case 'channelAdminLogEventActionParticipantUnmute':
case 'channelAdminLogEventActionParticipantVolume':
// todo
return null
case 'channelAdminLogEventActionToggleGroupCallSetting':
return {
type: 'call_setting_changed',
joinMuted: e.joinMuted
}
case 'channelAdminLogEventActionParticipantJoinByInvite':
return {
type: 'user_joined_invite',
link: new ChatInviteLink(this.client, e.invite, this._users)
}
case 'channelAdminLogEventActionExportedInviteDelete':
return {
type: 'invite_deleted',
link: new ChatInviteLink(this.client, e.invite, this._users)
}
case 'channelAdminLogEventActionExportedInviteRevoke':
return {
type: 'invite_revoked',
link: new ChatInviteLink(this.client, e.invite, this._users)
}
case 'channelAdminLogEventActionExportedInviteEdit':
return {
type: 'invite_edited',
old: new ChatInviteLink(this.client, e.prevInvite, this._users),
new: new ChatInviteLink(this.client, e.newInvite, this._users)
}
case 'channelAdminLogEventActionChangeHistoryTTL':
return {
type: 'ttl_changed',
old: e.prevValue,
new: e.newValue
}
default:
return null
}
}
export class ChatEvent {
readonly client: TelegramClient
readonly raw: tl.TypeChannelAdminLogEvent
readonly _users: Record<number, tl.TypeUser>
readonly _chats: Record<number, tl.TypeChat>
constructor(
client: TelegramClient,
raw: tl.TypeChannelAdminLogEvent,
users: Record<number, tl.TypeUser>,
chats: Record<number, tl.TypeChat>
) {
this.client = client
this.raw = raw
this._users = users
this._chats = chats
}
/**
* Event ID.
*
* Event IDs are generated in direct chronological order
* (i.e. newer events have bigger event ID)
*/
get id(): tl.Long {
return this.raw.id
}
/**
* Date of the event
*/
get date(): Date {
return new Date(this.raw.date * 1000)
}
private _actor?: User
/**
* Actor of the event
*/
get actor(): User {
if (!this._actor) {
this._actor = new User(this.client, this._users[this.raw.userId])
}
return this._actor
}
private _action?: ChatEvent.Action
get action(): ChatEvent.Action {
if (!this._action) {
this._action = _actionFromTl.call(this, this.raw.action)
}
return this._action!
}
}
makeInspectable(ChatEvent)

View file

@ -0,0 +1,38 @@
import { TelegramClient } from '../../client'
import { tl } from '@mtcute/tl'
import { Location } from '../media'
import { makeInspectable } from '../utils'
/**
* Geolocation of a supergroup
*/
export class ChatLocation {
readonly client: TelegramClient
readonly raw: tl.RawChannelLocation
constructor (client: TelegramClient, raw: tl.RawChannelLocation) {
this.client = client
this.raw = raw
}
private _location?: Location
/**
* Location of the chat
*/
get location(): Location {
if (!this._location) {
this._location = new Location(this.client, this.raw.geoPoint as tl.RawGeoPoint)
}
return this._location
}
/**
* Textual description of the address
*/
get address(): string {
return this.raw.address
}
}
makeInspectable(ChatLocation)

View file

@ -6,6 +6,7 @@ import { getMarkedPeerId, MaybeArray } from '@mtcute/core'
import { MtCuteArgumentError, MtCuteTypeAssertionError } from '../errors'
import { makeInspectable } from '../utils'
import { InputPeerLike, User } from './index'
import { ChatLocation } from './chat-location'
export namespace Chat {
/**
@ -409,6 +410,22 @@ export class Chat {
*/
readonly distance?: number
private _location?: ChatLocation
/**
* Location of the chat.
* Returned only in {@link TelegramClient.getFullChat}
*/
get location(): ChatLocation | null {
if (!this.fullPeer || this.fullPeer._ !== 'channelFull' || this.fullPeer.location?._ !== 'channelLocation')
return null
if (!this._location) {
this._location = new ChatLocation(this.client, this.fullPeer.location)
}
return this._location
}
private _linkedChat?: Chat
/**
* The linked discussion group (in case of channels)

View file

@ -5,6 +5,7 @@ export * from './chat'
export * from './chat-preview'
export { InputChatPermissions } from './chat-permissions'
export * from './chat-member'
export * from './chat-event'
export * from './chat-invite-link'
export * from './typing-status'