This commit is contained in:
2025-06-05 20:00:58 +03:00
parent c8f3c9801f
commit 1c732e16dd
203 changed files with 9793 additions and 3960 deletions

View File

@@ -1,25 +1,46 @@
const crypto = require('crypto')
const express = require('express')
const multer = require('multer')
const db = require('../include/db')
const bot = require('./bot')
const fs = require('fs')
const cookieParser = require('cookie-parser')
const app = express.Router()
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 1_000_000 // 1mb
}
})
const sessions = {}
const cache = {
// email -> code
register: {},
upgrade: {},
recovery: {},
'change-password': {},
'change-email': {},
'change-email2': {}
}
function checkEmail(email){
return String(email)
.toLowerCase()
.match(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/)
}
function sendEmail(email, subject, message) {
console.log(`${email} --> ${subject}: ${message}`)
}
function checkPassword(password) {
return password.length >= 8
}
app.use((req, res, next) => {
if (req.path == '/customer/login' ||
req.path == '/customer/register' ||
req.path == '/customer/activate')
const public = [
'/auth/email',
'/auth/telegram',
'/auth/email/register',
'/auth/email/recovery',
'/auth/logout'
]
if (public.includes(req.path))
return next()
const asid = req.query.asid || req.cookies.asid
@@ -31,105 +52,241 @@ app.use((req, res, next) => {
next()
})
// CUSTOMER
app.post('/customer/login', (req, res, next) => {
// AUTH
function createSession(req, res, customer_id) {
if (!customer_id)
throw Error('AUTH_ERROR::500')
res.locals.customer_id = customer_id
const asid = crypto.randomBytes(64).toString('hex')
req.session = sessions[asid] = {asid, customer_id }
res.setHeader('Set-Cookie', [`asid=${asid};httpOnly;path=/api/admin`])
}
app.post('/auth/email', (req, res, next) => {
res.locals.email = req.body?.email
res.locals.password = req.body?.password
let customer_id = db
.prepare(`
select id
from customers
where is_active = 1 and (
email is not null and email = :email and password is not null and password = :password or
email is null and password is null and telegram_user_id = :telegram_id
)
`)
const customer_id = db
.prepare(`select id from customers where email = :email and password is not null and password = :password `)
.pluck(true)
.get(res.locals)
if (!customer_id && !res.locals.email && !res.locals.password) {
customer_id = db
.prepare(`insert into customers (telegram_user_id, is_active) values (:telegram_id, 1) returning id`)
.safeIntegers(true)
.pluck(true)
.get(res.locals)
}
if (!customer_id)
throw Error('AUTH_ERROR::401')
res.locals.customer_id = customer_id
db
.prepare(`update customers set telegram_user_id = :telegram_id where id = :customer_id and email is not null`)
.run(res.locals)
const asid = crypto.randomBytes(64).toString('hex')
req.session = sessions[asid] = {asid, customer_id }
res.setHeader('Set-Cookie', [`asid=${asid};httpOnly;path=/api/admin`])
createSession(req, res, customer_id)
res.status(200).json({success: true})
})
app.post('/auth/telegram', (req, res, next) => {
let customer_id = db
.prepare(`select id from customers where telegram_id = :telegram_id`)
.pluck(true)
.get(res.locals) || db
.prepare(`replace into customers (telegram_id) values (:telegram_id) returning id`)
.pluck(true)
.get(res.locals)
createSession(req, res, customer_id)
res.status(200).json({success: true})
})
/*
Регистрация нового клиента/Перевод авторизации с TG на email выполняется за ТРИ последовательных вызова
1. Отравляется email. Если email корректный и уже неиспользуется, то сервер возвращает ОК и на указанный email отправляется код.
2. Отправляется email + код из письма. Если указан корректный код, то сервер отвечает ОК.
3. Отправляется email + код из письма + желаемый пароль. Если все ОК, то сервер создает учетную запись и возвращает ОК.
*/
app.post('/auth/email/:action(register|upgrade)', (req, res, next) => {
const email = String(req.body.email ?? '').trim()
const code = String(req.body.code ?? '').trim()
const password = String(req.body.password ?? '').trim()
const action = req.params.action
const stepNo = email && !code ? 1 : email && code && !password ? 2 : email && code && password ? 3 : -1
if (stepNo == -1)
throw Error('BAD_STEP::400')
if (stepNo == 1) {
if (!checkEmail(email))
throw Error('INCORRECT_EMAIL::400')
const customer_id = db
.prepare('select id from customers where email = :email')
.pluck(true)
.get({email})
if (customer_id)
throw Error('USED_EMAIL::400')
const code = Math.random().toString().substr(2, 4)
cache[action][email] = code
sendEmail(email, action.toUpperCase(), `${email} => ${code}`)
}
if (stepNo == 2) {
if (cache[action][email] != code)
throw Error('INCORRECT_CODE::400')
}
if (stepNo == 3) {
if (!checkPassword(password))
throw Error('INCORRECT_PASSWORD::400')
const query = action == 'register' ? 'insert into customers (email, password) values (:email, :password)' :
'update customers set email = :email, password = :password where id = :id'
db.prepare(query).run({email, password, id: res.locals.customer_id})
delete cache[action][email]
}
res.status(200).json({success: true})
})
/*
Смена email выполняется за ЧЕТЫРЕ последовательных вызовов
1. Отравляется пустой закпрос. Сервер на email пользователя из базы отправляет код.
2. Отправляется код из письма. Если указан корректный код, то сервер отвечает ОК.
3. Отправляется код из письма + новый email. Сервер отправляет код2 на новый email.
4. Отправлются оба кода и новый email. Если они проходят проверку, то сервер меняет email пользователя на новый и возвращает ОК.
*/
app.post('/auth/email/change-email', (req, res, next) => {
const email2 = String(req.body.email ?? '').trim()
const code = String(req.body.code ?? '').trim()
const code2 = String(req.body.code2 ?? '').trim()
const email = db
.prepare('select email from customers where id = :customer_id')
.pluck(true)
.get(res.locals)
const stepNo = !code ? 1 : code && !email ? 2 : code && email && !code2 ? 3 : code && email && code2 ? 4 : -1
if (stepNo == -1)
throw Error('BAD_STEP::400')
if (stepNo == 1) {
const code = Math.random().toString().substr(2, 4)
cache['change-email'][email] = code
sendEmail(email, 'CHANGE-EMAIL', `${email} => ${code}`)
}
if (stepNo == 2) {
if (cache['change-email'][email] != code)
throw Error('INCORRECT_CODE::400')
}
if (stepNo == 3) {
if (!checkEmail(email2))
throw Error('INCORRECT_EMAIL::400')
const code2 = Math.random().toString().substr(2, 4)
cache['change-email2'][email2] = code2
sendEmail(email2, 'CHANGE-EMAIL2', `${email2} => ${code2}`)
}
if (stepNo == 4) {
if (cache['change-email'][email] != code || cache['change-email2'][email2] != code2)
throw Error('INCORRECT_CODE::400')
const info = db
.prepare('update customers set email = :email where id = :customer_id')
.run(res.locals)
if (info.changes == 0)
throw Error('BAD_REQUEST::400')
delete cache['change-email'][email]
delete cache['change-email2'][email2]
}
res.status(200).json({success: true})
})
app.get('/customer/logout', (req, res, next) => {
delete sessions[req.session.asid]
res.setHeader('Set-Cookie', [`asid=; expired; httpOnly`])
/*
Смена пароля/восстановление доступа выполняется за ТРИ последовательных вызова
1. Отравляется пустой закпрос для смены запоса и email, в случае восстановления доступа. Сервер на email отправляет код.
2. Отправляется email + код из письма. Если указан корректный код, то сервер отвечает ОК.
3. Отправляется email + код из письма + новый пароль. Сервер изменяет пароль и возвращает ОК.
*/
app.post('/auth/email/:action(change-password|recovery)', (req, res, next) => {
const code = String(req.body.code ?? '').trim()
const password = String(req.body.password)
const action = req.params.action
const email = action == 'change-password' ? db
.prepare('select email from customers where id = :customer_id')
.pluck(true)
.get(res.locals) :
String(req.body.email ?? '').trim()
const stepNo = action == 'change-password' ?
(!code && !password ? 1 : code && !password ? 2 : code && password ? 3 : -1) :
(!email && !code && !password ? 1 : email && code && !password ? 2 : email && code && password ? 3 : -1)
if (stepNo == -1)
throw Error('BAD_STEP::400')
if (stepNo == 1) {
if (!checkEmail(email))
throw Error('INCORRECT_EMAIL::400')
const code = Math.random().toString().substr(2, 4)
cache[action][email] = code
sendEmail(email, action.toUpperCase(), `${email} => ${code}`)
}
if (stepNo == 2) {
if (cache[action][email] != code)
throw Error('INCORRECT_CODE::400')
}
if (stepNo == 3) {
if (cache[action][email] != code)
throw Error('INCORRECT_CODE::400')
if (!checkPassword(password))
throw Error('INCORRECT_PASSWORD::400')
const info = db
.prepare('update customers set password = :password where email = :email')
.run({ email, password })
if (info.changes == 0)
throw Error('BAD_REQUEST::400')
delete cache[action][email]
}
res.status(200).json({success: true})
})
app.post('/customer/register', (req, res, next) => {
const email = String(req.body.email).trim()
const password = String(req.body.password).trim()
app.get('/auth/logout', (req, res, next) => {
if (req.session?.asid)
delete sessions[req.session.asid]
const validateEmail = email => String(email).toLowerCase().match(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/)
if (!validateEmail(email))
throw Error('INCORRECT_EMAIL::400')
if (!password)
throw Error('EMPTY_PASSWORD::400')
const row = db
.prepare('select id from customers where email = :email')
.run({email})
if (row)
throw Error('DUPLICATE_EMAIL::400')
const key = crypto.randomBytes(32).toString('hex')
const info = db
.prepare('insert into customers (email, password, activation_key) values (:email, :password, :key)')
.run({email, password, key})
// To-Do: SEND MAIL
console.log(`http://127.0.0.1:3000/api/customer/activate?key=${key}`)
res.status(200).json({success: true, data: key})
})
app.get('/customer/activate', (req, res, next) => {
const row = db
.prepare('update customers set is_active = 1 where activation_key = :key returning id')
.get({key: req.query.key})
if (!row || !row.id)
throw Error('BAD_ACTIVATION_KEY::400')
res.status(200).json({success: true})
res.setHeader('Set-Cookie', [`asid=; expired; httpOnly;path=/api/admin`])
res.status(200).json({success: true})
})
// CUSTOMER
app.get('/customer/profile', (req, res, next) => {
const row = db
.prepare(`
select id, name, email, plan, coalesce(json_balance, '{}') json_balance, coalesce(json_company, '{}') json_company, upload_group_id
select id, name, email, plan,
coalesce(json_balance, '{}') json_balance, coalesce(json_company, '{}') json_company,
upload_chat_id, generate_key(-id, :time) upload_token
from customers
where id = :customer_id and is_active = 1
where id = :customer_id
`)
.get(res.locals)
if (row?.upload_group_id) {
row.upload_group = db
.prepare(`select id, name, telegram_id from groups where id = :group_id and project_id is null`)
if (row?.upload_chat_id) {
row.upload_chat = db
.prepare(`select id, name, telegram_id from chats where id = :chat_id and project_id is null`)
.safeIntegers(true)
.get({ group_id: row.upload_group_id})
delete row.upload_group_id
.get({ chat_id: row.upload_chat_id})
delete row.upload_chat_id
}
for (const key in row) {
@@ -145,6 +302,8 @@ app.get('/customer/profile', (req, res, next) => {
app.put('/customer/profile', (req, res, next) => {
if (req.body.company instanceof Object)
req.body.json_company = JSON.stringify(req.body.company)
else
delete req.body?.json_company
const info = db
.prepareUpdate(
@@ -160,96 +319,91 @@ app.put('/customer/profile', (req, res, next) => {
res.status(200).json({success: true})
})
// PROJECT
app.get('/project', (req, res, next) => {
const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : ''
app.get('/customer/settings', (req, res, next) => {
const row = db
.prepare(`select coalesce(json_settings, '{}') from customers where id = :customer_id`)
.pluck(true)
.get(res.locals)
res.status(200).json({success: true, data: JSON.parse(row)})
})
const rows = db
app.put('/customer/settings', (req, res, next) => {
res.locals.json_settings = JSON.stringify(req.body || {})
db
.prepare(`update customers set json_settings = :json_settings where id = :customer_id`)
.run(res.locals)
res.status(200).json({success: true})
})
// PROJECT
function getProject(id, customer_id) {
const row = db
.prepare(`
select id, name, description, logo
from projects
where customer_id = :customer_id ${where} and is_deleted <> 1
select id, name, description, logo, is_logo_bg, is_archived,
(select count(*) from chats where project_id = p.id) chat_count,
(select count(distinct user_id) from chat_users where chat_id in (select id from chats where project_id = p.id)) user_count
from projects p
where customer_id = :customer_id and p.id = :id
order by name
`)
.get({id, customer_id})
if (!row)
throw Error('NOT_FOUND::404')
row.is_archived = Boolean(row.is_archived)
row.is_logo_bg = Boolean(row.is_logo_bg)
return row
}
app.get('/project', (req, res, next) => {
const data = db
.prepare(`
select id, name, description, logo, is_logo_bg, is_archived,
(select count(*) from chats where project_id = p.id) chat_count,
(select count(distinct user_id) from chat_users where chat_id in (select id from chats where project_id = p.id)) user_count
from projects p
where customer_id = :customer_id
order by name
`)
.all(res.locals)
if (where && rows.length == 0)
throw Error('NOT_FOUND::404')
data.forEach(row => {
row.is_archived = Boolean(row.is_archived)
row.is_logo_bg = Boolean(row.is_logo_bg)
})
res.status(200).json({success: true, data: where ? rows[0] : rows})
})
app.get('/project/:pid(\\d+)', (req, res, next) => {
res.redirect(req.baseUrl + `/project?id=${req.params.pid}`)
res.status(200).json({success: true, data})
})
app.post('/project', (req, res, next) => {
res.locals.name = req.body?.name
res.locals.description = req.body?.description
res.locals.logo = req.body?.logo
res.locals.is_logo_bg = 'is_logo_bg' in req.body ? +req.body.is_logo_bg : undefined
const id = db
.prepare(`
insert into projects (customer_id, name, description, logo)
values (:customer_id, :name, :description, :logo)
insert into projects (customer_id, name, description, logo, is_logo_bg)
values (:customer_id, :name, :description, :logo, :is_logo_bg)
returning id
`)
.pluck(true)
.get(res.locals)
res.status(200).json({success: true, data: id})
const data = getProject(id, res.locals.customer_id)
res.status(200).json({success: true, data})
})
app.put('/project/:pid(\\d+)', (req, res, next) => {
res.locals.id = req.params.pid
res.locals.name = req.body?.name
res.locals.description = req.body?.description
res.locals.logo = req.body?.logo
const info = db
.prepareUpdate(
'projects',
['name', 'description', 'logo'],
res.locals,
['id', 'customer_id'])
.run(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true})
})
app.delete('/project/:pid(\\d+)', async (req, res, next) => {
res.locals.id = req.params.pid
const info = db
.prepare('update projects set id_deleted = 1 where id = :id and customer_id = :customer_id')
.run(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
const groupIds = db
.prepare(`select id from groups where project_id = :id`)
.pluck(true)
.all(res.locals)
for (const groupId of groupIds) {
await bot.sendMessage(groupId, 'Проект удален')
await bot.leaveGroup(groupId)
}
db.prepare(`updates groups set project_id = null where id in (${ groupIds.join(', ')})`).run()
res.status(200).json({success: true})
})
app.use ('/project/:pid(\\d+)/*', (req, res, next) => {
app.use ('(/project/:pid(\\d+)/*|/project/:pid(\\d+))', (req, res, next) => {
res.locals.project_id = parseInt(req.params.pid)
const row = db
.prepare('select 1 from projects where id = :project_id and customer_id = :customer_id and is_deleted <> 1')
.prepare('select 1 from projects where id = :project_id and customer_id = :customer_id and is_archived <> 1')
.get(res.locals)
if (!row)
@@ -258,55 +412,134 @@ app.use ('/project/:pid(\\d+)/*', (req, res, next) => {
next()
})
// USER
app.get('/project/:pid(\\d+)/user', (req, res, next) => {
const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : ''
app.get('/project/:pid(\\d+)', (req, res, next) => {
const data = getProject(req.params.pid, res.locals.customer_id)
res.status(200).json({success: true, data})
})
const rows = db
app.put('/project/:pid(\\d+)', (req, res, next) => {
res.locals.id = req.params.pid
res.locals.name = req.body?.name
res.locals.description = req.body?.description
res.locals.logo = req.body?.logo
res.locals.is_logo_bg = 'is_logo_bg' in req.body ? +req.body.is_logo_bg : undefined
const info = db
.prepareUpdate(
'projects',
['name', 'description', 'logo', 'is_logo_bg'],
res.locals,
['id', 'customer_id'])
.run(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
const data = getProject(req.params.pid, res.locals.customer_id)
res.status(200).json({success: true, data})
})
app.put('/project/:pid(\\d+)/:action(archive|restore)', async (req, res, next) => {
res.locals.id = req.params.pid
res.locals.is_archived = +(req.params.action == 'archive')
const info = db
.prepare(`
update projects
set is_archived = :is_archived
where id = :id and customer_id = :customer_id and coalesce(is_archived, 0) = not :is_archived
`)
.run(res.locals)
if (info.changes == 0)
throw Error('BAD_REQUEST::400')
const chatIds = db
.prepare(`select id from chats where project_id = :id`)
.pluck(true)
.all(res.locals)
for (const chatId of chatIds) {
await bot.sendMessage(chatId, res.locals.is_archived ? 'Проект помещен в архив. Отслеживание сообщений прекращено.' : 'Проект восстановлен из архива.')
}
const data = getProject(req.params.pid, res.locals.customer_id)
res.status(200).json({success: true, data})
})
// USER
function getUser(id, project_id) {
const row = db
.prepare(`
select u.id, u.telegram_id, u.firstname, u.lastname, u.username, u.photo,
ud.fullname, ud.role, ud.department, ud.is_blocked
ud.fullname, ud.email, ud.phone, ud.role, ud.department, ud.is_blocked,
cu.company_id
from users u
left join user_details ud on u.id = ud.user_id and ud.project_id = :project_id
left join company_users cu on u.id = cu.user_id
where id = :id
`)
.safeIntegers(true)
.all({id, project_id})
if (!row)
throw Error('NOT_FOUND::404')
row.is_blocked = Boolean(row.is_blocked)
return row
}
app.get('/project/:pid(\\d+)/user', (req, res, next) => {
const data = db
.prepare(`
select u.id, u.telegram_id, u.firstname, u.lastname, u.username, u.photo,
ud.fullname, ud.email, ud.phone, ud.role, ud.department, ud.is_blocked,
cu.company_id
from users u
left join user_details ud on u.id = ud.user_id and ud.project_id = :project_id
left join company_users cu on u.id = cu.user_id
where id in (
select user_id
from group_users
where group_id in (select id from groups where project_id = :project_id)
) ${where}
from chat_users
where chat_id in (select id from chats where project_id = :project_id)
)
`)
.safeIntegers(true)
.all(res.locals)
if (where && rows.length == 0)
throw Error('NOT_FOUND::404')
data.forEach(row => {
row.is_blocked = Boolean(row.is_blocked)
})
res.status(200).json({success: true, data: where ? rows[0] : rows})
res.status(200).json({success: true, data})
})
app.get('/project/:pid(\\d+)/user/:uid(\\d+)', (req, res, next) => {
res.redirect(req.baseUrl + `/project/${req.params.pid}/user?id=${req.params.uid}`)
const data = getUser(req.params.uid, req.params.pid)
res.status(200).json({success: true, data})
})
app.put('/project/:pid(\\d+)/user/:uid(\\d+)', (req, res, next) => {
res.locals.user_id = parseInt(req.params.uid)
res.locals.fullname = req.body?.fullname
res.locals.email = req.body?.email
res.locals.phone = req.body?.phone
res.locals.role = req.body?.role
res.locals.department = req.body?.department
res.locals.is_blocked = req.body?.is_blocked
res.locals.is_blocked = 'is_blocked' in req.body ? +req.body.is_blocked : undefined
const info = db
.prepareUpdate('user_details',
['fullname', 'role', 'department', 'is_blocked'],
.prepareUpsert('user_details',
['fullname', 'email', 'phone', 'role', 'department', 'is_blocked'],
res.locals,
['user_id', 'project_id']
)
.all(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
.run(res.locals)
res.status(200).json({success: true})
const data = getUser(req.params.uid, req.params.pid)
res.status(200).json({success: true, data})
})
app.get('/project/:pid(\\d+)/token', (req, res, next) => {
@@ -324,33 +557,47 @@ app.get('/project/:pid(\\d+)/token', (req, res, next) => {
})
// COMPANY
app.get('/project/:pid(\\d+)/company', (req, res, next) => {
const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : ''
const rows = db
function getCompany(id, project_id) {
const row = db
.prepare(`
select id, name, email, phone, description, logo,
select id, name, address, email, phone, site, description, logo,
(select json_group_array(user_id) from company_users where company_id = c.id) users
from companies c
where project_id = :project_id ${where}
where c.id = :id and project_id = :project_id
order by name
`)
.get({id, project_id})
if (!row)
throw Error('NOT_FOUND::404')
return row
}
app.get('/project/:pid(\\d+)/company', (req, res, next) => {
const data = db
.prepare(`
select id, name, address, email, phone, site, description, logo,
(select json_group_array(user_id) from company_users where company_id = c.id) users
from companies c
where project_id = :project_id
order by name
`)
.all(res.locals)
rows.forEach(row => row.users = JSON.parse(row.users || '[]'))
data.forEach(row => row.users = JSON.parse(row.users || '[]'))
if (where && rows.length == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true, data: where ? rows[0] : rows})
res.status(200).json({success: true, data})
})
app.get('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
res.redirect(req.baseUrl + `/project/${req.params.pid}/company?id=${req.params.cid}`)
const data = getCompany(req.params.cid, req.params.pid)
res.status(200).json({success: true, data})
})
app.post('/project/:pid(\\d+)/company', (req, res, next) => {
res.locals.name = req.body?.name
res.locals.address = req.body?.address
res.locals.email = req.body?.email
res.locals.phone = req.body?.phone
res.locals.site = req.body?.site
@@ -359,28 +606,31 @@ app.post('/project/:pid(\\d+)/company', (req, res, next) => {
const id = db
.prepare(`
insert into companies (project_id, name, email, phone, site, description, logo)
values (:project_id, :name, :email, :phone, :site, :description, :logo)
insert into companies (project_id, name, address, email, phone, site, description, logo)
values (:project_id, :name, :address, :email, :phone, :site, :description, :logo)
returning id
`)
.pluck(res.locals)
.pluck(true)
.get(res.locals)
res.status(200).json({success: true, data: id})
const data = getCompany(id, req.params.pid)
res.status(200).json({success: true, data})
})
app.put('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
res.locals.id = parseInt(req.params.cid)
res.locals.name = req.body?.name
res.locals.address = req.body?.address
res.locals.email = req.body?.email
res.locals.phone = req.body?.phone
res.locals.site = req.body?.site
res.locals.description = req.body?.description
res.locals.logo = req.body?.logo
const info = db
.prepareUpdate(
'companies',
['name', 'email', 'phone', 'site', 'description'],
['name', 'address', 'email', 'phone', 'site', 'description', 'logo'],
res.locals,
['id', 'project_id'])
.run(res.locals)
@@ -388,11 +638,12 @@ app.put('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true})
const data = getCompany(req.params.cid, req.params.pid)
res.status(200).json({success: true, data})
})
app.delete('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
res.locals.company_id = parseInt(req.params.cid)
res.locals.company_id = req.params.cid
const info = db
.prepare(`delete from companies where id = :company_id and project_id = :project_id`)
@@ -401,42 +652,7 @@ app.delete('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true})
})
app.get('/project/:pid(\\d+)/group', (req, res, next) => {
const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : ''
const rows = db
.prepare(`
select id, name, telegram_id, is_channel, user_count, bot_can_ban
from groups
where project_id = :project_id ${where}
`)
.all(res.locals)
if (where && rows.length == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true, data: where ? rows[0] : rows})
})
app.get('/project/:pid(\\d+)/group/:gid(\\d+)', (req, res, next) => {
res.redirect(req.baseUrl + `/project/${req.params.pid}/group?id=${req.params.uid}`)
})
app.delete('/project/:pid(\\d+)/group/:gid(\\d+)', async (req, res, next) => {
res.locals.group_id = parseInt(req.params.gid)
const info = db
.prepare(`update groups set project_id = null where id = :group_id and project_id = :project_id`)
.run(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
await bot.sendMessage(res.locals.group_id, 'Группа удалена из проекта')
res.status(200).json({success: true})
res.status(200).json({success: true, data: {id: req.params.cid}})
})
app.put('/project/:pid(\\d+)/company/:cid(\\d+)/user', (req, res, next) => {
@@ -456,8 +672,8 @@ app.put('/project/:pid(\\d+)/company/:cid(\\d+)/user', (req, res, next) => {
let rows = db
.prepare(`
select user_id
from group_users
where group_id in (select id from groups where project_id = :project_id)
from chat_users
where chat_id in (select id from chats where project_id = :project_id)
`)
.pluck(true) // .raw?
.get(res.locals)
@@ -478,7 +694,6 @@ app.put('/project/:pid(\\d+)/company/:cid(\\d+)/user', (req, res, next) => {
if (user_ids.some(user_id => !rows.contains(user_id)))
throw Error('USED_MEMBER::400')
db
.prepare(`delete from company_users where company_id = :company_id`)
.run(res.locals)
@@ -493,4 +708,59 @@ app.put('/project/:pid(\\d+)/company/:cid(\\d+)/user', (req, res, next) => {
res.status(200).json({success: true})
})
// CHATS
function getChat(id, project_id) {
const row = db
.prepare(`
select id, name, telegram_id, is_channel, description, logo, user_count, bot_can_ban
from chats c
where c.id = :id and project_id = :project_id
`)
.all({id, project_id})
if (!row)
throw Error('NOT_FOUND::404')
row.is_channel = Boolean(row.is_channel)
row.bot_can_ban = Boolean(row.bot_can_ban)
return row
}
app.get('/project/:pid(\\d+)/chat', (req, res, next) => {
const data = db
.prepare(`
select id, name, telegram_id, is_channel, description, logo, user_count, bot_can_ban
from chats
where project_id = :project_id
`)
.all(res.locals)
data.forEach(row => {
row.is_channel = Boolean(row.is_channel)
row.bot_can_ban = Boolean(row.bot_can_ban)
})
res.status(200).json({success: true, data})
})
app.get('/project/:pid(\\d+)/chat/:gid(\\d+)', (req, res, next) => {
const data = getChat(req.params.gid, req.params.pid)
res.status(200).json({success: true, data})
})
app.delete('/project/:pid(\\d+)/chat/:gid(\\d+)', async (req, res, next) => {
res.locals.chat_id = req.params.gid
const info = db
.prepare(`update chats set project_id = null where id = :chat_id and project_id = :project_id`)
.run(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
await bot.sendMessage(res.locals.chat_id, 'Чат удален из проекта')
res.status(200).json({success: true, data: {id: req.params.gid}})
})
module.exports = app

View File

@@ -1,19 +1,14 @@
const util = require('util')
const crypto = require('crypto')
const EventEmitter = require('events')
const fs = require('fs')
const db = require('../include/db')
const { Api, TelegramClient } = require('telegram')
const { StringSession } = require('telegram/sessions')
const { NewMessage } = require('telegram/events')
const { Button } = require('telegram/tl/custom/button')
const { CustomFile } = require('telegram/client/uploads')
// const session = new StringSession('1AgAOMTQ5LjE1NC4xNjcuNTABuxdIxmjimA0hmWpdrlZ4Fo7uoIGU4Bu9+G5QprS6zdtyeMfcssWEZp0doLRX/20MomQyF4Opsos0El0Ifj5aiNgg01z8khMLMeT98jS+1U/sh32p3GxZfxyXSxX1bD0NLRaXnqVyNNswYqRZPhboT28NMjDqwlz0nrW9rge+QMJDL7jIkXgSs+cmJBINiqsEI8jWjXmc8TU/17gngtjUHRf5kRM4y5gsNC4O8cF5lcHRx0G/U5ZVihTID8ItQ6EdEHjz6e4XErbVOJ81PfYkqEoPXVvkEmRM0/VbvCzFfixfas4Vzczfn98OHLd8P2MXcgokZ2rppvIV3fQXOHxJbA0=')
const session = new StringSession('')
let session
let client
let BOT_ID
const BOT_NAME = 'ready_or_not_2025_bot'
function registerUser (telegramId) {
db
@@ -78,121 +73,125 @@ async function updateUserPhoto (userId, data) {
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: file.toString('base64') })
.run({ user_id: userId, photo_id: photoId, photo: 'data:image/jpg;base64,' + file.toString('base64') })
}
async function registerGroup (telegramId, isChannel) {
db
.prepare(`insert or ignore into groups (telegram_id, is_channel) values (:telegram_id, :is_channel)`)
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)
.run({ telegram_id: telegramId, is_channel: +isChannel })
.get({telegram_id: telegramId})
const row = db
.prepare(`select id, name from groups where telegram_id = :telegram_id`)
.safeIntegers(true)
.get({telegram_id: telegramId})
if (!row?.name) {
const entity = isChannel ? { channelId: telegramId } : { chatId: telegramId }
const group = await client.getEntity(isChannel ? new Api.PeerChannel(entity) : new Api.PeerChat(entity))
db
.prepare(`update groups set name = :name where id = :group_id`)
.run({ group_id: row.id, name: group.title })
}
return row.id
}
async function attachGroup(groupId, isChannel, projectId) {
const info = db
.prepare(`update groups set project_id = :project_id where id = :group_id and coalesce(project_id, 1) = 1`)
.run({ group_id: groupId, project_id: projectId })
if (info.changes == 1) {
const inputPeer = isChannel ?
new Api.InputPeerChannel({ channelId: tgGroupId }) :
new Api.InputPeerChat({ chatlId: tgGroupId })
const query = `select (select name from customers where id = p.customer_id) || ' >> ' || p.name from projects p where id = :project_id`
const message = db
.prepare(query)
.pluck(true)
.get({project_id: projectId})
if (message)
await client.sendMessage(inputPeer, {message})
}
return info.changes == 1
}
async function onGroupAttach (tgGroupId, isChannel) {
const projectId = db
.prepare(`select project_id from groups where telegram_id = :telegram_id`)
.safeIntegers(true)
.pluck(true)
.get({ telegram_id: tgGroupId })
const entity = isChannel ? { channelId: tgGroupId } : { chatId: tgGroupId }
const inputPeer = await client.getEntity( isChannel ?
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 resultBtn = await client.sendMessage(inputPeer, {
message: 'ReadyOrNot',
buttons: client.buildReplyMarkup([[Button.url('Открыть проект', 'https://t.me/ready_or_not_2025_bot/userapp?startapp=user_' + projectId)]])
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: inputPeer,
peer,
id: resultBtn.id,
unpin: false
}))
//fs.appendFileSync('./1.log', '\n>' + tgGroupId + ':' + isChannel + '<\n')
}
async function reloadGroupUsers(groupId, onlyReset) {
async function reloadChatUsers(chatId, onlyReset) {
db
.prepare(`delete from group_users where group_id = :group_id`)
.run({ group_id: groupId })
.prepare(`delete from chat_users where chat_id = :chat_id`)
.run({ chat_id: chatId })
if (onlyReset)
return
const group = db
.prepare(`select telegram_id, is_channel, access_hash from groups where id = :group_id`)
.get({ group_id: groupId})
const chat = db
.prepare(`select telegram_id, is_channel, access_hash from chats where id = :chat_id`)
.get({ chat_id: chatId})
console.log (123, group)
if (!group)
if (!chat)
return
const tgGroupId = group.telegram_id
const isChannel = group.is_channel
let accessHash = group.access_hash
console.log ('HERE')
db
.prepare(`update groups set access_hash = :access_hash where id = :group_id`)
.safeIntegers(true)
.run({
group_id: groupId,
access_hash: accessHash,
})
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: tgGroupId }),
channel: new Api.PeerChannel({ channelId: tgChatId, accessHash }),
filter: new Api.ChannelParticipantsRecent(),
limit: 999999,
offset: 0
})) : await client.invoke(new Api.messages.GetFullChat({
chatId: tgGroupId,
}))
})) : await client.invoke(new Api.messages.GetFullChat({ chatId: tgChatId, accessHash }))
const users = result.users.filter(user => !user.bot)
for (const user of users) {
@@ -201,37 +200,42 @@ async function reloadGroupUsers(groupId, onlyReset) {
if (updateUser(userId, user)) {
await updateUserPhoto (userId, user)
const query = `insert or ignore into group_users (group_id, user_id) values (:group_id, :user_id)`
db.prepare(query).run({ group_id: groupId, user_id: userId })
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 false
return console.error ('registerUpload: ' + (data.projectId ? 'media' : 'project id') + ' is missing')
const uploadGroup = db
.prepare(`
select id, telegram_id, project_id, is_channel, access_hash
from groups
where id = (select upload_group_id
from customers
where id = (select customer_id from projects where id = :project_id limit 1)
limit 1)
limit 1
`)
.safeIntegers(true)
const customer_id = db
.prepare(`select customer_id from projects where project_id = :project_id`)
.pluck(true)
.get({project_id: data.projectId})
if (!uploadGroup || !uploadGroup.telegram_id || uploadGroup.id == data.originGroupId)
return false
if (!customer_id)
return console.error ('registerUpload: The customer is not found for project: ' + data.projectId)
const tgUploadGroupId = uploadGroup.telegram_id
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 })
const peer = uploadGroup.is_channel ?
new Api.PeerChannel({ channelId: tgUploadGroupId }) :
new Api.PeerChat({ chatlId: tgUploadGroupId })
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
@@ -247,26 +251,26 @@ async function registerUpload(data) {
const update = result.updates.find(u =>
(u.className == 'UpdateNewMessage' || u.className == 'UpdateNewChannelMessage') &&
u.message.className == 'Message' &&
(u.message.peerId.channelId?.value == tgUploadGroupId || u.message.peerId.chatId?.value == tgUploadGroupId) &&
(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 documents (project_id, origin_group_id, origin_message_id, group_id, message_id,
file_id, access_hash, filename, mime, caption, size, published_by, parent_type, parent_id)
values (:project_id, :origin_group_id, :origin_message_id, :group_id, :message_id,
:file_id, :access_hash, :filename, :mime, :caption, :size, :published_by, :parent_type, :parent_id)
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_group_id: data.originGroupId,
origin_chat_id: data.originchatId,
origin_message_id: data.originMessageId,
group_id: uploadGroup.id,
chat_id: chat.id,
message_id: update.message.id,
file_id: udoc.id.value,
filename: udoc.attributes.find(attr => attr.className == 'DocumentAttributeFilename')?.fileName,
@@ -275,14 +279,15 @@ async function registerUpload(data) {
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('Message.registerUpload: ' + err.message)
//fs.appendFileSync('./1.log', '\n\nERR:' + err.message + ':' + JSON.stringify (err.stack)+'\n\n')
console.error('registerUpload: ' + err.message)
}
return resultId
@@ -290,14 +295,14 @@ async function registerUpload(data) {
async function onNewServiceMessage (msg, isChannel) {
const action = msg.action || {}
const tgGroupId = isChannel ? msg.peerId?.channelId?.value : msg.peerId?.chatId?.value
const groupId = await registerGroup(tgGroupId, isChannel)
const tgChatId = isChannel ? msg.peerId?.channelId?.value : msg.peerId?.chatId?.value
const chatId = await registerChat(tgChatId, isChannel)
// Group/Channel rename
// Сhat rename
if (action.className == 'MessageActionChatEditTitle') {
const info = db
.prepare(`
update groups
update chats
set name = :name, is_channel = :is_channel, last_update_time = :last_update_time
where telegram_id = :telegram_id
`)
@@ -306,15 +311,18 @@ async function onNewServiceMessage (msg, isChannel) {
name: action.title,
is_channel: +isChannel,
last_update_time: Math.floor (Date.now() / 1000),
telegram_id: tgGroupId
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 groups
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
`)
@@ -322,9 +330,12 @@ async function onNewServiceMessage (msg, isChannel) {
.run({
name: action.title,
last_update_time: Date.now() / 1000,
old_telegram_id: tgGroupId,
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
@@ -335,7 +346,7 @@ async function onNewServiceMessage (msg, isChannel) {
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) {
if (tgUserIds.indexOf(BOT_ID) == -1) {
// Add/remove non-bot users
for (const tgUserId of tgUserIds) {
const userId = registerUser(tgUserId)
@@ -351,27 +362,29 @@ async function onNewServiceMessage (msg, isChannel) {
}
const query = isAdd ?
`insert or ignore into group_users (group_id, user_id) values (:group_id, :user_id)` :
`delete from group_users where group_id = :group_id and user_id = :user_id`
db.prepare(query).run({ group_id: groupId, user_id: userId })
`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 tgGroupId = isChannel ? msg.peerId?.channelId?.value : msg.peerId?.chatId?.value
const groupId = await registerGroup(tgGroupId, 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 groups where telegram_id = :telegram_id`)
.prepare(`select project_id from chats where telegram_id = :telegram_id`)
.safeIntegers(true)
.pluck(true)
.get({telegram_id: tgGroupId})
.get({telegram_id: tgChatId})
const media = new Api.InputMediaDocument({
id: new Api.InputDocument({
@@ -385,101 +398,55 @@ async function onNewMessage (msg, isChannel) {
projectId,
media,
caption: msg.message,
originGroupId: groupId,
originchatId: chatId,
originMessageId: msg.id,
parentType: 0,
publishedBy: registerUser (msg.fromId?.userId?.value)
publishedBy: registerUser (msg.fromId?.userId?.value),
published: msg.date
})
}
if (msg.message?.startsWith('KEY-')) {
let projectName = db
if (msg.message?.startsWith(`/start@${BOT_NAME} KEY-`) || msg.message?.startsWith('KEY-')) {
const rows = db
.prepare(`
select name
from projects
where id in (
select project_id from groups where id = :group_id
union
select id from projects where upload_group_id = :group_id)
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
`)
.pluck(true)
.get({ group_id: groupId })
.all({ chat_id: chatId })
if (projectName)
return await bot.sendMessage(groupId, 'Группа уже используется на проекте ' + projectName)
if (rows.length)
return await sendMessage(chatId, 'Чат уже используется')
const [_, time64, key] = msg.message.substr(3).split('-')
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 bot.sendMessage(groupId, 'Время действия ключа для привязки истекло')
return await sendMessage(chatId, 'Время действия ключа для привязки истекло')
const projectId = db
.prepare(`select id from projects where generate_key(id, :time) = :key`)
.pluck(true)
.get({ key: msg.message.trim(), time })
if (projectId) {
await attachGroup(groupId, isChannel, projectId)
await onGroupAttach(tgGroupId, isChannel)
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 (msg.message?.startsWith('/start')) {
// Called by https://t.me/ready_or_not_2025_bot?startgroup=<customer_id/project_id>
if (/start@ready_or_not_2025_bot (-|)([\d]+)$/g.test(msg.message)) {
const tgUserId = msg.fromId?.userId?.value
const param = +msg.message.split(' ')[1]
// Set upload group for customer
if (param < 0) {
const customerId = -param
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 })
db
.prepare(`
update customers
set upload_group_id = :group_id
where id = :customer_id and telegram_user_id = :telegram_user_id
`)
.safeIntegers(true)
.run({
group_id: groupId,
customer_id: customerId,
telegram_user_id: tgUserId
})
}
// Add group to project
if (param > 0) {
const projectId = param
const customerId = db
.prepare(`select customer_id from projects where id = :project_id`)
.pluck(true)
.get({project_id: projectId})
db
.prepare(`
update groups
set project_id = :project_id
where id = :group_id and exists(
select 1
from customers
where id = :customer_id and telegram_user_id = :telegram_user_id)
`)
.safeIntegers(true)
.run({
project_id: projectId,
group_id: groupId,
customer_id: customerId,
telegram_user_id: tgUserId
})
await reloadGroupUsers(groupId, false)
await onGroupAttach(tgGroupId, isChannel)
}
}
if (info.changes == 0)
console.error('Can\'t set upload chat: ' + chatId + ' to customer: ' + row.customer_id)
}
}
}
@@ -493,12 +460,19 @@ async function onNewUserMessage (msg) {
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})
const resultBtn = await client.sendMessage(inputPeer, {
await client.sendMessage(inputPeer, {
message: 'Сообщение от бота',
buttons: client.buildReplyMarkup([
[Button.url('Админка', 'https://t.me/ready_or_not_2025_bot/userapp?startapp=admin')],
[Button.url('Пользователь', 'https://t.me/ready_or_not_2025_bot/userapp?startapp=user')]
[Button.url('Админка', `https://t.me/${BOT_NAME}/userapp?startapp=admin`)],
[Button.url('Пользователь', `https://t.me/${BOT_NAME}/userapp?startapp=user`)],
[appButton]
])
})
} catch (err) {
@@ -508,41 +482,125 @@ async function onNewUserMessage (msg) {
}
async function onUpdatePaticipant (update, isChannel) {
const tgGroupId = isChannel ? update.channelId?.value : update.chatlId?.value
if (!tgGroupId || update.userId?.value != bot.id)
const tgChatId = isChannel ? update.channelId?.value : update.chatlId?.value
if (!tgChatId || update.userId?.value != BOT_ID)
return
const groupId = await registerGroup (tgGroupId, isChannel)
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 reloadGroupUsers(groupId, isBan)
await reloadChatUsers(chatId, isBan)
if (isBan) {
db
.prepare(`update groups set project_id = null where id = :group_id`)
.run({group_id: groupId})
.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 groups set bot_can_ban = :bot_can_ban where id = :group_id`)
.run({group_id: groupId, bot_can_ban: +botCanBan})
.prepare(`update chats set bot_can_ban = :bot_can_ban where id = :chat_id`)
.run({chat_id: chatId, bot_can_ban: +botCanBan})
}
class Bot extends EventEmitter {
async start (apiId, apiHash, botAuthToken) {
this.id = 7236504417n
async function uploadFile(projectId, fileName, mime, data, parentType, parentId, publishedBy) {
const file = await client.uploadFile({ file: new CustomFile(fileName, data.length, '', data), workers: 1 })
client = new TelegramClient(session, apiId, apiHash, {})
const media = new Api.InputMediaUploadedDocument({
file,
mimeType: mime,
attributes: [new Api.DocumentAttributeFilename({ fileName })]
})
client.addEventHandler(async (update) => {
if (update.className == 'UpdateConnectionState')
return
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'
@@ -557,98 +615,13 @@ class Bot extends EventEmitter {
if (update.className == 'UpdateChatParticipant' || update.className == 'UpdateChannelParticipant')
await onUpdatePaticipant(update, update.className == 'UpdateChannelParticipant')
})
await client.start({botAuthToken})
}
async uploadDocument(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
})
}
async downloadDocument(projectId, documentId) {
const document = db
.prepare(`
select file_id, access_hash, '' thumbSize, filename, mime
from documents where id = :document_id and project_id = :project_id
`)
.safeIntegers(true)
.get({project_id: projectId, document_id: documentId})
if (!document)
return false
const result = await client.downloadFile(new Api.InputDocumentFileLocation({
id: document.file_id,
accessHash: document.access_hash,
fileReference: Buffer.from(document.filename),
thumbSize: ''
}, {}))
return {
filename: document.filename,
mime: document.mime,
size: result.length,
data: result
} catch (err) {
console.error(err)
}
}
})
async reloadGroupUsers(groupId, onlyReset) {
return reloadGroupUsers(groupId, onlyReset)
}
await client.start({botAuthToken})
}
async sendMessage (groupId, message) {
const group = db
.prepare(`select telegram_id, is_channel from groups where id = :group_id`)
.get({ group_id: groupId})
if (!group)
return
const entity = group.is_channel ? { channelId: group.telegram_id } : { chatId: group.telegram_id }
const inputPeer = await client.getEntity( group.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 leaveGroup (groupId) {
const group = db
.prepare(`select telegram_id, is_channel from groups where id = :group_id`)
.get({ group_id: groupId})
if (!group)
return
if (group.is_channel) {
const inputPeer = await client.getEntity(new Api.InputPeerChannel({ channelId: group.telegram_id }))
await client.invoke(new Api.channels.LeaveChannel({ channel: inputPeer }))
} else {
await client.invoke(new Api.messages.DeleteChatUser({ chatId: group.telegram_id, userId: this.id }))
}
}
}
const bot = new Bot()
module.exports = bot
module.exports = { start, uploadFile, downloadFile, reloadChatUsers, sendMessage }

View File

@@ -5,15 +5,15 @@ create table if not exists customers (
name text check(name is null or trim(name) <> '' and length(name) < 256),
email text check(email is null or trim(email) <> '' and length(email) < 128),
password text check(password is null or length(password) > 7 and length(password) < 64),
telegram_user_id integer,
telegram_id integer,
plan integer,
json_balance text default '{}',
activation_key text,
is_active integer default 0,
is_blocked integer default 0,
json_company text default '{}',
upload_group_id integer,
upload_chat_id integer,
json_backup_server text default '{}',
json_backup_params text default '{}'
json_backup_params text default '{}',
json_settings text default '{}'
) strict;
create table if not exists projects (
@@ -22,34 +22,36 @@ create table if not exists projects (
name text not null check(trim(name) <> '' and length(name) < 256),
description text check(description is null or length(description) < 4096),
logo text,
is_deleted integer default 0
is_logo_bg integer default 0,
is_archived integer default 0
) strict;
create table if not exists groups (
create table if not exists chats (
id integer primary key autoincrement,
project_id integer references projects(id) on delete cascade,
name text,
telegram_id integer,
description text,
logo text,
access_hash integer,
is_channel integer check(is_channel in (0, 1)) default 0,
bot_can_ban integer default 0,
owner_id integer references users(id) on delete set null,
user_count integer,
last_update_time integer
);
create unique index if not exists idx_groups_telegram_id on groups (telegram_id);
create unique index if not exists idx_chats_telegram_id on chats (telegram_id);
create table if not exists users (
id integer primary key autoincrement,
telegram_id integer,
access_hash integer,
firstname text,
lastname text,
username text,
firstname text check(firstname is null or length(firstname) < 256),
lastname text check(lastname is null or length(lastname) < 256),
username text check(username is null or length(username) < 256),
photo_id integer,
photo text,
language_code text,
phone text,
json_phone_projects text default '[]',
json_settings text default '{}'
) strict;
create unique index if not exists idx_users_telegram_id on users (telegram_id);
@@ -57,9 +59,11 @@ create unique index if not exists idx_users_telegram_id on users (telegram_id);
create table if not exists user_details (
user_id integer references users(id) on delete cascade,
project_id integer references projects(id) on delete cascade,
fullname text,
role text,
department text,
fullname text check(fullname is null or length(fullname) < 256),
email text check(email is null or length(email) < 256),
phone text check(phone is null or length(phone) < 256),
role text check(role is null or length(role) < 256),
department text check(department is null or length(department) < 256),
is_blocked integer check(is_blocked in (0, 1)) default 0,
primary key (user_id, project_id)
) strict;
@@ -88,12 +92,12 @@ create table if not exists meetings (
meet_date integer
) strict;
create table if not exists documents (
create table if not exists files (
id integer primary key autoincrement,
project_id integer references projects(id) on delete cascade,
origin_group_id integer references groups(id) on delete set null,
origin_chat_id integer references chats(id) on delete set null,
origin_message_id integer,
group_id integer references groups(id) on delete set null,
chat_id integer references chats(id) on delete set null,
message_id integer,
file_id integer,
access_hash integer,
@@ -102,6 +106,7 @@ create table if not exists documents (
caption text check(caption is null or length(caption) < 4096),
size integer,
published_by integer references users(id) on delete set null,
published integer,
parent_type integer check(parent_type in (0, 1, 2)) default 0,
parent_id integer,
backup_state integer default 0
@@ -111,6 +116,7 @@ create table if not exists companies (
id integer primary key autoincrement,
project_id integer references projects(id) on delete cascade,
name text not null check(length(name) < 4096),
address text check(address is null or length(address) < 512),
email text check(email is null or length(email) < 128),
phone text check(phone is null or length(phone) < 128),
site text check(site is null or length(site) < 128),
@@ -143,20 +149,19 @@ create table if not exists company_users (
primary key (company_id, user_id)
) without rowid;
create table if not exists group_users (
group_id integer references groups(id) on delete cascade,
create table if not exists chat_users (
chat_id integer references chats(id) on delete cascade,
user_id integer references users(id) on delete cascade,
primary key (group_id, user_id)
primary key (chat_id, user_id)
) without rowid;
pragma foreign_keys = on;
create trigger if not exists trg_groups_update after update
on groups
create trigger if not exists trg_chats_update after update on chats
when NEW.project_id is null
begin
delete from group_users where group_id = NEW.id;
delete from chat_users where chat_id = NEW.id;
end;