before delete 3software

This commit is contained in:
2025-06-29 18:55:59 +03:00
parent ebd77a3e66
commit b51a472738
147 changed files with 257326 additions and 3151 deletions

View File

@@ -8,4 +8,4 @@ API_ID=26746106
API_HASH=29e5f83c04e635fa583721473a6003b5
BOT_TOKEN=7236504417:AAGVaodw3cRwGlf-jAhwnYb51OHaXcgpW8k
BOT_SID=1AgAOMTQ5LjE1NC4xNjcuNTEBu6zLey/IpmODwaKLyTbkKwFDY28LSFPf4UZpgSKCO4bP5gGtFOGVmNDhsJxhMtUWNzhyOX46GyDliNiZ4FUQdoQ6G93DEN8mYcREljmiCp5JchNyZPmhGxl2GeclPo0tp9T/yXFUyo7PD8YpuykHH/MdWVyZxPp93Pjjpi+E03DKCwD00tEpi2TAGzW/MyQ8HUAUIK45nkJA7dnv8Up7NB9LWJ2z+8Dx81oGdVYyOHBL9qy9722LyKtvLD47KpwINjJyZOdhdBM1W8bhsGE4JkHs6DXFXOzrmMrWaE30z4corikkQoNIDL/tXttv+bJULQbbyGZvskbXuvwkV/NVen0=
BOT_SID=1AgAOMTQ5LjE1NC4xNjcuNTEBuwHf5TwB0JAqpBNdICUe76iNT43Yz+Telwrt/sVZeS393zNvu1hxB1vnioPR8TdWOatPNETxHQPeqeNJflSiXr3Kvz3qo9Jlubn1rgtmrIedinniF8uK9eQegzEb3jOYM89HPogRiQ581YcGjJsMUNUNGyqup51xZVrzANxm1E2CagQqhZ1vQVPAaGA89PYtt3GkHw61NH8kuV2BmwypS4v2sXuY44324SdhPpKOAVIvi46GxMT79livgQCwNegL0EzbrekhjE1fBi7W39LpyHisbNPc7U6gvd2YoVgEid95+cNS3rJcE6xzH14yNxNzqdK6uTZPWIjgVA4ciFKPtUA=

BIN
backend/_old/backend_v4.zip Normal file

Binary file not shown.

BIN
backend/_old/backend_v5.zip Normal file

Binary file not shown.

BIN
backend/_old/backend_v6.zip Normal file

Binary file not shown.

BIN
backend/_old/backend_v7.zip Normal file

Binary file not shown.

1
backend/_old/v8/1.log Normal file
View File

@@ -0,0 +1 @@
[object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object]

26
backend/_old/v8/2.log Normal file
View File

@@ -0,0 +1,26 @@
Listening at port 3000
[2025-05-23T22:15:26.439] [INFO] - [Running gramJS version 2.26.21]
[2025-05-23T22:15:26.443] [INFO] - [Connecting to 149.154.167.51:80/TCPFull...]
[2025-05-23T22:15:26.492] [INFO] - [Connection to 149.154.167.51:80/TCPFull complete!]
[2025-05-23T22:15:26.493] [INFO] - [Using LAYER 198 for initial connect]
UpdateConnectionState {
state: 1,
_entities: Map(0) {},
_client: {
session: StringSession {
_serverAddress: '149.154.167.51',
_dcId: 2,
_port: 443,
_takeoutId: undefined,
_entities: [Set],
_updateStates: {},
_key: <Buffer 01 df e5 3c 01 d0 90 2a a4 13 5d 20 25 1e ef a8 8d 4f 8d d8 cf e4 de 97 0a ed fe c5 59 79 2d fd df 33 6f bb 58 71 07 5b e7 8a 83 d1 f1 37 56 39 ab 4f ... 206 more bytes>,
_authKey: [AuthKey]
},
apiId: 26746106,
apiHash: '29e5f83c04e635fa583721473a6003b5',
testServers: false,
networkSocket: [class PromisedNetSockets],
useWSS: false
}
}

79687
backend/_old/v8/3.log Normal file

File diff suppressed because it is too large Load Diff

2
backend/_old/v8/app.bat Normal file
View File

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

79
backend/_old/v8/app.js Normal file
View File

@@ -0,0 +1,79 @@
const express = require('express')
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser')
const crypto = require('crypto')
const fs = require('fs')
const util = require('util')
const bot = require('./apps/bot')
const app = express()
app.use(bodyParser.json({limit: '10mb'}))
app.use(cookieParser())
BigInt.prototype.toJSON = function () {
return Number(this)
}
app.use((req, res, next) => {
if(!(req.body instanceof Object))
return next()
const escapeHtml = str => str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;')
Object
.keys(req.body || {})
.filter(key => typeof(req.body[key]) == 'string' && key != 'password')
.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 = 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')
const timeDiff = Date.now() / 1000 - data.auth_date
if (hmac !== req.query.hash) // || timeDiff > 10)
throw Error('ACCESS_DENIED::401')
const user = JSON.parse(req.query.user)
res.locals.telegram_id = user.id
res.locals.start_param = req.query.start_param
if (!res.locals.telegram_id)
throw Error('ACCESS_DENIED::500')
next()
})
app.use('/api/admin', require('./apps/admin'))
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}})
})
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_HASH || '29e5f83c04e635fa583721473a6003b5',
process.env.BOT_TOKEN || '7236504417:AAGVaodw3cRwGlf-jAhwnYb51OHaXcgpW8k',
process.env.BOT_SID || ''
)
})

View File

