This commit is contained in:
2025-05-04 22:22:20 +03:00
parent cda54b1e95
commit ebd77a3e66
54 changed files with 1194 additions and 2580 deletions

11
backend/.env Normal file
View File

@@ -0,0 +1,11 @@
# node --env-file .env app.js
# Usage: process.env.PORT
# https://nodejs.org/en/learn/command-line/how-to-read-environment-variables-from-nodejs
PORT=3000
API_ID=26746106
API_HASH=29e5f83c04e635fa583721473a6003b5
BOT_TOKEN=7236504417:AAGVaodw3cRwGlf-jAhwnYb51OHaXcgpW8k
BOT_SID=1AgAOMTQ5LjE1NC4xNjcuNTEBu6zLey/IpmODwaKLyTbkKwFDY28LSFPf4UZpgSKCO4bP5gGtFOGVmNDhsJxhMtUWNzhyOX46GyDliNiZ4FUQdoQ6G93DEN8mYcREljmiCp5JchNyZPmhGxl2GeclPo0tp9T/yXFUyo7PD8YpuykHH/MdWVyZxPp93Pjjpi+E03DKCwD00tEpi2TAGzW/MyQ8HUAUIK45nkJA7dnv8Up7NB9LWJ2z+8Dx81oGdVYyOHBL9qy9722LyKtvLD47KpwINjJyZOdhdBM1W8bhsGE4JkHs6DXFXOzrmMrWaE30z4corikkQoNIDL/tXttv+bJULQbbyGZvskbXuvwkV/NVen0=

Binary file not shown.

BIN
backend/_old/backend_v2.zip Normal file

Binary file not shown.

BIN
backend/_old/backend_v3.zip Normal file

Binary file not shown.

1
backend/app.bat Normal file
View File

@@ -0,0 +1 @@
node --env-file .env app

View File

