dograma
Version:
NodeJS/Browser MTProto API Telegram client library,
441 lines (406 loc) • 14.5 kB
text/typescript
import type { TelegramClient } from "./TelegramClient";
import type { EntitiesLike, Entity, EntityLike, ValueOf } from "../define";
import { sleep, getMinBigInt, TotalList, betterConsoleLog } from "../Helpers";
import { RequestIter } from "../requestIter";
import { helpers, utils } from "../";
import { Api } from "../tl";
import bigInt, { BigInteger } from "big-integer";
import { inspect } from "../inspect";
const _MAX_PARTICIPANTS_CHUNK_SIZE = 200;
const _MAX_ADMIN_LOG_CHUNK_SIZE = 100;
const _MAX_PROFILE_PHOTO_CHUNK_SIZE = 100;
interface ChatActionInterface {
delay: number;
autoCancel: boolean;
}
class _ChatAction {
static _str_mapping = {
typing: new Api.SendMessageTypingAction(),
contact: new Api.SendMessageChooseContactAction(),
game: new Api.SendMessageGamePlayAction(),
location: new Api.SendMessageGeoLocationAction(),
"record-audio": new Api.SendMessageRecordAudioAction(),
"record-voice": new Api.SendMessageRecordAudioAction(), //alias
"record-round": new Api.SendMessageRecordRoundAction(),
"record-video": new Api.SendMessageRecordVideoAction(),
audio: new Api.SendMessageUploadAudioAction({ progress: 1 }),
voice: new Api.SendMessageUploadAudioAction({ progress: 1 }), // alias
song: new Api.SendMessageUploadAudioAction({ progress: 1 }), // alias
round: new Api.SendMessageUploadRoundAction({ progress: 1 }),
video: new Api.SendMessageUploadVideoAction({ progress: 1 }),
photo: new Api.SendMessageUploadPhotoAction({ progress: 1 }),
document: new Api.SendMessageUploadDocumentAction({ progress: 1 }),
file: new Api.SendMessageUploadDocumentAction({ progress: 1 }), // alias
cancel: new Api.SendMessageCancelAction(),
};
private _client: TelegramClient;
private readonly _chat: EntityLike;
private readonly _action: ValueOf<typeof _ChatAction._str_mapping>;
private readonly _delay: number;
private readonly autoCancel: boolean;
private _request?: Api.AnyRequest;
private _task: null;
private _running: boolean;
[inspect.custom]() {
return betterConsoleLog(this);
}
constructor(
client: TelegramClient,
chat: EntityLike,
action: ValueOf<typeof _ChatAction._str_mapping>,
params: ChatActionInterface = {
delay: 4,
autoCancel: true,
}
) {
this._client = client;
this._chat = chat;
this._action = action;
this._delay = params.delay;
this.autoCancel = params.autoCancel;
this._request = undefined;
this._task = null;
this._running = false;
}
async start() {
this._request = new Api.messages.SetTyping({
peer: this._chat,
action: this._action,
});
this._running = true;
this._update();
}
async stop() {
this._running = false;
if (this.autoCancel) {
await this._client.invoke(
new Api.messages.SetTyping({
peer: this._chat,
action: new Api.SendMessageCancelAction(),
})
);
}
}
async _update() {
while (this._running) {
if (this._request != undefined) {
await this._client.invoke(this._request);
}
await sleep(this._delay * 1000);
}
}
progress(current: number, total: number) {
if ("progress" in this._action) {
this._action.progress = 100 * Math.round(current / total);
}
}
}
interface ParticipantsIterInterface {
entity: EntityLike;
filter: any;
offset?: number;
search?: string;
showTotal?: boolean;
}
export class _ParticipantsIter extends RequestIter {
private filterEntity: ((entity: Entity) => boolean) | undefined;
private requests?: Api.channels.GetParticipants[];
[inspect.custom]() {
return betterConsoleLog(this);
}
async _init({
entity,
filter,
offset,
search,
showTotal,
}: ParticipantsIterInterface): Promise<boolean | void> {
if (filter && filter.constructor === Function) {
if (
[
Api.ChannelParticipantsBanned,
Api.ChannelParticipantsKicked,
Api.ChannelParticipantsSearch,
Api.ChannelParticipantsContacts,
].includes(filter)
) {
filter = new filter({
q: "",
});
} else {
filter = new filter();
}
}
entity = await this.client.getInputEntity(entity);
const ty = helpers._entityType(entity);
if (search && (filter || ty != helpers._EntityType.CHANNEL)) {
// We need to 'search' ourselves unless we have a PeerChannel
search = search.toLowerCase();
this.filterEntity = (entity: Entity) => {
return (
utils
.getDisplayName(entity)
.toLowerCase()
.includes(search!) ||
("username" in entity ? entity.username || "" : "")
.toLowerCase()
.includes(search!)
);
};
} else {
this.filterEntity = (entity: Entity) => true;
}
// Only used for channels, but we should always set the attribute
this.requests = [];
if (ty == helpers._EntityType.CHANNEL) {
if (showTotal) {
const channel = await this.client.invoke(
new Api.channels.GetFullChannel({
channel: entity,
})
);
if (!(channel.fullChat instanceof Api.ChatFull)) {
this.total = channel.fullChat.participantsCount;
}
}
if (this.total && this.total <= 0) {
return false;
}
this.requests.push(
new Api.channels.GetParticipants({
channel: entity,
filter:
filter ||
new Api.ChannelParticipantsSearch({
q: search || "",
}),
offset,
limit: _MAX_PARTICIPANTS_CHUNK_SIZE,
hash: bigInt.zero,
})
);
} else if (ty == helpers._EntityType.CHAT) {
if (!("chatId" in entity)) {
throw new Error(
"Found chat without id " + JSON.stringify(entity)
);
}
const full = await this.client.invoke(
new Api.messages.GetFullChat({
chatId: entity.chatId,
})
);
if (full.fullChat instanceof Api.ChatFull) {
if (
!(
full.fullChat.participants instanceof
Api.ChatParticipantsForbidden
)
) {
this.total = full.fullChat.participants.participants.length;
} else {
this.total = 0;
return false;
}
const users = new Map<string, Entity>();
for (const user of full.users) {
users.set(user.id.toString(), user);
}
for (const participant of full.fullChat.participants
.participants) {
const user = users.get(participant.userId.toString())!;
if (!this.filterEntity(user)) {
continue;
}
(user as any).participant = participant;
this.buffer?.push(user);
}
return true;
}
} else {
this.total = 1;
if (this.limit != 0) {
const user = await this.client.getEntity(entity);
if (this.filterEntity(user)) {
(user as any).participant = undefined;
this.buffer?.push(user);
}
}
return true;
}
}
async _loadNextChunk(): Promise<boolean | undefined> {
if (!this.requests?.length) {
return true;
}
this.requests[0].limit = Math.min(
this.limit - this.requests[0].offset,
_MAX_PARTICIPANTS_CHUNK_SIZE
);
const results = [];
for (const request of this.requests) {
results.push(await this.client.invoke(request));
}
for (let i = this.requests.length - 1; i >= 0; i--) {
const participants = results[i];
if (
participants instanceof
Api.channels.ChannelParticipantsNotModified ||
!participants.users.length
) {
this.requests.splice(i, 1);
continue;
}
this.requests[i].offset += participants.participants.length;
const users = new Map<string, Entity>();
for (const user of participants.users) {
users.set(user.id.toString(), user);
}
for (const participant of participants.participants) {
if (!("userId" in participant)) {
continue;
}
const user = users.get(participant.userId.toString())!;
if (this.filterEntity && !this.filterEntity(user)) {
continue;
}
(user as any).participant = participant;
this.buffer?.push(user);
}
}
return undefined;
}
[Symbol.asyncIterator](): AsyncIterator<Api.User, any, undefined> {
return super[Symbol.asyncIterator]();
}
}
interface _AdminLogFilterInterface {
join?: boolean;
leave?: boolean;
invite?: boolean;
restrict?: boolean;
unrestrict?: boolean;
ban?: boolean;
unban?: boolean;
promote?: boolean;
demote?: boolean;
info?: boolean;
settings?: boolean;
pinned?: boolean;
edit?: boolean;
delete?: boolean;
groupCall?: boolean;
}
interface _AdminLogSearchInterface {
admins?: EntitiesLike;
search?: string;
minId?: BigInteger;
maxId?: BigInteger;
}
class _AdminLogIter extends RequestIter {
private entity?: Api.TypeInputPeer;
private request?: Api.channels.GetAdminLog;
[inspect.custom]() {
return betterConsoleLog(this);
}
async _init(
entity: EntityLike,
searchArgs?: _AdminLogSearchInterface,
filterArgs?: _AdminLogFilterInterface
) {
let eventsFilter = undefined;
if (
filterArgs &&
Object.values(filterArgs).find((element) => element === true)
) {
eventsFilter = new Api.ChannelAdminLogEventsFilter({
...filterArgs,
});
}
this.entity = await this.client.getInputEntity(entity);
const adminList = [];
if (searchArgs && searchArgs.admins) {
for (const admin of searchArgs.admins) {
adminList.push(await this.client.getInputEntity(admin));
}
}
this.request = new Api.channels.GetAdminLog({
channel: this.entity,
q: searchArgs?.search || "",
minId: searchArgs?.minId,
maxId: searchArgs?.maxId,
limit: 0,
eventsFilter: eventsFilter,
admins: adminList || undefined,
});
}
async _loadNextChunk() {
if (!this.request) {
return true;
}
this.request.limit = Math.min(this.left, _MAX_ADMIN_LOG_CHUNK_SIZE);
const r = await this.client.invoke(this.request);
const entities = new Map();
for (const entity of [...r.users, ...r.chats]) {
entities.set(utils.getPeerId(entity), entity);
}
const eventIds = [];
for (const e of r.events) {
eventIds.push(e.id);
}
this.request.maxId = getMinBigInt([bigInt.zero, ...eventIds]);
for (const ev of r.events) {
if (
ev.action instanceof Api.ChannelAdminLogEventActionEditMessage
) {
// @ts-ignore
// TODO ev.action.prevMessage._finishInit(this.client, entities, this.entity);
// @ts-ignore
// TODO ev.action.newMessage._finishInit(this.client, entities, this.entity);
}
}
}
}
/**
* Used in iterParticipant and getParticipant. all params are optional.
*/
export interface IterParticipantsParams {
/** how many members to retrieve. defaults to Number.MAX_SAFE_INTEGER (everyone) */
limit?: number;
/** how many members to skip. defaults to 0 */
offset?: number;
/** a query string to filter participants based on their display names and usernames. defaults to "" (everyone) */
search?: string;
/** optional filter to be used. E.g only admins filter or only banned members filter. PS : some filters need more permissions. */
filter?: Api.TypeChannelParticipantsFilter;
/** whether to call an extra request (GetFullChannel) to show the total of users in the group/channel. if set to false total will be 0 */
showTotal?: boolean;
}
/** @hidden */
export function iterParticipants(
client: TelegramClient,
entity: EntityLike,
{ limit, offset, search, filter, showTotal = true }: IterParticipantsParams
) {
return new _ParticipantsIter(
client,
limit ?? Number.MAX_SAFE_INTEGER,
{},
{
entity: entity,
filter: filter,
offset,
search: search,
showTotal: showTotal,
}
);
}
/** @hidden */
export async function getParticipants(
client: TelegramClient,
entity: EntityLike,
params: IterParticipantsParams
) {
const it = client.iterParticipants(entity, params);
return (await it.collect()) as TotalList<Api.User>;
}