@@ -0,0 +1,861 @@
const crypto = require('crypto')
const express = require('express')
const db = require('../include/db')
const bot = require('./bot')
const fs = require('fs')
const app = express.Router()
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) => {
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
req.session = sessions[asid]
if (!req.session)
throw Error('ACCESS_DENIED::401')
res.locals.customer_id = req.session.customer_id
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
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)
throw Error('AUTH_ERROR::401')
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(`insert 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, telegram_id = null 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. Если коды проходят проверку, то сервер отвечает ОК.
5. Отправлются оба кода, новые email и password. Если они проходят проверку, то сервер меняет 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 password = String(req.body.password ?? '').trim()
const email = db
.prepare('select email from customers where id = :customer_id')
.pluck(true)
.get(res.locals)
const stepNo = !code ? 1 : code && !email2 ? 2 : code && email2 && !code2 ? 3 : code && email2 && code2 && !password ? 4 : code && email2 && code2 && password ? 5 : -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')
}
if (stepNo == 5) {
if (!checkPassword(password))
throw Error('INCORRECT_PASSWORD::400')
res.locals.email = email2
res.locals.password = password
const info = db
.prepare('update customers set email = :email, password = :password 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 ?? '').trim()
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) => {
res.locals.time = Math.floor(Date.now() / 1000)
const row = db
.prepare(`
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
`)
.get(res.locals)
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({ chat_id: row.upload_chat_id})
delete row.upload_chat_id
}
for (const key in row) {
if (key.startsWith('json_')) {
row[key.substr(5)] = JSON.parse(row[key])
delete row[key]
}
}
res.status(200).json({success: true, data: row})
})
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(
'customers',
['name', 'password', 'json_company'],
req.body,
['id'])
.run(Object.assign({}, req.body, {id: req.session.customer_id}))
if (info.changes == 0)
throw Error('NOT_FOUND::404')
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
function getProject(id, customer_id) {
const row = db
.prepare(`
select id, name, description, logo, is_logo_bg, company_id, 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, company_id, 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)
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})
})
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
res.locals.project_id = db
.prepare(`
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)
const json_company = db
.prepare(`select coalesce(json_company, '{}') from customers where id = :customer_id`)
.pluck(true)
.get(res.locals)
res.locals.company_id = addCompany(Object.assign({
name: 'My Company',
address: null,
email: null,
phone: null,
site: null,
description: null,
logo: null
}, JSON.parse(json_company), {project_id: res.locals.project_id}))
db
.prepare(`update projects set company_id = :company_id where id = :project_id`)
.run(res.locals)
const data = getProject(res.locals.project_id, res.locals.customer_id)
res.status(200).json({success: true, data})
})
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_archived <> 1')
.get(res.locals)
if (!row)
throw Error('ACCESS_DENIED::401')
next()
})
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})
})
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.email, ud.phone, ud.role, ud.department, ud.is_blocked,
(select company_id from company_users where user_id = :id) company_id,
(select json_group_array(chat_id) from chat_users where user_id = :id and chat_id in (select id from chats where project_id = :project_id)) chats
from users u
left join user_details ud on u.id = ud.user_id and ud.project_id = :project_id
where id = :id
`)
.safeIntegers(true)
.get({id, project_id})
if (!row)
throw Error('NOT_FOUND::404')
row.chats = JSON.parse(row.chats || '[]')
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,
(select company_id from company_users where user_id = u.id) company_id
from users u
left join user_details ud on u.id = ud.user_id and ud.project_id = :project_id
where id in (
select user_id
from chat_users
where chat_id in (select id from chats where project_id = :project_id)
)
`)
.safeIntegers(true)
.all(res.locals)
data.forEach(row => {
row.is_blocked = Boolean(row.is_blocked)
})
res.status(200).json({success: true, data})
})
app.get('/project/:pid(\\d+)/user/:uid(\\d+)', (req, res, next) => {
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 = 'is_blocked' in req.body ? +req.body.is_blocked : undefined
const info = db
.prepareUpsert('user_details',
['fullname', 'email', 'phone', 'role', 'department', 'is_blocked'],
res.locals,
['user_id', 'project_id']
)
.run(res.locals)
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) => {
res.locals.time = Math.floor(Date.now() / 1000)
const key = db
.prepare('select generate_key(id, :time) from projects where id = :project_id and customer_id = :customer_id')
.pluck(true)
.get(res.locals)
if (!key)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true, data: key})
})
// COMPANY
function addCompany(data) {
return db
.prepare(`
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(true)
.get(data)
}
function getCompany(id, project_id) {
const row = 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 c.id = :id and project_id = :project_id
order by name
`)
.get({id, project_id})
if (!row)
throw Error('NOT_FOUND::404')
row.users = JSON.parse(row.users || '[]')
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 company_id = c.id from projects where id = :project_id) is_own,
(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)
data.forEach(row => {
row.users = JSON.parse(row.users || '[]')
})
res.status(200).json({success: true, data})
})
app.get('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
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
res.locals.description = req.body?.description
res.locals.logo = req.body?.logo
const id = addCompany(res.locals)
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', 'address', 'email', 'phone', 'site', 'description', 'logo'],
res.locals,
['id', 'project_id'])
.run(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
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 = req.params.cid
const info = db
.prepare(`
delete from companies
where id = :company_id and project_id = :project_id and
not exists(select company_id from projects where id = :project_id)`)
.run(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true, data: {id: req.params.cid}})
})
app.put('/project/:pid(\\d+)/company/:cid(\\d+)/user', (req, res, next) => {
res.locals.company_id = parseInt(req.params.cid)
// Проверка, что есть доступ к компании
const row = db
.prepare('select 1 from companies where id = :company_id and project_id = :project_id')
.get(res.locals)
if (!row)
throw Error('NOT_FOUND::404')
const user_ids = req.body instanceof Array ? [...new Set(req.body.map(e => parseInt(e)))] : []
// Проверка, что пользователи имеют доступ к проекту
let rows = db
.prepare(`
select user_id
from chat_users
where chat_id in (select id from chats where project_id = :project_id)
`)
.pluck(true) // .raw?
.get(res.locals)
if (user_ids.some(user_id => !rows.contains(user_id)))
throw Error('INACCESSABLE_MEMBER::400')
// Проверка, что пользователи не участвуют в других компаниях на проекте
rows = db
.prepare(`
select user_id
from company_users
where company_id in (select id from companies where id <> :company_id and project_id = :project_id)
`)
.pluck(true) // .raw?
.get(res.locals)
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)
db
.prepare(`
insert into company_users (company_id, user_id)
select :company_id, value from json_each(:json_ids)
`)
.run(res.locals, {json_ids: JSON.stringify(user_ids)})
res.status(200).json({success: true})
})
app.get('/project/:pid(\\d+)/company/mapping', (req, res, next) => {
const data = db
.prepare(`
select company_id, json_group_array(show_to_id) show_to_ids
from company_mappings
where project_id = :project_id and company_id <> show_to_id
`)
.all(res.locals)
data.forEach(row => {
row.show_to_ids = JSON.parse(row.show_to_ids || '[]')
})
res.status(200).json({success: true, data})
})
app.put('/project/:pid(\\d+)/company/mapping', (req, res, next) => {
if(!(req.body instanceof Array))
throw Error('ARRAY_REQUIRED::500')
db
.prepare(`delete from company_mappings where project_id = :project_id`)
.run(res.locals)
req.body
.filter(row => Number.isInteger(row.company_id) && row.show_to_ids instanceof Array && row.show_to_ids.every(id => Number.isInteger(id)))
.forEach(row => {
row.show_to_ids.push(row.company_id)
const json_ids = row.show_to_ids.join(', ')
const check = db
.prepare(`select count(1) from companies where project_id = :project_id and id in (${json_ids}) `)
.get(res.locals)
if (check.count)
return console.error ('IGNORE: ', row)
const locals = {
project_ids: res.locals.project_id,
company_id: row.company_id,
json_ids
}
db
.prepare(`
insert into company_mappings (project_id, company_id, show_to_id) values
select :project_ids, :company_id, value from json_each(:json_ids)
`)
.run(locals)
})
res.status(200).json({success: true})
})
// CHATS
function getChat(id, project_id) {
const row = db
.prepare(`
select id, name, telegram_id, is_channel, invite_link, 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, invite_link, 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

684
backend/_old/v8/apps/bot.js Normal file
View File

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

View File

@@ -0,0 +1,751 @@
const express = require('express')
const multer = require('multer')
const crypto = require('crypto')
const fs = require('fs')
const contentDisposition = require('content-disposition')
const bot = require('./bot')
const db = require('../include/db')
const app = express.Router()
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 10_000_000 // 10mb
}
})
function hasAccess(project_id, user_id) {
return !!db
.prepare(`select 1 from projects where id = :project_id and is_archived <> 1`)
.pluck(true)
.get({project_id}) &&
!!db
.prepare(`
select 1
from chat_users
where user_id = :user_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)
`)
.pluck(true)
.get({project_id, user_id})
}
const sessions = {}
app.use((req, res, next) => {
if (req.path == '/auth')
return next()
const sid = req.query.sid || req.cookies.sid
req.session = sessions[sid]
if (!req.session)
throw Error('ACCESS_DENIED::401')
res.locals.user_id = req.session.user_id
next()
})
app.post('/auth', (req, res, next) => {
db
.prepare(`insert or ignore into users (telegram_id) values (:telegram_id)`)
.safeIntegers(true)
.run(res.locals)
const user_id = db
.prepare(`select id from users where telegram_id = :telegram_id`)
.safeIntegers(true)
.pluck(true)
.get(res.locals)
const sid = crypto.randomBytes(64).toString('hex')
req.session = sessions[sid] = {sid, user_id}
res.setHeader('Set-Cookie', [`sid=${sid};httpOnly;path=/api/miniapp`])
res.locals.user_id = user_id
res.status(200).json({success: true})
})
app.get('/project', (req, res, next) => {
const rows = db
.prepare(`
select p.id, p.name, p.description, p.logo, p.is_logo_bg, company_id,
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 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)
and p.is_archived <> 1
`)
.all(res.locals)
rows.forEach(row => {
row.is_logo_bg = Boolean(row.is_logo_bg)
})
res.status(200).json({success: true, data: rows})
})
app.use('/project/:pid(\\d+)/*', (req, res, next) => {
res.locals.project_id = parseInt(req.params.pid)
if (!hasAccess(res.locals.project_id, res.locals.user_id))
throw Error('ACCESS_DENIED::401')
const row = db
.prepare('select customer_id, company_id from projects where id = :project_id')
.get(res.locals)
res.locals.customer_id = row.customer_id
res.locals.customer_company_id = row.company_id
next()
})
function getUserCompanyId(user_id, project_id) {
return db
.prepare(`
select company_id
from company_users
where user_id = :user_id and company_id in (select id from companies where project_id = :project_id)
`)
.pluck(true)
.get({ user_id, project_id })
}
app.get('/project/:pid(\\d+)/user', (req, res, next) => {
const users = db
.prepare(`
with actuals (user_id) as (
select distinct 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
union
select assigned_to from tasks where project_id = :project_id
union
select created_by from meetings where project_id = :project_id
union
select published_by from files where project_id = :project_id
),
members (user_id, is_leave) as (
select user_id, 0 is_leave from actuals
union all
select user_id, 1 is_leave from contributors where user_id not in (select user_id from actuals)
)
select u.id,
u.telegram_id,
u.username,
u.firstname,
u.lastname,
u.photo,
ud.fullname,
ud.email,
ud.phone,
ud.role,
ud.department,
ud.is_blocked,
(select company_id
from company_users
where user_id = u.id and
company_id in (select id from companies where project_id = :project_id)) company_id,
m.is_leave
from users u
inner join members m on u.id = m.user_id
left join user_details ud on ud.user_id = u.id and ud.project_id = :project_id
`)
.safeIntegers(true)
.all(res.locals)
res.locals.company_id = getUserCompanyId(res.locals.user_id, res.locals.project_id)
// Список компаний, которые НЕ ВИДНЫ компании пользователя на проекте
const hidden = db
.prepare(`
select company_id from company_mappings where project_id = :project_id
except
select company_id from company_mappings where project_id = :project_id and show_to_id = :company_id`)
.pluck(true)
.all(res.locals)
users
.filter(user => user.company_id)
.filter(user => hidden.indexOf(user.company_id) != -1)
.forEach(user => user.company_id = res.locals.customer_company_id)
res.status(200).json({success: true, data: users})
})
app.get('/project/:pid(\\d+)/user/reload', async (req, res, next) => {
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 chatId of chatIds) {
await bot.reloadChatUsers(chatId)
await sleep(1000)
}
res.status(200).json({success: true})
})
app.get('/project/:pid(\\d+)/company', (req, res, next) => {
res.locals.company_id = getUserCompanyId(res.locals.user_id, res.locals.project_id)
const rows = db
.prepare(`
select id, name, address, email, phone, site, description
from companies
where project_id = :project_id and (
id = :company_id or
id in (select company_id from company_mappings where project_id = :project_id and show_to_id = :company_id) or
id not in (select company_id from company_mappings where project_id = :project_id) or
(select :customer_company_id)
)
`)
.all(res.locals)
res.status(200).json({success: true, data: rows})
})
// CHAT
app.get('/project/:pid(\\d+)/chat', (req, res, next) => {
const rows = db
.prepare(`
select id, name, invite_link, description, telegram_id, owner_id, user_count, logo,
(select json_group_array(user_id) from chat_users where chat_id = c.id) users,
(select count(1) from tasks where project_id = :project_id and chat_id = c.id) task_count
from chats c
where project_id = :project_id and id in (select chat_id from chat_users where user_id = :user_id)
`)
.safeIntegers(true)
.all(res.locals)
rows.forEach(row => {
row.users = JSON.parse(row.users)
})
res.status(200).json({success: true, data: rows})
})
// TASK
function getTask(id, user_id) {
const row = db
.prepare(`
select id, name, created_by, assigned_to, priority, status, time_spent, create_date, plan_date,
close_date, close_comment, coalesce(json_close_file_ids, '[]') close_file_ids, chat_id,
(select json_group_array(user_id) from task_users where task_id = t.id) observers,
(select json_group_array(id) from files where parent_type = 1 and parent_id = t.id) files
from tasks t
where t.id = :id
`)
.get({id})
if (!row)
throw Error('NOT_FOUND::404')
row.close_file_ids = JSON.parse(row.close_file_ids)
row.observers = JSON.parse(row.observers)
row.files = JSON.parse(row.files)
row.is_editable = row.created_by == user_id || row.assigned_to == user_id
return row
}
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, close_comment, coalesce(json_close_file_ids, '[]') close_file_ids, chat_id,
(select json_group_array(user_id) from task_users where task_id = t.id) observers,
(select json_group_array(id) from files where parent_type = 1 and parent_id = t.id) files
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) or
exists(select 1 from chat_users where chat_id = t.chat_id))
`)
.all(res.locals)
rows.forEach(row => {
row.close_file_ids = JSON.parse(row.close_file_ids)
row.observers = JSON.parse(row.observers)
row.files = JSON.parse(row.files)
})
res.status(200).json({success: true, data: rows})
})
app.post('/project/:pid(\\d+)/task', (req, res, next) => {
res.locals.name = req.body?.name
res.locals.status = parseInt(req.body?.status)
res.locals.priority = parseInt(req.body?.priority)
res.locals.assigned_to = req.body?.assigned_to ? parseInt(req.body?.assigned_to) : undefined
res.locals.create_date = Math.floor(Date.now() / 1000)
res.locals.plan_date = req.body?.plan_date ? parseInt(req.body?.plan_date) : undefined
res.locals.chat_id = req.body?.chat_id ? parseInt(req.body?.chat_id) : undefined
if (res.locals.assigned_to && !hasAccess(res.locals.project_id, res.locals.assigned_to))
throw Error('INCORRECT_ASSIGNED_TO::400')
if (res.locals.chat_id && !db.prepare(`select id from chats where project_id = :project_id and id = :chat_id`).pluck(true).get(res.locals))
throw Error('INCORRECT_CHAT_ID::400')
const id = db
.prepare(`
insert into tasks (project_id, name, created_by, assigned_to, priority, status, create_date, plan_date)
values (:project_id, :name, :user_id, :assigned_to, :priority, :status, :create_date, :plan_date)
returning id
`)
.pluck(true)
.get(res.locals)
const task = getTask(id, res.locals.user_id)
res.status(200).json({success: true, data: task})
})
app.use('/project/:pid(\\d+)/task/:tid(\\d+)*', (req, res, next) => {
res.locals.task_id = req.params.tid
const task = db
.prepare(`
select created_by, assigned_to
from tasks t
where id = :task_id and project_id = :project_id and (
created_by = :user_id or
assigned_to = :user_id or
exists(select 1 from task_users where task_id = :task_id and user_id = :user_id) or
exists(select 1 from chat_users where chat_id = t.chat_id))
`)
.get(res.locals)
if (!task)
throw Error('NOT_FOUND::404')
res.locals.is_author = task.created_by == res.locals.user_id
res.locals.is_assigned = task.assigned_to == res.locals.user_id
next()
})
app.get('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => {
const task = getTask(req.params.tid, res.locals.user_id)
res.status(200).json({success: true, data: task})
})
app.put('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => {
if (!res.locals.is_author && !res.locals.is_assigned)
throw Error('ACCESS_DENIED::401')
res.locals.id = res.locals.task_id
res.locals.name = req.body?.name
res.locals.status = parseInt(req.body?.status)
res.locals.priority = parseInt(req.body?.priority)
res.locals.assigned_to = req.body?.assigned_to ? parseInt(req.body?.assigned_to) : undefined
res.locals.plan_date = req.body?.plan_date ? parseInt(req.body?.plan_date) : undefined
res.locals.chat_id = req.body?.chat_id ? parseInt(req.body?.chat_id) : undefined
if (res.locals.chat_id && !db.prepare(`select id from chats where project_id = :project_id and id = :chat_id`).pluck(true).get(res.locals))
throw Error('INCORRECT_CHAT_ID::400')
const columns = res.locals.is_author ? ['name', 'assigned_to', 'priority', 'status', 'plan_date', 'time_spent', 'close_comment', 'json_close_file_ids', 'chat_id'] : ['status', 'time_spent', 'close_comment', 'json_close_file_ids']
const info = db
.prepareUpdate('tasks', columns, res.locals, ['id', 'project_id'])
.run(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
const task = getTask(res.locals.task_id, res.locals.user_id)
res.status(200).json({success: true, data: task})
})
app.delete('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => {
if (!res.locals.is_author)
throw Error('ACCESS_DENIED::401')
const info = db
.prepare(`delete from tasks where id = :task_id and project_id = :project_id and created_by = :user_id`)
.run(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true, data: {id: res.locals.task_id}})
})
app.put('/project/:pid(\\d+)/task/:tid(\\d+)/observer', (req, res, next) => {
if (!res.locals.is_author && !res.locals.is_assigned)
throw Error('ACCESS_DENIED::401')
const user_ids = req.body instanceof Array ? [...new Set(req.body.map(e => parseInt(e)))] : []
// Проверка, что выбранные пользователи имеют доступ к проекту
let rows = db
.prepare(`
select user_id
from chat_users
where chat_id in (select id from chats where project_id = :project_id)
`)
.pluck(true)
.all(res.locals)
if (user_ids.some(user_id => !rows.contains(user_id)))
throw Error('INACCESSABLE_USER::400')
res.locals.json_ids = JSON.stringify(user_ids)
db
.prepare(`
delete from task_users where task_id = :task_id;
insert into task_users (task_id, user_id) select :task_id, value from json_each(:json_ids)
`)
.run(res.locals)
res.status(200).json({success: true})
})
// MEETINGS
function getMeeting(id, user_id) {
const row = db
.prepare(`
select id, name, description, place, created_by, meet_date, chat_id, is_cancel,
(select json_group_array(user_id) from meeting_users where meeting_id = m.id) participants,
(select json_group_array(id) from files where parent_type = 2 and parent_id = m.id) files
from meetings m
where m.id = :id
`)
.get({id})
if (!row)
throw Error('NOT_FOUND::404')
row.participants = JSON.parse(row.participants)
row.files = JSON.parse(row.files)
row.is_editable = row.created_by == user_id
return row
}
app.get('/project/:pid(\\d+)/meeting', (req, res, next) => {
const rows = db
.prepare(`
select id, name, description, place, created_by, meet_date, duration, chat_id, is_cancel,
(select json_group_array(user_id) from meeting_users where meeting_id = m.id) participants,
(select json_group_array(id) from files where parent_type = 2 and parent_id = m.id) files,
created_by = :user_id is_editable
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))
`)
.all(res.locals)
rows.forEach(row => {
row.participants = JSON.parse(row.participants)
row.files = JSON.parse(row.files)
row.is_editable = Boolean(row.is_editable)
})
res.status(200).json({success: true, data: rows})
})
app.post('/project/:pid(\\d+)/meeting', (req, res, next) => {
res.locals.name = req.body?.name
res.locals.description = req.body?.description
res.locals.place = req.body?.place
res.locals.meet_date = req.body?.meet_date ? parseInt(req.body?.meet_date) : undefined
res.locals.duration = req.body?.duration ? parseInt(req.body?.duration) : undefined
res.locals.chat_id = req.body?.chat_id ? parseInt(req.body?.chat_id) : undefined
if (res.locals.chat_id && !db.prepare(`select id from chats where project_id = :project_id and id = :chat_id`).pluck(true).get(res.locals))
throw Error('INCORRECT_CHAT_ID::400')
const id = db
.prepare(`
insert into meetings (project_id, name, description, place, created_by, meet_date, duration)
values (:project_id, :name, :description, :place, :user_id, :meet_date, :duration)
returning id
`)
.pluck(true)
.get(res.locals)
const meeting = getMeeting(id, res.locals.user_id)
res.status(200).json({success: true, data: meeting})
})
app.use('/project/:pid(\\d+)/meeting/:mid(\\d+)*', (req, res, next) => {
res.locals.meeting_id = req.params.mid
const meeting = db
.prepare(`
select created_by
from meetings
where id = :meeting_id and project_id = :project_id
and (created_by = :user_id or exists(select 1 from meeting_users where meeting_id = :meeting_id and user_id = :user_id))
`)
.get(res.locals)
if (!meeting)
throw Error('NOT_FOUND::404')
res.locals.is_author = meeting.created_by == res.locals.user_id
next()
})
app.get('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => {
const meeting = getMeeting(req.params.mid, res.locals.user_id)
res.status(200).json({success: true, data: meeting})
})
app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => {
if (!res.locals.is_author)
throw Error('ACCESS_DENIED::401')
res.locals.id = res.locals.meeting_id
res.locals.name = req.body?.name
res.locals.description = req.body?.description
res.locals.place = req.body?.place
res.locals.meet_date = req.body?.meet_date ? parseInt(req.body?.meet_date) : undefined
res.locals.chat_id = req.body?.chat_id ? parseInt(req.body?.chat_id) : undefined
res.locals.is_cancel = +!!req.body?.is_cancel
if (res.locals.chat_id && !db.prepare(`select id from chats where project_id = :project_id and id = :chat_id`).pluck(true).get(res.locals))
throw Error('INCORRECT_CHAT_ID::400')
const info = db
.prepareUpdate('meetings', ['name', 'description', 'place', 'meet_date', 'chat_id', 'is_cancel'], res.locals, ['id', 'project_id'])
.run(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
const meeting = getMeeting(res.locals.meeting_id, res.locals.user_id)
res.status(200).json({success: true, data: meeting})
})
app.delete('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => {
if (!res.locals.is_author)
throw Error('ACCESS_DENIED::401')
const info = db
.prepare(`delete from meetings where id = :meeting_id and project_id = :project_id and created_by = :user_id`)
.run(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true, data: {id: res.locals.meeting_id}})
})
app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)/participant', (req, res, next) => {
if (!res.locals.is_author)
throw Error('ACCESS_DENIED::401')
const user_ids = req.body instanceof Array ? [...new Set(req.body.map(e => parseInt(e)))] : []
// Проверка, что выбранные пользователи имеют доступ к проекту
let rows = db
.prepare(`
select user_id
from chat_users
where chat_id in (select id from chats where project_id = :project_id)
`)
.pluck(true)
.all(res.locals)
if (user_ids.some(user_id => rows.indexOf(user_id)) == -1)
throw Error('INACCESSABLE_USER::400')
db
.prepare(`delete from meeting_users where meeting_id = :meeting_id`)
.run(res.locals)
res.locals.json_ids = JSON.stringify(user_ids)
db
.prepare(`insert into meeting_users (meeting_id, user_id) select :meeting_id, value from json_each(:json_ids)`)
.run(res.locals)
res.status(200).json({ success: true, data: user_ids })
})
// FILES
app.get('/project/:pid(\\d+)/file', (req, res, next) => {
// 1. Из групп, которые есть в проекте и в которых участвует пользователь
// 2. Из задач проекта, где пользователь автор, ответсвенный или наблюдатель
// 3. Из встреч на проекте, где пользователь создатель или участник
const rows = db
.prepare(`
select f.id, f.chat_id, c.telegram_id telegram_chat_id, f.message_id, f.filename, f.mime, f.caption, f.size, f.published_by, f.published, f.parent_id, f.parent_type
from files f
left join chats c on f.chat_id = c.id and f.parent_type = 0
where f.project_id = :project_id and (
chat_id in (select chat_id from chat_users where user_id = :user_id)
or
parent_type = 1 and parent_id in (
select id
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))
)
or
parent_type = 2 and parent_id in (
select id
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))
)
)
`)
.safeIntegers(true)
.all(res.locals)
res.status(200).json({success: true, data: rows})
})
app.post('/project/:pid(\\d+)/:type(task|meeting)/:id(\\d+)/attach', upload.any(), async (req, res, next) => {
res.locals.parent_id = req.params.id
res.locals.parent_type = req.params.type == 'task' ? 1 : 2
const chat_id = db
.prepare(`
select coalesce(chat_id, (select upload_chat_id from customers where id = :customer_id))
from ${req.params.type}s
where id = :parent_id and project_id = :project_id`)
.pluck(true)
.get(res.locals)
if (!chat_id)
throw Error('EMPTY_DESTINATION::500')
const file_ids = []
for (const file of req.files) {
if (file.size == 0)
continue
const filedata = {
project_id: req.params.pid,
chat_id,
filename: file.originalname,
mime: file.mimetype,
data: file.buffer,
size: file.size,
published_by: res.locals.user_id,
pablished: Math.floor(Date.now() / 1000),
parent_type: res.locals.parent_type,
parent_id: req.params.id
}
const file_id = await bot.sendFile(filedata)
if (file_id)
file_ids.push(file_id)
}
if (file_ids.length == 0)
throw Error('EMPTY_UPLOAD::500')
const files = db
.prepare(`select id, chat_id, message_id, filename, mime, size, published_by, published from files where id in (` + file_ids.join(',') + `)`)
.all()
res.status(200).json({success: true, data: files})
})
app.use('/project/:pid(\\d+)/file/:fid(\\d+)', (req, res, next) => {
res.locals.file_id = req.params.fid
const file = db
.prepare(`select * from files where id = :file_id and project_id = :project_id`)
.get(res.locals)
if (!file)
throw Error('NOT_FOUND::404')
if (file.parent_type == 0) {
res.locals.chat_id = file.chat_id
const row = db
.prepare(`select 1 from chat_users where chat_id = :chat_id and user_id = :user_id`)
.get(res.locals)
if (row) {
res.locals.can_download = true
}
} else {
res.locals.parent_id = file.parent_id
const parent = file.parent_type == 1 ? 'task' : 'meeting'
const row = db
.prepare(`
select 1
from ${parent}s
where id = :parent_id and project_id = :project_id or
exists(select 1 from ${parent}_users where ${parent}_id = :parent_id and user_id = :user_id)
`)
.get(res.locals)
if (row) {
res.locals.can_download = true
res.locals.can_delete = file.published_by == res.locals.user_id
}
}
next()
})
app.get('/project/:pid(\\d+)/file/:fid(\\d+)', async (req, res, next) => {
if (!res.locals.can_download)
throw Error('NOT_FOUND::404')
const file = await bot.downloadFile(res.locals.project_id, res.locals.file_id)
res.writeHead(200, {
'Content-Length': file.size,
'Content-Type': file.mime,
'Content-Disposition': contentDisposition(file.filename)
})
res.end(file.data)
})
app.delete('/project/:pid(\\d+)/file/:fid(\\d+)', (req, res, next) => {
if (!res.locals.can_delete)
throw Error('NOT_FOUND::404')
const info = db
.prepare(`delete from files where id = :id and project_id = :project_id`)
.run(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true, data: {id: res.locals.file_id}})
})
app.get('/settings', (req, res, next) => {
const row = db
.prepare(`select coalesce(json_settings, '{}') from users where id = :user_id`)
.pluck(true)
.get(res.locals)
res.status(200).json({success: true, data: JSON.parse(row)})
})
app.put('/settings', (req, res, next) => {
res.locals.json_settings = JSON.stringify(req.body || {})
const row = db
.prepare(`update users set json_settings = :json_settings where id = :user_id`)
.run(res.locals)
res.status(200).json({success: true})
})
module.exports = app

View File

@@ -0,0 +1,22 @@
СТРУКТУРА БАЗЫ
1. Переименована таблица documents в files
2. В таблицу files добавлено поле published, содержащее дату публикации файла
3. В таблице chats
* Добавлена колонка owner_id (ссылка на users.id; пусто)
* Добавлена колонка description
* Переименована колонка logo_url в logo
МИНИАПП
1. Операции POST/PUT возвращают объект
2. Операция DELETE возвращает data: {id: id-удаленого объекта}
3. Роут /project/:id/meeting/:id/participants заменен на /project/:id/meeting/:id/participant
4. В роут /project/:pid(\\d+)/file добавлен вывод origin_tg_chat_id для возможности сформировать ссылку на чат/сообщение на клиенте
5. Роут /project/:id/task или meeting/:id/attach возвращает описания файлов, а не просто их id
АДМИНКА
Общее
1. Файл /docs/api.xls обновлен, удалены ненужные листы
2. В коде document заменен на file

Binary file not shown.

View File

@@ -0,0 +1,182 @@
pragma foreign_keys = off;
create table if not exists customers (
id integer primary key autoincrement,
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_id integer,
plan integer,
json_balance text default '{}',
is_blocked integer default 0,
json_company text default '{}',
upload_chat_id integer,
json_backup_server text default '{}',
json_backup_params text default '{}',
json_settings text default '{}'
) strict;
create table if not exists projects (
id integer primary key autoincrement,
customer_id integer references customers(id) on delete cascade,
name text not null check(trim(name) <> '' and length(name) < 256),
description text check(description is null or length(description) < 4096),
logo text,
is_logo_bg integer default 0,
company_id integer references companies(id) on delete cascade,
is_archived integer default 0
) strict;
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,
invite_link text,
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_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 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,
json_settings text default '{}'
) strict;
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 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;
create table if not exists tasks (
id integer primary key autoincrement,
project_id integer references projects(id) on delete cascade,
name text not null check(trim(name) <> '' and length(name) < 256),
created_by integer references users(id) on delete set null,
assigned_to integer references users(id) on delete set null,
closed_by integer references users(id) on delete set null,
priority integer check(priority in (0, 1, 2, 3, 4, 5)) default 0,
status integer check(status >= 1 and status <= 10) default 1,
time_spent integer check(time_spent is null or time_spent > 0 and time_spent < 3 * 44640), -- one month
create_date integer,
plan_date integer,
close_date integer,
close_comment text check(close_comment is null or length(close_comment) < 1024),
json_close_file_ids text,
chat_id integer references chats(id) on delete set null
) strict;
create index if not exists idx_tasks_project_id on tasks (project_id);
create table if not exists meetings (
id integer primary key autoincrement,
project_id integer references projects(id) on delete cascade,
name text not null check(trim(name) <> '' and length(name) < 256),
description text check(description is null or length(description) < 1024),
place text check(place is null or length(place) < 4096),
created_by integer references users(id) on delete set null,
meet_date integer,
duration integer,
chat_id integer references chats(id) on delete set null,
is_cancel integer default 0
) strict;
create index if not exists idx_meetings_project_id on meetings (project_id);
create table if not exists files (
id integer primary key autoincrement,
project_id integer references projects(id) on delete cascade,
chat_id integer references chats(id) on delete set null,
message_id integer,
file_id integer,
access_hash integer,
filename text not null check(length(filename) < 256),
mime text check(mime is null or length(mime) < 128),
size integer,
caption text check(caption is null or length(caption) < 2048),
published_by integer references users(id) on delete set null,
published integer,
parent_type integer check(parent_type in (0, 1, 2, 3)) default 0,
parent_id integer
) strict;
create index if not exists idx_files_project_id on files (project_id);
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),
description text check(description is null or length(description) < 4096),
logo text
) strict;
create index if not exists idx_companies_project_id on companies (project_id);
create table if not exists company_mappings (
project_id integer references projects(id) on delete cascade,
company_id integer references companies(id) on delete cascade,
--show_as_id integer references companies(id) on delete cascade,
show_to_id integer references companies(id) on delete cascade
) strict;
create index if not exists idx_company_mappings_project_id on company_mappings (project_id);
create table if not exists task_users (
task_id integer references tasks(id) on delete cascade,
user_id integer references users(id) on delete cascade,
primary key (task_id, user_id)
) without rowid;
create table if not exists meeting_users (
meeting_id integer references meetings(id) on delete cascade,
user_id integer references users(id) on delete cascade,
primary key (meeting_id, user_id)
) without rowid;
create table if not exists company_users (
company_id integer references companies(id) on delete cascade,
user_id integer references users(id) on delete cascade,
primary key (company_id, user_id)
) without rowid;
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 (chat_id, user_id)
) without rowid;
pragma foreign_keys = on;
create trigger if not exists trg_chats_update after update on chats
when NEW.project_id is null
begin
delete from chat_users where chat_id = NEW.id;
end;

167050
backend/_old/v8/debug.log Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -0,0 +1,46 @@
const fs = require('fs')
const crypto = require('crypto')
const sqlite3 = require('better-sqlite3')
const db = sqlite3(`./data/db.sqlite`)
db.pragma('journal_mode = WAL')
db.pragma('foreign_keys = on')
db.exec(fs.readFileSync('./data/init.sql', 'utf8'))
db.function('generate_key', (id, time) => {
return [
'KEY',
Buffer.from(time + '').toString('base64'),
crypto.createHash('md5').update(`sa${time}-${id}lt`).digest('hex')
].join('-')
})
process.on('exit', () => db.close())
process.on('SIGHUP', () => process.exit(128 + 1))
process.on('SIGINT', () => process.exit(128 + 2))
process.on('SIGTERM', () => process.exit(128 + 15))
db.prepareUpdate = function (table, columns, data, where) {
const dataColumns = columns.filter(col => col in data)
if (dataColumns.length == 0)
throw Error('SQLite Error: No data to update')
return db.prepare(`update "${table}" ` +
`set ` + dataColumns.map(col => `"${col}" = :${col}`).join(', ') +
` where ` + where.map(col => `"${col}" = :${col}`).join(' and '))
}
db.prepareUpsert = function (table, columns, data, where) {
const dataColumns = columns.filter(col => col in data)
if (dataColumns.length == 0)
throw Error('SQLite Error: No data to update')
return db.prepare(
`insert into "${table}" (` + [...dataColumns, ...where].join(', ') + `) values (:` + [...dataColumns, ...where].join(', :') +
`) on conflict (` + where.join(',') + `) do update ` +
`set ` + dataColumns.map(col => `"${col}" = :${col}`).join(', ') +
` where ` + where.map(col => `"${col}" = :${col}`).join(' and '))
}
module.exports = db

1991
backend/_old/v8/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
{
"name": "telegram-bot",
"version": "1.0.0",
"main": "app.js",
"directories": {
"doc": "docs"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"better-sqlite3": "^11.8.0",
"body-parser": "^1.20.3",
"content-disposition": "^0.5.4",
"cookie-parser": "^1.4.7",
"express": "^4.21.2",
"express-session": "^1.18.1",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.16",
"telegram": "^2.26.16"
}
}

Binary file not shown.

View File

@@ -1 +1,2 @@
node --env-file .env app
node --env-file .env app
pause

View File

@@ -1,20 +1,39 @@
const http = require('http')
const express = require('express')
const WebSocket = require('ws')
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser')
const cookie = require('cookie')
const crypto = require('crypto')
const fs = require('fs')
const util = require('util')
const bot = require('./apps/bot')
const log = require('./include/log')
const app = express()
app.use(bodyParser.json({limit: '10mb'}))
app.use(cookieParser())
const adminapp = require('./apps/admin')
const miniapp = require('./apps/miniapp')
BigInt.prototype.toJSON = function () {
return Number(this)
}
const app = express()
const server = http.createServer(app)
const wss = new WebSocket.Server({ server })
app.use(bodyParser.json({limit: '10mb'}))
app.use(cookieParser())
app.use((req, res, next) => {
const start = Date.now()
const end = res.end
res.end = function (chunk, encoding) {
log.http (req, res, Date.now() - start)
end.apply (res, arguments)
}
next()
})
app.use((req, res, next) => {
if(!(req.body instanceof Object))
return next()
@@ -31,7 +50,6 @@ app.use((req, res, 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 = process.env.BOT_TOKEN || '7236504417:AAGVaodw3cRwGlf-jAhwnYb51OHaXcgpW8k'
const dataCheckString = Object.keys(data).sort().map((key) => `${key}=${data[key]}`).join('\n')
@@ -40,7 +58,7 @@ app.post('(/api/admin/auth/telegram|/api/miniapp/auth)', (req, res, next) => {
const timeDiff = Date.now() / 1000 - data.auth_date
if (hmac !== req.query.hash) // || timeDiff > 10)
if (hmac !== req.query?.hash) // || timeDiff > 10)
throw Error('ACCESS_DENIED::401')
const user = JSON.parse(req.query.user)
@@ -53,26 +71,31 @@ app.post('(/api/admin/auth/telegram|/api/miniapp/auth)', (req, res, next) => {
next()
})
app.use('/api/admin', require('./apps/admin'))
app.use('/api/miniapp', require('./apps/miniapp'))
app.use('/api/admin', adminapp.router)
app.use('/api/miniapp', miniapp.router)
app.use((err, req, res, next) => {
console.error(`Error for ${req.path}: ${err}`)
console.trace()
console.log('\n\n')
log.error(err)
let message, code
[message, code = 500] = err.message.split('::')
res.status(+code).json({success: false, error: { message, code}})
})
wss.on('connection', (ws, req) => {
const sid = cookie.parse(req.headers.cookie || '')?.sid
if (!miniapp.registerWS(sid, ws) && !adminapp.registerWS(sid, ws))
return ws.close(1008, 'Unauthorized')
})
const PORT = process.env.PORT || 3000
app.listen(PORT, async () => {
server.listen(PORT, async () => {
console.log(`Listening at port ${PORT}`)
bot.start(
+(process.env.API_ID || 26746106),
process.env.API_HASH || '29e5f83c04e635fa583721473a6003b5',
process.env.BOT_TOKEN || '7236504417:AAGVaodw3cRwGlf-jAhwnYb51OHaXcgpW8k'
process.env.BOT_TOKEN || '7236504417:AAGVaodw3cRwGlf-jAhwnYb51OHaXcgpW8k',
process.env.BOT_SID || ''
)
})

View File

@@ -1,15 +1,27 @@
const crypto = require('crypto')
const express = require('express')
const WebSocket = require('ws')
const db = require('../include/db')
const log = require('../include/log')
const eventBus = require('../include/eventbus')
const bot = require('./bot')
const fs = require('fs')
const app = express.Router()
const sessions = {}
function registerWS(sid, ws) {
const session = sessions[sid]
if (session)
session.ws = ws
return !!session
}
const cache = {
// email -> code
register: {},
upgrade: {},
recovery: {},
'change-password': {},
'change-email': {},
@@ -42,8 +54,8 @@ app.use((req, res, next) => {
if (public.includes(req.path))
return next()
const asid = req.query.asid || req.cookies.asid
req.session = sessions[asid]
const sid = req.query.sid || req.cookies.sid
req.session = sessions[sid]
if (!req.session)
throw Error('ACCESS_DENIED::401')
@@ -57,9 +69,9 @@ function createSession(req, res, 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`])
const sid = crypto.randomBytes(64).toString('hex')
req.session = sessions[sid] = {sid, customer_id }
res.setHeader('Set-Cookie', [`sid=${sid};httpOnly;path=/api/admin`])
}
app.post('/auth/email', (req, res, next) => {
@@ -67,7 +79,7 @@ app.post('/auth/email', (req, res, next) => {
res.locals.password = req.body?.password
const customer_id = db
.prepare(`select id from customers where is_blocked = 0 and email = :email and password is not null and password = :password `)
.prepare(`select id from customers where email = :email and password is not null and password = :password `)
.pluck(true)
.get(res.locals)
@@ -75,7 +87,7 @@ app.post('/auth/email', (req, res, next) => {
throw Error('AUTH_ERROR::401')
createSession(req, res, customer_id)
res.status(200).json({success: true})
res.status(200).json({success: true })
})
app.post('/auth/telegram', (req, res, next) => {
@@ -83,24 +95,25 @@ app.post('/auth/telegram', (req, res, next) => {
.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`)
.prepare(`insert 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})
res.status(200).json({ success: true })
})
/*
Регистрация нового клиента выполняется за ТРИ последовательных вызова
Регистрация нового клиента/Перевод авторизации с TG на email выполняется за ТРИ последовательных вызова
1. Отравляется email. Если email корректный и уже неиспользуется, то сервер возвращает ОК и на указанный email отправляется код.
2. Отправляется email + код из письма. Если указан корректный код, то сервер отвечает ОК.
3. Отправляется email + код из письма + желаемый пароль. Если все ОК, то сервер создает учетную запись и возвращает ОК.
*/
app.post('/auth/email/register', (req, res, next) => {
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)
@@ -119,12 +132,12 @@ app.post('/auth/email/register', (req, res, next) => {
throw Error('USED_EMAIL::400')
const code = Math.random().toString().substr(2, 4)
cache.register[email] = code
sendEmail(email, 'REGISTER', `${email} => ${code}`)
cache[action][email] = code
sendEmail(email, action.toUpperCase(), `${email} => ${code}`)
}
if (stepNo == 2) {
if (cache.register[email] != code)
if (cache[action][email] != code)
throw Error('INCORRECT_CODE::400')
}
@@ -132,11 +145,11 @@ app.post('/auth/email/register', (req, res, next) => {
if (!checkPassword(password))
throw Error('INCORRECT_PASSWORD::400')
db
.prepare('insert into customers (email, password) values (:email, :password)')
.run({email, password})
const query = action == 'register' ? 'insert into customers (email, password) values (:email, :password)' :
'update customers set email = :email, password = :password, telegram_id = null where id = :id'
db.prepare(query).run({email, password, id: res.locals.customer_id})
delete cache.register[email]
delete cache[action][email]
}
res.status(200).json({success: true})
@@ -144,23 +157,25 @@ app.post('/auth/email/register', (req, res, next) => {
/*
Смена email выполняется за ЧЕТЫРЕ последовательных вызовов
Смена email выполняется за ПЯТЬ последовательных вызовов
1. Отравляется пустой закпрос. Сервер на email пользователя из базы отправляет код.
2. Отправляется код из письма. Если указан корректный код, то сервер отвечает ОК.
3. Отправляется код из письма + новый email. Сервер отправляет код2 на новый email.
4. Отправлются оба кода и новый email. Если они проходят проверку, то сервер меняет email пользователя на новый и возвращает ОК.
4. Отправлются оба кода и новый email. Если коды проходят проверку, то сервер отвечает ОК.
5. Отправлются оба кода, новые email и password. Если они проходят проверку, то сервер меняет 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 password = String(req.body.password ?? '').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
const stepNo = !code ? 1 : code && !email2 ? 2 : code && email2 && !code2 ? 3 : code && email2 && code2 && !password ? 4 : code && email2 && code2 && password ? 5 : -1
if (stepNo == -1)
throw Error('BAD_STEP::400')
@@ -187,9 +202,17 @@ app.post('/auth/email/change-email', (req, res, next) => {
if (stepNo == 4) {
if (cache['change-email'][email] != code || cache['change-email2'][email2] != code2)
throw Error('INCORRECT_CODE::400')
}
if (stepNo == 5) {
if (!checkPassword(password))
throw Error('INCORRECT_PASSWORD::400')
res.locals.email = email2
res.locals.password = password
const info = db
.prepare('update customers set email = :email where id = :customer_id')
.prepare('update customers set email = :email, password = :password where id = :customer_id')
.run(res.locals)
if (info.changes == 0)
@@ -210,7 +233,7 @@ app.post('/auth/email/change-email', (req, res, next) => {
*/
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 password = String(req.body.password ?? '').trim()
const action = req.params.action
const email = action == 'change-password' ? db
@@ -260,18 +283,21 @@ app.post('/auth/email/:action(change-password|recovery)', (req, res, next) => {
})
app.get('/auth/logout', (req, res, next) => {
if (req.session?.asid)
delete sessions[req.session.asid]
if (req.session?.sid)
delete sessions[req.session.sid]
res.setHeader('Set-Cookie', [`asid=; expired; httpOnly;path=/api/admin`])
res.setHeader('Set-Cookie', [`sid=; expired; httpOnly;path=/api/admin`])
res.status(200).json({success: true})
})
// CUSTOMER
app.get('/customer/profile', (req, res, next) => {
res.locals.time = Math.floor(Date.now() / 1000)
const row = db
.prepare(`
select id, name, email, plan, coalesce(json_balance, '{}') json_balance, coalesce(json_company, '{}') json_company, upload_chat_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
`)
@@ -298,6 +324,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(
@@ -333,96 +361,86 @@ app.put('/customer/settings', (req, res, next) => {
})
// PROJECT
app.get('/project', (req, res, next) => {
const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : ''
const rows = db
function getProject(id, customer_id) {
const row = db
.prepare(`
select id, name, description, logo, is_logo_bg, is_archived,
select id, name, description, logo, is_logo_bg, company_id, 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}
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, company_id, 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
res.locals.project_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.redirect(req.baseUrl + `/project?id=${id}`)
})
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 = req.body?.is_logo_bg
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')
res.redirect(req.baseUrl + `/project?id=${req.params.pid}`)
})
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`)
const json_company = db
.prepare(`select coalesce(json_company, '{}') from customers where id = :customer_id`)
.pluck(true)
.all(res.locals)
.get(res.locals)
for (const chatId of chatIds) {
await bot.sendMessage(chatId, res.locals.is_archived ? 'Проект помещен в архив. Отслеживание сообщений прекращено.' : 'Проект восстановлен из архива.')
}
res.redirect(req.baseUrl + `/project?id=${req.params.pid}`)
res.locals.company_id = addCompany(Object.assign({
name: 'My Company',
address: null,
email: null,
phone: null,
site: null,
description: null,
logo: null
}, JSON.parse(json_company), {project_id: res.locals.project_id}))
db
.prepare(`update projects set company_id = :company_id where id = :project_id`)
.run(res.locals)
const data = getProject(res.locals.project_id, res.locals.customer_id)
res.status(200).json({success: true, data})
})
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
@@ -435,55 +453,138 @@ 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) => {
try {
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 chat_ids = db
.prepare(`select id from chats where project_id = :id`)
.pluck(true)
.all(res.locals)
for (const chat_id of chat_ids) {
await bot.sendMessage(chat_id, res.locals.is_archived ? 'ON_PROJECT_ARCHIVE' : 'ON_PROJECT_RESTORE')
}
const data = getProject(req.params.pid, res.locals.customer_id)
res.status(200).json({success: true, data})
} catch (err) {
next(err)
}
})
// 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,
(select company_id from company_users where user_id = :id) company_id,
(select json_group_array(chat_id) from chat_users where user_id = :id and chat_id in (select id from chats where project_id = :project_id)) chats
from users u
left join user_details ud on u.id = ud.user_id and ud.project_id = :project_id
where id = :id
`)
.safeIntegers(true)
.get({id, project_id})
if (!row)
throw Error('NOT_FOUND::404')
row.chats = JSON.parse(row.chats || '[]')
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,
(select company_id from company_users where user_id = u.id) company_id
from users u
left join user_details ud on u.id = ud.user_id and ud.project_id = :project_id
where id in (
select user_id
from chat_users
where chat_id in (select id from chats where project_id = :project_id)
) ${where}
)
`)
.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) => {
@@ -497,67 +598,92 @@ app.get('/project/:pid(\\d+)/token', (req, res, next) => {
if (!key)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true, data: key})
res.status(200).json({ success: true, data: key })
})
// 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 addCompany(data) {
return db
.prepare(`
select id, name, email, phone, description, logo,
(select json_chat_array(user_id) from company_users where company_id = c.id) users
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(true)
.get(data)
}
function getCompany(id, project_id) {
const row = 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 ${where}
where c.id = :id and project_id = :project_id
order by name
`)
.get({id, project_id})
if (!row)
throw Error('NOT_FOUND::404')
row.users = JSON.parse(row.users || '[]')
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 company_id = c.id from projects where id = :project_id) is_own,
(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
res.locals.description = req.body?.description
res.locals.logo = req.body?.logo
const id = db
.prepare(`
insert into companies (project_id, name, email, phone, site, description, logo)
values (:project_id, :name, :email, :phone, :site, :description, :logo)
returning id
`)
.pluck(res.locals)
.get(res.locals)
res.redirect(req.baseUrl + `/project/${req.params.pid}/company?id=${id}`)
const id = addCompany(res.locals)
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)
@@ -565,55 +691,24 @@ app.put('/project/:pid(\\d+)/company/:cid(\\d+)', (req, res, next) => {
if (info.changes == 0)
throw Error('NOT_FOUND::404')
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.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`)
.prepare(`
delete from companies
where id = :company_id and project_id = :project_id and
not exists(select company_id from projects where id = :project_id)`)
.run(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true})
})
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 chats
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+)/chat/:gid(\\d+)', (req, res, next) => {
res.redirect(req.baseUrl + `/project/${req.params.pid}/chat?id=${req.params.uid}`)
})
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 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})
res.status(200).json({success: true, data: {id: req.params.cid}})
})
app.put('/project/:pid(\\d+)/company/:cid(\\d+)/user', (req, res, next) => {
@@ -655,7 +750,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)
@@ -670,4 +764,140 @@ app.put('/project/:pid(\\d+)/company/:cid(\\d+)/user', (req, res, next) => {
res.status(200).json({success: true})
})
module.exports = app
app.get('/project/:pid(\\d+)/company/mapping', (req, res, next) => {
const data = db
.prepare(`
select company_id, json_group_array(show_to_id) show_to_ids
from company_mappings
where project_id = :project_id and company_id <> show_to_id
`)
.all(res.locals)
data.forEach(row => {
row.show_to_ids = JSON.parse(row.show_to_ids || '[]')
})
res.status(200).json({success: true, data})
})
app.put('/project/:pid(\\d+)/company/mapping', (req, res, next) => {
if(!(req.body instanceof Array))
throw Error('ARRAY_REQUIRED::500')
db
.prepare(`delete from company_mappings where project_id = :project_id`)
.run(res.locals)
req.body
.filter(row => Number.isInteger(row.company_id) && row.show_to_ids instanceof Array && row.show_to_ids.every(id => Number.isInteger(id)))
.forEach(row => {
row.show_to_ids.push(row.company_id)
const json_ids = row.show_to_ids.join(', ')
const check = db
.prepare(`select count(1) from companies where project_id = :project_id and id in (${json_ids}) `)
.get(res.locals)
if (check.count)
return log.error (Error('IGNORE: ' + JSON.stringify(row)))
const locals = {
project_ids: res.locals.project_id,
company_id: row.company_id,
json_ids
}
db
.prepare(`
insert into company_mappings (project_id, company_id, show_to_id) values
select :project_ids, :company_id, value from json_each(:json_ids)
`)
.run(locals)
})
res.status(200).json({ success: true })
})
// CHATS
function getChat(id, project_id) {
const row = db
.prepare(`
select id, name, telegram_id, is_channel, invite_link, description, logo, user_count, bot_can_ban
from chats
where id = :id and project_id = :project_id
`)
.get({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, invite_link, 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) => {
try {
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, 'ON_CHAT_REMOVE')
res.status(200).json({success: true, data: {id: req.params.gid}})
} catch (err){
next(err)
}
})
eventBus.on('chat-attached', chat => {
const customer_id = db
.prepare(`select customer_id from projects where id = :project_id`)
.pluck(true)
.get(chat)
if (!customer_id)
return
const msg = {
event: 'chat-attached',
entity: 'chat',
id: chat.id,
data: getChat(chat.id, chat.project_id)
}
Object.values(sessions)
.filter(s => s.customer_id == customer_id && s.ws?.readyState === WebSocket.OPEN)
.map(s => s.ws)
.forEach(ws => ws.send(JSON.stringify(msg)))
})
module.exports = { router: app, registerWS }

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,12 @@
const express = require('express')
const multer = require('multer')
const crypto = require('crypto')
const fs = require('fs')
const contentDisposition = require('content-disposition')
const bot = require('./bot')
const db = require('../include/db')
const log = require('../include/log')
const eventBus = require('../include/eventbus')
const app = express.Router()
const upload = multer({
@@ -15,23 +16,47 @@ const upload = multer({
}
})
const sessions = {}
function registerWS(sid, ws) {
const session = sessions[sid]
if (session)
session.ws = ws
return !!session
}
function hasAccess(project_id, user_id) {
return !!db
return !!db
.prepare(`select 1 from projects where id = :project_id and is_archived <> 1`)
.pluck(true)
.get({project_id}) &&
!!db
.prepare(`
select 1
from chat_users
where user_id = :user_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)
not exists(select 1 from user_details where user_id = :user_id and project_id = :project_id and is_blocked = 1)
`)
.pluck(true)
.get({project_id, user_id})
}
const sessions = {}
function getUserInfo (user_id, project_id) {
const user = db
.prepare(`
select u.telegram_id, coalesce(ud.fullname, coalesce(u.lastname, '') || ' ' || coalesce(u.firstname, '')) name
from users u left join user_details ud on u.id = ud.user_id and ud.project_id = :project_id
where u.id = :user_id
`)
.safeIntegers(true)
.get({ user_id, project_id })
return user
}
app.use((req, res, next) => {
if (req.path == '/user/login')
if (req.path == '/auth')
return next()
const sid = req.query.sid || req.cookies.sid
@@ -43,8 +68,7 @@ app.use((req, res, next) => {
next()
})
app.post('/user/login', (req, res, next) => {
app.post('/auth', (req, res, next) => {
db
.prepare(`insert or ignore into users (telegram_id) values (:telegram_id)`)
.safeIntegers(true)
@@ -58,18 +82,16 @@ app.post('/user/login', (req, res, next) => {
const sid = crypto.randomBytes(64).toString('hex')
req.session = sessions[sid] = {sid, user_id}
res.setHeader('Set-Cookie', [`sid=${sid};httpOnly;path=/`])
res.setHeader('Set-Cookie', [`sid=${sid};httpOnly;path=/api/miniapp`])
res.locals.user_id = user_id
res.status(200).json({success: true})
res.status(200).json({ success: true })
})
app.get('/project', (req, res, next) => {
const where = req.query.id ? ' and p.id = ' + parseInt(req.query.id) : ''
const rows = db
.prepare(`
select p.id, p.name, p.description, p.logo,
select p.id, p.name, p.description, p.logo, p.is_logo_bg, company_id,
c.name customer_name, c.upload_chat_id <> 0 has_upload
from projects p
inner join customers c on p.customer_id = c.id
@@ -78,18 +100,15 @@ app.get('/project', (req, res, next) => {
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
and p.is_archived <> 1
`)
.all(res.locals)
if (where && rows.length == 0)
throw Error('NOT_FOUND::404')
rows.forEach(row => {
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: rows})
})
app.use('/project/:pid(\\d+)/*', (req, res, next) => {
@@ -99,17 +118,27 @@ app.use('/project/:pid(\\d+)/*', (req, res, next) => {
throw Error('ACCESS_DENIED::401')
const row = db
.prepare('select customer_id from projects where id = :project_id')
.prepare('select customer_id, company_id from projects where id = :project_id')
.get(res.locals)
res.locals.customer_id = row.customer_id
res.locals.customer_company_id = row.company_id
next()
})
app.get('/project/:pid(\\d+)/user', (req, res, next) => {
const where = req.query.id ? ' and u.id = ' + parseInt(req.query.id) : ''
function getUserCompanyId(user_id, project_id) {
return db
.prepare(`
select company_id
from company_users
where user_id = :user_id and company_id in (select id from companies where project_id = :project_id)
`)
.pluck(true)
.get({ user_id, project_id })
}
app.get('/project/:pid(\\d+)/user', (req, res, next) => {
const users = db
.prepare(`
with actuals (user_id) as (
@@ -125,7 +154,7 @@ app.get('/project/:pid(\\d+)/user', (req, res, next) => {
union
select created_by from meetings where project_id = :project_id
union
select published_by from documents where project_id = :project_id
select published_by from files where project_id = :project_id
),
members (user_id, is_leave) as (
select user_id, 0 is_leave from actuals
@@ -138,8 +167,9 @@ app.get('/project/:pid(\\d+)/user', (req, res, next) => {
u.firstname,
u.lastname,
u.photo,
u.json_phone_projects,
ud.fullname,
ud.email,
ud.phone,
ud.role,
ud.department,
ud.is_blocked,
@@ -150,138 +180,189 @@ app.get('/project/:pid(\\d+)/user', (req, res, next) => {
m.is_leave
from users u
inner join members m on u.id = m.user_id
left join user_details ud on ud.user_id = u.id and ud.project_id = :project_id
where 1 = 1 ${where}
left join user_details ud on ud.user_id = u.id and ud.project_id = :project_id
`)
.safeIntegers(true)
.all(res.locals)
const companies = db
.prepare('select id, name, email, phone, site, description from companies where project_id = :project_id')
res.locals.company_id = getUserCompanyId(res.locals.user_id, res.locals.project_id)
// Список компаний, которые НЕ ВИДНЫ компании пользователя на проекте
const hidden = db
.prepare(`
select company_id from company_mappings where project_id = :project_id
except
select company_id from company_mappings where project_id = :project_id and show_to_id = :company_id`)
.pluck(true)
.all(res.locals)
.reduce((companies, row) => {
companies[row.id] = row
return companies
}, {})
users
.filter(user => user.company_id)
.filter(user => hidden.indexOf(user.company_id) != -1)
.forEach(user => user.company_id = res.locals.customer_company_id)
const mappings = {}
const company_id = users.find(m => m.id == res.locals.user_id).company_id
if (company_id) {
res.locals.company_id = company_id
db
.prepare('select show_as_id, show_to_id from company_mappings where project_id = :project_id and company_id = :company_id')
.all(res.locals)
.forEach(row => mappings[row.show_to_id] = row.show_to_id)
}
users.forEach(m => {
m.company = companies[mappings[m.company_id] || m.company_id]
delete m.company_id
})
users.forEach(m => {
const isHide = JSON.parse(m.json_phone_projects || []).indexOf(res.locals.project_id) == -1
if (isHide)
delete m.phone
delete m.json_phone_projects
})
if (where && users.length == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true, data: where ? users[0] : users})
res.status(200).json({success: true, data: users})
})
app.get('/project/:pid(\\d+)/user/reload', async (req, res, next) => {
const chatIds = db
.prepare(`select id from chats where project_id = :project_id`)
.all(res.locals)
.map(e => e.id)
try {
const chat_ids = 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))
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
for (const chatId of chatIds) {
await bot.reloadGroupUsers(chatId)
await sleep(1000)
}
for (const chat_id of chat_ids) {
await bot.reloadChatUsers(chat_id)
await sleep(1000)
}
res.status(200).json({success: true})
res.status(200).json({success: true})
} catch (err) {
next(err)
}
})
app.get('/project/:pid(\\d+)/chat', (req, res, next) => {
const where = req.query.id ? ' and id = ' + parseInt(req.query.id) : ''
app.get('/project/:pid(\\d+)/company', (req, res, next) => {
res.locals.company_id = getUserCompanyId(res.locals.user_id, res.locals.project_id)
const rows = db
.prepare(`
select id, name, telegram_id
from chats
where project_id = :project_id and id in (select chat_id from chat_users where user_id = :user_id)
${where}
select id, name, address, email, phone, site, description
from companies
where project_id = :project_id and (
id = :company_id or
id in (select company_id from company_mappings where project_id = :project_id and show_to_id = :company_id) or
id not in (select company_id from company_mappings where project_id = :project_id) or
(select :customer_company_id)
)
`)
.all(res.locals)
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: rows})
})
app.get('/project/:pid(\\d+)/chat/:gid(\\d+)', (req, res, next) => {
res.redirect(req.baseUrl + `/project/${req.params.pid}/chat?id=${req.params.gid}`)
// CHAT
app.get('/project/:pid(\\d+)/chat', (req, res, next) => {
const rows = db
.prepare(`
select id, name, invite_link, description, telegram_id, owner_id, user_count, logo,
(select json_group_array(user_id) from chat_users where chat_id = c.id) users,
(select count(1) from tasks where project_id = :project_id and chat_id = c.id) task_count
from chats c
where project_id = :project_id and id in (select chat_id from chat_users where user_id = :user_id)
`)
.safeIntegers(true)
.all(res.locals)
rows.forEach(row => {
row.users = JSON.parse(row.users)
})
res.status(200).json({success: true, data: rows})
})
// TASK
app.get('/project/:pid(\\d+)/task', (req, res, next) => {
const where = req.query.id ? ' and t.id = ' + parseInt(req.query.id) : ''
function getTask(id, user_id) {
const row = db
.prepare(`
select id, name, description, created_by, assigned_to, priority, status, time_spent, create_date, plan_date,
close_date, close_comment, coalesce(json_close_files, '[]') close_files, chat_id,
(select json_group_array(user_id) from task_users where task_id = t.id) observers,
(select json_group_array(id) from files where parent_type = 1 and parent_id = t.id) files
from tasks t
where t.id = :id
`)
.get({id})
if (!row)
throw Error('NOT_FOUND::404')
row.close_files = JSON.parse(row.close_files)
row.observers = JSON.parse(row.observers)
row.files = JSON.parse(row.files)
row.is_editable = row.created_by == user_id || row.assigned_to == user_id
return row
}
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_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
select id, name, description, created_by, assigned_to, priority, status, time_spent, create_date, plan_date,
close_date, close_comment, coalesce(json_close_files, '[]') close_files, chat_id,
(select json_group_array(user_id) from task_users where task_id = t.id) observers,
(select json_group_array(id) from files where parent_type = 1 and parent_id = t.id) files
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))
${where}
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) or
exists(select 1 from chat_users where chat_id = t.chat_id))
`)
.all(res.locals)
rows.forEach(row => {
row.close_files = JSON.parse(row.close_files)
row.observers = JSON.parse(row.observers)
row.attachments = JSON.parse(row.attachments)
row.files = JSON.parse(row.files)
})
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: rows})
})
app.get('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => {
res.redirect(req.baseUrl + `/project/${req.params.pid}/task?id=${req.params.tid}`)
})
app.post('/project/:pid(\\d+)/task', async (req, res, next) => {
try {
res.locals.name = req.body?.name
res.locals.description = req.body?.description
res.locals.status = parseInt(req.body?.status)
res.locals.priority = parseInt(req.body?.priority)
res.locals.assigned_to = req.body?.assigned_to ? parseInt(req.body?.assigned_to) : undefined
res.locals.create_date = Math.floor(Date.now() / 1000)
res.locals.plan_date = req.body?.plan_date ? parseInt(req.body?.plan_date) : undefined
res.locals.chat_id = req.body?.chat_id ? parseInt(req.body?.chat_id) : undefined
if (res.locals.assigned_to && !hasAccess(res.locals.project_id, res.locals.assigned_to))
throw Error('INCORRECT_ASSIGNED_TO::400')
app.post('/project/:pid(\\d+)/task', (req, res, next) => {
res.locals.name = req.body?.name
res.locals.status = parseInt(req.body?.status)
res.locals.priority = parseInt(req.body?.priority)
res.locals.assigned_to = req.body?.assigned_to ? parseInt(req.body?.assigned_to) : undefined
res.locals.create_date = Math.floor(Date.now() / 1000)
res.locals.plan_date = req.body?.plan_date ? parseInt(req.body?.plan_date) : undefined
if (res.locals.assigned_to && !hasAccess(res.locals.project_id, res.locals.assigned_to))
throw Error('INCORRECT_ASSIGNED_TO::400')
if (res.locals.chat_id && !db.prepare(`select id from chats where project_id = :project_id and id = :chat_id`).pluck(true).get(res.locals))
throw Error('INCORRECT_CHAT_ID::400')
const id = db
.prepare(`
insert into tasks (project_id, name, created_by, assigned_to, priority, status, create_date, plan_date)
values (:project_id, :name, :user_id, :assigned_to, :priority, :status, :create_date, :plan_date)
returning id
`)
.pluck(true)
.get(res.locals)
const id = db
.prepare(`
insert into tasks (project_id, name, description, created_by, assigned_to, priority, status, create_date, plan_date, chat_id)
values (:project_id, :name, :description, :user_id, :assigned_to, :priority, :status, :create_date, :plan_date, :chat_id)
returning id
`)
.pluck(true)
.get(res.locals)
res.status(200).json({success: true, data: id})
const task = getTask(id, res.locals.user_id)
if (res.locals.chat_id) {
const creator = getUserInfo(task.created_by, task.project_id)
const assignee = getUserInfo(task.assigned_to, task.project_id)
const args = {
PRIORITY: '$TASK_PRIORITY_' + (task.priority || '0'),
NAME: task.name,
PLAN_DATE: new Date(task.plan_date * 1000).toLocaleString('ru'),
CREATOR: creator.name,
CREATOR_ID: creator.telegram_id,
ASSIGNEE: assignee.name,
ASSIGNEE_ID: assignee.telegram_id,
URL: bot.USER_APP + '/?startapp=p' + res.locals.project_id + 't' + task.id
}
message_id = await bot.sendMessage(res.locals.chat_id, 'TASK_MESSAGE', args)
db
.prepare(`update tasks set message_id = :message_id where id = :id`)
.run({ id, message_id})
}
res.status(200).json({success: true, data: task})
} catch (err) {
next (err)
}
})
app.use('/project/:pid(\\d+)/task/:tid(\\d+)*', (req, res, next) => {
@@ -290,9 +371,12 @@ app.use('/project/:pid(\\d+)/task/:tid(\\d+)*', (req, res, next) => {
const task = db
.prepare(`
select created_by, assigned_to
from tasks
where id = :task_id and project_id = :project_id
and (created_by = :user_id or assigned_to = :user_id or exists(select 1 from task_users where task_id = :task_id and user_id = :user_id))
from tasks t
where id = :task_id and project_id = :project_id and (
created_by = :user_id or
assigned_to = :user_id or
exists(select 1 from task_users where task_id = :task_id and user_id = :user_id) or
exists(select 1 from chat_users where chat_id = t.chat_id))
`)
.get(res.locals)
@@ -305,18 +389,43 @@ app.use('/project/:pid(\\d+)/task/:tid(\\d+)*', (req, res, next) => {
next()
})
app.get('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => {
const task = getTask(req.params.tid, res.locals.user_id)
res.status(200).json({success: true, data: task})
})
app.put('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => {
if (!res.locals.is_author && !res.locals.is_assigned)
throw Error('ACCESS_DENIED::401')
res.locals.id = res.locals.task_id
res.locals.name = req.body?.name
res.locals.description = req.body?.description
res.locals.status = parseInt(req.body?.status)
res.locals.priority = parseInt(req.body?.priority)
res.locals.assigned_to = req.body?.assigned_to ? parseInt(req.body?.assigned_to) : undefined
res.locals.plan_date = req.body?.plan_date ? parseInt(req.body?.plan_date) : undefined
res.locals.close_comment = req.body?.close_comment
res.locals.json_close_files = null
res.locals.chat_id = req.body?.chat_id ? parseInt(req.body?.chat_id) : undefined
if (res.locals.chat_id && !db.prepare(`select id from chats where project_id = :project_id and id = :chat_id`).pluck(true).get(res.locals))
throw Error('INCORRECT_CHAT_ID::400')
const columns = res.locals.is_author ? ['name', 'assigned_to', 'priority', 'status', 'plan_date', 'time_spent'] : ['status', 'time_spent']
if (req.body?.close_files instanceof Array) {
const file_ids = db
.prepare(`select id from files where parent_type = 1 and parent_id = :task_id`)
.pluck(true)
.all(res.locals)
const close_files = req.body.close_files
.map(id => parseInt(id))
.filter(id => file_ids.indexOf(id) != -1)
res.json_close_files = JSON.stringify(close_files)
}
const columns = res.locals.is_author ? ['name', 'description', 'assigned_to', 'priority', 'status', 'plan_date', 'time_spent', 'close_comment', 'json_close_files', 'chat_id'] : ['status', 'time_spent', 'close_comment', 'json_close_file_ids']
const info = db
.prepareUpdate('tasks', columns, res.locals, ['id', 'project_id'])
.run(res.locals)
@@ -324,7 +433,8 @@ app.put('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => {
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true})
const task = getTask(res.locals.task_id, res.locals.user_id)
res.status(200).json({success: true, data: task})
})
app.delete('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => {
@@ -338,7 +448,7 @@ app.delete('/project/:pid(\\d+)/task/:tid(\\d+)', (req, res, next) => {
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true})
res.status(200).json({success: true, data: {id: res.locals.task_id}})
})
app.put('/project/:pid(\\d+)/task/:tid(\\d+)/observer', (req, res, next) => {
@@ -373,51 +483,82 @@ app.put('/project/:pid(\\d+)/task/:tid(\\d+)/observer', (req, res, next) => {
})
// MEETINGS
app.get('/project/:pid(\\d+)/meeting', (req, res, next) => {
const where = req.query.id ? ' and m.id = ' + parseInt(req.query.id) : ''
function getMeeting(id, user_id) {
const row = db
.prepare(`
select id, name, description, place, created_by, meet_date, chat_id, is_cancel,
(select json_group_array(user_id) from meeting_users where meeting_id = m.id) participants,
(select json_group_array(id) from files where parent_type = 2 and parent_id = m.id) files
from meetings m
where m.id = :id
`)
.get({id})
if (!row)
throw Error('NOT_FOUND::404')
row.participants = JSON.parse(row.participants)
row.files = JSON.parse(row.files)
row.is_editable = row.created_by == user_id
return row
}
app.get('/project/:pid(\\d+)/meeting', (req, res, next) => {
const rows = db
.prepare(`
select id, name, description, created_by, meet_date,
(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
select id, name, description, place, created_by, meet_date, duration, chat_id, is_cancel,
(select json_group_array(user_id) from meeting_users where meeting_id = m.id) participants,
(select json_group_array(id) from files where parent_type = 2 and parent_id = m.id) files,
created_by = :user_id is_editable
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))
${where}
`)
.all(res.locals)
rows.forEach(row => {
row.participants = JSON.parse(row.participants)
row.attachments = JSON.parse(row.attachments)
row.files = JSON.parse(row.files)
row.is_editable = Boolean(row.is_editable)
})
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: rows})
})
app.get('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => {
res.redirect(req.baseUrl + `/project/${req.params.pid}/meeting?id=${req.params.mid}`)
})
app.post('/project/:pid(\\d+)/meeting', async (req, res, next) => {
try {
res.locals.name = req.body?.name
res.locals.description = req.body?.description
res.locals.place = req.body?.place
res.locals.meet_date = req.body?.meet_date ? parseInt(req.body?.meet_date) : undefined
res.locals.duration = req.body?.duration ? parseInt(req.body?.duration) : undefined
res.locals.chat_id = req.body?.chat_id ? parseInt(req.body?.chat_id) : undefined
if (res.locals.chat_id && !db.prepare(`select id from chats where project_id = :project_id and id = :chat_id`).pluck(true).get(res.locals))
throw Error('INCORRECT_CHAT_ID::400')
app.post('/project/:pid(\\d+)/meeting', (req, res, next) => {
res.locals.name = req.body?.name
res.locals.description = req.body?.description
res.locals.meet_date = req.body?.meet_date ? parseInt(req.body?.meet_date) : undefined
const id = db
.prepare(`
insert into meetings (project_id, name, description, place, created_by, meet_date, duration, chat_id)
values (:project_id, :name, :description, :place, :user_id, :meet_date, :duration, :chat_id)
returning id
`)
.pluck(true)
.get(res.locals)
const id = db
.prepare(`
insert into meetings (project_id, name, description, created_by, meet_date)
values (:project_id, :name, :description, :user_id, :meet_date)
returning id
`)
.pluck(true)
.get(res.locals)
res.status(200).json({success: true, data: id})
const meeting = getMeeting(id, res.locals.user_id)
if (res.locals.chat_id) {
const url = bot.USER_APP + '/?startapp=p' + res.locals.project_id + 'm' + meeting.id
message_id = await bot.sendMessage(res.locals.chat_id, 'MEETING_MESSAGE', meeting)
db
.prepare(`update meetings set message_id = :message_id where id = :id`)
.run({ id, message_id})
}
res.status(200).json({success: true, data: meeting})
} catch (err) {
next(err)
}
})
app.use('/project/:pid(\\d+)/meeting/:mid(\\d+)*', (req, res, next) => {
@@ -440,6 +581,11 @@ app.use('/project/:pid(\\d+)/meeting/:mid(\\d+)*', (req, res, next) => {
next()
})
app.get('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => {
const meeting = getMeeting(req.params.mid, res.locals.user_id)
res.status(200).json({success: true, data: meeting})
})
app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => {
if (!res.locals.is_author)
throw Error('ACCESS_DENIED::401')
@@ -447,16 +593,23 @@ app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => {
res.locals.id = res.locals.meeting_id
res.locals.name = req.body?.name
res.locals.description = req.body?.description
res.locals.place = req.body?.place
res.locals.meet_date = req.body?.meet_date ? parseInt(req.body?.meet_date) : undefined
res.locals.chat_id = req.body?.chat_id ? parseInt(req.body?.chat_id) : undefined
res.locals.is_cancel = +!!req.body?.is_cancel
if (res.locals.chat_id && !db.prepare(`select id from chats where project_id = :project_id and id = :chat_id`).pluck(true).get(res.locals))
throw Error('INCORRECT_CHAT_ID::400')
const info = db
.prepareUpdate('meetings', ['name', 'description', 'meet_date'], res.locals, ['id', 'project_id'])
.prepareUpdate('meetings', ['name', 'description', 'place', 'meet_date', 'chat_id', 'is_cancel'], res.locals, ['id', 'project_id'])
.run(res.locals)
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true})
const meeting = getMeeting(res.locals.meeting_id, res.locals.user_id)
res.status(200).json({success: true, data: meeting})
})
app.delete('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => {
@@ -470,10 +623,10 @@ app.delete('/project/:pid(\\d+)/meeting/:mid(\\d+)', (req, res, next) => {
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true})
res.status(200).json({success: true, data: {id: res.locals.meeting_id}})
})
app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)/participants', (req, res, next) => {
app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)/participant', (req, res, next) => {
if (!res.locals.is_author)
throw Error('ACCESS_DENIED::401')
@@ -486,7 +639,7 @@ app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)/participants', (req, res, next)
from chat_users
where chat_id in (select id from chats where project_id = :project_id)
`)
.pluck(true) // .raw?
.pluck(true)
.all(res.locals)
if (user_ids.some(user_id => rows.indexOf(user_id)) == -1)
@@ -501,72 +654,123 @@ app.put('/project/:pid(\\d+)/meeting/:mid(\\d+)/participants', (req, res, next)
.prepare(`insert into meeting_users (meeting_id, user_id) select :meeting_id, value from json_each(:json_ids)`)
.run(res.locals)
res.status(200).json({success: true})
res.status(200).json({ success: true, data: user_ids })
})
// DOCUMENTS
app.get('/project/:pid(\\d+)/document', (req, res, next) => {
const ids = String(req.query.id).split(',').map(e => parseInt(e)).filter(e => e > 0)
const where = ids.length > 0 ? ' and id in (' + ids.join(', ') + ')' : ''
// Документы
// FILES
app.get('/project/:pid(\\d+)/file', (req, res, next) => {
// 1. Из групп, которые есть в проекте и в которых участвует пользователь
// 2. Из задач проекта, где пользователь автор, ответсвенный или наблюдатель
// 3. Из встреч на проекте, где пользователь создатель или участник
// To-Do: отдавать готовую ссылку --> как минимум GROUP_ID надо заменить на tgGroupId
const rows = db
.prepare(`
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_chat_id in (select chat_id from chat_users where user_id = :user_id)
or
parent_type = 1 and parent_id in (
select f.id, f.chat_id, c.telegram_id telegram_chat_id, f.message_id, f.filename, f.mime, f.caption, f.size, f.published_by, f.published, f.parent_id, f.parent_type
from files f
left join chats c on f.chat_id = c.id and f.parent_type = 0
where f.project_id = :project_id and (
chat_id in (select chat_id from chat_users where user_id = :user_id)
or
parent_type = 1 and parent_id in (
select id
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))
)
or
parent_type = 2 and parent_id in (
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))
)
or
parent_type = 2 and parent_id in (
select id
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))
)
)
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))
)
)
`)
.all(res.locals)
.safeIntegers(true)
.all(res.locals)
if (where && rows.length == 0)
throw Error('NOT_FOUND::404')
const upload_ids = db
.prepare(`select upload_chat_id from customers where upload_chat_id is not null`)
.pluck(true)
.all()
.reduce((res, e) => { res[e] = true; return res}, {})
res.status(200).json({success: true, data: ids.length == 1 ? rows[0] : rows})
rows
.filter (row => upload_ids[row.chat_id])
.forEach(row => {
row.chat_id = null
row.message_id = null
})
res.status(200).json({success: true, data: rows})
})
app.post('/project/:pid(\\d+)/:type(task|meeting)/:id(\\d+)/attach', upload.any(), async (req, res, next) => {
const parentType = req.params.type == 'task' ? 1 : 2
const parentId = req.params.id
try {
res.locals.parent_id = req.params.id
res.locals.parent_type = req.params.type == 'task' ? 1 : 2
const ids = []
for (const file of req.files) {
const id = await bot.uploadDocument(res.locals.project_id, file.originalname, file.mimetype, file.buffer, parentType, parentId, res.locals.user_id)
ids.push(id)
const chat_id = db
.prepare(`
select coalesce(chat_id, (select upload_chat_id from customers where id = :customer_id))
from ${req.params.type}s
where id = :parent_id and project_id = :project_id`)
.pluck(true)
.get(res.locals)
if (!chat_id)
throw Error('EMPTY_DESTINATION::500')
const file_ids = []
for (const file of req.files) {
if (file.size == 0)
continue
const filedata = {
project_id: req.params.pid,
chat_id,
filename: file.originalname,
mime: file.mimetype,
data: file.buffer,
size: file.size,
published_by: res.locals.user_id,
pablished: Math.floor(Date.now() / 1000),
parent_type: res.locals.parent_type,
parent_id: req.params.id
}
try {
const file_id = await bot.sendFile(filedata)
if (file_id)
file_ids.push(file_id)
} catch (err) {
log.error(err)
}
}
if (file_ids.length == 0)
throw Error('EMPTY_UPLOAD::500')
const files = db
.prepare(`select id, chat_id, message_id, filename, mime, size, published_by, published from files where id in (` + file_ids.join(',') + `)`)
.all()
res.status(200).json({success: true, data: files})
} catch (err) {
next(err)
}
res.redirect(req.baseUrl + `/project/${req.params.pid}/document?id=` + ids.join(','))
})
app.use('/project/:pid(\\d+)/document/:did(\\d+)', (req, res, next) => {
res.locals.document_id = req.params.did
app.use('/project/:pid(\\d+)/file/:fid(\\d+)', (req, res, next) => {
res.locals.file_id = req.params.fid
const doc = db
.prepare(`select * from documents where id = :document_id and project_id = :project_id`)
const file = db
.prepare(`select * from files where id = :file_id and project_id = :project_id`)
.get(res.locals)
if (!doc)
if (!file)
throw Error('NOT_FOUND::404')
if (doc.parent_type == 0) {
res.locals.chat_id = doc.chat_id
if (file.parent_type == 0) {
res.locals.chat_id = file.chat_id
const row = db
.prepare(`select 1 from chat_users where chat_id = :chat_id and user_id = :user_id`)
.get(res.locals)
@@ -575,8 +779,8 @@ app.use('/project/:pid(\\d+)/document/:did(\\d+)', (req, res, next) => {
res.locals.can_download = true
}
} else {
res.locals.parent_id = doc.parent_id
const parent = doc.parent_type == 1 ? 'task' : 'meeting'
res.locals.parent_id = file.parent_id
const parent = file.parent_type == 1 ? 'task' : 'meeting'
const row = db
.prepare(`
@@ -589,36 +793,43 @@ app.use('/project/:pid(\\d+)/document/:did(\\d+)', (req, res, next) => {
if (row) {
res.locals.can_download = true
res.locals.can_delete = doc.published_by == res.locals.user_id
res.locals.can_delete = file.published_by == res.locals.user_id
}
}
next()
})
app.get('/project/:pid(\\d+)/document/:did(\\d+)', async (req, res, next) => {
if (!res.locals.can_download)
throw Error('NOT_FOUND::404')
app.get('/project/:pid(\\d+)/file/:fid(\\d+)', async (req, res, next) => {
try {
if (!res.locals.can_download)
throw Error('NOT_FOUND::404')
const file = await bot.downloadDocument(res.locals.project_id, res.locals.document_id)
res.writeHead(200, {
'Content-Length': file.size,
'Content-Type': file.mime,
'Content-Disposition': contentDisposition(file.filename)
})
const file = await bot.downloadFile(res.locals.project_id, res.locals.file_id)
res.writeHead(200, {
'Content-Length': file.size,
'Content-Type': file.mime,
'Content-Disposition': contentDisposition(file.filename)
})
res.end(file.data)
res.end(file.data)
} catch (err) {
next(err)
}
})
app.delete('/project/:pid(\\d+)/document/:id(\\d+)', (req, res, next) => {
app.delete('/project/:pid(\\d+)/file/:fid(\\d+)', (req, res, next) => {
if (!res.locals.can_delete)
throw Error('NOT_FOUND::404')
const doc = db
.prepare(`delete from documents where id = :id and project_id = :project_id`)
const info = db
.prepare(`delete from files where id = :id and project_id = :project_id`)
.run(res.locals)
res.status(200).json({success: true})
if (info.changes == 0)
throw Error('NOT_FOUND::404')
res.status(200).json({success: true, data: {id: res.locals.file_id}})
})
app.get('/settings', (req, res, next) => {
@@ -640,4 +851,28 @@ app.put('/settings', (req, res, next) => {
res.status(200).json({success: true})
})
module.exports = app
/*
eventBus.on('data', evt => {
if (evt.)
const msg = {
event: evt.event,
entity: evt.entity,
id: evt.id,
source: 'miniapp'
}
const users = {}
if (evt.entity == 'project') {
}
if (evt.entity == 'task') {
msg.data = getTask(evt.id, null)
}
if (evt.entity == 'meeting') {
msg.data = getMeeting(evt.id, null)
}
})
*/
module.exports = { router: app, registerWS }

Binary file not shown.

BIN
backend/data/db.sqlite-shm Normal file

Binary file not shown.

BIN
backend/data/db.sqlite-wal Normal file

Binary file not shown.

View File

@@ -23,6 +23,7 @@ create table if not exists projects (
description text check(description is null or length(description) < 4096),
logo text,
is_logo_bg integer default 0,
company_id integer references companies(id) on delete cascade,
is_archived integer default 0
) strict;
@@ -31,9 +32,13 @@ create table if not exists chats (
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,
invite_link text,
bot_can_ban integer default 0,
owner_id integer references users(id) on delete set null,
user_count integer,
last_update_time integer
);
@@ -43,14 +48,12 @@ 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);
@@ -58,73 +61,89 @@ 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;
create table if not exists tasks (
id integer primary key autoincrement,
project_id integer references projects(id) on delete cascade,
name text not null check(trim(name) <> '' and length(name) < 4096),
project_id integer references projects(id) on delete cascade,
name text not null check(trim(name) <> '' and length(name) < 256),
description text not null check(trim(description) <> '' and length(description) < 4096),
created_by integer references users(id) on delete set null,
assigned_to integer references users(id) on delete set null,
closed_by integer references users(id) on delete set null,
priority integer check(priority in (0, 1, 2, 3, 4, 5)) default 0,
status integer check(status >= 1 and status <= 10) default 1,
time_spent integer check(time_spent is null or time_spent > 0 and time_spent < 44640), -- one month
time_spent integer check(time_spent is null or time_spent > 0 and time_spent < 3 * 44640), -- one month
create_date integer,
plan_date integer,
close_date integer
close_date integer,
close_comment text check(close_comment is null or length(close_comment) < 1024),
json_close_files text,
chat_id integer references chats(id) on delete set null,
message_id integer
) strict;
create index if not exists idx_tasks_project_id on tasks (project_id);
create table if not exists meetings (
id integer primary key autoincrement,
project_id integer references projects(id) on delete cascade,
name text not null check(trim(name) <> '' and length(name) < 4096),
description text check(description is null or length(description) < 4096),
name text not null check(trim(name) <> '' and length(name) < 256),
description text check(description is null or length(description) < 1024),
place text check(place is null or length(place) < 4096),
created_by integer references users(id) on delete set null,
meet_date integer
meet_date integer,
duration integer,
chat_id integer references chats(id) on delete set null,
message_id integer,
is_cancel integer default 0
) strict;
create index if not exists idx_meetings_project_id on meetings (project_id);
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_chat_id integer references chats(id) on delete set null,
origin_message_id integer,
chat_id integer references chats(id) on delete set null,
message_id integer,
file_id integer,
access_hash integer,
filename text not null check(length(filename) < 256),
mime text check(mime is null or length(mime) < 128),
caption text check(caption is null or length(caption) < 4096),
size integer,
caption text check(caption is null or length(caption) < 2048),
published_by integer references users(id) on delete set null,
parent_type integer check(parent_type in (0, 1, 2)) default 0,
parent_id integer,
backup_state integer default 0
published integer,
parent_type integer check(parent_type in (0, 1, 2, 3)) default 0,
parent_id integer
) strict;
create index if not exists idx_files_project_id on files (project_id);
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),
description text check(description is null or length(description) < 4096),
logo text
) strict;
create index if not exists idx_companies_project_id on companies (project_id);
create table if not exists company_mappings (
project_id integer references projects(id) on delete cascade,
company_id integer references companies(id) on delete cascade,
show_as_id integer references companies(id) on delete cascade,
--show_as_id integer references companies(id) on delete cascade,
show_to_id integer references companies(id) on delete cascade
) strict;
create index if not exists idx_company_mappings_project_id on company_mappings (project_id);
create table if not exists task_users (
task_id integer references tasks(id) on delete cascade,
@@ -153,8 +172,7 @@ create table if not exists chat_users (
pragma foreign_keys = on;
create trigger if not exists trg_chats_update after update
on chats
create trigger if not exists trg_chats_update after update on chats
when NEW.project_id is null
begin
delete from chat_users where chat_id = NEW.id;

BIN
backend/data/log.sqlite Normal file

Binary file not shown.

Binary file not shown.

View File

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

View File

@@ -1,93 +0,0 @@
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 поначалу отображали только задачи, связанные с пользователем, потом они добавили крыж "Показывать все задачи" по просьбам пользователей. Почему пользователям это понадобилось? Возможно потому что у них нет механизма наблюдателей.
Возможно нужно для контроля начальником => отдельное приложение
-----------------------------------------------------
Предположим, что бот был в группе, а потом его удалили из нее.
Должна ли группа отображаться в списке групп проекта?
-----------------------------------------------------

View File

@@ -1,69 +0,0 @@
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

Binary file not shown.

View File

@@ -4,38 +4,15 @@ const sqlite3 = require('better-sqlite3')
const db = sqlite3(`./data/db.sqlite`)
db.pragma('journal_mode = WAL')
db.pragma('foreign_keys = on')
db.exec(fs.readFileSync('./data/init.sql', 'utf8'))
/*
db.exec(`attach database './data/backup.sqlite' as backup`)
db.function('backup', (tblname, ...values) => {
db
.prepare(`insert into backup.${tblname} select ` + values.map(e => '?').join(', '))
.run(values)
})
const backupQuery = db
.prepare(`
select group_concat(tbl || char(13) || trg, char(13) || char(13)) from (
select 'create table if not exists backup.' || t.name || ' (' || group_concat(c.name || ' ' || c.type, ', ') || ', time integer);' tbl,
'create trigger if not exists trg_' || t.name || '_delete after delete on ' || t.name || ' for each row begin ' ||
' select backup (' || t.name || ',' || group_concat('OLD.' || c.name, ', ') || ', strftime(''%s'', ''now'')); end;' trg
from sqlite_master t left join pragma_table_xinfo c on t.tbl_name = c.arg and c.schema = 'main'
where t.sql is not null and t.type = 'table' and t.name <> 'sqlite_sequence'
group by t.type, t.name order by t.type, t.name)
`)
.pluck(true)
.get()
db.exec(backupQuery)
*/
db.function('generate_key', (id, time) => {
return [
'KEY',
Buffer.from(time + '').toString('base64'),
crypto.createHash('md5').update(`sa${time}-${id}lt`).digest('hex')
crypto.createHash('md5').update(`sa${time}-${id}lty`).digest('hex')
].join('-')
})
@@ -54,4 +31,16 @@ db.prepareUpdate = function (table, columns, data, where) {
` where ` + where.map(col => `"${col}" = :${col}`).join(' and '))
}
db.prepareUpsert = function (table, columns, data, where) {
const dataColumns = columns.filter(col => col in data)
if (dataColumns.length == 0)
throw Error('SQLite Error: No data to update')
return db.prepare(
`insert into "${table}" (` + [...dataColumns, ...where].join(', ') + `) values (:` + [...dataColumns, ...where].join(', :') +
`) on conflict (` + where.join(',') + `) do update ` +
`set ` + dataColumns.map(col => `"${col}" = :${col}`).join(', ') +
` where ` + where.map(col => `"${col}" = :${col}`).join(' and '))
}
module.exports = db

View File

@@ -0,0 +1,5 @@
const EventEmitter = require('events')
class EventBus extends EventEmitter {}
const eventBus = new EventBus()
module.exports = eventBus

58
backend/include/i18n.js Normal file
View File

@@ -0,0 +1,58 @@
const i18n = {
en: {
ON_PROJECT_ARCHIVE: 'The project was moved to an rachive. Message tracking has been discontinued.',
ON_PROJECT_RESTORE: 'The project has been recovered from the archives.',
ON_CHAT_REMOVE: 'The chat was removed from a project.',
CHAT_IN_USE: 'Chat is already in use',
EXPIRED_KEY: 'The key validity time for binding has expired',
OPEN_PROJECT: 'Open project',
WELCOME: 'Welcome, strange',
TASK_MESSAGE: ':clipboard: New task ${PRIORITY}\n${NAME}\n[${CREATOR}](tg://user?id=${CREATOR_ID}) > [${ASSIGNEE}](tg://user?id=${ASSIGNEE_ID})\nPlan date: ${PLAN_DATE}\n[Open a task page](${URL})',
TASK_PRIORITY_0: '',
TASK_PRIORITY_1: '',
TASK_PRIORITY_2: '(Important)',
TASK_PRIORITY_3: '(Critical)',
MEETING_MESSAGE: 'Meeting ${name}',
ADMIN_APP: 'Administrator Access',
USER_APP: 'User Access'
},
ru: {
ON_PROJECT_ARCHIVE: 'Проект помещен в архив. Отслеживание сообщений прекращено.',
ON_PROJECT_RESTORE: 'Проект восстановлен из архива.',
ON_CHAT_REMOVE: 'Чат удален из проекта.',
CHAT_IN_USE: 'Чат уже используется',
EXPIRED_KEY: 'Время действия ключа для привязки истекло',
OPEN_PROJECT: 'Открыть проект',
WELCOME: 'Добро пожаловать',
TASK_MESSAGE: '📋 Новая задача ${PRIORITY}\n${NAME}\n<a href="tg://user?id=${CREATOR_ID}">${CREATOR}</a> > <a href="tg://user?id=${ASSIGNEE_ID}">${ASSIGNEE}</a>\nСрок: ${PLAN_DATE}\n<a href="${URL}">Перейти на страницу задачи</a>',
TASK_PRIORITY_0: '',
TASK_PRIORITY_1: '',
TASK_PRIORITY_2: '(Важно)',
TASK_PRIORITY_3: '(Критично)',
MEETING_MESSAGE: 'Встреча ${name}',
ADMIN_APP: 'Режим администратора',
USER_APP: 'Режим пользователя'
}
}
const hasLocale = (locale) => !!i18n[locale]
function getString (locale, code, args = {}) {
const params = {}
Object.keys(args instanceof Object ? args : {}).forEach(key => {
const value = args[key]
params[key] = value[0] == '$' ? getString(locale, value.substring(1)) : value
})
let res = i18n[locale] ? i18n[locale][code] : undefined
if (res === undefined)
res = i18n.en[code]
if (res === undefined)
res = code
if (res === undefined)
res = 'ERROR'
res = res.replace(/\${(\w+)}/g, (match, key) => params.hasOwnProperty(key) ? params[key] : match)
return res
}
module.exports = { hasLocale, getString }

90
backend/include/log.js Normal file
View File

@@ -0,0 +1,90 @@
const sqlite3 = require('better-sqlite3')
const db = sqlite3(`./data/log.sqlite`)
db.pragma('journal_mode = DELETE')
db.pragma('synchronous = 0')
db.exec(`
create table if not exists http (time integer default (strftime('%s','now')), method text, url text, headers text, body text, status text, duration integer);
create table if not exists mtproto (time integer default (strftime('%s','now')), message text);
create table if not exists error (time integer default (strftime('%s','now')), message text, stack text);
`)
function http (req, res, duration) {
const data = {
method: req.method,
url: req.url,
status: res.statusCode,
duration
};
['headers', 'body'].forEach(key => {
const value = req.body[key]
try {
data[key] = value instanceof Array && value.length < 1_000_000 || value instanceof Object ? JSON.stringify(value) : value?.toString() || null
} catch (err) {
error(err)
data[key] = value?.toString() || null
}
})
db
.prepare(`insert into http (method, url, headers, body, status, duration) values (:method, :url, :headers, :body, :status, :duration)`)
.run(data)
}
function safeStringify(obj, indent = 2) {
const cache = new Set()
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (cache.has(value))
return '[Circular]'
cache.add(value)
}
return value
}, indent)
}
function mtproto (update) {
const message = update
message.self = message
db
.prepare(`insert into mtproto (message) values (:message)`)
.run({
message: safeStringify(message)
})
}
function error (...args) {
const stacks = []
const message = args.map(arg => {
if (arg instanceof Error) {
stacks.push(arg.stack)
return 'Error: ' + arg.message
} else if (arg instanceof Object) {
try {
return JSON.stringify(arg)
} catch (err) { }
}
return String(arg)
}).join('\n')
if (stacks.length == 0)
stacks.push(new Error().stack)
console.error(message)
db
.prepare(`insert into error (message, stack) values (:message, :stack)`)
.run({
message,
stack: stacks.join('\n\n')
})
}
module.exports = { http, mtproto, error }

View File

@@ -12,12 +12,14 @@
"better-sqlite3": "^11.8.0",
"body-parser": "^1.20.3",
"content-disposition": "^0.5.4",
"cookie": "^1.0.2",
"cookie-parser": "^1.4.7",
"express": "^4.21.2",
"express-session": "^1.18.1",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.16",
"telegram": "^2.26.16"
"telegram": "^2.26.16",
"ws": "^8.18.2"
}
},
"node_modules/@cryptography/aes": {
@@ -81,9 +83,9 @@
"license": "MIT"
},
"node_modules/better-sqlite3": {
"version": "11.9.1",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.9.1.tgz",
"integrity": "sha512-Ba0KR+Fzxh2jDRhdg6TSH0SJGzb8C0aBY4hR8w8madIdIzzC6Y1+kx5qR6eS1Z+Gy20h6ZU28aeyg0z1VIrShQ==",
"version": "11.10.0",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
"integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -293,12 +295,12 @@
}
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
"node": ">=18"
}
},
"node_modules/cookie-parser": {
@@ -314,6 +316,15 @@
"node": ">= 0.8.0"
}
},
"node_modules/cookie-parser/node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
@@ -485,9 +496,9 @@
}
},
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
@@ -686,6 +697,15 @@
"node": ">= 0.8.0"
}
},
"node_modules/express-session/node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express-session/node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
@@ -1098,6 +1118,7 @@
"version": "1.4.5-lts.2",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz",
"integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==",
"deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
@@ -1301,9 +1322,9 @@
}
},
"node_modules/pump": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
@@ -1395,9 +1416,9 @@
"license": "MIT"
},
"node_modules/real-cancellable-promise": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/real-cancellable-promise/-/real-cancellable-promise-1.2.1.tgz",
"integrity": "sha512-JwhiWJTMMyzFYfpKsiSb8CyQktCi1MZ8ZBn3wXvq28qXDh8Y5dM7RYzgW3r6SV22JTEcof8pRsvDp4GxLmGIxg==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/real-cancellable-promise/-/real-cancellable-promise-1.2.2.tgz",
"integrity": "sha512-Qh1RvIGdekUCv/ZkK9IiAkah2/Q++p66KHe6TSgHnx4QSbr5vCo3qDoszqRO1TSH+6h6HI5aDVBVrQCQBGj44Q==",
"license": "MIT"
},
"node_modules/safe-buffer": {
@@ -1427,9 +1448,9 @@
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -1635,9 +1656,9 @@
}
},
"node_modules/socks": {
"version": "2.8.4",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz",
"integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==",
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz",
"integrity": "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww==",
"license": "MIT",
"dependencies": {
"ip-address": "^9.0.5",
@@ -1702,9 +1723,9 @@
}
},
"node_modules/tar-fs": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz",
"integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
"integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==",
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
@@ -1967,6 +1988,27 @@
"slide": "^1.1.5"
}
},
"node_modules/ws": {
"version": "8.18.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@@ -15,11 +15,13 @@
"better-sqlite3": "^11.8.0",
"body-parser": "^1.20.3",
"content-disposition": "^0.5.4",
"cookie": "^1.0.2",
"cookie-parser": "^1.4.7",
"express": "^4.21.2",
"express-session": "^1.18.1",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.16",
"telegram": "^2.26.16"
"telegram": "^2.26.16",
"ws": "^8.18.2"
}
}

