From 7dd00a2f243c7d43ca1cc0485eead2f3d2aac2a0 Mon Sep 17 00:00:00 2001 From: Vlad Date: Sat, 28 Sep 2024 03:26:28 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D0=B8?= =?UTF-8?q?=20=D0=B4=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- database.py | 11 +++ handlers/__pycache__/keys.cpython-310.pyc | Bin 9044 -> 10364 bytes .../__pycache__/notifications.cpython-310.pyc | Bin 2689 -> 3899 bytes handlers/__pycache__/pay.cpython-310.pyc | Bin 7536 -> 7514 bytes handlers/keys.py | 91 ++++++++++++++---- handlers/notifications.py | 45 ++++----- handlers/pay.py | 42 +++----- key_management.py | 55 +++++++---- 8 files changed, 152 insertions(+), 92 deletions(-) diff --git a/database.py b/database.py index 3f4bd37b..565a3157 100644 --- a/database.py +++ b/database.py @@ -35,6 +35,14 @@ async def add_connection(tg_id: int, balance: float = 0.0, trial: int = 0): ''', tg_id, balance, trial) await conn.close() +async def check_connection_exists(tg_id: int): + conn = await asyncpg.connect(DATABASE_URL) + exists = await conn.fetchval(''' + SELECT EXISTS(SELECT 1 FROM connections WHERE tg_id = $1) + ''', tg_id) + await conn.close() + return exists + async def store_key(tg_id: int, client_id: str, email: str, expiry_time: int, key: str): conn = await asyncpg.connect(DATABASE_URL) await conn.execute(''' @@ -85,3 +93,6 @@ async def get_key_count(tg_id: int) -> int: count = await conn.fetchval('SELECT COUNT(*) FROM keys WHERE tg_id = $1', tg_id) await conn.close() return count if count is not None else 0 + +async def get_all_users(conn): + return await conn.fetch('SELECT tg_id FROM connections') \ No newline at end of file diff --git a/handlers/__pycache__/keys.cpython-310.pyc b/handlers/__pycache__/keys.cpython-310.pyc index 0500dff1b7dcf0f37d8c22b45355ecd83b596b60..bcefdc3ae887440f573fb6fc0e614456e8bf1b5f 100644 GIT binary patch delta 3232 zcma)8T}&L;6`ngYyF0r(JM2F!3(G7lm^B6)Q~wc{7@O2k8`rAsICc~f?7Jqc&5tj$ zO-yGCZX6|+Oe1nrwN^`jhd!W65j9bo)Jv`On$PS3du z*dQuprTfjf=bX87?>Xl?cYpu2|DH5Pd_Iqazs}Du{ibW$s3OJbru=lv-zR7}t+*kb zugrwz!Zb!bH>7z&y%|R~a!IC@G}^nC+oF6Hf~4H`fNA#Q16tZ^jRVer>PD; zxVVoRVkxhfl~KQ#=`eHK-T`9k17tv^xUO&0n!b%|o|SBw-&3{)7KxNMh%_Fw{P=wu z;FanD66B}Vj3?)S@U6_gIc@Uy)dQ}RFgtjmgMX&>=m&RR=qRe)r+betsJzor7cP26 z``K8gZ)k*Oa`mg8gN}p`J#%qT+MH za|ZR#6Yvk%UPz=9;#!vV(;yAmib!)95^D%ughnaO%ESyRea*uFrC%sxP~>6W?~d<_ zLyCe{FdMRcBJJU9#12v@^2gF{pq(bx>|m|oOYS{^6-Ze~q^v@u%;f)eA0SEI=6T`oLtE3^C+;r%9ff48t`7CwYGJ}i8&{Ayvb@Im3d<=4fV=7T`} zx#s$z=6Y(@cemH~w%4C35>Kugmb&-)l3Cz)yxZL|@qPY7lmFQpE-&hX!($f9U9<*A zhQ~@(Gjq%2+w^*Ovb0mEnv;5Uyb)$BfnXw_G1mcrXMaoISB`sMP#7MM)gfR>=8o~! zSULYfZ?8(DZaab>py(JIy)tOACRFU;XMBCnw4jK`W-f$Qgxv^YwKkN#g0Kf+FF-oX zqA2e}IEYYzupi+Ggs&qU@?f)mCI-(vx)V}_+?v}b5X^X*PFI#tLz#N25(i&sL(_M+2CrU4Q0c&hD7G?1$UCkYBQu}u2jX~@p-7aVS`7T&9>n}>y@4R223##`@+q81Q%Ck-&hpXn+B%c zMYb(pFL5o0pEs8$mtQXYtYw{|{I&4jDtyyqCva&KK~M!t7nfdkMEKvr_2AvY1YMo) zqA03%>7ziNM37)?5Mlj1>^Pqe?RT&&X!*yXgHd)Fq<`YysKN{+`sXm-VU2uy_;y63 zvKL=B5kx+lMPpkD$8=_)@(RK*?$U7;#CIVKU5y*M1$GVv#u4;f>oFOels5eb6pt>= zMlTW4z2vM2kmO6K!pmEfvgp1WT5O8%h_yH2_tWKHs)+b>tfA}6Hd?F=Xh0(VA`_3* z2LDei?iXo4gxl*xILsR=+njievE4injc}_nN%EMW=<0FDy^j%e_1H4R1&;mK%HPT< zk*6M5;J&zMHfIbBxn7>E3GtQc4ow`JuycHGqSq-R)yQuqo+a0o{*p+NkuCaWQ5hUw zyK->DTM&y4q2d3KMq-#h_6&lcndrl%a6)85NjF*ygmDDq{KlJ#FPJrv{iuEd;ZZe? z@K4Nm>nJFTewG=%k{KQxd!g?_&ay^^*&*~5A^tW>Ho_|9(#f5&O!)ca^W-KHEWj9A z#}IM|-@=lNBXPcpfqoH$baknbh{H7TzS_weQIiz?2N1A=OZRLZrJMX=Z9@Wc=$G*7 k&}E(YNf?Gx_RAVPb;5Vmm4|#mqNQ{-Nc0qp1{m%C1MfHA$N&HU delta 2701 zcma)8Uu;{|8NcWL@t^DK>pyYg#IYUI*hxLwrdcQu6Q%7+N<-QX7HIibvLxp=*lq0W z_?i`Stpr*z9tfyCdte}>y)pGh+LMPs;$hPEz&3#MAs zm4`+;KY!=@zWbeXe%~MEzmNRss96aHbqW5AD_@^|bNI5EB!$a&H~21c;bE(7B}U^^ zza*^^8nCod`&pTG(ymMDMelWJ&= zM&&N4;elTTe!aG*Td3~rBjT5@Fmn%W=vfJhCKNcE^4#J8`gUa#vPld@C22)fOvNNIIVsCaS;dsI z#J41W6IIw@{`a0FU-ONTQT{JqR{jU!3*iKBWMZ-Ubz5E0Y|mCQ)cU*pS$~|Y@#p;q z7dsriWKiEZl|4aql!0cs5h$Y^q=AIgFsR{dYH3g;!*qNrCJn6;NKDuXz!#oI&<>Tt zG(w|}f)QvNB+3-J7;P83kl1z5_%@I5GeQaVCtM=1i#R5bAA%Kz6@etEGy$oN+R;** z6BKEUmD-&sg}PkUwi0WiouETU%m}Gb?@T)`&=yu<7 zf4%Yg#!oWtwT+iHUUolpukWjM^DhHwZ!v62QZ2=kMy9{kfxcJVwT)LZ?)BCu1$|_J zS);XnFpUhLsPnp!%w$GpM*kDZJ-IhGzc)uSxnqUgi9&9&u8j=LjOw*69!a$GPmHdK zdobpk?#FoW#?r<`_xi>w?lt#X#{I4P8*w2N;IP-+kKp&0?wb2CT=Vs&Q7l(&R-3hp zm2$OJK3&0_nuK`#p_KrKf)`-`VLyU6 zbP%QQA`Btm!}BqQoU#K51%yt7dl7~ajvBsw191L35{MsbWk8jk565Pt`&`aG|M&&bOvcf;M0 zz)6-spQ8XZtcojrwCWyrpd+D8Ctdm+i4{L}XMV&pvu@*Ah*>9PE3Y8 zG>zFB0}kvdJXusn(>pT_bj(_81icRNKXmLP7x}Z@56bM^EiBKXy~Xl-D30-TXKLM_ z90#V?CsPrU5Zr~(){THIC5oUn2&BdNzffnzR$&@|P@A0vx%K#MErVBY+k%+H-{|Qj@A37X2S^23>DX+XdSF{gwb=39!0*PO z3%>DnuTN)wA@^XXu!jKtDmXcd+pVNPaSryL@(Dr+JI&wdJ>l7`!Y_KCCYklo>|JEB zAHUfeDWZ#-O0oRyR6*+wCB~jZgBM|^8Ka=#6oM!?4W*kzWB0QFf-dpTa>2#(D2ury zSl%|0nkx2`m`P2OPQOr{vMW>81=}jqZ8PfTlZz0k8Nyz)E-Vz;3!)yI>TCrDbqQ@@ zW-Xkz%Ejt)Q!_Q&u9VrR1XbM{X!CfFGQwufzT7t;-;nrv-w(-)ybw>4DnA|1?!mz( zXjONj00y|2redz{=cKbOI|MOx`0uQ$f*>!Yi5S57RI6tNTX|mTU+Tdj$7T>LgmVZL rgclKpxVgJ;KN1<1d~%o=vO&Tm2FoKGN|YG;U>$&E!pg$xgH`xHh7e^k diff --git a/handlers/__pycache__/notifications.cpython-310.pyc b/handlers/__pycache__/notifications.cpython-310.pyc index c354ebf23502ac51d83aeca7cf655d8fde3c12cb..b4f6a01f2bd0317eb01e6d2de50c1e1bba754a80 100644 GIT binary patch literal 3899 zcmbtX-ES1v6~A{rc4ybKU$!xXk7P=5s;$5dRTZT!K>=(6Ep{tRMKe-68t)z3Yi4$w zJF_vib|b)~i9{SEUQ=3IedUPAtgjqXQ<##`_YzJh`gMqOg!TXLyKP3lReM6oTq zvZt7es4GxcP1Q@8DbZHlw5OSxmoYPQ?-k5~sAt@w zS29bYo^^Y@KC{p3H~U4q$KB%%m;;nJ>M}9+zD=0UayJRfIep8rxsT-?Wm;kdR=la0 zFFAWx_d5kvTBd81^*X7!1FX*(m{wRnE8SF?Wi{3;t7dzSWyM*`@_bfzv94RzyLH=5MtA{a#)lT{ zYFMq!SkpeYYF)A{ROq*a55O!@{NwoWtnWF)@3}S`9(CRup7dSc0(%V4!g*ad;&Au+ zU}%1kmtZk&6DUw70~723?OTqE@oU9mC56_ROu3 zjt-Z>c9T!_Ef9r-`Nh9b($pzg%7+Ek-_SoLcco=Y$z}2mtZC}+>$ozhEzkv0-ZSw9 zUJ)Qxo%QQHsQyhRWa$@KyS z7(6#JI6g8sS$_o%68$>biav+`qv%QWpmn45u@U_tT8HL(^rUq?E^%kxb!ydM)|#?i zyH;_)g%$LQI9CC;p0X=fEZ`a>mqJyLhy|w-EK={o4emtit&fc8Vf3i=N$W%7#MH_9 zUg$qDJay859j~`;L>sLS;eSJneYZU}WC+zhLtR?S(WaQVe$ed6fhe|y0Cn;5TXgx8-10&i3JD^q8qkGW5VZc`R zz$*8_Opn1#8+fzP=7>=)#%ViPtX1Y`z)F6t=2SwCXyy`{Dpe0FHGhEvph*%ng3$JW zqojqCKlsWA&?1on&*s}wmxQ*wTLB90q?V9v8{~qpC5(epx3H){JSHXs%Q$fpj9;~DB1^@pT zD%UBnDwzZRzasNegRII7@`Dbm2~NYl?dU*}eUZe(5|B#|$d6NYEm(jw zDic7CBknVa?0aC44?|Iw#AWmEW5;$5At1;{pzk#NgFF<{HC-xFg%+h=s?%MlK;PQV z3ciHKbs9pJ3&^0$klEt9DJ{(|y&g)da;S7}zA1-l18%+@XBAi@g%IhNX(z@t6o|Yf z$O2R*V-^sm=d?zuAu-4fR@ylO#P3%DcNx}&sLP~w2x4b;@#hdb(-1omnHiRDq)~Eb z|Lqu>em`9+Km>(1MoJcnPAc7?-i07C=1 zcGL0>(EbDL{$E7jx(!6H3-l#w58?q{#X?X$CYiAeAzd&qKZ*kF-n^a$b$pNZ`&`(JUz&}rWB~8-kKH4wk z>0v>l5|FL*)nSQ`!PN3AT$~1pG6_>@yXwzy+vAv8a}|pe6mhQIKO{bIL}*Ha6eitq z3c}8GbtW;0@Z>^g{&dxa52JwVI1fLf>?s=#D6Cf6o)maPiMylkCZ7;}-Gd+>QzTv# zyTO+Z$V{jKV?q)D(&I^x6hy!UW#AdR3@A(~!j*O+`Fc5tp9{eeC^S+8qA0=-OTAsD Qq@t9U_16YyHnX4p3om02G5`Po delta 1521 zcmZux&2Jk;6rY*>^m@H^Oo~FARu)Y|Orn+p>Y<8Kg$9X=l0(xfUm zsiSqBG&!LLT2`D&B>PAYsEVklXw@D#a##R&v&#wJRut~76E-u&L1_wk#3 za%%gSC>@LLZ?P-rSAy(nUNKU6szZX)09jBWi>?$n?I_N`+{}2$e8J zO`6%0`bHStcsIlyjaq@!k-Mm)LMGBM>XLy|BQ)mF=O04=T z*$d9h6;~$boY+NQ^a``A)g+Q78ITeR0f6G}~`BDr+@B zdX0ut30Nw!wM)^D+Js~>E&5%XJi$xMGF(_7lC*Pk&1KE9=vq-fDgS`jcyx!HA5DfE zUf@=nWr1)Twepuv)92>?h3JNUvYp2xSd}cv_(xZCtYZ`QlljqmJ0z^FZZF~?vihqh9 z_io3J3YX^!@$>ju{G@j${u$ zqJ$)rjut5dc2hYd@E(+XKpm2DNz@Nms5L(Z7RtcFya21A%z1bO+GnGDOCGQhR-BEZ z{@VPeHmFyE3q`_m5HcGAv3@POYhFD5`YwE3NO1J0IoH+&HZlCbW`V!q2Fr=!udTX4 zBCmLEL;MsN=K!1JViZJC5q0LY7C(|i8#GF|VrMw+l^zVQF|XqIep&Rwt=NIQY z+#dl+woF2Bju?1YI)yDVCKxOK#op)F<=Goi&N|Z;u_RgB^=iznpRKy9fx~>J113?K zw+_2Rg{M|^Yb*sy!T3R_pTOmI%@OSuUXMg5Vii=;r{VdouR1gQ1^7H`9p&I9@nL4M PN=#yr+*lr~%A5Ey|v=dOI4|Ad+i^vmmbP=SxyPcFNJNw1Ssnd%%GY~0nj)9(_Ae}z;@NnqDceLOCb3|#xs=Ezh-A_g zDcf>JVj2srN-SimD{{7#b+HJGu99q<*|MHwbu6|jZ4g#(%CjlfVEU(gtdYf6eJdoJ z_83jOFq&DyW3+n=qh%LLD{J#89UdjQ3nRtS9;4G^wC}>`V4WVLOI#;+iC<`RzwSm2 z#&Y>exoqa`LZ$2`@-t?BuEvuyuNN%a5{KzSr47X5ZDPNCAkheB6q=<1sFFeyq7mLG z=H=0&(zvwfcl_(b@okVr#Zh?5rn9mmISM0d^exbJTp|+qeBdJvWpqtOulQSTC6Z8l zU1PGNelC4MR|t{blir81MfD?HPzT@!Ez{0fwqctb6N@LrH)`K;g};U!lny4J#sCNB%o~uL3uW6?s)^~U;F_!$ zobj6A2pZgX0E1$61JGM4jmoMVB|aJ_4V02F{M5&apAxViH~9G3tuO}<9fB8hn&EH2 zKYs;b2%%|&5d&^RJK3m#9^gQ?T)_14nG!(gfoxt0%r-ilM6fv=^+5l!zK1edG9 z*12t6E7&tH{Mf0@+uA2YK{0rr=nn5C!(u!<5ITVlZ>=L@Ied(~DZUTCll7+cTl2+& z?aD>7%nu@M6yX#Co?0G2IE`=y0p;Rj2wn_Yz|#FSTf#Z{6=6i0b{v)ENG~}h?nefR z0lP{Mox}HN#tj%Hj8l%E$M;^Op@D-Fn+KxbkJP%kH|zV!$mVbLDYAt5<)sMk150n=-ZDQxZw_JS#$6fCKF6HkJqSUB zJqY^{UP5?IhgS4$tMp7cNa=-QQQT|{4UYpw4?icBt8EEj*_`8n;r$3E!T`b*gwx{3 z#-1H|K4?sXl96~KO*P0@N>U_B+K76KK538}h(_UgqNw6zyo1c6;)72WR}5Avl>bNY zd-&Cf&2QuTW#uy37sT(0i=jN47ZEOr(Uvh~7|juJr{#~3r*InkQ)038(ZF^V;q6%! z_&L$%aGY?$n4Xc(L z8Qr=7&U^L$Eckc^!OQr0wD5~Hg~D5U5`1v2lqzhYXr3TqINjWyf?}o67}fmY#%2<3 Ng(gEwK^tr%{{e^R+?N0V delta 2329 zcmaJ?Z){Ul6o2>q>+7F&?f$h}+pTj9crYe7P#8lI#DRpWpg~5UcD-+-rEBNyYbHr& zH-<4rh`}3`#9%fPjUR=vNJ5wb)RP zuc6IILm}Ep>!|+%844RMv#qqA1}+G5ga!>M+eRA-IplEsOYPFiW3V-j;rB5OE_(wJk>?pU<1VbMY3jztHbBA1E88^j(yD&AGYkU9(V zQ__}D5JW$Wq6VM|Dp84tuppn4_U|GGg-NBX%#yM^M<&&>%A%&2mC8a{rQ#*=kU+tL z2%wWeN6I3VE=j26F}alx{+!%dk;~c_!c}pG5aGCR0@fzA_cTh5ElCq(LMXeA3A$3U zAOKGs9_!2MdFzMjl=Yo;-MUr%*jlKbk6X8`MVKyH3-RhSycVr%)eo%&P~QZVbu(U_ zv2MVQhxX`T!K&P-eq`OK&N#MfZrup(r>$Gy0er$C;J#)K@AKAma0|!lJb9bIYzxzyM&O+Bx}>~9gk zBe3nb;!me?x#3j$Z1SCw!OpP_piHB|@*{(w!ngyYI07(D%0gDil1Z^lW<^uVLgAsH zf>UP2R4Jk20jRP#N?;;Wr6!MxRJ}wd;bvrAS&h2N64jjh;cP4mv+g-z*$ow-+U^9O zbnPMS{FZCFr74q9!Qv(#fgl0=7}-vozHtdAu3Y9HgeV1O?L7gU$57R%}HT^ z8@_(>JfHO)t~hR$;&?7&+EUKQvn{yq1%$l_`0XhSfwlM!y4)EzfoE+z01%3h=MuSfSMV|*Z zlY#j=!6>P8ppzPZAn9S|B=i<)Par%_kuAZu%Hp`tiQq)R&5qc1ibheLw-Pol+emm_jT(h)MEGCyd z7&#-2&E_=!t1%RlJVcg65+#1pMzksML4({tJR&>~R5$N#>L6JxPv3HR)D#`dp(N9g1i+qPG_P<)(zTS?~%MFU?(iZO@U`qk8`DTfZ*iya2~2J{wI;}eOt zT}$d3<9Src=|VnlyM|M_R6cFke&<8xyCcznQ;QV^;G>Zaa)o~o>FxlcH8B9aCLsDH zpX4X&B_!U19p|3M#ea$H2v)$mE+rDI4TU%}c`*7JzZPwLBY@|stWbikSNr&!yNEr{ zuG_!V(qDHv`XK1oL4=bCSp;;vq7OL{w(@XnU3(LBJ{98fD87~!;tRk~V8meTY9s#v DqclgL diff --git a/handlers/keys.py b/handlers/keys.py index ab89bb5e..b244c00d 100644 --- a/handlers/keys.py +++ b/handlers/keys.py @@ -11,6 +11,17 @@ from database import get_balance, update_balance router = Router() +from datetime import datetime, timedelta +import asyncpg +from aiogram import Router, types +from auth import login_with_credentials +from bot import bot +from client import delete_client, extend_client_key +from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL +from database import get_balance, update_balance + +router = Router() + # Обработка запроса на просмотр ключей @router.callback_query(lambda c: c.data == 'view_keys') async def process_callback_view_keys(callback_query: types.CallbackQuery): @@ -30,26 +41,26 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery): key_name = record['email'] client_id = record['client_id'] # Заменяем подчеркивание на вертикальную черту в callback_data - button = types.InlineKeyboardButton(text=key_name, callback_data=f'view_key|{key_name}|{client_id}') + button = types.InlineKeyboardButton(text=f"🔑 {key_name}", callback_data=f'view_key|{key_name}|{client_id}') buttons.append([button]) # Создаем клавиатуру с кнопками inline_keyboard = types.InlineKeyboardMarkup(inline_keyboard=buttons) - response_message = "Выберите ключ для просмотра информации:" + response_message = "Выберите ключ для просмотра информации:" # Редактируем сообщение с клавиатурой - await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=inline_keyboard) + await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=inline_keyboard, parse_mode="HTML") else: # Если нет ключей, добавляем кнопку "Создать ключ" и "Назад" - response_message = "У вас нет ключей." + response_message = "У вас нет ключей." # Кнопка "Создать ключ" - create_key_button = types.InlineKeyboardButton(text='Создать ключ', callback_data='create_key') - back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_profile') # Измените на правильное значение для кнопки "Назад" + create_key_button = types.InlineKeyboardButton(text='➕ Создать ключ', callback_data='create_key') + back_button = types.InlineKeyboardButton(text='🔙 Назад', callback_data='view_profile') # Измените на правильное значение для кнопки "Назад" keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[create_key_button], [back_button]]) - await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard) + await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard, parse_mode="HTML") finally: await conn.close() @@ -82,27 +93,27 @@ async def process_callback_view_key(callback_query: types.CallbackQuery): time_left = expiry_date - current_date if time_left.total_seconds() <= 0: - days_left_message = "Ключ истек." + days_left_message = "Ключ истек." elif time_left.days > 0: - days_left_message = f"Осталось дней: {time_left.days}" + days_left_message = f"Осталось дней: {time_left.days}" else: hours_left = time_left.seconds // 3600 - days_left_message = f"Осталось часов: {hours_left}" + days_left_message = f"Осталось часов: {hours_left}" - response_message = (f"Ваш ключ:\n
{key}
\n" - f"Дата окончания: {expiry_date.strftime('%Y-%m-%d %H:%M:%S')}\n" + response_message = (f"🔑 Ваш ключ:\n
{key}
\n" + f"📅 Дата окончания: {expiry_date.strftime('%Y-%m-%d %H:%M:%S')}\n" f"{days_left_message}") # Кнопки для продления, инструкций и удаления - renew_button = types.InlineKeyboardButton(text='Продлить ключ', callback_data=f'renew_key|{client_id}') - instructions_button = types.InlineKeyboardButton(text='Инструкции по использованию', callback_data='instructions') - delete_button = types.InlineKeyboardButton(text='Удалить ключ', callback_data=f'delete_key|{client_id}') - back_button = types.InlineKeyboardButton(text='Назад в профиль', callback_data='view_profile') + renew_button = types.InlineKeyboardButton(text='⏳ Продлить ключ', callback_data=f'renew_key|{client_id}') + instructions_button = types.InlineKeyboardButton(text='📘 Инструкции', callback_data='instructions') + delete_button = types.InlineKeyboardButton(text='❌ Удалить ключ', callback_data=f'delete_key|{client_id}') + back_button = types.InlineKeyboardButton(text='🔙 Назад в профиль', callback_data='view_profile') keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[renew_button], [instructions_button], [delete_button], [back_button]]) await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard, parse_mode="HTML") else: - await bot.edit_message_text("Информация о ключе не найдена.", chat_id=tg_id, message_id=callback_query.message.message_id, parse_mode="HTML") + await bot.edit_message_text("Информация о ключе не найдена.", chat_id=tg_id, message_id=callback_query.message.message_id, parse_mode="HTML") finally: await conn.close() @@ -119,13 +130,53 @@ async def process_callback_delete_key(callback_query: types.CallbackQuery): client_id = callback_query.data.split('|')[1] # Используем разделитель вертикальная черта confirmation_keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ - [types.InlineKeyboardButton(text='Да, удалить', callback_data=f'confirm_delete|{client_id}')], - [types.InlineKeyboardButton(text='Нет, отменить', callback_data='view_keys')] + [types.InlineKeyboardButton(text='✅ Да, удалить', callback_data=f'confirm_delete|{client_id}')], + [types.InlineKeyboardButton(text='❌ Нет, отменить', callback_data='view_keys')] ]) - await bot.edit_message_text("Вы уверены, что хотите удалить ключ?", chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=confirmation_keyboard) + await bot.edit_message_text("Вы уверены, что хотите удалить ключ?", chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=confirmation_keyboard, parse_mode="HTML") await callback_query.answer() +# Обработка выбора плана продления +@router.callback_query(lambda c: c.data.startswith('renew_key|')) +async def process_callback_renew_key(callback_query: types.CallbackQuery): + tg_id = callback_query.from_user.id + client_id = callback_query.data.split('|')[1] # Используем разделитель вертикальная черта + + try: + conn = await asyncpg.connect(DATABASE_URL) + try: + record = await conn.fetchrow('SELECT email, expiry_time FROM keys WHERE client_id = $1', client_id) + + if record: + email = record['email'] + expiry_time = record['expiry_time'] + current_time = datetime.utcnow().timestamp() * 1000 # Получаем текущее время в миллисекундах + + # Убираем проверку на истекший ключ + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text='📅 1 месяц (100 руб.)', callback_data=f'renew_plan|1|{client_id}')], + [types.InlineKeyboardButton(text='📅 3 месяца (250 руб.)', callback_data=f'renew_plan|3|{client_id}')], + [types.InlineKeyboardButton(text='🔙 Назад', callback_data='view_profile')] + ]) + + balance = await get_balance(tg_id) + response_message = (f"Выберите план продления:\n" + f"💰 Баланс: {balance} руб.\n" + f"📅 Текущая дата истечения ключа: {datetime.utcfromtimestamp(expiry_time / 1000).strftime('%Y-%m-%d %H:%M:%S')}") + + await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard, parse_mode="HTML") + + finally: + await conn.close() + + except Exception as e: + await bot.edit_message_text(f"Ошибка при выборе плана: {e}", chat_id=tg_id, message_id=callback_query.message.message_id, parse_mode="HTML") + + await callback_query.answer() + +async def handle_error(tg_id, callback_query, message): + await bot.edit_message_text(message, chat_id=tg_id, message_id=callback_query.message.message_id) # Подтверждение удаления @router.callback_query(lambda c: c.data.startswith('renew_key|')) diff --git a/handlers/notifications.py b/handlers/notifications.py index 6ddce0db..b8df99b2 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -2,12 +2,18 @@ import asyncpg from datetime import datetime, timedelta from aiogram import Bot from aiogram import Router, types +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup # Импортируем необходимые классы from bot import bot +from config import DATABASE_URL, ADMIN_ID from aiogram.filters import Command +from database import get_all_users +from aiogram.fsm.state import StatesGroup, State +from aiogram.fsm.context import FSMContext router = Router() -from config import DATABASE_URL +class NotificationStates(StatesGroup): + waiting_for_notification_text = State() async def notify_expiring_keys(bot: Bot): try: @@ -25,8 +31,14 @@ async def notify_expiring_keys(bot: Bot): email = record['email'] expiry_time = record['expiry_time'] expiry_date = datetime.utcfromtimestamp(expiry_time / 1000).strftime('%Y-%m-%d %H:%M:%S') + + # Создаем клавиатуру с кнопкой "Пополнить баланс" + keyboard = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text='Пополнить баланс', callback_data='replenish_balance')] + ]) + message = f"Ваш ключ {email} истечет {expiry_date}. Пожалуйста, продлите его." - await bot.send_message(chat_id=tg_id, text=message, parse_mode='HTML') + await bot.send_message(chat_id=tg_id, text=message, parse_mode='HTML', reply_markup=keyboard) # Получаем все истекшие ключи expired_records = await conn.fetch(''' @@ -37,31 +49,16 @@ async def notify_expiring_keys(bot: Bot): for record in expired_records: tg_id = record['tg_id'] email = record['email'] + + # Создаем клавиатуру с кнопкой "Пополнить баланс" + keyboard = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text='Пополнить баланс', callback_data='replenish_balance')] + ]) + message = f"Ваш ключ {email} уже истек. Пожалуйста, продлите его." - await bot.send_message(chat_id=tg_id, text=message, parse_mode='HTML') + await bot.send_message(chat_id=tg_id, text=message, parse_mode='HTML', reply_markup=keyboard) finally: await conn.close() except Exception as e: print(f"Ошибка при отправке уведомлений: {e}") - -@router.message(Command(commands=['notify'])) -async def notify_command(message: types.Message): - # Запрашиваем у администратора ID пользователя и текст уведомления - await message.answer("Введите ID пользователя и текст уведомления в формате:\n/notify user_id текст", parse_mode="HTML") - -# Обработка команды уведомления -@router.message() -async def process_notification(message: types.Message): - if message.text.startswith("/notify"): - try: - # Парсим команду - command, user_id, *text = message.text.split() - text = ' '.join(text) - - # Отправляем сообщение пользователю - await bot.send_message(chat_id=user_id, text=text) - await message.answer(f"Уведомление успешно отправлено пользователю {user_id}.") - - except Exception as e: - await message.answer(f"Ошибка при отправке уведомления: {e}") diff --git a/handlers/pay.py b/handlers/pay.py index fbad89e2..8cd1a965 100644 --- a/handlers/pay.py +++ b/handlers/pay.py @@ -6,7 +6,7 @@ from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from bot import bot from config import ADMIN_ID, DATABASE_URL -from database import get_balance, get_key_count, update_balance +from database import get_balance, get_key_count, update_balance, add_connection, check_connection_exists from handlers.profile import process_callback_view_profile router = Router() @@ -17,9 +17,6 @@ class ReplenishBalanceState(StatesGroup): waiting_for_admin_confirmation = State() async def send_message_with_deletion(chat_id, text, reply_markup=None, state=None, message_key='last_message_id'): - """ - Отправляет новое сообщение и удаляет предыдущее сообщение (если оно существует), сохраненное в состоянии FSM. - """ if state: try: state_data = await state.get_data() @@ -42,10 +39,20 @@ async def send_message_with_deletion(chat_id, text, reply_markup=None, state=Non async def process_callback_replenish_balance(callback_query: types.CallbackQuery, state: FSMContext): tg_id = callback_query.from_user.id + # Проверяем, есть ли у пользователя ключи + key_count = await get_key_count(tg_id) + + # Если ключей нет, проверяем, существует ли запись с таким tg_id + if key_count == 0: + exists = await check_connection_exists(tg_id) + # Если записи нет, создаем нового клиента в базе данных + if not exists: + await add_connection(tg_id, balance=0.0, trial=0) + await state.set_state(ReplenishBalanceState.choosing_transfer_method) keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text='По реквизитам', callback_data='transfer_method_requisites')], - [InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_profile')] # Кнопка "Назад" + [InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_profile')] ]) await callback_query.message.edit_text( @@ -54,13 +61,10 @@ async def process_callback_replenish_balance(callback_query: types.CallbackQuery ) await callback_query.answer() - @router.callback_query(lambda c: c.data == 'back_to_profile') async def back_to_profile_handler(callback_query: types.CallbackQuery, state: FSMContext): await process_callback_view_profile(callback_query, state) - - @router.callback_query(lambda c: c.data.startswith('transfer_method_')) async def process_transfer_method_selection(callback_query: types.CallbackQuery, state: FSMContext): data = callback_query.data.split('_', 2) @@ -76,7 +80,7 @@ async def process_transfer_method_selection(callback_query: types.CallbackQuery, [InlineKeyboardButton(text='100 RUB', callback_data='amount_100')], [InlineKeyboardButton(text='300 RUB', callback_data='amount_300')], [InlineKeyboardButton(text='500 RUB', callback_data='amount_500')], - [InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_profile')] # Кнопка "Назад" + [InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_profile')] ]) await callback_query.message.edit_text( @@ -90,7 +94,6 @@ async def process_transfer_method_selection(callback_query: types.CallbackQuery, await send_message_with_deletion(callback_query.from_user.id, "Неверный метод перевода.", state=state, message_key='transfer_method_error_message_id') return - @router.callback_query(lambda c: c.data.startswith('amount_')) async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext): data = callback_query.data.split('_', 1) @@ -102,18 +105,15 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F amount_str = data[1] amount = int(amount_str) - # Получаем ID сообщения с кнопками выбора суммы state_data = await state.get_data() amount_selection_message_id = state_data.get('amount_selection_message_id') - # Удаляем сообщение с кнопками выбора суммы if amount_selection_message_id: try: await bot.delete_message(chat_id=callback_query.from_user.id, message_id=amount_selection_message_id) except Exception as e: print(f"Ошибка при удалении сообщения: {e}") - # Обновляем состояние и сохраняем выбранную сумму await state.update_data(amount=amount) await state.set_state(ReplenishBalanceState.waiting_for_admin_confirmation) @@ -126,10 +126,9 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F "После перевода отправьте чек и дождитесь подтверждения." ) - # Редактируем текущее сообщение await callback_query.message.edit_text( text=message, - reply_markup=None # Или добавьте клавиатуру, если нужно + reply_markup=None ) admin_keyboard = InlineKeyboardMarkup(inline_keyboard=[ @@ -147,8 +146,6 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F await send_message_with_deletion(ADMIN_ID, admin_message, reply_markup=admin_keyboard, state=state, message_key='admin_request_message_id') await callback_query.answer() - - @router.callback_query(lambda c: c.data.startswith('admin_')) async def process_admin_confirmation(callback_query: types.CallbackQuery, state: FSMContext): if callback_query.from_user.id != ADMIN_ID: @@ -169,35 +166,26 @@ async def process_admin_confirmation(callback_query: types.CallbackQuery, state: try: user_id = int(user_id_str) - # Получаем данные из состояния для удаления сообщения с реквизитами state_data = await state.get_data() requisites_message_id = state_data.get('requisites_message_id') if action == 'confirm': - # Подключаемся к базе данных PostgreSQL conn = await asyncpg.connect(DATABASE_URL) try: - # Обновляем баланс пользователя await update_balance(user_id, amount) - - # Получаем обновленный баланс balance = await get_balance(user_id) - # Создаем клавиатуру с кнопкой "Профиль" profile_button = InlineKeyboardButton(text='Профиль', callback_data='view_profile') profile_keyboard = InlineKeyboardMarkup(inline_keyboard=[[profile_button]]) - # Отправляем уведомление с кнопкой "Профиль" администратору await send_message_with_deletion(callback_query.from_user.id, f"Баланс пользователя успешно пополнен на {amount} RUB.\nТекущий баланс: {balance}", state=state, message_key='admin_confirm_message_id') - # Отправляем уведомление пользователю с кнопкой "Профиль" await bot.send_message( user_id, f"Ваш баланс был успешно пополнен на {amount} RUB.", reply_markup=profile_keyboard ) - # Удаляем сообщение с реквизитами requisites_message_id = state_data.get('requisites_message_id') if requisites_message_id: try: @@ -218,4 +206,4 @@ async def process_admin_confirmation(callback_query: types.CallbackQuery, state: finally: await state.clear() - await callback_query.answer() \ No newline at end of file + await callback_query.answer() diff --git a/key_management.py b/key_management.py index 2336a92e..ef667ddb 100644 --- a/key_management.py +++ b/key_management.py @@ -9,7 +9,6 @@ from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import (CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message) -from pytz import timezone from auth import link, login_with_credentials from bot import bot, dp @@ -20,6 +19,7 @@ from database import (add_connection, get_balance, has_active_key, store_key, update_balance) from handlers.profile import process_callback_view_profile from handlers.start import start_command +from handlers.notifications import send_notification router = Router() @@ -46,16 +46,22 @@ async def process_callback_create_key(callback_query: CallbackQuery, state: FSMC if trial_status == 1: await callback_query.message.edit_text( - "У вас уже был пробный ключ. Новый стоит 100 рублей и сразу на месяц. \n\n" - "Хотите продолжить?", + "⚠️ У вас уже был пробный ключ.\n\n" + "Новый ключ будет выдан на один месяц и стоит 100 рублей.\n\n" + "Хотите продолжить?", + parse_mode="HTML", # Добавляем параметр parse_mode reply_markup=InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text='Да, создать новый ключ', callback_data='confirm_create_new_key')], - [InlineKeyboardButton(text='Назад', callback_data='cancel_create_key')] + [InlineKeyboardButton(text='✅ Да, создать новый ключ', callback_data='confirm_create_new_key')], + [InlineKeyboardButton(text='↩️ Назад', callback_data='cancel_create_key')] ]) ) await state.update_data(creating_new_key=True) else: - await callback_query.message.edit_text("Вам будет выдан пробный ключ. Пожалуйста, выберите имя для вашего ключа:") + await callback_query.message.edit_text( + "🎉 Вам будет выдан пробный ключ на 24 часа!\n\n" + "Пожалуйста, введите название для вашего пробного ключа:", + parse_mode="HTML" + ) await state.set_state(Form.waiting_for_key_name) await callback_query.answer() @@ -77,7 +83,7 @@ async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContex await state.clear() return - await callback_query.message.edit_text("🔑 Пожалуйста, выберите имя для вашего нового ключа:") + await callback_query.message.edit_text("🔑 Пожалуйста, введите имя нового ключа:") await state.set_state(Form.waiting_for_key_name) await state.update_data(creating_new_key=True) @@ -113,7 +119,6 @@ async def handle_text(message: Message, state: FSMContext): async def handle_key_name_input(message: Message, state: FSMContext): tg_id = message.from_user.id - key_name = sanitize_key_name(message.text) if not key_name: @@ -140,7 +145,7 @@ async def handle_key_name_input(message: Message, state: FSMContext): if trial_status == 0: # Создаем пробный ключ на 1 день - expiry_time = int((current_time + timedelta(days=1, hours=3)).timestamp() * 1000) + expiry_time = current_time + timedelta(days=1, hours=3) else: # Проверяем баланс перед созданием нового ключа balance = await get_balance(tg_id) @@ -152,26 +157,25 @@ async def handle_key_name_input(message: Message, state: FSMContext): return await update_balance(tg_id, -100) - expiry_time = int((current_time + timedelta(days=30, hours=3)).timestamp() * 1000) + expiry_time = current_time + timedelta(days=30, hours=3) + + expiry_timestamp = int(expiry_time.timestamp() * 1000) try: # Попробуем добавить клиента - response = add_client(session, client_id, email, tg_id, limit_ip=1, total_gb=0, expiry_time=expiry_time, enable=True, flow="xtls-rprx-vision") + response = add_client(session, client_id, email, tg_id, limit_ip=1, total_gb=0, expiry_time=expiry_timestamp, enable=True, flow="xtls-rprx-vision") - # Проверяем статус ответа от сервера if not response.get("success", True): error_msg = response.get("msg", "Неизвестная ошибка.") if "Duplicate email" in error_msg: await message.bot.send_message(tg_id, "❌ Этот email уже используется. Пожалуйста, выберите другое имя для ключа.") - await state.set_state(Form.waiting_for_key_name) # Возвращаем пользователя к вводу имени ключа + await state.set_state(Form.waiting_for_key_name) return else: raise Exception(error_msg) - # Если добавление клиента прошло успешно, получаем ссылку connection_link = link(session, client_id, email) - # Проверка существующей записи conn = await asyncpg.connect(DATABASE_URL) try: existing_connection = await conn.fetchrow('SELECT * FROM connections WHERE tg_id = $1', tg_id) @@ -180,11 +184,21 @@ async def handle_key_name_input(message: Message, state: FSMContext): await conn.execute('UPDATE connections SET trial = 1 WHERE tg_id = $1', tg_id) else: await add_connection(tg_id, 0, 1) - finally: await conn.close() - await store_key(tg_id, client_id, email, expiry_time, connection_link) + await store_key(tg_id, client_id, email, expiry_timestamp, connection_link) + + # Рассчитываем оставшееся время до окончания действия ключа + remaining_time = expiry_time - current_time + days = remaining_time.days + hours, remainder = divmod(remaining_time.seconds, 3600) + minutes, _ = divmod(remainder, 60) + + # Формируем сообщение с информацией о ключе + remaining_time_message = ( + f"Оставшееся время ключа: {days} день" + ) keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text='📖 Инструкции по использованию', callback_data='instructions')], @@ -193,8 +207,10 @@ async def handle_key_name_input(message: Message, state: FSMContext): key_message = ( "✅ Ключ успешно создан:\n" - f"
{connection_link}
" + f"
{connection_link}
\n\n" + f"{remaining_time_message}" ) + await message.bot.send_message(tg_id, key_message, parse_mode="HTML", reply_markup=keyboard) except Exception as e: @@ -202,9 +218,6 @@ async def handle_key_name_input(message: Message, state: FSMContext): await state.clear() - - - @dp.callback_query(F.data == 'instructions') async def handle_instructions(callback_query: CallbackQuery): instructions_message = (