Compare commits

...

127 Commits
main ... tgbot

Author SHA1 Message Date
2c53b07c99 added restincs for video size 2024-09-13 14:20:44 +03:00
1af59253bf added render group msg 2024-09-13 11:12:34 +03:00
bfe21bf0c5 added logic for edit msg pic 2024-09-13 10:03:01 +03:00
68499826c9 added logic for edit msg pic 2024-09-13 10:02:17 +03:00
2635dcfffb added edit_message_media request 2024-09-12 14:33:34 +03:00
94d7926fd7 added edit_message_media request 2024-09-12 14:33:26 +03:00
aaa19c0cea -- 2024-09-12 16:38:43 +03:00
fc0edd3f5e added to tg handle render s3 video 2024-09-12 15:48:41 +03:00
2d1ebf73e1 - 2024-09-12 15:35:00 +03:00
a5eedacf90 - 2024-09-12 15:34:21 +03:00
3a2b168c0e added array for img render 2024-09-12 15:29:15 +03:00
c0c2cb3706 update tg handle using vgram methods 2024-09-12 15:11:21 +03:00
bc6699bba0 upd render video and photo 2024-09-12 14:58:52 +03:00
473c85dddc added render photo from s3 2024-09-12 11:47:18 +03:00
24cfc562f7 added render video from s3 2024-09-12 13:50:30 +03:00
25c8c76ed7 added comments for supporting code 2024-09-11 18:09:39 +03:00
26d8f69c29 added comments for supporting code 2024-09-11 18:09:23 +03:00
887f064fe7 added render start page 2024-09-11 12:46:27 +03:00
b1bfa1cb02 added render start page 2024-09-11 12:43:39 +03:00
af1c778c08 added render start page 2024-09-11 12:42:32 +03:00
c685a60116 added render start page 2024-09-11 12:36:10 +03:00
0958c32f97 added render start page 2024-09-11 12:34:49 +03:00
9d64d6d692 added render date type question, only input time parse 2024-09-10 22:55:30 +03:00
66b99938cc render buttons in matrix 2024-09-10 16:29:36 +03:00
7ee8a4bdf6 aded another logic 2024-09-10 15:18:22 +03:00
06070952a2 aded another logic 2024-09-10 15:14:50 +03:00
bc1c5c3193 added file to for for que tipe file 2024-09-10 12:17:30 +03:00
d03621702b added file to for for que tipe file 2024-09-10 12:17:18 +03:00
17ca27163e added send to answerer if dont file type, for file and date need work 2024-09-09 21:49:09 +03:00
1ef8a3af77 - 2024-09-09 18:05:44 +03:00
22d5d31e54 added meat and bones for send to put answer respondent answers, need todo calback and another 2024-09-09 18:02:45 +03:00
243e7968a7 add fn for send request for put answers 2024-09-09 15:22:36 +03:00
c3d637dc05 added method for send request to answerer-get quiz data 2024-09-09 14:12:40 +03:00
8c17881fa2 finish logic result type 2024-09-08 20:57:23 +03:00
d8ef717028 - 2024-09-08 13:29:05 +03:00
580d5b975a try search peviously respondent serch 2024-09-08 13:28:38 +03:00
62cc64b1df added methods for new table 2024-09-06 18:24:49 +03:00
d52e972df9 add logic for save quiz settings 2024-09-06 14:29:19 +03:00
a1a88d3eed added rander file question type 2024-09-06 11:31:28 +03:00
47b043136f added rander file question type 2024-09-06 11:29:54 +03:00
562212e6ca added rander file question type 2024-09-06 11:29:36 +03:00
0331e4406e added range and validate number question type 2024-09-05 18:06:49 +03:00
1423c78af8 added range and validate number question type 2024-09-05 18:06:39 +03:00
18fab9828e add logic for render and send text type question 2024-09-05 17:29:13 +03:00
cf1518cfef update render page type 2024-09-05 11:15:18 +03:00
934c3faae1 added render page 2024-09-04 17:38:38 +03:00
e68e652538 - 2024-09-04 14:22:10 +03:00
ba8d50c145 added logix for edit varimg img 2024-09-04 13:45:32 +03:00
52b4e251f4 callback_data <= 64 bytes 2024-09-04 11:38:22 +03:00
874f4777b5 callback_data <= 64 bytes 2024-09-04 11:37:08 +03:00
af52470196 added render varimg need edit 2024-09-03 17:03:17 +03:00
82f1d3e96d added in callback data que type 2024-09-03 15:56:43 +03:00
efd2997b8f added render rating 2024-09-03 13:57:36 +03:00
fe093e124e added to callback data struct with need values 2024-09-03 11:48:14 +03:00
8d20ba8bd4 change after test need work 2024-09-02 18:20:27 +03:00
35c55ca337 added logic for indexing linear or no questions 2024-09-02 17:11:09 +03:00
715fbc622e added bones and meat for tg handle question types 2024-08-30 17:47:43 +03:00
aaf330f1b3 added todo 2024-08-29 17:38:28 +03:00
94bdd8f0cd added models for question content 2024-08-29 15:20:54 +03:00
9f627ac60d added models for question content 2024-08-29 15:20:40 +03:00
3cf0c377ac added generate signature function 2024-08-28 16:00:34 +03:00
4bbeaf905e added method for get state from db 2024-08-27 14:35:31 +03:00
c8a78aed34 added question type enum 2024-08-27 13:30:02 +03:00
781d83935d update wc deley 2024-08-27 11:48:39 +03:00
5a0f499338 added ticker and select case 2024-08-25 17:15:44 +03:00
8f9f437aed added prepare graceful shutdown 2024-08-25 14:58:27 +03:00
6fe4c25a74 added prepare graceful shutdown 2024-08-25 14:58:08 +03:00
5ef7156044 added update bot name in db 2024-08-25 13:09:25 +03:00
276aa67134 added manage bots and delete them 2024-08-25 13:00:55 +03:00
c4793bcfa9 added bot манагер 2024-08-23 18:07:18 +03:00
e4862eeef5 now add repo to entity worker not pg.DB 2024-08-23 16:50:22 +03:00
c174357896 added serching bot for current instance 2024-08-23 16:37:00 +03:00
6fee9a4943 added rework manage bots logic 2024-08-23 16:22:33 +03:00
f8650d27e9 manage bots 1'st try 2024-08-23 15:08:51 +03:00
4add7b4fee added lock tables, transactions, update checkout, delete expired 2024-08-23 12:54:15 +03:00
4b8d32bf88 remove type time.time 2024-08-23 12:01:36 +03:00
e30966bfb1 - 2024-08-22 20:01:24 +03:00
acfce9ce1c added some punkts 2024-08-22 19:03:53 +03:00
bbfc351f48 added prepare for worker 2024-08-22 17:14:00 +03:00
b23687df30 - 2024-08-20 18:44:49 +03:00
27c13f75e2 return account id 2024-08-20 16:14:42 +03:00
d9d01120c5 check auth in handlers 2024-08-20 15:20:06 +03:00
ba74f8b6db added middleware need jwt 2024-08-19 18:14:18 +03:00
68b8ea9327 added restrictions 2024-08-19 14:09:06 +03:00
03e7bc9419 added restrictions 2024-08-19 14:08:36 +03:00
489768bcdf added soft delete integration method 2024-08-19 12:40:37 +03:00
0db50cd19e added method for update bot token 2024-08-19 12:26:46 +03:00
3622e5fbce added method for create integration 2024-08-19 12:13:40 +03:00
7ff7b315b6 init one http controller full 2024-08-18 16:50:27 +03:00
c5353870f6 added init http server 2024-08-18 16:26:33 +03:00
9325e0adee delete go files 2024-08-18 14:19:13 +03:00
bdc2d1dfce finish repo methods, now return id inserted fields 2024-08-18 12:13:02 +03:00
60be28c2be update models 2024-08-18 11:37:44 +03:00
0264a58cb6 repository tests 2024-08-16 18:17:45 +03:00
11d46ada2a added another sql repo methods 2024-08-16 15:15:22 +03:00
e71a8b8ccf added orm structs 2024-08-16 12:43:07 +03:00
5512bfb0c4 added orm 2024-08-16 12:16:42 +03:00
d1e3e7a485 added skelet for work with pg 2024-08-15 18:54:02 +03:00
beaaf8ef08 added sql table structs 2024-08-15 17:24:06 +03:00
22776dd9e7 added good render message 2024-08-15 12:20:07 +03:00
65f43fd26c change user state 2024-08-14 17:51:08 +03:00
a180888b2f added logic for speking repondent with tg bot 2024-08-14 17:44:21 +03:00
af9aca41b4 added tg bot handle 2024-08-13 18:36:47 +03:00
5b079a5872 added some workflow tests 2024-08-12 17:59:29 +03:00
20f24be349 added test for run_benchmark func 2024-08-12 16:00:04 +03:00
36e6a0f986 - 2024-08-12 14:21:43 +03:00
6f0d84aa87 remember n if sent 2024-08-12 14:03:38 +03:00
a4d4cee6cd delete v benchmark code, added separate methods and our struct 2024-08-12 13:26:30 +03:00
f7be57f9dd moved the benchmark from v to our file, added a constructor to initialize our benchmark 2024-08-12 11:57:35 +03:00
57491651d2 added 2 test for predict_n 2024-08-11 17:51:51 +03:00
5ee7a22486 added bench file to our repo 2024-08-11 17:16:10 +03:00
cfd1bb66bd added init function for prepare dataset out bench in v code, but it anti pattern 2024-08-09 17:44:20 +03:00
6e368518c5 added init function for prepare dataset out bench 2024-08-09 17:15:43 +03:00
3e4851555d added fix start state and move seed to main file 2024-08-09 15:40:00 +03:00
5f73c5ab5a added our bench to main 2024-08-08 23:40:21 +03:00
e730068834 rework logic, parse fromm file 2024-08-08 22:57:58 +03:00
7a43213aa4 added simply readme for go2v 2024-08-05 15:10:55 +03:00
d9db1b406a commented sleep 2024-08-05 14:52:36 +03:00
2d295bb384 added test result at time for v code 2024-08-05 14:30:20 +03:00
20b6d055bb - 2024-08-02 16:35:37 +03:00
545366564f added bench and unsafe for strconv 2024-08-02 16:03:46 +03:00
aee5d92d8d added bench for go and save results 2024-08-02 14:26:08 +03:00
756272d064 remove contructirs from v 2024-08-02 14:19:49 +03:00
dfa0f2daff add waitgroup and thred call in function 2024-08-02 14:10:30 +03:00
124ee61e1d added worked code on v lang need check with documentation and do with corutines 2024-08-01 17:23:54 +03:00
55f710cbbb added base simply logic for quiz with simulation user choice 2024-07-30 14:29:28 +03:00
9fe660aa71 added base simply logic for quiz with simulation user choice 2024-07-30 14:28:59 +03:00
24 changed files with 10793 additions and 0 deletions