Binary file not shown.

View File

@@ -0,0 +1,47 @@
[sqlite-x]
font-size =16
header-row = 1 ; 0/1
filter-row =0
dark-theme =0
; Below params can be changed only in the ini file
font = Arial ;
disable-grid-lines = 1 ; 0/1, Disable/Enable grid lines
max-column-width = 300 ;
cache-size = 2000 ; Cached rows
delete-journal = 0 ; 0 - keep, 1 - ask, 2 - delete
filter-align = 0 ; -1 - left, 0 - center, 1 - right
copy-column = 0 ; Ctrl + C: 0 - copy a current cell, 1 - copy all cells from selected rows/current column
; column-delimiter = ; Used when copy rows. Should be one char. Default is TAB
editable = 1 ; Allow to edit data in a cell by F2 or Space
exit-by-escape = 1 ;
open-last-db = 1 ; Open the last database on app start
recent=F:\project\projectsNode\backend\data\db.sqlite?
; Colors
; Light theme
text-color = 0 ; RGB(0, 0, 0)
back-color = 16777215 ; RGB(255, 255, 255)
back-color2 = 15790320 ; RGB(240, 240, 240)
filter-text-color = 0 ; RGB(0, 0, 0)
filter-back-color = 15790320 ; RGB(240, 240, 240)
selection-text-color = 16777215 ; RGB(255, 255, 255)
selection-back-color = 6956042 ; RGB(10, 36, 106)
current-cell-back-color = 10903622 ; RGB(70, 96, 166)
; splitter-color = <as button>
; Dark theme
text-color-dark = 14474460 ; RGB(220, 220, 220)
back-color-dark = 2105376 ; RGB(32, 32, 32)
back-color2-dark = 3421236 ; RGB(52, 52, 52)
filter-text-color-dark = 16777215 ; RGB(255, 255, 255)
filter-back-color-dark = 3947580 ; RGB(60, 60, 60)
selection-text-color-dark = 14474460 ; RGB(220, 220, 220)
selection-back-color-dark = 6710856 ; RGB(72, 102, 102)
current-cell-back-color-dark = 4079136 ; RGB(32, 62, 62)
splitter-position=200
position-x=93
position-y=151
width=1686
height=600
; splitter-color = <as button>