@@ -8,14 +8,14 @@ const bot = require('./apps/bot')
const app = express()
app.use(bodyParser.json())
app.use(bodyParser.json({limit: '10mb'}))
app.use(cookieParser())
BigInt.prototype.toJSON = function () {
return Number(this)
}
/* app.use((req, res, next) => {
app.use((req, res, next) => {
if(!(req.body instanceof Object))
return next()
@@ -26,14 +26,14 @@ BigInt.prototype.toJSON = function () {
.map(key => req.body[key] = escapeHtml(req.body[key]))
next()
}) */
})
app.post('(/api/admin/auth/telegram|/api/miniapp/auth)', (req, res, next) => {
const data = Object.assign({}, req.query)
delete data.hash
const hash = req.query?.hash
const BOT_TOKEN = '7236504417:AAGVaodw3cRwGlf-jAhwnYb51OHaXcgpW8k'
const BOT_TOKEN = process.env.BOT_TOKEN || '7236504417:AAGVaodw3cRwGlf-jAhwnYb51OHaXcgpW8k'
const dataCheckString = Object.keys(data).sort().map((key) => `${key}=${data[key]}`).join('\n')
const secretKey = crypto.createHmac('sha256', 'WebAppData').update(BOT_TOKEN).digest()
const hmac = crypto.createHmac('sha256', secretKey).update(dataCheckString).digest('hex')
@@ -59,20 +59,19 @@ app.use('/api/miniapp', require('./apps/miniapp'))
app.use((err, req, res, next) => {
console.error(`Error for ${req.path}: ${err}`)
console.trace()
console.log('\n\n')
let message, code
[message, code = 500] = err.message.split('::')
res.status(code).json({success: false, error: { message, code}})
res.status(+code).json({success: false, error: { message, code}})
})
app.use(express.static('public'))
const PORT = process.env.PORT || 3000
app.listen(PORT, async () => {
console.log(`Listening at port ${PORT}`)
bot.start(
process.env.API_ID || 26746106,
+(process.env.API_ID || 26746106),
process.env.API_HASH || '29e5f83c04e635fa583721473a6003b5',
process.env.BOT_TOKEN || '7236504417:AAGVaodw3cRwGlf-jAhwnYb51OHaXcgpW8k'
)

View File

@@ -1,24 +1,45 @@
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 emailCache = {} // key = email, value = code
const cache = {
// email -> code
register: {},
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 == '/auth/email' || req.path == '/auth/telegram' || req.path == '/auth/register' || req.path == '/auth/logout')
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
@@ -59,10 +80,10 @@ app.post('/auth/email', (req, res, next) => {
app.post('/auth/telegram', (req, res, next) => {
let customer_id = db
.prepare(`select id from customers where is_blocked = 0 and telegram_id = :telegram_id`)
.prepare(`select id from customers where telegram_id = :telegram_id`)
.pluck(true)
.get(res.locals) || db
.prepare(`replace into customers (telegram_id, is_blocked) values (:telegram_id, 0) returning id`)
.prepare(`replace into customers (telegram_id) values (:telegram_id) returning id`)
.pluck(true)
.get(res.locals)
@@ -70,73 +91,198 @@ app.post('/auth/telegram', (req, res, next) => {
res.status(200).json({success: true})
})
app.get('/auth/logout', (req, res, next) => {
if (req.session?.asid)
delete sessions[req.session.asid]
res.setHeader('Set-Cookie', [`asid=; expired; httpOnly;path=/api/admin`])
res.status(200).json({success: true})
})
app.post('/auth/register', (req, res, next) => {
/*
Регистрация нового клиента выполняется за ТРИ последовательных вызова
1. Отравляется email. Если email корректный и уже неиспользуется, то сервер возвращает ОК и на указанный email отправляется код.
2. Отправляется email + код из письма. Если указан корректный код, то сервер отвечает ОК.
3. Отправляется email + код из письма + желаемый пароль. Если все ОК, то сервер создает учетную запись и возвращает ОК.
*/
app.post('/auth/email/register', (req, res, next) => {
const email = String(req.body.email ?? '').trim()
const code = String(req.body.code ?? '').trim()
const password = String(req.body.password ?? '').trim()
if (email) {
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')
const customer_id = db
.prepare('select id from customers where email = :email')
.pluck(true)
.get({email})
if (customer_id)
throw Error('USED_EMAIL::400')
}
if (email && !code) {
const code = Math.random().toString().substr(2, 4)
emailCache[email] = code
// To-Do: send email
console.log(`${email} => ${code}`)
}
if (email && code && !password) {
if (emailCache[email] != code)
throw Error('INCORRECT_CODE::400')
}
if (email && code && password) {
if (password.length < 8)
throw Error('INCORRECT_PASSWORD::400')
db
.prepare('insert into customers (email, password, is_blocked) values (:email, :password, 0)')
.run({email, password})
}
res.status(200).json({success: true})
})
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.register[email] = code
sendEmail(email, 'REGISTER', `${email} => ${code}`)
}
if (stepNo == 2) {
if (cache.register[email] != code)
throw Error('INCORRECT_CODE::400')
}
if (stepNo == 3) {
if (!checkPassword(password))
throw Error('INCORRECT_PASSWORD::400')
db
.prepare('insert into customers (email, password) values (:email, :password)')
.run({email, password})
delete cache.register[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})
})
/*
Смена пароля/восстановление доступа выполняется за ТРИ последовательных вызова
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.get('/auth/logout', (req, res, next) => {
if (req.session?.asid)
delete sessions[req.session.asid]
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
from customers
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) {
@@ -167,15 +313,36 @@ app.put('/customer/profile', (req, res, next) => {
res.status(200).json({success: true})
})
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)})
})
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
app.get('/project', (req, res, next) => {
const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : ''
const rows = 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 ${where}
order by name
`)
.all(res.locals)
@@ -204,7 +371,7 @@ app.post('/project', (req, res, next) => {
.pluck(true)
.get(res.locals)
res.status(200).json({success: true, data: id})
res.redirect(req.baseUrl + `/project?id=${id}`)
})
app.put('/project/:pid(\\d+)', (req, res, next) => {
@@ -212,11 +379,12 @@ app.put('/project/:pid(\\d+)', (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 = req.body?.is_logo_bg
const info = db
.prepareUpdate(
'projects',
['name', 'description', 'logo'],
['name', 'description', 'logo', 'is_logo_bg'],
res.locals,
['id', 'customer_id'])
.run(res.locals)
@@ -224,39 +392,41 @@ app.put('/project/:pid(\\d+)', (req, res, next) => {
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true})
res.redirect(req.baseUrl + `/project?id=${req.params.pid}`)
})
app.delete('/project/:pid(\\d+)', async (req, res, next) => {
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 id_deleted = 1 where id = :id and customer_id = :customer_id')
.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('NOT_FOUND::404')
throw Error('BAD_REQUEST::400')
const groupIds = db
.prepare(`select id from groups where project_id = :id`)
const chatIds = db
.prepare(`select id from chats where project_id = :id`)
.pluck(true)
.all(res.locals)
for (const groupId of groupIds) {
await bot.sendMessage(groupId, 'Проект удален')
await bot.leaveGroup(groupId)
for (const chatId of chatIds) {
await bot.sendMessage(chatId, res.locals.is_archived ? 'Проект помещен в архив. Отслеживание сообщений прекращено.' : 'Проект восстановлен из архива.')
}
db.prepare(`updates groups set project_id = null where id in (${ groupIds.join(', ')})`).run()
res.status(200).json({success: true})
res.redirect(req.baseUrl + `/project?id=${req.params.pid}`)
})
app.use ('/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)
@@ -277,8 +447,8 @@ app.get('/project/:pid(\\d+)/user', (req, res, next) => {
left join user_details ud on u.id = ud.user_id and ud.project_id = :project_id
where id in (
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)
) ${where}
`)
.safeIntegers(true)
@@ -337,7 +507,7 @@ app.get('/project/:pid(\\d+)/company', (req, res, next) => {
const rows = db
.prepare(`
select id, name, email, phone, description, logo,
(select json_group_array(user_id) from company_users where company_id = c.id) users
(select json_chat_array(user_id) from company_users where company_id = c.id) users
from companies c
where project_id = :project_id ${where}
order by name
@@ -373,7 +543,7 @@ app.post('/project/:pid(\\d+)/company', (req, res, next) => {
.pluck(res.locals)
.get(res.locals)
res.status(200).json({success: true, data: id})
res.redirect(req.baseUrl + `/project/${req.params.pid}/company?id=${id}`)
})
app.put('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
@@ -395,7 +565,7 @@ 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})
res.redirect(req.baseUrl + `/project/${req.params.pid}/company?id=${req.params.cid}`)
})
app.delete('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
@@ -411,13 +581,13 @@ app.delete('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
res.status(200).json({success: true})
})
app.get('/project/:pid(\\d+)/group', (req, res, next) => {
app.get('/project/:pid(\\d+)/chat', (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
from chats
where project_id = :project_id ${where}
`)
.all(res.locals)
@@ -428,20 +598,20 @@ app.get('/project/:pid(\\d+)/group', (req, res, next) => {
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.get('/project/:pid(\\d+)/chat/:gid(\\d+)', (req, res, next) => {
res.redirect(req.baseUrl + `/project/${req.params.pid}/chat?id=${req.params.uid}`)
})
app.delete('/project/:pid(\\d+)/group/:gid(\\d+)', async (req, res, next) => {
res.locals.group_id = parseInt(req.params.gid)
app.delete('/project/:pid(\\d+)/chat/:gid(\\d+)', async (req, res, next) => {
res.locals.chat_id = parseInt(req.params.gid)
const info = db
.prepare(`update groups set project_id = null where id = :group_id and project_id = :project_id`)
.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.group_id, 'Группа удалена из проекта')
await bot.sendMessage(res.locals.chat_id, 'Чат удален из проекта')
res.status(200).json({success: true})
})
@@ -463,8 +633,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)

View File

@@ -10,9 +10,7 @@ const { NewMessage } = require('telegram/events')
const { Button } = require('telegram/tl/custom/button')
const { CustomFile } = require('telegram/client/uploads')
//const session = new StringSession('1AgAOMTQ5LjE1NC4xNjcuNTABu2OaFuD5Oyi5wGck+n5ldAfshzYfwlWee+OUxYBvFzlKAdW11Hsndu1SJBLUnKjP8sTJEPbLwdqANBhBXmQMghLVAblwK6TxLfsWxy2zf/HGLeNXohhrsep0hBxu9imyHV6OI6gQG+c5qaGkzjZrz0AcS4ut0xy99XrXgjiNfnjeMX7a0mOk6IK9iKdwbX9kXTfclFLVppiBGXolYJjVb2E57tk4+7RncIVyw+Fxn0NZfnhEfHJZly6j03arZOeM5VYl9ul8+3lJDD+KJJHeMgImmYjmcFcF3CbtkhPuTSPnWKtCnm2sRzepn5VFfoG6zgYff04fBdKGvHAai+wQSOY=')
const session = new StringSession('1AgAOMTQ5LjE1NC4xNjcuNTEBuzSgmBQR5/m8M8cyOnsLCIOkYQJTizJoJRZiPKK+eBjMuodc0JuKQwzeWBRJI/c6YxaBHvokpngf5kr57uly+meSPPlFq6MyoSSQDbEJ3VAAWJu+/ALN0ickE92RjRfM5Kw6DimC9FXuMgJJsoUHtk/i+ZGXy9JB+q67G0yy8NvFIuWpFHJDkwmi0qTlTgJ5UOm4PYkV01iNUcV5siaWFVTTLsetHtBUdMOzg5WjjvuOyYV/MIx+z7ynhvF3DxLPCugxqhCvZ/RW+0vldrTX5TZ0BzIDk2eNFQjRORJcZo6upwvH7aZYStV4DxhIi1dEYu5gyvnt4vkbR5kuvE/GqO0=')
const session = new StringSession(process.env.BOT_SID || '')
let client
@@ -82,38 +80,38 @@ async function updateUserPhoto (userId, data) {
.run({ user_id: userId, photo_id: photoId, photo: file.toString('base64') })
}
async function registerGroup (telegramId, isChannel) {
async function registerChat (telegramId, isChannel) {
db
.prepare(`insert or ignore into groups (telegram_id, is_channel) values (:telegram_id, :is_channel)`)
.prepare(`insert or ignore into chats (telegram_id, is_channel) values (:telegram_id, :is_channel)`)
.safeIntegers(true)
.run({ telegram_id: telegramId, is_channel: +isChannel })
const row = db
.prepare(`select id, name from groups where telegram_id = :telegram_id`)
.prepare(`select id, name from chats 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))
const chat = 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 })
.prepare(`update chats set name = :name where id = :chat_id`)
.run({ chat_id: row.id, name: chat.title })
}
return row.id
}
async function attachGroup(groupId, isChannel, projectId) {
async function attachChat(chatId, 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 })
.prepare(`update chats set project_id = :project_id where id = :chat_id and coalesce(project_id, 1) = 1`)
.run({ chat_id: chatId, project_id: projectId })
if (info.changes == 1) {
const inputPeer = isChannel ?
new Api.InputPeerChannel({ channelId: tgGroupId }) :
new Api.InputPeerChat({ chatlId: tgGroupId })
new Api.InputPeerChannel({ channelId: tgChatId }) :
new Api.InputPeerChat({ chatId: tgChatId })
const query = `select (select name from customers where id = p.customer_id) || ' >> ' || p.name from projects p where id = :project_id`
const message = db
@@ -127,14 +125,14 @@ async function attachGroup(groupId, isChannel, projectId) {
return info.changes == 1
}
async function onGroupAttach (tgGroupId, isChannel) {
async function onChatAttach (tgChatId, isChannel) {
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 entity = isChannel ? { channelId: tgGroupId } : { chatId: tgGroupId }
const entity = isChannel ? { channelId: tgChatId } : { chatId: tgChatId }
const inputPeer = await client.getEntity( isChannel ?
new Api.InputPeerChannel(entity) :
new Api.InputPeerChat(entity)
@@ -151,48 +149,44 @@ async function onGroupAttach (tgGroupId, isChannel) {
unpin: false
}))
//fs.appendFileSync('./1.log', '\n>' + tgGroupId + ':' + isChannel + '<\n')
//fs.appendFileSync('./1.log', '\n>' + tgChatId + ':' + 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')
const tgChatId = chat.telegram_id
const isChannel = chat.is_channel
let accessHash = chat.access_hash
db
.prepare(`update groups set access_hash = :access_hash where id = :group_id`)
.prepare(`update chats set access_hash = :access_hash where id = :chat_id`)
.safeIntegers(true)
.run({
group_id: groupId,
chat_id: chatId,
access_hash: accessHash,
})
const result = isChannel ?
await client.invoke(new Api.channels.GetParticipants({
channel: new Api.PeerChannel({ channelId: tgGroupId }),
channel: new Api.PeerChannel({ channelId: tgChatId }),
filter: new Api.ChannelParticipantsRecent(),
limit: 999999,
offset: 0
})) : await client.invoke(new Api.messages.GetFullChat({
chatId: tgGroupId,
chatId: tgChatId,
}))
const users = result.users.filter(user => !user.bot)
@@ -202,8 +196,8 @@ 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 })
const query = `insert or ignore into chat_users (chat_id, user_id) values (:chat_id, :user_id)`
db.prepare(query).run({ chat_id: chatId, user_id: userId })
}
}
}
@@ -212,11 +206,11 @@ async function registerUpload(data) {
if (!data.projectId || !data.media)
return false
const uploadGroup = db
const uploadChat = db
.prepare(`
select id, telegram_id, project_id, is_channel, access_hash
from groups
where id = (select upload_group_id
from chats
where id = (select upload_chat_id
from customers
where id = (select customer_id from projects where id = :project_id limit 1)
limit 1)
@@ -225,14 +219,14 @@ async function registerUpload(data) {
.safeIntegers(true)
.get({project_id: data.projectId})
if (!uploadGroup || !uploadGroup.telegram_id || uploadGroup.id == data.originGroupId)
if (!uploadChat || !uploadChat.telegram_id || uploadChat.id == data.originchatId)
return false
const tgUploadGroupId = uploadGroup.telegram_id
const tgUploadChatId = uploadChat.telegram_id
const peer = uploadGroup.is_channel ?
new Api.PeerChannel({ channelId: tgUploadGroupId }) :
new Api.PeerChat({ chatlId: tgUploadGroupId })
const peer = uploadChat.is_channel ?
new Api.PeerChannel({ channelId: tgUploadChatId }) :
new Api.PeerChat({ chatlId: tgUploadChatId })
let resultId = 0
@@ -248,16 +242,16 @@ 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 == tgUploadChatId || u.message.peerId.chatId?.value == tgUploadChatId) &&
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,
insert into documents (project_id, origin_chat_id, origin_message_id, chat_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,
values (:project_id, :origin_chat_id, :origin_message_id, :chat_id, :message_id,
:file_id, :access_hash, :filename, :mime, :caption, :size, :published_by, :parent_type, :parent_id)
returning id
`)
@@ -265,9 +259,9 @@ async function registerUpload(data) {
.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: uploadChat.id,
message_id: update.message.id,
file_id: udoc.id.value,
filename: udoc.attributes.find(attr => attr.className == 'DocumentAttributeFilename')?.fileName,
@@ -291,14 +285,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
// Ghat 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
`)
@@ -307,7 +301,7 @@ 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
})
}
@@ -315,7 +309,7 @@ async function onNewServiceMessage (msg, isChannel) {
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
`)
@@ -323,7 +317,7 @@ 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
})
}
@@ -352,27 +346,27 @@ 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({
@@ -386,7 +380,7 @@ 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)
@@ -399,22 +393,22 @@ async function onNewMessage (msg, isChannel) {
select name
from projects
where id in (
select project_id from groups where id = :group_id
select project_id from chats where id = :chat_id
union
select id from projects where upload_group_id = :group_id)
select id from projects where upload_chat_id = :chat_id)
`)
.pluck(true)
.get({ group_id: groupId })
.get({ chat_id: chatId })
if (projectName)
return await bot.sendMessage(groupId, 'Группа уже используется на проекте ' + projectName)
return await bot.sendMessage(chatId, 'Группа уже используется на проекте ' + projectName)
const [_, time64, key] = msg.message.substr(3).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 bot.sendMessage(chatId, 'Время действия ключа для привязки истекло')
const projectId = db
.prepare(`select id from projects where generate_key(id, :time) = :key`)
@@ -422,8 +416,8 @@ async function onNewMessage (msg, isChannel) {
.get({ key: msg.message.trim(), time })
if (projectId) {
await attachGroup(groupId, isChannel, projectId)
await onGroupAttach(tgGroupId, isChannel)
await attachChat(chatId, isChannel, projectId)
await onChatAttach(tgChatId, isChannel)
}
}
@@ -440,12 +434,12 @@ async function onNewMessage (msg, isChannel) {
db
.prepare(`
update customers
set upload_group_id = :group_id
set upload_chat_id = :chat_id
where id = :customer_id and telegram_user_id = :telegram_user_id
`)
.safeIntegers(true)
.run({
group_id: groupId,
chat_id: chatId,
customer_id: customerId,
telegram_user_id: tgUserId
})
@@ -462,9 +456,9 @@ async function onNewMessage (msg, isChannel) {
db
.prepare(`
update groups
update chats
set project_id = :project_id
where id = :group_id and exists(
where id = :chat_id and exists(
select 1
from customers
where id = :customer_id and telegram_user_id = :telegram_user_id)
@@ -472,13 +466,13 @@ async function onNewMessage (msg, isChannel) {
.safeIntegers(true)
.run({
project_id: projectId,
group_id: groupId,
chat_id: chatId,
customer_id: customerId,
telegram_user_id: tgUserId
})
await reloadGroupUsers(groupId, false)
await onGroupAttach(tgGroupId, isChannel)
await reloadChatUsers(chatId, false)
await onChatAttach(tgChatId, isChannel)
}
}
}
@@ -516,34 +510,34 @@ 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
this.id = BigInt(botAuthToken.split(':')[0])
client = new TelegramClient(session, apiId, apiHash, {})
@@ -568,7 +562,7 @@ class Bot extends EventEmitter {
})
await client.start({botAuthToken})
console.log('SID: ', session.save())
console.log('BOT_SID: ', session.save())
}
async uploadDocument(projectId, fileName, mime, data, parentType, parentId, publishedBy) {
@@ -616,20 +610,20 @@ class Bot extends EventEmitter {
}
}
async reloadGroupUsers(groupId, onlyReset) {
return reloadGroupUsers(groupId, onlyReset)
async reloadChatUsers(chatId, onlyReset) {
return reloadChatUsers(chatId, onlyReset)
}
async sendMessage (groupId, message) {
const group = db
.prepare(`select telegram_id, is_channel from groups where id = :group_id`)
.get({ group_id: groupId})
async sendMessage (chatId, message) {
const chat = db
.prepare(`select telegram_id, is_channel from chats where id = :chat_id`)
.get({ chat_id: chatId})
if (!group)
if (!chat)
return
const entity = group.is_channel ? { channelId: group.telegram_id } : { chatId: group.telegram_id }
const inputPeer = await client.getEntity( group.is_channel ?
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)
)
@@ -640,19 +634,19 @@ class Bot extends EventEmitter {
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})
async leaveChat (chatId) {
const chat = db
.prepare(`select telegram_id, is_channel from chats where id = :chat_id`)
.get({ chat_id: chatId})
if (!group)
if (!chat)
return
if (group.is_channel) {
const inputPeer = await client.getEntity(new Api.InputPeerChannel({ channelId: group.telegram_id }))
if (chat.is_channel) {
const inputPeer = await client.getEntity(new Api.InputPeerChannel({ channelId: chat.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 }))
await client.invoke(new Api.messages.DeleteChatUser({ chatId: chat.telegram_id, userId: this.id }))
}
}
}

View File

@@ -19,9 +19,9 @@ function hasAccess(project_id, user_id) {
return !!db
.prepare(`
select 1
from group_users
from chat_users
where user_id = :user_id and
group_id in (select id from groups where project_id = :project_id) and
chat_id in (select id from chats where project_id = :project_id) and
not exists(select 1 from user_details where user_id = :user_id and project_id = :project_id and is_blocked = 1) and
not exists(select 1 from projects where id = :project_id and is_deleted = 1)
`)
@@ -70,13 +70,13 @@ app.get('/project', (req, res, next) => {
const rows = db
.prepare(`
select p.id, p.name, p.description, p.logo,
c.name customer_name, c.upload_group_id <> 0 has_upload
c.name customer_name, c.upload_chat_id <> 0 has_upload
from projects p
inner join customers c on p.customer_id = c.id
where p.id in (
select project_id
from groups
where id in (select group_id from group_users where user_id = :user_id)
from chats
where id in (select chat_id from chat_users where user_id = :user_id)
) and not exists(select 1 from user_details where user_id = :user_id and project_id = p.id and is_blocked = 1)
${where} and is_deleted <> 1
`)
@@ -114,9 +114,9 @@ app.get('/project/:pid(\\d+)/user', (req, res, next) => {
.prepare(`
with actuals (user_id) as (
select distinct user_id
from group_users
where group_id in (select id from groups where project_id = :project_id)
and group_id in (select group_id from group_users where user_id = :user_id)
from chat_users
where chat_id in (select id from chats where project_id = :project_id)
and chat_id in (select chat_id from chat_users where user_id = :user_id)
),
contributors (user_id) as (
select created_by from tasks where project_id = :project_id
@@ -193,29 +193,29 @@ app.get('/project/:pid(\\d+)/user', (req, res, next) => {
})
app.get('/project/:pid(\\d+)/user/reload', async (req, res, next) => {
const groupIds = db
.prepare(`select id from groups where project_id = :project_id`)
const chatIds = db
.prepare(`select id from chats where project_id = :project_id`)
.all(res.locals)
.map(e => e.id)
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
for (const groupId of groupIds) {
await bot.reloadGroupUsers(groupId)
for (const chatId of chatIds) {
await bot.reloadGroupUsers(chatId)
await sleep(1000)
}
res.status(200).json({success: true})
})
app.get('/project/:pid(\\d+)/group', (req, res, next) => {
app.get('/project/:pid(\\d+)/chat', (req, res, next) => {
const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : ''
const rows = db
.prepare(`
select id, name, telegram_id
from groups
where project_id = :project_id and id in (select group_id from group_users where user_id = :user_id)
from chats
where project_id = :project_id and id in (select chat_id from chat_users where user_id = :user_id)
${where}
`)
.all(res.locals)
@@ -226,8 +226,8 @@ app.get('/project/:pid(\\d+)/group', (req, res, next) => {
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.gid}`)
app.get('/project/:pid(\\d+)/chat/:gid(\\d+)', (req, res, next) => {
res.redirect(req.baseUrl + `/project/${req.params.pid}/chat?id=${req.params.gid}`)
})
// TASK
@@ -237,8 +237,8 @@ app.get('/project/:pid(\\d+)/task', (req, res, next) => {
const rows = db
.prepare(`
select id, name, created_by, assigned_to, priority, status, time_spent, create_date, plan_date, close_date,
(select json_group_array(user_id) from task_users where task_id = t.id) observers,
(select json_group_array(id) from documents where parent_type = 1 and parent_id = t.id) attachments
(select json_chat_array(user_id) from task_users where task_id = t.id) observers,
(select json_chat_array(id) from documents where parent_type = 1 and parent_id = t.id) attachments
from tasks t
where project_id = :project_id and
(created_by = :user_id or assigned_to = :user_id or exists(select 1 from task_users where task_id = t.id and user_id = :user_id))
@@ -351,8 +351,8 @@ app.put('/project/:pid(\\d+)/task/:tid(\\d+)/observer', (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)
.all(res.locals)
@@ -379,8 +379,8 @@ app.get('/project/:pid(\\d+)/meeting', (req, res, next) => {
const rows = db
.prepare(`
select id, name, description, created_by, meet_date,
(select json_group_array(user_id) from meeting_users where meeting_id = m.id) participants,
(select json_group_array(id) from documents where parent_type = 2 and parent_id = m.id) attachments
(select json_chat_array(user_id) from meeting_users where meeting_id = m.id) participants,
(select json_chat_array(id) from documents where parent_type = 2 and parent_id = m.id) attachments
from meetings m
where project_id = :project_id and
(created_by = :user_id or exists(select 1 from meeting_users where meeting_id = m.id and user_id = :user_id))
@@ -483,8 +483,8 @@ app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)/participants', (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?
.all(res.locals)
@@ -516,10 +516,10 @@ app.get('/project/:pid(\\d+)/document', (req, res, next) => {
// To-Do: отдавать готовую ссылку --> как минимум GROUP_ID надо заменить на tgGroupId
const rows = db
.prepare(`
select id, origin_group_id, origin_message_id, filename, mime, caption, size, published_by, parent_id, parent_type
select id, origin_chat_id, origin_message_id, filename, mime, caption, size, published_by, parent_id, parent_type
from documents d
where project_id = :project_id ${where} and (
origin_group_id in (select group_id from group_users where user_id = :user_id)
origin_chat_id in (select chat_id from chat_users where user_id = :user_id)
or
parent_type = 1 and parent_id in (
select id
@@ -566,9 +566,9 @@ app.use('/project/:pid(\\d+)/document/:did(\\d+)', (req, res, next) => {
throw Error('NOT_FOUND::404')
if (doc.parent_type == 0) {
res.locals.group_id = doc.group_id
res.locals.chat_id = doc.chat_id
const row = db
.prepare(`select 1 from group_users where group_id = :group_id and user_id = :user_id`)
.prepare(`select 1 from chat_users where chat_id = :chat_id and user_id = :user_id`)
.get(res.locals)
if (row) {

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -10,9 +10,10 @@ create table if not exists customers (
json_balance text default '{}',
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 (
@@ -21,10 +22,11 @@ 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,
@@ -35,7 +37,7 @@ create table if not exists groups (
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,
@@ -90,9 +92,9 @@ create table if not exists meetings (
create table if not exists documents (
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,
@@ -142,20 +144,20 @@ 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;

BIN
backend/docs/api.xls Normal file

Binary file not shown.

12
backend/docs/public.txt Normal file
View File

@@ -0,0 +1,12 @@
Описание ПО
Программа состоит из двух частей: бота и приложение miniapp, привязанное к боту. Приложение отображает проекты для пользователя, задачи и встречи на проекте, прикрепленные к ним файлы, а также список тех, с кем пользователь пересекался в группах. Бот отслеживает не только участников группы, но и при наличии админских прав в группе, пересылаемые файлы и сообщения для резервного копирования.
Термины
Клиенты - специальные аккаунты для организаций, регистрируемые в miniapp, позволяющие управлять проектами и контактами на них.
Пользователи - пользователи Telegram, участвующие в одной или нескольких группах где есть бот. Пользователи могут заходить в miniapp и
После добавления бота в группу, группа привязывается к специальному внутреннему клиенту, который имеет только один проект "Вне проектов". Администратор группы может
После добавления в группу и привязки токеном к аккаунту клиента, бот отслеживает новые сообщения в группе. При обнаружении файла он копируется с специальную группу, указанную клиентом, и регистрирует его в списке файлов, доступных для участников группы. Бот не хранит ни сами файлы, ни сообщения.

View File

@@ -0,0 +1,93 @@
id-телеграма могут быть больше 32-битного int, поэтому лучше использовать свои id + кеш, который позволит быстро конвертировать одни id в другие. Хотя может быть использование BigInt будет достаточно. Большие id могут негативно сказаться на скорости выборки данных и индексировании.
A: Будут использоваться свои id, а ID Telegram записиываться в отдельные колонки.
-----------------------------------------------------
По хорошему для каждого customer надо иметь свою базу. Поскольку для отображения данных проекта требуется информация только именно этого customer, то информация может быть локализвана. При этом остается общая база, в которой хранится список пользователей (таблица users) и информация, необходимая для формирования первой сводной страницы, напр. общее число незавершенных задач и календарь совещаний, а также id-клиентов, у которых пользователь участвует/вал в проектах.
? Можно ли сделать так, чтобы клиент имел свою установку, но при этом пользователи обращались к нашему боту/miniapp?
Если данные запрашивать не напрямую из базы, а по http/https, то каждый узел будет иметь свою базу.
-----------------------------------------------------
Хотелось бы не хранить сообщения локально. А запрашивать их за день и отправлять их бекап в специальную группу. Бот имеет ограничение 20-30 запросов в секунду, т.е. за минуту бот может получить данные из 1200 чатов.
A: Не хранить сообщения. Запрашивать их, если клиент установил настройку.
-----------------------------------------------------
При удалении проекта/задачи/встречи/компании надо также чистить links. Возможно лучше сделать отдельные таблицы для каждой связки, чтобы использовать FK + delete cascade
A: Операция выполняется DELETE CASCADE автоматически
-----------------------------------------------------
ФИО пользователей задает customer и ФИО в адресной книге всегда показывается. Телефон задает пользователь и пользователь может указать на каком проекте его показывать, а где нет. По умолчанию "Не показывать"
<- может храниться в users.is_show_phone = [project_id1, project_id3]
A: Хранится в настойках пользователя в таблице users
-----------------------------------------------------
Если пользователь изменил задачу, то у тех, у кого она есть в миниаппе, должны получить по ней обновление. Вопрос в том, как маякнуть клиенту, что что-то обновилось?
Два варианта: клиент должен запрашивать обновления скажем раз в минуту или же использовать WebSocket. Есть вероятность, что для сервера поддерживать кучу WebScoket слишком накладно.
-----------------------------------------------------
Не делать API, который возвращает одну сущность, а только список.
Если надо сущность, то можно /api/project?id=1
Или можно использовать redirect с /api/project/:id на /api/project?id=1
A: Да
-----------------------------------------------------
Кто заполняет ФИО пользователя? Если админ, тогда возможна ситуация, когда на проекте одного админа человек будет с ФИО, а на проекте другого - нет. Возможно ФИО также должен заполнять сам пользователь. По сути есть некоторая дыра, когда пользователь предоставляет ФИО, телефон и номер телеграма всем желающим,
когда можно заманить человека в группу с ботом и узнать его личные данные.
A: Фамилию задает клиент отдельно на каждом проекте. А также роль и департамент.
-----------------------------------------------------
Польщователь сам решает показывать ли ему ФИО и/или телефон на
проектах конкретного админа системы.
А: Пользователь решает только про телефон
-----------------------------------------------------
Добавить документы может любой из участников. Удалять можно только своё или если создатель.
A: Да.
-----------------------------------------------------
Если переслать документ боту, то бот спросит к какой задаче прикрепить. Автор в этом случае тот, кто переслал сообщение.
A: В пересылаем соообщении нет информации о том, откуда оно было получено, соотв. нельзя определить к какому проекту относится пересылаемое напрямую. Как вариант - использовать последний проект, где пользователь отправил сообщение в группе + кнопка Другой проект.
Если бот не имеет админских прав, то это будет плохо работать.
-----------------------------------------------------
Предположим, что бота добавляют в группу А. Что делать, если не все в группе используют бота/miniapp?
-----------------------------------------------------
Если бот не получает админских прав, то привязать группу токеном не получится, т.к. бот не видит сообщений. Можно сделать, чтобы пользователь отправил комманду /attach<token>. Пока передать параметр в команду невозможно.
https://gram.js.org/tl/bots/SetBotCommands
Для отправки пользователям сообщений бот должен знать не только их id, но и accessHash. Аналогично для отправки сообщения в чат или скачивания файла, т.е. надо хранить еще и это поле.
Когда пользователь отписывается от бота, то надо обнулять его hash. В адресной книге тех, к кому у бота нет доступа, т.е. они участники группы, но у них нет бота, отображать специальным образом.
А: если у того, кому назначили задачу, нет бота и следовательно accessHash, то можно отправлять в один из чатов, в котором участвуют создатель и ответственный, возможно отсортировав по времени последнего сообщения от каждого из них. Наблюдатели должны использовать бота.
-----------------------------------------------------
Возможно дать некоторым пользователям добавлять группы к проекту самостоятельно, путем нажатия кнопки "Добавить группу".
-----------------------------------------------------
В uTasks поначалу отображали только задачи, связанные с пользователем, потом они добавили крыж "Показывать все задачи" по просьбам пользователей. Почему пользователям это понадобилось? Возможно потому что у них нет механизма наблюдателей.
Возможно нужно для контроля начальником => отдельное приложение
-----------------------------------------------------
Предположим, что бот был в группе, а потом его удалили из нее.
Должна ли группа отображаться в списке групп проекта?
-----------------------------------------------------

69
backend/docs/telegram.txt Normal file
View File

@@ -0,0 +1,69 @@
const { Api, TelegramClient } = require('telegram')
const { StringSession } = require('telegram/sessions')
const session = new StringSession('')
const apiId = 26746106
const apiHash = '29e5f83c04e635fa583721473a6003b5'
const BOT_TOKEN = '7236504417:AAGVaodw3cRwGlf-jAhwnYb51OHaXcgpW8k';
const client = new TelegramClient(session, apiId, apiHash, {})
(async function run() {
await client.start({botAuthToken: BOT_TOKEN})
...
})()
Получение информации о себе
const result = await client.getEntity("me")
Группа при создании имеет тип group. Её id выглядит как -4646437202
Для получения информации надо отбросить минус
const result = await client.getEntity(new Api.PeerChat({ chatId: 4646437202n }))
Для получения пользователей и описания
const result = await client.invoke(new Api.messages.GetFullChat({chatId: 4646437202n}))
После включения топиков группа меняется на канал и её id становится вида -1002496664184
Для получения информации
const result = await client.getEntity(new Api.PeerChannel({ channelId: -1002496664184n }))
При этом аттрибут forum = true
Если топики отключить, то id и класс уже не меняется, но forum = false
Для получения списка пользователей канала
const channel = new Api.PeerChannel({ channelId: -1002496664184n })
const result = await client.invoke(
new Api.channels.GetParticipants({
channel,
filter: new Api.ChannelParticipantsRecent(),
limit: 999999,
offset:0 ,
})
)
Скачивание файла из сообщения на диск
client.addEventHandler(async (update) => {
const msg = update.message
if (msg?.className == 'Message') {
const buffer = await client.downloadMedia(msg, {})
fs.writeFileSync("d:/CODES/Telegram/tmp/file", buffer)
}
}, new NewMessage({}));
Примеры кода
Получение списка пользователей в супергруппе
https://gist.github.com/waptik/9de410055eac8a60668ce7ac1e5183ac
Ссылка для выбора группы - https://qna.habr.com/q/1374460
У пользователя есть Name = First Name + Last Name - обязательно, а также username - опционально для t.me/username
Выбор куда отправить - ForwardTo - https://core.telegram.org/widgets/share
Размеры превьюшек - https://core.telegram.org/api/files#stripped-thumbnails
you can just pass an ID.
if channel/superroup => add -100
if group add => -
if user don't add anything
for example for your test group2 it would be
client.sendMessage(-463172658,{params})
https://github.com/gram-js/gramjs/issues/146#issuecomment-913528553

View File

@@ -1 +0,0 @@
node app

View File

@@ -1134,9 +1134,9 @@
"license": "ISC"
},
"node_modules/node-abi": {
"version": "3.74.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz",
"integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==",
"version": "3.75.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz",
"integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==",
"license": "MIT",
"dependencies": {
"semver": "^7.3.5"

View File

@@ -1,339 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="format-detection" content="telephone=no" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="MobileOptimized" content="176" />
<meta name="HandheldFriendly" content="True" />
<meta name="robots" content="noindex,nofollow" />
<script src="https://telegram.org/js/telegram-web-app.js?57"></script>
<title></title>
<style>
*[hidden] {display: none;}
.block {border: 1px black solid; padding:10px; margin-bottom: 10px;}
#settings input {width: 45%;}
#delete {color:red; cursor: pointer; margin-left: 10px;}
</style>
</head>
<body>
<div id = "adminapp" hidden>
<b>Админка2</b>
<div id = "settings" class = "block">
<b>Настройки</b><br>
<input id = "name" type = "text" placeholder = "Name">
<div>
<button id = "upload-group" href = "">Не задано</button>
<button id = "upload-group-selector" href = "" admin>Задать</button>
</div>
<b>Company</b>
<div id = "company">
<input id = "company-name" type = "text" placeholder = "Name">
<input id = "company-phone" type = "text" placeholder = "Phone">
<input id = "company-site" type = "text" placeholder = "Site">
<input id = "company-description" type = "text" placeholder = "Description">
</div>
<button id = "update-profile">Update</button>
</div>
<div id = "projects" class = "block">
<b>Список проектов</b><button id = "add-project">Добавить/Обновить</button>
<br>
<input id = "project-id" placeholder = "Id">
<input id = "project-name" placeholder = "Name">
<input id = "project-description" placeholder = "Description">
<br>
<div id = "project-list">Loading...</div>
</div>
<div id = "groups" class = "block">
<b>Список групп</b><button id = "add-group" href="" admin>Добавить</button>
<br>
<div id = "group-list">Проект не выбран</div>
<button id = "share" href = "">Отправить ключ подключения</button>
</div>
<div id = "users" class = "block">
<b>Список пользователей</b>
<br>
<div id = "user-list">Проект не выбран</div>
</div>
<div id = "companies" class = "block">
<b>Список компаний</b><button id = "add-company">Добавить/Обновить</button>
<br>
<input id = "company-id" placeholder = "Id">
<input id = "company-name" placeholder = "Name">
<input id = "company-description" placeholder = "Description">
<br>
<div id = "company-list">Проект не выбран</div>
</div>
</div>
<div id = "miniapp" style = "min-height: 20px; border: 1px black solid; padding:10px; word-break: break-all;" hidden>
Пользов
</div>
<script type="text/javascript">
const $miniapp = document.getElementById('miniapp')
const $adminapp = document.getElementById('adminapp')
$adminapp.querySelector('#settings #update-profile').addEventListener('click', async function (event) {
const $e = this.parentNode
const data = {
name: $e.querySelector('#name').value,
company: {
name: $e.querySelector('#company-name').value,
phone: $e.querySelector('#company-phone').value,
site: $e.querySelector('#company-site').value,
description: $e.querySelector('#company-description').value
}
}
const res = await fetch('/api/admin/customer/profile', {
method: 'PUT',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
}).then(res => res.json())
console.log(res)
})
$adminapp.querySelector('#projects #project-list').addEventListener('click', async function (event) {
const $e = event.target
if ($e.id != 'delete')
return
const id = $e.parentNode.id
const res = await fetch('/api/admin/project/' + id, {
method: 'DELETE'
}).then(res => res.json())
.then(res => $e.parentNode.remove())
.catch(alert)
})
$adminapp.querySelector('#projects #project-list').addEventListener('click', async function (event) {
const $e = event.target
if ($e.parentNode.id != 'project-list')
return
$adminapp.querySelector('#projects #project-id').value = $e.id
$adminapp.querySelector('#projects #project-name').value = $e.getAttribute('name')
$adminapp.querySelector('#projects #project-description').value = $e.getAttribute('title')
$adminapp.querySelector('#groups #add-group').setAttribute('href', 'https://t.me/ready_or_not_2025_bot?startgroup=' + $e.id)
reloadGroups($e.id)
reloadUsers($e.id)
reloadCompanies($e.id)
const reqKey = await fetch('/api/admin/project/' + $e.id + '/token').then(res => res.json())
const url = 'https://t.me/share/url?url=' + reqKey.data
$adminapp.querySelector('#groups #share').setAttribute('href', url)
})
$adminapp.querySelector('#companies #company-list').addEventListener('click', async function (event) {
const $e = event.target
if ($e.id != 'delete')
return
const id = $e.parentNode.id
const project_id = $adminapp.querySelector('#groups #group-list').getAttribute('project-id')
const res = await fetch(`/api/admin/project/${project_id}/company/${id}`, {
method: 'DELETE'
}).then(res => res.json())
.then(res => $e.parentNode.remove())
.catch(alert)
})
$adminapp.querySelector('#companies #company-list').addEventListener('click', async function (event) {
const $e = event.target
if ($e.parentNode.id != 'company-list')
return
$adminapp.querySelector('#companies #company-id').value = $e.id
$adminapp.querySelector('#companies #company-name').value = $e.getAttribute('name')
$adminapp.querySelector('#companies #company-description').value = $e.getAttribute('title')
})
$adminapp.querySelector('#groups #group-list').addEventListener('click', async function (event) {
const $e = event.target
if ($e.id != 'delete')
return
const id = $e.parentNode.id
const project_id = $adminapp.querySelector('#groups #group-list').getAttribute('project-id')
const res = await fetch(`/api/admin/project/${project_id}/group/${id}`, {
method: 'DELETE'
}).then(res => res.json())
.then(res => $e.parentNode.remove())
.catch(alert)
})
$adminapp.querySelectorAll('button[href]')
.forEach($e => {
$e.addEventListener('click', async function (event) {
// &admin=change_info+delete_messages+restrict_members+invite_users+pin_messages+promote_members
let url = this.getAttribute('href')
if ($e.hasAttribute('admin'))
url += '&admin=change_info+pin_messages'
window.Telegram.WebApp.openTelegramLink(url)
})
})
async function reloadProjects() {
const reqProjects = await fetch('/api/admin/project').then(res => res.json())
const projects = reqProjects.data
const $projectList = $adminapp.querySelector('#projects #project-list')
$projectList.innerHTML = projects.map(e => `<div id = "${e.id}" title = "${e.description}" name = "${e.name}">${e.name}<span id = "delete">x</div></div>`).join('') || 'Пусто'
}
async function reloadGroups(projectId) {
const req = await fetch('/api/admin/project/' + projectId + '/group').then(res => res.json())
const groups = req.data
const $groupList = $adminapp.querySelector('#groups #group-list')
$groupList.setAttribute('project-id', projectId)
$groupList.innerHTML = groups.map(e => `<div id = "${e.id}" name = "${e.name}">${e.name}<span id = "delete">x</div></div>`).join('') || 'Пусто'
}
async function reloadCompanies(projectId) {
const req = await fetch('/api/admin/project/' + projectId + '/company').then(res => res.json())
const companies = req.data
const $companyList = $adminapp.querySelector('#companies #company-list')
$companyList.setAttribute('project-id', projectId)
$companyList.innerHTML = companies.map(e => `<div id = "${e.id}" name = "${e.name}" title = "${e.description}">${e.name}<span id = "delete">x</div></div>`).join('') || 'Пусто'
}
async function reloadUsers(projectId) {
const req = await fetch('/api/admin/project/' + projectId + '/user').then(res => res.json())
const users = req.data
const $userList = $adminapp.querySelector('#users #user-list')
$userList.setAttribute('user-id', projectId)
$userList.innerHTML = users.map(e => `<div id = "${e.id}">${e.firstname} ${e.lastname}</div>`).join('') || 'Пусто'
}
$adminapp.querySelector('#projects #add-project').addEventListener('click', async function (event) {
const $e = this.parentNode
const id = +$e.querySelector('#project-id').value
const data = {
name: $e.querySelector('#project-name').value,
description: $e.querySelector('#project-description').value
}
const url = id ? '/api/admin/project/' + id : '/api/admin/project'
const method = id ? 'PUT' : 'POST'
await fetch(url, {
method,
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
}).then(res => res.json()).then(() => {
reloadProjects()
$e.querySelector('#project-id').value = ''
$e.querySelector('#project-name').value = ''
$e.querySelector('#project-description').value = ''
}).catch(alert)
})
$adminapp.querySelector('#companies #add-company').addEventListener('click', async function (event) {
const $e = this.parentNode
const project_id = +$adminapp.querySelector('#groups #group-list').getAttribute('project-id')
const id = +$e.querySelector('#company-id').value
const data = {
name: $e.querySelector('#company-name').value,
description: $e.querySelector('#company-description').value
}
const url = '/api/admin/project/' + project_id + '/company' + (id ? '/' + id : '')
const method = id ? 'PUT' : 'POST'
await fetch(url, {
method,
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
}).then(res => res.json()).then(() => {
reloadCompanies(project_id)
$e.querySelector('#company-id').value = ''
$e.querySelector('#company-name').value = ''
$e.querySelector('#company-description').value = ''
}).catch(alert)
})
window.addEventListener('load', async (event) => {
const startParams = (Telegram.WebApp.initDataUnsafe.start_param || '').split('_')
//const isAdmin = startParams[0] == 'admin'
const isAdmin = true
const $app = isAdmin ? $adminapp : $miniapp
$app.hidden = false
const login_url = isAdmin ? '/api/admin/auth/telegram?' : '/api/miniapp/auth?'
await Telegram.WebApp.ready()
console.log(Telegram)
if (Telegram.WebApp.initData == '') {
alert('NO INIT DATA')
return 0
}
console.log('TG', Telegram)
const login = await fetch(login_url + Telegram.WebApp.initData, {method: 'POST'}).then(res => res.json())
console.log(login)
if (isAdmin) {
const reqProfile = await fetch('/api/admin/customer/profile').then(res => res.json())
const profile = reqProfile.data
$adminapp.querySelector('#settings #name').value = profile.name || ''
$adminapp.querySelector('#settings #company-name').value = profile.company?.name || ''
$adminapp.querySelector('#settings #company-phone').value = profile.company?.phone || ''
$adminapp.querySelector('#settings #company-site').value = profile.company?.site || ''
$adminapp.querySelector('#settings #company-description').value = profile.company?.description || ''
$adminapp.querySelector('#settings #upload-group-selector').setAttribute('href', 'https://t.me/ready_or_not_2025_bot?startgroup=-' + profile.id)
const $uploadGroup = $adminapp.querySelector('#settings #upload-group')
if (profile.upload_group) {
$uploadGroup.innerHTML = profile.upload_group.name || 'Ошибка'
$uploadGroup.setAttribute('href', 'https://t.me/c/' + profile.upload_group.telegram_id)
} else {
$uploadGroup.innerHTML = 'Не задано'
$uploadGroup.setAttribute('href', '')
}
await reloadProjects()
} else {
// Пользовательский
if (startParams[1])
alert('Группа на проекте ' + startParams[1])
}
})
</script>
</body>
</html>

View File

@@ -1,65 +0,0 @@
<!DOCTYPE html>
<html>
<body>
<style>
.block {margin-bottom: 10px}
#projects > div {cursor: pointer}
</style>
<div class = "block">
<label for = "user">USER</label>
<select id = "user"></select>
</div>
<div class = "block">
<div id = "project-title">ПРОЕКТЫ</div>
<div id = "projects">EMPTY</div>
</div>
<div class = "block">
<div id = "member-title">УЧАСТНИКИ</div>
<div id = "members"></div>
</div>
<script type="text/javascript">
(async function () {
try {
const $user = document.getElementById('user')
const $projects = document.getElementById('projects')
const $members = document.getElementById('members')
let members = {}
$user.addEventListener('change', async () => {
const user_id = $user.value
const projects = await fetch('/api/miniapp/project?user_id=' + user_id).then(res => res.json())
$projects.innerHTML = projects.data.map(e =>
`<div id = "${e.id}" title = '${JSON.stringify(e)}'>${e.name} - ${e.description}</div>`
).join('')
$members.innerHTML = ''
})
$projects.addEventListener('click', async (evt) => {
if (evt.target.parentNode != $projects)
return
const _members = await fetch('/api/miniapp/project/' + evt.target.id + '/member?user_id=' + $user.value).then(res => res.json())
members = _members.data
$members.innerHTML = members.map(e =>
`<div id = "${e.id}" title = '${JSON.stringify(e)}' is-blocked = '${e.is_blocked}'>${e.id} - ${e.telegram_name}</div>`
).join('')
})
const user_ids = await fetch('/api/miniapp/user').then(res => res.json())
$user.innerHTML = user_ids.data.map(id => `<option value = '${id}'>${id}</option>`).join('')
$user.dispatchEvent(new Event('change'))
} catch (err) {
console.error(err)
alert(err.message)
}
})();
</script>
</body>
</html>