npm workspaces
moved bot to apps/bot
This commit is contained in:
Generated
+14795
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "stats_bot",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "sh -ac '. ../../.env; ts-node src/index'",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"debug-level": "^3.1.2",
|
||||
"input": "^1.0.1",
|
||||
"mongodb": "^6.3.0",
|
||||
"mongoose": "^8.1.1",
|
||||
"prompt": "^1.3.0",
|
||||
"telegraf": "^4.15.3",
|
||||
"telegram": "^2.19.10"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Context, NarrowedContext, Telegraf } from 'telegraf';
|
||||
import { message } from 'telegraf/filters';
|
||||
import { ChatFromGetChat, Update } from 'telegraf/typings/core/types/typegram';
|
||||
import { ioc } from './utils/ioc';
|
||||
import { AudienceChangeController, audienceChangeSymbol } from './controllers/audienceChange';
|
||||
import { log } from './utils/log';
|
||||
|
||||
export type ChannelInfo = {
|
||||
chatId: number,
|
||||
memberCount: number,
|
||||
};
|
||||
|
||||
export type MessageInfo = {
|
||||
|
||||
};
|
||||
|
||||
export class ChannelBot {
|
||||
private readonly bot: Telegraf;
|
||||
|
||||
constructor(
|
||||
private readonly token: string,
|
||||
) {
|
||||
|
||||
this.bot = new Telegraf(
|
||||
process.env.TELEGRAM_BOT_TOKEN ?? '',
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
public async connect(): Promise<void> {
|
||||
|
||||
process.once('SIGINT', () => this.bot.stop('SIGINT'));
|
||||
process.once('SIGTERM', () => this.bot.stop('SIGTERM'));
|
||||
|
||||
this.bot.on('chat_member', this.handleChatMemberUpdate.bind(this));
|
||||
|
||||
this.bot.launch(
|
||||
{
|
||||
allowedUpdates: [
|
||||
'chat_join_request',
|
||||
'chat_member',
|
||||
'channel_post',
|
||||
'edited_channel_post',
|
||||
'poll',
|
||||
'poll_answer'
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async handleChatMemberUpdate(ctx: NarrowedContext<Context<Update>, Update.ChatMemberUpdate>): Promise<void> {
|
||||
try {
|
||||
log.bot.info('chat_member event', JSON.stringify(ctx.chatMember));
|
||||
|
||||
const change = ctx.chatMember;
|
||||
const status = change.new_chat_member.status;
|
||||
const statusBefore = change.old_chat_member.status;
|
||||
const role = ['restricted', 'left', 'kicked'].includes(status)
|
||||
? statusBefore
|
||||
: status;
|
||||
|
||||
const ctrl = ioc.get<AudienceChangeController>(audienceChangeSymbol);
|
||||
|
||||
const chatMemberCount = await this.bot.telegram.getChatMembersCount(change.chat.id);
|
||||
|
||||
ctrl.add({
|
||||
channelId: change.chat.id,
|
||||
user: {
|
||||
id: change.new_chat_member.user.id,
|
||||
username: change.new_chat_member.user.username,
|
||||
firstName: change.new_chat_member.user.first_name,
|
||||
lastName: change.new_chat_member.user.last_name,
|
||||
isBot: change.new_chat_member.user.is_bot,
|
||||
isPremium: change.new_chat_member.user.is_premium ? true : false,
|
||||
role,
|
||||
},
|
||||
timestamp: new Date(change.date * 1000),
|
||||
status,
|
||||
statusBefore,
|
||||
invitedByLink: change.invite_link?.invite_link ?? undefined,
|
||||
|
||||
memberCountAfter: chatMemberCount,
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
log.bot.error('chat member change processing error', err);
|
||||
}
|
||||
}
|
||||
|
||||
public async getChannelInfo(channelId: number): Promise<ChannelInfo> {
|
||||
|
||||
const memberCount = await this.bot.telegram.getChatMembersCount(channelId);
|
||||
|
||||
const info: ChatFromGetChat = await this.bot.telegram.getChat(channelId);
|
||||
|
||||
return {
|
||||
chatId: info.id,
|
||||
memberCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// bot.on('channel_post', async (ctx) => {
|
||||
// console.log('channel_post');
|
||||
// console.dir(ctx.channelPost.chat.id);
|
||||
|
||||
// await sendChannelStats(ctx.chat.id);
|
||||
// });
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
import { Api, TelegramClient } from "telegram";
|
||||
import { StringSession } from "telegram/sessions";
|
||||
import prompt, { message } from 'prompt';
|
||||
import { CustomMessage } from 'telegram/tl/custom/message';
|
||||
|
||||
export type MessageInfo = {
|
||||
id: number;
|
||||
type?: string;
|
||||
text?: string;
|
||||
link?: string;
|
||||
date?: Date;
|
||||
viewCount: number;
|
||||
reactionCount: number;
|
||||
action?: string;
|
||||
forwardedFrom?: {
|
||||
type: string;
|
||||
chatId: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type ParseMessageOptions = {
|
||||
includeText: boolean;
|
||||
includeLink: boolean;
|
||||
statsOnly: boolean;
|
||||
};
|
||||
|
||||
export class ChannelClient {
|
||||
|
||||
private client: TelegramClient | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly apiId: number,
|
||||
private readonly apiHash: string,
|
||||
private readonly session: string = '',
|
||||
) {
|
||||
}
|
||||
|
||||
public async connect(): Promise<void> {
|
||||
this.client = new TelegramClient(
|
||||
new StringSession(this.session),
|
||||
this.apiId,
|
||||
this.apiHash,
|
||||
{
|
||||
connectionRetries: 5,
|
||||
}
|
||||
);
|
||||
|
||||
prompt.start();
|
||||
|
||||
await this.client.start({
|
||||
phoneNumber: async () => (await prompt.get(['phone'])).phone.toString(),
|
||||
password: async () => (await prompt.get(['password'])).password.toString(),
|
||||
phoneCode: async () =>
|
||||
(await prompt.get(['code'])).code.toString(),
|
||||
onError: (err) => console.log(err),
|
||||
});
|
||||
|
||||
// const session = client.session.authKey?.;
|
||||
|
||||
// if (this.session !== session) {
|
||||
|
||||
// }
|
||||
}
|
||||
|
||||
public async getChannelMessages(
|
||||
channelId: number,
|
||||
options: Partial<ParseMessageOptions> = {},
|
||||
): Promise<MessageInfo[]> {
|
||||
|
||||
if (!this.client || !this.client.connected)
|
||||
throw new Error('Client not connected');
|
||||
|
||||
const messages: MessageInfo[] = [];
|
||||
|
||||
// await client.sendMessage(channelId, { message: "Hello!" });
|
||||
for await (const message of this.client.iterMessages(
|
||||
channelId,
|
||||
{
|
||||
reverse: true,
|
||||
}
|
||||
)) {
|
||||
messages.push(await this.parseMessage(message, options));
|
||||
};
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
public async getChannelUsers(
|
||||
channelId: number,
|
||||
): Promise<unknown> {
|
||||
|
||||
if (!this.client || !this.client.connected)
|
||||
throw new Error('Client not connected');
|
||||
|
||||
const users: unknown[] = [];
|
||||
|
||||
let offset = 0;
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
let cnt = 0;
|
||||
for await (const user of this.client.iterParticipants(
|
||||
channelId,
|
||||
{
|
||||
offset,
|
||||
limit: 200,
|
||||
}
|
||||
)) {
|
||||
users.push({
|
||||
id: user.id.toJSNumber(),
|
||||
username: user.username,
|
||||
name: user.firstName + ' ' + user.lastName,
|
||||
phone: user.phone ?? undefined,
|
||||
isBot: user.bot,
|
||||
status: user.status?.className.replace('UserStatus', '').toLowerCase(),
|
||||
lastOnlineAt: (user.status as any)?.wasOnline
|
||||
? new Date((user.status as any).wasOnline * 1000)
|
||||
: undefined,
|
||||
role: (user as any).participant?.className.replace('ChannelParticipant', '').toLowerCase(),
|
||||
joinedAt: (user as any).participant?.date
|
||||
? new Date((user as any).participant?.date * 1000)
|
||||
: undefined,
|
||||
});
|
||||
cnt++;
|
||||
};
|
||||
offset += cnt;
|
||||
if (cnt < 200)
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
public async getChannelMessage(
|
||||
channelId: number,
|
||||
messageId: number,
|
||||
options: Partial<ParseMessageOptions> = {},
|
||||
): Promise<MessageInfo | undefined> {
|
||||
|
||||
if (!this.client || !this.client.connected)
|
||||
throw new Error('Client not connected');
|
||||
|
||||
for await (const message of this.client.iterMessages(
|
||||
channelId,
|
||||
{
|
||||
ids: messageId,
|
||||
}
|
||||
)) {
|
||||
return await this.parseMessage(message, options);
|
||||
};
|
||||
}
|
||||
|
||||
private async parseMessage(
|
||||
message: Api.Message,
|
||||
options: Partial<ParseMessageOptions> = {},
|
||||
): Promise<MessageInfo> {
|
||||
|
||||
const optionsFull: ParseMessageOptions = {
|
||||
statsOnly: false,
|
||||
includeText: false,
|
||||
includeLink: false,
|
||||
...options,
|
||||
};
|
||||
|
||||
const msg: MessageInfo = {
|
||||
id: message.id,
|
||||
viewCount: message.views ?? 0,
|
||||
reactionCount: message.reactions?.results.length ?? 0,
|
||||
};
|
||||
|
||||
if (!optionsFull.statsOnly) {
|
||||
msg.type = message.className;
|
||||
msg.date = new Date(message.date * 1000);
|
||||
|
||||
if (optionsFull.includeText)
|
||||
msg.text = message.text;
|
||||
|
||||
if (optionsFull.includeLink)
|
||||
msg.link = `https://t.me/${(message.chat as Api.Channel)?.username}/${message.id}`;
|
||||
|
||||
if (msg.type === 'MessageService') {
|
||||
msg.action = message.action?.className;
|
||||
}
|
||||
|
||||
if (message.fwdFrom?.fromId) {
|
||||
const origin = message.fwdFrom.fromId;
|
||||
msg.forwardedFrom = {
|
||||
type: origin.className,
|
||||
chatId:
|
||||
origin.className === 'PeerChannel'
|
||||
? Number(`-100${origin.channelId.toString()}`) :
|
||||
(
|
||||
origin.className === 'PeerChat'
|
||||
? -origin.chatId.toJSNumber() :
|
||||
origin.userId.toJSNumber()
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import mongoose from "mongoose";
|
||||
|
||||
export async function connect() {
|
||||
const dbUri = process.env.MONGO_URI ?? '';
|
||||
|
||||
await mongoose.connect(dbUri);
|
||||
console.log("Connected to db");
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { ChannelClient } from './client';
|
||||
import { ChannelBot } from './bot';
|
||||
import { connect } from './database';
|
||||
import { ioc } from './utils/ioc';
|
||||
import { AudienceChangeController } from './controllers/audienceChange';
|
||||
|
||||
(async () => {
|
||||
|
||||
try {
|
||||
await connect();
|
||||
|
||||
AudienceChangeController.register();
|
||||
|
||||
const apiId = Number(process.env.TELEGRAM_API_ID ?? '0');
|
||||
const apiHash = process.env.TELEGRAM_API_HASH ?? '';
|
||||
const apiSession = process.env.TELEGRAM_API_SESSION ?? '';
|
||||
|
||||
const botToken = process.env.TELEGRAM_BOT_TOKEN ?? '';
|
||||
|
||||
const channelId = Number(process.env.TELEGRAM_CHANNEL_ID ?? '0');
|
||||
|
||||
const client = new ChannelClient(
|
||||
apiId,
|
||||
apiHash,
|
||||
apiSession,
|
||||
);
|
||||
|
||||
const bot = new ChannelBot(
|
||||
botToken,
|
||||
);
|
||||
|
||||
await bot.connect();
|
||||
// await client.connect();
|
||||
|
||||
// try {
|
||||
// const messages = await client.getChannelMessages(channelId, { statsOnly: true });
|
||||
// console.dir(messages);
|
||||
|
||||
// const users = await client.getChannelUsers(channelId);
|
||||
|
||||
// console.dir(users);
|
||||
// }
|
||||
// catch (err) {
|
||||
// console.error(err);
|
||||
// }
|
||||
|
||||
// console.dir(await bot.getChannelInfo(channelId));
|
||||
|
||||
// for (const message of messages)
|
||||
// console.dir(await client.getChannelMessage(channelId, message.id));
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
throw err;
|
||||
}
|
||||
})()
|
||||
.catch((err) => console.error);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import mongoose from "mongoose";
|
||||
|
||||
import { IUser, UserSchema } from './user';
|
||||
|
||||
export type IAudienceChange = {
|
||||
channelId: number,
|
||||
|
||||
user: IUser,
|
||||
|
||||
timestamp: Date,
|
||||
status: string,
|
||||
statusBefore: string,
|
||||
invitedByLink: string | undefined,
|
||||
|
||||
memberCountAfter: number;
|
||||
};
|
||||
|
||||
const AudienceChangeSchema = new mongoose.Schema<IAudienceChange>(
|
||||
{
|
||||
channelId: { type: Number, required: true },
|
||||
|
||||
user: UserSchema,
|
||||
|
||||
timestamp: Date,
|
||||
status: { type: String },
|
||||
statusBefore: { type: String },
|
||||
invitedByLink: { type: String },
|
||||
|
||||
memberCountAfter: { type: Number, required: true },
|
||||
},
|
||||
{
|
||||
collection: 'audienceChange',
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
export const AudienceChangeModel = mongoose.model<IAudienceChange>(
|
||||
'audienceChange',
|
||||
AudienceChangeSchema,
|
||||
);
|
||||
@@ -0,0 +1,23 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
export type IUser = {
|
||||
id: number;
|
||||
username: string | undefined;
|
||||
firstName: string;
|
||||
lastName: string | undefined;
|
||||
isBot: boolean;
|
||||
isPremium: boolean;
|
||||
role: string; // 'member' | 'administrator' | 'creator';
|
||||
}
|
||||
|
||||
export const UserSchema = new mongoose.Schema<IUser>(
|
||||
{
|
||||
id: { type: Number, required: true },
|
||||
username: { type: String, required: true },
|
||||
firstName: { type: String },
|
||||
lastName: { type: String },
|
||||
isBot: { type: Boolean, required: true },
|
||||
isPremium: { type: Boolean, required: true },
|
||||
role: { type: String, required: true },
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Schema, model, connect } from 'mongoose';
|
||||
|
||||
// 1. Create an interface representing a document in MongoDB.
|
||||
interface IUser {
|
||||
name: string;
|
||||
email: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
// 2. Create a Schema corresponding to the document interface.
|
||||
const userSchema = new Schema<IUser>({
|
||||
name: { type: String, required: true },
|
||||
email: { type: String, required: true },
|
||||
avatar: String
|
||||
});
|
||||
|
||||
// 3. Create a Model.
|
||||
const User = model<IUser>('User', userSchema);
|
||||
|
||||
run().catch(err => console.log(err));
|
||||
|
||||
async function run() {
|
||||
// 4. Connect to MongoDB
|
||||
await connect('mongodb://127.0.0.1:27017/test');
|
||||
|
||||
const user = new User({
|
||||
name: 'Bill',
|
||||
email: 'bill@initech.com',
|
||||
avatar: 'https://i.imgur.com/dM7Thhn.png'
|
||||
});
|
||||
await user.save();
|
||||
|
||||
console.log(user.email); // 'bill@initech.com'
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
class IOC {
|
||||
private readonly items = new Map<Symbol, any>();
|
||||
|
||||
public set<T>(key: Symbol, obj: T): void {
|
||||
this.items.set(key, obj);
|
||||
}
|
||||
|
||||
public get<T>(key: Symbol): T {
|
||||
return this.items.get(key) as T;
|
||||
}
|
||||
|
||||
public create<A extends any[], T extends { new(...args: A): T; }>(
|
||||
key: Symbol,
|
||||
...args: A
|
||||
): T {
|
||||
const cls = this.get(key) as { new(...args: A): T; };
|
||||
return new cls(...args);
|
||||
}
|
||||
}
|
||||
|
||||
export const ioc = new IOC();
|
||||
@@ -0,0 +1,11 @@
|
||||
import Log from 'debug-level';
|
||||
|
||||
const ns = 'tgstat';
|
||||
|
||||
export const log = {
|
||||
namespace: ns,
|
||||
bot: new Log(`${ns}:bot`),
|
||||
client: new Log(`${ns}:client`),
|
||||
db: new Log(`${ns}:db`),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user