From 73c4fc37f78d2bfae18aa2a0e74675549d8290e8 Mon Sep 17 00:00:00 2001 From: yarok Date: Thu, 9 Jul 2026 23:34:34 +0300 Subject: [PATCH] initial --- Dockerfile | 14 + __pycache__/api.cpython-313.pyc | Bin 0 -> 5554 bytes __pycache__/config.cpython-313.pyc | Bin 0 -> 872 bytes __pycache__/database.cpython-313.pyc | Bin 0 -> 15478 bytes __pycache__/formatting.cpython-313.pyc | Bin 0 -> 1806 bytes __pycache__/keyboards.cpython-313.pyc | Bin 0 -> 5321 bytes __pycache__/logger.cpython-313.pyc | Bin 0 -> 392 bytes __pycache__/schedule.cpython-313.pyc | Bin 0 -> 1429 bytes __pycache__/states.cpython-313.pyc | Bin 0 -> 959 bytes api.py | 193 +++++ bot.py | 95 +++ config.py | 31 + data/users.db | Bin 0 -> 8196 bytes database.py | 501 +++++++++++ database/database.db | Bin 0 -> 28672 bytes docker-compose.yml | 15 + formatting.py | 76 ++ handlers/__init__.py | 0 handlers/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 140 bytes handlers/__pycache__/admin.cpython-313.pyc | Bin 0 -> 5565 bytes handlers/__pycache__/group.cpython-313.pyc | Bin 0 -> 27905 bytes handlers/__pycache__/start.cpython-313.pyc | Bin 0 -> 3105 bytes handlers/__pycache__/support.cpython-313.pyc | Bin 0 -> 2918 bytes handlers/admin.py | 168 ++++ handlers/group.py | 803 ++++++++++++++++++ handlers/start.py | 77 ++ handlers/support.py | 82 ++ keyboards.py | 229 +++++ logger.py | 12 + logs/bot.log | 128 +++ requirements.txt | 27 + schedule.py | 41 + states.py | 13 + 33 files changed, 2505 insertions(+) create mode 100644 Dockerfile create mode 100644 __pycache__/api.cpython-313.pyc create mode 100644 __pycache__/config.cpython-313.pyc create mode 100644 __pycache__/database.cpython-313.pyc create mode 100644 __pycache__/formatting.cpython-313.pyc create mode 100644 __pycache__/keyboards.cpython-313.pyc create mode 100644 __pycache__/logger.cpython-313.pyc create mode 100644 __pycache__/schedule.cpython-313.pyc create mode 100644 __pycache__/states.cpython-313.pyc create mode 100644 api.py create mode 100644 bot.py create mode 100644 config.py create mode 100644 data/users.db create mode 100644 database.py create mode 100644 database/database.db create mode 100644 docker-compose.yml create mode 100644 formatting.py create mode 100644 handlers/__init__.py create mode 100644 handlers/__pycache__/__init__.cpython-313.pyc create mode 100644 handlers/__pycache__/admin.cpython-313.pyc create mode 100644 handlers/__pycache__/group.cpython-313.pyc create mode 100644 handlers/__pycache__/start.cpython-313.pyc create mode 100644 handlers/__pycache__/support.cpython-313.pyc create mode 100644 handlers/admin.py create mode 100644 handlers/group.py create mode 100644 handlers/start.py create mode 100644 handlers/support.py create mode 100644 keyboards.py create mode 100644 logger.py create mode 100644 logs/bot.log create mode 100644 requirements.txt create mode 100644 schedule.py create mode 100644 states.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5ea80c9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.13-slim + +WORKDIR /app + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["python", "bot.py"] \ No newline at end of file diff --git a/__pycache__/api.cpython-313.pyc b/__pycache__/api.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d8ca08b2fd65ebe82b072c07e91cc5669d87875 GIT binary patch literal 5554 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 aEBLs(@p+z8iiFYAB5?*TjK3+c_xuaYTyehu literal 0 HcmV?d00001 diff --git a/__pycache__/config.cpython-313.pyc b/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..937b28e0691ae04125ee9d410e72136c9d12e388 GIT binary patch literal 872 zcma)(&r2IY6vy9glKq_@Hdb#Xq?ZPoY_&+SXhCCIQ%z#CNkI_8YSy?m?uOYYwx>u- z5ij=Af1{LA3q5$~sn-QB=@h|ID0qveP9 z1MuD3^agj~bcZqX8Z^+D86e)u&M*&I!p2~Mjl;;rG2B71SJTtLH4d6j^N+CSp`!R# z>cZ6mBiO+kQFsgvtKy8pbU%OvWw>#kf+{NH6o7?59HYI8IW{){AVUmmEFj!}5^=qH z!vB++*MuH55j2sAnna`p{*p&P78!3R_p}VA#yI#Y9?3E!lw*%PpJ1q5xAkhJYBvpQ zgNhSVxop0Yozf^!<|Y*<3#Ce_Fq6qseqt_LDHms--Ck)n8_r!--L#U))ggT`Sxi$kZ!7a!#oaSH!~6fSig?mrE1LGclSK=*8S+dclX@=nENi4 zLX?C;S=*u9IT-O_73FHi8D+p})XgUKnO2kdF+q6G;FW=fi-LVeu;A7oGc=xsAU1rWrp3;}s7z0JJ5f5Kw;#5>RuPp$x?pd<&u z@NRY|`|8Po5^sq|lCqoFNwodJwto~G$ZU(pw$ZDt@KJQog~$&+@cZ^Z)<= literal 0 HcmV?d00001 diff --git a/__pycache__/database.cpython-313.pyc b/__pycache__/database.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8fb4da4278d24945bc2980b2dd6f59f78ed02741 GIT binary patch literal 15478 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 U>-j|dO2B2Mzn=B0uiy>;FF|TZ>i_@% literal 0 HcmV?d00001 diff --git a/__pycache__/formatting.cpython-313.pyc b/__pycache__/formatting.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f55d7aaa4960ce0a21bd039994d0674a1e3213d7 GIT binary patch literal 1806 zcmah}-EZ4e6u*ugC$SyJZJMU-%I2zF*A&)1RAoi2ihwc-qkX7lG^r0$nz$b$i6eiY zm8Z~&lA>uz#HNH7Dl~XNyi~Agg|hBnaM+8ngftBa#1pA9-Z?j4?GP1L@%8cV{LZ=O zo{!Z0eg?sM?cIgzWj8{<;g8mGHHBI?2zQZ$B;pEshtLQSNv=Mmxz@-C^(Q7p2#1X; zt5Gvy#5ZC_jKE-X1>jA>Q)9s2T_mHWliDUpBF52V^Sl%x6C+JL4%{`@GPbX;WvV4j zHoc@=G7&`ceF&vU*(H+_3D-dQS@N{F$8VWz*@sZyj3qt%sGIXg#p@uu$2u z_N^a8>!DSyY>k;ApaY`}s$17ESpPV%_8nx>pku1LpM5NT=wg`X+7LHa5S|(b8*W31 zz)b>yxEBBJ0SR0kT0g-V8LN1Fa3%K*?r!3Sj{8k?hQ`;`q%s~&tGb~k#`V~`5;qgd z_^PTUqlU4OS{qAmI^LN?G`SLw&N>4P#kCf3Y$l<`q6vK#ga$=3V-IBtk>ukr~*cVHcw*v9%1g2sv2HoNfWeQUq+QDqwv_6Rup zm~V!$q5ye%fOEJ9Y5fl0512U<#s*oB;L2q%z=_|(O2~NGI;h-oh>K#c((r9&po|)Pi$ws)D{b0wZ+`GqKT)OVV>T}{O^xpBD6!UDTane-k}m3y5Z2# zbXrNp9ruQzBz1?@Rn1W15#FKVQ6sA1GGcKPR0&1b)s#aS$_>LI0dqVJNOve~?a(PR zxuR%}TL(*^V80&ST)D2q4981Q(CjS z3;rTpdgdlWOm?oypzgln)xuR<7`269%}sXm*~@^2dW-Xgd0RMZ3!Q+5`0Sg22E)aX z!iX*OH_#xTeFM<&nc{k3-4=#zAp+bmpMC2D`clJh6Yc6LP8FtXVW@$2@el)t{2g|% zH$PIkU3kItVvpT(F24!Vz;jZ6{sX|yPFH>Ij_D)PI;T-C?@Us~Gdimcg949ny|Pu3 QAc$YlV2z$92%*mZ2cr_}p#T5? literal 0 HcmV?d00001 diff --git a/__pycache__/keyboards.cpython-313.pyc b/__pycache__/keyboards.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2f1faa66a7dbd237bede5dd38c96bd99bc79df72 GIT binary patch literal 5321 zcmbVQYit|G5#D?9juc;_9+o0IXGKxXxG|$TZYtG{8`*GdsZwEISQtb@7+0A!P*PkAr9E;N!Fe}9cEu7-c090>~5w6S`K`JXcr}MpBO<%x%gs-89 zi~=-W(&b*hi-cXn={6W$y!6^jnzdl%@~5nitR<*()^%&ix_SjxoQl7YN^4B&^*Wck`M285s0Y{>*=Is!!iM`=6yb&AMcLXuSu&tND!eN9&rEd*%vn zD-$Kf+I|{0$7kZBWdf)Kcfyo)h1`KAu^);YPfo=n#&pt5B_|_h?09@EJsFRb$Cx{& zPukuoV?0rrhBm`MA3SD1R2lM_XUm#Qf)83sxbI7)62i z`idT<(OqmLZJSq{Jq>b^cqEyIV4Ay50?&+{#5QQ$>m(vvgwq9GWO|l{Yc%+3{+#tT zs3o^fgVY0)K0|Q>G>BCrR=5TVw~W|6GiA_Jb+@fR?10A%Lsh$5wn?@rL4-}YaZ_Fw zZpyXMrqCMMrsknhyQy8!$C4mXau_A*3L2d*^zz!5@kKDTG9TYkS9Ez-$&}d)Xy$dL zyaLBqQ5BA{UIrt4m)Y3${M?KiuT@_vIcA&$OKJcq+Z!_`CyyGjiBaGgX;fA5(uLPF z>sMghA21Vs4~`&{e|g5sMuU7jF=o&e9z??;RdE*isB`6IW9+%KnX*;G9GzmQ02h~^ zIBMeZHRp8LG}Xh=fb%tYOdTpX9CgbLRm=1*i}EiHo;q0Yb{1MXixQE1t3;4wI3vmB zFuDsZ4{yMjHD3JQvd25O`|R#>Pg-J|W1AHufS2Hk*)g7m#v3F;5?qvo$bkqPARpxp z*UEwfmMS_#d7Z-(@&PVGTPU@~op6NEm!1AjUU&8K5w3b{Lp_9eh(<;##~bBzrI&*P zQ-^K!7)_?9hmEOt8f{zKW?jpl0i~bPY&n)pQ!{RJ;Tf+PpNz*+qfEHZcCr71`zQhm zK#+f&;cZ2=VZoM->1jAE2B8d0k;aCZ)DNR<$?2dvG_7<%euq|>hi(UMA3WwUs4`^P zAIu!M6>OcAmX*Mq|E&MBKv$t->%#VY#}4a($l_0yc743*{XJIS;9q*IwxL2-PwwF* zHUEu%t7os(xo=exf{jH%^7=j(T&jCjB8{8YT1bOd*rcrrJl;OObo*@aWZa;(G?h%G zj>pj8%Eb!}c7_&E*%|T?@Sumdgb?NLeS$}ND(45MxKnV@r}nJwHv?W)M69Y+=<+&> zYaK;dM^V>N-0LVFhZ1pBZD~X-tKl%>Y+dOjN4Q-jT_wb*2$nqomMwvu%TWn>--n(n z>e3tfju2uzKuA=+x5f~>aKF_R40YXq)Ck@)R-%=)>)t5d9AA`UwF~qw*Ufy$xhET+ zL9_-4ho(xBobG=V;;e)cB?&dEl=cN6sRw|j0vAvXI4B3}Nw`7I6LsrBwttlK1I+_m zuqxo($A0uiJrq^h`W0BeSZ4`MdUGA61>`+U-d&cD2vuE<;EiVm@S~L>!lvP{*CuHM zXzLFUM7-l5a2h?H^;-btchE0dALL(7D@b~i@lQJgvJhlsKnA&J9>}%ccLeA2uUOa1 zqp*WQ>@`_<$-96#r}C%M0UU*%<2tzBIdsAx#om1paH&dS1Ak+Ec(=^!Fc1P6XPC_a zf{h8soW}3VluPBtW$W#-@F|LRJ)@R=P@m29tsq>l z;8Io)6xfzOed{HNu4ZJ08+~cV9nLLiY*Q6?X#2|Zj53$#483?6hSF}BDlAisUKn`V zN?8^{DTYQRH)soHiaOBRK);USXk+w2sBGu`rY*o&TYNqqpRfg*eBKto|Jv?p8vpSq zo6A;Nn_-pVItoqg=ZCU;7PcciG>VHO{?I2!t-U&%19n zZ7Xcn3f((kea$VO3lcaF9|<;b>&)eCzhI*TuUcD4t*?H-TSLU zR+~xFreY^iJ#*?=HT%#jzSZq8NpS~hY@H9DZ_5l60!^8N%dTMd(3zhXnp0TB_nT5wLqWjd{0q z5isa1K;ztuWTubyJ;o4-!u!YyPL@V6v9dEFGM&zO3ht1EB^K3E%o%Kn8i`|Zx*6JZ z3o?1wIXwKl?(iDQD#LC4TXzvua-1eGl#pWjDl8zP6%Ly0o)KHkf@Xdt!d;-^Gsg z;V>+yQod@cMiFlBmx=|<^z?MHHW|az{2DwaMwQiMtkCv2m^cQ8BIKBY4C$B;)3Kn6 z0ijwB2-VbhY$Bc-9UqG!?w1RCK^6|~hequUs0q;^2gBe4ED)@;hK_-Ig&u*R9g=>| z$)lQKw&JB%-_op$`P1MauR_fd`WY|94*Kq~m64GzOm3h^4{;OGYTh9dD2h7xudcugemr|g<@V!HLTF=ZDBl- zqUfqAf&|4o!OoiEHtIiTD7&YU_(kVQ`UBvi*_%Itswi+A_XRI;{2c<-7Y&4a@?XUN z32FN`+4l+g)@|9#$+tUHs9FMC`|a)^Cl|Ya$Z_)g-Xg*OMPZHo-~Jij00JEz5NP)U Q-&`Z`cc&93VCnP!0ZaM~&Hw-a literal 0 HcmV?d00001 diff --git a/__pycache__/logger.cpython-313.pyc b/__pycache__/logger.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eeb466376b5eb396f456eee4c9cddd941cdcd81c GIT binary patch literal 392 zcmey&%ge<81g)?9vX%qs#~=<2ut6E0(}0Yr48aV+jNS}I3^5ErOfV$~T8SZ;2`IyY z5P`9RnS)s@nH50#U?#$(fV2{WDU?^llFq2f`Vz$RtK!MYPcPO_$}iCaGO8q1H4=-H zOEPm)HH#H$6jU{GQ;Ule(}6rq=35+TnK`L>iMgq_SaVX#Qgd#xrR5jpCYETj-eL!8 zPS4Cszr~%DSe%*coS&DLnSP7K)6dQS7H4{Di4RaZwdfWbNLgypN`}uM%Wo;@XXNLm z>L(WD7nkJc=ocqvq^6YSr0PRd=@nGo;;_lh2MO5~@c}h30&y|O7ay1z85wUg@O@xo zVC1;YAbg2I_y(`^4KBfcolczxEb^Z?Ss9r=2rzI7Ugwm$#3^-|Q>KxvhzF<>0HLT| AGynhq literal 0 HcmV?d00001 diff --git a/__pycache__/schedule.cpython-313.pyc b/__pycache__/schedule.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5e73f4ddb416afd03dd06fc0706cf7ba43df954d GIT binary patch literal 1429 zcmb_cO>7%g5T3Vx-o$@h$FopWd!3 zf`nAjOT?w3sDcwG4&i1fND-l}TA#SBDN?E}NI(N9;*!*n%Fl&)j+gLz<4JGkdvE5= z%$u3_I(~mY;NZTy^va7oz$3PB2iF6g83Z>V0~wMA$~c!MDUNa}p7JVN^JyU^P@#*3 zw3w2pgjgH`>RBQQX^R<}OK^^`s9T29XO$|(qEWW=1aWx1UP7iU8PFgas%I&HS3AYKD-}-Y}+;+<9*H7W7bo6yPjB0tJFrr=H~tBq7f= zomA>U&_8GR5w{8f1Hoz(Tz z+b?uPJ{o!gyb{`GWY_nvTJKLcW9_4p?eOHj{$r-U6NN(~yT>6IxmJ0z^6qo@1F;P` zX3Mdiv)~PWtBmhNn3fO8&%6B)9`1nP4{aVe_#pDkYI1d^Ilo4lrX4>0gy)pdV?grT zwumaf?)acD{)<_}`@Rv>&XKo><`=GyAzDA74RCkIh!&LYCQc*1cZ_I?bnj__;lwbi zv?QXP5n3J(!$E@kEu~8b`n74XHLPfpV(X;Dq?01snHCTyWXhhBBeSoIeO%q5cdNbE z5lo=JteZx+W4cB80RJf*l)WKGK2xc&W;sWPaLM}6j3C+O2_X+5@FN`i9tOUK(T=2& JI6Z`--vKA$FE;=H literal 0 HcmV?d00001 diff --git a/__pycache__/states.cpython-313.pyc b/__pycache__/states.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e20e50fe08a8907961f00349f197c4baa48a5062 GIT binary patch literal 959 zcmbtT&ui2`6rP#mk>LA#Ss2Wb>nM5~SyX=R)bx zKfpi9xrln|P1tz%y~zev@E~*uZ{GXf%zNK_Gg%J?J%;1+kJ*P2XY3~h-EcO}dWp^r z3t7nDuoDjaM1UAmEHFl&urYhWCM>iLSZL41k=5=6%b*UT?iOkrbO%vy3w4rRzjsR; zeW9JOimF)enMoZ(QJ1~|8OR0NOX2of}BEK)?c^$1bnW2=+6jZfJ^EfZ0G<@{% zW0Y2Dp2#d#DoSGgSW1OQNEb4yDoEegRjhEahrQNBNRNZFB8!8lEL2sj0<}1csb4_5 zVl^!d1yDPn#{qj}`eaCXJ?KuF z(51jVN|>F7(tQdac82f`@vo1bULRk+ZHd!@yApqWnx%P5+%TkG|3@6+a&_@f;`>CL z3{~tD-zHAEneXZcQCcJrWz(g~rsl?sIPC*9_hvLO_5Kz|doSCE^$HHK82)rj1!#U* kobzw&Iev{T__M1Q4a2GNI6u4^H4LZ573BX${Ltw52gsDcAOHXW literal 0 HcmV?d00001 diff --git a/api.py b/api.py new file mode 100644 index 0000000..0098e72 --- /dev/null +++ b/api.py @@ -0,0 +1,193 @@ +import httpx + +from datetime import date +from cachetools import TTLCache + +from config import ( + API_URL, + REQUEST_TIMEOUT, + GROUPS_CACHE_TTL, +) + +from database import ( + get_cached_schedule, + save_schedule_cache, +) + + +def get_schedule_ttl(day: date) -> int: + """ + Возвращает TTL кэша в минутах. + """ + + today = date.today() + + if day == today: + # Сегодня — обновляем каждый час + return 60 + + if day > today: + # Будущие даты — раз в сутки + return 60 * 24 + + # Прошедшие даты — неделю + return 60 * 24 * 7 + + +class ScheduleAPI: + + def __init__(self): + + self.client = httpx.AsyncClient( + timeout=REQUEST_TIMEOUT + ) + + self._groups_cache = TTLCache( + maxsize=1, + ttl=GROUPS_CACHE_TTL + ) + + async def close(self): + await self.client.aclose() + + async def _get( + self, + endpoint: str, + params: dict | None = None + ): + + response = await self.client.get( + f"{API_URL}/{endpoint}", + params=params + ) + + response.raise_for_status() + + return response.json() + + async def get_groups(self): + + if "groups" in self._groups_cache: + return self._groups_cache["groups"] + + response = await self._get("Groups") + + groups = ( + response + .get("data", {}) + .get("groups", []) + ) + + self._groups_cache["groups"] = groups + + return groups + + async def get_group_by_id( + self, + group_id: int + ): + + groups = await self.get_groups() + + for group in groups: + + if group["groupID"] == group_id: + return group + + return None + + async def search_group_by_name( + self, + text: str + ): + + groups = await self.get_groups() + + text = text.upper().strip() + + exact = [ + g + for g in groups + if g["groupName"].upper() == text + ] + + if exact: + return exact + + return [ + g + for g in groups + if text in g["groupName"].upper() + ] + + async def get_schedule( + self, + group_id: int, + day: date | None = None + ): + + request_day = day or date.today() + + schedule_date = request_day.isoformat() + + ttl = get_schedule_ttl(request_day) + + # Проверяем кэш + cached = await get_cached_schedule( + group_id, + schedule_date, + ttl + ) + + if cached is not None: + print(f"[CACHE] {group_id} {schedule_date}") + return cached + + print(f"[API] {group_id} {schedule_date}") + + params = { + "idGroup": group_id, + "sdate": schedule_date + } + + response = await self._get( + "Rasp", + params + ) + + schedule = response.get( + "data", + {} + ) + + # Сохраняем в кэш + await save_schedule_cache( + group_id, + schedule_date, + schedule + ) + + return schedule + + async def get_teachers(self): + + response = await self._get("Prepods") + + return ( + response + .get("data", {}) + .get("prepods", []) + ) + + async def get_auditories(self): + + response = await self._get("Auds") + + return ( + response + .get("data", {}) + .get("auds", []) + ) + + +api = ScheduleAPI() \ No newline at end of file diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..eb698fb --- /dev/null +++ b/bot.py @@ -0,0 +1,95 @@ +import asyncio +import logging +from schedule import refresh_cache +from aiogram import Bot, Dispatcher +from aiogram.enums import ParseMode +from aiogram.client.default import DefaultBotProperties +from aiogram.fsm.storage.memory import MemoryStorage +from config import BOT_TOKEN +from api import api +from handlers.support import router as support_router +from handlers.start import router as start_router +from handlers.group import router as group_router +from handlers.admin import router as admin_router +from logger import logger +from database import ( + init_db, + clear_old_cache +) + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s | %(levelname)s | %(name)s | %(message)s" +) + +bot = Bot( + token=BOT_TOKEN, + default=DefaultBotProperties( + parse_mode=ParseMode.HTML + ) +) + +dp = Dispatcher( + storage=MemoryStorage() +) + +dp.include_router(start_router) +dp.include_router(group_router) +dp.include_router(support_router) +dp.include_router(admin_router) + + +#стартует бот +async def on_startup(): + + await init_db() + + deleted = await clear_old_cache() + + logger.info("База данных инициализирована") + + logger.info( + f"Удалено {deleted} записей кэша" + ) + + asyncio.create_task( + refresh_cache() + ) + + logger.info( + "Фоновое обновление кэша запущено" + ) + + +async def on_shutdown(): + + await api.close() + + await bot.session.close() + + logger.info("Бот остановлен") + + +async def main(): + + await on_startup() + + try: + + logger.info("Бот запущен") + + await dp.start_polling(bot) + + finally: + + await on_shutdown() + + +if __name__ == "__main__": + + try: + asyncio.run(main()) + + except KeyboardInterrupt: + logger.info("Остановка...") + diff --git a/config.py b/config.py new file mode 100644 index 0000000..77a5197 --- /dev/null +++ b/config.py @@ -0,0 +1,31 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + + +ADMIN_IDS = [ + int(x) + for x in os.getenv( + "ADMIN_IDS", + "" + ).split(",") + if x +] + +load_dotenv() + + +BOT_TOKEN = os.getenv("BOT_TOKEN") + +API_URL = os.getenv( + "API_URL", + "https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api" +) + +REQUEST_TIMEOUT = 15 + +GROUPS_CACHE_TTL = 3600 # 1 час + +if not BOT_TOKEN: + raise RuntimeError("BOT_TOKEN не найден в .env") \ No newline at end of file diff --git a/data/users.db b/data/users.db new file mode 100644 index 0000000000000000000000000000000000000000..c052a5adbe550ea46a2c87a4f29b9e7f6188dc57 GIT binary patch literal 8196 zcmeI#y9&ZE6b9g=2tpU#x}Hr1b@2tP5~Nr~YY;n0#VQ4BwU@t4>f>f*H}Qv-nj z1Rwwb2tWV=5P$##AOHaflt|!yyEb^$Fbu9voz&YL{4DX~q*%0}BTOksngd% ttl_minutes: + + return None + + + return json.loads(row[0]) + +async def save_schedule_cache( + group_id: int, + schedule_date: str, + data: dict +): + + async with aiosqlite.connect(DB_NAME) as db: + + await db.execute( + """ + INSERT INTO schedule_cache( + group_id, + date, + data, + created_at + ) + + VALUES(?,?,?,?) + + ON CONFLICT(group_id, date) + + DO UPDATE SET + + data = excluded.data, + created_at = excluded.created_at + """, + ( + group_id, + schedule_date, + json.dumps( + data, + ensure_ascii=False + ), + datetime.now().isoformat() + ) + ) + + await db.commit() + + +async def clear_old_cache(days: int = 30): + + async with aiosqlite.connect(DB_NAME) as db: + + cursor = await db.execute( + """ + DELETE FROM schedule_cache + WHERE datetime(created_at) + < datetime('now', ?) + """, + (f"-{days} days",) + ) + + await db.commit() + + return cursor.rowcount + + +async def get_active_groups(): + + async with aiosqlite.connect(DB_NAME) as db: + + cursor = await db.execute( + """ + SELECT DISTINCT group_id + FROM users + WHERE group_id IS NOT NULL + """ + ) + + rows = await cursor.fetchall() + + return [row[0] for row in rows] \ No newline at end of file diff --git a/database/database.db b/database/database.db new file mode 100644 index 0000000000000000000000000000000000000000..603f836d2e7c6be9fa69400de4d837ac4ac38d41 GIT binary patch literal 28672 zcmeI%J#X4T7zc1W5M0p^W%H2bwqSuOMOBBYOKEbg+=3yFErrQ)@FhV413p`gx>QM{ z_8ax%^t1GAUV@E*r4#+1Wbnb`v+wsjhp>Ek(d>te+>C;u8&v%sTO zQq<#P`RRM;UJn?L#^X^C&fb(WlQt|$tj32XC9@B!weuO1e07C1}C8f}XhcC(oiI%J%?w~ScyO2n*>bAA4m1xZ-WC_Hq- zh))MOrym=3(;_FMG4oGyf_njTL*_Z+-?^o?=%v+Yb;$gvGut%9q@2Y!>Dg%yg?&Xm z)#Qh_ecxmEeed4ky*uVb1LpKxv6t?ZX3)BEPOr#nbV|H!ko3{j+h7!pg=C$0;*zyR zbf;KSYc;t$P3>eLZa5syCK2;{jHkQ%=~Sn`torVd)y0w8qqV1~nkGN>rekiHQz1QK zD)Qge(0uBP0^Cp>G0xTs5<%|Ppyk@wQjxZyU)XddMT;E8OG$fID5-5tmXczbWyX!d zQPR&;j&qvMetc*6BK`^cQIi!_Rpp;=lHA0rizhF8sZ1mveMgY(MGa%?f00Izz00bZa0SG_<0uX=z1a?pW&;NHYshAQ3AOHafKmY;| PfB*y_009UspoM* literal 0 HcmV?d00001 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6f57846 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +services: + schedule_bot: + build: . + container_name: schedule_bot + restart: unless-stopped + + env_file: + - .env + + volumes: + - ./database.db:/app/database.db + + dns: + - 8.8.8.8 + - 1.1.1.1 diff --git a/formatting.py b/formatting.py new file mode 100644 index 0000000..e836613 --- /dev/null +++ b/formatting.py @@ -0,0 +1,76 @@ +from collections import defaultdict + + +def format_schedule(data): + + rasp = data.get("rasp", []) + + if not rasp: + return "🎉 Сегодня занятий нет." + + + days = defaultdict(list) + + + for lesson in rasp: + + days[ + lesson["день_недели"] + ].append(lesson) + + + text = "" + + + for day, lessons in days.items(): + + text += f"\n📅 {day}\n\n" + + + for item in sorted( + lessons, + key=lambda x: x["номерЗанятия"] + ): + + number = item.get( + "номерЗанятия", + "?" + ) + + start = item.get( + "начало", + "" + ) + + end = item.get( + "конец", + "" + ) + + subject = item.get( + "дисциплина", + "Без названия" + ) + + teacher = item.get( + "преподаватель", + "-" + ) + + room = item.get( + "аудитория", + "-" + ) + + + text += ( + f"{number}️⃣ " + f"{start}-{end}\n" + f"📘 {subject}\n" + f"👤 {teacher}\n" + f"🏫 {room}\n\n" + "────────────\n\n" + ) + + + return text \ No newline at end of file diff --git a/handlers/__init__.py b/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/handlers/__pycache__/__init__.cpython-313.pyc b/handlers/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f48232ed0ca61aa8c711618f1ee4b541e131c677 GIT binary patch literal 140 zcmey&%ge<81P8o*vq1D?5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa~ihenx(7s(xZY zesM{Dj(%}+MrulFPO5%JVqQv4YEiL%e0*kJW=VX!UP0w84x8M3kgQ!1D^M@U>|zk( NBQql-V-Yiu1po@YANK$N literal 0 HcmV?d00001 diff --git a/handlers/__pycache__/admin.cpython-313.pyc b/handlers/__pycache__/admin.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..06643927f968f1748bc38904266a8320bd8fa393 GIT binary patch literal 5565 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 literal 0 HcmV?d00001 diff --git a/handlers/__pycache__/group.cpython-313.pyc b/handlers/__pycache__/group.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ec6d57034293bdabbd05a68d959fc4ddcb4ff680 GIT binary patch literal 27905 zcmd6P32+#kqR(aWutXeu65OJ4QSd)oZXt; z?|hxAf6<+R;P{XZLA4jV6yQC_A&LQ6KKIC01_;X9S zrv$0mpLeLn?>WSyB%hVk`g{BZhicXu{Dt)UDPF4c7adyRFUC=#-Yxn|S>1Yn*&(ZT zWv$s?&Pp2&RrWou4QJ|n$Wp`kE7tK!iFRJ5uBoptb*BdP^=nY-?634!wHh>R+L`)= zC-72#v=PwG(yC?JboU)d;nkpYjQ;9FE{s!6I##JkZL5|&dlPVfTDv!OK4j{9GWAP? z`T`o1I{WLi7PYf~#UY#8a(zb2t$%b|4O$!8ncBj*HDs#iPxn1-(B6-BR^QXcjCMBv(d}%~ z@M~waosNulwmeKb{>?h$vn2!HR^Zd4r@HIUzcmA&LI<~H;M?|4__piddn5zj_J_jP zse^Av2EIogX8d&LLznh`w6pr2x-;78JYn1-aVQRG}%J!|Df+J zJsKDq7(Nz|k~O4Uj78iP91TdL$wHqcB%oNnD*sdgUJ9RAfLFs~3h+&>F4S>8VgFgviet?yPMtM)8{@nWqb&wUrQ`In<3e}mpZepLX9otJMn8SL zRD@fD)DdsIvE}5*sezWj>5<^*$WTl0=*fX&p`n46lb8rY15&Vs4RQ17GjaRz5$RN5 z^e_#vRE3*s2+JyDV_eLYJ2f;hG_xV%s+FB}vpfEJbgt_k_Izv4clL!FI^_Dz*IQ-# zR$17(i0WKiWJnJW9*f(Lh9qfVn6REXntX87vEyI^w*&pW%pFV)oA&s_hJHgI)yPVI z-f!q<1JlnROb(x4@S8UAO^}KEAoFG?4X2B17yRZ;kb(7aAGB(B_9tJN-*T@z{Ng=& z17r`}8yb(SEpSGYIbq{W7P|Acvv{T~;#?6ctkk*s@$rraKEL;l-8r>o zV$0+uHPWbs!P?JqO6e#1c;eeqDi(%M!psfiQhlK;4enH@dO$U?9L7ObY=6;ih z+f|NQmVR?za>}b6!KvTk7ieYqO@Pb_9cnlDEwpg<;WgUvJh)SD%8IEed(fd_>~m>Q z>RiJeBvQ>%YO(bjnt)ZqF)a3Terw0e92Y1=uXOwBxB2a;<)EdH<*oKozb>nX7C}{= z{UT}`{VwIH4u}edj=X!em7})BaHO@RI}#ubOa9iWHQD%il&4S6wRcp%H&X6r4`d5(bA9eEz;^_t-tsoYWZvuEugq9DMSS0 zLU2SHWfVzj#x-dTRS>|iPR50!1A{|xBSZnI6{S9_w3aTKm8Rn2Cwlr0f1>lzp16t0 z1#u%}hPXxLOi~-wHL<#J6Ll*Xq%{GOr6gta3UtRzdJHwU;s}0<=eYNXf_VzBY

2 z#8Vq}){gCr3APvZoZItHqBmjWOzs8FU@{~`&Yd^5>yE=c@z_{T%$5_jm5z1Yapljn z%dV=iM>Qh&o801YQ%tl?StqQq+`{S4hI4CU<+ZatbG~rhTDf+eT)G~_&6>B!8LhSj zBbQf_aH#Z;=+s?@dusQKyI(#!Q-A5{*#}@+R8A3m|AT~Vj6SjPd|RqJpSDDXJ!g!EV9#^O;$z6bYNmIVkvqEtmA>{-04oB zte$y%mY;0}iO-$u{Dsh*PG1+E;5k>_M?p8n_PmJ7zfrpf z<}FZMeZfLo*Bvm=977xXFyZ@+pD^shEGB9?ET9EJm&%(!<;{%BTZT=3v7h%_6^iV) z9q7B4c%jy^qenV4-tW+;EK$RD3%G*NsE*e$+}dyUyJ!K|asC|e2?68qA7~s7oM^(* zL~Z@?aQkz$@c{KU`}2s-`aR3k&i5B+wNa~ZnOa2|wa{0!2WEfqGIx}udI7wp?Z$id zxl9|0VQart8-oDG4E-; z@C&&16ZsA4lPCbwGJ@vgWF7(?9vMz!U{VJv#Jvg~)>i&frga9jf_2ECwTP}M+k{Q_ zG|hb`BDShOr?<_vMa23Ald;&$7@D+&TDF8BV~~#c272`xhV3=^C*eN_d|iG$L;{Qq ztx0<&jc?*%&&%IqkNkb5=qh_^+!Fqxtk!xpd_n#${@$SXqO|%F zew>qECu}&shOzo{`L)n%XyTpAFTZnn`j7a(Vqo{ND(sE>*?VZbC`6F6E55I1+C9Zz?5;elaf)KHAh8k^uk&4e6R(sEiOpR?`@uz3a z?4m713>bG#c+s0{-td>$FrW9bSAc==%0EiY9{I;!dOhLu%^_mD@4S5OWB=QvCx9W& zH?eh+xO(-Q)W;CyVlK1E*VKo&Jk6esEbMW9dx%z#_nonqy&*OgURNeg_=_sKOJ8B= z@=*&--u~UZfsI7fz0JVm21mA)1 zHO3{}oTu!{>Wiyqo|%9oIQ7TdgaWKh^2bAcD7Bn)X%Ms zu6!iC@{!2O9bwCk-$948|5M&d(iR;F4tJ6)<#=K5xxEVpqt#5}TkZm9g81eV9pu>WohbNZfkqdjnIlZ#jd&m8WvE9GPD;YQ65glQ%WL&tDTXd!L zVyRrd{<<-eyMfamc8^+1oI86xPtM;Gc5Fc{2$I5t<3(X%JLf4) zz&7M?kL~&B{e1?!kRXY1msj#fo_}UDhL}cLB%Z0mxYorg=lAVoFqh zlF=>E!Hp?XQ@^pFM@_ZOVZm=m*RuNsjg$AAI>504P;tOEbtJHU;h?GwqQ;UQ_>G$k z*}d>`C%6{KUVecjvOc1(S;=qGT8F&08FHGK(*OG{+O@Q@q`uw+Onx)v=r?KH5DcJ2 z=VUcN*OPc)c|cDNJm)vjeM}~?>;qY5_5n*$ih-Vge@toC0JGLlJi|6|{YGfyH*vU_R&LoZ;W9Tk z3-nNk9n!$*p);xlj7c)mleokPT_;G%aOj9VN ziIzrB6>{nfqKQIBGPL{bjYt9{q3+l1UJ@)7wcm5UIMcd5UU%I{H!d<&- z{MpGZU#p!t_ElflUOO8I+Z*OqMC>gwNAA?#iM=yLzi?E?3X8`dy(8w#)Lq;h7As%0 zEI7H`>fgFKF$dk9a!fd;pZJAXao1Bm)Bj6P?Sy5VPjwUYF~8tz#+jb4+GZbr#TD_? ze_-Wut70JbzHokBEZ_Tq$(Wl54Y{y1k<0pTqyF2hw)a_P;_~a>e~iZ{EmY#67S`g* zM?q2p&sTP@vB`+k>N%^~w9Uw1|oRnhc7t$)dN-{VJtxGCpC9n zNqP&_0F#AMHm%ZMQ?!=$=isogRV3_Jg+E1DQl5uN&*-fDnisP0_aGU6IpfhtZel8g zbQ10Q4ARp$F+qS!_@C_<`@Yrr40ywtjyxGNd`Pq}3cYVLUj$O@LCpYXut>Q(u8rD|Jrm zqaCnA?XMHQDHnd{D8FG|b~A%}gUV1%edN~5N~s0tUGDwn_M7@hH^@p_ua|p1#`J1g zZpMO*;P@$yt2oZ#@UR7(w?DKSc;ERtsLv%$nSc^UIvQoy|xX`fXMOh39ZRZu$(l^`%`@mq`w!YX^yrDAIr;2*7ZfYE~keWdT(WURLD* z=vB^q;;T_dW!O;}ad^SqJ%v%vs<%C>=6WKYwy3jhZ0Ec7+#~}%uxK<8KZG8UV3`^G zoFszv7d|^3fSve`yKwq57p>za?G~~5c0tXh!>TY~%pnpCt&3$gE+>EL>4~Q&PesKt zSuE4;an;POn=1>$5MHxhUcX;wG{<9Z z(Ts5>?@}3hrdr;?^H+uQ>t#>F>}Rj7yI%a=?Q&b!&syc&o_iSMOzhlc) z7*1oXpyi-#^fsmE^K?}xp+{e2Mww~&VcSU~5>7XL!{_+>^0}l(KAW0Tq`DQRQJ{il2c4>9{rf+^Ck7z!jJ~6=xG+(NxFm=UA44!`QCe39e?j7gtO1 zq<-+`gG3@&>Lb%;R!TXXm+ud_T#|EW`~tk*04^scxg0qz$Pkp3()w%#Y*V?M9bC?V z-)uyf!`7L><%B-6EM=t!;Bw%ceMHH#lGbZ~j-J$^s&+Mu%bCE{=5hQDjz7bZ%Q&2S z|A#Ap^MN>AihC)XfSjY!S;{_388HzFvx$=+I&o=igK-Q}+sFW#>lkcb~QjgBC2M3>8$6fSd~>SnPQdee-(Fz&+m3Cp$dl1?Lj+$`mLDutV6bm3+P-{Tc-Zf*cPU&Hs*2=lezANBlOd3;Zu@Rr9+a2Zc| zCCj}mM^%*6QO@L~xTpmnWhhKZ4fOYzBdX;Nkdi=d!CtbHuE#XVDEtOSZdDvmrIHF% zpO}q_!rktzN*x6$L@!7R%u0Tcv*pC-kI67t)D6faQ)ZSY&`_I_YDlV68qw2bFiED6 zs;{e9h)YSo->OGM^~YrSJ}?`=w?SYp;y8+9AC46`au~t2?U#t)GFtj!HK6+{7N3Lx z&K5j5^6X(mgCa)c5xSjdZ|Ed4%_o(CW~m-f&D=3I`7fbXyh${b3@YCv`$`&tP1z$9 zUro9^uh3g(+&DNq%0h7i68R<+!9FJ$$U;&*Xe^_^M0}+$QuYF6=O{al3}(M$gQJHj zR!<^lk57;$=o*RiN^l*?@j5@f3uk=nm3Lxb_?-e$b7KOdcP&IkyL?_ZLRb3x!dkBrKG~1namjETGsrz6zxvn05%@ z0(>bfb8vR|)T)V9lTA^fNEV9j*z%&bQbff>ZFON=UBbYdI{CY~HIdx9goU%Oz+Kt* zd!||@S|YZxs8ALb$`&@D+Y1hgigBTp1$c6b7X%e5|0qZ==6omL)xqB|@?FjFCzKLg z$#<<0ZdA1ZzUkt-+Ju`qMFiLLU2BD#4L-p0ZoX^1FrQ21^NoDhMq%D(CU^~Dn{Q+J zT9&V8`9{j6PBfSyG!tQB#-Tp|_y6% z-qBh}1*I;n&sKf|R(cfacA!*$ML_W$U8_>*2SvH$kr@JD~wVrz{gWd)6ehaE0)E>+M?{bo-aI8}aBxQ{sXtlj7nns8t2CuS9VE)j zQlAdXOlePgx>bsGYFd!ie#RBXd^i1*vm1*#>p!rX5@s@R$JW#-7jt-(iu29ppeu@shl)CX`HfGC?l#HxBW?AC^XO`Nh6Y? zE>6=WqL|nXzzDIO_V}otC`GqCO*g`qNYV$3y~OqrhLH&=^d=yqP*UU+x8?#RNr>$= znMIYq!z>Mc0~w>tHm;;5Cgw!N62w15#qzLN4soP}$gRgbw&%BaXGNM3(6K#RF(g~# z9r$+;qaU?bgzXg(dsWolKu$vAoIhf3Mev4DWy;aKitU$j#!ci@3{7^-O|Gogl&kuCC_F| zEFk#`VexNX@0-|;1PsW6fSaRJig|XU7H#}#3w^$5;1@YC#w8m!Oa9I zDs%wTM?sn;=WBOX@_)nkGzx!P1?Yx}r;rSD3&HC<#-||!dzO{zm zSuEUYEd%_PgWp*yyyYw)xQ5?ZF1%H1CU_Oy^;UD`&I04lQ76azbEk>mJOj#qp3f6p zAP`(iHGf`CIg1_7V16{Q73K#(VSZpXm|NLNV=Q_E35(yv7C(>eYD~<4cag-5H1A?O zN7*@ZSW{=Eze0r%X(|^0ps5TYA|;tdgtCuVXw&)pO8zhSa^Z>@{jWpSjQ-P-Vg0`e zx}o$RjO~G4hbguj!bqIX1Iq)kQ<9!c2*RFZgu}wYBvbA~1Vwhju8kxUPHD>-gcWTj zcdme8samrhQwHny3wjzQ%#NvExU>7fGZIa**B* zOQ~2EznMk7C_27gZ!OFB7Vk#Kz^dnT>$ei&%xkTsVyv9n| z3bLvi{UnC`lhU6fXDa$2hgTc=G=9pnU})!{b|Xp>22hHxWxjueN`}Xo`STyW6fa9w zP!zl&zt-%PXV@KzW%NfWcjEUbVnF_Z$|S>+iht1OPE#l_L&31f=9${eMzdO)ri^JH zjC(#sKL;pd{F2z8bdj=lWN~ZHb4LeGj}DFu$Bl5|A?c(HmHP1 z5<8Cb1A5y_Y)7#n^TYt{T#&wvYX3q+Zwd=UoKt6#CvS^xbykOpBBIK;4VT6_;xpH$jnibu7UMc(NGgv`b{J$FmWqb0t#OMEHwX$eK_kiwsD8y6A% zYz4p`0QzNkm*#QLzeqbHUH!L81CPT?;Psa-oz#6oP}0BJc0-+Vd9?#A#Da88k%v zIJ$Iv&_MqG-Ddfa-o%y_#1aINHVM5(n1o&1y-(4r%Dxf+D0z#cz zXa#UN%PU##WqA$D>sVfI>nS(hT2tOrWCU03$uYm>G!dL<0*1GWOt|`1xe@SL8;M#S z%$}dYY>3b@X5&pVo77%h5C@gKqE<5XCD}=`D7j^C2^s!Z&=k{{fi6peUXo$xUD5|b z8U{zI&l*IAU&X(7(502>`SDUZ-?B`{n8tLB{e}m|MwIGhVfr<{0zq|j#D_CjFg-9hrl&B`609*g&aPDyHpFQuBMWrzl`eAcqqCFqraJ? z7s2R^)S8$ol0{#Gq@kv%8*JVmR{U6hk4u{{p@aVufNBCl_}{K*d2_hDIa=NtE^m$G zwM89mV?FQM-H4AS+9Fr(jkxwj#eK544_ip?<`pP5(%FHz-f;bfNZ!V%V;xJkPw-R>C^9k&Z=HR79cHz5GiMzI61gPKXsH;IUVIi)36g+?@K407Ig52IMKyC*FxxL#AxxJd;2EJPqZZxh1d=n(c zCfuy(qzm)g_-=p8`;gx9!;qI|RidmW43AKD5}D7HX5JunbswUWA~1~JFd9Ts zp6Ypti`wLtzCpeHzuwpe(Z$W_hTc@!#D60U8(c0j4igfC= z(|r~~1tz}|{%qJEBA7j!{@6yjr^De2x!0iFj=PyW=~2%Y{l7d>MJoaZ{yphhzN{4y@DhgZha-?&1a#5D56 z-r~!6c!|ssis|$#1ksz-d|TsyGkjal5o{14!xI}`=`~zGOE38Z0F~Z2bCvj%46?vn zSq1f2{!_gZy>!|$v4wKrXb1i=(kMD%* z)Kf(8O1`sAxUtGia2t>OteF|QJ2ECgQt?Q6K=EpUF+1IRQEE1>k25XOJfF#+zqGQX zRV+CrAcg1>frsaYsVQMpts{(WS<=iDS@R-km(ZdC<}gsH*^7MwigQx_(~(-g`*`=Hl}e&=4o z?;a)RnG#FnAZM7UDb+A25))LUTwgbs-70L}6K<@o1bh?v@k-&Qy_(?FeD`YMW)s2l zF21{2n9tb^_!jD{5pLOw2!>U!O}JHSCU_-JIqpK<%}4?;(=a5}ZVe}M3KWr#EbkEZa~AVkmO_%x1M9EuH2 z*uT`I#>mb%5NGST_oq0Dm}yqX4nv_wVrV)jBt{A9ex2H)>^~Aoc^|+Mp%bd@{b)qG zCi}nyz`2Y)4Zd&CCv^w%y$L|>!w`V2wEkUsOaBQx_W6xnKK^ySt4jDuBOualt4X^B z4DGh7mb6>I&~Cf1-NsNva0%blAk3GV39jZT$6d%*u)KkC^>ZBfW{aM4EJKhcgD2Cb zJ^>&p8mX)uMakq(+qkREJQ&t=SSSnBQ{3MWPieQ2qPD1eZAm1Mo<)|faypZ79jCBq zUHU05|1-^vl>k(BqwTr%XFYCspVmGtDNRMNI&geTQq7wR~XQ+UZZX_7gMO_Y9yD!-#)S_L3$m|9`|jTEdP zQ*YX?WM!PBH&CB_Jm#IRyn-+0dF8LcQh-x3Bw4;b+DkRTjVn6AQVW z5zm!2e9bl++mG`@m|qmj%+2rIZ`=LNpEQTabpnB%5< z&2c=19yYx`nNzY(lG^JkOl;kZV9gxB(T>B;%<`JzQ;+9lL5WL04CO@(M%I4nYK17# z#_g@L;|6NmZUR+1&-(9Q{yz9ls#u1T$< zRM2>a%g^kcEuOW>D|g637gfeK3Ho9v%P7KJt4wCwn9!(gYPM4V2URv=w*$+{n6Z`x zp92B37L(zl;4VB7Eb=?HkVSsST3FO1goy>8{|XF_oR9*iBwBwGExdS+TGV`ea0nry zL5YZc+^Ln0o;f`bltijd#*}!Wb|Ex6I23HwzWFYZ8WHzs7mo)|H6K;JN-xnOh@0_= z*HIv%Rif;@q3#8a1hBi2v<8LDviZ_O^{pD>AlqHf_S;Avs>r0A0)mK-l687Um(H8pTVP}O3=_K6|zZ4A`tBhI?TRA5_diu7#tlOKA}=@lHHh6 zr+kuI*|9%Gg*NuN+TgQ;2oipYuwAB7e1`tSz;maiSt|J}%Dzq6-&6Ko%E;i)48qcn z2qMLwiRw%yV-kl%T!T$?X(K^i$|{k?#Vw~s==<>7q+jAE36a4s;e{rQJkS4{tNS%q z@hh(O*W8+4aT|Zdt^5`J^!%FJ9OgFv7tW4Lzv3ExXE5`I4>@GekZtbC;xThV5Jkh7 zkSOH&ZPTB`H_P~KGe?+#hWpu7EQnV(*w& zVz=<+gdHapJ0Lbx${SLjIEme^V8==AJfK8*7IyV@6i15MB7A$oKsQw^a_UWMS;2BQ zl|_oyMfmjzLz6PLI8jR*SV7`Q_AtrSt>L292#@Ei!3|p%Ih^PQGz%!Pi*BGvKh0k; zUo^|5>%wjYKW6i6kut{>{OQwMtKAfKD=&d+TavZ$67*`X54+b#cy!f)SB9B{6Ln=H ZD_HJiv6XVM?t05#lc<$ZZ(yi2wILVxQ&Y5%1 zJ@=m9Id@8ip&@vl`?c?dX%(R#nNWS?3R8Lq9pD`2~2Q8AcDnYp;t^uL`uj+ z=CatUBvhhuUg`}c!X(UjxmQa>NF<>Xoy$u3Tf!g)msOCXBx<2!xE<}UEKAH|W`A2X zm^5k2Qh>ShS&@uI4AsOC|UXWJjZ{A?O+>Vl3b!K&}c!<<^sN2AU(BD~JcP(A# zNw4v=b=ZhSe_`e^!B_gTg}g(2{3Hl^Pd1&lGegO|P4d24kz&|apLJZ<9(I5Y4?FqP zFv%8jFu_RMqnXr;&WVAnO@{oa%O^u z#0R8j0GJ~pLNDR>#Xebwog6i)RvUP_%RZ{fT<8XfXOjLFz($JdUJ5;vdXe0a>Ru!U||EI2=X#`50t7QG8}+*`D~YrtOeX1%L0 zUi4<^Bz?0@)4X?qeZ!lzyf5fDJqcFVyxD`=p@GA`#*xdN7xsb2LtO)h*#vsVyWq`% z(|J1S%~{@;-kf)tPJnRHn`epz7%q8palo%moZQ%nRN5vl7IK9T|3h5oy+!&u4|#5{ z>kzt1>YFk&-|L;>B%_6_%ZDBlJ`|5LfA}j{=K)Xa_A5^-+av^wl^!L zkqAWShmVtNnpdDNj1G}apd$d3@riz_$Uv>xyGYvy@Oe74hzRVBJm-5alc^BY} zEX(qDca3DzPM4j_y7}x_mpeG(3>C(lt`X=TV-9hoK z(P`bi-sa8#;avvuc?IEj7mOHeuy4h-)Q^j0aa5n3L^5Zo5yz0ZevVJ@?`3?YO61y1ug$_#0u| z(;?l^x)^@|_jF1(4z>gTbrbH{Eq&drFunsb+R5o|Mty0-8OznQPnIFxJ|73B+&-ZZ ztTs?kR-Po@sI{=theDNkp$dJ4pI}CC{_F=9z|Q;ByT%lD6Xxd)$(Hq zhWW2s9+ZH0m7b)hz`3gViCf}XNi?Z45@j2Wq%_yz7CSk1S^Mow|^S|!9yWDO4ce%y(S8mY1c6 zr2}UF@Db$tGiaiaBKjH}_CjqsroDxDOd;%8NIfGBKzwyDo5|aQd0*#$2dPXUJpc`f zrG&HqS(_04AHvNDO9Hzd+~@*Q!24U&-&5A01d~^){w_L*RX~Od58`V$w6FJ8v?C-}9!4H!? zV8flrTXVSji{;+w&b+Vl6Z94M9RsHvw7k!9fD#rjKb+4|30PD{a_CSx%R2R8vKMp= z;_k;l0uf_;6E)vN`@ThO-=dbA=%HJJh=t!n2wUHW%`^9mD*$sLbgE^dWm-QQUBaz_ z(1Ep?Z6yRlNkoFh6+s`anyE9&S#t@4Ud75MY6-!>4BNOOsH@n$gU^Mh!%KL3AVjbN Smq1B`ks0n-VH&arBK#8yt24U* literal 0 HcmV?d00001 diff --git a/handlers/__pycache__/support.cpython-313.pyc b/handlers/__pycache__/support.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ad77a8056a4962382e4d563f7705f2dcae9f26c9 GIT binary patch literal 2918 zcmbVOU2Gf07M|IiU9W%EaehpaDkSTq6x`&-pf(^v8WGH26D3GqMY4OzT6G2)cfeICQ>se&j~Jm3kdAabga$Fig8~&a z))$7vK|%@F#UW`>rZU&bkZ(|-3fH9}|6qUyxGoO`2SYT(bsu1fG@4)4B&V0q7ae;DInJ_Df(Oo#{s8yZCqD5K{2sI391jKE03b`Q^j4UM<6Mt74 zHk5<5aFGi1IUGr0R?}C z2MGDR3A9x_Ad{OiXn&J2NJpH^7c(qzyv8_(C(*wkaVkkPwv$4T06VGUO+e_Ek_bHI zT@O#|sm|V?S~+pdV>DEco*zaiM_SRSkP3P;ci*E#$D?8x81A>P2E4^zUQrkwk;mA# z+QduXDi~Copq2U)03bI1LDfZ#^HIF-fJ`Z9?_c=pX_wjuwM|PtAd?qiTvrs@! z#?V;U!|kR6>k2LFJaJ{Fvdjr6b zA!-!nPEO3H>4{RI)Ctq9^NcirDS#30t@LY*43>cdzJ~D|_G#@;La$b8f;r!x`?F@9 z-@g-BGF(mQUJ%AVKo^XEJ_hz#2GU`{fM2ficvX-E7QlW-gW=2el6BR7I~EU8mdB2N zn&#&xN+w(xLUzg_?1G?vHWD(8+?2;Dl%==hf9BPUQ7C5fIY%ncY_8}?nK>g(Df_r_ zd6#Zz19z`p7=Wv!r{=S{3DB@%vU1aSqjk(L$RkypX^aiRV>UxAGn%>6gc=WSDm5EQ zv#m65C@r?q0^Oz1VyG;Mjf_!h}tETSa z$%0{0iP`Es+q5~~ovHlcpzMP?GQYCS%AKW5rHkpwG^8ERLK!@#vok#LDC2d<@BPa# zE2$?_{=cS@V(gCxzmI5yX)I;FKxL4ql2A(De4brv{j>{gkVnY;3WO_SjQ@e|-$Jo1 zba)H3ZK3up)Ut&dx6lLIG73cAYC0vm3KEnzTVHQ|7k?<9m8}CkcBJP!{HQC0u)L`@ zxFWo71*2sN-enO9kMR*#t4`Y_kqx5RM1$K<5FjloX#34wRWNu1m@B~>d;@CP2U^(& NQ@Pj#{ticket[0]}\n" + f"👤 Пользователь: {ticket[1]}\n" + f"📝 Сообщение:\n" + f"{ticket[2]}\n" + f"📌 Статус: {ticket[3]}\n" + "━━━━━━━━━━━━━━\n" + ) + + await message.answer(text) + +@router.callback_query( + F.data.startswith("reply_ticket:") +) +async def reply_ticket( + callback: CallbackQuery, + state: FSMContext +): + + ticket_id = int( + callback.data.split(":")[1] + ) + + await state.update_data( + ticket_id=ticket_id + ) + + await state.set_state( + AdminState.waiting_reply + ) + + await callback.message.answer( + "💬 Напишите ответ пользователю:" + ) + + await callback.answer() + +@router.message(AdminState.waiting_reply) +async def send_admin_reply( + message: Message, + state: FSMContext, + bot +): + + data = await state.get_data() + + ticket_id = data.get( + "ticket_id" + ) + + + if not ticket_id: + + await message.answer( + "❌ Тикет не найден." + ) + + await state.clear() + return + + + ticket = await get_ticket( + ticket_id + ) + + + if not ticket: + + await message.answer( + "❌ Обращение не найдено." + ) + + await state.clear() + return + + + user_id = ticket[1] + + + try: + + await bot.send_message( + user_id, + ( + "📩 Ответ поддержки\n\n" + f"{message.text}" + ) + ) + + + await message.answer( + "✅ Ответ отправлен пользователю." + ) + + + except Exception as e: + + await message.answer( + f"❌ Не удалось отправить сообщение:\n{e}" + ) + + + await state.clear() + + +@router.callback_query( + F.data.startswith("close_ticket:") +) +async def close_ticket_handler( + callback: CallbackQuery +): + + ticket_id = int( + callback.data.split(":")[1] + ) + + + await close_ticket( + ticket_id + ) + + + await callback.message.edit_text( + callback.message.html_text + + "\n\n✅ Тикет закрыт" + ) + + + await callback.answer( + "Тикет закрыт" + ) \ No newline at end of file diff --git a/handlers/group.py b/handlers/group.py new file mode 100644 index 0000000..7d677df --- /dev/null +++ b/handlers/group.py @@ -0,0 +1,803 @@ +from datetime import date, timedelta +from aiogram import Router, F +from aiogram.filters import Command +from aiogram.types import Message, CallbackQuery +from aiogram.utils.keyboard import InlineKeyboardBuilder +from aiogram.fsm.context import FSMContext +from states import GroupState +from api import api +from database import save_group, get_group, update_subgroup +import math +from keyboards import main_keyboard +from keyboards import subgroup_keyboard +from keyboards import ( + groups_keyboard, + date_keyboard, + calendar_keyboard, + main_keyboard, + start_keyboard, +) + +router = Router() + +DAYS = ( + "Пн", + "Вт", + "Ср", + "Чт", + "Пт", + "Сб", + "Вс", +) + + +def format_date(day: date) -> str: + return f"{day.strftime('%d.%m')} ({DAYS[day.weekday()]})" + + +def build_keyboard(group_id: int, current_day: date): + + kb = InlineKeyboardBuilder() + + kb.button( + text="⬅️", + callback_data=f"day:{group_id}:{(current_day - timedelta(days=1)).isoformat()}" + ) + + kb.button( + text=format_date(current_day), + callback_data="ignore" + ) + + kb.button( + text="➡️", + callback_data=f"day:{group_id}:{(current_day + timedelta(days=1)).isoformat()}" + ) + + kb.adjust(3) + + return kb.as_markup() + +async def build_groups_keyboard(page: int = 0): + + groups = await api.get_groups() + groups.sort( + key=lambda g: g["groupName"] +) + + PER_PAGE = 30 + + start = page * PER_PAGE + end = start + PER_PAGE + + current = groups[start:end] + + kb = InlineKeyboardBuilder() + + for group in current: + kb.button( + text=group["groupName"], + callback_data=f"select:{group['groupID']}" + ) + + kb.adjust(3) + + pages = math.ceil(len(groups) / PER_PAGE) + + if page > 0: + kb.button( + text="◀️", + callback_data=f"groups_page:{page-1}" + ) + + kb.button( + text=f"{page+1}/{pages}", + callback_data="ignore" + ) + + if page < pages - 1: + kb.button( + text="▶️", + callback_data=f"groups_page:{page+1}" + ) + + kb.adjust(3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3) + + return kb.as_markup() + +def build_schedule( + schedule: dict, + group: dict, + selected_day: date +) -> str: + + lessons = [ + lesson + for lesson in schedule.get("rasp", []) + if lesson.get("дата", "")[:10] == selected_day.isoformat() +] + + lessons.sort( + key=lambda lesson: ( + lesson.get("номерЗанятия", 0), + lesson.get("начало", "") + ) +) + + days = ( + "Понедельник", + "Вторник", + "Среда", + "Четверг", + "Пятница", + "Суббота", + "Воскресенье" + ) + + text = ( + "╔════════════════════╗\n" + " 📅 РАСПИСАНИЕ\n" + "╚════════════════════╝\n\n" + f"🎓 {group['groupName']}\n" + f"📆 {days[selected_day.weekday()]}, " + f"{selected_day.strftime('%d.%m.%Y')}\n\n" + ) + + if not lessons: + return text + "🎉 На этот день занятий нет." + + separator = "━━━━━━━━━━━━━━━━━━━━\n" + + for lesson in lessons: + + text += separator + + number = lesson.get("номерЗанятия", "-") + + text += ( + f"🟧 {number} занятие\n" + f"🕐 {lesson.get('начало', '?')} — " + f"{lesson.get('конец', '?')}\n\n" + ) + + text += ( + f"📚 {lesson.get('дисциплина', '-')}\n" + ) + + teacher = lesson.get("преподаватель") + + if teacher: + text += f"👨‍🏫 {teacher}\n" + + aud = lesson.get("аудитория") + + if aud: + text += f"🏫 Аудитория: {aud}\n" + + subgroup = lesson.get("номерПодгруппы") + + if subgroup: + text += f"👥 Подгруппа: {subgroup}\n" + + text += "\n" + + return text + + +async def send_schedule( + message: Message, + group: dict, + selected_day: date +): + + schedule = await api.get_schedule( + group["groupID"], + selected_day + ) + + text = build_schedule( + schedule, + group, + selected_day + ) + + LIMIT = 4000 + + if len(text) <= LIMIT: + + await message.answer( + text, + reply_markup=build_keyboard( + group["groupID"], + selected_day + ) + ) + + return + + parts = [] + current = "" + + for line in text.split("\n"): + + if len(current) + len(line) + 1 > LIMIT: + parts.append(current) + current = "" + + current += line + "\n" + + if current: + parts.append(current) + + for i, part in enumerate(parts): + + await message.answer( + part, + reply_markup=( + build_keyboard( + group["groupID"], + selected_day + ) + if i == len(parts) - 1 + else None + ) + ) + + +@router.message(Command("find_group")) +@router.message( + F.text.in_( + { + "🔍 Выбрать группу", + "🔍 Сменить группу" + } + ) +) +async def find_group(message: Message): + + await message.answer( + "📚 Выберите свою группу", + reply_markup=await build_groups_keyboard(0) + ) + +@router.message( + GroupState.waiting_group +) +async def search_group( + message: Message, + state: FSMContext +): + + query = message.text.strip() + + groups = await api.search_group_by_name(query) + + if not groups: + + await message.answer( + "❌ Группа не найдена.\nПопробуйте ещё раз." + ) + return + + if len(groups) == 1: + + group = groups[0] + + await message.answer( + "✅ Группа сохранена.", + reply_markup=main_keyboard() + ) + + await state.clear() + + await message.answer( + f"✅ Группа {group['groupName']} сохранена." + ) + + await message.answer( + "📅 Выберите дату:", + reply_markup=date_keyboard() +) + + return + + kb = InlineKeyboardBuilder() + + for group in groups[:10]: + + kb.button( + text=f"{group['groupName']} (курс {group['course']})", + callback_data=f"select:{group['groupID']}" + ) + + kb.adjust(1) + + await message.answer( + "Выберите группу:", + reply_markup=kb.as_markup() + ) +async def search_group(message: Message): + + query = message.text.strip() + + groups = await api.search_group_by_name(query) + + if not groups: + + await message.answer( + "❌ Группа не найдена." + ) + + return + + if len(groups) == 1: + + group = groups[0] + + await save_group( + message.from_user.id, + group["groupID"], + group["groupName"] + ) + + await message.answer( + f"✅ Группа {group['groupName']} сохранена." + ) + + await send_schedule( + message, + group, + date.today() + ) + + return + + kb = InlineKeyboardBuilder() + + for group in groups[:10]: + + kb.button( + text=f"{group['groupName']} (курс {group['course']})", + callback_data=f"select:{group['groupID']}" + ) + + kb.adjust(1) + + await message.answer( + "Найдено несколько групп.\nВыберите нужную:", + reply_markup=kb.as_markup() + ) + + +# ========================== +# ВЫБОР ГРУППЫ +# ========================== + +@router.callback_query( + F.data.startswith("select:") +) +async def select_group( + callback: CallbackQuery, + state: FSMContext +): + + group_id = int( + callback.data.split(":")[1] + ) + + group = await api.get_group_by_id( + group_id + ) + + if group is None: + + await callback.answer( + "Группа не найдена.", + show_alert=True + ) + + return + + await save_group( + callback.from_user.id, + group["groupID"], + group["groupName"] +) + + await state.clear() + + await callback.message.edit_text( + "🎓 Группа сохранена!\n\n" + "Теперь выберите свою подгруппу:", + reply_markup=subgroup_keyboard() +) + + await callback.answer() + +@router.callback_query(F.data.startswith("groups_page:")) +async def groups_page(callback: CallbackQuery): + + page = int(callback.data.split(":")[1]) + + await callback.message.edit_reply_markup( + reply_markup=await build_groups_keyboard(page) + ) + + await callback.answer() + +@router.message( + F.text.regexp(r"^\d{1,2}[./]\d{1,2}([./]\d{2,4})?$") +) +async def schedule_by_date(message: Message): + + saved_group = await get_group( + message.from_user.id + ) + + if not saved_group: + + await message.answer( + "❌ Сначала выберите группу." + ) + return + + text = message.text.strip().replace("/", ".") + + try: + + parts = text.split(".") + + if len(parts) == 2: + + selected_date = date( + year=date.today().year, + month=int(parts[1]), + day=int(parts[0]), + ) + + else: + + year = int(parts[2]) + + if year < 100: + year += 2000 + + selected_date = date( + year=year, + month=int(parts[1]), + day=int(parts[0]), + ) + + except ValueError: + + await message.answer( + "❌ Некорректная дата." + ) + + return + + group = await api.get_group_by_id( + saved_group["group_id"] + ) + + if group is None: + + await message.answer( + "❌ Группа не найдена." + ) + + return + + await send_schedule( + message, + group, + selected_date + ) + + # ========================== +# ПУСТАЯ КНОПКА +# ========================== + +@router.callback_query(F.data == "ignore") +async def ignore_callback(callback: CallbackQuery): + await callback.answer() + + +# ========================== +# ПЕРЕКЛЮЧЕНИЕ ДНЕЙ +# ========================== + +@router.callback_query(F.data.startswith("day:")) +async def change_day(callback: CallbackQuery): + + try: + + _, group_id, day = callback.data.split(":") + + selected_date = date.fromisoformat(day) + + group = await api.get_group_by_id( + int(group_id) + ) + + if group is None: + + await callback.answer( + "Группа не найдена.", + show_alert=True + ) + + return + + schedule = await api.get_schedule( + group["groupID"], + selected_date + ) + + text = build_schedule( + schedule, + group, + selected_date + ) + + if len(text) > 4096: + + await callback.answer( + "Расписание слишком большое. Используйте ввод даты.", + show_alert=True + ) + + return + + await callback.message.edit_text( + text, + reply_markup=build_keyboard( + group["groupID"], + selected_date + ) + ) + + await callback.answer() + + except Exception as e: + + await callback.answer( + str(e), + show_alert=True + ) + +@router.callback_query(F.data == "date:today") +async def date_today(callback: CallbackQuery): + + saved = await get_group(callback.from_user.id) + + if not saved: + await callback.answer("Сначала выберите группу") + return + + group = await api.get_group_by_id(saved["group_id"]) + + schedule = await api.get_schedule( + group["groupID"], + date.today() + ) + + text = build_schedule( + schedule, + group, + date.today() + ) + + await callback.message.edit_text( + text, + reply_markup=build_keyboard( + group["groupID"], + date.today() + ) + ) + +@router.callback_query(F.data == "date:tomorrow") +async def date_tomorrow(callback: CallbackQuery): + + tomorrow = date.today() + timedelta(days=1) + + saved_group = await get_group(callback.from_user.id) + + if not saved_group: + await callback.answer("Сначала выберите группу.", show_alert=True) + return + + group = await api.get_group_by_id(saved_group["group_id"]) + + await callback.message.edit_text( + build_schedule( + await api.get_schedule(group["groupID"], tomorrow), + group, + tomorrow + ), + reply_markup=build_keyboard(group["groupID"], tomorrow) + ) + + await callback.answer() + +@router.callback_query(F.data == "date:input") +async def input_date(callback: CallbackQuery): + + await callback.message.answer( + "Введите дату в формате:\n\n" + "01.09.2026" + ) + + await callback.answer() + + +@router.callback_query(F.data == "date:calendar") +async def open_calendar(callback: CallbackQuery): + + today = date.today() + + await callback.message.edit_reply_markup( + reply_markup=calendar_keyboard( + today.year, + today.month + ) + ) + + await callback.answer() + + +@router.callback_query(F.data.startswith("month:")) +async def change_month(callback: CallbackQuery): + + _, year, month = callback.data.split(":") + + await callback.message.edit_reply_markup( + reply_markup=calendar_keyboard( + int(year), + int(month) + ) + ) + + await callback.answer() + + +@router.callback_query(F.data.startswith("calendar:")) +async def calendar_select(callback: CallbackQuery): + + _, year, month, day = callback.data.split(":") + + selected_day = date( + int(year), + int(month), + int(day) + ) + + saved_group = await get_group( + callback.from_user.id + ) + + if not saved_group: + + await callback.answer( + "Сначала выберите группу.", + show_alert=True + ) + return + + group = await api.get_group_by_id( + saved_group["group_id"] + ) + + schedule = await api.get_schedule( + group["groupID"], + selected_day + ) + + text = build_schedule( + schedule, + group, + selected_day + ) + + await callback.message.edit_text( + text, + reply_markup=build_keyboard( + group["groupID"], + selected_day + ) + ) + + await callback.answer() + + + +@router.callback_query(F.data == "calendar_today") +async def calendar_today(callback: CallbackQuery): + + today = date.today() + + saved_group = await get_group( + callback.from_user.id + ) + + if not saved_group: + return + + group = await api.get_group_by_id( + saved_group["group_id"] + ) + + schedule = await api.get_schedule( + group["groupID"], + today + ) + + text = build_schedule( + schedule, + group, + today + ) + + await callback.message.edit_text( + text, + reply_markup=build_keyboard( + group["groupID"], + today + ) + ) + + await callback.answer() + +@router.message(F.text == "📅 Расписание") +async def schedule_menu(message: Message): + + await message.answer( + "📅 Выберите дату:", + reply_markup=date_keyboard() + ) + +@router.message(F.text == "🔄 Группа") +async def change_group(message: Message): + + await message.answer( + "Введите номер группы:", + reply_markup=start_keyboard() + ) + +@router.message(F.text == "⚙️ Моя группа") +async def my_group(message: Message): + + saved_group = await get_group(message.from_user.id) + + if not saved_group: + await message.answer( + "❌ У вас ещё не выбрана группа.\n\nНажмите «🔍 Выбрать группу»." + ) + return + + await message.answer( + f"🎓 Ваша группа: {saved_group['group_name']}" + ) + +@router.callback_query(F.data.startswith("sub:")) +async def select_subgroup(callback: CallbackQuery): + + subgroup = callback.data.split(":")[1] + + await update_subgroup( + callback.from_user.id, + subgroup + ) + + await callback.answer( + "Подгруппа сохранена!" + ) + + await callback.message.edit_text( + f"✅ Подгруппа {subgroup} сохранена!" +) + + await callback.message.answer( + "Выберите действие:", + reply_markup=main_keyboard() +) \ No newline at end of file diff --git a/handlers/start.py b/handlers/start.py new file mode 100644 index 0000000..284969b --- /dev/null +++ b/handlers/start.py @@ -0,0 +1,77 @@ +from aiogram import Router, F +from aiogram.filters import CommandStart, Command +from aiogram.types import Message +from database import get_group +from keyboards import main_keyboard, start_keyboard +from database import ( + get_group, + update_user +) +from database import save_phone + +router = Router() + + +@router.message(CommandStart()) +async def start(message: Message): + + await update_user( + user_id=message.from_user.id, + username=message.from_user.username, + first_name=message.from_user.first_name, + last_name=message.from_user.last_name +) + + group = await get_group(message.from_user.id) + + if group: + + text = ( + f"👋 Добро пожаловать!\n\n" + f"Ваша группа:\n" + f"{group['group_name']}\n\n" + "Выберите действие ниже." + ) + + await message.answer( + text, + reply_markup=main_keyboard() + ) + + else: + + text = ( + "👋 Добро пожаловать!\n\n" + "Похоже, вы еще не выбрали группу.\n\n" + "Нажмите 🔍 Выбрать группу " + ) + + await message.answer( + text, + reply_markup=start_keyboard() + ) + + +@router.message(Command("help")) +async def help_command(message: Message): + + await message.answer( + "📖 Справка\n\n" + "🔍 Выберите группу.\n" + "📅 Получайте расписание на любой день.\n" + "💾 После выбора группа сохраняется автоматически.\n" + ) + +@router.message(F.contact) +async def get_phone( + message: Message +): + + await save_phone( + message.from_user.id, + message.contact.phone_number + ) + + await message.answer( + "✅ Телефон сохранён." + ) \ No newline at end of file diff --git a/handlers/support.py b/handlers/support.py new file mode 100644 index 0000000..f3f58fa --- /dev/null +++ b/handlers/support.py @@ -0,0 +1,82 @@ +from aiogram import Router, F +from aiogram.types import Message +from aiogram.fsm.context import FSMContext +from states import SupportState +from database import create_ticket +from config import ADMIN_IDS +from aiogram import Bot +from keyboards import ticket_keyboard + + +router = Router() + + +# пользователь нажал кнопку поддержки +@router.message(F.text == "✉️ Поддержка") +async def support_start( + message: Message, + state: FSMContext +): + + await state.set_state( + SupportState.waiting_message + ) + + await message.answer( + "✉️ Опишите проблему.\n\n" + "Напишите сообщение, и оно будет отправлено разработчику." + ) + + +# пользователь написал проблему --> соснул хуйца, я не буду фиксить +@router.message( + SupportState.waiting_message +) +@router.message( + SupportState.waiting_message +) +async def send_ticket( + message: Message, + state: FSMContext, + bot: Bot +): + + ticket_id = await create_ticket( + message.from_user.id, + message.text + ) + + + # уведомляем админов + + for admin_id in ADMIN_IDS: + + try: + + await bot.send_message( + admin_id, + ( + "📩 Новое обращение!\n\n" + f"🆔 Тикет: #{ticket_id}\n" + f"👤 Пользователь: " + f"{message.from_user.id}\n\n" + f"📝 Сообщение:\n" + f"{message.text}" + ), + reply_markup=ticket_keyboard(ticket_id) +) + + except Exception as e: + + print( + f"Ошибка отправки админу {admin_id}: {e}" + ) + + + await state.clear() + + + await message.answer( + f"✅ Обращение #{ticket_id} отправлено.\n\n" + "Спасибо за обратную связь!" + ) \ No newline at end of file diff --git a/keyboards.py b/keyboards.py new file mode 100644 index 0000000..45f8b80 --- /dev/null +++ b/keyboards.py @@ -0,0 +1,229 @@ +from aiogram.types import ( + ReplyKeyboardMarkup, + KeyboardButton, + InlineKeyboardMarkup, + InlineKeyboardButton, +) +from aiogram.types import KeyboardButton, ReplyKeyboardMarkup +import calendar +from datetime import date +from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from aiogram.utils.keyboard import InlineKeyboardBuilder + + +def main_keyboard(): + + return ReplyKeyboardMarkup( + keyboard=[ + [ + KeyboardButton(text="📅 Расписание"), + KeyboardButton(text="🔍 Сменить группу") + ], + [ + KeyboardButton(text="✉️ Поддержка") + ] + ], + resize_keyboard=True + ) +def start_keyboard(): + + return ReplyKeyboardMarkup( + keyboard=[ + [ + KeyboardButton(text="🔍 Выбрать группу") + ] + ], + resize_keyboard=True + ) + +def date_keyboard(): + + kb = InlineKeyboardBuilder() + + kb.button( + text="📅 Сегодня", + callback_data="date:today" + ) + + kb.button( + text="🗓 Календарь", + callback_data="date:calendar" + ) + + kb.adjust(2) + + return kb.as_markup() + + +def groups_keyboard(groups): + + keyboard = [] + + for group in groups: + + keyboard.append( + [ + InlineKeyboardButton( + text=f"{group['groupName']} (курс {group['course']})", + callback_data=f"select_group:{group['groupID']}" + ) + ] + ) + + return InlineKeyboardMarkup( + inline_keyboard=keyboard + ) + +def calendar_keyboard(year: int, month: int): + + kb = [] + + months = [ + "", + "Январь", + "Февраль", + "Март", + "Апрель", + "Май", + "Июнь", + "Июль", + "Август", + "Сентябрь", + "Октябрь", + "Ноябрь", + "Декабрь", + ] + + kb.append([ + InlineKeyboardButton( + text=f"{months[month]} {year}", + callback_data="ignore" + ) + ]) + + kb.append([ + InlineKeyboardButton(text="Пн", callback_data="ignore"), + InlineKeyboardButton(text="Вт", callback_data="ignore"), + InlineKeyboardButton(text="Ср", callback_data="ignore"), + InlineKeyboardButton(text="Чт", callback_data="ignore"), + InlineKeyboardButton(text="Пт", callback_data="ignore"), + InlineKeyboardButton(text="Сб", callback_data="ignore"), + InlineKeyboardButton(text="Вс", callback_data="ignore"), + ]) + + cal = calendar.monthcalendar(year, month) + + for week in cal: + + row = [] + + for day in week: + + if day == 0: + row.append( + InlineKeyboardButton( + text=" ", + callback_data="ignore" + ) + ) + else: + row.append( + InlineKeyboardButton( + text=str(day), + callback_data=f"calendar:{year}:{month}:{day}" + ) + ) + + kb.append(row) + + prev_month = month - 1 + prev_year = year + + if prev_month == 0: + prev_month = 12 + prev_year -= 1 + + next_month = month + 1 + next_year = year + + if next_month == 13: + next_month = 1 + next_year += 1 + + kb.append([ + InlineKeyboardButton( + text="⬅️", + callback_data=f"month:{prev_year}:{prev_month}" + ), + InlineKeyboardButton( + text="🏠 Сегодня", + callback_data="calendar_today" + ), + InlineKeyboardButton( + text="➡️", + callback_data=f"month:{next_year}:{next_month}" + ), + ]) + + return InlineKeyboardMarkup( + inline_keyboard=kb + ) + +def subgroup_keyboard(): + + kb = InlineKeyboardBuilder() + + kb.button(text="Общая", callback_data="sub:0") + + for i in range(1, 5): + kb.button(text=f"{i}А", callback_data=f"sub:{i}А") + kb.button(text=f"{i}Б", callback_data=f"sub:{i}Б") + + kb.adjust(1, 2, 2, 2, 2) + + return kb.as_markup() + +def support_keyboard(): + + return ReplyKeyboardMarkup( + keyboard=[ + [ + KeyboardButton( + text="✉️ Поддержка" + ) + ] + ], + resize_keyboard=True + ) + +def ticket_keyboard(ticket_id: int): + + return InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text="💬 Ответить", + callback_data=f"reply_ticket:{ticket_id}" + ), + InlineKeyboardButton( + text="✅ Закрыть", + callback_data=f"close_ticket:{ticket_id}" + ) + ] + ] + ) + +def phone_keyboard(): + + return ReplyKeyboardMarkup( + keyboard=[ + [ + KeyboardButton( + text="📱 Отправить телефон", + request_contact=True + ) + ] + ], + resize_keyboard=True, + one_time_keyboard=True + ) \ No newline at end of file diff --git a/logger.py b/logger.py new file mode 100644 index 0000000..6451cff --- /dev/null +++ b/logger.py @@ -0,0 +1,12 @@ +import logging + + +logging.basicConfig( + filename="logs/bot.log", + level=logging.INFO, + format= + "%(asctime)s | %(message)s" +) + + +logger = logging.getLogger() \ No newline at end of file diff --git a/logs/bot.log b/logs/bot.log new file mode 100644 index 0000000..9241644 --- /dev/null +++ b/logs/bot.log @@ -0,0 +1,128 @@ +2026-07-09 22:53:52,570 | База данных инициализирована +2026-07-09 22:53:52,570 | Бот запущен +2026-07-09 22:53:52,571 | Start polling +2026-07-09 22:53:52,854 | Run polling for bot @ural_colleges_bot id=8809518082 - 'УЭК' +2026-07-09 22:53:55,054 | Update id=210321910 is handled. Duration 712 ms by bot id=8809518082 +2026-07-09 22:54:00,942 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Groups "HTTP/1.1 200 OK" +2026-07-09 22:54:01,106 | Update id=210321911 is handled. Duration 1852 ms by bot id=8809518082 +2026-07-09 22:54:03,001 | Update id=210321912 is handled. Duration 439 ms by bot id=8809518082 +2026-07-09 22:54:04,262 | Update id=210321913 is handled. Duration 356 ms by bot id=8809518082 +2026-07-09 22:54:06,227 | Update id=210321914 is handled. Duration 107 ms by bot id=8809518082 +2026-07-09 22:54:07,597 | Update id=210321915 is handled. Duration 215 ms by bot id=8809518082 +2026-07-09 22:54:09,584 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12679&sdate=2026-07-02 "HTTP/1.1 200 OK" +2026-07-09 22:54:09,812 | Update id=210321916 is handled. Duration 891 ms by bot id=8809518082 +2026-07-09 22:54:11,908 | Update id=210321917 is handled. Duration 110 ms by bot id=8809518082 +2026-07-09 22:54:13,406 | Update id=210321918 is handled. Duration 117 ms by bot id=8809518082 +2026-07-09 22:54:15,273 | Update id=210321919 is handled. Duration 228 ms by bot id=8809518082 +2026-07-09 22:54:16,785 | Update id=210321920 is handled. Duration 212 ms by bot id=8809518082 +2026-07-09 22:54:17,569 | Update id=210321921 is handled. Duration 210 ms by bot id=8809518082 +2026-07-09 22:54:18,463 | Update id=210321922 is handled. Duration 210 ms by bot id=8809518082 +2026-07-09 22:54:19,173 | Update id=210321923 is handled. Duration 216 ms by bot id=8809518082 +2026-07-09 22:54:23,103 | Update id=210321924 is handled. Duration 201 ms by bot id=8809518082 +2026-07-09 22:54:24,779 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12679&sdate=2026-02-02 "HTTP/1.1 200 OK" +2026-07-09 22:54:24,983 | Update id=210321925 is handled. Duration 998 ms by bot id=8809518082 +2026-07-09 22:54:33,062 | Update id=210321926 is handled. Duration 111 ms by bot id=8809518082 +2026-07-09 22:55:26,983 | Received SIGINT signal +2026-07-09 22:55:26,984 | Polling stopped for bot @ural_colleges_bot id=8809518082 - 'УЭК' +2026-07-09 22:55:26,984 | Polling stopped +2026-07-09 22:55:27,235 | Бот остановлен +2026-07-09 23:03:50,570 | База данных инициализирована +2026-07-09 23:03:50,570 | Удалено 0 записей кэша +2026-07-09 23:03:50,570 | Фоновое обновление кэша запущено +2026-07-09 23:03:50,571 | Бот запущен +2026-07-09 23:03:50,571 | Start polling +2026-07-09 23:03:50,925 | Run polling for bot @ural_colleges_bot id=8809518082 - 'УЭК' +2026-07-09 23:03:56,560 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12679&sdate=2026-07-09 "HTTP/1.1 200 OK" +2026-07-09 23:03:56,785 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12679&sdate=2026-07-10 "HTTP/1.1 200 OK" +2026-07-09 23:03:59,421 | Update id=210321927 is not handled. Duration 2 ms by bot id=8809518082 +2026-07-09 23:04:01,960 | Update id=210321928 is handled. Duration 578 ms by bot id=8809518082 +2026-07-09 23:04:12,965 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Groups "HTTP/1.1 200 OK" +2026-07-09 23:04:13,099 | Update id=210321929 is handled. Duration 1131 ms by bot id=8809518082 +2026-07-09 23:04:14,717 | Update id=210321930 is handled. Duration 244 ms by bot id=8809518082 +2026-07-09 23:04:18,738 | Update id=210321931 is handled. Duration 342 ms by bot id=8809518082 +2026-07-09 23:04:21,391 | Update id=210321932 is handled. Duration 125 ms by bot id=8809518082 +2026-07-09 23:04:23,676 | Update id=210321933 is handled. Duration 211 ms by bot id=8809518082 +2026-07-09 23:04:25,564 | Update id=210321934 is handled. Duration 251 ms by bot id=8809518082 +2026-07-09 23:04:28,888 | Update id=210321935 is handled. Duration 218 ms by bot id=8809518082 +2026-07-09 23:04:29,991 | Update id=210321936 is handled. Duration 309 ms by bot id=8809518082 +2026-07-09 23:04:30,928 | Update id=210321937 is handled. Duration 262 ms by bot id=8809518082 +2026-07-09 23:04:31,692 | Update id=210321938 is handled. Duration 269 ms by bot id=8809518082 +2026-07-09 23:04:32,744 | Update id=210321939 is handled. Duration 304 ms by bot id=8809518082 +2026-07-09 23:04:37,039 | Update id=210321940 is handled. Duration 291 ms by bot id=8809518082 +2026-07-09 23:04:40,175 | Update id=210321941 is handled. Duration 400 ms by bot id=8809518082 +2026-07-09 23:04:42,251 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12638&sdate=2025-11-07 "HTTP/1.1 200 OK" +2026-07-09 23:04:43,480 | Update id=210321942 is handled. Duration 2103 ms by bot id=8809518082 +2026-07-09 23:09:58,630 | Received SIGINT signal +2026-07-09 23:09:58,631 | Polling stopped for bot @ural_colleges_bot id=8809518082 - 'УЭК' +2026-07-09 23:09:58,631 | Polling stopped +2026-07-09 23:09:58,882 | Бот остановлен +2026-07-09 23:11:18,096 | База данных инициализирована +2026-07-09 23:11:18,096 | Удалено 0 записей кэша +2026-07-09 23:11:18,096 | Фоновое обновление кэша запущено +2026-07-09 23:11:18,096 | Бот запущен +2026-07-09 23:11:18,096 | Start polling +2026-07-09 23:11:18,580 | Run polling for bot @ural_colleges_bot id=8809518082 - 'УЭК' +2026-07-09 23:11:19,151 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12638&sdate=2026-07-09 "HTTP/1.1 200 OK" +2026-07-09 23:11:19,341 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12638&sdate=2026-07-10 "HTTP/1.1 200 OK" +2026-07-09 23:11:19,899 | Update id=210321943 is handled. Duration 1197 ms by bot id=8809518082 +2026-07-09 23:11:36,111 | Update id=210321944 is handled. Duration 298 ms by bot id=8809518082 +2026-07-09 23:11:37,267 | Update id=210321945 is handled. Duration 93 ms by bot id=8809518082 +2026-07-09 23:11:44,401 | Update id=210321946 is handled. Duration 144 ms by bot id=8809518082 +2026-07-09 23:12:17,894 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Groups "HTTP/1.1 200 OK" +2026-07-09 23:12:18,487 | Update id=210321947 is handled. Duration 2905 ms by bot id=8809518082 +2026-07-09 23:12:23,004 | Update id=210321948 is handled. Duration 254 ms by bot id=8809518082 +2026-07-09 23:12:24,230 | Update id=210321949 is handled. Duration 240 ms by bot id=8809518082 +2026-07-09 23:12:28,705 | Update id=210321950 is handled. Duration 210 ms by bot id=8809518082 +2026-07-09 23:12:31,863 | Update id=210321951 is handled. Duration 347 ms by bot id=8809518082 +2026-07-09 23:12:35,168 | Update id=210321952 is handled. Duration 116 ms by bot id=8809518082 +2026-07-09 23:12:37,284 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12769&sdate=2026-07-09 "HTTP/1.1 200 OK" +2026-07-09 23:12:37,410 | Update id=210321953 is handled. Duration 892 ms by bot id=8809518082 +2026-07-09 23:12:39,908 | Update id=210321954 is handled. Duration 109 ms by bot id=8809518082 +2026-07-09 23:12:40,110 | Update id=210321955 is handled. Duration 100 ms by bot id=8809518082 +2026-07-09 23:12:41,239 | Update id=210321956 is handled. Duration 104 ms by bot id=8809518082 +2026-07-09 23:12:42,118 | Update id=210321957 is handled. Duration 105 ms by bot id=8809518082 +2026-07-09 23:12:42,834 | Update id=210321958 is handled. Duration 109 ms by bot id=8809518082 +2026-07-09 23:12:43,753 | Update id=210321959 is handled. Duration 101 ms by bot id=8809518082 +2026-07-09 23:12:44,576 | Update id=210321960 is handled. Duration 95 ms by bot id=8809518082 +2026-07-09 23:12:46,112 | Update id=210321961 is handled. Duration 104 ms by bot id=8809518082 +2026-07-09 23:12:50,923 | Update id=210321962 is handled. Duration 102 ms by bot id=8809518082 +2026-07-09 23:12:56,554 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12769&sdate=2026-07-08 "HTTP/1.1 200 OK" +2026-07-09 23:12:56,885 | Update id=210321963 is handled. Duration 1132 ms by bot id=8809518082 +2026-07-09 23:12:58,757 | Update id=210321964 is handled. Duration 233 ms by bot id=8809518082 +2026-07-09 23:12:59,798 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12769&sdate=2026-07-07 "HTTP/1.1 200 OK" +2026-07-09 23:13:00,053 | Update id=210321965 is handled. Duration 426 ms by bot id=8809518082 +2026-07-09 23:13:00,614 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12769&sdate=2026-07-06 "HTTP/1.1 200 OK" +2026-07-09 23:13:00,844 | Update id=210321966 is handled. Duration 398 ms by bot id=8809518082 +2026-07-09 23:13:01,290 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12769&sdate=2026-07-05 "HTTP/1.1 200 OK" +2026-07-09 23:13:01,583 | Update id=210321967 is handled. Duration 464 ms by bot id=8809518082 +2026-07-09 23:13:28,484 | Update id=210321968 is handled. Duration 440 ms by bot id=8809518082 +2026-07-09 23:13:34,189 | Update id=210321969 is handled. Duration 348 ms by bot id=8809518082 +2026-07-09 23:13:41,625 | Update id=210321970 is handled. Duration 215 ms by bot id=8809518082 +2026-07-09 23:13:50,633 | Update id=210321971 is handled. Duration 234 ms by bot id=8809518082 +2026-07-09 23:13:54,291 | Update id=210321972 is handled. Duration 658 ms by bot id=8809518082 +2026-07-09 23:13:56,382 | Update id=210321973 is handled. Duration 224 ms by bot id=8809518082 +2026-07-09 23:13:57,276 | Update id=210321974 is handled. Duration 95 ms by bot id=8809518082 +2026-07-09 23:13:58,843 | Update id=210321975 is handled. Duration 217 ms by bot id=8809518082 +2026-07-09 23:14:00,451 | HTTP Request: GET https://xn--j1ab.xn----7sbndgvfca2ar9a.xn--p1ai/api/Rasp?idGroup=12769&sdate=2026-07-04 "HTTP/1.1 200 OK" +2026-07-09 23:14:00,677 | Update id=210321976 is handled. Duration 1037 ms by bot id=8809518082 +2026-07-09 23:14:02,721 | Update id=210321977 is handled. Duration 111 ms by bot id=8809518082 +2026-07-09 23:14:04,556 | Update id=210321978 is handled. Duration 206 ms by bot id=8809518082 +2026-07-09 23:14:16,271 | Update id=210321979 is handled. Duration 352 ms by bot id=8809518082 +2026-07-09 23:14:20,890 | Update id=210321980 is handled. Duration 209 ms by bot id=8809518082 +2026-07-09 23:14:25,022 | Update id=210321981 is handled. Duration 87 ms by bot id=8809518082 +2026-07-09 23:14:26,404 | Update id=210321982 is handled. Duration 319 ms by bot id=8809518082 +2026-07-09 23:14:32,718 | Update id=210321983 is not handled. Duration 2 ms by bot id=8809518082 +2026-07-09 23:14:41,187 | Received SIGINT signal +2026-07-09 23:14:41,188 | Polling stopped for bot @ural_colleges_bot id=8809518082 - 'УЭК' +2026-07-09 23:14:41,189 | Polling stopped +2026-07-09 23:14:41,441 | Бот остановлен +2026-07-09 23:27:37,784 | База данных инициализирована +2026-07-09 23:27:37,784 | Удалено 0 записей кэша +2026-07-09 23:27:37,784 | Фоновое обновление кэша запущено +2026-07-09 23:27:37,784 | Бот запущен +2026-07-09 23:27:37,784 | Start polling +2026-07-09 23:27:38,246 | Run polling for bot @ural_colleges_bot id=8809518082 - 'УЭК' +2026-07-09 23:27:49,524 | Received SIGINT signal +2026-07-09 23:27:49,525 | Polling stopped for bot @ural_colleges_bot id=8809518082 - 'УЭК' +2026-07-09 23:27:49,525 | Polling stopped +2026-07-09 23:27:49,779 | Бот остановлен diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..44df3d1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,27 @@ +aiofiles==24.1.0 +aiogram==3.13.1 +aiohappyeyeballs==2.7.1 +aiohttp==3.10.11 +aiosignal==1.4.0 +aiosqlite==0.20.0 +annotated-types==0.7.0 +anyio==4.14.1 +attrs==26.1.0 +cachetools==5.5.0 +certifi==2026.6.17 +frozenlist==1.8.0 +h11==0.16.0 +httpcore==1.0.9 +httpx==0.27.2 +idna==3.18 +magic-filter==1.0.12 +multidict==6.7.1 +propcache==0.5.2 +pydantic==2.9.2 +pydantic_core==2.23.4 +python-dateutil==2.9.0.post0 +python-dotenv==1.0.1 +six==1.17.0 +sniffio==1.3.1 +typing_extensions==4.16.0 +yarl==1.24.2 diff --git a/schedule.py b/schedule.py new file mode 100644 index 0000000..382f8e4 --- /dev/null +++ b/schedule.py @@ -0,0 +1,41 @@ +import asyncio + +from datetime import date, timedelta + +from api import api +from database import get_active_groups + + +async def refresh_cache(): + + while True: + + groups = await get_active_groups() + + if groups: + + print(f"[CACHE] Обновляем {len(groups)} групп") + + for group_id in groups: + + try: + + # сегодня + await api.get_schedule( + group_id, + date.today() + ) + + # завтра + await api.get_schedule( + group_id, + date.today() + timedelta(days=1) + ) + + except Exception as e: + + print( + f"[CACHE] Ошибка {group_id}: {e}" + ) + + await asyncio.sleep(1800) \ No newline at end of file diff --git a/states.py b/states.py new file mode 100644 index 0000000..f9b9c56 --- /dev/null +++ b/states.py @@ -0,0 +1,13 @@ +from aiogram.fsm.state import State, StatesGroup +from aiogram.fsm.state import StatesGroup, State + +class SupportState(StatesGroup): + + waiting_message = State() + +class GroupState(StatesGroup): + waiting_group = State() + +class AdminState(StatesGroup): + + waiting_reply = State() \ No newline at end of file