From 09c42edfdcc205c27d771c095a4394acaa4d78b4 Mon Sep 17 00:00:00 2001 From: User Date: Tue, 5 May 2026 18:25:28 +0300 Subject: [PATCH] Initial commit: VK Sales Bot project structure --- .env | 8 + .env.example | 8 + .gitignore | 0 .kilo/kilo.jsonc | 3 + .vscode/settings.json | 3 + Dockerfile | 6 + config/__init__.py | 0 config/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 154 bytes config/__pycache__/phrases.cpython-311.pyc | Bin 0 -> 5900 bytes config/__pycache__/settings.cpython-311.pyc | Bin 0 -> 1742 bytes config/phrases.py | 84 +++ config/settings.py | 25 + core/__init__.py | 0 core/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 152 bytes core/__pycache__/exporter.cpython-311.pyc | Bin 0 -> 5568 bytes core/__pycache__/fsm.cpython-311.pyc | Bin 0 -> 15631 bytes core/__pycache__/models.cpython-311.pyc | Bin 0 -> 1566 bytes core/__pycache__/validator.cpython-311.pyc | Bin 0 -> 4366 bytes core/exceptions.py | 5 + core/exporter.py | 76 +++ core/fsm.py | 233 +++++++ core/models.py | 20 + core/validator.py | 52 ++ create_repo.ps1 | 15 + data/leads.xlsx | Bin 0 -> 5201 bytes docs/developer_guide.md | 30 + docs/user_guide.md | 22 + logs/bot.log | 590 ++++++++++++++++++ main.py | 28 + requirements.txt | 9 + services/__init__.py | 0 services/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 156 bytes services/__pycache__/vk_bot.cpython-311.pyc | Bin 0 -> 9024 bytes services/vk_bot.py | 127 ++++ start.py | 27 + tests/__init__.py | 0 tests/test_exporter.py | 0 tests/test_fsm.py | 0 tests/test_validator.py | 24 + utils/__init__.py | 0 utils/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 153 bytes utils/__pycache__/backup.cpython-311.pyc | Bin 0 -> 2115 bytes utils/__pycache__/logger.cpython-311.pyc | Bin 0 -> 755 bytes utils/__pycache__/middleware.cpython-311.pyc | Bin 0 -> 4312 bytes utils/backup.py | 29 + utils/logger.py | 17 + utils/middleware.py | 71 +++ 47 files changed, 1512 insertions(+) create mode 100644 .env create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .kilo/kilo.jsonc create mode 100644 .vscode/settings.json create mode 100644 Dockerfile create mode 100644 config/__init__.py create mode 100644 config/__pycache__/__init__.cpython-311.pyc create mode 100644 config/__pycache__/phrases.cpython-311.pyc create mode 100644 config/__pycache__/settings.cpython-311.pyc create mode 100644 config/phrases.py create mode 100644 config/settings.py create mode 100644 core/__init__.py create mode 100644 core/__pycache__/__init__.cpython-311.pyc create mode 100644 core/__pycache__/exporter.cpython-311.pyc create mode 100644 core/__pycache__/fsm.cpython-311.pyc create mode 100644 core/__pycache__/models.cpython-311.pyc create mode 100644 core/__pycache__/validator.cpython-311.pyc create mode 100644 core/exceptions.py create mode 100644 core/exporter.py create mode 100644 core/fsm.py create mode 100644 core/models.py create mode 100644 core/validator.py create mode 100644 create_repo.ps1 create mode 100644 data/leads.xlsx create mode 100644 docs/developer_guide.md create mode 100644 docs/user_guide.md create mode 100644 logs/bot.log create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 services/__init__.py create mode 100644 services/__pycache__/__init__.cpython-311.pyc create mode 100644 services/__pycache__/vk_bot.cpython-311.pyc create mode 100644 services/vk_bot.py create mode 100644 start.py create mode 100644 tests/__init__.py create mode 100644 tests/test_exporter.py create mode 100644 tests/test_fsm.py create mode 100644 tests/test_validator.py create mode 100644 utils/__init__.py create mode 100644 utils/__pycache__/__init__.cpython-311.pyc create mode 100644 utils/__pycache__/backup.cpython-311.pyc create mode 100644 utils/__pycache__/logger.cpython-311.pyc create mode 100644 utils/__pycache__/middleware.cpython-311.pyc create mode 100644 utils/backup.py create mode 100644 utils/logger.py create mode 100644 utils/middleware.py diff --git a/.env b/.env new file mode 100644 index 0000000..9895b63 --- /dev/null +++ b/.env @@ -0,0 +1,8 @@ +VK_GROUP_ID=233127658 +VK_TOKEN="vk1.a.LAMaFdJdNPYBM2OyaMnw71WS4I4ITwTirs7dxRa3zcrVCzWIEp_wOYe75zZp_ZDpXARQFV4ZXlNpqLtCWUDpAsXGKDfhYjuT9G1vjw0blI3zXTh5k6S-P0Txy5a_JUbZiAfSVIZEYCb6hHs_mv71te9TpHaw-knSwbmxLYRklAIwX5hBlRe-vWM8cBZljmdNx7vh0Poe0-W6hnoEEDh6uQ" +ADMIN_IDS=25076348 +DATA_DIR=data +LEADS_FILE=data/leads.xlsx +BACKUP_DIR=data/backups +LOG_LEVEL=DEBUG +TIMEZONE=Europe/Moscow \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..51d9377 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +VK_GROUP_ID=233127658 +VK_TOKEN=vk1.a.XTkUvOpbeC9ReN0WxLLynwe19Xdfr5kt4_lWcFpJINpS4O-MsQMeSxc9WWj7IqBTDDOLcpdgjGS4MdBsorXIEosmvV-iYwbExBqZaRTdtl7DcrRsYg0uW1gavDc_SBItLjoCnje7WlO5vz8i5pxXgkSuiki9vMbfYZdjYZYF8q5z7sResjJ-ZdBKS73WMCmrgQx0I22rGY0rRR7HKCeC_g +ADMIN_IDS=25076348 +DATA_DIR=data +LEADS_FILE=data/leads.xlsx +BACKUP_DIR=data/backups +LOG_LEVEL=INFO +TIMEZONE=Europe/Moscow \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/.kilo/kilo.jsonc b/.kilo/kilo.jsonc new file mode 100644 index 0000000..571a15b --- /dev/null +++ b/.kilo/kilo.jsonc @@ -0,0 +1,3 @@ +{ + "$schema": "https://app.kilo.ai/config.json" +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c9ebf2d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python-envs.defaultEnvManager": "ms-python.python:system" +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9fcc55b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.11-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +CMD ["python", "main.py"] \ No newline at end of file diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/__pycache__/__init__.cpython-311.pyc b/config/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d78aa981522a31ff4375d924139951f3ba410112 GIT binary patch literal 154 zcmZ3^%ge<81lIFkW`gL)AOZ#$p^VRLK*n^26oz01O-8?!3`I;p{%4TnFI``&n56uY z__FNy%@K;i>4BO~Jn1{hJq3={(Z4|yY# literal 0 HcmV?d00001 diff --git a/config/__pycache__/phrases.cpython-311.pyc b/config/__pycache__/phrases.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..80fe86a9e0adcc7a911a4ea011d39c59e5fd1203 GIT binary patch literal 5900 zcmb_g?Ne0O9lpB=!ou>hAR6Ct$s5urN?U7WI?bT0N{tHxtEG2rwx??{1>*(tMdL;DCF3RYW#eV@l`gHJ-a6ycwDiQ)KkMp`>YakkDXi`10&!`9XC}t-y`w+A9d{z(fId5N5)4H0%aNZr*39OiMq+sv3u4dIE zHLGLxv6{xNGgvy=sHYk_84~Psx;=__5A5@5(jLvo zD;`vT0m2Tix|{TMbAbj@hxyAsU~UpBZ%cAU1tnuv6!+D82ckN-Oy~kjzs3C;3;xHheLCY zF%0l0YR(>qjOcP13@o%v+h<|H{Bj#rQs*m}Gl#%33$O0dw{&-RH|Y4-CV$P=2N3-A zEhORzo-$|9av$z3&|N2i3--91J~cy(B#|b`qeMgB0%FPSndorrC z0J#W@PzbJQg5L?oBT7gndj`9}@;BKPe+%eVmCxMFM}bkWVC{#%b;tJRoR*RL4n{ z_x;8|Kutj06t?A{Unl`&fVAm^xOj{bg3b0tLdw1jx#O=dO*i%lZ1H^?i_XIkQcO}U zG+4uQk-C=A&(g|+Li>V!h9Jm7=?dzUrHZ0}*8igh=b4l&%rlAnPEb87g~8AwHgHTz z10Dk__GQwlj;N7XlBAy{6ycx?Yn_B}p(I>~w2Sst!hBNAG+QH#9O5bIJkN5h)759l zCKH_im97{-($WG5085rv(6cebm>0UIl_#kf1~@bUo_|Fs=5=%>0tlhixKd+I|V;1CH@ez`f5Qn-XZ$cky<&OH4^d!A%h6uz`ARO2M zTT8?``d8u~{)V>K%BrwB^a9R`Rj-@&tKbx+)yr}CuUw6l4~j(YI+ z?{k7DLP00v>QRoZao z5KnE6uL4P)3gl2EmCB9zFv`V@iz672gfBDD$kU4OqT>V*M}$cxu?nmOF7{S?{RTnx z;sW^I<0|fEaxWkS51jg#gLT|7t7d_ZYw{ASGV!39WC~!B#14L!v(WDjr!p<8W5h) zEyeL=m_UI&6DFFi%OnMs&$4zR@tA`1&Vj-`wL0+m&I|yLz&Cw6&!KQ0^&Vrr^MrAS z85Ac0barbJUcU3-?+HXLd#aGgt#GE0?Z8};<SAc((_{$sVV2`Ez195AOJIvot*|;%kEGj9b8C;skM_0&z)<$m= z&09(Lri_v&xM#iUi4YUdd+DMMUZ3(jr8m6EX05UQz@Y;&sRM_eb5f3e5+HFl>+c=w zi@zUGJW*sW6__179v|q%{54@s>8*LBh@Rj!S}Y#vJBhXL4fYK64vfV529Cww$Hq~d zV%#zW!}7R@glCJ!`jSTGfvN5ZoJV)<{6n*QA{HvT&pyJKkf1pja~N?)1*u-Fcukdgz;H=%smF+g>yQPxDtM_{&$#_-$^oE(~X7`)vpu3UCD~U89B7Y(Qz3T;-p1M@M_S%!YfaU-bFv~cTkX!awnSTET?gAbI>N1~ zs=UdTBbX}Q-x&@^+ami@Mbx&hEmfK;yhKb8-NAeX#e5TOI}}cp@iU{i%=IP~4R=RV zrCjCtSL(g`dAKVT@CK3gRA|rPXtX`zEsXJf)si_ZtM<0weXQd^XG<5>g}li~duFZz zbNkvl55*3JySiHThf~xn6P{)nKx9?>o}Y&IM%$n~(jLYCpQS3YQ_%x$k%OsFc0zP8 z5dqga$}4f7A+Fqg>-LH3yYel`irX4*0mI1bYG>`VvK0 zT@firl%`i{!M*-O3C2~2{fQvPWgq$z#TZu}_b1BIMXL&XWNR>8u2uZPpD0}{k7J)- zEAJ{u1io6+M3ptWpysREUn}QNt4{k1YW-==`@ouXSz%!VZKzC#w2D&{6x=;~zz()1 z0yHo#T0$_e@R#CWWxCi`*e$cw=`yW+T_Uhpwk}b!Sk(ldL)#M-=^`z-1{1+ryvlx* P)wpwsKq4@-72*3Y)r1RF literal 0 HcmV?d00001 diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ecb6a459a6cbf44ea450e8d4add84458592c4283 GIT binary patch literal 1742 zcma)6T}<0n6uvegapKq{DSw^Bh^l2HVX(1H1Eva^5SONefJjz>#cGAvP2D&+vJGRb zlf0?YsKlgnLIP`Rb-i4~bwB0FA! zWD&CCmB>I#r2#SMtdS44M1;f(EfFD!tz9^ON2j*f`Ae6xM*>KJ*N+vu`A)Hi?-GeD zi1t3iX){p*(u$fF1yMDm)pg{$=JUpFNZ*(drWfWuoflF`NFgUTH^XHLXUsxa%q_%Y zaJYfgE+~S#;Iq-Mh`U4$`7erduD^C07<(eGhjI4)fZQI-f}cT@gzdVa$racpuNufB zhu??9hK`9B4j&DbKUrQ;4Pm_`$fe5idTB_{E0R8h_Ld9k>MgmrtV@O=uNL*;$`?nz z%ZjWU1+`q6&{&iKkqqb`B4vWQf3dl0#2@*8ihmXVl>e5|&Ue{raJT3Co(C6qF5bVn zee*2t5WxWS#*?$D47$cF1V_ZXkV?jLaUq#nfG#m_cbm~ zz5#uS_|y!#e;nyq$rnm%6&-rhbJIeayTPTQE0wuA2dP|YmgDC#+$O`VX=+7!Z&uX{ z>gPE$NlMy4y$EV)Gc-6F9uNc}8b)g9cs;|NMY|!X>d@UBzJ@0PJry*6MPAWd*rNpD zmaIsE0Ns_mCaoHfO2o4qu1dpyXV6`gFmbeYJR9pCQ>EV~ZC9KG#9hrO@;hsde z-wRgcx*fTWxM2AgZU3U_ZJy?>k~`kLSnc-XZtLw)D==mU#w>c=rpHl2Cb*S5IXwMv z;PJ8*i(8?D9ZFctl+8?;zSf8(D>h+;ChgFq#aywOD=5O>wUyXTRZF#xEq~1R$4u{8 z;kAB?92OniK1US>&ryk0@&FAkgB{FKt1RJ$Tppk9#a V#r#0x12ZEd;|B&9QN#=s0|484BNYGu literal 0 HcmV?d00001 diff --git a/core/__pycache__/exporter.cpython-311.pyc b/core/__pycache__/exporter.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e8514f30d7981f8b70b6701bc5e95f13c68c5e5f GIT binary patch literal 5568 zcmbUlZERE5^}c66`}r$2aT1d7F&7A63h9^vBzy@%0_{RmI?6gSx>u9;5)6**-S(64)rRD&Y^UQY6>+yeXw0A4SVk`oedVn z9pL{-n(ZLVWr+lsm9pG4SH#OpnY#}3TctJ_Q<^hNri-U4A40A}?N*6`e1~1~-NQn2 z0(t19K4>(3gK_PQq-Og1;zOnfTiBLOuM|(lH3`oULC@71xbKp1nA*`_$=_ zCY?Vg#m}Wroj!`=`hqD+)@;&nosCD$MLKRpZ2>S!@(t_8 zoa0qc%r`X8H8kHCG#Ym08g?XKd&UsP&!7jwlvbZMU ziOfJ!J#31pw319;7)spaO(C9?l_ArmB>Uq@r8_EE0_akP;;N>ad>ZzIx_g-7on_XCeCWdQv_q}1uqE%FL%A)b=5glbwio`;aHd9Ka%qw(Tlkd zz{#vBX&4qXoe3~PHkAhhYl&ksD9;t5m5C~dNU2sKR(zJALJ4y( z8PO4kZB$8|v)ox1?1m-4-DS8~hI`6z<|wcZ{ZUU@0^DDQ;|W;;dQ}mZ9aEnE3d1$x zwCxKok)^C7>+I*5id38<>zKyN{7$l+-vu0SS1OmbereFz>LM&sGd{Uv#97%3j zPqdB2{)B8Gbj^rho2{nUDtofRa&ufHvUfyyM;PP=$&lkD8RR@b!Jqw}zyx@Yll5df zQJsUl)srJ9;ohy5s4ue**3u_HpC5q6&lsbi=rhLU2v`ZU6*Kx@^jT2%ab_g`#LUCz zKp9;$6A$!zGy*f+Pn3~?0MmHe^1=V!MlYJ@EgLHnS!VrUy?Z;r54D+U-WE0XL=&gGP>$Z-OgP>5br~TyWEvGapz{Aa$NP z3-Gmpx^HS%PQEqWa&7g0)=y~Q0B0!z9N^d2w9dQ^0J0Hk%Z1wXP+LA!`z@q`06npE zkQIlx!ln`+m{0YHvMM%|Bd$_`_=Np*G>+>iR=1 z!owB;@iqt0A9mZT{~e08QBJRLNyuhbfnWs^#5&Ww18F zX(by<$ddh)`7HM_1k#^4hQY3m@LI_lLa!xjTo$}!E37<6G;}$O>1=5g*_q{+vr4i6 z;RgJpuFN4AR1t1KQ1O6Sli!pg4}FH2mHQY+u;zOa2rEYQyI_j$f+3oH7HB%zG|(hV zO)odS+H~9upn9p&=~(Z%Oj>#-G!G@zq2W+Ft4DglLTC!J9zM%NY=CW0^iHND30f8rS6-x9Q(M_LT>Km5s(@V_ggW>hYsjYo^xTSY`NkGiEQ_5grv_}g;+Hoce&9$1MjX9W^2s%ROSw23u3Y$n!s{x7js!BNUw9A0u$ zj^ob{EMO5U$&TAjyTbxuE(He|5>y5)_Ewt`l?5TPfPTPA_8FGcV<|Y%FyLEEXG^PB zsA4IUDfYzD-sc>?UC8rR9&uzj*|pRIi%`AD96CV#bK|`TXyk(kM)>$F(C*6rW0#SBk6AJs{3Ad+Kdfie3MmXwc0G18b zInUnwnzc7p%<_784`%1T@&*d_Q0A=3&Q(0>PFI;(#P@5wtLn-{_Ke_@P6{a5(|1uyjZu){ z%_T2#IofTyt*-zQ-38N76#S@YnRc<`;2Q_gurJ9P-GiM_80dZkxaq~$AAJ?@^5-8s zBiZ+bN_%cmk6`cP0MMR8n8SI_aLiJ_s5ke2Dc<+rtGZt`-}>5I>udTk$!L8$*ZMZJ zSlMXo2lFjfjr%9wnEcuFPYrQCe*CO~?`KJZuO-qzO3uLd z9?A4e($6!ogw;av4kXSZ&CDh05=Jn-Fo1n`Jo8#uigSM zTKW}uhtN2^p+KOh1YS|V2Wc6sX%OnJo+%J0D#1>d04}ElLZWb(yV`=I53@NcV2vQG zDi8p;b`f6=v(AA#j|NRwI+m1Us`W~6qk1Ibrx|BV5kD}o@ZAZ0tw;dB=VfTdivNB1 zF{v4tt0Yv$J@!WEi}y1zvZRG^>c==9qantG|Mc8&u^E)O3w`qSsMvNQe%E5S}d literal 0 HcmV?d00001 diff --git a/core/__pycache__/fsm.cpython-311.pyc b/core/__pycache__/fsm.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..855a67083abd33e1c90c5848098b0488f05f09dd GIT binary patch literal 15631 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 PB8=cm@IJeO%8uY~T?c|vULEY{VmKqEw zNl>YPsZB|umgG@JOR9F(ELjqDW3qqpBR_6iQ?*lFMOCV{wrcsumZr-1E9dkK!_2U5 zymRN=+vnVS?xW9l&gnnq=GqCAnZEz}@}Tz>KFk+Wvh(}`bZ!udNSvPpxE7AXKJVvS zc%v0s1ZV}nDPU?b88VaK9I&)ljK0~Q6R@^ep>L6GQjTotFiF-SQ;S`)rJn8Z>`0S4 z;MpnhO~m7z#Z7oP-Qo{*cFME?x>m`n%Bn9QLx0I}uippHzEJR}tc3mQeUK}%s``SR zif&TnURAeHSq;-*JC{5UHfHz>@PA$e+#4i9Cg5RlxeKfZDRw`OOV^T;WR2rg)SoO7 z?k>!EkeVlPUAQu#ZZ&6q41xQUgh|s~9!A1HQJRQk?&l-iL2~-7enGX(^9ebh(jVa? zLgp+bKTEGATW>qhktQ;-Ib&Q-giKm9HNpMc!p*QMyQ2-T&)GvZ1?^v1RAhf(^nS*W_YkC?vByCF%%<2Ak$nm}hCnq0{J5t z@q~RYvQM;Y+9hoyL;HPRxB8T}3;442T$$1kZ41bX;_Z~fg`nZbr`T9I` z`+ce|_~jtQx1jS>*3AL0+J4SsqE=%{Pl!s2p407BRhb5Llktpj5}~}>5~yxH8&caY zbhY`qx?3-Fl`CGqtds+rsvBpb3nIoK8;$~dy?iI4yv&_}5 zx!Pl{c4lpl=amk;6JJs|M|kr^EG+4Diqm5pE(i2j|Tf$&?93nk$(joMe%5e#)<$ zA4K|~_kUKhCRZ}!{5Kdszq97o?#!IEv%Qkd*qJ?jXE%xD0IpqfW^r?Y>y+}ogPWIm zwwvU(wVzLSCXy?{PcF%Ze#Qv5v~)5*dxWQ8!8uF=cBcX7QGoX)adV}(>BNZ>9uY{n zQ2|`&6-4$o^d#D#b_w9PeVYhwVp<#020(yp7&9eSZmsI$TMz1F)&cK9=PGrsY8I`c zVHWMC&xGn{5l(%)N1R0``id)`MOZXb7tllDVxa6ccpN~zHULXP50-qQU1NU{(apeC z!Ix1djPsaj0W`XxgwN7^?AaC7OI4-Er=Bw)?h)u}=pjri2O~p~G5RcLCE|4B zc;}aEw>?|CEt;>b-4$EA%V5Tr76Kl*34VgV<x4IUS)K9v@yDKGWk4R_H_Nz{ZES@Z%N4ixtui~WsOZx|FtT? z8n&TtDTtnAuDzOTZ_KrqS@+I>KRCv!+nD2w<~S2`oMEOjbQRo2h9|(chj;k=1zge3 zO>Y{C-y{rQHKm8)VN(YuaYI&#KS(mebr$Y2!?GZmCe7KFZzJjGNDIb9MT<(7^QrKT z{^b^*9bWsn2uJrsIMqEr1H4}<$Y<`c3(cL-Qt%!|!nQCY(UWYMrvQYfR)^$50E90( zMR0pDQPDh;Wh0>*AcWmN?^^=45LJY+zJ2ZB9LEk zKO~%yBc8V6E4#v9AjioKjw2&B3Gyfp*KnVB1f8$grdzttQLiHFc1%3m{GlG1COyP} z;UQkxYC7eh+}llS`_^=OsUo*wB3n`854C&!N^M0-TnZ~I`+&UshI|9QV~!BVVLm}8 z4h`=fYhjLc>CD&l4aj*+Mw$fY{{{O54xfH?VRV^rzsNIg1g<)aFP=RV7bb+4BL}bk zPjHV>QM(Kc+#`Cf;4y`Xl<`5+XB*ukP?GvihT+Or(I53-{Z3R_*b@YqiHLbRU{%&o^Cd5 zhT^Gp!5jQAkz(5Mi^n)9g=`AVh9bJ@50Ic|(1l>A2R?jM+JQHr+v~^c>ZreP)lI7QF z`E{}UI_9i<`EtflfcvRkAT!pykvz7%CTao#b=sY1HS3<3bq`xqUqXT8*jx+ke=Nkg zdg2(|!rOiUs52nI$$`Sjfda=I5EOL2e=pp>{y-J^N0q&O#FeWFw zleGqBhy8Lbtp^^aT*_BK;0PQSCo5TIjT75|@rx5To3C-QhGo_`*~BtyoUCG*b dict: + """Возвращает статистику по лидам""" + if not self.file_path.exists(): + return {"total": 0, "today": 0, "statuses": {}} + df = pd.read_excel(self.file_path, engine='openpyxl') + total = len(df) + today = len(df[pd.to_datetime(df['timestamp']).dt.date == datetime.now().date()]) + statuses = df['status'].value_counts().to_dict() + return {"total": total, "today": today, "statuses": statuses} + + def export(self, target_path: Path = None): + """Принудительный экспорт (копия)""" + if target_path is None: + target_path = settings.DATA_DIR / f"export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" + shutil.copy2(self.file_path, target_path) + return target_path \ No newline at end of file diff --git a/core/fsm.py b/core/fsm.py new file mode 100644 index 0000000..56686cb --- /dev/null +++ b/core/fsm.py @@ -0,0 +1,233 @@ +from enum import StrEnum, auto +from typing import Dict, Optional +from loguru import logger +from core.models import LeadData +from core.validator import validate_fio, validate_phone, parse_time +from core.exporter import ExcelExporter +from config import phrases + +class DialogState(StrEnum): + START = auto() + ASK_CONSENT = auto() + COLLECT_FI = auto() # ФИО пользователя + ASK_PARENT_DATA = auto() # Спрашиваем, хочет ли пользователь ввести данные родителей + COLLECT_PARENT_FIO = auto() # Ввод ФИО родителя + COLLECT_PARENT_PHONE = auto() # Ввод телефона родителя + COLLECT_PHONE = auto() + COLLECT_TIME = auto() + CONFIRM = auto() + FINISHED = auto() + OBJECTION_HANDLED = auto() + +class DialogManager: + def __init__(self, exporter: ExcelExporter): + self.exporter = exporter + self.sessions: Dict[int, dict] = {} + + # def _handle_age(self, user_id: int, text: str) -> str: + # result = validate_age(text) + # if not result.is_valid: + # return result.error_message + "\n" + phrases.ASK_AGE + # session = self.sessions[user_id] + # session["data"].age = result.value + # logger.info(f"User {user_id} age = {result.value}") + # if result.value < 18: + # session["state"] = DialogState.ASK_PARENT_DATA + # logger.info(f"State changed to ASK_PARENT_DATA for user {user_id}") + # return phrases.ASK_PARENT_CONSENT + # else: + # session["state"] = DialogState.COLLECT_PHONE + # logger.info(f"Age >=18, state changed to COLLECT_PHONE") + # return phrases.ASK_PHONE + + def _handle_parent_consent(self, user_id: int, text: str) -> str: + """Обрабатывает ответ на вопрос про данные родителей. + Если пользователь вводит 'пропустить' и т.п. — пропускает. + Иначе считает, что это и есть ФИО родителя, и переходит к телефону. + """ + session = self.sessions[user_id] + text_lower = text.lower().strip() + if text_lower in ("пропустить", "нет", "не надо", "skip", "не хочу"): + session["state"] = DialogState.COLLECT_PHONE + logger.info(f"Parent data skipped for user {user_id}") + return phrases.PARENT_DATA_SKIPPED + "\n" + phrases.ASK_PHONE + else: + # Считаем, что пользователь сразу ввёл ФИО родителя + result = validate_fio(text) + if not result.is_valid: + return phrases.PARENT_FIO_INVALID + "\n" + phrases.ASK_PARENT_CONSENT + session["data"].parent_full_name = result.value + session["state"] = DialogState.COLLECT_PARENT_PHONE + logger.info(f"Parent FIO saved for user {user_id}, state changed to COLLECT_PARENT_PHONE") + return phrases.ASK_PARENT_PHONE + + + def _handle_parent_fio(self, user_id: int, text: str) -> str: + result = validate_fio(text) # используем ту же валидацию, что и для ФИО пользователя + if not result.is_valid: + # Подменяем сообщение об ошибке для родителя + return phrases.PARENT_FIO_INVALID + "\n" + phrases.ASK_PARENT_FIO + session = self.sessions[user_id] + session["data"].parent_full_name = result.value + session["state"] = DialogState.COLLECT_PARENT_PHONE + return phrases.ASK_PARENT_PHONE + + def _handle_parent_phone(self, user_id: int, text: str) -> str: + result = validate_phone(text) # используем существующий валидатор телефона + if not result.is_valid: + return phrases.PARENT_PHONE_INVALID + "\n" + phrases.ASK_PARENT_PHONE + session = self.sessions[user_id] + session["data"].parent_phone = result.value + session["state"] = DialogState.COLLECT_PHONE + return phrases.PARENT_DATA_SAVED + "\n" + phrases.ASK_PHONE + + def handle_message(self, user_id: int, text: str) -> str: + """Обрабатывает сообщение и возвращает ответ бота""" + logger.info(f"=== HANDLE: user {user_id}, text='{text}', current state={self.sessions.get(user_id, {}).get('state')} ===") + # Команда /start или "начать заново" + if text.lower() in (phrases.CMD_START, phrases.BUTTON_RESTART.lower(), "начать заново"): + return self._reset_dialog(user_id) + + session = self.sessions.get(user_id) + if not session: + return self._start_dialog(user_id) + + state = session["state"] + lead = session["data"] + + # Обработка возражений на любом этапе + if self._is_objection(text): + return self._handle_objection(user_id, text) + + # Продолжение после возражения "подумаю" + if state == DialogState.OBJECTION_HANDLED and text.lower() == phrases.BUTTON_CONTINUE.lower(): + # Возвращаемся к предыдущему состоянию + prev_state = session.get("prev_state", DialogState.COLLECT_FI) + session["state"] = prev_state + # Не рекурсивный вызов, а продолжение основного потока + state = session["state"] + lead = session["data"] + + # Обычные состояния + if state == DialogState.ASK_CONSENT: + return self._handle_consent(user_id, text) + if state == DialogState.COLLECT_FI: + return self._handle_fio(user_id, text) + elif state == DialogState.ASK_PARENT_DATA: # Спрашиваем про данные родителей + return self._handle_parent_consent(user_id, text) + elif state == DialogState.COLLECT_PARENT_FIO: + return self._handle_parent_fio(user_id, text) + elif state == DialogState.COLLECT_PARENT_PHONE: + return self._handle_parent_phone(user_id, text) + elif state == DialogState.COLLECT_PHONE: + return self._handle_phone(user_id, text) + elif state == DialogState.COLLECT_TIME: + return self._handle_time(user_id, text) + elif state == DialogState.CONFIRM: + return self._handle_confirm(user_id, text) + + def _start_dialog(self, user_id: int) -> str: + self.sessions[user_id] = { + "state": DialogState.ASK_CONSENT, # ← было COLLECT_FI + "data": LeadData(user_id=user_id) + } + return phrases.ASK_CONSENT + + def _reset_dialog(self, user_id: int) -> str: + if user_id in self.sessions: + del self.sessions[user_id] + return self._start_dialog(user_id) + + def _handle_fio(self, user_id: int, text: str) -> str: + result = validate_fio(text) + if not result.is_valid: + return result.error_message + "\n" + phrases.ASK_FI + session = self.sessions[user_id] + session["data"].full_name = result.value + session["state"] = DialogState.ASK_PARENT_DATA + return phrases.ASK_PARENT_CONSENT + + def _handle_phone(self, user_id: int, text: str) -> str: + result = validate_phone(text) + if not result.is_valid: + return result.error_message + "\n" + phrases.ASK_PHONE + session = self.sessions[user_id] + session["data"].phone = result.value + session["state"] = DialogState.COLLECT_TIME + return phrases.ASK_TIME + + def _handle_time(self, user_id: int, text: str) -> str: + result = parse_time(text) + if not result.is_valid: + return result.error_message + "\n" + phrases.ASK_TIME + session = self.sessions[user_id] + session["data"].preferred_time = result.value + parent_info = "" + if session["data"].parent_full_name and session["data"].parent_phone: + parent_info = f"Родитель: {session['data'].parent_full_name}, тел: {session['data'].parent_phone}\n" + elif session["data"].parent_full_name: + parent_info = f"Родитель: {session['data'].parent_full_name} (телефон не указан)\n" + elif session["data"].parent_phone: + parent_info = f"Родитель: телефон {session['data'].parent_phone} (ФИО не указано)\n" + session["state"] = DialogState.CONFIRM + return phrases.CONFIRM_MESSAGE.format( + fio=session["data"].full_name, + phone=session["data"].phone, + time=session["data"].preferred_time, + parent_info=parent_info + ) + + def _handle_confirm(self, user_id: int, text: str) -> str: + if text.lower() in ("да", phrases.BUTTON_YES.lower()): + lead = self.sessions[user_id]["data"] + lead.status = "completed" + self.exporter.save_lead(lead) + self.sessions[user_id]["state"] = DialogState.FINISHED + return phrases.CONFIRM_YES + elif text.lower() in ("нет", phrases.BUTTON_NO.lower()): + return self._reset_dialog(user_id) + else: + return "Пожалуйста, ответьте 'Да' или 'Нет'." + + def _is_objection(self, text: str) -> bool: + obj_phrases = ["не сейчас", "подумаю", "не хочу оставлять телефон", "не хочу говорить номер"] + return any(phrase in text.lower() for phrase in obj_phrases) + + def _handle_objection(self, user_id: int, text: str) -> str: + session = self.sessions.get(user_id) + if not session: + return self._start_dialog(user_id) + + lead = session["data"] + if "не сейчас" in text.lower(): + lead.status = "postponed" + self.exporter.save_lead(lead) + self.sessions[user_id]["state"] = DialogState.FINISHED + return phrases.OBJECTION_NOT_NOW + elif "подумаю" in text.lower(): + # Сохраняем текущее состояние, переходим в OBJECTION_HANDLED + session["prev_state"] = session["state"] + session["state"] = DialogState.OBJECTION_HANDLED + return phrases.OBJECTION_THINK + elif any(phrase in text.lower() for phrase in ["не хочу оставлять телефон", "не хочу говорить номер"]): + lead.status = "rejected" + self.exporter.save_lead(lead) + self.sessions[user_id]["state"] = DialogState.FINISHED + return phrases.OBJECTION_NO_PHONE + return self.handle_message(user_id, text) # fallback + + def _handle_consent(self, user_id: int, text: str) -> str: + text_lower = text.lower().strip() + session = self.sessions.get(user_id) + if not session: + return self._start_dialog(user_id) + + if any(phrase in text_lower for phrase in phrases.CONSENT_YES_PHRASES): + session["data"].consent_given = True + session["state"] = DialogState.COLLECT_FI # ← было COLLECT_FIO + return phrases.CONSENT_YES + "\n" + phrases.ASK_FI # ← было ASK_FIO + elif any(phrase in text_lower for phrase in phrases.CONSENT_NO_PHRASES): + del self.sessions[user_id] + return phrases.CONSENT_NO + else: + return "Пожалуйста, ответьте 'Да' (согласен) или 'Нет' (не согласен)." \ No newline at end of file diff --git a/core/models.py b/core/models.py new file mode 100644 index 0000000..8b73734 --- /dev/null +++ b/core/models.py @@ -0,0 +1,20 @@ +from datetime import datetime + +from pydantic import BaseModel, Field +from typing import Optional + +class LeadData(BaseModel): + user_id: int + full_name: Optional[str] = None + phone: Optional[str] = None + preferred_time: Optional[str] = None + timestamp: datetime = Field(default_factory=datetime.now) + status: str = "new" + consent_given: bool = False + parent_full_name: Optional[str] = None + parent_phone: Optional[str] = None + +class ValidationResult(BaseModel): + is_valid: bool + value: Optional[str] = None + error_message: Optional[str] = None \ No newline at end of file diff --git a/core/validator.py b/core/validator.py new file mode 100644 index 0000000..30fbbfe --- /dev/null +++ b/core/validator.py @@ -0,0 +1,52 @@ +from loguru import logger +import re +import dateparser +from datetime import datetime +from core.models import ValidationResult +from config import settings + + +def validate_fio(text: str) -> ValidationResult: + """Проверка ФИ: 2 слова, кириллица, возможен дефис""" + text = text.strip() + words = text.split() + if len(words) != 2: + return ValidationResult(is_valid=False, error_message="Должно быть 2 слова") + pattern = r'^[А-ЯЁа-яё\-]+$' + for word in words: + if not re.match(pattern, word): + return ValidationResult(is_valid=False, error_message="Только буквы кириллицы и дефис") + return ValidationResult(is_valid=True, value=text) + + +def validate_phone(phone: str) -> ValidationResult: + """Принимает +7 (XXX) XXX-XX-XX или просто 10 цифр после 7/8""" + cleaned = re.sub(r'\D', '', phone) + if len(cleaned) == 11 and cleaned.startswith('7'): + cleaned = '8' + cleaned[1:] + if len(cleaned) == 10: + cleaned = '8' + cleaned + if len(cleaned) == 11 and cleaned.startswith('8'): + formatted = f"+7 ({cleaned[1:4]}) {cleaned[4:7]}-{cleaned[7:9]}-{cleaned[9:]}" + return ValidationResult(is_valid=True, value=formatted) + return ValidationResult(is_valid=False, error_message="Некорректный номер") + + +def parse_time(text: str) -> ValidationResult: + """Парсит естественно-языковое время, возвращает строку для отображения""" + text_lower = text.lower().strip() + if any(phrase in text_lower for phrase in ["любое время", "не важно", "в любое", "когда удобно"]): + return ValidationResult(is_valid=True, value="Любое удобное время") + + try: + parsed = dateparser.parse( + text, + settings={'TIMEZONE': settings.TIMEZONE, 'RETURN_AS_TIMEZONE_AWARE': False} + ) + if parsed and parsed > datetime.now(): + formatted = parsed.strftime("%d.%m.%Y в %H:%M") + return ValidationResult(is_valid=True, value=formatted) + except Exception as e: + logger.warning(f"Failed to parse time '{text}': {e}") + + return ValidationResult(is_valid=False, error_message="Не удалось распознать время. Укажите, например: 'завтра после 18', 'в среду утром' или 'любое время'.") \ No newline at end of file diff --git a/create_repo.ps1 b/create_repo.ps1 new file mode 100644 index 0000000..a676233 --- /dev/null +++ b/create_repo.ps1 @@ -0,0 +1,15 @@ +$body = @{ + name = 'bot_vk_ikp' + description = 'VK Sales Bot' + private = $false +} | ConvertTo-Json + +$bytes = [System.Text.Encoding]::UTF8.GetBytes($body) + +$headers = @{ + 'Authorization' = 'Bearer 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' + 'Accept' = 'application/vnd.github.v3+json' +} + +$result = Invoke-RestMethod -Method POST -Uri 'https://api.github.com/user/repos' -Headers $headers -Body $bytes -ContentType 'application/json; charset=utf-8' +Write-Host "Repository created: $($result.html_url)" \ No newline at end of file diff --git a/data/leads.xlsx b/data/leads.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..989e6a593b2de52b5980f390bc7b1a22c802ea0e GIT binary patch literal 5201 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 Dm_z#d literal 0 HcmV?d00001 diff --git a/docs/developer_guide.md b/docs/developer_guide.md new file mode 100644 index 0000000..2ea7e17 --- /dev/null +++ b/docs/developer_guide.md @@ -0,0 +1,30 @@ +# Документация разработчика + +## Архитектура +Проект построен на **конечном автомате (FSM)**. Диалог управляется классом `DialogManager`. +Состояния: `COLLECT_FI` → `COLLECT_PHONE` → `COLLECT_TIME` → `CONFIRM` → `FINISHED`. +Поддерживаются возражения и перезапуск. + +**Компоненты:** +- `core/fsm.py` – логика диалога. +- `core/validator.py` – валидация ФИО, телефона, парсинг времени. +- `core/exporter.py` – работа с Excel (атомарная запись, бэкапы). +- `services/vk_bot.py` – интеграция с VK API, обработка команд. +- `utils/middleware.py` – мидлвары (логирование, аналитика). + +## Добавление нового поля в анкету +1. Добавить текст в `config/phrases.py`. +2. Расширить модель `LeadData` в `core/models.py`. +3. Добавить состояние в `DialogState` и методы в `DialogManager`. +4. Добавить валидатор в `core/validator.py`. +5. Обновить `ExcelExporter._init_file` и метод сохранения. + +## Тестирование +Установите тестовые зависимости: `pip install pytest pytest-asyncio responses`. +Запуск: `pytest tests/`. +Пример теста для валидатора: +```python +def test_validate_fio(): + from core.validator import validate_fio + assert validate_fio("Иванов Иван Иванович").is_valid + assert not validate_fio("John Doe").is_valid \ No newline at end of file diff --git a/docs/user_guide.md b/docs/user_guide.md new file mode 100644 index 0000000..24546b8 --- /dev/null +++ b/docs/user_guide.md @@ -0,0 +1,22 @@ +# Руководство оператора "Сотрудник первой линии" + +## Быстрый старт +1. Убедитесь, что установлен Python 3.11+ и все зависимости (`pip install -r requirements.txt`). +2. Скопируйте `.env.example` в `.env` и укажите токен группы VK. +3. Запустите бота: `python main.py` +4. Проверьте статус: напишите боту `/status` (только для администраторов, указанных в ADMIN_IDS). + +## Управление +- `/status` – проверить, работает ли бот. +- `/export` – создать копию файла лидов с меткой времени. +- `/backup` – вручную создать резервную копию. +- `/stats` – показать статистику (всего лидов, за сегодня, по статусам). +- `/reload` – перезагрузить текстовые фразы без перезапуска. + +## Просмотр данных +Файл с лидами находится в `data/leads.xlsx`. Резервные копии – в `data/backups/`. + +## Решение проблем +- Бот не отвечает: проверьте токен и права доступа группы (нужны `messages` и `groups`). +- Ошибка записи в файл: убедитесь, что папка `data` существует и доступна для записи. +- Бот не видит сообщения: группа должна быть публичной или добавьте бота в беседу. \ No newline at end of file diff --git a/logs/bot.log b/logs/bot.log new file mode 100644 index 0000000..8568474 --- /dev/null +++ b/logs/bot.log @@ -0,0 +1,590 @@ +2026-05-04T13:22:21.443454+0300 | INFO | Initializing bot... +2026-05-04T13:22:21.685842+0300 | INFO | Bot started +2026-05-04T13:22:21.686877+0300 | INFO | Backup scheduler started +2026-05-04T13:23:07.722613+0300 | DEBUG | Message from 25076348 in peer 2000000002: [club233127658|@ikpro] 🔄 Начать заново +2026-05-04T13:23:07.724180+0300 | INFO | Processed message from 25076348: [club233127658|@ikpro] 🔄 Начать заново +2026-05-04T13:23:07.724696+0300 | INFO | === HANDLE: user 25076348, text='[club233127658|@ikpro] 🔄 Начать заново', current state=None === +2026-05-04T13:23:07.727877+0300 | INFO | Response to user 25076348: Пожалуйста, подтвердите согласие на обработку персональных данных (Да / Нет): +2026-05-04T13:23:11.904304+0300 | DEBUG | Message from 25076348 in peer 2000000002: да +2026-05-04T13:23:11.904304+0300 | INFO | Processed message from 25076348: да +2026-05-04T13:23:11.904840+0300 | INFO | === HANDLE: user 25076348, text='да', current state=ask_consent === +2026-05-04T13:23:11.904840+0300 | INFO | Response to user 25076348: ✅ Спасибо за согласие! Продолжим оформление заявки. +Пожалуйста, укажите вашу фамилию и имя (два слов +2026-05-04T13:23:34.586781+0300 | DEBUG | Message from 25076348 in peer 2000000002: ффф ыавпва +2026-05-04T13:23:34.586781+0300 | INFO | Processed message from 25076348: ффф ыавпва +2026-05-04T13:23:34.587312+0300 | INFO | === HANDLE: user 25076348, text='ффф ыавпва', current state=collect_fi === +2026-05-04T13:23:34.588357+0300 | INFO | Response to user 25076348: Укажите ваш возраст (целое число лет): +2026-05-04T13:23:37.967348+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12 +2026-05-04T13:23:37.967876+0300 | INFO | Processed message from 25076348: 12 +2026-05-04T13:23:37.967876+0300 | INFO | === HANDLE: user 25076348, text='12', current state=collect_age === +2026-05-04T13:23:37.967876+0300 | DEBUG | validate_age input: ''12'' +2026-05-04T13:23:37.968411+0300 | DEBUG | cleaned: '12' +2026-05-04T13:23:37.968411+0300 | DEBUG | age valid: 12 +2026-05-04T13:23:37.969451+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-04T13:36:16.863923+0300 | INFO | Initializing bot... +2026-05-04T13:36:17.084594+0300 | INFO | Bot started +2026-05-04T13:36:17.085630+0300 | INFO | Backup scheduler started +2026-05-04T13:37:06.672828+0300 | DEBUG | Message from 25076348 in peer 2000000002: Привет +2026-05-04T13:37:06.673343+0300 | INFO | Processed message from 25076348: Привет +2026-05-04T13:37:06.673343+0300 | INFO | === HANDLE: user 25076348, text='Привет', current state=None === +2026-05-04T13:37:06.673883+0300 | INFO | Response to user 25076348: Пожалуйста, подтвердите согласие на обработку персональных данных (Да / Нет): +2026-05-04T13:37:09.159946+0300 | DEBUG | Message from 25076348 in peer 2000000002: да +2026-05-04T13:37:09.159946+0300 | INFO | Processed message from 25076348: да +2026-05-04T13:37:09.160473+0300 | INFO | === HANDLE: user 25076348, text='да', current state=ask_consent === +2026-05-04T13:37:09.160473+0300 | INFO | Response to user 25076348: ✅ Спасибо за согласие! Продолжим оформление заявки. +Пожалуйста, укажите вашу фамилию и имя (два слов +2026-05-04T13:37:15.495991+0300 | DEBUG | Message from 25076348 in peer 2000000002: ааа ррр +2026-05-04T13:37:15.496513+0300 | INFO | Processed message from 25076348: ааа ррр +2026-05-04T13:37:15.497027+0300 | INFO | === HANDLE: user 25076348, text='ааа ррр', current state=collect_fi === +2026-05-04T13:37:15.497027+0300 | INFO | Response to user 25076348: Укажите ваш возраст (целое число лет): +2026-05-04T13:37:18.753724+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12 +2026-05-04T13:37:18.753724+0300 | INFO | Processed message from 25076348: 12 +2026-05-04T13:37:18.754244+0300 | INFO | === HANDLE: user 25076348, text='12', current state=collect_age === +2026-05-04T13:37:18.754244+0300 | DEBUG | validate_age input: ''12'' +2026-05-04T13:37:18.754244+0300 | DEBUG | cleaned: '12' +2026-05-04T13:37:18.754244+0300 | DEBUG | age valid: 12 +2026-05-04T13:37:18.754756+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-04T13:37:22.075536+0300 | DEBUG | Message from 25076348 in peer 2000000002: 23 +2026-05-04T13:37:22.076047+0300 | INFO | Processed message from 25076348: 23 +2026-05-04T13:37:22.076047+0300 | INFO | === HANDLE: user 25076348, text='23', current state=collect_age === +2026-05-04T13:37:22.076047+0300 | DEBUG | validate_age input: ''23'' +2026-05-04T13:37:22.076566+0300 | DEBUG | cleaned: '23' +2026-05-04T13:37:22.076566+0300 | DEBUG | age valid: 23 +2026-05-04T13:37:22.076566+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-04T14:09:25.567877+0300 | INFO | Initializing bot... +2026-05-04T14:09:25.744659+0300 | INFO | Bot started +2026-05-04T14:09:25.745178+0300 | INFO | Backup scheduler started +2026-05-04T14:09:34.624213+0300 | DEBUG | Message from 25076348 in peer 2000000002: Привет +2026-05-04T14:09:34.624213+0300 | INFO | Processed message from 25076348: Привет +2026-05-04T14:09:34.624213+0300 | INFO | === HANDLE: user 25076348, text='Привет', current state=None === +2026-05-04T14:09:34.624740+0300 | INFO | Response to user 25076348: Пожалуйста, подтвердите согласие на обработку персональных данных (Да / Нет): +2026-05-04T14:09:36.950764+0300 | DEBUG | Message from 25076348 in peer 2000000002: да +2026-05-04T14:09:36.950764+0300 | INFO | Processed message from 25076348: да +2026-05-04T14:09:36.951300+0300 | INFO | === HANDLE: user 25076348, text='да', current state=ask_consent === +2026-05-04T14:09:36.951300+0300 | INFO | Response to user 25076348: ✅ Спасибо за согласие! Продолжим оформление заявки. +Пожалуйста, укажите вашу фамилию и имя (два слов +2026-05-04T14:09:45.685252+0300 | DEBUG | Message from 25076348 in peer 2000000002: вав авп +2026-05-04T14:09:45.685252+0300 | INFO | Processed message from 25076348: вав авп +2026-05-04T14:09:45.685252+0300 | INFO | === HANDLE: user 25076348, text='вав авп', current state=collect_fi === +2026-05-04T14:09:45.685773+0300 | INFO | Response to user 25076348: Укажите ваш возраст (целое число лет): +2026-05-04T14:09:49.196572+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12 +2026-05-04T14:09:49.197077+0300 | INFO | Processed message from 25076348: 12 +2026-05-04T14:09:49.197077+0300 | INFO | === HANDLE: user 25076348, text='12', current state=collect_age === +2026-05-04T14:09:49.197077+0300 | DEBUG | validate_age input: ''12'' +2026-05-04T14:09:49.197077+0300 | DEBUG | cleaned: '12' +2026-05-04T14:09:49.197077+0300 | DEBUG | age valid: 12 +2026-05-04T14:09:49.197077+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-04T14:09:53.320692+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12лет +2026-05-04T14:09:53.320692+0300 | INFO | Processed message from 25076348: 12лет +2026-05-04T14:09:53.320692+0300 | INFO | === HANDLE: user 25076348, text='12лет', current state=collect_age === +2026-05-04T14:09:53.321204+0300 | DEBUG | validate_age input: ''12лет'' +2026-05-04T14:09:53.321204+0300 | DEBUG | cleaned: '12' +2026-05-04T14:09:53.321204+0300 | DEBUG | age valid: 12 +2026-05-04T14:09:53.321724+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-04T14:10:19.043522+0300 | DEBUG | Message from 25076348 in peer 2000000002: 13 +2026-05-04T14:10:19.043522+0300 | INFO | Processed message from 25076348: 13 +2026-05-04T14:10:19.043522+0300 | INFO | === HANDLE: user 25076348, text='13', current state=collect_age === +2026-05-04T14:10:19.044052+0300 | DEBUG | validate_age input: ''13'' +2026-05-04T14:10:19.044052+0300 | DEBUG | cleaned: '13' +2026-05-04T14:10:19.044052+0300 | DEBUG | age valid: 13 +2026-05-04T14:10:19.044052+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-04T14:10:22.029805+0300 | DEBUG | Message from 25076348 in peer 2000000002: 18 +2026-05-04T14:10:22.029805+0300 | INFO | Processed message from 25076348: 18 +2026-05-04T14:10:22.029805+0300 | INFO | === HANDLE: user 25076348, text='18', current state=collect_age === +2026-05-04T14:10:22.030324+0300 | DEBUG | validate_age input: ''18'' +2026-05-04T14:10:22.030324+0300 | DEBUG | cleaned: '18' +2026-05-04T14:10:22.030324+0300 | DEBUG | age valid: 18 +2026-05-04T14:10:22.030324+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-04T14:52:44.897174+0300 | INFO | Initializing bot... +2026-05-04T14:52:45.151496+0300 | INFO | Bot started +2026-05-04T14:52:45.152530+0300 | INFO | Backup scheduler started +2026-05-04T14:53:41.300176+0300 | DEBUG | Message from 25076348 in peer 2000000002: привет +2026-05-04T14:53:41.302249+0300 | INFO | Processed message from 25076348: привет +2026-05-04T14:53:41.302249+0300 | INFO | === HANDLE: user 25076348, text='привет', current state=None === +2026-05-04T14:53:41.306431+0300 | INFO | Response to user 25076348: Пожалуйста, подтвердите согласие на обработку персональных данных (Да / Нет): +2026-05-04T14:53:43.736066+0300 | DEBUG | Message from 25076348 in peer 2000000002: да +2026-05-04T14:53:43.736066+0300 | INFO | Processed message from 25076348: да +2026-05-04T14:53:43.736066+0300 | INFO | === HANDLE: user 25076348, text='да', current state=ask_consent === +2026-05-04T14:53:43.736606+0300 | INFO | Response to user 25076348: ✅ Спасибо за согласие! Продолжим оформление заявки. +Пожалуйста, укажите вашу фамилию и имя (два слов +2026-05-04T14:53:49.142783+0300 | DEBUG | Message from 25076348 in peer 2000000002: ппп ррр +2026-05-04T14:53:49.142783+0300 | INFO | Processed message from 25076348: ппп ррр +2026-05-04T14:53:49.143321+0300 | INFO | === HANDLE: user 25076348, text='ппп ррр', current state=collect_fi === +2026-05-04T14:53:49.152638+0300 | INFO | Response to user 25076348: Укажите ваш возраст (целое число лет): +2026-05-04T14:53:51.561650+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12 +2026-05-04T14:53:51.561650+0300 | INFO | Processed message from 25076348: 12 +2026-05-04T14:53:51.562169+0300 | INFO | === HANDLE: user 25076348, text='12', current state=collect_age === +2026-05-04T14:53:51.562169+0300 | DEBUG | validate_age input: ''12'' +2026-05-04T14:53:51.562169+0300 | DEBUG | cleaned: '12' +2026-05-04T14:53:51.562710+0300 | DEBUG | age valid: 12 +2026-05-04T14:53:51.563228+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-04T14:53:57.436097+0300 | DEBUG | Message from 25076348 in peer 2000000002: 13 +2026-05-04T14:53:57.436097+0300 | INFO | Processed message from 25076348: 13 +2026-05-04T14:53:57.436624+0300 | INFO | === HANDLE: user 25076348, text='13', current state=collect_age === +2026-05-04T14:53:57.436624+0300 | DEBUG | validate_age input: ''13'' +2026-05-04T14:53:57.436624+0300 | DEBUG | cleaned: '13' +2026-05-04T14:53:57.436624+0300 | DEBUG | age valid: 13 +2026-05-04T14:53:57.437143+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-04T14:54:17.932582+0300 | ERROR | Fatal error: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response')) +Traceback (most recent call last): + + File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\urllib3\connectionpool.py", line 787, in urlopen + response = self._make_request( + │ └ + └ + File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\urllib3\connectionpool.py", line 534, in _make_request + response = conn.getresponse() + │ └ + └ + File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\urllib3\connection.py", line 571, in getresponse + httplib_response = super().getresponse() + File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\http\client.py", line 1374, in getresponse + response.begin() + │ └ + └ + File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\http\client.py", line 318, in begin + version, status, reason = self._read_status() + │ └ + └ + File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\http\client.py", line 287, in _read_status + raise RemoteDisconnected("Remote end closed connection without" + └ + +http.client.RemoteDisconnected: Remote end closed connection without response + + +During handling of the above exception, another exception occurred: + + +Traceback (most recent call last): + + File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\requests\adapters.py", line 645, in send + resp = conn.urlopen( + │ └ + └ + File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\urllib3\connectionpool.py", line 841, in urlopen + retries = retries.increment( + │ └ + └ Retry(total=0, connect=None, read=False, redirect=None, status=None) + File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\urllib3\util\retry.py", line 490, in increment + raise reraise(type(error), error, _stacktrace) + │ │ │ └ + │ │ └ ProtocolError('Connection aborted.', RemoteDisconnected('Remote end closed connection without response')) + │ └ ProtocolError('Connection aborted.', RemoteDisconnected('Remote end closed connection without response')) + └ + File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\urllib3\util\util.py", line 38, in reraise + raise value.with_traceback(tb) + │ └ None + └ None + File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\urllib3\connectionpool.py", line 787, in urlopen + response = self._make_request( + │ └ + └ + File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\urllib3\connectionpool.py", line 534, in _make_request + response = conn.getresponse() + │ └ + └ + File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\urllib3\connection.py", line 571, in getresponse + httplib_response = super().getresponse() + File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\http\client.py", line 1374, in getresponse + response.begin() + │ └ + └ + File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\http\client.py", line 318, in begin + version, status, reason = self._read_status() + │ └ + └ + File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\http\client.py", line 287, in _read_status + raise RemoteDisconnected("Remote end closed connection without" + └ + +urllib3.exceptions.ProtocolError: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response')) + + +During handling of the above exception, another exception occurred: + + +Traceback (most recent call last): + + File "m:\bot_vk_ikp\vk-sales-bot\main.py", line 28, in + main() + └ + +> File "m:\bot_vk_ikp\vk-sales-bot\main.py", line 20, in main + bot.run() + │ └ + └ + + File "m:\bot_vk_ikp\vk-sales-bot\services\vk_bot.py", line 90, in run + for event in self.longpoll.listen(): + │ │ │ └ + │ │ └ + │ └ + └ <({'group_id': 233127658, 'type': 'message_new', 'event_id': 'a8c630d5264c1fc3... + + File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\vk_api\bot_longpoll.py", line 286, in listen + for event in self.check(): + │ │ └ + │ └ + └ <({'group_id': 233127658, 'type': 'message_new', 'event_id': 'a8c630d5264c1fc3... + File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\vk_api\bot_longpoll.py", line 255, in check + response = self.session.get( + │ └ + └ + File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\requests\sessions.py", line 605, in get + return self.request("GET", url, **kwargs) + │ │ │ └ {'params': {'act': 'a_check', 'key': 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJxdWV1ZV9pZCI6IjIzMzEyNzY1OCIsInVudGlsIjoxNzc3OD... + │ │ └ 'https://lp.vk.com/whp/233127658' + │ └ + └ + File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\requests\sessions.py", line 592, in request + resp = self.send(prep, **send_kwargs) + │ │ │ └ {'timeout': 35, 'allow_redirects': True, 'proxies': OrderedDict(), 'stream': False, 'verify': True, 'cert': None} + │ │ └ + │ └ + └ + File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\requests\sessions.py", line 706, in send + r = adapter.send(request, **kwargs) + │ │ │ └ {'timeout': 35, 'proxies': OrderedDict(), 'stream': False, 'verify': True, 'cert': None} + │ │ └ + │ └ + └ + File "C:\Users\User\AppData\Local\Programs\Python\Python311\Lib\site-packages\requests\adapters.py", line 660, in send + raise ConnectionError(err, request=request) + │ └ + └ + +requests.exceptions.ConnectionError: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response')) +2026-05-04T15:32:36.840163+0300 | INFO | Initializing bot... +2026-05-04T15:32:37.085881+0300 | INFO | Bot started +2026-05-04T15:32:37.086882+0300 | INFO | Backup scheduler started +2026-05-04T15:32:43.889888+0300 | DEBUG | Message from 25076348 in peer 2000000002: привет +2026-05-04T15:32:43.891134+0300 | INFO | Processed message from 25076348: привет +2026-05-04T15:32:43.891134+0300 | INFO | === HANDLE: user 25076348, text='привет', current state=None === +2026-05-04T15:32:43.894134+0300 | INFO | Response to user 25076348: Пожалуйста, подтвердите согласие на обработку персональных данных (Да / Нет): +2026-05-04T15:32:47.508547+0300 | DEBUG | Message from 25076348 in peer 2000000002: да +2026-05-04T15:32:47.508547+0300 | INFO | Processed message from 25076348: да +2026-05-04T15:32:47.508547+0300 | INFO | === HANDLE: user 25076348, text='да', current state=ask_consent === +2026-05-04T15:32:47.508547+0300 | INFO | Response to user 25076348: ✅ Спасибо за согласие! Продолжим оформление заявки. +Пожалуйста, укажите вашу фамилию и имя (два слов +2026-05-04T15:32:55.451299+0300 | DEBUG | Message from 25076348 in peer 2000000002: ааа ппп +2026-05-04T15:32:55.451299+0300 | INFO | Processed message from 25076348: ааа ппп +2026-05-04T15:32:55.451299+0300 | INFO | === HANDLE: user 25076348, text='ааа ппп', current state=collect_fi === +2026-05-04T15:32:55.453302+0300 | INFO | Response to user 25076348: Укажите ваш возраст (целое число лет): +2026-05-04T15:32:59.095014+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12 +2026-05-04T15:32:59.095014+0300 | INFO | Processed message from 25076348: 12 +2026-05-04T15:32:59.095014+0300 | INFO | === HANDLE: user 25076348, text='12', current state=collect_age === +2026-05-04T15:32:59.095014+0300 | DEBUG | validate_age input: ''12'' +2026-05-04T15:32:59.096013+0300 | DEBUG | cleaned: '12' +2026-05-04T15:32:59.096013+0300 | DEBUG | age valid: 12 +2026-05-04T15:32:59.098012+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-04T15:33:02.913004+0300 | DEBUG | Message from 25076348 in peer 2000000002: 23 +2026-05-04T15:33:02.914007+0300 | INFO | Processed message from 25076348: 23 +2026-05-04T15:33:02.914007+0300 | INFO | === HANDLE: user 25076348, text='23', current state=collect_age === +2026-05-04T15:33:02.914007+0300 | DEBUG | validate_age input: ''23'' +2026-05-04T15:33:02.914007+0300 | DEBUG | cleaned: '23' +2026-05-04T15:33:02.914007+0300 | DEBUG | age valid: 23 +2026-05-04T15:33:02.915005+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-04T18:09:52.845335+0300 | INFO | Initializing bot... +2026-05-04T18:09:53.046373+0300 | INFO | Bot started +2026-05-04T18:09:53.047373+0300 | INFO | Backup scheduler started +2026-05-04T18:10:10.241270+0300 | DEBUG | Message from 25076348 in peer 2000000002: привет +2026-05-04T18:10:10.242269+0300 | INFO | Processed message from 25076348: привет +2026-05-04T18:10:10.242269+0300 | INFO | === HANDLE: user 25076348, text='привет', current state=None === +2026-05-04T18:10:10.245270+0300 | INFO | Response to user 25076348: Пожалуйста, подтвердите согласие на обработку персональных данных (Да / Нет): +2026-05-04T18:10:13.272789+0300 | DEBUG | Message from 25076348 in peer 2000000002: да +2026-05-04T18:10:13.273787+0300 | INFO | Processed message from 25076348: да +2026-05-04T18:10:13.273787+0300 | INFO | === HANDLE: user 25076348, text='да', current state=ask_consent === +2026-05-04T18:10:13.273787+0300 | INFO | Response to user 25076348: ✅ Спасибо за согласие! Продолжим оформление заявки. +Пожалуйста, укажите вашу фамилию и имя (два слов +2026-05-04T18:10:18.694847+0300 | DEBUG | Message from 25076348 in peer 2000000002: аа рп +2026-05-04T18:10:18.694847+0300 | INFO | Processed message from 25076348: аа рп +2026-05-04T18:10:18.694847+0300 | INFO | === HANDLE: user 25076348, text='аа рп', current state=collect_fi === +2026-05-04T18:10:18.696350+0300 | INFO | Response to user 25076348: Укажите ваш возраст (целое число лет): +2026-05-04T18:10:21.671256+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12 +2026-05-04T18:10:21.671256+0300 | INFO | Processed message from 25076348: 12 +2026-05-04T18:10:21.672255+0300 | INFO | === HANDLE: user 25076348, text='12', current state=collect_age === +2026-05-04T18:10:21.672255+0300 | DEBUG | validate_age input: ''12'' +2026-05-04T18:10:21.672255+0300 | DEBUG | cleaned: '12' +2026-05-04T18:10:21.672255+0300 | DEBUG | age valid: 12 +2026-05-04T18:10:21.672255+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-04T18:12:55.999161+0300 | INFO | Initializing bot... +2026-05-04T18:12:56.620865+0300 | INFO | Bot started +2026-05-04T18:12:56.620865+0300 | INFO | Backup scheduler started +2026-05-04T18:13:16.401304+0300 | DEBUG | Message from 25076348 in peer 2000000002: 5 +2026-05-04T18:13:16.402310+0300 | INFO | Processed message from 25076348: 5 +2026-05-04T18:13:16.402310+0300 | INFO | === HANDLE: user 25076348, text='5', current state=None === +2026-05-04T18:13:16.402310+0300 | INFO | Response to user 25076348: Пожалуйста, подтвердите согласие на обработку персональных данных (Да / Нет): +2026-05-04T18:13:19.498303+0300 | DEBUG | Message from 25076348 in peer 2000000002: всыв +2026-05-04T18:13:19.498303+0300 | INFO | Processed message from 25076348: всыв +2026-05-04T18:13:19.498303+0300 | INFO | === HANDLE: user 25076348, text='всыв', current state=ask_consent === +2026-05-04T18:13:19.498303+0300 | INFO | Response to user 25076348: Пожалуйста, ответьте 'Да' (согласен) или 'Нет' (не согласен). +2026-05-04T18:13:22.869492+0300 | DEBUG | Message from 25076348 in peer 2000000002: да +2026-05-04T18:13:22.869492+0300 | INFO | Processed message from 25076348: да +2026-05-04T18:13:22.870492+0300 | INFO | === HANDLE: user 25076348, text='да', current state=ask_consent === +2026-05-04T18:13:22.870492+0300 | INFO | Response to user 25076348: ✅ Спасибо за согласие! Продолжим оформление заявки. +Пожалуйста, укажите вашу фамилию и имя (два слов +2026-05-04T18:13:30.113454+0300 | DEBUG | Message from 25076348 in peer 2000000002: пав пав +2026-05-04T18:13:30.113454+0300 | INFO | Processed message from 25076348: пав пав +2026-05-04T18:13:30.113454+0300 | INFO | === HANDLE: user 25076348, text='пав пав', current state=collect_fi === +2026-05-04T18:13:30.114454+0300 | INFO | Response to user 25076348: Укажите ваш возраст (целое число лет): +2026-05-04T18:13:32.914351+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12 +2026-05-04T18:13:32.915351+0300 | INFO | Processed message from 25076348: 12 +2026-05-04T18:13:32.915351+0300 | INFO | === HANDLE: user 25076348, text='12', current state=collect_age === +2026-05-04T18:13:32.915351+0300 | DEBUG | validate_age input: ''12'' +2026-05-04T18:13:32.915351+0300 | DEBUG | cleaned: '12' +2026-05-04T18:13:32.915351+0300 | DEBUG | age valid: 12 +2026-05-04T18:13:32.915351+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-04T18:13:36.111171+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12 +2026-05-04T18:13:36.111171+0300 | INFO | Processed message from 25076348: 12 +2026-05-04T18:13:36.111171+0300 | INFO | === HANDLE: user 25076348, text='12', current state=collect_age === +2026-05-04T18:13:36.112171+0300 | DEBUG | validate_age input: ''12'' +2026-05-04T18:13:36.112171+0300 | DEBUG | cleaned: '12' +2026-05-04T18:13:36.112171+0300 | DEBUG | age valid: 12 +2026-05-04T18:13:36.112171+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-04T18:13:43.488167+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12лет +2026-05-04T18:13:43.488167+0300 | INFO | Processed message from 25076348: 12лет +2026-05-04T18:13:43.488167+0300 | INFO | === HANDLE: user 25076348, text='12лет', current state=collect_age === +2026-05-04T18:13:43.489166+0300 | DEBUG | validate_age input: ''12лет'' +2026-05-04T18:13:43.489166+0300 | DEBUG | cleaned: '12' +2026-05-04T18:13:43.489166+0300 | DEBUG | age valid: 12 +2026-05-04T18:13:43.489166+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-04T18:13:48.007000+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12 лет +2026-05-04T18:13:48.007000+0300 | INFO | Processed message from 25076348: 12 лет +2026-05-04T18:13:48.007000+0300 | INFO | === HANDLE: user 25076348, text='12 лет', current state=collect_age === +2026-05-04T18:13:48.008153+0300 | DEBUG | validate_age input: ''12 лет'' +2026-05-04T18:13:48.008153+0300 | DEBUG | cleaned: '12' +2026-05-04T18:13:48.008153+0300 | DEBUG | age valid: 12 +2026-05-04T18:13:48.008153+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-04T18:13:51.898194+0300 | DEBUG | Message from 25076348 in peer 2000000002: 1 +2026-05-04T18:13:51.898194+0300 | INFO | Processed message from 25076348: 1 +2026-05-04T18:13:51.899194+0300 | INFO | === HANDLE: user 25076348, text='1', current state=collect_age === +2026-05-04T18:13:51.899194+0300 | DEBUG | validate_age input: ''1'' +2026-05-04T18:13:51.899194+0300 | DEBUG | cleaned: '1' +2026-05-04T18:13:51.899194+0300 | DEBUG | age valid: 1 +2026-05-04T18:13:51.899194+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-04T18:13:55.621492+0300 | DEBUG | Message from 25076348 in peer 2000000002: 120 +2026-05-04T18:13:55.622492+0300 | INFO | Processed message from 25076348: 120 +2026-05-04T18:13:55.622492+0300 | INFO | === HANDLE: user 25076348, text='120', current state=collect_age === +2026-05-04T18:13:55.622492+0300 | DEBUG | validate_age input: ''120'' +2026-05-04T18:13:55.622492+0300 | DEBUG | cleaned: '120' +2026-05-04T18:13:55.622492+0300 | DEBUG | age valid: 120 +2026-05-04T18:13:55.622492+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-04T18:14:00.545873+0300 | DEBUG | Message from 25076348 in peer 2000000002: 158 +2026-05-04T18:14:00.545873+0300 | INFO | Processed message from 25076348: 158 +2026-05-04T18:14:00.545873+0300 | INFO | === HANDLE: user 25076348, text='158', current state=collect_age === +2026-05-04T18:14:00.545873+0300 | DEBUG | validate_age input: ''158'' +2026-05-04T18:14:00.545873+0300 | DEBUG | cleaned: '158' +2026-05-04T18:14:00.546870+0300 | INFO | Response to user 25076348: Возраст должен быть от 1 до 120 лет. +Укажите ваш возраст (целое число лет): +2026-05-04T18:14:29.195477+0300 | DEBUG | Message from 25076348 in peer 2000000002: 11 +2026-05-04T18:14:29.195477+0300 | INFO | Processed message from 25076348: 11 +2026-05-04T18:14:29.196478+0300 | INFO | === HANDLE: user 25076348, text='11', current state=collect_age === +2026-05-04T18:14:29.196478+0300 | DEBUG | validate_age input: ''11'' +2026-05-04T18:14:29.196478+0300 | DEBUG | cleaned: '11' +2026-05-04T18:14:29.196478+0300 | DEBUG | age valid: 11 +2026-05-04T18:14:29.196478+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-04T21:42:30.942354+0300 | INFO | Initializing bot... +2026-05-04T21:42:31.199461+0300 | INFO | Bot started +2026-05-04T21:42:31.200460+0300 | INFO | Backup scheduler started +2026-05-04T21:43:03.630075+0300 | INFO | Initializing bot... +2026-05-04T21:43:04.103937+0300 | INFO | Bot started +2026-05-04T21:43:04.104937+0300 | INFO | Backup scheduler started +2026-05-04T21:43:32.010181+0300 | DEBUG | Message from 25076348 in peer 2000000002: привет +2026-05-04T21:43:32.012181+0300 | INFO | Processed message from 25076348: привет +2026-05-04T21:43:32.012181+0300 | INFO | === HANDLE: user 25076348, text='привет', current state=None === +2026-05-04T21:43:32.015181+0300 | INFO | Response to user 25076348: Пожалуйста, подтвердите согласие на обработку персональных данных (Да / Нет): +2026-05-04T21:43:34.178176+0300 | DEBUG | Message from 25076348 in peer 2000000002: да +2026-05-04T21:43:34.178176+0300 | INFO | Processed message from 25076348: да +2026-05-04T21:43:34.178176+0300 | INFO | === HANDLE: user 25076348, text='да', current state=ask_consent === +2026-05-04T21:43:34.178176+0300 | INFO | Response to user 25076348: ✅ Спасибо за согласие! Продолжим оформление заявки. +Пожалуйста, укажите вашу фамилию и имя (два слов +2026-05-04T21:43:40.704112+0300 | DEBUG | Message from 25076348 in peer 2000000002: ааа ппп +2026-05-04T21:43:40.704112+0300 | INFO | Processed message from 25076348: ааа ппп +2026-05-04T21:43:40.704112+0300 | INFO | === HANDLE: user 25076348, text='ааа ппп', current state=collect_fi === +2026-05-04T21:43:40.706111+0300 | INFO | Response to user 25076348: Укажите ваш возраст (целое число лет): +2026-05-04T21:43:43.405452+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12 +2026-05-04T21:43:43.405452+0300 | INFO | Processed message from 25076348: 12 +2026-05-04T21:43:43.406451+0300 | INFO | === HANDLE: user 25076348, text='12', current state=collect_age === +2026-05-04T21:43:43.406451+0300 | DEBUG | validate_age input: ''12'' +2026-05-04T21:43:43.406451+0300 | DEBUG | cleaned: '12' +2026-05-04T21:43:43.406451+0300 | DEBUG | age valid: 12 +2026-05-04T21:43:43.407452+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-04T21:43:53.242075+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12 +2026-05-04T21:43:53.243076+0300 | INFO | Processed message from 25076348: 12 +2026-05-04T21:43:53.243076+0300 | INFO | === HANDLE: user 25076348, text='12', current state=collect_age === +2026-05-04T21:43:53.243076+0300 | DEBUG | validate_age input: ''12'' +2026-05-04T21:43:53.243076+0300 | DEBUG | cleaned: '12' +2026-05-04T21:43:53.243076+0300 | DEBUG | age valid: 12 +2026-05-04T21:43:53.243076+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-04T21:44:00.659922+0300 | DEBUG | Message from 25076348 in peer 2000000002: "12" +2026-05-04T21:44:00.659922+0300 | INFO | Processed message from 25076348: "12" +2026-05-04T21:44:00.659922+0300 | INFO | === HANDLE: user 25076348, text='"12"', current state=collect_age === +2026-05-04T21:44:00.659922+0300 | DEBUG | validate_age input: ''"12"'' +2026-05-04T21:44:00.659922+0300 | DEBUG | cleaned: '12' +2026-05-04T21:44:00.660921+0300 | DEBUG | age valid: 12 +2026-05-04T21:44:00.660921+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-05T07:00:09.347195+0300 | INFO | Initializing bot... +2026-05-05T07:00:09.569224+0300 | INFO | Bot started +2026-05-05T07:00:09.570223+0300 | INFO | Backup scheduler started +2026-05-05T07:00:20.263695+0300 | INFO | Initializing bot... +2026-05-05T07:00:20.447752+0300 | INFO | Bot started +2026-05-05T07:00:20.447752+0300 | INFO | Backup scheduler started +2026-05-05T07:00:51.798725+0300 | DEBUG | Message from 25076348 in peer 2000000002: привет +2026-05-05T07:00:51.798725+0300 | INFO | Processed message from 25076348: привет +2026-05-05T07:00:51.798725+0300 | INFO | === HANDLE: user 25076348, text='привет', current state=None === +2026-05-05T07:00:51.799725+0300 | INFO | Response to user 25076348: Пожалуйста, подтвердите согласие на обработку персональных данных (Да / Нет): +2026-05-05T07:00:55.300865+0300 | DEBUG | Message from 25076348 in peer 2000000002: да +2026-05-05T07:00:55.300865+0300 | INFO | Processed message from 25076348: да +2026-05-05T07:00:55.301864+0300 | INFO | === HANDLE: user 25076348, text='да', current state=ask_consent === +2026-05-05T07:00:55.301864+0300 | INFO | Response to user 25076348: ✅ Спасибо за согласие! Продолжим оформление заявки. +Пожалуйста, укажите вашу фамилию и имя (два слов +2026-05-05T07:01:00.589119+0300 | DEBUG | Message from 25076348 in peer 2000000002: ааа ппп +2026-05-05T07:01:00.589119+0300 | INFO | Processed message from 25076348: ааа ппп +2026-05-05T07:01:00.590119+0300 | INFO | === HANDLE: user 25076348, text='ааа ппп', current state=collect_fi === +2026-05-05T07:01:00.590119+0300 | INFO | Response to user 25076348: Укажите ваш возраст (целое число лет): +2026-05-05T07:01:03.015859+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12 +2026-05-05T07:01:03.015859+0300 | INFO | Processed message from 25076348: 12 +2026-05-05T07:01:03.015859+0300 | INFO | === HANDLE: user 25076348, text='12', current state=collect_age === +2026-05-05T07:01:03.016362+0300 | DEBUG | validate_age input: ''12'' +2026-05-05T07:01:03.016362+0300 | DEBUG | cleaned: '12' +2026-05-05T07:01:03.016362+0300 | DEBUG | age valid: 12 +2026-05-05T07:01:03.016362+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-05T07:01:04.461068+0300 | DEBUG | Message from 25076348 in peer 2000000002: 12 +2026-05-05T07:01:04.461068+0300 | INFO | Processed message from 25076348: 12 +2026-05-05T07:01:04.461068+0300 | INFO | === HANDLE: user 25076348, text='12', current state=collect_age === +2026-05-05T07:01:04.461068+0300 | DEBUG | validate_age input: ''12'' +2026-05-05T07:01:04.462069+0300 | DEBUG | cleaned: '12' +2026-05-05T07:01:04.462069+0300 | DEBUG | age valid: 12 +2026-05-05T07:01:04.462069+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-05T07:01:06.186293+0300 | DEBUG | Message from 25076348 in peer 2000000002: 31 +2026-05-05T07:01:06.186798+0300 | INFO | Processed message from 25076348: 31 +2026-05-05T07:01:06.186798+0300 | INFO | === HANDLE: user 25076348, text='31', current state=collect_age === +2026-05-05T07:01:06.186798+0300 | DEBUG | validate_age input: ''31'' +2026-05-05T07:01:06.186798+0300 | DEBUG | cleaned: '31' +2026-05-05T07:01:06.186798+0300 | DEBUG | age valid: 31 +2026-05-05T07:01:06.187799+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-05T07:01:08.344400+0300 | DEBUG | Message from 25076348 in peer 2000000002: 41 +2026-05-05T07:01:08.344400+0300 | INFO | Processed message from 25076348: 41 +2026-05-05T07:01:08.344400+0300 | INFO | === HANDLE: user 25076348, text='41', current state=collect_age === +2026-05-05T07:01:08.344400+0300 | DEBUG | validate_age input: ''41'' +2026-05-05T07:01:08.344400+0300 | DEBUG | cleaned: '41' +2026-05-05T07:01:08.344400+0300 | DEBUG | age valid: 41 +2026-05-05T07:01:08.345902+0300 | INFO | Response to user 25076348: Пожалуйста, введите число (ваш возраст). +Укажите ваш возраст (целое число лет): +2026-05-05T08:33:06.946148+0300 | INFO | Initializing bot... +2026-05-05T08:33:07.210217+0300 | INFO | Bot started +2026-05-05T08:33:07.211222+0300 | INFO | Backup scheduler started +2026-05-05T08:33:29.318049+0300 | DEBUG | Message from 25076348 in peer 2000000002: авсыв +2026-05-05T08:33:29.319049+0300 | INFO | Processed message from 25076348: авсыв +2026-05-05T08:33:29.319049+0300 | INFO | === HANDLE: user 25076348, text='авсыв', current state=None === +2026-05-05T08:33:29.321050+0300 | INFO | Response to user 25076348: Пожалуйста, подтвердите согласие на обработку персональных данных (Да / Нет): +2026-05-05T08:33:34.372307+0300 | DEBUG | Message from 25076348 in peer 2000000002: да +2026-05-05T08:33:34.372307+0300 | INFO | Processed message from 25076348: да +2026-05-05T08:33:34.372307+0300 | INFO | === HANDLE: user 25076348, text='да', current state=ask_consent === +2026-05-05T08:33:34.372307+0300 | INFO | Response to user 25076348: ✅ Спасибо за согласие! Продолжим оформление заявки. +Пожалуйста, укажите вашу фамилию и имя (два слов +2026-05-05T08:33:39.200486+0300 | DEBUG | Message from 25076348 in peer 2000000002: аваыв павпва +2026-05-05T08:33:39.200486+0300 | INFO | Processed message from 25076348: аваыв павпва +2026-05-05T08:33:39.200486+0300 | INFO | === HANDLE: user 25076348, text='аваыв павпва', current state=collect_fi === +2026-05-05T08:33:39.201486+0300 | INFO | Response to user 25076348: 👪 Укажите, пожалуйста, контактные данные родителя или опекуна. Вы можете предоставить их сейчас или +2026-05-05T08:34:08.120612+0300 | DEBUG | Message from 25076348 in peer 2000000002: пропустить +2026-05-05T08:34:08.121613+0300 | INFO | Processed message from 25076348: пропустить +2026-05-05T08:34:08.121613+0300 | INFO | === HANDLE: user 25076348, text='пропустить', current state=ask_parent_data === +2026-05-05T08:34:08.121613+0300 | INFO | Parent data skipped for user 25076348 +2026-05-05T08:34:08.121613+0300 | INFO | Response to user 25076348: ✅ Данные родителя пропущены. Продолжаем оформление. +Отлично! Теперь укажите ваш номер телефона в фор +2026-05-05T08:34:34.619625+0300 | DEBUG | Message from 25076348 in peer 2000000002: +7(900)000-00-00 +2026-05-05T08:34:34.619625+0300 | INFO | Processed message from 25076348: +7(900)000-00-00 +2026-05-05T08:34:34.619625+0300 | INFO | === HANDLE: user 25076348, text='+7(900)000-00-00', current state=collect_phone === +2026-05-05T08:34:34.619625+0300 | INFO | Response to user 25076348: Укажите удобное время по МСК для звонка (например: 'завтра после 18', 'в среду утром', 'сегодня в 15 +2026-05-05T08:34:52.867495+0300 | DEBUG | Message from 25076348 in peer 2000000002: сегодня в 15:00 +2026-05-05T08:34:52.867495+0300 | INFO | Processed message from 25076348: сегодня в 15:00 +2026-05-05T08:34:52.867495+0300 | INFO | === HANDLE: user 25076348, text='сегодня в 15:00', current state=collect_time === +2026-05-05T08:34:54.349050+0300 | INFO | Response to user 25076348: Проверьте данные: +ФИ: аваыв павпва +Телефон: +7 (900) 000-00-00 +Время звонка по МСК: 05.05.2026 в 15: +2026-05-05T08:34:58.958675+0300 | DEBUG | Message from 25076348 in peer 2000000002: да +2026-05-05T08:34:58.958675+0300 | INFO | Processed message from 25076348: да +2026-05-05T08:34:58.958675+0300 | INFO | === HANDLE: user 25076348, text='да', current state=collect_time === +2026-05-05T08:34:59.533476+0300 | INFO | Response to user 25076348: Не удалось распознать время. Укажите, например: 'завтра после 18', 'в среду утром' или 'любое время' +2026-05-05T08:35:39.028252+0300 | DEBUG | Message from 25076348 in peer 2000000002: сегодня в 15:00 +2026-05-05T08:35:39.028252+0300 | INFO | Processed message from 25076348: сегодня в 15:00 +2026-05-05T08:35:39.028252+0300 | INFO | === HANDLE: user 25076348, text='сегодня в 15:00', current state=collect_time === +2026-05-05T08:35:39.030760+0300 | INFO | Response to user 25076348: Проверьте данные: +ФИ: аваыв павпва +Телефон: +7 (900) 000-00-00 +Время звонка по МСК: 05.05.2026 в 15: +2026-05-05T08:35:46.969686+0300 | DEBUG | Message from 25076348 in peer 2000000002: Да +2026-05-05T08:35:46.969686+0300 | INFO | Processed message from 25076348: Да +2026-05-05T08:35:46.970686+0300 | INFO | === HANDLE: user 25076348, text='Да', current state=collect_time === +2026-05-05T08:35:46.974190+0300 | INFO | Response to user 25076348: Не удалось распознать время. Укажите, например: 'завтра после 18', 'в среду утром' или 'любое время' +2026-05-05T09:07:22.565746+0300 | INFO | Initializing bot... +2026-05-05T09:07:22.797990+0300 | INFO | Bot started +2026-05-05T09:07:22.798991+0300 | INFO | Backup scheduler started +2026-05-05T09:07:25.017845+0300 | DEBUG | Message from 25076348 in peer 2000000002: авп +2026-05-05T09:07:25.017845+0300 | INFO | Processed message from 25076348: авп +2026-05-05T09:07:25.018845+0300 | INFO | === HANDLE: user 25076348, text='авп', current state=None === +2026-05-05T09:07:25.018845+0300 | INFO | Response to user 25076348: Пожалуйста, подтвердите согласие на обработку персональных данных (Да / Нет): +2026-05-05T09:07:29.353564+0300 | DEBUG | Message from 25076348 in peer 2000000002: да +2026-05-05T09:07:29.354564+0300 | INFO | Processed message from 25076348: да +2026-05-05T09:07:29.354564+0300 | INFO | === HANDLE: user 25076348, text='да', current state=ask_consent === +2026-05-05T09:07:29.354564+0300 | INFO | Response to user 25076348: ✅ Спасибо за согласие! Продолжим оформление заявки. +Пожалуйста, укажите вашу фамилию и имя (два слов +2026-05-05T09:07:36.689785+0300 | DEBUG | Message from 25076348 in peer 2000000002: ваыва авп +2026-05-05T09:07:36.689785+0300 | INFO | Processed message from 25076348: ваыва авп +2026-05-05T09:07:36.689785+0300 | INFO | === HANDLE: user 25076348, text='ваыва авп', current state=collect_fi === +2026-05-05T09:07:36.689785+0300 | INFO | Response to user 25076348: 👪 Укажите, пожалуйста, контактные данные родителя или опекуна. Вы можете предоставить их сейчас или +2026-05-05T09:07:47.065944+0300 | DEBUG | Message from 25076348 in peer 2000000002: пропустить +2026-05-05T09:07:47.065944+0300 | INFO | Processed message from 25076348: пропустить +2026-05-05T09:07:47.065944+0300 | INFO | === HANDLE: user 25076348, text='пропустить', current state=ask_parent_data === +2026-05-05T09:07:47.066947+0300 | INFO | Parent data skipped for user 25076348 +2026-05-05T09:07:47.066947+0300 | INFO | Response to user 25076348: ✅ Данные родителя пропущены. Продолжаем оформление. +Отлично! Теперь укажите ваш номер телефона в фор +2026-05-05T09:08:13.305154+0300 | DEBUG | Message from 25076348 in peer 2000000002: +7(999)000-00-00 +2026-05-05T09:08:13.306155+0300 | INFO | Processed message from 25076348: +7(999)000-00-00 +2026-05-05T09:08:13.306155+0300 | INFO | === HANDLE: user 25076348, text='+7(999)000-00-00', current state=collect_phone === +2026-05-05T09:08:13.306155+0300 | INFO | Response to user 25076348: Укажите удобное время по МСК для звонка (например: 'завтра после 18', 'в среду утром', 'сегодня в 15 +2026-05-05T09:08:26.129071+0300 | DEBUG | Message from 25076348 in peer 2000000002: сегодня в 15:00 +2026-05-05T09:08:26.130574+0300 | INFO | Processed message from 25076348: сегодня в 15:00 +2026-05-05T09:08:26.130574+0300 | INFO | === HANDLE: user 25076348, text='сегодня в 15:00', current state=collect_time === +2026-05-05T09:08:27.594462+0300 | INFO | Response to user 25076348: Проверьте данные: +ФИ: ваыва авп +Телефон: +7 (999) 000-00-00 +Время звонка по МСК: 05.05.2026 в 15:00 + +2026-05-05T09:08:31.676242+0300 | DEBUG | Message from 25076348 in peer 2000000002: [club233127658|@ikpro] ✅ Да +2026-05-05T09:08:31.677239+0300 | INFO | Processed message from 25076348: [club233127658|@ikpro] ✅ Да +2026-05-05T09:08:31.677239+0300 | INFO | === HANDLE: user 25076348, text='[club233127658|@ikpro] ✅ Да', current state=confirm === +2026-05-05T09:08:31.677239+0300 | INFO | Response to user 25076348: Пожалуйста, ответьте 'Да' или 'Нет'. +2026-05-05T09:08:38.235869+0300 | DEBUG | Message from 25076348 in peer 2000000002: да +2026-05-05T09:08:38.235869+0300 | INFO | Processed message from 25076348: да +2026-05-05T09:08:38.236871+0300 | INFO | === HANDLE: user 25076348, text='да', current state=confirm === +2026-05-05T09:08:38.653742+0300 | INFO | Lead saved for user 25076348 +2026-05-05T09:08:38.653742+0300 | INFO | Response to user 25076348: Спасибо! Ваши данные сохранены. Менеджер свяжется с вами в указанное время. Хорошего дня! 🌟 diff --git a/main.py b/main.py new file mode 100644 index 0000000..a6a44f2 --- /dev/null +++ b/main.py @@ -0,0 +1,28 @@ +from utils.logger import logger +from core.exporter import ExcelExporter +from core.fsm import DialogManager +from services.vk_bot import VKBot +from config import settings +from utils.middleware import logging_middleware + + +def main(): + """Точка входа в приложение""" + logger.info("Initializing bot...") + exporter = ExcelExporter(settings.LEADS_FILE) + fsm = DialogManager(exporter) + bot = VKBot(fsm, exporter) + + # Добавляем мидлвары (можно расширить) + bot.middlewares.add(logging_middleware) + + try: + bot.run() + except KeyboardInterrupt: + logger.info("Bot stopped") + except Exception as e: + logger.exception(f"Fatal error: {e}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6b746e8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +vk_api==11.9.9 +pandas==2.2.0 +openpyxl==3.1.2 +python-dotenv==1.0.0 +loguru==0.7.2 +pydantic==2.5.3 +dateparser==1.2.0 +filelock==3.13.1 +schedule==1.2.0 \ No newline at end of file diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/__pycache__/__init__.cpython-311.pyc b/services/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d7eff79edf42666d96dec20a9a7e67e612d7546e GIT binary patch literal 156 zcmZ3^%ge<81n1|!%mmSoK?DpiLK&agfQ;!3DGb33nv8xc8H$*I{LdiCU;4RLF-iF) z@nzZZnb`#~W!bvLi8-moxZ*aiur-W2WCb_#t#fIqKFwN1_0DbB{u*7 literal 0 HcmV?d00001 diff --git a/services/__pycache__/vk_bot.cpython-311.pyc b/services/__pycache__/vk_bot.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..06b1cb781bd343b8529b2d73da53b3f98e1c006d GIT binary patch literal 9024 zcma(%ZEPDycDwvsl1qsaC7Y5cYc0v9SxZWs#OJe<*w6>9$g(I|QY@Q^!_wNdO^F}6 zUD6){l`2kh@Lko5a_4J=o^z)I+%&BnK=EeUnIIylG$qKSDPl^RBW4oS%?V4=8nGsA5gV>s680n&p)hPsIFinYGwF)BaNU-m zlkSKc!}df~vN}?YVJhKC)`2rmy%8^lor$_+eWV`4WMWHXtBL3*Sl3$wOMhS@ zh#T;yJrN&EjuAokm#7*JnpF4k>7I1vXgYOvB%Mg8Rk#{D&!sZsmu5I1a2}r?t2d`#d3Xm zxkTvVOq$PNM7=P1j$^Y4F3QH@iA&MbvB~M|3{X}N#aTANU5N2q@3~k!1t_X}CLZGX zG(TCG4lZnk%2y)*yhm_Egk*_`i6uERYvL?tEUfu0OT@~$Su1B_ZJZr2_P4ANilyEn zA`aG}VOb~VJY$1JvqW4h4J|Zlfu*Wat;YvpiB$6$A*oVaLGnp+i3n&Dp!!#JB$6OU z=7^}#oHIcU@Y<8H>JULp89Xvn3D%&vNqzwRZWw)mk3Lr-Q(f$lBc_V@Qceb3z6pSj zL+@SMcNuSUy+toS0N{pRA)v2IuVfl@h#)pmx%D=dfK@bw;q_TF>1i$%tm12+jjx4F zwVj`i#%AKM#?$a_c_gq(9UqL24-baIsuNma9|`evibrdux}d3lbok|w=s=%JC1BUi zpj~S}%VjVd)A?y04XnD7#^w=%X4N8ai8IAv1;mVDm~T3h2VOm$&O~9H`1H)H=chY` zSb`He0r#rF@#o`{oB-G;)VpRbsZ=x?PsKCQ=xi0<6J2_}3it_UAe$!?@0Kf;1xoQT zP`K|kGS@mjIw5x+6dMmojfdpMLx8Tn=i9N&UQge;DDNATd`D&9QADiNZn^r@dTlUY z87gAG)+|+T!B2zvQr>|fnA`It@dK)TX|HA3Xnfbb$dD+;br4;3FJ4Ldlo8}K-{2PW0po&am zL>p1Mv)c(iJZH&Sri>lVTBflCd{f5QFK{mG`dF+rNB)FhZ8^(Ni1#h!r-39=t!2H8 z-Ok!e$AJD+2^U)>m&+jOoaJ@P1u}!SWZ?l>nK{d2_&ZAYZIY_U?*ehre3H082Ax@? zn#7+CX;5+?qR+$9bjhN#|N42kpYH7x|NxAGH0N`&9jY%K`V2Z9``GZg8B!YydRBB z6>tJNW+_&s(P`92=F!nmEt6@MaO}T$~MZghhJ9_ z9^VFGbJP|yrP{M_e#!g&xedbN9wzU3wp^_eTY6U}AcJ(5)Jn^sStQ4AR{i zt(54HOov1|ROq>3g&x2`K>OwL{uhbc&Yl;|zqfZgdOYS&J#MIc+T7IhZS$w!wgNnf zPbQF}2c2uIPyP$*lZ>H32yl3An##}AoGC}L=EBLI$eET)s4+L3Wei>M1eVZJZox?6 ziMZPI(Mq*YQM=sS#5MI@klcEn;GaxtL*tb3H?-Sk{!b*eVVldi8ai<^*VOJO;%iv< zGQNg2-^@1$8a!yrz5=T#{s^n6TcCMXrFrR1@s@P?5o$?#6Vw+2+6$?;RnTv%NRLTx zfZ|(a0Jw~G9M#$hgB*Z>hH#l&O#&fQym2u2&xL;XlQJ_dvsiNjDhw`rN z8-(3)l2qurb-E=_xBR_bW)6Jzl-v^$=@yB8MW$a7=~v2{_kY$Rcb~+~5t#-$loWTZ zxTQmMcYx}u*@L5@9Tau~|v@p6FjeEdpSpI{{nMN*I;Tqc2`P#!PAoCUdbtiAaCfSoi8>nOuG zfyPyedEYi?eZ1_sCy|}jr7Fli!qTie;hwYMvTDwrvn`i=TItlVOYBpIK`b|Su%0Dt z8MLnpPq~}SR=oiD$IH)FKWgpq_qr$B16{?Rg0G0&$6p{-id{&^?tqG20af~I28v%p zWfkjH(7Bqj27+H=0`M;nXG6f}Kd!yai0^9ZxX5lr)9Ik6k&lX*V9rR>Z}wEX#sekK;!R#*}P8o2}l6~9bmGZh+-Km$0xvh9>ll}{_8u)Zrp(`n!gKRp@X>6m3;pF_g`9Sm?=dJBg76?VH< zr0-(6L3}hLG_w_GLKg93Y#~>=f;HM_tfRYcXdnz9s$-g6a`q1OMZtEAzdXk80p2P` zgieeMkB%2UhPB{=*3i@4JNWX5YSZE&szdJ$MhTe|zA4MYCG)4l_ zIoJU=eDLb%Irz+gsLf4LZ`EzvVFJtNXHig)YcbBep>#}jgWAny)r z5cNRo-Fo%Kb#EZ=4M^Ug>clG%lJ%4v~IXc#Mqg4@7y5V(7(=ib!feAcfM}7RM#ojbzTW=n8>Unt^nY()VPG*X(M;Jyfh;Q1QIU>nBURU3$zn4-S?a(W zp#>9I#P^8OPl2LMW-CHhBvBgp0(*h%I({m3sRLpc&~8}jWy9|3Jy2P$S^J1Z^3)IJ zNa#tHgu>9LXW^Nuu$qe`-vG3RwS;<2X?@DDap+^FNH=9zH?ZcVc*Cwh(4b_sri`!# zYtEU#%3(VXVGR_}*c6I@c^fi6#tNBo6(21Ex$swDQ#|@iOG^knR#5Zg!r#IuNDbm= zxg_MFNjUCo0Ty`u;XY})|?e0-1fIkW!8FAXry%HDX%`>a-9y9mN`?LD7S}? zrei7AQBF5!gY~E{<)y>9iWLWIPb?#&vFXW~bJm>w1K6cEjGaotE?d|OA+4?=IfRK? z=cux^@#(FsGe=>c4pyre#3hN7a2qg3y-oqQS2YMiPss2U9^l#jSe`D8Cj{IoBKu{0 zujqXB5LD`)KyGBw3eQxN0ihTUeqh|^E5=189zU!bjmuWOZ>~JAY&A_} zikn1XHYSl45QlAD%qXX$fip}N#ntuwhqIm8*Sx!Li6)dC{n-tXQUSkFE((}l<1(8wE!YU*|C`Xu5AUb+H1!z8b=BEr%`JPG6AiT?qKO*PBWiP%{nx<3cwSH?g4y*%oF#h zss-2LkVLi0RI5m}-lLkA*lS6N>X4}pk?Pp6v^iQfOs(z~rFr|cQ|rxp^38js=4a*R zXE%tgHRI$1oG*f-vZa2URGOG;2R_;+2Yd5Py$=Xa!%=e0`bW10DLZ#xPs%%=U+OP` z+IC%kQEuD2)OXL{dhMtfe06mzWRTYOX{C5Pq5+csl`MRYxYW`kxAYiIf!)_Ht_Ke00|!?xtXZVM zkQ^9VdI^m@DDIkA?S~A~-HYPLxOi+F@_&6@LrMMfzIVNSf4+VH zYT)iOQu|@K{qTDGk$n3Rka_&L)IK4%Pb`I$!0z=xZ$8j_HzWm)$blnEFDZUz-QSh> zcYSzamAbo2@(;@XLD4_>-w*%P)CN-4k0STZ$nW{KU8BVvBWuSXgCzNm$-ZM^c>*L3 z<~FsXuH%@2o);LPi1ChINNcR<8Ai~8(dI5D+Hxat#*qor?ez_UXwkqiJsRKPt&3c{S;5#d(Iy^m-f8tzUp2q`C*B@ zeiYwsDQ1W#gs{6;V~{~wgW&ct$#YEh90Q;*;z7xCNcJ2OJ%=_8^US+dSF09Fet1d_ z7z2O@hhQfQv#|AV`1|XL-&5TOLi4c`Fc&Ps)fgNldC*+Y2hco0f3fCc_YS=b<;Sb^OP#VYJcC@Qmq0V}-q&6zd0r;MO#%+X{SttLgUYLbGv2gp7<9qDB8rkma-3R`4eg8u{(o_cJw_=D9Ao zgvZOQx^Y#%j>oI6(%DQrA#`b<%-G7P9t~Bz4%Y66YYH+H6})y)<<)h6S4qn>k^#S* zO9P(@C$;Dj8`BS|WRdmjH3;zjXMNg?)$k4g&BAeJYEXcr%q z6rxouPYMwb%acNMiRDQlI>hp%5Ie>4q!5jwo;EBzNy3|dbp8P2|NNL%yjK4Uph5o; literal 0 HcmV?d00001 diff --git a/services/vk_bot.py b/services/vk_bot.py new file mode 100644 index 0000000..189b047 --- /dev/null +++ b/services/vk_bot.py @@ -0,0 +1,127 @@ +import vk_api +from vk_api.bot_longpoll import VkBotLongPoll, VkBotEventType +from vk_api.keyboard import VkKeyboard, VkKeyboardColor +from loguru import logger +from config import settings, phrases +from core.fsm import DialogManager +from core.exporter import ExcelExporter +from utils.backup import schedule_daily_backup +from utils.middleware import MiddlewareChain +from vk_api.exceptions import ApiError +import requests.exceptions + +class VKBot: + def __init__(self, fsm: DialogManager, exporter: ExcelExporter): + self.fsm = fsm + self.exporter = exporter + self.vk_session = vk_api.VkApi(token=settings.VK_TOKEN) + self.longpoll = VkBotLongPoll(self.vk_session, settings.VK_GROUP_ID) + self.vk = self.vk_session.get_api() + self.middlewares = MiddlewareChain() + + def _send_message(self, peer_id: int, text: str, keyboard=None): + """Отправка сообщения с возможной клавиатурой""" + try: + self.vk.messages.send( + peer_id=peer_id, + message=text, + random_id=0, + keyboard=keyboard.get_keyboard() if keyboard else None + ) + + except ApiError as e: + if e.code == 901: + logger.warning(f"Can't send to peer {peer_id}: {e}") + # Здесь можно отправить уведомление администратору или сохранить ID + else: + logger.error(f"VK API error {e.code} for peer {peer_id}: {e}") + raise + except Exception as e: + logger.error(f"Failed to send message to {peer_id}: {e}") + raise + + + def _get_keyboard_for_state(self, state): + """Генерирует клавиатуру в зависимости от состояния""" + keyboard = VkKeyboard(one_time=False) + if state == "ASK_CONSENT": # <-- новый блок + keyboard.add_button(phrases.BUTTON_CONSENT_YES, color=VkKeyboardColor.POSITIVE) + keyboard.add_button(phrases.BUTTON_CONSENT_NO, color=VkKeyboardColor.NEGATIVE) + elif state in ("CONFIRM", "confirm"): + keyboard.add_button(phrases.BUTTON_YES, color=VkKeyboardColor.POSITIVE) + keyboard.add_button(phrases.BUTTON_NO, color=VkKeyboardColor.NEGATIVE) + else: + keyboard.add_button(phrases.BUTTON_RESTART, color=VkKeyboardColor.SECONDARY) + return keyboard + + def _handle_command(self, user_id: int, text: str) -> bool: + """Обработка команд администрирования""" + if user_id not in settings.ADMIN_IDS: + return False + if text == phrases.CMD_STATUS: + self._send_message(user_id, "✅ Бот работает и принимает сообщения.") + return True + elif text == phrases.CMD_EXPORT: + path = self.exporter.export() + self._send_message(user_id, f"Экспорт создан: {path}") + return True + elif text == phrases.CMD_BACKUP: + self.exporter.backup() + self._send_message(user_id, "Резервная копия создана.") + return True + elif text == phrases.CMD_STATS: + stats = self.exporter.get_stats() + msg = (f"📊 Статистика:\nВсего лидов: {stats['total']}\n" + f"За сегодня: {stats['today']}\nСтатусы: {stats['statuses']}") + self._send_message(user_id, msg) + return True + elif text == phrases.CMD_RELOAD: + import importlib + import config.phrases + importlib.reload(config.phrases) + # Обновляем ссылку на phrases в текущем модуле + globals()['phrases'] = config.phrases + self._send_message(user_id, "Конфигурация перезагружена.") + return True + return False + + def run(self): + logger.info("Bot started") + schedule_daily_backup(self.exporter) + + while True: + try: + for event in self.longpoll.listen(): + try: + if event.type == VkBotEventType.MESSAGE_NEW and event.message: + user_id = event.message.from_id + peer_id = event.message.peer_id + text = event.message.text + logger.debug(f"Message from {user_id} in peer {peer_id}: {text}") + + if not self.middlewares.process(user_id, text): + continue + + if self._handle_command(user_id, text): + continue + + response = self.fsm.handle_message(user_id, text) + logger.info(f"Response to user {user_id}: {response[:100]}") + state = self.fsm.sessions.get(user_id, {}).get("state", "") + keyboard = self._get_keyboard_for_state(state) + self._send_message(peer_id, response, keyboard) + + except ApiError as e: + logger.error(f"VK API error while processing event: {e}") + except Exception as e: + logger.error(f"Error processing event: {e}", exc_info=True) + + except requests.exceptions.ConnectionError as e: + logger.warning(f"LongPoll connection lost: {e}. Reconnecting in 5 seconds...") + import time + time.sleep(5) + except Exception as e: + logger.error(f"Fatal error in longpoll loop: {e}", exc_info=True) + logger.warning("Reconnecting in 10 seconds...") + import time + time.sleep(10) diff --git a/start.py b/start.py new file mode 100644 index 0000000..817298f --- /dev/null +++ b/start.py @@ -0,0 +1,27 @@ +""" +Заглушка для проверки токена VK API. + +Перед использованием: +1. Скопируйте .env.example в .env +2. Заполните VK_TOKEN и VK_GROUP_ID в .env +3. Запустите: python start.py +""" +import os +from vk_api import VkApi +from dotenv import load_dotenv + +load_dotenv() + +token = os.getenv("VK_TOKEN") +if not token: + print("❌ Ошибка: VK_TOKEN не найден в .env файле!") + print("Скопируйте .env.example в .env и заполните данные.") + exit(1) + +vk_session = VkApi(token=token) +try: + perms = vk_session.get_api().account.getAppPermissions() + print(f"✅ Токен активен. Права: {perms}") + print("4096 = messages, 8192 = groups, 12288 = messages + groups") +except Exception as e: + print(f"❌ Ошибка проверки токена: {e}") diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_exporter.py b/tests/test_exporter.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_fsm.py b/tests/test_fsm.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_validator.py b/tests/test_validator.py new file mode 100644 index 0000000..b8696ae --- /dev/null +++ b/tests/test_validator.py @@ -0,0 +1,24 @@ +import pytest +from core.validator import validate_fio, validate_phone, parse_time + +def test_fio_valid(): + res = validate_fio("Иванов Иван Иванович") + assert res.is_valid + assert res.value == "Иванов Иван Иванович" + +def test_fio_invalid(): + res = validate_fio("Иванов Иван") + assert not res.is_valid + +def test_phone_valid(): + res = validate_phone("+7 (912) 345-67-89") + assert res.is_valid + assert "912" in res.value + +def test_phone_clean(): + res = validate_phone("89123456789") + assert res.is_valid + +def test_parse_time(): + res = parse_time("завтра в 15:00") + assert res.is_valid \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/__pycache__/__init__.cpython-311.pyc b/utils/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6f9db10ee19c2099f7b59e26597ec22b64493c47 GIT binary patch literal 153 zcmZ3^%ge<81Rv(V%mmSoK?DpiLK&agfQ;!3DGb33nv8xc8H$*I{LdiCUpl^4F-iF) z@nzZZnb`#~W!bvLi8-moxVL2oS;9KWPgNiCW(h??dFt%hXCS6(*K@OT zJ2N}GGru|i==XaOj0XKEdLSV57khCAUlI<3AZ#E7DV&BziZeID+3)-a4>PX`(|81P zND)@ih^TNwD1cuu_fUYdTv}psQY8_tv>%%=JP;2ZO0c>HyWc|-oWiZT7dhjEU92o` zf$=sx#SwGPhBnzQ6+_{-z;nCoy~*NLj0NtrRq_0byit8@8gn`F0se zEC`!bj>XCO^m#dC)E;YN9!FMO6(^{I+__UTU_m$N4t-{>m`n6Gkao>wneNhGL0E#t zWx8*!(Wi8u?#gBc6i+~rp^r*S;IdDj(LGq&hs7OO-UCbeNCv-U`WPI3H`i_BKV&du zi~HcT8*$FYW)Yq`cnWKw2i@m3L9QGX7d}y#<8mpLDMv%-0y4^JMyMT;wnb!| z^e>lB;TO<%==;~o%b_~}%SH4B@S&14%gv%W;SQSR0zxR=_CYwR&CBEA$W$sRt8;23 zWvGg*ry>zm*C$e%HlMD!S=3H|sjBqI0nu_59SA#%g`@F_gys2gE}|xlXd-TjDj^9H z;4MzI-0EC1K@64X3>aHZwIAol1_*Om*sEP1QSLZ8T*>HGQ@eVW#PZ|g6g+J>>5YuAt{hna(Z1g+^Cx#sl`ajLhp?DV{qFi$u5$^yZ zPBdDqto<)U#N9tF#r7z%ia#1bK@Gb({zP{Q*TA2uvrfo3!A5;e@SYPp7O5faJ# ziuq-L2w=iK*Tl~lLnnl`Ep-Nw&t-x!-+pZ3_AG8cz?YumOAiGTcVuw~#T^Cg5?cz$VJ{_gt}*Lx-TZ9t z8uj;^{{F1LKl4!zd)Ee+2bV&tp-d=`y{osVblJpLviJ&h=nxKu0xmmAqy=QcvTpyY zrbw!o2zHB~5RsXdq3V^pcRInoB{#?gcx7F{ literal 0 HcmV?d00001 diff --git a/utils/__pycache__/logger.cpython-311.pyc b/utils/__pycache__/logger.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..855a0d5ee11d004c93a4a7d4cdb206fa2735cd56 GIT binary patch literal 755 zcmZ8fO=}ZD7@pbPY?4i5Fcd`WVXrHsReF&Sau7p{L_LTeLL{u6>9Xm5l$l8&iGc|D z4SEy2M5X;PJ$P8=BzWpA(w=(qo!zbwXJ?;z-jC;|4L$#A}NtFvCbfjQs)yc&7(Lv_C01OkI85lkEzsM zLZrzBiAO?~yRQxg-Ok%i_XoOyEOhhmMY9i`4#p<~|KzkkK51|_pu7RT{z&)%?`Jo< zdpfx(wZfQ;0@`NVAcdEM_!oe+jxkQq{fo_+AZw9+33_m`Ie($*!dzBA)|RzJP34(O zD=~`8=SgLlSoNRQ&bnw|?GFODRdb#~T0u literal 0 HcmV?d00001 diff --git a/utils/__pycache__/middleware.cpython-311.pyc b/utils/__pycache__/middleware.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bfdefce7e99d12cf786242f4eef949ee1e9e12ba GIT binary patch literal 4312 zcma)9>u(g-6`$9xXV>esu>k|YFaczHdAOulO-o#qq%QeTYE`m7pcQCYcE--aKHNLA zIIK+N5UVVe)i_csTS}t3U#JnPx_P*`5FDw0!Hl&MtC1?9R{g*?Bd8SdslRi_`!rU2 zclO-Jx%bRH=brPsXa3aI78Fo|>F<(%M(bbHsTBWaX=)OCWAl$>>E72Ct@q^RS#8GwC220GzB>a>FMnu=2%1%reEF!tea57=J%DJRzp{MF`%dnDZ!wnGC zjg%EnZ0>?rRqcL=?t)+lQBfD7k}gJNU78i5iY^Mce9ds8tM z)C1T@&~5*DQrA<)RGb+8Zv?G}7{fRTj3mT5 z&B)j?{?B)UEC>_Av=FPZbzxMvD~{Y1-PUw<-zMy+m_};cBstp4f%NGMm$O#v+SOR{ z>g0uMS5KJnlwqF0*oB;xOqmyIYX>H;yJ{?!%p|Q?EWfA87y}iK3c+Rv$c#`vcA}t^ z)IBzTp3~OcU`=46>MUT89$c5xXe>aJE2zD)RIhVoP?#3)VEkT{N3J<4uvScnNURN& z>yJ-P8W|nkFuJT0#H4p>8fz!M9M|=HSF<7*Bq8QO5a^(WpuuDH;JSKnDd?!jO6oDY z@~|BwrX^<@ESA(=#WFsyTt73c9Lpp~n@aJa!L{>TuwJ`i;o^=PvLsB))8e$$7%HX= zt;A7bu2Yxbg7V1OF&2WTh~jCq8*SDN;!5c$Q$a&R@sw+sWZ8san)&W#1veRF6Z1zP zP_lD(sq@f=AO(6jDZE?m+BN_E`H4l#={i#CIeI$&BU7 zro~+4ayFY{uVL`6z~ysoSO5c@P%xWmkeTYQtX#L1>l>2j8^As*#4q}eFTPI$$D4^? z@$FnRHw0A6hj~?mhUjfeyq(Y{68KjO9V?aRZ-X=;OGsA?1!$s-E4qIccnL&NfO3BH z13;A)>H~FxXsdcFjwigQyVD544WeZn4K&`rS8L+4{)%L;G?$fAMq2n3vOfk;Kn0-qKK$(JA# z0NyG9r{JwB*gG&&G{oM!TU7AW}^z2m3G!og2?oC{=A7Dd| z?5_u&lF6P9fw-Z1d2YCISwr+xaqc8HGvcHU=cXm}swj{o46Ncx4H0A(!CH2dUL2@u z5s|$(s`5+<(n<(%PtAAL&ZLTQR6#(5;cBIMjGjog})M%c!z%Ka={ zTQE85z&{DNAU$Iq22w4Uo#!@%HA`vAO4b=T<#lr|EwO!JhvS z-aVJPoteu#4)?Byd!2B9DcoO>%ia4H$4WgXo$ix`(1u?KbuAwGY`|{q2Mz}!<=+0n z@Z8Aan@;HPhQC2+u3dR=&u0ggTS|LRa*dynU0x8Ou$rH8)Pw&C3ZeFbHTSER?OPr!9UEG29imt4DR*_x58}mvHd;|q4?b3V*VW!7vwD4(+jkan+=Q==O7i9Z zFA34b#w**qup?6LB1G7K1U~Zg5OC?-ay|i2-|{j!jnYCSjN2hJ^2A3#W5VQBWRWY* z3^1GbwC~}FnH3p9UgMqcWOlNW;}F_3jXbzC%B$4`ubN4y%eRYeb5u(fubRnG$#--S zGEi+xJh0|_|Kdfx72X9o%BVN%uK{;IW_w+meJp&lR}6p!P%eL9d9l7 zf#4~Qd9|Yqm6Rb{87jB77Y4UR0--Ed)Qg1wt*9=4?%SfO0u1)$&O?^H0B8OR9{-V3 z@V<;T-Jd>x>OXN-wSUh1j=r1L?7xDwrcqLY;3@DACVU7vb6(8c2Y+QC-|*r+&)GZP zT6zxzPjT3*L0@6Ukw}<-s^(bEJ43&9eG-Sn6bNHdR`QI-Uep+e zUKbHdTgfvTdr@PY6D6@@Nk-U-9m}V9RYXvu*G$4PCL$Y0wTxIrRUG5hwuF1_e*u&6 Bb&~)9 literal 0 HcmV?d00001 diff --git a/utils/backup.py b/utils/backup.py new file mode 100644 index 0000000..a3677b7 --- /dev/null +++ b/utils/backup.py @@ -0,0 +1,29 @@ +import schedule +import threading +import time +from loguru import logger + + +def schedule_daily_backup(exporter): + """Запускает ежесуточное резервное копирование в отдельном потоке""" + + def job(): + try: + exporter.backup() + logger.info("Daily backup executed successfully") + except Exception as e: + logger.error(f"Backup failed: {e}") + + schedule.every().day.at("03:00").do(job) + + def run_scheduler(): + while True: + try: + schedule.run_pending() + except Exception as e: + logger.error(f"Scheduler error: {e}") + time.sleep(30) # Уменьшил интервал с 60 до 30 секунд для более точного срабатывания + + thread = threading.Thread(target=run_scheduler, daemon=True) + thread.start() + logger.info("Backup scheduler started") diff --git a/utils/logger.py b/utils/logger.py new file mode 100644 index 0000000..3c54b4e --- /dev/null +++ b/utils/logger.py @@ -0,0 +1,17 @@ +from loguru import logger +import sys +from config import settings + +logger.remove() +logger.add( + sys.stdout, + format="{time:HH:mm:ss} | {level: <8} | {name} - {message}", + level=settings.LOG_LEVEL +) +logger.add( + "logs/bot.log", + rotation="1 day", + retention="30 days", + format="{time} | {level} | {message}", + level="DEBUG" +) \ No newline at end of file diff --git a/utils/middleware.py b/utils/middleware.py new file mode 100644 index 0000000..8ee32bd --- /dev/null +++ b/utils/middleware.py @@ -0,0 +1,71 @@ +from loguru import logger +from typing import Dict, List +from datetime import datetime, timedelta + + +class MiddlewareChain: + """Простая цепочка мидлваров (логирование, антиспам, аналитика)""" + def __init__(self): + self.middlewares: List[callable] = [] + + def add(self, middleware: callable): + self.middlewares.append(middleware) + + def process(self, user_id: int, text: str) -> bool: + for mw in self.middlewares: + if not mw(user_id, text): + return False + return True + + +# ==================== Антиспам-фильтр ==================== + +class SpamFilter: + """Простой антиспам-фильтр: блокирует пользователя, если он отправляет + более `max_messages` сообщений за `window_seconds` секунд.""" + + def __init__(self, max_messages: int = 5, window_seconds: int = 60): + self.max_messages = max_messages + self.window_seconds = window_seconds + # user_id -> [timestamp1, timestamp2, ...] + self._messages: Dict[int, List[float]] = {} + + def is_spam(self, user_id: int) -> bool: + now = datetime.now().timestamp() + window_start = now - self.window_seconds + + # Инициализируем список, если нет + if user_id not in self._messages: + self._messages[user_id] = [] + + # Удаляем старые записи за пределами окна + self._messages[user_id] = [ + ts for ts in self._messages[user_id] if ts > window_start + ] + + # Проверяем, не превышен ли лимит + if len(self._messages[user_id]) >= self.max_messages: + return True + + # Записываем текущее сообщение + self._messages[user_id].append(now) + return False + + def reset(self, user_id: int): + """Сброс счётчика для пользователя (например, при /start)""" + self._messages.pop(user_id, None) + + +# ==================== Логирование ==================== + +def logging_middleware(user_id: int, text: str) -> bool: + logger.info(f"Processed message from {user_id}: {text}") + return True + + +# ==================== Пример: мидлвар для аналитики ==================== + +def analytics_middleware(user_id: int, text: str) -> bool: + """Заглушка для аналитики — можно расширить позже.""" + logger.debug(f"Analytics: user {user_id} sent '{text}'") + return True