1
.gitignore vendored Normal file

@ -0,0 +1 @@
.idea/

@ -1,6 +1,25 @@
# telegram
`go2v` — утилита для транслирования кода на Go в V. В текущий момент доступна только старая версия утилиты.
Для установки `go2v`, нужно клонировать репозиторий в данный момент с ветки `old_implementation` и запустить команду `v run .` для компиляции файлов:
```sh
git clone https://github.com/vlang/go2v.git -b old_implementation
cd go2v
v run .
```
После установки утилиты, можно использовать go2v для трансляции Go-файлов в V:
```sh
v run . path/to/file.go
```
Транслированный файл будет создан в том же каталоге с именем v.
Может потребоваться проверка синтаксиса, так как транслирование может содержать ошибки.
Дополнительную информацию и документацию можно найти в репозитории [go2v на GitHub](https://github.com/vlang/go2v/tree/old_implementation).
## Getting started

60
bot_manager/manager.v Normal file

@ -0,0 +1,60 @@
module bot_manager
import repository
import tg_handle
import models
pub struct BotManager {
pub mut:
active_bots map[i64]&tg_handle.TgBot
repo repository.Repo
}
pub fn new_bot_manager(repo repository.Repo) BotManager {
return BotManager{
active_bots: map[i64]&tg_handle.TgBot{}
repo: repo
}
}
pub fn (mut bm BotManager) manage_bots(new_bots map[i64]models.TelegramIntegration) ! {
// перебираем переданную мапку, сраавниваем то что работает с тем что пришло, дублей быть не должно
for bot_id, bot_data in new_bots {
if bot_id !in bm.active_bots {
bm.start_bot(bot_data)!
}
}
}
fn (mut bm BotManager) start_bot(bot models.TelegramIntegration) ! {
// доп проверка ну а вдруг
if bot.id in bm.active_bots {
eprintln('already started: $bot.id')
return
}
tg_bot := tg_handle.new_tg_bot( bm.repo,bot)
bm.active_bots[bot.id] = tg_bot
println('started: $bot.id')
}
pub fn (mut bm BotManager)deleted_bot(bots []models.TelegramIntegration)!{
for bot in bots {
if bot.id in bm.active_bots {
handle := bm.active_bots[bot.id]or {
println('Error get bot handle for: $bot.id')
continue
}
handle.stop_ch <- 1
bm.active_bots.delete(bot.id)
println('stopped and deleted: $bot.id')
} else {
println('bot not found: $bot.id')
}
}
}
pub fn (mut bm BotManager) stop_all_bots() {
for _,bot in bm.active_bots{
bot.stop_ch <-1
}
}

60
client/answerer.v Normal file

@ -0,0 +1,60 @@
module client
import json
import net.http
pub const answerer_url = 'http://answerer_url'
struct GetQuizData {
quiz_id string
limit int
need_config bool
}
pub fn get_quiz_data(quiz_id string,session string) ! {
url := answerer_url+'/settings'
req_data := json.encode(GetQuizData{
quiz_id: quiz_id
limit: 10
need_config: false
})
mut req := http.new_request(.post, url, req_data)
req.add_custom_header('X-SessionKey', session)or{
println('failed create custom header: $err ')
}
response := req.do() or {
return error('Failed to create http get_quiz_data request: $err')
}
if response.status_code != 200 {
return error('Failed to get http quiz data: $response.status_code')
}
}
pub fn put_answers(session string, qid string,answers_json string, files map[string][]http.FileData) ! {
url := answerer_url+'/answer'
mut header := http.Header{}
header.add_custom('X-SessionKey', session)or{
println('failed create custom header: $err ')
}
conf := http.PostMultipartFormConfig{
header: header ,
form: {
'answers': answers_json
'qid' : qid
},
files: files
}
response := http.post_multipart_form(url, conf) or {
return error('Failed to send multipart form request: $err')
}
if response.status_code != 200 {
return error('Failed to get valid response: $response.status_code')
}
}

203
client/tg_files.v Normal file

@ -0,0 +1,203 @@
module client
import net.http
import json
pub struct DepsLoadSendVideo {
pub mut:
video_url string
bot_token string
caption string
reply_markup string
chat_id string
}
pub fn load_and_send_video(deps DepsLoadSendVideo) ! {
response_s3 := http.get(deps.video_url) or { return error('Failed to load video: $err') }
if response_s3.status_code != 200 {
return error('Failed to load video, status code: $response_s3.status_code')
}
max_size := 50 * 1024 * 1024 // 50 MB в байтах
if response_s3.body.len > max_size {
return error('Video file size exceeds 50 MB limit, url - ${deps.video_url}')
}
mut files := map[string][]http.FileData{}
file_to_upload := http.FileData{
filename: '${deps.video_url.split('/').last()}.mp4'
content_type: 'application/octet-stream'
data: response_s3.body
}
files['video']=[file_to_upload]
conf := http.PostMultipartFormConfig{
form: {
'chat_id':deps.chat_id
'caption':deps.caption
'reply_markup':deps.reply_markup
'parse_mode':'Markdown'
}
files: files
}
response_tg := http.post_multipart_form('https://api.telegram.org/bot${deps.bot_token}/sendVideo', conf) or {
return error('Failed to send multipart form request: $err')
}
if response_tg.status_code != 200 {
return error('Failed to get valid response: $response_tg.status_code, body: $response_tg.body')
}
}
pub struct DepsLoadSendPhoto {
pub mut:
photo_url string
bot_token string
caption string
reply_markup string
chat_id string
}
pub fn load_and_send_photo(deps DepsLoadSendPhoto) ! {
response_s3 := http.get(deps.photo_url) or { return error('Failed to load photo: $err') }
if response_s3.status_code != 200 {
return error('Failed to load photo, status code: $response_s3.status_code')
}
mut files := map[string][]http.FileData{}
file_to_upload := http.FileData{
filename: '${deps.photo_url.split('/').last()}.png'
content_type: 'application/octet-stream'
data: response_s3.body
}
files['photo']=[file_to_upload]
conf := http.PostMultipartFormConfig{
form: {
'chat_id':deps.chat_id
'caption':deps.caption
'reply_markup':deps.reply_markup
'parse_mode':'Markdown'
}
files: files
}
response_tg := http.post_multipart_form('https://api.telegram.org/bot${deps.bot_token}/sendPhoto', conf) or {
return error('Failed to send multipart form request: $err')
}
if response_tg.status_code != 200 {
return error('Failed to get valid response: $response_tg.status_code, body: $response_tg.body')
}
}
pub struct DepsEditMessageMedia {
pub mut:
media_url string
bot_token string
chat_id string
message_id string
caption string
reply_markup string
}
pub fn edit_message_media(deps DepsEditMessageMedia) ! {
response_s3 := http.get(deps.media_url) or { return error('Failed to load media: $err') }
if response_s3.status_code != 200 {
return error('Failed to load media, status code: $response_s3.status_code')
}
mut files := map[string][]http.FileData{}
file_to_upload := http.FileData{
filename: '${deps.media_url.split('/').last()}.png'
content_type: 'application/octet-stream'
data: response_s3.body
}
files['media'] = [file_to_upload]
media := json.encode({
'type': 'photo',
'media': 'attach://media',
'caption':deps.caption
})
conf := http.PostMultipartFormConfig{
form: {
'chat_id': deps.chat_id
'message_id': deps.message_id
'media': media
'reply_markup':deps.reply_markup
}
files: files
}
response_tg := http.post_multipart_form('https://api.telegram.org/bot${deps.bot_token}/editMessageMedia', conf) or {
return error('Failed to send multipart form request: $err')
}
if response_tg.status_code != 200 {
return error('Failed to get valid response: $response_tg.status_code, body: $response_tg.body')
}
}
pub struct DepsSendMediaGroup {
pub mut:
bot_token string
chat_id string
input_media []DepsInputMedia
}
pub struct DepsInputMedia {
pub mut:
media_url string
caption string
}
pub fn send_media_group(deps DepsSendMediaGroup) ! {
mut files := map[string][]http.FileData{}
mut arr_input_media := []map[string]string{}
for input_media in deps.input_media {
response_s3 := http.get(input_media.media_url) or {
return error('Failed to load media: $err')
}
if response_s3.status_code != 200 {
return error('Failed to load media, status code: $response_s3.status_code')
}
file_to_upload := http.FileData{
filename: '${input_media.media_url.split('/').last()}.png'
content_type: 'application/octet-stream'
data: response_s3.body
}
files[input_media.media_url.split('/').last()] = [file_to_upload]
media := {
'type': 'photo'
'media': 'attach://$input_media.media_url.split('/').last()'
'caption': input_media.caption
}
arr_input_media << media
}
media_json := json.encode(arr_input_media)
conf := http.PostMultipartFormConfig{
form: {
'chat_id': deps.chat_id
'media': media_json
}
files: files
}
response_tg := http.post_multipart_form('https://api.telegram.org/bot${deps.bot_token}/sendMediaGroup', conf) or {
return error('Failed to send multipart form request: $err')
}
if response_tg.status_code != 200 {
return error('Failed to send media group, status code: $response_tg.status_code, body: $response_tg.body')
}
}

96
controllers/integration.v Normal file

@ -0,0 +1,96 @@
module controllers
import repository
import veb
import models
import json
import utils
pub struct IntegrationControllers {
pub mut:
repo repository.Repo
}
@['/:quizID'; get]
fn (mut c IntegrationControllers) get(mut ctx veb.Context,quizID i64) veb.Result {
account_id := utils.jwt_auth(mut ctx)
if account_id == ""{
return ctx.request_error("empty jwt")
}
integration := c.repo.get_integration_by_id(quizID) or {
return ctx.server_error('Error getting integration: $err')
}
return ctx.json(integration)
}
@['/:quizID'; post]
fn (mut c IntegrationControllers) create(mut ctx veb.Context,quizID i64) veb.Result {
account_id := utils.jwt_auth(mut ctx)
if account_id==""{
return ctx.request_error("empty jwt")
}
req := json.decode(models.TelegramIntegrationCreateReq,ctx.req.data)or {
return ctx.request_error('Failed parse request body: $err')
}
mut integration := models.TelegramIntegration{
account_id: account_id
quiz_id: quizID
bot_token: req.bot_token
repeatable: false
bot_name: ''
deleted: false
status: models.active_status
}
if integration.account_id == "" || integration.bot_token == "" || integration.quiz_id==0{
return ctx.request_error('Missing required fields')
}
id := c.repo.create_integration(integration) or {
return ctx.server_error('Failed create integration: $err')
}
integration.id = id
return ctx.json(integration)
}
@['/:quizID'; patch]
fn (mut c IntegrationControllers) update(mut ctx veb.Context,quizID int) veb.Result {
account_id := utils.jwt_auth(mut ctx)
if account_id == ""{
return ctx.request_error("empty jwt")
}
req := json.decode(models.TelegramIntegrationUpdateReq,ctx.req.data)or {
return ctx.request_error('Failed parse request body: $err')
}
if req.id== 0 || req.bot_token == ""{
return ctx.request_error('Missing required fields')
}
c.repo.update_bot_token(req.id,req.bot_token) or {
return ctx.server_error('Failed update integration: $err')
}
return ctx.ok("OK")
}
@['/:quizID'; delete]
fn (mut c IntegrationControllers) delete(mut ctx veb.Context,quizID i64) veb.Result {
account_id := utils.jwt_auth(mut ctx)
if account_id==""{
return ctx.request_error("empty jwt")
}
if quizID == 0{
return ctx.request_error('quizID dont be 0')
}
c.repo.soft_delete_integration(quizID)or {
return ctx.server_error('Failed soft delete integration: $err')
}
return ctx.ok("OK")
}

300
instance_worker/worker.v Normal file

@ -0,0 +1,300 @@
module instance_worker
import time
import models
import dariotarantini.vgram
import repository
import bot_manager
import tools
pub struct InstanceWorker {
pub mut:
instance_id int
tg_sender vgram.Bot
repo repository.Repo
bot_manager bot_manager.BotManager
stop_ch chan int
}
pub fn (mut worker InstanceWorker) start() {
mut max_checkout := worker.get_max_checkout()or {
eprintln(err)
return
}
now := time.now().unix()
if now>max_checkout{
d := now - max_checkout
if d >=10{
max_checkout = now
}else{
max_checkout = now+10
}
}else{
max_checkout +=10
}
max_checkout += 5*60
new_instance := models.TelegramIntegrationInstance{
checkout: max_checkout
limit: models.bot_limit
amount: 0
}
worker.register_instance(new_instance) or {
eprintln(err)
return
}
mut count := 0
mut ticker := tools.new_ticker(models.check_interval)
defer {
ticker.stop()
}
wait_time := max_checkout - time.now().unix()
if wait_time > 0 {
time.sleep(wait_time * time.second)
}
for {
select {
_ := <-ticker.c{
// начинаем транзакцию
worker.repo.pg_db.exec('BEGIN') or { panic(err) }
// блокируем таблицы
worker.repo.pg_db.exec('LOCK TABLE telegram_integration IN ACCESS EXCLUSIVE MODE') or { panic(err) }
worker.repo.pg_db.exec('LOCK TABLE telegram_integration_instance IN ACCESS EXCLUSIVE MODE') or { panic(err) }
// обновляем только тогда когда прошла хотябы 1 итерация
if count>0{
worker.update_instance_checkout() or {
eprintln(err)
worker.repo.pg_db.exec('ROLLBACK') or { panic(err)}
return
}
}
// ищем и удаляем устаревшие инстансы
worker.remove_stale_instances() or {
eprintln(err)
worker.repo.pg_db.exec('ROLLBACK') or { panic(err)}
return
}
current_bots := worker.manage_bots()or{
eprintln(err)
worker.repo.pg_db.exec('ROLLBACK') or { panic(err)}
return
}
// если все успешно комитим транзакцию
worker.repo.pg_db.exec('COMMIT') or { panic(err) }
// обновляем количество итераций
count += 1
// запускаем ботов через манагера
worker.bot_manager.manage_bots(current_bots)or{
eprintln(err)
return
}
// ищем удаленных или стопнутых
deleted_stopped := worker.get_stopped_deleted() or{
eprintln(err)
return
}
// удаляем удаленных или стопнутых
worker.bot_manager.deleted_bot(deleted_stopped)or{
eprintln(err)
return
}
// оновляем инстансы у удаленных на 0
worker.update_instance(deleted_stopped)or{
eprintln(err)
return
}
}
_ := <-worker.stop_ch{
worker.cleanup() or {
println('$err')
}
return
}
}
}
}
fn (mut worker InstanceWorker) register_instance(data models.TelegramIntegrationInstance)! {
instance_id := sql worker.repo.pg_db {
insert data into models.TelegramIntegrationInstance
} or {
return error('Failed register telegram integration instance: $err')
}
worker.instance_id = instance_id
}
fn (mut worker InstanceWorker) get_max_checkout() !i64 {
selected_members := sql worker.repo.pg_db {
select from models.TelegramIntegrationInstance order by checkout desc
}or {
return error('Failed to get max checkout ${err}')
}
if selected_members.len == 0{
return 0
}
return selected_members[0].checkout
}
fn (mut worker InstanceWorker)update_instance_checkout()!{
now := time.now().unix()
sql worker.repo.pg_db {
update models.TelegramIntegrationInstance
set checkout = now where id == worker.instance_id
}or {
return error('Failed update instance checkout: $err')
}
}
fn (mut worker InstanceWorker) remove_stale_instances() ! {
expired := time.now().unix() - 10 * 60 // 10 минут назад
data_expired := sql worker.repo.pg_db {
select from models.TelegramIntegrationInstance where checkout < expired
}or{
return error('Failed get expired data: $err')
}
sql worker.repo.pg_db {
delete from models.TelegramIntegrationInstance where checkout < expired
}or{
return error('Failed remove stale instances: $err')
}
for _,data in data_expired{
sql worker.repo.pg_db{
update models.TelegramIntegration set instance_id = 0 where instance_id == data.id
} or{
return error('Failed update integration table: $err')
}
}
}
fn (mut worker InstanceWorker) manage_bots() !map[i64]models.TelegramIntegration {
mut current_instance_bots := map[i64]models.TelegramIntegration{}
// получаем количество ботов которые еще не назначены ни одному инстансу
mut free_bots := sql worker.repo.pg_db {
select from models.TelegramIntegration where status == models.active_status.str() && deleted == false && instance_id == 0
}or{
return error('Failed fetch free bots: $err')
}
// получаем все "живые" инстансы
all_active_instance:= sql worker.repo.pg_db{
select from models.TelegramIntegrationInstance
}or {
return error('Failed get current instance: $err')
}
// считаем общее количество слотов
mut total_slots := 0
for instance in all_active_instance {
total_slots += instance.limit - instance.amount
}
// количество свободных ботов
bots_assign := free_bots.len
// если количество ботов больше чем суммарное количество слотов всех инстансов отправляем оповещение
if bots_assign > total_slots {
msg := 'Осторожно: Свободных ботов больше чем инстансы могут взять в работу.'
worker.tg_sender.send_message(vgram.ConfigSendMessage{
chat_id: models.telegram_channel_id,
text: msg
})
}
// распределяем ботов по инстансам
mut bot_index := 0
mut assigned_bots := 0
for instance in all_active_instance {
free_slots := instance.limit - instance.amount
mut bots_for_instance := bots_assign / all_active_instance.len
// если доступных слотов больше то используем минимальное значение
if bots_for_instance > free_slots {
bots_for_instance = free_slots
}
sql worker.repo.pg_db {
update models.TelegramIntegrationInstance set amount = amount + bots_for_instance where id == instance.id
} or {
return error('Failed update instance amount: $err')
}
// назначаем ботов инстансу
for i := 0; i < bots_for_instance; i++ {
if bot_index >= free_bots.len {
break
}
bot := free_bots[bot_index]
if instance.id == worker.instance_id{
current_instance_bots[bot.id] = bot
}
sql worker.repo.pg_db {
update models.TelegramIntegration set instance_id = instance.id where id == bot.id
} or {
return error('Failed update bot instance_id: $err')
}
bot_index++
assigned_bots++
}
// если все боты распределены выходим
if assigned_bots >= bots_assign {
break
}
}
return current_instance_bots
}
fn (mut worker InstanceWorker) get_stopped_deleted() ![]models.TelegramIntegration {
mut results := sql worker.repo.pg_db {
select from models.TelegramIntegration where (status == models.stopped_status.str() || deleted == true) && instance_id == worker.instance_id
}or{
return error('Failed fetch stopped deleted bots: $err')
}
return results
}
fn (mut worker InstanceWorker)update_instance(bots []models.TelegramIntegration)!{
for bot in bots{
sql worker.repo.pg_db{
update models.TelegramIntegration set instance_id = 0 where id == bot.id
}or{
return error('Failed update instance for bots: $err')
}
}
}
pub fn (mut worker InstanceWorker) stop(){
worker.stop_ch <- 1
}
fn (mut worker InstanceWorker) cleanup() !{
worker.bot_manager.stop_all_bots()
sql worker.repo.pg_db {
delete from models.TelegramIntegrationInstance where id == worker.instance_id
} or {
return error('Error deleting instance in cleanup: $err')
}
sql worker.repo.pg_db {
update models.TelegramIntegration set instance_id = 0 where instance_id == worker.instance_id
}or{
return error('Error updating instance_id in cleanup: $err')
}
}

107
main.v Normal file

@ -0,0 +1,107 @@
module main
import json
import log
import os
import models
import repository
import context
import db.pg
import veb
import controllers
import instance_worker
import dariotarantini.vgram
import bot_manager
import time
import tg_handle
//v -enable-globals -cc gcc -cflags "-IC:/PostgreSQL/16/include -LC:/PostgreSQL/16/lib" run main.v
__global (
data_set_map map[string]models.Question
)
fn init() {
f := os.read_file('treedata.json') or {
log.error('err read json file: $err')
return
}
data_set := json.decode([]models.Question, f) or {
log.error('err unmarshall json: $err')
return
}
for data in data_set {
data_set_map[data.title] = data
}
}
fn main() {
mut c := context.background()
mut ctx, cancel := context.with_cancel(mut c)
go fn [cancel] () {
shutdown(cancel)
}()
mut repo := repository.Repo{db: map[string]string{},data_set_map: data_set_map}
repo.connect_to_db(pg.Config{
host: 'localhost'
port: 35432
user: 'squiz'
password: 'Redalert2'
dbname: 'squiz'
})or {
eprintln("err conect to db")
}
mut wc := &instance_worker.InstanceWorker{
repo: repo
tg_sender: vgram.new_bot(models.telegram_bot_token)
bot_manager: bot_manager.new_bot_manager(repo)
stop_ch: chan int{}
}
spawn wc.start()
// tg_handle.new_tg_bot(repo,models.TelegramIntegration{
// id: 5
// quiz_id: 26357
// bot_token: '7127966184:AAG1steOCH4wDvHRe9QcsXJPS4dWRyRYsqg'
// })
// tg_handle.new_tg_bot(repo,models.TelegramIntegration{
// id: 4
// quiz_id: 26384
// bot_token: '6712573453:AAFqTOsgwe_j48ZQ1GzWKQDT5Nwr-SAWjz8'
// })
mut app := &models.App{}
mut integration_controllers := &controllers.IntegrationControllers{repo:repo}
app.register_controller[controllers.IntegrationControllers, models.Context]('/integration', mut integration_controllers)!
spawn fn (mut app models.App) {
veb.run[models.App, models.Context](mut app, 8000)
}(mut app)
_ = <-ctx.done()
wc.stop()
time.sleep(2 * time.second)
repo.stop_db()
}
fn shutdown(cancel context.CancelFn) {
os.signal_opt(.int, fn [cancel] (sig os.Signal) {
println('signal: $sig')
cancel()
}) or {
eprintln('$err')
}
os.signal_opt(.term, fn [cancel] (sig os.Signal) {
println('signal signal: $sig')
cancel()
}) or {
eprintln('$err')
}
}

140
models/models.v Normal file

@ -0,0 +1,140 @@
module models
// структуры объявляются в принципе как в гошке, заисключением того что видимо взято из С происходит деление на публичные и приватные
// поэтому для того чтобы реализовать инкапсуляцию, если указать ключевое слово pub - будет все доступно
import time
pub struct Button {
pub mut:
text string @[json: 'Text']
state string @[json: 'State']
}
pub struct Question {
pub mut:
title string @[json: 'Title']
description string @[json: 'Description']
buttons []Button @[json: 'Buttons']
}
@[table: 'question']
pub struct QuizQuestion {
pub mut:
id i64 @[primary; sql: serial]
quiz_id i64 @[sql: 'quiz_id']
title string @[sql: 'title']
description string @[sql: 'description']
question_type string @[sql: 'questiontype']
content string @[sql: 'content']
required bool @[sql: 'required']
deleted bool @[sql: 'deleted']
page i64 @[sql:'page']
}
@[table: 'telegram_integration']
pub struct TelegramIntegration {
pub mut:
id i64 @[primary; sql: serial]
account_id string @[sql: 'accountid']
quiz_id i64 @[sql: 'quizid']
bot_token string @[sql: 'bot_token']
repeatable bool @[sql: 'repeatable']
bot_name string @[sql: 'bot_name']
deleted bool @[sql: 'deleted']
status BotStatus @[sql:'status']
instance_id int @[sql:'instance_id']
}
@[table: 'respondent_state']
pub struct RespondentState {
pub mut:
id i64 @[primary; sql: serial]
telegram_id int @[sql: 'telegram_id']
quiz_id i64 @[sql: 'quizid']
state i64 @[sql: 'state']
lang string @[sql: 'lang']
contact string @[sql: 'contact']
finish bool @[sql: 'finish']
session string @[sql:'session']
}
@[table: 'telegram_integration_instance']
pub struct TelegramIntegrationInstance {
pub mut:
id i64 @[primary; sql: serial]
checkout i64 @[sql: 'checkout']
limit int @[sql: 'limited']
amount int @[sql: 'amount']
}
type BotStatus = string
pub const (
active_status = BotStatus('active')
stopped_status = BotStatus('stopped')
banned_status = BotStatus('banned')
)
type QuestionType = string
pub const(
question_type_variant = QuestionType('variant')
question_type_images = QuestionType('images')
question_type_varimg = QuestionType('varimg')
question_type_emoji = QuestionType('emoji')
question_type_text = QuestionType('text')
question_type_select = QuestionType('select')
question_type_date = QuestionType('date')
question_type_number = QuestionType('number')
question_type_file = QuestionType('file')
question_type_page = QuestionType('page')
question_type_rating = QuestionType('rating')
question_type_result = QuestionType('result')
)
pub const (
check_interval = 5 * time.minute
bot_limit = 10 // лимит ботов на инстанс
telegram_channel_id = '-1002217604546'
telegram_bot_token = '6712573453:AAFqTOsgwe_j48ZQ1GzWKQDT5Nwr-SAWjz8'
buttons_per_row = 5 // количество кнопок в одном ряду
)
pub struct CallbackData {
pub mut:
answer_id string
state_id i64
}
[table: 'telegram_user_quiz_result']
pub struct TelegramUserQuizResult {
pub mut:
id i64 [primary; sql: serial]
bot_id i64 @[sql: 'bot_id']
quiz_id i64 @[sql: 'quiz_id']
current_field string @[sql: 'current_field']
user_answer string @[sql: 'user_answer']
state string @[sql: 'state']
session string @[sql:'session']
question_id i64 @[sql:"question_id"]
iter int @[sql: "iter"]
}
type UserResultTgStatus = string
pub const(
user_result_tg_status_in_progress = UserResultTgStatus('in_progress')
user_result_tg_status_completed = UserResultTgStatus('completed')
)
pub struct ResultContent {
pub mut:
text string [json: 'text']
name string [json: 'name']
email string [json: 'email']
phone string [json: 'phone']
address string [json: 'address']
}
pub struct ImageContent{
pub mut:
image string [json: 'Image']
description string [json: 'Description']
}

296
models/question_types.v Normal file

@ -0,0 +1,296 @@
module models
pub struct QuizQuestionDate {
pub mut:
id i64 @[json: 'id']
required bool @[json: 'required']
inner_name_check bool @[json: 'innerNameCheck']
inner_name string @[json: 'innerName']
date_range bool @[json: 'dateRange']
time bool @[json: 'time']
hint QuestionHint @[json: 'hint']
rule QuestionBranchingRule @[json: 'rule']
back string @[json: 'back']
original_back string @[json: 'originalBack']
autofill bool @[json: 'autofill']
}
pub struct QuizQuestionEmoji {
pub mut:
id i64 @[json: 'id']
multi bool @[json: 'multi']
own bool @[json: 'own']
required bool @[json: 'required']
inner_name_check bool @[json: 'innerNameCheck']
inner_name string @[json: 'innerName']
variants []QuestionVariant @[json: 'variants']
hint QuestionHint @[json: 'hint']
rule QuestionBranchingRule @[json: 'rule']
back string @[json: 'back']
original_back string @[json: 'originalBack']
autofill bool @[json: 'autofill']
}
type UploadFileType = string
pub const(
upload_file_type_picture = UploadFileType('picture')
upload_file_type_video = UploadFileType('video')
upload_file_type_audio = UploadFileType('audio')
upload_file_type_document = UploadFileType('document')
)
pub struct QuizQuestionFile {
pub mut:
id i64 @[json: 'id']
required bool @[json: 'required']
inner_name_check bool @[json: 'innerNameCheck']
inner_name string @[json: 'innerName']
date_range bool @[json: 'dateRange']
tipe UploadFileType @[json: 'type']
hint QuestionHint @[json: 'hint']
rule QuestionBranchingRule @[json: 'rule']
back string @[json: 'back']
original_back string @[json: 'originalBack']
autofill bool @[json: 'autofill']
}
type ImageRatio = string
pub const(
image_ratio_one_to_one = ImageRatio('1:1')
image_ratio_one_to_two = ImageRatio('1:2')
image_ratio_two_to_one = ImageRatio('2:1')
)
type ImageFormat = string
pub const(
image_format_carousel = ImageFormat('carousel')
image_format_masonry = ImageFormat('masonry')
)
pub struct QuizQuestionImages {
pub mut:
id i64 @[json: "id"]
own bool @[json: "own"]
multi bool @[json: "multi"]
xy ImageRatio @[json: "xy"]
inner_name_check bool @[json: "innerNameCheck"]
inner_name string @[json: "innerName"]
large bool @[json: "large"]
format ImageFormat @[json: "format"]
required bool @[json: "required"]
variants []QuestionVariant @[json: "variants"]
hint QuestionHint @[json: "hint"]
rule QuestionBranchingRule @[json: "rule"]
back string @[json: "back"]
original_back string @[json: "originalBack"]
autofill bool @[json: "autofill"]
large_check bool @[json: "largeCheck"]
}
type NumberForm = string
pub const (
number_form_star = NumberForm('star')
number_form_trophie = NumberForm('trophie')
number_form_flag = NumberForm('flag')
number_form_heart = NumberForm('heart')
number_form_like = NumberForm('like')
number_form_bubble = NumberForm('bubble')
number_form_hashtag = NumberForm('hashtag')
)
pub struct QuizQuestionNumber {
pub mut:
id i64 @[json: "id"]
required bool @[json: "required"]
inner_name_check bool @[json: "innerNameCheck"]
inner_name string @[json: "innerName"]
range string @[json: "range"]
start int @[json: "start"]
default_value int @[json: "defaultValue"]
step int @[json: "step"]
steps int @[json: "steps"]
choose_range bool @[json: "chooseRange"]
hint QuestionHint @[json: "hint"]
rule QuestionBranchingRule @[json: "rule"]
back string @[json: "back"]
original_back string @[json: "originalBack"]
autofill bool @[json: "autofill"]
form NumberForm @[json: "form"]
}
pub struct QuizQuestionPage {
pub mut:
id i64 @[json: "id"]
inner_name_check bool @[json: "innerNameCheck"]
inner_name string @[json: "innerName"]
text string @[json: "text"]
picture string @[json: "picture"]
original_picture string @[json: "originalPicture"]
use_image bool @[json: "useImage"]
video string @[json: "video"]
hint QuestionHint @[json: "hint"]
rule QuestionBranchingRule @[json: "rule"]
back string @[json: "back"]
original_back string @[json: "originalBack"]
autofill bool @[json: "autofill"]
}
pub struct QuizQuestionRating {
pub mut:
id i64 @[json: "id"]
required bool @[json: "required"]
inner_name_check bool @[json: "innerNameCheck"]
inner_name string @[json: "innerName"]
steps int @[json: "steps"]
rating_expanded bool @[json: "ratingExpanded"]
form string @[json: "form"]
hint QuestionHint @[json: "hint"]
rule QuestionBranchingRule @[json: "rule"]
back string @[json: "back"]
original_back string @[json: "originalBack"]
autofill bool @[json: "autofill"]
rating_positive_description string @[json: "ratingPositiveDescription"]
rating_negative_description string @[json: "ratingNegativeDescription"]
}
pub struct QuizQuestionResult {
pub mut:
id i64 @[json: "id"]
back string @[json: "back"]
original_back string @[json: "originalBack"]
video string @[json: "video"]
inner_name string @[json: "innerName"]
text string @[json: "text"]
price []int @[json: "price"]
use_image bool @[json: "useImage"]
rule ResultQuestionBranchingRule @[json: "rule"]
hint QuestionHint @[json: "hint"]
autofill bool @[json: "autofill"]
usage bool @[json: "usage"]
redirect string @[json: "redirect"]
}
pub struct QuizQuestionSelect {
pub mut:
id i64 @[json: "id"]
multi bool @[json: "multi"]
required bool @[json: "required"]
inner_name_check bool @[json: "innerNameCheck"]
inner_name string @[json: "innerName"]
default string @[json: "default"]
variants []QuestionVariant @[json: "variants"]
rule QuestionBranchingRule @[json: "rule"]
hint QuestionHint @[json: "hint"]
back string @[json: "back"]
original_back string @[json: "originalBack"]
autofill bool @[json: "autofill"]
}
type AnswerType = string
pub const(
answer_type_single = AnswerType('single')
answer_type_multi = AnswerType('multi')
answer_type_number_only = AnswerType('numberOnly')
)
pub struct QuizQuestionText {
pub mut:
id i64 @[json: "id"]
placeholder string @[json: "placeholder"]
inner_name_check bool @[json: "innerNameCheck"]
inner_name string @[json: "innerName"]
required bool @[json: "required"]
autofill bool @[json: "autofill"]
answer_type AnswerType @[json: "answerType"]
hint QuestionHint @[json: "hint"]
rule QuestionBranchingRule @[json: "rule"]
back string @[json: "back"]
original_back string @[json: "originalBack"]
only_numbers bool @[json: "onlyNumbers"]
}
pub struct QuizQuestionVariant{
pub mut:
id i64 @[json: "id"]
large_check bool @[json: "largeCheck"]
multi bool @[json: "multi"]
own bool @[json: "own"]
inner_name_check bool @[json: "innerNameCheck"]
required bool @[json: "required"]
inner_name string @[json: "innerName"]
variants []QuestionVariant @[json: "variants"]
hint QuestionHint @[json: "hint"]
rule QuestionBranchingRule @[json: "rule"]
back string @[json: "back"]
original_back string @[json: "originalBack"]
autofill bool @[json: "autofill"]
}
pub struct QuizQuestionVarImg {
pub mut:
id i64 @[json: "id"]
own bool @[json: "own"]
inner_name_check bool @[json: "innerNameCheck"]
inner_name string @[json: "innerName"]
required bool @[json: "required"]
variants []QuestionVariant @[json: "variants"]
hint QuestionHint @[json: "hint"]
rule QuestionBranchingRule @[json: "rule"]
back string @[json: "back"]
original_back string @[json: "originalBack"]
autofill bool @[json: "autofill"]
large_check bool @[json: "largeCheck"]
repl_text string @[json: "replText"]
}
pub struct QuestionHint {
pub mut:
text string @[json: 'text'] // Текст подсказки
video string @[json: 'video'] // URL видео подсказки
}
pub struct QuestionBranchingRule {
pub mut:
children []int @[json: 'children']
main []QuestionBranchingRuleMain @[json: 'main']
parent_id int @[json: 'parentId']
default int @[json: 'default']
}
pub struct QuestionBranchingRuleMain {
pub mut:
next i64 @[json: 'next']
orr bool @[json: 'or']
rules []RuleCondition @[json: 'rules']
}
pub struct RuleCondition {
pub mut:
question i64 @[json: 'question']
answers []string @[json: 'answers']
}
pub struct QuestionVariant {
pub mut:
id string @[json: 'id']
answer string @[json: 'answer']
hints string @[json: 'hints']
extended_text string @[json: 'extendedText']
original_image_url string @[json: 'originalImageUrl']
points int @[json: 'points']
}
pub struct ResultQuestionBranchingRule {
pub mut:
children []i64 @[json: 'children']
main []QuestionBranchingRuleMain @[json: 'main']
parent_id i64 @[json: 'parentId']
default i64 @[json: 'default']
min_score int @[json: 'minScore']
}
type ResultForm = string
pub const(
result_form_after = ResultForm('after')
result_form_before = ResultForm('before')
)

101
models/quiz.v Normal file

@ -0,0 +1,101 @@
module models
import time
@[table: 'quiz']
pub struct Quiz {
pub mut:
id i64 @[primary; sql: serial]
qid string @[sql: 'qid']
account_id string @[sql: 'accountid']
deleted bool @[sql: 'deleted']
archived bool @[sql: 'archived']
repeatable bool @[sql: 'repeatable']
name string @[sql: 'name']
description string @[sql: 'description']
config string @[sql: 'config']
}
type ShowResultType = string
pub const(
show_result_type_after = ShowResultType('after')
show_result_type_before = ShowResultType('before')
show_result_type_email = ShowResultType('email')
)
pub struct QuizConfig {
pub mut:
have_root string @[json: 'haveRoot']
form_contact FormContact @[json: 'formContact']
result_info ResultInfo @[json: 'resultInfo']
show_fc bool @[json:'showfc']
antifraud bool @[json:'antifraud']
info QuizConfigInfo @[json:'info']
startpage QuizConfigStartPage @[json:'startpage']
}
pub struct FormContact {
pub mut:
title string @[json: 'title']
desc string @[json: 'desc']
fields FormFields @[json: 'fields']
button string @[json: 'button']
}
pub struct FormFields {
pub mut:
name ContactField @[json: 'name']
email ContactField @[json: 'email']
phone ContactField @[json: 'phone']
text ContactField @[json: 'text']
address ContactField @[json: 'address']
}
pub struct ContactField {
pub mut:
text string @[json: 'text']
inner_text string @[json: 'innerText']
key string @[json: 'key']
required bool @[json: 'required']
used bool @[json: 'used']
}
pub struct ResultInfo {
pub mut:
when string @[json: 'when']
share bool @[json: 'share']
replay bool @[json: 'replay']
theme string @[json: 'theme']
reply string @[json: 'reply']
repl_name string @[json: 'replname']
show_result_form string @[json: 'showResultForm']
}
pub struct QuizConfigInfo {
pub mut:
orgname string @[json:'orgname'] // имя компании
phonenumber string @[json:'phonenumber'] // телефон компании
site string @[json:'site'] // сайт компании
law string @[json:'law'] // юр инфа
}
pub struct QuizConfigStartPage {
pub mut:
description string @[json:'description'] // описание
background Background @[json:'background']
}
pub struct Background {
pub mut:
bg_type string @[json:'type'] //video или image
desktop string @[json:'desktop'] // URL фона для десктопа
video string @[json:'video'] // Видео URL
}
@[table: 'answer']
pub struct Answer {
pub mut:
content string @[sql: 'content']
question_id i64 @[sql: 'question_id']
result bool
}

12
models/reqresp.v Normal file

@ -0,0 +1,12 @@
module models
pub struct TelegramIntegrationCreateReq {
pub :
bot_token string
}
pub struct TelegramIntegrationUpdateReq {
pub:
bot_token string
id i64
}

13
models/srv.v Normal file

@ -0,0 +1,13 @@
module models
import veb
pub struct Context {
veb.Context
}
pub struct App {
veb.Controller
veb.Middleware[Context]
}

81
models/variants.v Normal file

@ -0,0 +1,81 @@
module models
pub const data_set = [
{
'end': Question{
title: 'Last question'
description: 'This is last question'
buttons: []
}
'q1': Question{
title: 'Question 1'
description: 'This question 1 after start question'
buttons: [
Button{
text: 'q2'
state: 'q2'
},
Button{
text: 'q3'
state: 'q3'
},
Button{
text: 'q4'
state: 'q4'
},
]
}
'q2': Question{
title: 'Question 2'
description: 'This question 2 after question 1'
buttons: [
Button{
text: 'Go end'
state: 'end'
},
Button{
text: 'Need end'
state: 'end'
},
]
}
'q3': Question{
title: 'Question 3'
description: 'This question 3 after question 1'
buttons: [
Button{
text: 'Want end'
state: 'end'
},
Button{
text: 'Go dota 2'
state: 'end'
},
]
}
'q4': Question{
title: 'Question 4'
description: 'This question 4 after question 1'
buttons: [
Button{
text: 'Run end'
state: 'end'
},
Button{
text: 'Swimming to end'
state: 'end'
},
]
}
'start': Question{
title: 'Start question'
description: 'This first question, start'
buttons: [
Button{
text: 'start'
state: 'q1'
},
]
}
},
]

262
repository/repository.v Normal file

@ -0,0 +1,262 @@
module repository
import models
import db.pg
// todo дообработать ошибки сейчас что то не возвращаются они
pub struct Repo {
pub mut:
db map[string]string
pg_db pg.DB
data_set_map map[string]models.Question
}
pub fn (mut r Repo) connect_to_db(cfg pg.Config) ! {
r.pg_db = pg.connect(cfg)!
println('Connected to the database successfully.')
}
pub fn (mut r Repo)save_state(chat_id string,state string) {
r.db[chat_id]=state
println(r.db)
}
// pub fn (mut r Repo)get_state(chat_id string)!string{
// if chat_id in r.db {
// return r.db[chat_id]
// }
// return error('empty key')
// }
pub fn (mut r Repo)get_next_que_data(state string)!models.Question{
if state in r.data_set_map {
return r.data_set_map[state]
}
return error('empty')
}
// todo допилить работу со стейтами, надо понять как без telegram id стейт получать и обновлять
// методы QuizRespondentState
// вызывается при /start, создает запись
pub fn (mut r Repo) create_state(state models.RespondentState) !i64 {
state_id := sql r.pg_db {
insert state into models.RespondentState
} or {
return error('Failed insert state: $err')
}
return state_id
}
// вызывать при любом другом, кроме вопроса с типом result
pub fn (mut r Repo) update_state(id i64, new_state i64) ! {
sql r.pg_db {
update models.RespondentState set state = new_state where id == id
} or {
return error('Failed update state: $err')
}
}
pub fn (mut r Repo) get_state(state_id i64)!models.RespondentState{
result := sql r.pg_db {
select from models.RespondentState where id == state_id
}or{
return error('error getting state from db')
}
if result.len == 0{
return error('state not found')
}
return result[0]
}
// выставляется при типе вопроса result = true
pub fn (mut r Repo) finish_state(id i64) ! {
sql r.pg_db {
update models.RespondentState set finish = true where id == id
} or {
return error('Failed finish state: $err')
}
}
pub fn (mut r Repo) get_state_by_tg_quiz_id(tg_id int,quiz_id i64)!models.RespondentState {
result := sql r.pg_db {
select from models.RespondentState where quiz_id == quiz_id && telegram_id == tg_id && finish == false
}or{
return error('error getting state from db')
}
if result.len == 0{
return error('state not found')
}
return result[0]
}
pub fn (mut r Repo)get_state_by_tg_quiz_id_finish_true(tg_id int,quiz_id i64)!int {
result := sql r.pg_db {
select from models.RespondentState where quiz_id == quiz_id && telegram_id == tg_id && finish == true
}or{
return error('error getting state from db')
}
if result.len == 0{
return 0
}
return result.len
}
// методы с question
// получение вопроса по id
pub fn (mut r Repo) get_question_by_id(id i64) !models.QuizQuestion {
question := sql r.pg_db {
select from models.QuizQuestion where id == id && deleted == false
} or {
return error('Question not found')
}
if question.len==0{
return error('Question not found')
}
return question[0]
}
pub fn (mut r Repo) get_questions_by_quiz_id(quiz_id i64) ![]models.QuizQuestion {
question := sql r.pg_db {
select from models.QuizQuestion where quiz_id == quiz_id && deleted == false order by page
} or {
return error('question not found for quiz_id')
}
if question.len==0{
return error("not found")
}
return question
}
pub fn (mut r Repo) get_all_results(id i64)![]models.QuizQuestion {
result := sql r.pg_db {
select from models.QuizQuestion where quiz_id == id && question_type == models.question_type_result.str() && deleted ==false
} or {
return error('question with type result not found for quiz_id')
}
return result
}
// методы Quiz
pub fn (mut r Repo) get_quiz(quiz_id i64)!models.Quiz {
result := sql r.pg_db{
select from models.Quiz where id == quiz_id && deleted == false && archived == false
}or{
return error('QUIZ not found')
}
if result.len == 0{
return error('QUIZ not found')
}
return result[0]
}
// методы TelegramIntegration
// получение интеграции по id
pub fn (mut r Repo) get_integration_by_id(id i64) !models.TelegramIntegration {
integration := sql r.pg_db {
select from models.TelegramIntegration where id == id && deleted == false limit 1
} or {
return error('Integration not found')
}
if integration.len == 0 {
return error('Integration not found')
}
return integration[0]
}
// создает запись о интеграции
pub fn (mut r Repo) create_integration(integration models.TelegramIntegration) !int {
integration_id := sql r.pg_db {
insert integration into models.TelegramIntegration
} or {
return error('Failed create integration: $err')
}
return integration_id
}
// обновляет токен бота
pub fn (mut r Repo) update_bot_token(id i64, new_token string) ! {
sql r.pg_db {
update models.TelegramIntegration set bot_token = new_token where id == id && deleted == false
} or {
return error('Failed to update bot token: $err')
}
}
// обновляет поля ниже токена
pub fn (mut r Repo) update_integration_fields(id i64, integration models.TelegramIntegration) ! {
sql r.pg_db {
update models.TelegramIntegration set repeatable = integration.repeatable, bot_name = integration.bot_name, deleted = integration.deleted where id == id && deleted == false
} or {
return error('Failed update integration fields: $err')
}
}
pub fn (mut r Repo)soft_delete_integration(quiz_id i64)!{
sql r.pg_db{
update models.TelegramIntegration set deleted = true where quiz_id == quiz_id
}or{
return error('Failed soft delete integration: $err')
}
}
pub fn (mut r Repo) update_bot_name(bot_id i64,name string)!{
sql r.pg_db {
update models.TelegramIntegration set bot_name = name where id == bot_id
}or{
return error('Failed update bot name: $err')
}
}
// методы telegram_user_quiz_result
pub fn (mut r Repo) create_record_user_result(data models.TelegramUserQuizResult) ! {
sql r.pg_db {
insert data into models.TelegramUserQuizResult
} or {
return error('Failed create record user result: $err')
}
}
pub fn (mut r Repo) update_record_user_result(data models.TelegramUserQuizResult)!{
sql r.pg_db {
update models.TelegramUserQuizResult set current_field = data.current_field,user_answer = data.user_answer, state = data.state,iter = data.iter where session == data.session
}or{
return error('Failed update bot name: $err')
}
}
pub fn (r Repo) get_record_user_result(ses string)!models.TelegramUserQuizResult {
result := sql r.pg_db{
select from models.TelegramUserQuizResult where session == ses && state == models.user_result_tg_status_in_progress.str()
}or{
return error('record user result not found')
}
if result.len == 0{
return error('record user result not found')
}
return result[0]
}
// закрываем соединение с бд
pub fn (mut r Repo)stop_db(){
r.pg_db.close()
}

@ -0,0 +1,86 @@
module repository
import db.pg
import models
//v -enable-globals -cc gcc -cflags "-IC:/PostgreSQL/16/include -LC:/PostgreSQL/16/lib" run repository/repository_test.v
fn test_init(){
mut repo := Repo{}
repo.connect_to_db(pg.Config{
host: 'localhost'
port: 35432
user: 'squiz'
password: 'Redalert2'
dbname: 'squiz'
})or {
eprintln("err conect to db")
}
state := models.RespondentState{
state: 'initial'
finish: false
quiz_id: 1,
telegram_id: 1,
}
id_create_state := repo.create_state(state)or {
eprintln("Failed to create state: $err")
return
}
repo.update_state(id_create_state, 'updated_state')or{
eprintln("Failed to update state: $err")
return
}
repo.finish_state(id_create_state) or{
eprintln("Failed to finish state: $err")
return
}
question_result := repo.get_question_by_id(2721) or {
eprintln("Failed to get question by id: $err")
return
}
println(question_result)
integration := models.TelegramIntegration{
account_id: 'account1'
quiz_id: 1
bot_token: 'token'
repeatable: true
bot_name: 'integration_name'
deleted: false
}
id_create_integration := repo.create_integration(integration)or {
eprintln("Failed to create integration: $err")
return
}
println("iddddddddddddddd create_integration ${id_create_integration}")
integration_result := repo.get_integration_by_id(id_create_integration)or {
eprintln("Failed to get integration by id: $err")
return
}
println(integration_result)
repo.update_bot_token(id_create_integration, 'new_token')or {
eprintln("Failed to update bot token: $err")
return
}
updated_integration := models.TelegramIntegration{
account_id: 'account1'
quiz_id: 1
bot_token: 'old_token'
repeatable: false
bot_name: 'new_name'
deleted: false
}
repo.update_integration_fields(id_create_integration, updated_integration)or {
eprintln("Failed to update integration fields: $err")
return
}
}

@ -0,0 +1,23 @@
module state_manager
// pub struct StateManager {
// pub mut:
// questions map[string]models.Question
// state string
// client &client.Client
// }
//
// pub fn (mut sm StateManager) listener(state string) {
// sm.state = state
// sm.send_question()
// }
//
// pub fn (mut sm StateManager) send_question() {
// mut question := sm.questions[sm.state]
// mut message := unsafe { strconv.v_sprintf('%s\n%s', question.title, question.description)}
// for _, button in question.buttons {
// message += unsafe { strconv.v_sprintf('\n%s', button.text)}
// }
// sm.client.send_message(message)
// }

1621
tg_handle/tg.v Normal file

File diff suppressed because it is too large Load Diff

21
tg_handle/tg_test.v Normal file

@ -0,0 +1,21 @@
module tg_handle
import json
import models
fn test_find_next_question_id() {
json_content := '{"id":1,"hint":{"text":" ","video":" "},"rule":{"children":[2,3],"main":[{"next":2,"or":"true","rules":[{"question":1,"answers":["AF2rL-7HqZ83Ks6VgrUra"]}]}],"parentId":"root","default":3},"back":" ","originalBack":" ","autofill":false,"own":false,"multi":false,"xy":"1:1","innerNameCheck":false,"innerName":" ","large":false,"format":"carousel","required":false,"variants":[{"id":"hGS4EzG4GppXPoRmKgaDz","answer":"first img","extendedText":" ","originalImageUrl":" ","hints":" "},{"id":"AF2rL-7HqZ83Ks6VgrUra","answer":"second img","extendedText":" ","hints":" ","originalImageUrl":" "},{"id":"aOcmbnORWlJt2Gm7bnZd7","answer":"third img","extendedText":" ","hints":" ","originalImageUrl":" "}],"largeCheck":false}'
question := json.decode(models.QuizQuestionImages, json_content) or {
assert false
return
}
mut callback_data := "AF2rL-7HqZ83Ks6VgrUra"
mut next_question_id := find_next_question_id(question.rule, callback_data)
assert next_question_id == 1
callback_data = "some_wrong_answer"
next_question_id = find_next_question_id(question.rule, callback_data)
assert next_question_id == 2
}

37
tools/ticker.v Normal file

@ -0,0 +1,37 @@
module tools
import time
pub struct Ticker {
mut:
last_sent time.Time
interval time.Duration
stopped bool
pub:
c chan time.Time
}
pub fn new_ticker(interval time.Duration) &Ticker {
mut t := &Ticker{
last_sent: time.now()
interval: interval
c: chan time.Time{}
stopped: false
}
spawn t.tick()
return t
}
fn (mut t Ticker) tick() {
for !t.stopped {
now := time.now()
if now - t.last_sent >= t.interval {
t.last_sent = now
t.c <- now
}
}
}
pub fn (mut t Ticker) stop() {
t.stopped = true
}

7184
treedata.json Normal file

File diff suppressed because it is too large Load Diff

54
utils/middleware.v Normal file

@ -0,0 +1,54 @@
module utils
import net.http
import encoding.base64
import veb
import json
// todo надо rs 256 реализовать
pub fn jwt_auth(mut ctx veb.Context) (string) {
auth_header := ctx.get_header(http.CommonHeader.authorization) or {
println('Authorization missing ')
ctx.res.set_status(http.Status.unauthorized)
return ""
}
token_string := auth_header.trim_string_left("Bearer ")
if token_string == auth_header {
ctx.res.set_status(http.Status.unauthorized)
return ""
}
public_key := "MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgHgnvr7O2tiApjJfid1orFnIGm69
80fZp+Lpbjo+NC/0whMFga2Biw5b1G2Q/B2u0tpO1Fs/E8z7Lv1nYfr5jx2S8x6B
dA4TS2kB9Kf0wn0+7wSlyikHoKhbtzwXHZl17GsyEi6wHnsqNBSauyIWhpha8i+Y
+3GyaOY536H47qyXAgMBAAE="
if public_key == ""{
ctx.res.set_status(http.Status.internal_server_error)
return ""
}
parts := token_string.split('.')
if parts.len != 3 {
ctx.res.set_status(http.Status.unauthorized)
return ""
}
header := base64.url_decode_str(parts[0])
payload := base64.url_decode_str(parts[1])
signature := [parts[2]]
println(header)
println(payload)
println(signature)
data := json.decode(map[string]string,payload)or{
ctx.res.set_status(http.Status.unauthorized)
return ""
}
id := data['id']
println(id)
return id
}

11
utils/session_generate.v Normal file

@ -0,0 +1,11 @@
module utils
import crypto.md5
import time
pub fn generate_signature(quizID string,respondent_id string) string {
mut u := time.now().unix().str()
u += quizID + respondent_id
hash := md5.hexhash(u)
return hash[..20]
}

@ -0,0 +1,5 @@
module utils
fn test_generate_signature(){
println(generate_signature(100,20))
}