Files
tgCrewUser/backend/apps/bot.js
2025-06-05 20:00:58 +03:00

628 lines
19 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 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 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)
)
const chatId = db
.prepare(`replace into chats (telegram_id, is_channel, access_hash, name) values (:telegram_id, :is_channel, :access_hash, :name) 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 (chatId) {
const chat = db
.prepare(`select id, telegram_id, access_hash, is_channel from chats where id = :id`)
.safeIntegers(true)
.get({id: chatId})
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 description = :description, logo = :logo, user_count = :user_count, last_update_time = :last_update_time where id = :id`)
.safeIntegers(true)
.run({
id: chatId,
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(chatId, projectId) {
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: chatId, project_id: projectId })
if (!chat.telegram_id)
return console.error('Can\'t attach chat: ' + chatId + ' to project: ' + projectId)
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 (select name from customers where id = p.customer_id) || ' >> ' || p.name from projects p where id = :project_id`)
.pluck(true)
.get({project_id: projectId})
const resultBtn = await client.sendMessage(peer, {
message,
buttons: client.buildReplyMarkup([[Button.url('Открыть проект', `https://t.me/${BOT_NAME}/userapp?startapp=` + projectId)]])
})
await client.invoke(new Api.messages.UpdatePinnedMessage({
peer,
id: resultBtn.id,
unpin: false
}))
}
async function reloadChatUsers(chatId, onlyReset) {
db
.prepare(`delete from chat_users where chat_id = :chat_id`)
.run({ chat_id: chatId })
if (onlyReset)
return
const chat = db
.prepare(`select telegram_id, is_channel, access_hash from chats where id = :chat_id`)
.get({ chat_id: chatId})
if (!chat)
return
const tgChatId = chat.telegram_id
const isChannel = chat.is_channel
const accessHash = chat.access_hash
const result = isChannel ?
await client.invoke(new Api.channels.GetParticipants({
channel: new Api.PeerChannel({ channelId: tgChatId, accessHash }),
filter: new Api.ChannelParticipantsRecent(),
limit: 999999,
offset: 0
})) : await client.invoke(new Api.messages.GetFullChat({ chatId: tgChatId, accessHash }))
const users = result.users.filter(user => !user.bot)
for (const user of users) {
const userId = registerUser(user.id.value, user)
if (updateUser(userId, user)) {
await updateUserPhoto (userId, user)
db
.prepare(`insert or ignore into chat_users (chat_id, user_id) values (:chat_id, :user_id)`)
.run({ chat_id: chatId, user_id: userId })
}
}
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: chatId})
}
async function registerUpload(data) {
if (!data.projectId || !data.media)
return console.error ('registerUpload: ' + (data.projectId ? 'media' : 'project id') + ' is missing')
const customer_id = db
.prepare(`select customer_id from projects where project_id = :project_id`)
.pluck(true)
.get({project_id: data.projectId})
if (!customer_id)
return console.error ('registerUpload: The customer is not found for project: ' + data.projectId)
const chat = db
.prepare(
`select id, telegram_id, project_id, is_channel, access_hash from chats
where id = (select upload_chat_id from customers where id = :customer_id`)
.safeIntegers(true)
.get({ customer_id })
if (!chat || !chat.telegram_id || chat.id == data.originchatId)
return console.error ('registerUpload: The upload chat is not set for customer: ' + customer_id)
const peer = chat.is_channel ?
new Api.PeerChannel({ channelId: chat.telegram_id, accessHash: chat.access_hash }) :
new Api.PeerChat({ chatlId: chat.telegram_id, accessHash: chat.access_hash })
let resultId = 0
try {
const result = await client.invoke(new Api.messages.SendMedia({
peer,
media: data.media,
message: data.caption || '',
background: true,
silent: true
}))
const update = result.updates.find(u =>
(u.className == 'UpdateNewMessage' || u.className == 'UpdateNewChannelMessage') &&
u.message.className == 'Message' &&
(u.message.peerId.channelId?.value == chat.telegram_id || u.message.peerId.chatId?.value == chat.telegram_id) &&
u.message.media)
const udoc = update?.message?.media?.document
if (udoc) {
resultId = db
.prepare(`
insert into files (project_id, origin_chat_id, origin_message_id, chat_id, message_id,
file_id, access_hash, filename, mime, caption, size, published_by, published, parent_type, parent_id)
values (:project_id, :origin_chat_id, :origin_message_id, :chat_id, :message_id,
:file_id, :access_hash, :filename, :mime, :caption, :size, :published_by, :published, :parent_type, :parent_id)
returning id
`)
.safeIntegers(true)
.pluck(true)
.get({
project_id: data.projectId,
origin_chat_id: data.originchatId,
origin_message_id: data.originMessageId,
chat_id: chat.id,
message_id: update.message.id,
file_id: udoc.id.value,
filename: udoc.attributes.find(attr => attr.className == 'DocumentAttributeFilename')?.fileName,
access_hash: udoc.accessHash.value,
mime: udoc.mimeType,
caption: data.caption,
size: udoc.size.value,
published_by: data.publishedBy,
published: data.published,
parent_type: data.parentType,
parent_id: data.parentId
})
}
} catch (err) {
//fs.appendFileSync('./1.log', '\n\nERR:' + err.message + ':' + JSON.stringify (err.stack)+'\n\n')
console.error('registerUpload: ' + err.message)
}
return resultId
}
async function onNewServiceMessage (msg, isChannel) {
const action = msg.action || {}
const tgChatId = isChannel ? msg.peerId?.channelId?.value : msg.peerId?.chatId?.value
const chatId = await registerChat(tgChatId, isChannel)
// С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: +isChannel,
last_update_time: Math.floor (Date.now() / 1000),
telegram_id: tgChatId
})
if (info.changes == 0)
console.error('onNewServiceMessage: Can\'t update a chat title: ' + tgChatId)
}
// 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: tgChatId,
new_telegram_id: action.channelId.value
})
if (info.changes == 0)
console.error('onNewServiceMessage: Can\'t apply a chat migration to channel: ' + tgChatId)
}
// 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 userId = registerUser(tgUserId)
if (isAdd) {
try {
const user = await client.getEntity(new Api.PeerUser({ userId: tgUserId }))
updateUser(userId, user)
await updateUserPhoto (userId, user)
} catch (err) {
console.error(msg.className + ', ' + userId + ': ' + 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: chatId, user_id: userId })
}
}
}
}
async function onNewMessage (msg, isChannel) {
const tgChatId = isChannel ? msg.peerId?.channelId?.value : msg.peerId?.chatId?.value
const chatId = await registerChat(tgChatId, isChannel)
// Document is detected
if (msg.media?.document) {
const doc = msg.media.document
const projectId = db
.prepare(`select project_id from chats where telegram_id = :telegram_id`)
.safeIntegers(true)
.pluck(true)
.get({telegram_id: tgChatId})
const media = new Api.InputMediaDocument({
id: new Api.InputDocument({
id: doc.id.value,
accessHash: doc.accessHash.value,
fileReference: doc.fileReference
})
})
await registerUpload({
projectId,
media,
caption: msg.message,
originchatId: chatId,
originMessageId: msg.id,
parentType: 0,
publishedBy: registerUser (msg.fromId?.userId?.value),
published: msg.date
})
}
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: chatId })
if (rows.length)
return await sendMessage(chatId, 'Чат уже используется')
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(chatId, 'Время действия ключа для привязки истекло')
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 })
if (row.project_id) {
await attachChat(chatId, row.project_id)
await reloadChatUsers(chatId)
}
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: chatId })
if (info.changes == 0)
console.error('Can\'t set upload chat: ' + chatId + ' to customer: ' + row.customer_id)
}
}
}
async function onNewUserMessage (msg) {
if (msg.message == '/start' && msg.peerId?.className == 'PeerUser') {
const tgUserId = msg.peerId?.userId?.value
const userId = registerUser(tgUserId)
try {
const user = await client.getEntity(new Api.PeerUser({ userId: tgUserId }))
updateUser(userId, user)
await updateUserPhoto (userId, 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: tgUserId, 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 + ', ' + userId + ': ' + err.message)
}
}
}
async function onUpdatePaticipant (update, isChannel) {
const tgChatId = isChannel ? update.channelId?.value : update.chatlId?.value
if (!tgChatId || update.userId?.value != BOT_ID)
return
const chatId = await registerChat (tgChatId, isChannel)
const isBan = update.prevParticipant && !update.newParticipant
const isAdd = (!update.prevParticipant || update.prevParticipant?.className == 'ChannelParticipantBanned') && update.newParticipant
if (isBan || isAdd)
await reloadChatUsers(chatId, isBan)
if (isBan) {
db
.prepare(`update chats set project_id = null where id = :chat_id`)
.run({chat_id: chatId})
}
const botCanBan = update.newParticipant?.adminRights?.banUsers || 0
db
.prepare(`update chats set bot_can_ban = :bot_can_ban where id = :chat_id`)
.run({chat_id: chatId, bot_can_ban: +botCanBan})
}
async function uploadFile(projectId, fileName, mime, data, parentType, parentId, publishedBy) {
const file = await client.uploadFile({ file: new CustomFile(fileName, data.length, '', data), workers: 1 })
const media = new Api.InputMediaUploadedDocument({
file,
mimeType: mime,
attributes: [new Api.DocumentAttributeFilename({ fileName })]
})
return await registerUpload({
projectId,
media,
parentType,
parentId,
publishedBy,
published: Math.floor(Date.now() / 1000)
})
}
async function downloadFile(projectId, fileId) {
const file = db
.prepare(`
select file_id, access_hash, '' thumbSize, filename, mime
from files where id = :id and project_id = :project_id
`)
.safeIntegers(true)
.get({project_id: projectId, id: fileId})
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 (chatId, message) {
const chat = db
.prepare(`select telegram_id, is_channel from chats where id = :chat_id`)
.get({ chat_id: chatId})
if (!chat)
return
const entity = chat.is_channel ? { channelId: chat.telegram_id } : { chatId: chat.telegram_id }
const inputPeer = await client.getEntity( chat.is_channel ?
new Api.InputPeerChannel(entity) :
new Api.InputPeerChat(entity)
)
await client.sendMessage(inputPeer, {message})
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
await delay(1000)
}
async function leaveChat (chatId) {
const chat = db
.prepare(`select telegram_id, access_hash, is_channel from chats where id = :chat_id`)
.get({ chat_id: chatId})
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, {})
client.addEventHandler(async (update) => {
if (update.className == 'UpdateConnectionState')
return
try {
// console.log(update)
if (update.className == 'UpdateNewMessage' || update.className == 'UpdateNewChannelMessage') {
const msg = update?.message
const isChannel = update.className == 'UpdateNewChannelMessage'
if (!msg)
return
const result = msg.peerId?.className == 'PeerUser' ? await onNewUserMessage(msg) :
msg.className == 'MessageService' ? await onNewServiceMessage(msg, isChannel) :
await onNewMessage(msg, isChannel)
}
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, uploadFile, downloadFile, reloadChatUsers, sendMessage }