Files
tgCrewAdmin/backend/_old/v8/apps/bot.js
2025-06-29 18:55:59 +03:00

685 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const fs = require('fs')
const util = require('util')
const db = require('../include/db')
const { Api, TelegramClient } = require('telegram')
const { StringSession } = require('telegram/sessions')
const { Button } = require('telegram/tl/custom/button')
const { CustomFile } = require('telegram/client/uploads')
let session
let client
let BOT_ID
const BOT_NAME = 'ready_or_not_2025_bot'
function debug (msg) {
//console.log ('DEBUG: ', msg)
fs.appendFileSync('./debug.log', msg instanceof Object ? util.inspect(msg) : msg)
}
function registerUser (telegramId) {
db
.prepare(`insert or ignore into users (telegram_id) values (:telegram_id)`)
.safeIntegers(true)
.run({ telegram_id: telegramId })
return db
.prepare(`select id from users where telegram_id = :telegram_id`)
.safeIntegers(true)
.pluck(true)
.get({ telegram_id: telegramId })
}
function updateUser (userId, data) {
const info = db
.prepare(`
update users set firstname = :firstname, lastname = :lastname, username = :username,
access_hash = :access_hash, language_code = :language_code where id = :user_id
`)
.safeIntegers(true)
.run({
user_id: userId,
firstname: data.firstName,
lastname: data.lastName,
username: data.username,
access_hash: data.accessHash.value,
language_code: data.langCode
})
return info.changes == 1
}
async function updateUserPhoto (userId, data) {
const photoId = data.photo?.photoId?.value
if (!photoId)
return
const tgUserId = db
.prepare(`select telegram_id from users where id = :user_id and coalesce(photo_id, 0) <> :photo_id`)
.safeIntegers(true)
.pluck(true)
.get({user_id: userId, photo_id: photoId })
if (!tgUserId)
return
const photo = await client.invoke(new Api.photos.GetUserPhotos({
userId: new Api.PeerUser({ userId: tgUserId }),
maxId: photoId,
offset: -1,
limit: 1,
}));
const file = await client.downloadFile(new Api.InputPhotoFileLocation({
id: photoId,
accessHash: photo.photos[0]?.accessHash,
fileReference: Buffer.from('random'),
thumbSize: 'a',
}, {}))
db
.prepare(`update users set photo_id = :photo_id, photo = :photo where id = :user_id`)
.safeIntegers(true)
.run({ user_id: userId, photo_id: photoId, photo: 'data:image/jpg;base64,' + file.toString('base64') })
}
async function registerChat (telegramId, isChannel) {
const chat = db
.prepare(`select id, name, is_channel, access_hash from chats where telegram_id = :telegram_id`)
.safeIntegers(true)
.get({telegram_id: telegramId})
if (chat && chat.access_hash && chat.is_channel == isChannel && chat.name)
return chat.id
const entity = isChannel ? { channelId: telegramId } : { chatId: telegramId }
const tgChat = await client.getEntity( isChannel ?
new Api.InputPeerChannel(entity) :
new Api.InputPeerChat(entity)
)
db
.prepare(`insert or ignore into chats (telegram_id) values (:telegram_id)`)
.safeIntegers(true)
.run({ telegram_id: telegramId })
const chatId = db
.prepare(`update chats set is_channel = :is_channel, access_hash = :access_hash, name = :name where telegram_id = :telegram_id returning id`)
.safeIntegers(true)
.pluck(true)
.get({
telegram_id: telegramId,
is_channel: +isChannel,
access_hash: tgChat.accessHash.value,
name: tgChat.title
})
await updateChat(chatId)
return chatId
}
async function updateChat (chat_id) {
const chat = db
.prepare(`select id, telegram_id, access_hash, is_channel from chats where id = :chat_id`)
.safeIntegers(true)
.get({ chat_id })
const peer = chat.is_channel ?
new Api.InputPeerChannel({ channelId: chat.telegram_id, accessHash: chat.access_hash }) :
new Api.InputPeerChat({ chatId: chat.telegram_id, accessHash: chat.access_hash })
const data = chat.is_channel ?
await client.invoke(new Api.channels.GetFullChannel({ channel: peer })) :
await client.invoke(new Api.messages.GetFullChat({ chatId: chat.telegram_id, accessHash: chat.access_hash }))
const file = data?.fullChat?.chatPhoto ? await client.downloadFile(new Api.InputPeerPhotoFileLocation({ peer, photoId: data.fullChat.chatPhoto?.id }, {})) : null
logo = file ? 'data:image/jpg;base64,' + file.toString('base64') : null
db
.prepare(`
update chats
set invite_link = :invite_link, description = :description, logo = :logo, user_count = :user_count, last_update_time = :last_update_time
where id = :chat_id
`)
.safeIntegers(true)
.run({
chat_id,
invite_link: data.fullChat?.exportedInvite?.link,
description: data.fullChat.about,
logo,
user_count: data.fullChat.participantsCount - (data.users || []).filter(user => user.bot).length,
last_update_time: Math.floor(Date.now() / 1000)
})
}
async function attachChat(chat_id, project_id) {
console.log('attachChat: ', chat_id, project_id)
const chat = db
.prepare(`update chats set project_id = :project_id where id = :chat_id returning telegram_id, access_hash, is_channel`)
.safeIntegers(true)
.get({ chat_id, project_id })
if (!chat.telegram_id)
return console.error('Can\'t attach chat: ' + chat_id + ' to project: ' + project_id)
console.log('attachChat: build peer')
const peer = chat.is_channel ?
new Api.InputPeerChannel({ channelId: chat.telegram_id, accessHash: chat.access_hash }) :
new Api.InputPeerChat({ chatId: chat.telegram_id, accessHash: chat.access_hash })
const message = db
.prepare(`select p.name from projects p where id = :project_id`)
.pluck(true)
.get({ project_id })
console.log('attachChat: send message')
const resultBtn = await client.sendMessage(peer, {
message,
buttons: client.buildReplyMarkup([[Button.url('Открыть проект', `https://t.me/${BOT_NAME}/userapp?startapp=` + project_id)]])
})
console.log('attachChat: pin message')
await client.invoke(new Api.messages.UpdatePinnedMessage({
peer,
id: resultBtn.id,
unpin: false
}))
}
async function reloadChatUsers(chat_id, onlyReset) {
console.log('reloadChatUsers: ', chat_id, onlyReset)
db
.prepare(`delete from chat_users where chat_id = :chat_id`)
.run({ chat_id })
if (onlyReset)
return
const chat = db
.prepare(`select telegram_id, is_channel, access_hash from chats where id = :chat_id`)
.get({ chat_id })
if (!chat)
return
console.log('reloadChatUsers: get user')
const result = chat.is_channel ?
await client.invoke(new Api.channels.GetParticipants({
channel: new Api.PeerChannel({ channelId: chat.telegram_id, accessHash: chat.access_hash }),
filter: new Api.ChannelParticipantsRecent(),
limit: 999999,
offset: 0
})) : await client.invoke(new Api.messages.GetFullChat({ chatId: chat.telegram_id, accessHash: chat.access_hash }))
console.log('reloadChatUsers: process users')
const users = result.users.filter(user => !user.bot)
for (const user of users) {
const user_id = registerUser(user.id.value, user)
if (updateUser(user_id, user)) {
await updateUserPhoto (user_id, user)
db
.prepare(`insert or ignore into chat_users (chat_id, user_id) values (:chat_id, :user_id)`)
.run({ chat_id, user_id })
}
}
console.log('reloadChatUsers: end of user processing')
db
.prepare(`update chats set user_count = (select count(1) from chat_users where chat_id = :chat_id) where id = :chat_id`)
.run({ chat_id })
}
async function onNewServiceMessage (msg, is_channel) {
const action = msg.action || {}
const tg_chat_id = is_channel ? msg.peerId?.channelId?.value : msg.peerId?.chatId?.value
const chat_id = await registerChat(tg_chat_id, is_channel)
// Сhat rename
if (action.className == 'MessageActionChatEditTitle') {
const info = db
.prepare(`
update chats
set name = :name, is_channel = :is_channel, last_update_time = :last_update_time
where telegram_id = :telegram_id
`)
.safeIntegers(true)
.run({
name: action.title,
is_channel,
last_update_time: Math.floor (Date.now() / 1000),
telegram_id: tg_chat_id
})
if (info.changes == 0)
console.error('onNewServiceMessage: Can\'t update a chat title: ' + tg_chat_id)
}
// Chat to Channel
if (action.className == 'MessageActionChatMigrateTo') {
const info = db
.prepare(`
update chats
set telegram_id = :new_telegram_id, name = :name, is_channel = 1, last_update_time = :last_update_time
where telegram_id = :old_telegram_id
`)
.safeIntegers(true)
.run({
name: action.title,
last_update_time: Date.now() / 1000,
old_telegram_id: tg_chat_id,
new_telegram_id: action.channelId.value
})
if (info.changes == 0)
console.error('onNewServiceMessage: Can\'t apply a chat migration to channel: ' + tg_chat_id)
}
// User/s un/register
if (action.className == 'MessageActionChatAddUser' || action.className == 'MessageActionChatDeleteUser' ||
action.className == 'MessageActionChannelAddUser' || action.className == 'MessageActionChannelDeleteUser'
) {
const tgUserIds = [action.user, action.users, action.userId].flat().filter(Boolean).map(e => BigInt(e.value))
const isAdd = action.className == 'MessageActionChatAddUser' || action.className == 'MessageActionChannelAddUser'
if (tgUserIds.indexOf(BOT_ID) == -1) {
// Add/remove non-bot users
for (const tgUserId of tgUserIds) {
const user_id = registerUser(tgUserId)
if (isAdd) {
try {
const user = await client.getEntity(new Api.PeerUser({ userId: tgUserId }))
updateUser(user_id, user)
await updateUserPhoto (user_id, user)
} catch (err) {
console.error(msg.className + ', ' + user_id + ': ' + err.message)
}
}
const query = isAdd ?
`insert or ignore into chat_users (chat_id, user_id) values (:chat_id, :user_id)` :
`delete from chat_users where chat_id = :chat_id and user_id = :user_id`
db
.prepare(query)
.run({ chat_id, user_id })
}
}
}
}
async function onNewMessage (msg, is_сhannel) {
const telegram_id = is_сhannel ? msg.peerId?.channelId?.value : msg.peerId?.chatId?.value
const chat_id = await registerChat(telegram_id, is_сhannel)
console.log(msg)
const file = msg.media?.document || msg.media?.photo
if (file) {
const is_photo = file.className == 'Photo'
const tg_user_id = msg.senderId?.value
const filedata = {
chat_id,
message_id: msg.id,
caption: msg.message,
published_by: registerUser(tg_user_id),
published: msg.date,
parent_type: 0,
parent_id: null
}
if (is_photo) {
function formatTime(time) {
const date = new Date(time * 1000)
const isoString = date.toISOString()
const [datePart, timePart] = isoString.split('T')
const [year, month, day] = datePart.split('-')
const [hours, minutes] = timePart.split(':')
return `${year}-${month}-${day}_${hours}-${minutes}`
}
filedata.filename = 'photo_' + formatTime(msg.date) + '.jpg'
filedata.mime = 'image/jpeg'
console.log(file.sizes)
const s = file.sizes.reduce((prev, e) => (prev.w > e.w) ? prev : e)
filedata.size = s.size || s.sizes?.reduce((a, b) => Math.max(a, b))
} else {
filedata.filename = file.attributes?.find(attr => attr.className == 'DocumentAttributeFilename')?.fileName
filedata.mime = file.mimeType
filedata.size = doc.size?.value
}
function updateFileAccess(file_id, telegram_file_id, access_hash) {
return db
.prepare(`update files set file_id = :telegram_file_id, access_hash = :access_hash where id = :file_id returning id`)
.safeIntegers(true)
.pluck(true)
.get({ file_id, telegram_file_id, access_hash })
}
if (tg_user_id != BOT_ID) {
const project_id = db
.prepare(`select project_id from chats where telegram_id = :telegram_id`)
.safeIntegers(true)
.pluck(true)
.get({ telegram_id })
const customer_id = db
.prepare(`select customer_id from projects where id = :project_id`)
.get({ project_id })
if (!project_id || !customer_id)
return console.error ('Register document: project/customer is not found: ', file, project_id, customer_id)
filedata.project_id = project_id
filedata.id = registerFile (filedata)
} else {
filedata = db
.prepare(`select * from files where chat_id = :chat_id and filename = :filename`)
.safeIntegers(true)
.get({ chat_id, filename })
if (!filedata)
return
}
updateFileAccess(filedata.id, file.id?.value, file.accessHash?.value)
const upload_id = db
.prepare(`select upload_chat_id from customers where id = (select customer_id from projects where id = :project_id)`)
.safeIntegers(true)
.pluck(true)
.get(filedata)
if (!upload_id)
return console.error ('Upload chat is not set. Backup skipped for ', filedata.id)
if (upload_id == chat_id)
return
let data = file.buffer
if (is_photo) {
try {
const res = await downloadFile(filedata.project_id, filedata.id)
data = res.data
} catch (err) { }
}
if (!data)
return console.error ('No data for ', filedata.id)
const uploaddata = Object.assign({}, filedata, {
chat_id: upload_id,
data,
published_by: null,
parent_type: 3,
parent_id: filedata.id
})
sendFile(uploaddata)
}
if (msg.message?.startsWith(`/start@${BOT_NAME} KEY-`) || msg.message?.startsWith('KEY-')) {
const rows = db
.prepare(`
select 1 from chats where id = :chat_id and project_id is not null
union all
select 1 from customers where upload_chat_id = :chat_id
`)
.all({ chat_id })
if (rows.length)
return await sendMessage(chat_id, 'Чат уже используется')
const rawkey = msg.message.substr(msg.message?.indexOf('KEY-'))
const [_, time64, key] = rawkey.split('-')
const now = Math.floor(Date.now() / 1000)
const time = Buffer.from(time64, 'base64')
if (now - 3600 >= time && time >= now)
return await sendMessage(chat_id, 'Время действия ключа для привязки истекло')
const row = db
.prepare(`
select (select id from projects where generate_key(id, :time) = :rawkey) project_id,
(select id from customers where generate_key(-id, :time) = :rawkey) customer_id
`)
.get({ rawkey, time })
console.log ('PROJECT_ID: ', row.project_id)
if (row.project_id) {
await attachChat(chat_id, row.project_id)
await reloadChatUsers(chat_id)
}
if (row.customer_id) {
const info = db
.prepare(`update customers set upload_chat_id = :chat_id where id = :customer_id`)
.safeIntegers(true)
.run({ customer_id: row.customer_id, chat_id })
if (info.changes == 0)
console.error('Can\'t set upload chat: ' + chat_id + ' to customer: ' + row.customer_id)
}
}
}
async function onNewUserMessage (msg) {
if (msg.message == '/start' && msg.peerId?.className == 'PeerUser') {
const tg_user_id = msg.peerId?.userId?.value
const user_id = registerUser(tg_user_id)
try {
const user = await client.getEntity(new Api.PeerUser({ userId: tg_user_id }))
updateUser(user_id, user)
await updateUserPhoto (user_id, user)
const appButton = new Api.KeyboardButtonWebView({
text: "Open Mini-App", // Текст на кнопке
url: "https://h5sj0gpz-3000.euw.devtunnels.ms/", // URL вашего Mini-App (HTTPS!)
});
const inputPeer = new Api.InputPeerUser({userId: tg_user_id, accessHash: user.accessHash.value})
await client.sendMessage(inputPeer, {
message: 'Сообщение от бота',
buttons: client.buildReplyMarkup([
[Button.url('Админка', `https://t.me/${BOT_NAME}/userapp?startapp=admin`)],
[Button.url('Пользователь', `https://t.me/${BOT_NAME}/userapp?startapp=user`)],
[appButton]
])
})
} catch (err) {
console.error(msg.className + ', ' + user_id + ': ' + err.message)
}
}
}
async function onUpdatePaticipant (update, is_channel) {
const tg_chat_id = is_channel ? update.channelId?.value : update.chatId?.value
if (!tg_chat_id || update.userId?.value != BOT_ID)
return
const chat_id = await registerChat (tg_chat_id, is_channel)
const is_ban = update.prevParticipant && !update.newParticipant
const is_add = (!update.prevParticipant || update.prevParticipant?.className == 'ChannelParticipantBanned') && update.newParticipant
if (is_ban || is_add)
await reloadChatUsers(chat_id, is_ban)
if (is_ban) {
//db
// .prepare(`update chats set project_id = null where id = :chat_id`)
// .run({chat_id: chatId})
}
const bot_can_ban = +update.newParticipant?.adminRights?.banUsers || 0
db
.prepare(`update chats set bot_can_ban = :bot_can_ban where id = :chat_id`)
.run({ chat_id, bot_can_ban })
}
async function downloadFile(project_id, file_id) {
const file = db
.prepare(`
select file_id, access_hash, '' thumbSize, filename, mime
from files where id = :file_id and project_id = :project_id
`)
.safeIntegers(true)
.get({ project_id, file_id })
if (!file)
return false
const result = await client.downloadFile(new Api.InputDocumentFileLocation({
id: file.file_id,
accessHash: file.access_hash,
fileReference: Buffer.from(file.filename),
thumbSize: ''
}, {}))
return {
filename: file.filename,
mime: file.mime,
size: result.length,
data: result
}
}
async function sendMessage (chat_id, message) {
const chat = db
.prepare(`select telegram_id, access_hash, is_channel from chats where id = :chat_id`)
.get({ chat_id })
if (!chat)
return
const entity = chat.is_channel ? { channelId: chat.telegram_id, accessHash: chat.access_hash } : { chatId: chat.telegram_id, accessHash: chat.access_hash }
const peer = await client.getEntity( chat.is_channel ?
new Api.InputPeerChannel(entity) :
new Api.InputPeerChat(entity)
)
await client.sendMessage(peer, {message})
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
await delay(1000)
}
function registerFile(filedata) {
const file_id = db
.prepare(`
insert into files (project_id, chat_id, message_id, filename, mime, size, caption, published_by, published, parent_type, parent_id) values
(:project_id, :chat_id, :message_id, :filename, :mime, :size, :caption, :published_by, :published, :parent_type, :parent_id)
returning id
`)
.pluck(true)
.get(filedata)
return file_id
}
async function sendFile(filedata) {
const file_id = registerFile(filedata)
try {
const chat = db
.prepare(`select id, telegram_id, project_id, is_channel, access_hash from chats where id = :chat_id`)
.safeIntegers(true)
.get({ chat_id: filedata.chat_id })
if (!chat)
throw Error('CHAT_NOT_FOUND::404')
if (!chat.telegram_id || !chat.access_hash)
throw Error('CHAT_INACCESSABLE::404')
const peer = chat.is_channel ?
new Api.PeerChannel({ channelId: chat.telegram_id, accessHash: chat.access_hash }) :
new Api.PeerChat({ chatId: chat.telegram_id, accessHash: chat.access_hash })
const file = await client.uploadFile({ file: new CustomFile(filedata.filename, filedata.data.length, '', filedata.data), workers: 1 })
const media = new Api.InputMediaUploadedDocument({
file,
mimeType: filedata.mime,
attributes: [new Api.DocumentAttributeFilename({ fileName: filedata.filename })]
})
await client.invoke(new Api.messages.SendMedia({
peer,
media,
message: filedata.caption,
background: true,
silent: true
}))
} catch (err) {
db.prepare(`delete from files where id = :file_id`).get({ file_id })
console.error('SendFile', err)
}
return file_id
}
async function leaveChat (chat_id) {
const chat = db
.prepare(`select telegram_id, access_hash, is_channel from chats where id = :chat_id`)
.get({ chat_id })
if (!chat)
return
if (chat.is_channel) {
const inputPeer = await client.getEntity(new Api.InputPeerChannel({ channelId: chat.telegram_id, accessHash: chat.access_hash }))
await client.invoke(new Api.channels.LeaveChannel({ channel: inputPeer }))
} else {
await client.invoke(new Api.messages.DeleteChatUser({ chatId: chat.telegram_id, userId: this.id, accessHash: chat.access_hash }))
}
}
async function start (apiId, apiHash, botAuthToken, sid) {
BOT_ID = BigInt(botAuthToken.split(':')[0])
session= new StringSession(sid || '')
client = new TelegramClient(session, apiId, apiHash, {})
if (fs.existsSync('./debug.log'))
fs.unlinkSync('./debug.log')
client.addEventHandler(async (update) => {
if (update.className == 'UpdateConnectionState')
return
try {
debug(update)
if (update.className == 'UpdateNewMessage' || update.className == 'UpdateNewChannelMessage') {
const msg = update?.message
const is_channel = update.className == 'UpdateNewChannelMessage' ? 1 : 0
if (!msg)
return
const result = msg.peerId?.className == 'PeerUser' ? await onNewUserMessage(msg) :
msg.className == 'MessageService' ? await onNewServiceMessage(msg, is_channel) :
await onNewMessage(msg, is_channel)
}
if (update.className == 'UpdateChatParticipant' || update.className == 'UpdateChannelParticipant')
await onUpdatePaticipant(update, update.className == 'UpdateChannelParticipant')
} catch (err) {
console.error(err)
}
})
await client.start({botAuthToken})
}
module.exports = { start, downloadFile, reloadChatUsers, sendMessage, sendFile }