From 5dced765cb1aa0d4e13241cd6825b5cc30f7cce2 Mon Sep 17 00:00:00 2001 From: Zikil Date: Sun, 3 Nov 2024 21:16:44 +0700 Subject: [PATCH] init main --- .DS_Store | Bin 0 -> 6148 bytes .gitignore | 5 + LICENSE | 201 +++++++ dj_bot.conf | 12 + main.py | 83 +++ requirements.txt | 97 ++++ settings.ini | 3 + tgbot/.DS_Store | Bin 0 -> 8196 bytes tgbot/__init__.py | 1 + tgbot/data/__init__.py | 0 tgbot/data/config.py | 42 ++ tgbot/database/__init__.py | 0 tgbot/database/db_helper.py | 124 +++++ tgbot/database/db_settings.py | 37 ++ tgbot/database/db_tenders.py | 137 +++++ tgbot/database/db_users.py | 137 +++++ tgbot/keyboards/__init__.py | 0 tgbot/keyboards/inline_main.py | 26 + tgbot/keyboards/inline_misc.py | 22 + tgbot/keyboards/reply_main.py | 49 ++ tgbot/keyboards/reply_misc.py | 22 + tgbot/middlewares/__init__.py | 13 + tgbot/middlewares/middleware_throttling.py | 63 +++ tgbot/middlewares/middleware_user.py | 48 ++ tgbot/routers/.DS_Store | Bin 0 -> 6148 bytes tgbot/routers/__init__.py | 30 ++ tgbot/routers/admin/__init__.py | 0 tgbot/routers/admin/admin_menu.py | 121 +++++ tgbot/routers/main_errors.py | 29 + tgbot/routers/main_missed.py | 61 +++ tgbot/routers/main_start.py | 235 ++++++++ tgbot/routers/user/__init__.py | 0 tgbot/routers/user/user_menu.py | 59 +++ tgbot/services/__init__.py | 0 tgbot/services/api_session.py | 30 ++ tgbot/services/parser_tendors.py | 242 +++++++++ tgbot/services/tender_plan.py | 588 +++++++++++++++++++++ tgbot/services/test.py | 163 ++++++ tgbot/utils/.DS_Store | Bin 0 -> 6148 bytes tgbot/utils/__init__.py | 0 tgbot/utils/const_functions.py | 387 ++++++++++++++ tgbot/utils/misc/__init__.py | 0 tgbot/utils/misc/bot_commands.py | 49 ++ tgbot/utils/misc/bot_filters.py | 22 + tgbot/utils/misc/bot_logging.py | 32 ++ tgbot/utils/misc/bot_models.py | 7 + tgbot/utils/misc_functions.py | 123 +++++ 47 files changed, 3300 insertions(+) create mode 100644 .DS_Store create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 dj_bot.conf create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 settings.ini create mode 100644 tgbot/.DS_Store create mode 100644 tgbot/__init__.py create mode 100644 tgbot/data/__init__.py create mode 100644 tgbot/data/config.py create mode 100644 tgbot/database/__init__.py create mode 100644 tgbot/database/db_helper.py create mode 100644 tgbot/database/db_settings.py create mode 100644 tgbot/database/db_tenders.py create mode 100644 tgbot/database/db_users.py create mode 100644 tgbot/keyboards/__init__.py create mode 100644 tgbot/keyboards/inline_main.py create mode 100644 tgbot/keyboards/inline_misc.py create mode 100644 tgbot/keyboards/reply_main.py create mode 100644 tgbot/keyboards/reply_misc.py create mode 100644 tgbot/middlewares/__init__.py create mode 100644 tgbot/middlewares/middleware_throttling.py create mode 100644 tgbot/middlewares/middleware_user.py create mode 100644 tgbot/routers/.DS_Store create mode 100644 tgbot/routers/__init__.py create mode 100644 tgbot/routers/admin/__init__.py create mode 100644 tgbot/routers/admin/admin_menu.py create mode 100644 tgbot/routers/main_errors.py create mode 100644 tgbot/routers/main_missed.py create mode 100644 tgbot/routers/main_start.py create mode 100644 tgbot/routers/user/__init__.py create mode 100644 tgbot/routers/user/user_menu.py create mode 100644 tgbot/services/__init__.py create mode 100644 tgbot/services/api_session.py create mode 100644 tgbot/services/parser_tendors.py create mode 100644 tgbot/services/tender_plan.py create mode 100644 tgbot/services/test.py create mode 100644 tgbot/utils/.DS_Store create mode 100644 tgbot/utils/__init__.py create mode 100644 tgbot/utils/const_functions.py create mode 100644 tgbot/utils/misc/__init__.py create mode 100644 tgbot/utils/misc/bot_commands.py create mode 100644 tgbot/utils/misc/bot_filters.py create mode 100644 tgbot/utils/misc/bot_logging.py create mode 100644 tgbot/utils/misc/bot_models.py create mode 100644 tgbot/utils/misc_functions.py diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..e9d7acfd504751e95315e56cf8df9640d488ec2f GIT binary patch literal 6148 zcmeHKu};H447DLhk-BtbLMJ*RhN?dhs<5*32T&*~h^FaKI)@dBKj3>10|TGHr|^7s zO9GMD5CU7ed%m-MzC0;+XCmV9?Q}@gC!!QhkVWYbneH_0n6nr;uCbcWhLek`Ty`w; zJEvstd$gh%6;x4s{|9-}ww6_%7xSvXZ2Itc{Cs!)bn@9_`i;l(s=?6V8^S4}HC@tz z)=l;XFQc2Lt(p6NG1%nZ#+KDb<}Q0P@6p+-_o20$~jWYA9QY!5WVFV1Ailt*GI| zR(!B!-V`q!vt#`b-H9_r@0|f>pv}Op4*PQdpW>J4Eb`kSK5_<}fj`CoC;22Ff A3;+NC literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..37985fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +venv +*pycache* +*.xls* +*.log +*.db diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/dj_bot.conf b/dj_bot.conf new file mode 100644 index 0000000..f9fb6ff --- /dev/null +++ b/dj_bot.conf @@ -0,0 +1,12 @@ +# supervisorctl +[program:dj_bot] +directory=/root/djimbo_template/ +command=python3.9 main.py + +autostart=True +autorestart=True + +stderr_logfile=/root/djimbo_template/tgbot/data/sv_log_err.log +; stderr_logfile_maxbytes=10MB +stdout_logfile=/root/djimbo_template/tgbot/data/sv_log_out.log +; stdout_logfile_maxbytes=10MB \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..114df9d --- /dev/null +++ b/main.py @@ -0,0 +1,83 @@ +# - *- coding: utf- 8 - *- +import asyncio +import os +import sys + +import colorama +from aiogram import Bot, Dispatcher +from aiogram.client.default import DefaultBotProperties + +from tgbot.data.config import BOT_TOKEN, BOT_SCHEDULER, get_admins +from tgbot.database.db_helper import create_dbx +from tgbot.middlewares import register_all_middlwares +from tgbot.routers import register_all_routers +from tgbot.services.api_session import AsyncRequestSession +from tgbot.utils.misc.bot_commands import set_commands +from tgbot.utils.misc.bot_logging import bot_logger +from tgbot.utils.misc_functions import autobackup_admin, startup_notify, tenders_sched, tenders_sched_ap, tenders_sched_ap_in_tenderplan + +colorama.init() + + +# Запуск шедулеров +async def scheduler_start(bot): + # BOT_SCHEDULER.add_job(autobackup_admin, trigger="cron", hour=00, args=(bot,)) # Ежедневный Автобэкап в 00:00 + BOT_SCHEDULER.add_job(tenders_sched_ap_in_tenderplan, 'cron', hour='21', minute='0', args=(bot,)) # + # BOT_SCHEDULER.add_job(tenders_sched, 'cron', hour='18', minute='0', args=(bot,)) # + # BOT_SCHEDULER.add_job(tenders_sched_ap, 'cron', hour='6', minute='0', args=(bot,)) # + +# scheduler.add_job(func, 'cron', day_of_week='mon-fri', hour=5, minute=30, end_date='2021-05-30') + + +# Запуск бота и базовых функций +async def main(): + BOT_SCHEDULER.start() # Запуск Шедулера + dp = Dispatcher() # Образ Диспетчера + arSession = AsyncRequestSession() # Пул асинхронной сессии запросов + bot = Bot( # Образ Бота + token=BOT_TOKEN, + default=DefaultBotProperties( + parse_mode="HTML", + ) + ) + + register_all_middlwares(dp) # Регистрация всех мидлварей + register_all_routers(dp) # Регистрация всех роутеров + + try: + await set_commands(bot) # Установка команд + await startup_notify(bot) # Уведомления админам при запуске бота + await scheduler_start(bot) # Подключение шедулеров + + bot_logger.warning("BOT WAS STARTED") + print(colorama.Fore.LIGHTYELLOW_EX + f"~~~~~ Bot was started - @{(await bot.get_me()).username} ~~~~~") + print(colorama.Fore.LIGHTBLUE_EX + "~~~~~ ~~~~~") + print(colorama.Fore.RESET) + + if len(get_admins()) == 0: print("***** ENTER ADMIN ID IN settings.ini *****") + + await bot.delete_webhook() # Удаление вебхуков, если они имеются + await bot.get_updates(offset=-1) # Сброс пендинг апдейтов + + await dp.start_polling( + bot, + arSession=arSession, + allowed_updates=dp.resolve_used_update_types(), + ) + finally: + await arSession.close() + await bot.session.close() + + +if __name__ == "__main__": + create_dbx() + + try: + asyncio.run(main()) + except (KeyboardInterrupt, SystemExit): + bot_logger.warning("Bot was stopped") + # finally: + # if sys.platform.startswith("win"): + # os.system("cls") + # else: + # os.system("clear") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d81f733 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,97 @@ +aiofiles==23.2.1 +aiogram==3.4.1 +aiohttp==3.9.3 +aiosignal==1.3.1 +annotated-types==0.6.0 +anyio==4.3.0 +appnope==0.1.4 +APScheduler==3.10.4 +asttokens==2.4.1 +async-timeout==4.0.3 +attrs==23.2.0 +beautifulsoup4==4.12.3 +bs4==0.0.2 +cachetools==5.3.3 +certifi==2024.2.2 +charset-normalizer==3.3.2 +click==8.1.7 +colorama==0.4.6 +colorlog==6.8.2 +comm==0.2.2 +contourpy==1.2.1 +cycler==0.12.1 +debugpy==1.8.1 +decorator==5.1.1 +et-xmlfile==1.1.0 +exceptiongroup==1.2.0 +executing==2.0.1 +fastapi==0.110.1 +fastjsonschema==2.19.1 +fonttools==4.51.0 +frozenlist==1.4.1 +h11==0.14.0 +idna==3.7 +ipykernel==6.29.4 +ipython==8.23.0 +jedi==0.19.1 +Jinja2==3.1.3 +jsonschema==4.21.1 +jsonschema-specifications==2023.12.1 +jupyter_client==8.6.1 +jupyter_core==5.7.2 +kiwisolver==1.4.5 +magic-filter==1.0.12 +MarkupSafe==2.1.5 +matplotlib==3.8.4 +matplotlib-inline==0.1.6 +multidict==6.0.5 +nbformat==5.10.4 +nest-asyncio==1.6.0 +numpy==1.26.4 +openpyxl==3.1.2 +packaging==24.0 +pandas==2.2.2 +parso==0.8.4 +pexpect==4.9.0 +pillow==10.3.0 +platformdirs==4.2.0 +plotly==5.21.0 +prompt-toolkit==3.0.43 +psutil==5.9.8 +ptyprocess==0.7.0 +pure-eval==0.2.2 +pybit==5.6.2 +pycryptodome==3.20.0 +pydantic +pydantic_core +Pygments==2.17.2 +pyparsing==3.1.2 +python-dateutil==2.9.0.post0 +python-dotenv==1.0.1 +pytz==2024.1 +pyzmq==25.1.2 +rapidfuzz==3.9.0 +referencing==0.34.0 +requests==2.31.0 +rpds-py==0.18.0 +scipy==1.13.0 +six==1.16.0 +sniffio==1.3.1 +soupsieve==2.5 +stack-data==0.6.3 +starlette==0.37.2 +ta==0.11.0 +tenacity==8.2.3 +thefuzz==0.22.1 +tornado==6.4 +traitlets==5.14.2 +typing==3.7.4.3 +typing_extensions==4.11.0 +tzdata==2024.1 +tzlocal==5.2 +urllib3==2.2.1 +uvicorn==0.20.0 +wcwidth==0.2.13 +websocket-client==1.7.0 +websockets==12.0 +yarl==1.9.4 diff --git a/settings.ini b/settings.ini new file mode 100644 index 0000000..984e050 --- /dev/null +++ b/settings.ini @@ -0,0 +1,3 @@ +[settings] +bot_token=7096948690:AAGe_b8fOKsLSvjK-Yk703JhEN3ySaqucKo +admin_id=340394898 diff --git a/tgbot/.DS_Store b/tgbot/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..e01d0691312a9ae26a4a549bd8d14e2856dc3056 GIT binary patch literal 8196 zcmeHMO-ma=7=9;Oy2VRRlF)+#9(py$p1Mk;xyMrxqRECDOxR#di^Z1x1*Jcr(2J*@ z{0W{)udz_P2>t|rfCr!XSTmE|jp(5Sdc8RQJIJWbcHXM zFyu6~u6m}G?2H5o#1pk?7k!U9)XcDXf*oK7*a3Ec9bgCM!2#Uass#o2zFFmw9bgBZ zqyy^xAkh_u788T|)qz2m0Ki2|>xO640Xjxn3@s)Gbtv{U)q}91!j>4qgkxPQIxw`D z7&PG|OgIUfS=b6i=!^ z`|(6Y-ny5!a;@?PX~_-jh$(YZ+{gtU7h38 zDD|*lU)Ra|*}mTIO7C-c7}E$Rtb-@tOHoa-3|C)$t9x;|uh0L~KCZ4}mZzsAkqqY! zXAMd|!o@b8rW76i$>X@1zI_>|=RdQTt7;a{(i^B+@Bfe9p99$ec3}P;5R0`&Z38-OZJkl9 w?pleygRYw5B?ff}27Mg^bvX{a`@;~=5>yovT1*UL28}-iXd8H72mY%Aw{@;3v;Y7A literal 0 HcmV?d00001 diff --git a/tgbot/__init__.py b/tgbot/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tgbot/__init__.py @@ -0,0 +1 @@ + diff --git a/tgbot/data/__init__.py b/tgbot/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tgbot/data/config.py b/tgbot/data/config.py new file mode 100644 index 0000000..206b3e1 --- /dev/null +++ b/tgbot/data/config.py @@ -0,0 +1,42 @@ +# - *- coding: utf- 8 - *- +import configparser + +from apscheduler.schedulers.asyncio import AsyncIOScheduler + +# Токен бота +BOT_TOKEN = configparser.ConfigParser() +BOT_TOKEN.read("settings.ini") +BOT_TOKEN = BOT_TOKEN['settings']['bot_token'].strip().replace(' ', '') +BOT_TIMEZONE = "Asia/Krasnoyarsk" # Временная зона бота + +# Пути к файлам +PATH_DATABASE = "tgbot/data/database.db" # Путь к БД +PATH_LOGS = "tgbot/data/logs.log" # Путь к Логам +PATH_EXCEL = "tgbot/data/articles_sheet.xlsx" # Путь к таблице с артиклами + +# Образы и конфиги +BOT_SCHEDULER = AsyncIOScheduler(timezone=BOT_TIMEZONE) # Образ шедулера +start_status = True # Оповещение админам о запуске бота (True или False) + + +# Получение администраторов бота +def get_admins() -> list[int]: + read_admins = configparser.ConfigParser() + read_admins.read('settings.ini') + + admins = read_admins['settings']['admin_id'].strip().replace(" ", "") + + if "," in admins: + admins = admins.split(",") + else: + if len(admins) >= 1: + admins = [admins] + else: + admins = [] + + while "" in admins: admins.remove("") + while " " in admins: admins.remove(" ") + while "," in admins: admins.remove(",") + while "\r" in admins: admins.remove("\r") + + return list(map(int, admins)) diff --git a/tgbot/database/__init__.py b/tgbot/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tgbot/database/db_helper.py b/tgbot/database/db_helper.py new file mode 100644 index 0000000..f3df718 --- /dev/null +++ b/tgbot/database/db_helper.py @@ -0,0 +1,124 @@ +# - *- coding: utf- 8 - *- +import sqlite3 + +from tgbot.data.config import PATH_DATABASE +from tgbot.utils.const_functions import ded + + +# Преобразование полученного списка в словарь +def dict_factory(cursor, row) -> dict: + save_dict = {} + + for idx, col in enumerate(cursor.description): + save_dict[col[0]] = row[idx] + + return save_dict + + +# Форматирование запроса без аргументов +def update_format(sql, parameters: dict) -> tuple[str, list]: + values = ", ".join([ + f"{item} = ?" for item in parameters + ]) + sql += f" {values}" + + return sql, list(parameters.values()) + + +# Форматирование запроса с аргументами +def update_format_where(sql, parameters: dict) -> tuple[str, list]: + sql += " WHERE " + + sql += " AND ".join([ + f"{item} = ?" for item in parameters + ]) + + return sql, list(parameters.values()) + + +################################################################################ +# Создание всех таблиц для БД +def create_dbx(): + with sqlite3.connect(PATH_DATABASE) as con: + con.row_factory = dict_factory + + ############################################################ + # Создание таблицы с хранением - пользователей + if len(con.execute("PRAGMA table_info(storage_users)").fetchall()) == 8: + print("DB was found(1/4)") + else: + con.execute( + ded(f""" + CREATE TABLE storage_users( + increment INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + user_login TEXT, + user_name TEXT, + user_surname TEXT, + user_fullname TEXT, + notif TEXT, + user_unix INTEGER + ) + """) + ) + print("DB was not found(1/4) | Creating...") + + # Создание таблицы с хранением - настроек + if len(con.execute("PRAGMA table_info(storage_settings)").fetchall()) == 2: #!!!!! + print("DB was found(2/4)") + else: + con.execute( + ded(f""" + CREATE TABLE storage_settings( + status_work TEXT, + status_sched_tenders TEXT + ) + """) + ) + + con.execute( + ded(f""" + INSERT INTO storage_settings( + status_work, + status_sched_tenders + ) + VALUES (?,?) + """), + [ + 'True', + 'True' + ] + ) + print("DB was not found(2/4) | Creating...") + + # Создание таблицы с хранением - тендеров + if len(con.execute("PRAGMA table_info(storage_tenders)").fetchall()) == 5: + print("DB was found(3/4)") + else: + con.execute( + ded(f""" + CREATE TABLE storage_tenders( + tender_id INTEGER PRIMARY KEY AUTOINCREMENT, + tender_name TEXT, + tender_link TEXT, + date_creat DATE, + date_until DATE + ) + """) + ) + print("DB was not found(3/4) | Creating...") + + # Создание таблицы с хранением - товаров + if len(con.execute("PRAGMA table_info(storage_goods)").fetchall()) == 3: + print("DB was found(4/4)") + else: + con.execute( + ded(f""" + CREATE TABLE storage_goods( + good_id INTEGER PRIMARY KEY AUTOINCREMENT, + good_name TEXT, + tender_id INTEGER REFERENCES storage_tenders(tender_id) ON UPDATE CASCADE + ) + """) + ) + print("DB was not found(4/4) | Creating...") \ No newline at end of file diff --git a/tgbot/database/db_settings.py b/tgbot/database/db_settings.py new file mode 100644 index 0000000..d0fd407 --- /dev/null +++ b/tgbot/database/db_settings.py @@ -0,0 +1,37 @@ +# - *- coding: utf- 8 - *- +import sqlite3 + +from pydantic import BaseModel + +from tgbot.data.config import PATH_DATABASE +from tgbot.database.db_helper import dict_factory, update_format + + +# Модель таблицы +class SettingsModel(BaseModel): + status_work: str + status_sched_tenders: str + + +# Работа с настройками +class Settingsx: + storage_name = "storage_settings" + + # Получение записи + @staticmethod + def get() -> SettingsModel: + with sqlite3.connect(PATH_DATABASE) as con: + con.row_factory = dict_factory + sql = f"SELECT * FROM {Settingsx.storage_name}" + + return SettingsModel(**con.execute(sql).fetchone()) + + # Редактирование записи + @staticmethod + def update(**kwargs): + with sqlite3.connect(PATH_DATABASE) as con: + con.row_factory = dict_factory + sql = f"UPDATE {Settingsx.storage_name} SET" + sql, parameters = update_format(sql, kwargs) + + con.execute(sql, parameters) diff --git a/tgbot/database/db_tenders.py b/tgbot/database/db_tenders.py new file mode 100644 index 0000000..c717359 --- /dev/null +++ b/tgbot/database/db_tenders.py @@ -0,0 +1,137 @@ +# - *- coding: utf- 8 - *- +import sqlite3 + +from pydantic import BaseModel + +from tgbot.data.config import PATH_DATABASE +from tgbot.database.db_helper import dict_factory, update_format_where, update_format +from tgbot.utils.const_functions import get_unix, ded + + +# Модель таблицы +class UserModel(BaseModel): + increment: int + user_id: int + user_login: str + user_name: str + user_surname: str + user_fullname: str + notif: str + user_unix: int + + +# Работа с юзером +class Userx: + storage_name = "storage_users" + + # Добавление записи + @staticmethod + def add( + user_id: int, + user_login: str, + user_name: str, + user_surname: str, + user_fullname: str, + notif: str = "False", + ): + user_unix = get_unix() + + with sqlite3.connect(PATH_DATABASE) as con: + con.row_factory = dict_factory + + con.execute( + ded(f""" + INSERT INTO {Userx.storage_name} ( + user_id, + user_login, + user_name, + user_surname, + user_fullname, + notif, + user_unix + ) VALUES (?, ?, ?, ?, ?, ?, ?) + """), + [ + user_id, + user_login, + user_name, + user_surname, + user_fullname, + notif, + user_unix, + ], + ) + + # Получение записи + @staticmethod + def get(**kwargs) -> UserModel: + with sqlite3.connect(PATH_DATABASE) as con: + con.row_factory = dict_factory + sql = f"SELECT * FROM {Userx.storage_name}" + sql, parameters = update_format_where(sql, kwargs) + + response = con.execute(sql, parameters).fetchone() + + if response is not None: + response = UserModel(**response) + + return response + + # Получение записей + @staticmethod + def gets(**kwargs) -> list[UserModel]: + with sqlite3.connect(PATH_DATABASE) as con: + con.row_factory = dict_factory + sql = f"SELECT * FROM {Userx.storage_name}" + sql, parameters = update_format_where(sql, kwargs) + + response = con.execute(sql, parameters).fetchall() + + if len(response) >= 1: + response = [UserModel(**cache_object) for cache_object in response] + + return response + + # Получение всех записей + @staticmethod + def get_all() -> list[UserModel]: + with sqlite3.connect(PATH_DATABASE) as con: + con.row_factory = dict_factory + sql = f"SELECT * FROM {Userx.storage_name}" + + response = con.execute(sql).fetchall() + + if len(response) >= 1: + response = [UserModel(**cache_object) for cache_object in response] + + return response + + # Редактирование записи + @staticmethod + def update(user_id, **kwargs): + with sqlite3.connect(PATH_DATABASE) as con: + con.row_factory = dict_factory + sql = f"UPDATE {Userx.storage_name} SET" + sql, parameters = update_format(sql, kwargs) + parameters.append(user_id) + + con.execute(sql + "WHERE user_id = ?", parameters) + + # Удаление записи + @staticmethod + def delete(**kwargs): + with sqlite3.connect(PATH_DATABASE) as con: + con.row_factory = dict_factory + sql = f"DELETE FROM {Userx.storage_name}" + sql, parameters = update_format_where(sql, kwargs) + + con.execute(sql, parameters) + + # Очистка всех записей + @staticmethod + def clear(): + with sqlite3.connect(PATH_DATABASE) as con: + con.row_factory = dict_factory + sql = f"DELETE FROM {Userx.storage_name}" + + con.execute(sql) diff --git a/tgbot/database/db_users.py b/tgbot/database/db_users.py new file mode 100644 index 0000000..c717359 --- /dev/null +++ b/tgbot/database/db_users.py @@ -0,0 +1,137 @@ +# - *- coding: utf- 8 - *- +import sqlite3 + +from pydantic import BaseModel + +from tgbot.data.config import PATH_DATABASE +from tgbot.database.db_helper import dict_factory, update_format_where, update_format +from tgbot.utils.const_functions import get_unix, ded + + +# Модель таблицы +class UserModel(BaseModel): + increment: int + user_id: int + user_login: str + user_name: str + user_surname: str + user_fullname: str + notif: str + user_unix: int + + +# Работа с юзером +class Userx: + storage_name = "storage_users" + + # Добавление записи + @staticmethod + def add( + user_id: int, + user_login: str, + user_name: str, + user_surname: str, + user_fullname: str, + notif: str = "False", + ): + user_unix = get_unix() + + with sqlite3.connect(PATH_DATABASE) as con: + con.row_factory = dict_factory + + con.execute( + ded(f""" + INSERT INTO {Userx.storage_name} ( + user_id, + user_login, + user_name, + user_surname, + user_fullname, + notif, + user_unix + ) VALUES (?, ?, ?, ?, ?, ?, ?) + """), + [ + user_id, + user_login, + user_name, + user_surname, + user_fullname, + notif, + user_unix, + ], + ) + + # Получение записи + @staticmethod + def get(**kwargs) -> UserModel: + with sqlite3.connect(PATH_DATABASE) as con: + con.row_factory = dict_factory + sql = f"SELECT * FROM {Userx.storage_name}" + sql, parameters = update_format_where(sql, kwargs) + + response = con.execute(sql, parameters).fetchone() + + if response is not None: + response = UserModel(**response) + + return response + + # Получение записей + @staticmethod + def gets(**kwargs) -> list[UserModel]: + with sqlite3.connect(PATH_DATABASE) as con: + con.row_factory = dict_factory + sql = f"SELECT * FROM {Userx.storage_name}" + sql, parameters = update_format_where(sql, kwargs) + + response = con.execute(sql, parameters).fetchall() + + if len(response) >= 1: + response = [UserModel(**cache_object) for cache_object in response] + + return response + + # Получение всех записей + @staticmethod + def get_all() -> list[UserModel]: + with sqlite3.connect(PATH_DATABASE) as con: + con.row_factory = dict_factory + sql = f"SELECT * FROM {Userx.storage_name}" + + response = con.execute(sql).fetchall() + + if len(response) >= 1: + response = [UserModel(**cache_object) for cache_object in response] + + return response + + # Редактирование записи + @staticmethod + def update(user_id, **kwargs): + with sqlite3.connect(PATH_DATABASE) as con: + con.row_factory = dict_factory + sql = f"UPDATE {Userx.storage_name} SET" + sql, parameters = update_format(sql, kwargs) + parameters.append(user_id) + + con.execute(sql + "WHERE user_id = ?", parameters) + + # Удаление записи + @staticmethod + def delete(**kwargs): + with sqlite3.connect(PATH_DATABASE) as con: + con.row_factory = dict_factory + sql = f"DELETE FROM {Userx.storage_name}" + sql, parameters = update_format_where(sql, kwargs) + + con.execute(sql, parameters) + + # Очистка всех записей + @staticmethod + def clear(): + with sqlite3.connect(PATH_DATABASE) as con: + con.row_factory = dict_factory + sql = f"DELETE FROM {Userx.storage_name}" + + con.execute(sql) diff --git a/tgbot/keyboards/__init__.py b/tgbot/keyboards/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tgbot/keyboards/inline_main.py b/tgbot/keyboards/inline_main.py new file mode 100644 index 0000000..1cd47ea --- /dev/null +++ b/tgbot/keyboards/inline_main.py @@ -0,0 +1,26 @@ +# - *- coding: utf- 8 - *- +from aiogram.types import InlineKeyboardMarkup +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from tgbot.data.config import get_admins +from tgbot.utils.const_functions import ikb + + +# Кнопки инлайн меню +def menu_finl(user_id: int) -> InlineKeyboardMarkup: + keyboard = InlineKeyboardBuilder() + + # keyboard.row( + # ikb("User X", data="user_inline_x"), + # ikb("User 1", data="user_inline:user_btn"), + # ikb("User 2", data="..."), + # ) + + # if user_id in get_admins(): + # keyboard.row( + # ikb("Admin X", data="admin_inline_x"), + # ikb("Admin 1", data="admin_inline:admin_btn"), + # ikb("Admin 2", data="unknown"), + # ) + + return keyboard.as_markup() diff --git a/tgbot/keyboards/inline_misc.py b/tgbot/keyboards/inline_misc.py new file mode 100644 index 0000000..ffb83dc --- /dev/null +++ b/tgbot/keyboards/inline_misc.py @@ -0,0 +1,22 @@ +# - *- coding: utf- 8 - *- +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from tgbot.utils.const_functions import ikb + +# Тестовые админ инлайн кнопки +admin_inl = InlineKeyboardBuilder( +).row( + # ikb("Admin Inline 1", data="..."), + # ikb("Admin Inline 2", data="..."), +).row( + # ikb("Admin Inline 3", data="..."), +).as_markup() + +# Тестовые юзер инлайн кнопки +user_inl = InlineKeyboardBuilder( +).row( + # ikb("User Inline 1", data="..."), + # ikb("User Inline 2", data="..."), +).row( + # ikb("User Inline 3", data="..."), +).as_markup() diff --git a/tgbot/keyboards/reply_main.py b/tgbot/keyboards/reply_main.py new file mode 100644 index 0000000..7587159 --- /dev/null +++ b/tgbot/keyboards/reply_main.py @@ -0,0 +1,49 @@ +# - *- coding: utf- 8 - *- +from aiogram.types import ReplyKeyboardMarkup +from aiogram.utils.keyboard import ReplyKeyboardBuilder + +from tgbot.data.config import get_admins +from tgbot.utils.const_functions import rkb + + +# Кнопки главного меню +def menu_frep(user_id: int) -> ReplyKeyboardMarkup: + keyboard = ReplyKeyboardBuilder() + + # BotCommand(command="start", description="♻️ Restart bot"), + # BotCommand(command="parser", description="Запускает поиск тендоров"), + # BotCommand(command="status", description="Статус бота"), + # BotCommand(command="get_notif", description="Получать уведомления"), + # BotCommand(command="stop_get", description="Остановить получение уведомлений"), + # BotCommand(command="start_shed", description="Запуск работы по расписанию"), + # BotCommand(command="stop_shed", description="Остановка работы по расписанию"), + # # BotCommand(command="inline", description="🌀 Get Inline keyboard"), + # BotCommand(command="log", description="🖨 Get Logs"), + # BotCommand(command="db", description="📦 Get Database"), + + keyboard.row( + rkb("Поиск в tenderpro"), rkb("Tendrepro поиск за все время "), + ) + + keyboard.row( + rkb("Поиск в tenderplan"), rkb("Показать tenderplan"), + ) + + # keyboard.row( + # rkb("Поиск в tenderplan"), + # ) + + keyboard.row( + rkb("Получать уведомления"), rkb("Не получать уведомления"), rkb("Показать таблицу"), rkb("Статус бота"), rkb("Показать автопитер"), + ) + + # keyboard.row( + # rkb("Показать таблицу"), rkb("Статус бота"), + # ) + + # if user_id in get_admins(): + # keyboard.row( + # rkb("Admin Inline"), rkb("Admin Reply"), + # ) + + return keyboard.as_markup(resize_keyboard=True) diff --git a/tgbot/keyboards/reply_misc.py b/tgbot/keyboards/reply_misc.py new file mode 100644 index 0000000..c14c89f --- /dev/null +++ b/tgbot/keyboards/reply_misc.py @@ -0,0 +1,22 @@ +# - *- coding: utf- 8 - *- +from aiogram.utils.keyboard import ReplyKeyboardBuilder + +from tgbot.utils.const_functions import rkb + +# Тестовые админ реплай кнопки +admin_rep = ReplyKeyboardBuilder( +).row( + # rkb("Admin Reply 1"), + # rkb("Admin Reply 2"), +).row( + # rkb("🔙 Main menu"), +).as_markup(resize_keyboard=True) + +# Тестовые юзер реплай кнопки +user_rep = ReplyKeyboardBuilder( +).row( + # rkb("User Reply 1"), + # rkb("User Reply 2"), +).row( + # rkb("🔙 Main menu"), +).as_markup(resize_keyboard=True) diff --git a/tgbot/middlewares/__init__.py b/tgbot/middlewares/__init__.py new file mode 100644 index 0000000..7602a1d --- /dev/null +++ b/tgbot/middlewares/__init__.py @@ -0,0 +1,13 @@ +# - *- coding: utf- 8 - *- +from aiogram import Dispatcher + +from tgbot.middlewares.middleware_user import ExistsUserMiddleware +from tgbot.middlewares.middleware_throttling import ThrottlingMiddleware + + +# Регистрация всех миддлварей +def register_all_middlwares(dp: Dispatcher): + dp.callback_query.outer_middleware(ExistsUserMiddleware()) + dp.message.outer_middleware(ExistsUserMiddleware()) + + dp.message.middleware(ThrottlingMiddleware()) diff --git a/tgbot/middlewares/middleware_throttling.py b/tgbot/middlewares/middleware_throttling.py new file mode 100644 index 0000000..4aa814a --- /dev/null +++ b/tgbot/middlewares/middleware_throttling.py @@ -0,0 +1,63 @@ +# - *- coding: utf- 8 - *- +import time +from typing import Any, Awaitable, Callable, Dict, Union + +from aiogram import BaseMiddleware +from aiogram.dispatcher.flags import get_flag +from aiogram.types import Message, User +from cachetools import TTLCache + + +# Антиспам +class ThrottlingMiddleware(BaseMiddleware): + def __init__(self, default_rate: Union[int, float] = 1) -> None: + self.default_rate = default_rate + + self.users = TTLCache(maxsize=10_000, ttl=600) + + async def __call__(self, handler: Callable[[Message, Dict[str, Any]], Awaitable[Any]], event: Message, data): + this_user: User = data.get("event_from_user") + + if get_flag(data, "rate") is not None: + self.default_rate = get_flag(data, "rate") + + if self.default_rate == 0: + return await handler(event, data) + + if this_user.id not in self.users: + self.users[this_user.id] = { + 'last_throttled': int(time.time()), + 'count_throttled': 0, + 'now_rate': self.default_rate, + } + + return await handler(event, data) + else: + if int(time.time()) - self.users[this_user.id]['last_throttled'] >= self.users[this_user.id]['now_rate']: + self.users.pop(this_user.id) + + return await handler(event, data) + else: + self.users[this_user.id]['last_throttled'] = int(time.time()) + + if self.users[this_user.id]['count_throttled'] == 0: + self.users[this_user.id]['count_throttled'] = 1 + self.users[this_user.id]['now_rate'] = self.default_rate + 2 + + return await handler(event, data) + elif self.users[this_user.id]['count_throttled'] == 1: + self.users[this_user.id]['count_throttled'] = 2 + self.users[this_user.id]['now_rate'] = self.default_rate + 3 + + await event.reply( + "❗ Пожалуйста, не спамьте.\n" + "❗ Please, do not spam.", + ) + elif self.users[this_user.id]['count_throttled'] == 2: + self.users[this_user.id]['count_throttled'] = 3 + self.users[this_user.id]['now_rate'] = self.default_rate + 5 + + await event.reply( + "❗ Бот не будет отвечать до прекращения спама.\n" + "❗ The bot will not respond until the spam stops.", + ) diff --git a/tgbot/middlewares/middleware_user.py b/tgbot/middlewares/middleware_user.py new file mode 100644 index 0000000..ad10d25 --- /dev/null +++ b/tgbot/middlewares/middleware_user.py @@ -0,0 +1,48 @@ +# - *- coding: utf- 8 - *- +from aiogram import BaseMiddleware + +from tgbot.database.db_users import Userx +from tgbot.utils.const_functions import clear_html + + +# Проверка юзера в БД и его добавление +class ExistsUserMiddleware(BaseMiddleware): + async def __call__(self, handler, event, data): + this_user = data.get("event_from_user") + + if not this_user.is_bot: + get_user = Userx.get(user_id=this_user.id) + + user_id = this_user.id + user_login = this_user.username + user_name = clear_html(this_user.first_name) + user_surname = clear_html(this_user.last_name) + user_fullname = clear_html(this_user.first_name) + user_language = this_user.language_code + + if user_login is None: user_login = "" + if user_name is None: user_name = "" + if user_surname is None: user_surname = "" + if user_fullname is None: user_fullname = "" + if user_language != "ru": user_language = "en" + + if len(user_surname) >= 1: user_fullname += f" {user_surname}" + + if get_user is None: + Userx.add(user_id, user_login.lower(), user_name, user_surname, user_fullname) + else: + if user_name != get_user.user_name: + Userx.update(get_user.user_id, user_name=user_name) + + if user_surname != get_user.user_surname: + Userx.update(get_user.user_id, user_surname=user_surname) + + if user_fullname != get_user.user_fullname: + Userx.update(get_user.user_id, user_fullname=user_fullname) + + if user_login.lower() != get_user.user_login: + Userx.update(get_user.user_id, user_login=user_login.lower()) + + data['User'] = Userx.get(user_id=user_id) + + return await handler(event, data) diff --git a/tgbot/routers/.DS_Store b/tgbot/routers/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..81a343712ce4c72c01fcaaa10941e9b3441cc436 GIT binary patch literal 6148 zcmeHK!AiqG5S^`6Di!Hb@RF-|>L0`s??P{0TALPIXiQNmc+0_?|KKDjI^*+ZJI%9+lzsoO-ZRUSq&Loc@TfiA4qi@2 zk4Im5_rKvi8J4Sf<6<^ZWuGz{(M4F};a#{|uIu{s<5}w}#b^1Vmen3%{x0XQ(v+_7 zj$ek^cMr;I<=p%4TQg4PvE|IG*2BCfoY%w{Fb0f)pJD(#o26D4w9yza28@9@1N?o6 zpp3C%A?QCH82k|cScTaO<_(?zBgI&;5X1_^NhnZ4ogOiqgu@=GU#wUNN;o+^d^pYQ z^n~JkcHBSm?&M-Y8;t>DAY@=eE*sqckJq37!yvmd28@A!#el0O?WBcA(%!oCaNKJn rXcx-DeudyX1QXwi;mfUf59$T>h$p~Uu@Hm>Vm|_&1{;ilUuEDEmd$X) literal 0 HcmV?d00001 diff --git a/tgbot/routers/__init__.py b/tgbot/routers/__init__.py new file mode 100644 index 0000000..bb7d82c --- /dev/null +++ b/tgbot/routers/__init__.py @@ -0,0 +1,30 @@ +# - *- coding: utf- 8 - *- +from aiogram import Dispatcher, F + +from tgbot.routers import main_errors, main_missed, main_start +from tgbot.routers.admin import admin_menu +from tgbot.routers.user import user_menu +from tgbot.utils.misc.bot_filters import IsAdmin + + +# Регистрация всех роутеров +def register_all_routers(dp: Dispatcher): + # Подключение фильтров + main_errors.router.message.filter(F.chat.type == "private") + main_start.router.message.filter(F.chat.type == "private") + + user_menu.router.message.filter(F.chat.type == "private") + admin_menu.router.message.filter(F.chat.type == "private", IsAdmin()) + + main_missed.router.message.filter(F.chat.type == "private") + + # Подключение обязательных роутеров + dp.include_router(main_errors.router) # Роутер ошибки + dp.include_router(main_start.router) # Роутер основных команд + + # Подключение пользовательских роутеров (юзеров и админов) + dp.include_router(user_menu.router) # Юзер роутер + dp.include_router(admin_menu.router) # Админ роутер + + # Подключение обязательных роутеров + dp.include_router(main_missed.router) # Роутер пропущенных апдейтов diff --git a/tgbot/routers/admin/__init__.py b/tgbot/routers/admin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tgbot/routers/admin/admin_menu.py b/tgbot/routers/admin/admin_menu.py new file mode 100644 index 0000000..cd0352f --- /dev/null +++ b/tgbot/routers/admin/admin_menu.py @@ -0,0 +1,121 @@ +# - *- coding: utf- 8 - *- +import os + +import aiofiles +from aiogram import Router, Bot, F +from aiogram.filters import Command +from aiogram.types import FSInputFile, Message, CallbackQuery +from aiogram.utils.media_group import MediaGroupBuilder + +from tgbot.data.config import PATH_DATABASE, PATH_LOGS +from tgbot.database.db_users import UserModel +from tgbot.keyboards.inline_misc import admin_inl +from tgbot.keyboards.reply_misc import admin_rep +from tgbot.utils.const_functions import get_date +from tgbot.utils.misc_functions import send_employees +from tgbot.utils.misc.bot_models import FSM, ARS +from tgbot.utils.misc.bot_logging import bot_logger +from tgbot.services.parser_tendors import get_tenders_from_url + + +router = Router(name=__name__) + + +# # start shed +# @router.message(F.text.in_(('start_shed'))) +# @router.message(Command(commands=['start_shed'])) +# async def parser(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel): +# bot_logger.warning(f"command start_shed from {User.user_name}") + +# await send_employees(bot,"ss") + +# tenders_id = get_tenders_from_url() +# bot_logger.warning(f"tenders_id: {tenders_id}") +# answ = "" +# for num, tend in enumerate(tenders_id): +# answ += f"{num+1}. Наименование/артикул: {tend['article']}, id тендера: {tend['id_tender']}, url: {tend['url_tender']} \n \n" + +# await message.answer(answ) + + + +# # Кнопка - Admin Inline +# @router.message(F.text == 'Admin Inline') +# async def admin_button_inline(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel): +# await state.clear() + +# await message.answer("Click Button - Admin Inline", reply_markup=admin_inl) + + +# # Кнопка - Admin Reply +# @router.message(F.text == 'Admin Reply') +# async def admin_button_reply(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel): +# await state.clear() + +# await message.answer("Click Button - Admin Reply", reply_markup=admin_rep) + + +# # Колбэк - Admin X +# @router.callback_query(F.data == 'admin_inline_x') +# async def admin_callback_inline_x(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: UserModel): +# await call.answer(f"Click Admin X") + + +# # Колбэк - Admin +# @router.callback_query(F.data.startswith('admin_inline:')) +# async def admin_callback_inline(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: UserModel): +# get_data = call.data.split(":")[1] + +# await call.answer(f"Click Admin - {get_data}", True) + + +# Получение Базы Данных +@router.message(Command(commands=['db', 'database'])) +async def admin_database(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel): + await state.clear() + + await message.answer_document( + FSInputFile(PATH_DATABASE), + caption=f"📦 #BACKUP | {get_date()}", + ) + + +# # Получение логов +# @router.message(Command(commands=['log', 'logs'])) +# async def admin_log(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel): +# await state.clear() + +# media_group = MediaGroupBuilder( +# caption=f"🖨 #LOGS | {get_date(full=False)}", +# ) + +# if os.path.isfile(PATH_LOGS): +# media_group.add_document(media=FSInputFile(PATH_LOGS)) + +# if os.path.isfile("tgbot/data/sv_log_err.log"): +# media_group.add_document(media=FSInputFile("tgbot/data/sv_log_err.log")) + +# if os.path.isfile("tgbot/data/sv_log_out.log"): +# media_group.add_document(media=FSInputFile("tgbot/data/sv_log_out.log")) + +# await message.answer_media_group(media=media_group.build()) + + +# Очистить логи +@router.message(Command(commands=['clear_log', 'clear_logs', 'log_clear', 'logs_clear'])) +async def admin_logs_clear(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel): + await state.clear() + + if os.path.isfile(PATH_LOGS): + async with aiofiles.open(PATH_LOGS, "w") as file: + await file.write(f"{get_date()} | LOGS WAS CLEAR") + + if os.path.isfile("tgbot/data/sv_log_err.log"): + async with aiofiles.open("tgbot/data/sv_log_err.log", "w") as file: + await file.write(f"{get_date()} | LOGS WAS CLEAR") + + if os.path.isfile("tgbot/data/sv_log_out.log"): + async with aiofiles.open("tgbot/data/sv_log_out.log", "w") as file: + await file.write(f"{get_date()} | LOGS WAS CLEAR") + + await message.answer("🖨 The logs have been cleared") diff --git a/tgbot/routers/main_errors.py b/tgbot/routers/main_errors.py new file mode 100644 index 0000000..556365a --- /dev/null +++ b/tgbot/routers/main_errors.py @@ -0,0 +1,29 @@ +# - *- coding: utf- 8 - *- +from aiogram import Router +from aiogram.filters import ExceptionMessageFilter +from aiogram.handlers import ErrorHandler + +from tgbot.utils.misc.bot_logging import bot_logger + +router = Router(name=__name__) + + +# Ошибка с блокировкой бота пользователем +# @router.errors(ExceptionTypeFilter(TelegramForbiddenError)) +# class MyHandler(ErrorHandler): +# async def handle(self): +# pass + + +# Ошибка с редактированием одинакового сообщения +@router.errors(ExceptionMessageFilter( + "Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message") +) +class MyHandler(ErrorHandler): + async def handle(self): + bot_logger.exception( + f"====================\n" + f"Exception name: {self.exception_name}\n" + f"Exception message: {self.exception_message}\n" + f"====================" + ) diff --git a/tgbot/routers/main_missed.py b/tgbot/routers/main_missed.py new file mode 100644 index 0000000..f7b312b --- /dev/null +++ b/tgbot/routers/main_missed.py @@ -0,0 +1,61 @@ +# - *- coding: utf- 8 - *- +import io +from aiogram import Router, Bot, F +from aiogram.types import CallbackQuery, Message, BufferedInputFile +import aiogram +import pandas as pd +from tgbot.database.db_users import UserModel +from tgbot.utils.const_functions import del_message +from tgbot.utils.misc.bot_models import FSM, ARS + +from tgbot.services.parser_tendors import get_tenders_from_article, get_excel_from_tenders + +router = Router(name=__name__) + + +# Колбэк с удалением сообщения +@router.callback_query(F.data == 'close_this') +async def main_callback_close(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: UserModel): + await del_message(call.message) + + +# Колбэк с обработкой кнопки +@router.callback_query(F.data == '...') +async def main_callback_answer(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: UserModel): + await call.answer(cache_time=30) + + +# Колбэк с обработкой удаления сообщений потерявших стейт +@router.callback_query() +async def main_callback_missed(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: UserModel): + await call.answer(f"❗️ Miss callback: {call.data}", True) + + +# Обработка всех неизвестных команд +@router.message() +async def main_message_missed(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel): + try: + tenders = await get_tenders_from_article(message.text) + if (len(str(tenders))>2000): + tenders = pd.DataFrame(tenders) + get_excel_from_tenders(tenders) + with io.BytesIO() as output: + tenders.to_excel(output) + excel_data = output.getvalue() + file_excel = io.BytesIO(excel_data) + await message.answer_document(BufferedInputFile(file_excel.getvalue(), f"{message.text}.xlsx"), caption = f"Нашлось по запросу '{message.text}'") + else: + answ = "" + for num, tend in enumerate(tenders): + answ += f"{num+1}. Наименование/артикул: {tend['article']}, id тендера: {tend['id_tender']}, прием до: {tend['date_until']}, url: {tend['url_tender']} \n" + mes = f"Нашлось по запросу {message.text}: \n" + if answ == "": + mes += "Ничего не найдено" + else: + mes += answ + await message.answer(f"{mes}") + # except aiogram.exceptions.TelegramBadRequest: + # await message.answer(f"Ошибка: wg mes too long, {len(str(tenders))}") + except Exception as e: + await message.answer(f"Ошибка: {e}") + diff --git a/tgbot/routers/main_start.py b/tgbot/routers/main_start.py new file mode 100644 index 0000000..64cf3c6 --- /dev/null +++ b/tgbot/routers/main_start.py @@ -0,0 +1,235 @@ +# - *- coding: utf- 8 - *- +import os +import io +import pandas as pd + +from aiogram import Router, Bot, F +from aiogram.filters import Command +from aiogram.types import FSInputFile, Message, CallbackQuery, BufferedInputFile + +from aiogram.utils.media_group import MediaGroupBuilder +from tgbot.database.db_users import UserModel, Userx +from tgbot.keyboards.reply_main import menu_frep +from tgbot.utils.const_functions import ded +from tgbot.utils.misc.bot_models import FSM, ARS +from tgbot.utils.misc.bot_logging import bot_logger +from tgbot.services.parser_tendors import get_tenders_from_url, get_excel_from_tenders, get_articles +from tgbot.services.tender_plan import tenders_with_goods, search_in_tenderplan +from tgbot.data.config import BOT_SCHEDULER, PATH_EXCEL, PATH_LOGS +from tgbot.utils.const_functions import get_date + +router = Router(name=__name__) + + +# Открытие главного меню +@router.message(F.text.in_(())) +@router.message(Command(commands=['start'])) +async def main_start(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel): + await state.clear() + + bot_logger.warning(f"command start from {User.user_name}") + await message.answer( + ded(f""" + Привет, {User.user_name} + Это бот для поиска тендеров на сайте Tender.pro + Введите /parser для поиска прямо сейчас (ищет долго, минут 10) + Или /get_notif для получения уведомления в 8 и 18 часов + Команда /status показывает статус бота + Поиск за все время выведет таблицу в которой будут тендеры за все время, не важно какой статус + Чтобы обновить таблицу по которой искать просто пришлите ее мне + """), + reply_markup=menu_frep(message.from_user.id), + ) + + +# parser +@router.message(F.text.in_(('parser', 'Начать поиск сейчас', 'Поиск в tenderpro'))) +@router.message(Command(commands=['parser'])) +async def parser(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel): + try: + bot_logger.warning(f"command parser from {User.user_name}") + await message.answer("Идет поиск тендеров") + tenders_id = await get_tenders_from_url() + bot_logger.warning(f"tenders_id: {tenders_id}") + if (len(str(tenders_id))>4000): + tenders_id = pd.DataFrame(tenders_id) + get_excel_from_tenders(tenders_id) + with io.BytesIO() as output: + tenders_id.to_excel(output) + excel_data = output.getvalue() + file_excel = io.BytesIO(excel_data) + await message.answer_document(BufferedInputFile(file_excel.getvalue(), f"{message.text}.xlsx"), caption = f"Нашлось по запросу '{message.text}'") + else: + answ = "" + for num, tend in enumerate(tenders_id): + answ += f"{num+1}. Наименование/артикул: {tend['article']}, id тендера: {tend['id_tender']}, прием до: {tend['date_until']}, url: {tend['url_tender']} \n \n" + mes = f"Ответ на запрос поиска тендеров: \n \n" + if answ == "": + mes += "Ничего не найдено" + else: + mes += answ + await message.answer(f"{mes}") + except Exception as e: + await message.answer(f"Ошибка: {e}") + + +# status +@router.message(F.text.in_(('status', 'Статус бота'))) +@router.message(Command(commands=['status'])) +async def status(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel): + bot_logger.warning(f"command status from {User.user_name}") + jobs = BOT_SCHEDULER.get_jobs() + bot_logger.warning(f"jobs: {jobs}") + await message.answer(f"jobs: \n{[str(j) for j in jobs]}") + +# start notification +@router.message(F.text.in_(('Получать уведомления', 'get_notif'))) +@router.message(Command(commands=['get_notif'])) +async def get_notif(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel): + bot_logger.warning(f"command get_notif from {User.user_name}") + + if User.notif == "False": + Userx.update(User.user_id, notif = "True") + await message.answer("Теперь вы будете получать уведомления") + + else: + await message.answer("Вы уже получаете уведомления") + +# stop notification +@router.message(F.text.in_(('Не получать уведомления', 'stop_get_notif'))) +@router.message(Command(commands=['stop_get_notif'])) +async def stop_get_notif(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel): + bot_logger.warning(f"command stop_get_notif from {User.user_name}") + + if User.notif == "True": + Userx.update(User.user_id, notif = "False") + await message.answer("Теперь вы не будете получать уведомления") + + else: + await message.answer("Вы и так не получаете уведомления") + + +# Загрузка таблицы в бот +@router.message(F.content_type.in_({'document', 'file'})) +async def upload_excel(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel): + if (message.document.file_name.find('.xls') != -1): + file_id = message.document.file_id + file = await bot.get_file(file_id) + file_path = file.file_path + if message.document.file_name.find('articles_sheet') != -1: + link_temp = "tgbot/data/articles_sheet_temp.xlsx" + await bot.download_file(file_path, link_temp) + try: + arts = get_articles(link=link_temp) + bot_logger.warning(f"command upload_excel from {User.user_name}. файл: {message.document.file_name}, загружен") + await bot.download_file(file_path, "tgbot/data/articles_sheet.xlsx") + await message.answer("Таблица загружена") + except Exception as e: + bot_logger.warning(f"command upload_excel from {User.user_name}. файл: {message.document.file_name}, не загружен. Ошибка {e}") + await message.answer(f"Ошибка {e} \nФайл не был загружен") + else: + await bot.download_file(file_path, f"tgbot/data/price_{message.document.file_name}") + await message.answer(f"Таблица {message.document.file_name} загружена") + + else: + bot_logger.warning(f"command upload_excel from {User.user_name}. файл: {message.document.file_name}, не загружен") + await message.answer("Файл должен быть с расширением .xls или .xlsx") + + +# Выгрузка таблицы +@router.message(F.text.in_(('Показать таблицу', 'get_articles'))) +@router.message(Command(commands=['get_sheet', 'article'])) +async def get_sheet(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel): + await state.clear() + await message.answer_document( + FSInputFile(PATH_EXCEL), + # caption=f"📦 #BACKUP | {get_date()}", + caption=f"Таблица, которую вы загрузили и по которой выполняется поиск", + ) + +# Поиск тендеров за все время +@router.message(F.text.in_(('Поиск за все время', 'excel_from_tenders'))) +@router.message(Command(commands=['excel_from_tenders', 'tenders'])) +async def excel_from_tenders(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel): + await state.clear() + tenders_id = await get_tenders_from_url(tender_state=100) + get_excel_from_tenders(tenders_id=tenders_id) + + await message.answer_document( + FSInputFile('tgbot/data/tenders_id_all.xlsx'), + # caption=f"📦 #BACKUP | {get_date()}", + caption=f"Тендеры за все время.", + ) + + +# Получение логов +@router.message(Command(commands=['log', 'logs'])) +async def admin_log(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel): + await state.clear() + + media_group = MediaGroupBuilder( + caption=f"🖨 #LOGS | {get_date(full=False)}", + ) + + if os.path.isfile(PATH_LOGS): + media_group.add_document(media=FSInputFile(PATH_LOGS)) + + if os.path.isfile("tgbot/data/sv_log_err.log"): + media_group.add_document(media=FSInputFile("tgbot/data/sv_log_err.log")) + + if os.path.isfile("tgbot/data/sv_log_out.log"): + media_group.add_document(media=FSInputFile("tgbot/data/sv_log_out.log")) + + await message.answer_media_group(media=media_group.build()) + + +# Поиск тендеров в автопитере +@router.message(F.text.in_(('Поиск в автопитере', 'tenders_with_goods'))) +@router.message(Command(commands=['tenders_with_goods', 'search_ap'])) +async def search_in_ap(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel): + await state.clear() + bot_logger.warning(f"command search_in_ap from {User.user_name}") + await message.answer("Идет поиск тендеров") + tenders_with_goods(1) + await message.answer_document( + FSInputFile('tgbot/data/tenders_with_goods.xlsx'), + caption=f"Тендеры в автопитере.", + ) + + +# таблица тендеров в автопитере +@router.message(F.text.in_(('Показать автопитер', 'excel_ap'))) +@router.message(Command(commands=['excel_from_ap', 'show_ap'])) +async def excel_from_ap(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel): + await state.clear() + bot_logger.warning(f"command excel_from_ap from {User.user_name}") + await message.answer_document( + FSInputFile('tgbot/data/tenders_with_goods.xlsx'), + caption=f"Тендеры в автопитере.", + ) + + +# Поиск тендеров в tenderplan +@router.message(F.text.in_(('Поиск в tenderplan', 'tenders_in_tenderplan'))) +@router.message(Command(commands=['tenders_in_tenderplan', 'search_in_tenderplan', 'tenderplan'])) +async def search_in_tenderplan1(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel): + await state.clear() + bot_logger.warning(f"command search_in_tenderplan from {User.user_name}") + await message.answer("Идет поиск тендеров") + await search_in_tenderplan() + await message.answer_document( + FSInputFile('tgbot/data/tenders_tenderplan_from_art.xlsx'), + caption=f"Тендеры в tenderplan.", + ) + + +# таблица тендеров в tenderplan +@router.message(F.text.in_(('Показать tenderplan', 'excel_tenderplan'))) +@router.message(Command(commands=['excel_from_tenderplan', 'show_tenderplan'])) +async def excel_from_tenderplan(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel): + await state.clear() + bot_logger.warning(f"command excel_from_tenderplan from {User.user_name}") + await message.answer_document( + FSInputFile('tgbot/data/tenders_tenderplan_from_art.xlsx'), + caption=f"Тендеры в tenderplan.", + ) \ No newline at end of file diff --git a/tgbot/routers/user/__init__.py b/tgbot/routers/user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tgbot/routers/user/user_menu.py b/tgbot/routers/user/user_menu.py new file mode 100644 index 0000000..a38ac6f --- /dev/null +++ b/tgbot/routers/user/user_menu.py @@ -0,0 +1,59 @@ +# - *- coding: utf- 8 - *- +from aiogram import Router, Bot, F +from aiogram.filters import Command +from aiogram.types import Message, CallbackQuery + +from tgbot.database.db_users import UserModel +from tgbot.keyboards.inline_main import menu_finl +from tgbot.keyboards.inline_misc import user_inl +from tgbot.keyboards.reply_misc import user_rep +from tgbot.utils.misc.bot_models import FSM, ARS + +router = Router(name=__name__) + + +# # Кнопка - User Inline +# @router.message(F.text == 'User Inline') +# async def user_button_inline(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel): +# await state.clear() + +# await message.answer( +# "Click Button - User Inline", +# reply_markup=user_inl, +# ) + + +# # Кнопка - User Reply +# @router.message(F.text == 'User Reply') +# async def user_button_reply(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel): +# await state.clear() + +# await message.answer( +# "Click Button - User Reply", +# reply_markup=user_rep, +# ) + + +# # Команда - /inline +# @router.message(Command(commands="inline")) +# async def user_command_inline(message: Message, bot: Bot, state: FSM, arSession: ARS, User: UserModel): +# await state.clear() + +# await message.answer( +# "Click command - /inline", +# reply_markup=menu_finl(message.from_user.id), +# ) + + +# # Колбэк - User X +# @router.callback_query(F.data == 'user_inline_x') +# async def user_callback_inline_x(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: UserModel): +# await call.answer(f"Click User X") + + +# # Колбэк - User +# @router.callback_query(F.data.startswith('user_inline:')) +# async def user_callback_inline(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS, User: UserModel): +# get_data = call.data.split(":")[1] + +# await call.answer(f"Click User - {get_data}", True) diff --git a/tgbot/services/__init__.py b/tgbot/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tgbot/services/api_session.py b/tgbot/services/api_session.py new file mode 100644 index 0000000..738917b --- /dev/null +++ b/tgbot/services/api_session.py @@ -0,0 +1,30 @@ +# - *- coding: utf- 8 - *- +from typing import Optional + +import aiohttp + + +# In handler +# session = await arSession.get_session() +# response = await session.get(...) +# response = await session.post(...) + +# Асинхронная сессия для запросов +class AsyncRequestSession: + def __init__(self) -> None: + self._session: Optional[aiohttp.ClientSession] = None + + # Получение сессии + async def get_session(self) -> aiohttp.ClientSession: + if self._session is None: + new_session = aiohttp.ClientSession() + self._session = new_session + + return self._session + + # Закрытие сессии + async def close(self) -> None: + if self._session is None: + return None + + await self._session.close() diff --git a/tgbot/services/parser_tendors.py b/tgbot/services/parser_tendors.py new file mode 100644 index 0000000..3819c87 --- /dev/null +++ b/tgbot/services/parser_tendors.py @@ -0,0 +1,242 @@ +import requests +from bs4 import BeautifulSoup +import asyncio +from aiohttp import ClientSession +import pandas as pd +from time import time +import os, sys; sys.path.append(os.path.dirname(os.path.realpath(__file__))) +from tgbot.data.config import PATH_EXCEL +from tgbot.utils.misc.bot_logging import bot_logger + + + +# arrt = [ +# {'Name': 'О-514002', 'articles': ['О-514002', 'JX0818', '61000070005', '3831236', 'W 962/6', 'OP592', 'C-6204']}, +# {'Name': 'F-714117', 'articles': ['F-714117', '612630080087', 'FC-71090', 'R010018']}, +# {'Name': 'F-742003', 'articles': ['F-742003', '612630080088', 'PL420', 'VG1540080032', 'A 960 477 00 03', 'PL 420/7X', 'SFC-7939-30B']}, +# {'Name': 'F-742003X', 'articles': ['F-742003X', '612630080088', 'PL420 с подогревом', 'VG1540080032', 'A960 477 00 03','PL 420/7X', 'SFC-7939-30B']}, +# ] + +# article = { +# 'О-514002': ['О-514002', 'JX0818', '61000070005', '3831236', 'W 962/6', 'OP592', 'C-6204'], +# 'F-714117': ['F-714117', '612630080087', 'FC-71090', 'R010018'], +# 'F-742003': ['F-742003', '612630080088', 'PL420', 'VG1540080032', 'A 960 477 00 03', 'PL 420/7X', 'SFC-7939-30B'], +# 'F-742003X': ['F-742003X', '612630080088', 'PL420 с подогревом', 'VG1540080032', 'A960 477 00 03','PL 420/7X', 'SFC-7939-30B'], +# } + +# art1 = { +# 'F-714117': ['612630080087', 'FC-71090', 'R010018'], +# 'F-742003': ['612630080088', 'PL420', 'VG1540080032'], +# } + + +# def get_articles(): +# link = PATH_EXCEL +# art = pd.read_excel(link, skiprows=1) +# art = art.loc[:,["Наименование Wanlanda","Наименование аналога"]] +# art["Наименование аналога"] = art["Наименование аналога"].str.split(", |,") +# for a in art.iloc: +# a["Наименование аналога"].append(a["Наименование Wanlanda"]) +# return art + +def get_articles(link = PATH_EXCEL): + # link = PATH_EXCEL + all_article = pd.DataFrame(index=[], columns=['Наименование', 'Артикул']) + excel_reader = pd.ExcelFile(link) + for sheet_name in excel_reader.sheet_names: + exc = excel_reader.parse(sheet_name, usecols=['Наименование', 'Артикул']) + # exc['Наименование'] = exc['Наименование'].str + exc['Наименование'] = sheet_name + " / " + exc['Наименование'].astype(str) # добавление названия листа к наименованию позиции + all_article = pd.concat([all_article,exc], ignore_index=True) + all_article = all_article.dropna(inplace=False) + all_article["Артикул"] = all_article["Артикул"].astype(str) + all_article["Артикул"] = all_article["Артикул"].str.split(", | ,") + # for a in art.iloc: + # a["Артикул"].append(a["Наименование"]) + # print(f"колич артик:{len(all_article)}") + bot_logger.warning(f"колич артик:{len(all_article)}") + print(f"all_article: {all_article[:2]}") + return all_article[:] + +# для pd +def get_urls(tender_state = 1, article = 0): + # tender_state = 1:открытые 100:все + urls = [] + if (article == 0): + article = get_articles() + for val in article.iloc: + # print(val["Артикул"]) + for art in val["Артикул"]: + urls.append({"article": f"{val['Наименование']}/{art}", "url": f"http://www.tender.pro/api/tenders/list?&good_name={art}&tender_state={tender_state}&by=1000"}) + else: + articles = article.split(", |,") + for art in articles: + urls.append({"article": f"{art}", "url": f"http://www.tender.pro/api/tenders/list?&good_name={art}&tender_state={tender_state}&by=1000"}) + + return urls + + +async def fetch(url, session): + async with session.get(url['url']) as response: + status = response.status + date = response.headers.get("DATE") + print(f"{date}:{response.url} with status {status}") + data = {'url': url, 'response': await response.text()} + return data + + +async def bound_fetch(sem, url, session): + # Getter function with semaphore. + async with sem: + return await fetch(url, session) + + +# def get_tenders_from_url1(): +# urls = get_urls() +# tenders_id = [] +# for url in urls: +# response = requests.get(url["url"]) +# soup = BeautifulSoup(response.content, "html.parser") +# for tender in soup.find_all("td", class_="tender__id"): +# id_tender = tender.text +# print(id_tender + str(url["article"])) +# tenders_id.append({"article": url["article"], "id_tender": id_tender, "url_tender": f"https://www.tender.pro/api/tender/{id_tender}/view_public"}) +# return tenders_id + + +async def get_tenders_from_url(tender_state = 1): + urls = get_urls(tender_state) + return await search_tenders(urls) + +async def get_tenders_from_article(article): + urls = get_urls(article = article) + return await search_tenders(urls) + + +def sooup(soup, tenders_id, res): + for tender in soup.find_all("tr", class_="table-stat__row"): + try: + id_tender = tender.find("td", class_="tender__id").text + date_tender = tender.find("td", class_="tender__untill").text + except Exception: + continue + # if id_tender == None: + # continue + # id_tender = id_tender.text + for id in tenders_id: + if id_tender in id["id_tender"]: + print("ПОВТОРЕНИЕ") + return tenders_id + print(id_tender, date_tender) + # resp = requests.get(f"http://www.tender.pro/api/_tender.item.json?_key=1732ede4de680a0c93d81f01d7bac7d1&company_id=1&id={id_tender}") + # try: + # goods = resp.json().get("result").get("data") + # goods_name = "" + # goods_amount = 0 + # for g in goods: + # name = g.get("name") + # amount = float(g.get("amount")) + # if res['url']['article'].split("/")[1] in name: + # goods_name += name + " " + # goods_amount += amount + tenders_id.append({ + "article": res['url']['article'], + "id_tender": id_tender, + "date_until": date_tender, + "url_tender": f"https://www.tender.pro/api/tender/{id_tender}/view_public", + # "goods_name": goods_name, + # "goods_amount": goods_amount, + }) + # except Exception as e: + # print(e) + # pass + return tenders_id + + +async def search_tenders(urls): + tasks = [] + # create instance of Semaphore + sem = asyncio.Semaphore(5) + results = [] + t = time() + # Create client session that will ensure we dont open new connection + # per each request. + async with ClientSession() as session: + for url in urls: + # pass Semaphore and session to every GET request + task = asyncio.ensure_future(bound_fetch(sem, url, session)) + tasks.append(task) + + responses = asyncio.gather(*tasks) + results = await responses + print(time()-t) + print(len(results)) + t1 = time() + tenders_id = [] + for res in results: + soup = BeautifulSoup(res["response"], "html.parser") + pag = soup.find("div", class_="pagination-pages") + print(f"pag: {pag}") + if (pag != None): + print(f"pag: {str(pag)[44:45]}") + pages = int(str(pag)[44:45]) + urls2 = [] + for i in range(pages): + urls2.append(f"{res['url']['url']}&page={i}") + print(urls2) + for ur in urls2: + response = requests.get(ur) + soup1 = BeautifulSoup(response.content, "html.parser") + tenders_id = sooup(soup1, tenders_id, res) + tenders_id = sooup(soup, tenders_id, res) + + print(time() - t1) + for tend in tenders_id: + print(tend) + + return tenders_id + +def get_excel_from_tenders(tenders_id, link = 'tgbot/data/tenders_id_all.xlsx'): + tends = pd.DataFrame(tenders_id) + tends.to_excel(link) + + + +# https://www.tender.pro/api/tender/876455/view_public + +# urls = get_urls1(article) +# tenders_id = get_tenders_from_url(urls) +# for tend in tenders_id: +# print(tend) + + + +# zik@MacBook-Air-Ila парсер % /usr/local/bin/python3 /Users/zik/Documents/Programs/парсер/parser_tendors.py +# 876455F-714117/612630080087 +# 878638F-742003/PL420 +# {'article': 'F-714117/612630080087', 'id_tender': '876455'} +# {'article': 'F-742003/PL420', 'id_tender': '878638'} + + +# http://www2.tender.pro/api/tenders/list?sid=15932209&company_id=415538&face_id=440662&order=3&tmpl-opts=%22company_id%3A415538%22%2C%22face_id%3A440662%22%2C%22order%3A3%22%2C%22view_tenders_list-tmpl-signup%3A1%22%2C%22filter_reset%3A1%22%2C%22view_tenders_list-tmpl-name%3A%22%2C%22view_tenders_list-tmpl-default%3A%22%2C%22tender_id%3A%22%2C%22tender_name%3A%22%2C%22company_name%3A%22%2C%22good_name%3ASFC-7939-30B%22%2C%22tender_type%3A100%22%2C%22tender_state%3A1%22%2C%22tender_interest_type%3A%22%2C%22tender_invited%3A%22%2C%22country%3A0%22%2C%22region%3A%22%2C%22basis%3A0%22%2C%22tender_show_own%3A0%22%2C%22okved%3A%22%2C%22dateb%3A%22%2C%22datee%3A%22%2C%22dateb2%3A%22%2C%22datee2%3A%22%2C%22by%3A25%22&view_tenders_list-tmpl-signup=1&filter_tmpl=0&filter_reset=1&view_tenders_list-tmpl-name=&view_tenders_list-tmpl-default=&tender_id=&tender_name=&company_name=&good_name=VG1540080032&tender_type=100&tender_state=1&tender_interest_type=&tender_invited=&country=0®ion=&basis=0&tender_show_own=0&okved=&dateb=&datee=&dateb2=&datee2=&by=25 +# http://www.tender.pro/api/_tender.info.json?_key=1732ede4de680a0c93d81f01d7bac7d1&company_id=44441&id=144276 + +# https://www.tender.pro/api/tenders/list?sid= +# &company_id= +# &face_id=0 +# &order=3 +# &tender_id= +# &tender_name= +# &company_name= +# &good_name=PL+420 +# &tender_type=100 +# &tender_state=100 +# &country=0 +# ®ion= +# &basis=0 +# &okved= +# &dateb=&datee=&dateb2=&datee2= + +# PL420 + +# https://www.tender.pro/api/tenders/list?&good_name=PL420&tender_state=100 diff --git a/tgbot/services/tender_plan.py b/tgbot/services/tender_plan.py new file mode 100644 index 0000000..21f6eb7 --- /dev/null +++ b/tgbot/services/tender_plan.py @@ -0,0 +1,588 @@ +import os +import glob +import requests +import json +import pandas as pd +import re +from thefuzz import fuzz # type: ignore +from bs4 import BeautifulSoup +from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE +from datetime import datetime +import time +import asyncio +from aiohttp import ClientSession + +from tgbot.utils.misc.bot_logging import bot_logger +from tgbot.services.parser_tendors import get_articles + + +cookies = { + 'jwt': 's%3ABearer%200b998917d77807264c0b82d9ff64ca6fcefb01437019151eff52a4e706ebcd405099a030761ba0f5c6574aea12c525293c7da298b267c462224502a8c88c5289.p%2FACkoaEjBIK4u5vArNpU3Fh24DqBsyfGcHN8h%2BILig', + 'referer': 'https://tenderplan.ru/app', + 'source': 'response_type=code&client_id=619e606a7883684e0e3d10c7&redirect_uri=https%253A%252F%252Fbitrix24.tenderplan.ru%252Ftenderplan%252Foauth&scope=resources%253Aexternal%2520comments%2520marks%253Aread%2520notes%2520relations%253Aread%2520firm%253Aread&state=2f8e5161-d7e0-4e88-9e37-df3f1f75f20a', + '__ddg1_': 'ZKa7JlUseYuy3cvawO9W', +} + +headers = { + 'Accept': '*/*', + 'Authorization': 'Bearer 0b998917d77807264c0b82d9ff64ca6fcefb01437019151eff52a4e706ebcd405099a030761ba0f5c6574aea12c525293c7da298b267c462224502a8c88c5289', + 'Sec-Fetch-Site': 'same-origin', + 'Accept-Language': 'ru', + # 'Accept-Encoding': 'gzip, deflate, br', + 'Sec-Fetch-Mode': 'cors', + 'Host': 'tenderplan.ru', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15', + 'Connection': 'keep-alive', + 'Referer': 'https://tenderplan.ru/app', + # 'Cookie': 'jwt=s%3ABearer%200b998917d77807264c0b82d9ff64ca6fcefb01437019151eff52a4e706ebcd405099a030761ba0f5c6574aea12c525293c7da298b267c462224502a8c88c5289.p%2FACkoaEjBIK4u5vArNpU3Fh24DqBsyfGcHN8h%2BILig; referer=https://tenderplan.ru/app; source=response_type=code&client_id=619e606a7883684e0e3d10c7&redirect_uri=https%253A%252F%252Fbitrix24.tenderplan.ru%252Ftenderplan%252Foauth&scope=resources%253Aexternal%2520comments%2520marks%253Aread%2520notes%2520relations%253Aread%2520firm%253Aread&state=2f8e5161-d7e0-4e88-9e37-df3f1f75f20a; __ddg1_=ZKa7JlUseYuy3cvawO9W', + 'Sec-Fetch-Dest': 'empty', +} + +# def search_in_tenderplan(): + # https://tenderplan.ru/api/tenders/getlist?q=F-714117 + # https://tenderplan.ru/api/tenders/getlist?page=1&q=Komatsu + # ... + + +def get_urls(article = 0): + urls = [] + if (article == 0): + article = get_articles() + for val in article.iloc: + # print(val["Артикул"]) + for art in val["Артикул"]: + urls.append({"article": f"{val['Наименование']} / {art}", "art": art, "url": f"https://tenderplan.ru/api/tenders/getlist?isActual=1&q={art}"}) + else: + articles = article.split(", |,") + for art in articles: + urls.append({"article": f"{art}", "url": f"https://tenderplan.ru/api/tenders/getlist?isActual=1&q={art}"}) + + return urls + + +# async def fetch(url, session): +# st = '' +# try: +# async with session.get(url['url']) as response: +# k = 0 +# st = response.status +# while response.status != 200: +# time.sleep(5) +# print('sleeep') +# k += 1 +# if k > 5: break + +# date = response.headers.get("DATE") +# print(f"{date}:{response.url} with status {response.status}") +# data = {'url': url, 'response': await response.json()} +# return data +# except Exception as e: +# print(e) +# bot_logger.error(f"st:{st} --- {e}") + + +# async def bound_fetch(sem, url, session): +# # Getter function with semaphore. +# async with sem: +# return await fetch(url, session) + +async def fetch(url, session, retry_event): + st = '' + try: + async with session.get(url['url']) as response: + k = 0 + st = response.status + while response.status == 429: + await retry_event.wait() # Ожидаем разрешения продолжения запросов + async with session.get(url['url']) as response: + st = response.status + + while response.status != 200 and k <= 5: + await asyncio.sleep(5) # Асинхронная задержка + print('sleeep') + k += 1 + async with session.get(url['url']) as response: + st = response.status + + if response.status == 200: + date = response.headers.get("DATE") + print(f"{date}: {response.url} со статусом {response.status}") + data = {'url': url, 'response': await response.json()} + return data + else: + print(f"Не удалось получить {url['url']} после {k} попыток") + return None + except Exception as e: + print(e) + bot_logger.error(f"st: {st} --- {e}") + +async def bound_fetch(sem, url, session, retry_event): + # Функция получения данных с семафором + async with sem: + return await fetch(url, session, retry_event) + + + +# async def get_tenders_from_url(tender_state = 1): +# urls = get_urls(tender_state) +# return await search_tenders(urls) + +# async def get_tenders_from_article(article): +# urls = get_urls(article = article) +# return await search_tenders(urls) + +def sooup(tenders_id, tenders, res): + for tender in tenders: + tend_name = tender.get('orderName') + tend_id = tender.get('_id') + good_count = '' + submissionCloseDateTime = tender.get('submissionCloseDateTime') + date_until = datetime.fromtimestamp(submissionCloseDateTime/1000).strftime('%Y-%m-%d') + + params = {'id': tend_id,} + response = requests.get('https://tenderplan.ru/api/tenders/get', params=params, cookies=cookies, headers=headers) + if "Количество" in json.loads(response.json().get('json')).get("0").get("fv").get("0").get("fv").get("th").get("1").get("fv"): + goods = json.loads(response.json().get('json')).get("0").get("fv").get("0").get("fv").get("tb") + for good in goods: + good_name = goods.get(good).get('0').get('fv') + if res.get('url').get('art') in good_name: + good_count = goods.get(good).get('1').get('fv') + + print(f"name - {good_name}") + + print(tend_id, date_until) + + # поиск цены в таблицах + price = [] + path = "tgbot/data/" + abs_path = os.path.abspath(path) + # Поиск и вывод файлов + excel_files = [name for name in glob.glob(f'{abs_path}/price*.xls*')] + # print(f'excel_files-{excel_files}') + for file in excel_files: + # print(f'file-{file}') + # Загрузка Excel файла + excel_file = file + df = pd.read_excel(excel_file) + search_term = res.get("url").get("art") + if search_term.isalpha(): + continue + # Поиск строк, содержащих текст запроса + sear = df[df.apply(lambda row: row.astype(str).str.contains(search_term, case=False).any(), axis=1)].to_dict('index') + print("sear ",sear) + price.append({file.split('/')[-1]: sear}) + print("price ",price) + + # price - ['Артикул','Бренд','Кол-во','Цена, руб. с НДС'] + + for pri in price: + print("pri - ", sear) + for keyprice, valprice in pri.items(): + for p in valprice.values(): + print("p - ", p) + tenders_id.append({ + "article": res.get('url').get('article'), + "art0": res.get('url').get('art'), + # "price": price, + "файл": keyprice, + "Артикул": p.get("Артикул"), + "Бренд": p.get("Бренд"), + "Кол-во": p.get("Кол-во"), + "Цена": p.get("Цена, руб. с НДС"), + "good_count": good_count, + "id_tender": tend_id, + "url_tender": f"https://tenderplan.ru/app?tender={tend_id}", + "date_until": date_until, + "tend_name": tend_name, + "platform": response.json().get('platform').get('name'), + "href": response.json().get('href'), + }) + else: + tenders_id.append({ + "article": res.get('url').get('article'), + "art0": res.get('url').get('art'), + # "price": price, + "файл": '--', + "Артикул": '--', + "Бренд": '--', + "Кол-во": '--', + "Цена": '--', + "good_count": good_count, + "id_tender": tend_id, + "url_tender": f"https://tenderplan.ru/app?tender={tend_id}", + "date_until": date_until, + "tend_name": tend_name, + "platform": response.json().get('platform').get('name'), + "href": response.json().get('href'), + }) + # except Exception as e: + # print(e) + # pass + return tenders_id + + +async def search_in_tenderplan(urls = 0): + tenders_id = [] + try: + if urls == 0: + urls = get_urls() + else: + return 0 + tasks = [] + # create instance of Semaphore + sem = asyncio.Semaphore(3) + retry_event = asyncio.Event() + retry_event.set() # Устанавливаем событие в начальное состояние + + results = [] + t = time.time() + # Create client session that will ensure we dont open new connection + # per each request. + async with ClientSession(cookies=cookies, headers=headers) as session: + tasks = [bound_fetch(sem, url, session, retry_event) for url in urls] + + while True: + results1 = await asyncio.gather(*tasks, return_exceptions=True) + for result in results1: + if isinstance(result, Exception): + continue + if result and result.get('response').get('status') == 429: + retry_event.clear() # Останавливаем отправку запросов при 429 + await asyncio.sleep(5) # Ждем некоторое время перед повторной попыткой + retry_event.set() # Разрешаем отправку запросов снова + if all(result and result['response']['status'] == 200 for result in results if not isinstance(result, Exception)): + break # Завершаем, если все запросы успешны + + # responses = asyncio.gather(*tasks) + results = results1 + print(time.time()-t) + print(len(results)) + t1 = time.time() + for res in results: + try: + if res.get('response').get('tenders'): + tenders = res.get('response').get('tenders') + #pagination + # if len(tenders) > 50: + # page = 1 + # urls2 = f"https://tenderplan.ru/api/tenders/getlist?page={page}&q={}" + # while len(tenders) > 50: + # response = requests.get(ur) + # soup1 = BeautifulSoup(response.json, "html.parser") + # tenders_id = sooup(soup1, tenders_id, res) + ######## + tenders_id = sooup(tenders_id, tenders, res) + else: + print('tenders none') + # bot_logger.error(f"tenders none, скорее всего 429 ошибка") + # raise Exception("tenders none") + except Exception as e: + print(e) + bot_logger.error(f"{e}") + + print(time.time() - t1) + for tend in tenders_id: + print(tend) + + get_excel_from_tenderplan(tenders_id) + + return tenders_id + except Exception as e: + print(e) + get_excel_from_tenderplan(tenders_id) + bot_logger.error(f"{e}") + +def get_excel_from_tenderplan(tenders_id, link = 'tgbot/data/tenders_tenderplan_from_art.xlsx'): + # oldtends = pd.DataFrame(link) + # newtends = pd.DataFrame(tenders_id) + + # Предположим, ваш список с тендерами называется `` + df = pd.DataFrame(tenders_id) + df = df.drop_duplicates(ignore_index=True) + + # Читаем существующий Excel файл, если он есть + try: + existing_df = pd.read_excel(link, engine='openpyxl') + except FileNotFoundError: + existing_df = pd.DataFrame() + + # Объединяем DataFrame с помощью merge, используя indicator=True + merged_df = df.merge(existing_df, how='left', indicator=True) + + # Определяем строки, которые есть в ОБЕИХ таблицах + in_both_df = merged_df[merged_df['_merge'] == 'both'] + + # Сохраняем в Excel с выделением нужных строк красным + writer = pd.ExcelWriter(link, engine='xlsxwriter') + df.to_excel(writer, sheet_name='Tenders', index=False) + + # Получаем объект workbook + workbook = writer.book + # Получаем объект worksheet + worksheet = workbook.get_worksheet_by_name('Tenders') + + # Применяем стиль к строкам, которые есть в ОБЕИХ таблицах + for index in in_both_df.index: + worksheet.conditional_format(index+1, 0, index+1, len(df.columns)-1, { + 'type': 'no_errors', + 'format': workbook.add_format({'bg_color': '#FFC7CE'}) + }) + + # Сохраняем файл + writer.close() + + + + # newtends.to_excel(link) + + + +######################################--------AUTOPITER-------######################################## + + +def split_search(search_string): + search_split = [] + digit_count = sum(char.isdigit() for char in search_string) + if digit_count <= 1: + return [] + for word in search_string.split(" "): + digit_count = sum(char.isdigit() for char in word) + if ( + digit_count >= len(word) // 4 + and len(word)>=6 + and not(bool(re.search('[а-яА-Я]', word))) + + ): + search_split.append(word) + # if bool(re.search(r'\d', good_name)): + # continue + return search_split + + +def search_in_autopiter(search: str): + + + params = { + 'detailNumber': search, + 'isFullQuery': 'true', + } + + try: + ap_search = search + resp = requests.get('https://autopiter.ru/api/api/searchdetails', params=params) + print("response - ", resp.status_code) + print("good search - ", ap_search) + goodauto = [] + + if resp.status_code == 200: + params = {'idArticles': [position.get('id') for position in resp.json().get('data').get('catalogs')]} + get_cost_resp = requests.get('https://autopiter.ru/api/api/appraise/getcosts', params=params) + print(get_cost_resp) + + for position in resp.json().get('data').get('catalogs'): + # print("position ",position) + ap_name = position.get('name') + ap_id = position.get('id') + ap_number = position.get('number') + if get_cost_resp.status_code == 200: + ap_originalPrice = [cost.get('originalPrice') for cost in get_cost_resp.json().get('data') if cost.get('id') == ap_id and cost.get('originalPrice') > 0] + if ap_originalPrice == []: continue + else: ap_originalPrice = ap_originalPrice[0] + else: + ap_originalPrice = "--" + # print("ap_originalPrice - ",ap_originalPrice) + # print("descr совпадение - ",fuzz.partial_token_sort_ratio(name, descr)) + goodauto.append({ + 'ap_search': ap_search, + 'ap_name': ILLEGAL_CHARACTERS_RE.sub(r'', ap_name), + 'fuzz': fuzz.partial_token_sort_ratio(ap_search, ap_name), + 'ap_number': ap_number, + 'ap_originalPrice': ap_originalPrice, + 'link_autopiter': f"https://autopiter.ru/goods/{ap_number}/{position.get('catalogUrl')}/id{ap_id}", + 'ap_id': ap_id, + }) + elif resp.status_code == 429: + raise Exception("429") + if goodauto != []: + goodauto = pd.DataFrame(goodauto).sort_values(by="fuzz",ascending=False)[:1] + elif goodauto == []: + goodauto = [{ + 'ap_search': '----', + 'ap_name': '----', + 'fuzz': '', + 'ap_number': '', + 'ap_originalPrice': '', + 'link_autopiter': '----', + }] + return goodauto + # goodsinauto.append(goodsinauto) + + # print("name - ",search_in_autopiter(name)) + # amount = float(g.get("amount")) + # if tend.get('article').split("/")[1] in name: + # goods_name += name + " " + # goods_amount += amount + except Exception as e: + print(e) + bot_logger.error(f"{e}") + return [] + + +def tenders_with_goods(pagecount: int = 1): + try: + tenders_with_goods = [] + count = 0 + page = 0 + countn = 50 + while True: + try: + params = {'page': page} + response = requests.get('https://tenderplan.ru/api/tenders/getlist', params=params, cookies=cookies, headers=headers) + # if (response.json().get('tenders') == []): + # break + if (page>=pagecount): + break + # if (count>countn): + # break + page += 1 + tenders = response.json().get('tenders') + print(f"--------{len(tenders)}") + for tend in tenders: + try: + tend_name = tend.get('orderName') + id = tend.get('_id') + print(f"tender -- {tend_name} -- {id}") + params = { + 'id': id, + } + response = requests.get('https://tenderplan.ru/api/tenders/get', params=params, cookies=cookies, headers=headers) + if "ObjectInfo" in response.json().get('json'): + goods = json.loads(response.json().get('json'))["0"]["fv"]["0"]["fv"]["tb"] + submission_close_timestamp = int(json.loads(response.json().get('json'))['1']['fv']['1']['fv']) + print(submission_close_timestamp) + submission_close_datetime = datetime.fromtimestamp(submission_close_timestamp/1000).strftime('%Y-%m-%d %H:%M:%S') + for good in goods: + # if (count>countn): + # break + good_name = goods.get(good).get('0').get('fv') + print(f"name - {good_name}") + # count += 1 + + for name in split_search(good_name): + ap_search = search_in_autopiter(name) + for ap_s in ap_search.iterrows(): + # print(ap_s) + count += 1 + print("count",count) + tenders_with_goods.append({ + "tend_name": tend_name, + "tend_link": f"https://tenderplan.ru/app?tender={id}", + "tend_under": submission_close_datetime, + "good_name": good_name, + "ap_good_name": name, + "ap_search_name": ap_s[1].get('ap_name'), + "ap_search_fuzz": ap_s[1].get('fuzz'), + "ap_search_link": ap_s[1].get('link_autopiter'), + "ap_id": ap_s[1].get('ap_id'), + "ap_search_price": ap_s[1].get('ap_originalPrice'), + }) + # break + except Exception as e1: + print("error -- ",e1) + bot_logger.error(f"{e1}") + except Exception as e: + print(e) + bot_logger.error(f"{e}") + print(f"count - {count}") + + # for twg in tenders_with_goods: + # print(twg) + + print(f"excel!!!!!!!!!") + tenders_with_goods = get_all_price(tenders_with_goods) + tends = pd.DataFrame(tenders_with_goods) + tends.to_excel(r'tgbot/data/tenders_with_goods.xlsx') + print(f"excel!!!!!!!!!2") + + + return tenders_with_goods + except Exception as e: + print(e) + bot_logger.error(f"{e}") + + +def get_all_price(tenders_with_goods): + ap_ids = [] + c = 0 + tenders_with_goods2 = [] + for ap in tenders_with_goods: + ap_ids.append(ap.get('ap_id')) + + params = {'idArticles': ap_ids} + get_cost_resp = requests.get('https://autopiter.ru/api/api/appraise/getcosts', params=params) + while get_cost_resp.status_code == 429: + time.sleep(5) + c += 1 + get_cost_resp = requests.get('https://autopiter.ru/api/api/appraise/getcosts', params=params) + if c>10: break + + print(get_cost_resp.status_code) + cost_resp = get_cost_resp.json().get('data') + for ap in tenders_with_goods: + try: + id = ap.get("ap_id") + cost = [cost.get('originalPrice') for cost in cost_resp if cost.get('id') == id] + + if cost: + ap['ap_search_price'] = cost[0] + tenders_with_goods2.append(ap) + except Exception as e: + print(e) + bot_logger.error(f"{e}") + return tenders_with_goods2 + + +def beautitext(text: str): + + # texts = re.findall(r'\w', text) + + tt = [] + + texts = text.split(" ") + for te in texts: + digit_count = sum(char.isdigit() for char in te) + if digit_count >= len(te) // 2: + tt.append(te) + # if bool(re.search(r'\d', te)): + # tt.append(te) + + + return str(tt) + +# print(beautitext('Помпа ЗМЗ-406, 409 /Евро-3, Евро-4')) +# tenders_with_goods() + + + +# print(search_in_autopiter('Форсунка топливная Common Rail КАМАЗ')) + + + + + + +# curl 'https://tenderplan.ru/api/tenders/getlist?' \ +# -X 'GET' \ +# -H 'Accept: */*' \ +# -H 'Authorization: Bearer f7dcac67acdb2a348f5c81fc26cfafaba892a7bc02dceb97ffe079ad60b0edb4399522590c067b24459b785fa019006943700cc47033ca26b7aecd22f3777077' \ +# -H 'Sec-Fetch-Site: same-origin' \ +# -H 'Accept-Language: ru' \ +# -H 'Accept-Encoding: gzip, deflate, br' \ +# -H 'Sec-Fetch-Mode: cors' \ +# -H 'Host: tenderplan.ru' \ +# -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15' \ +# -H 'Referer: https://tenderplan.ru/app?key=0&tender=6628ae9952e24fc13583dd05' \ +# -H 'Connection: keep-alive' \ +# -H 'Cookie: jwt=s%3ABearer%20f7dcac67acdb2a348f5c81fc26cfafaba892a7bc02dceb97ffe079ad60b0edb4399522590c067b24459b785fa019006943700cc47033ca26b7aecd22f3777077.UH%2BcCzOylTzLr%2BF6Hf4kerem6GuMoVK%2FBSiOYmPCkEc; source=key=0&tender=6628ae9952e24fc13583dd05; previousUrl=tenderplan.ru%2Fbitrix24%2Finstructions%2F; tildasid=1713941269630.966260; tildauid=1713888711831.359844; referer=https://tenderplan.ru/app; __ddg1_=ZKa7JlUseYuy3cvawO9W' \ +# -H 'Sec-Fetch-Dest: empty' \ +# -H 'Socket: ZTvzcuitnjsM-m4OBCP4' \ No newline at end of file diff --git a/tgbot/services/test.py b/tgbot/services/test.py new file mode 100644 index 0000000..ad843f7 --- /dev/null +++ b/tgbot/services/test.py @@ -0,0 +1,163 @@ +import requests +import json +import pandas as pd +import re +from thefuzz import fuzz, process +from bs4 import BeautifulSoup +from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE +from datetime import datetime +import time + +from tgbot.utils.misc.bot_logging import bot_logger + +cookies = { + 'jwt': 's%3ABearer%20...', + 'referer': 'https://tenderplan.ru/app?key=0&tender=6639e01152e24fc13574139f', + 'source': 'key=0&tender=6639e01152e24fc13574139f', + 'tildauid': '1713888711831.359844', + '__ddg1_': 'ZKa7JlUseYuy3cvawO9W', +} + +headers = { + 'Accept': '*/*', + 'Authorization': 'Bearer a4dc57cc44d5ca62a06a7b19660840a66f3048028b417bbc813a1acf6f3691da841b9120373431377409359f64430b0644ee22ddf072fbe6ad656b57eeebe83d', + 'Sec-Fetch-Site': 'same-origin', + 'Accept-Language': 'ru', + 'Sec-Fetch-Mode': 'cors', + 'Host': 'tenderplan.ru', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15', + 'Connection': 'keep-alive', + 'Referer': 'https://tenderplan.ru/app?key=0&tender=6639e01152e24fc13574139f', + 'Sec-Fetch-Dest': 'empty', +} + +def clean_text(text): + return re.sub(r'[^\w\s]', '', text).lower() + +def split_search(search_string): + search_split = [] + digit_count = sum(char.isdigit() for char in search_string) + if digit_count <= 1: + return [] + for word in search_string.split(" "): + digit_count = sum(char.isdigit() for char in word) + if digit_count >= len(word) // 4 and len(word) >= 6 and not re.search('[а-яА-Я]', word): + search_split.append(word) + return search_split + +def search_in_autopiter(search): + params = {'detailNumber': search, 'isFullQuery': 'true'} + try: + resp = requests.get('https://autopiter.ru/api/api/searchdetails', params=params) + goodauto = [] + + if resp.status_code == 200: + params = {'idArticles': [position.get('id') for position in resp.json().get('data').get('catalogs')]} + get_cost_resp = requests.get('https://autopiter.ru/api/api/appraise/getcosts', params=params) + + for position in resp.json().get('data').get('catalogs'): + ap_name = position.get('name') + ap_id = position.get('id') + ap_number = position.get('number') + if get_cost_resp.status_code == 200: + ap_originalPrice = next((cost.get('originalPrice') for cost in get_cost_resp.json().get('data') if cost.get('id') == ap_id and cost.get('originalPrice') > 0), "--") + else: + ap_originalPrice = "--" + goodauto.append({ + 'ap_search': search, + 'ap_name': ILLEGAL_CHARACTERS_RE.sub(r'', ap_name), + 'fuzz': fuzz.partial_token_sort_ratio(search, ap_name), + 'ap_number': ap_number, + 'ap_originalPrice': ap_originalPrice, + 'link_autopiter': f"https://autopiter.ru/goods/{ap_number}/{position.get('catalogUrl')}/id{ap_id}", + 'ap_id': ap_id, + }) + elif resp.status_code == 429: + raise Exception("429") + if goodauto: + goodauto = pd.DataFrame(goodauto).sort_values(by="fuzz", ascending=False)[:1] + else: + goodauto = [{ + 'ap_search': '----', + 'ap_name': '----', + 'fuzz': '', + 'ap_number': '', + 'ap_originalPrice': '', + 'link_autopiter': '----', + }] + return goodauto + except Exception as e: + bot_logger.error(f"{e}") + return [] + +def tenders_with_goods(pagecount=1): + try: + tenders_with_goods = [] + page = 0 + + while page < pagecount: + params = {'page': page} + response = requests.get('https://tenderplan.ru/api/tenders/getlist', params=params, cookies=cookies, headers=headers) + tenders = response.json().get('tenders') + page += 1 + + for tend in tenders: + tend_name = tend.get('orderName') + id = tend.get('_id') + params = {'id': id} + response = requests.get('https://tenderplan.ru/api/tenders/get', params=params, cookies=cookies, headers=headers) + if "ObjectInfo" in response.json().get('json'): + goods = json.loads(response.json().get('json'))["0"]["fv"]["0"]["fv"]["tb"] + submission_close_timestamp = int(json.loads(response.json().get('json'))['1']['fv']['1']['fv']) + submission_close_datetime = datetime.fromtimestamp(submission_close_timestamp / 1000).strftime('%Y-%m-%d %H:%M:%S') + + for good in goods.values(): + good_name = good.get('0').get('fv') + for name in split_search(good_name): + ap_search = search_in_autopiter(name) + for ap_s in ap_search.iterrows(): + tenders_with_goods.append({ + "tend_name": tend_name, + "tend_link": f"https://tenderplan.ru/app?tender={id}", + "tend_under": submission_close_datetime, + "good_name": good_name, + "ap_good_name": name, + "ap_search_name": ap_s[1].get('ap_name'), + "ap_search_fuzz": ap_s[1].get('fuzz'), + "ap_search_link": ap_s[1].get('link_autopiter'), + "ap_id": ap_s[1].get('ap_id'), + "ap_search_price": ap_s[1].get('ap_originalPrice'), + }) + tenders_with_goods = get_all_price(tenders_with_goods) + tends = pd.DataFrame(tenders_with_goods) + tends.to_excel('tgbot/data/tenders_with_goods.xlsx') + + return tenders_with_goods + except Exception as e: + bot_logger.error(f"{e}") + +def get_all_price(tenders_with_goods): + ap_ids = [ap.get('ap_id') for ap in tenders_with_goods] + params = {'idArticles': ap_ids} + get_cost_resp = requests.get('https://autopiter.ru/api/api/appraise/getcosts', params=params) + + retry_count = 0 + while get_cost_resp.status_code == 429 and retry_count < 10: + time.sleep(5) + retry_count += 1 + get_cost_resp = requests.get('https://autopiter.ru/api/api/appraise/getcosts', params=params) + + if get_cost_resp.status_code == 200: + cost_resp = get_cost_resp.json().get('data') + for ap in tenders_with_goods: + id = ap.get("ap_id") + cost = next((cost.get('originalPrice') for cost in cost_resp if cost.get('id') == id), None) + if cost: + ap['ap_search_price'] = cost + + return tenders_with_goods + +def beautitext(text): + texts = text.split(" ") + tt = [te for te in texts if sum(char.isdigit() for char in te) >= len(te) // 2] + return str(tt) diff --git a/tgbot/utils/.DS_Store b/tgbot/utils/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..18b6f569e8c55009d105c175cc89600e7f061231 GIT binary patch literal 6148 zcmeHKu};H44E2=^L0vjB-Y-;wJyc<2>c$diifE-qiG(_Ii-hv@~uKQh2~H^gmaRM7@^x4pk?^)R01Wl>4m&oBEui!x1Tt8xa9_&j?3x>>$o{^mVD zz`I(svv|X{6j4D-dZfIY;l)RLt)0{LQ}vTm6I+zqEOxDT&;8_cO^g9!z!*3d22itE z;-R38#(*(k3^WYz_rXIM6UACEd^#|M762H*90YUjB{;?_CW^HnRv=D7ffDMp#c&c1 zyH~$Nu@;nYa@u@2J=tl8;=<|J-v@VciJ*r23_y64>dol)$fumx; zMd>u1;FYwt_Fj%_t%u%1S=g@@Y(g*zr5L_kiqD`yVD~%$CW^HnED-w<2sGGW4E!kr E-{e?OQUCw| literal 0 HcmV?d00001 diff --git a/tgbot/utils/__init__.py b/tgbot/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tgbot/utils/const_functions.py b/tgbot/utils/const_functions.py new file mode 100644 index 0000000..3ff05d3 --- /dev/null +++ b/tgbot/utils/const_functions.py @@ -0,0 +1,387 @@ +# - *- coding: utf- 8 - *- +import random +import time +import uuid +from datetime import datetime +from typing import Union + +import pytz +from aiogram import Bot +from aiogram.types import InlineKeyboardButton, KeyboardButton, WebAppInfo, Message, InlineKeyboardMarkup, \ + ReplyKeyboardMarkup + +from tgbot.utils.misc.bot_logging import bot_logger +from tgbot.data.config import get_admins, BOT_TIMEZONE + +######################################## AIOGRAM ######################################## +# Генерация реплай кнопки +def rkb(text: str) -> KeyboardButton: + return KeyboardButton(text=text) + + +# Генерация инлайн кнопки +def ikb(text: str, data: str = None, url: str = None, switch: str = None, web: str = None) -> InlineKeyboardButton: + if data is not None: + return InlineKeyboardButton(text=text, callback_data=data) + elif url is not None: + return InlineKeyboardButton(text=text, url=url) + elif switch is not None: + return InlineKeyboardButton(text=text, switch_inline_query=switch) + elif web is not None: + return InlineKeyboardButton(text=text, web_app=WebAppInfo(url=web)) + + +# Удаление сообщения с обработкой ошибки от телеграма +async def del_message(message: Message): + try: + await message.delete() + except: + ... + + +# Умная отправка сообщений (автоотправка сообщения с фото или без) +async def smart_message( + bot: Bot, + user_id: int, + text: str, + keyboard: Union[InlineKeyboardMarkup, ReplyKeyboardMarkup] = None, + photo: Union[str, None] = None, +): + if photo is not None and photo.title() != "None": + await bot.send_photo( + chat_id=user_id, + photo=photo, + caption=text, + reply_markup=keyboard, + ) + else: + await bot.send_message( + chat_id=user_id, + text=text, + reply_markup=keyboard, + ) + + +# Отправка сообщения всем админам +async def send_admins(bot: Bot, text: str, markup=None, not_me=0): + for admin in get_admins(): + try: + if str(admin) != str(not_me): + await bot.send_message( + admin, + text, + reply_markup=markup, + disable_web_page_preview=True, + ) + except: + ... + +# # Отправка сообщения пользователям по тендорам +# async def send_employees(bot: Bot, text: str, markup=None, not_me=0): +# get_users = get_employees() +# bot_logger.warning("employee {get_users}") + + +# # for admin in get_admins(): +# # try: +# # if str(admin) != str(not_me): +# # await bot.send_message( +# # admin, +# # text, +# # reply_markup=markup, +# # disable_web_page_preview=True, +# # ) +# # except: +# # ... + +######################################## ПРОЧЕЕ ######################################## +# Удаление отступов в многострочной строке ("""text""") +def ded(get_text: str) -> str: + if get_text is not None: + split_text = get_text.split("\n") + if split_text[0] == "": split_text.pop(0) + if split_text[-1] == "": split_text.pop() + save_text = [] + + for text in split_text: + while text.startswith(" "): + text = text[1:].strip() + + save_text.append(text) + get_text = "\n".join(save_text) + else: + get_text = "" + + return get_text + + +# Очистка текста от HTML тэгов ('test' -> *b*test*/b*) +def clear_html(get_text: str) -> str: + if get_text is not None: + if "" in get_text: get_text = get_text.replace(">", "*") + else: + get_text = "" + + return get_text + + +# Очистка пробелов в списке (['', 1, ' ', 2] -> [1, 2]) +def clear_list(get_list: list) -> list: + while "" in get_list: get_list.remove("") + while " " in get_list: get_list.remove(" ") + while "." in get_list: get_list.remove(".") + while "," in get_list: get_list.remove(",") + while "\r" in get_list: get_list.remove("\r") + while "\n" in get_list: get_list.remove("\n") + + return get_list + + +# Разбив списка на несколько частей ([1, 2, 3, 4] 2 -> [[1, 2], [3, 4]]) +def split_list(get_list: list, count: int) -> list[list]: + return [get_list[i:i + count] for i in range(0, len(get_list), count)] + + +# Получение текущей даты (True - дата с временем, False - дата без времени) +def get_date(full: bool = True) -> str: + if full: + return datetime.now(pytz.timezone(BOT_TIMEZONE)).strftime("%d.%m.%Y %H:%M:%S") + else: + return datetime.now(pytz.timezone(BOT_TIMEZONE)).strftime("%d.%m.%Y") + + +# Получение текущего unix времени (True - время в наносекундах, False - время в секундах) +def get_unix(full: bool = False) -> int: + if full: + return time.time_ns() + else: + return int(time.time()) + + +# Конвертация unix в дату и даты в unix +def convert_date(from_time, full=True, second=True) -> Union[str, int]: + from tgbot.data.config import BOT_TIMEZONE + + if "-" in str(from_time): + from_time = from_time.replace("-", ".") + + if str(from_time).isdigit(): + if full: + to_time = datetime.fromtimestamp(from_time, pytz.timezone(BOT_TIMEZONE)).strftime("%d.%m.%Y %H:%M:%S") + elif second: + to_time = datetime.fromtimestamp(from_time, pytz.timezone(BOT_TIMEZONE)).strftime("%d.%m.%Y %H:%M") + else: + to_time = datetime.fromtimestamp(from_time, pytz.timezone(BOT_TIMEZONE)).strftime("%d.%m.%Y") + else: + if " " in str(from_time): + cache_time = from_time.split(" ") + + if ":" in cache_time[0]: + cache_date = cache_time[1].split(".") + cache_time = cache_time[0].split(":") + else: + cache_date = cache_time[0].split(".") + cache_time = cache_time[1].split(":") + + if len(cache_date[0]) == 4: + x_year, x_month, x_day = cache_date[0], cache_date[1], cache_date[2] + else: + x_year, x_month, x_day = cache_date[2], cache_date[1], cache_date[0] + + x_hour, x_minute, x_second = cache_time[0], cache_time[2], cache_time[2] + + from_time = f"{x_day}.{x_month}.{x_year} {x_hour}:{x_minute}:{x_second}" + else: + cache_date = from_time.split(".") + + if len(cache_date[0]) == 4: + x_year, x_month, x_day = cache_date[0], cache_date[1], cache_date[2] + else: + x_year, x_month, x_day = cache_date[2], cache_date[1], cache_date[0] + + from_time = f"{x_day}.{x_month}.{x_year}" + + if " " in str(from_time): + to_time = int(datetime.strptime(from_time, "%d.%m.%Y %H:%M:%S").timestamp()) + else: + to_time = int(datetime.strptime(from_time, "%d.%m.%Y").timestamp()) + + return to_time + + +# Генерация уникального айди +def gen_id() -> int: + mac_address = uuid.getnode() + time_unix = int(str(time.time_ns())[:16]) + + return mac_address + time_unix + + +# Генерация пароля | default, number, letter, onechar +def gen_password(len_password: int = 16, type_password: str = "default") -> str: + if type_password == "default": + char_password = list("1234567890abcdefghigklmnopqrstuvyxwzABCDEFGHIGKLMNOPQRSTUVYXWZ") + elif type_password == "letter": + char_password = list("abcdefghigklmnopqrstuvyxwzABCDEFGHIGKLMNOPQRSTUVYXWZ") + elif type_password == "number": + char_password = list("1234567890") + elif type_password == "onechar": + char_password = list("1234567890") + + random.shuffle(char_password) + random_chars = "".join([random.choice(char_password) for x in range(len_password)]) + + if type_password == "onechar": + random_chars = f"{random.choice('abcdefghigklmnopqrstuvyxwzABCDEFGHIGKLMNOPQRSTUVYXWZ')}{random_chars[1:]}" + + return random_chars + + +# Дополнение к числу корректного времени (1 -> 1 день, 3 -> 3 дня) +def convert_times(get_time: int, get_type: str = "day") -> str: + get_time = int(get_time) + if get_time < 0: get_time = 0 + + if get_type == "second": + get_list = ['секунда', 'секунды', 'секунд'] + elif get_type == "minute": + get_list = ['минута', 'минуты', 'минут'] + elif get_type == "hour": + get_list = ['час', 'часа', 'часов'] + elif get_type == "day": + get_list = ['день', 'дня', 'дней'] + elif get_type == "month": + get_list = ['месяц', 'месяца', 'месяцев'] + else: + get_list = ['год', 'года', 'лет'] + + if get_time % 10 == 1 and get_time % 100 != 11: + count = 0 + elif 2 <= get_time % 10 <= 4 and (get_time % 100 < 10 or get_time % 100 >= 20): + count = 1 + else: + count = 2 + + return f"{get_time} {get_list[count]}" + + +# Проверка на булевый тип +def is_bool(value: Union[bool, str, int]) -> bool: + value = str(value).lower() + + if value in ('y', 'yes', 't', 'true', 'on', '1'): + return True + elif value in ('n', 'no', 'f', 'false', 'off', '0'): + return False + else: + raise ValueError(f"invalid truth value {value}") + + +######################################## ЧИСЛА ######################################## +# Преобразование экспоненциальных чисел в читаемый вид (1e-06 -> 0.000001) +def snum(amount: Union[int, float], remains: int = 2) -> str: + format_str = "{:." + str(remains) + "f}" + str_amount = format_str.format(float(amount)) + + if remains != 0: + if "." in str_amount: + remains_find = str_amount.find(".") + remains_save = remains_find + 8 - (8 - remains) + 1 + + str_amount = str_amount[:remains_save] + + if "." in str(str_amount): + while str(str_amount).endswith('0'): str_amount = str(str_amount)[:-1] + + if str(str_amount).endswith('.'): str_amount = str(str_amount)[:-1] + + return str(str_amount) + + +# Конвертация любого числа в вещественное, с удалением нулей в конце (remains - округление) +def to_float(get_number, remains: int = 2) -> Union[int, float]: + if "," in str(get_number): + get_number = str(get_number).replace(",", ".") + + if "." in str(get_number): + get_last = str(get_number).split(".") + + if str(get_last[1]).endswith("0"): + while True: + if str(get_number).endswith("0"): + get_number = str(get_number)[:-1] + else: + break + + get_number = round(float(get_number), remains) + + str_number = snum(get_number) + if "." in str_number: + if str_number.split(".")[1] == "0": + get_number = int(get_number) + else: + get_number = float(get_number) + else: + get_number = int(get_number) + + return get_number + + +# Конвертация вещественного числа в целочисленное +def to_int(get_number: float) -> int: + if "," in get_number: + get_number = str(get_number).replace(",", ".") + + get_number = int(round(float(get_number))) + + return get_number + + +# Проверка ввода на число +def is_number(get_number: Union[str, int, float]) -> bool: + if str(get_number).isdigit(): + return True + else: + if "," in str(get_number): get_number = str(get_number).replace(",", ".") + + try: + float(get_number) + return True + except ValueError: + return False + + +# Преобразование числа в читаемый вид (123456789 -> 123 456 789) +def format_rate(amount: Union[float, int], around: int = 2) -> str: + if "," in str(amount): amount = float(str(amount).replace(",", ".")) + if " " in str(amount): amount = float(str(amount).replace(" ", "")) + amount = str(round(amount, around)) + + out_amount, save_remains = [], "" + + if "." in amount: save_remains = amount.split(".")[1] + save_amount = [char for char in str(int(float(amount)))] + + if len(save_amount) % 3 != 0: + if (len(save_amount) - 1) % 3 == 0: + out_amount.extend([save_amount[0]]) + save_amount.pop(0) + elif (len(save_amount) - 2) % 3 == 0: + out_amount.extend([save_amount[0], save_amount[1]]) + save_amount.pop(1) + save_amount.pop(0) + else: + print("Error 4388326") + + for x, char in enumerate(save_amount): + if x % 3 == 0: out_amount.append(" ") + out_amount.append(char) + + response = "".join(out_amount).strip() + "." + save_remains + + if response.endswith("."): + response = response[:-1] + + return response diff --git a/tgbot/utils/misc/__init__.py b/tgbot/utils/misc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tgbot/utils/misc/bot_commands.py b/tgbot/utils/misc/bot_commands.py new file mode 100644 index 0000000..4afb310 --- /dev/null +++ b/tgbot/utils/misc/bot_commands.py @@ -0,0 +1,49 @@ +# - *- coding: utf- 8 - *- +from aiogram import Bot +from aiogram.types import BotCommand, BotCommandScopeChat, BotCommandScopeDefault + +from tgbot.data.config import get_admins + +# Команды для юзеров +user_commands = [ + BotCommand(command="start", description="Restart bot"), + BotCommand(command="parser", description="Запускает поиск тендоров"), + BotCommand(command="status", description="статус бота"), + BotCommand(command="get_notif", description="получать уведомления"), + BotCommand(command="stop_get", description="остановить получение уведомлений"), + BotCommand(command="get_sheet", description="Получить таблицу"), + BotCommand(command="excel_from_tenders", description="Поиск за все время"), + BotCommand(command="tenders_with_goods", description="Поиск в автопитере"), + BotCommand(command="tenderplan", description="Поиск в tenderplan"), + BotCommand(command="log", description="Get Logs"), + # BotCommand(command="inline", description="🌀 Get Inline keyboard"), +] + +# Команды для админов +admin_commands = [ + BotCommand(command="start", description="Restart bot"), + BotCommand(command="parser", description="Запускает поиск тендоров"), + BotCommand(command="status", description="статус бота"), + BotCommand(command="get_notif", description="получать уведомления"), + BotCommand(command="stop_get_notif", description="остановить получение уведомлений"), + BotCommand(command="start_shed", description="Запуск работы по расписанию"), + BotCommand(command="stop_shed", description="остановка работы по расписанию"), + BotCommand(command="get_sheet", description="Получить таблицу"), + BotCommand(command="excel_from_tenders", description="Поиск за все время"), + BotCommand(command="tenders_with_goods", description="Поиск в автопитере"), + BotCommand(command="tenderplan", description="Поиск в tenderplan"), + BotCommand(command="log", description="Get Logs"), + # BotCommand(command="inline", description="🌀 Get Inline keyboard"), + BotCommand(command="db", description="📦 Get Database"), +] + + +# Установка команд +async def set_commands(bot: Bot): + await bot.set_my_commands(user_commands, scope=BotCommandScopeDefault()) + + for admin in get_admins(): + try: + await bot.set_my_commands(admin_commands, scope=BotCommandScopeChat(chat_id=admin)) + except: + pass diff --git a/tgbot/utils/misc/bot_filters.py b/tgbot/utils/misc/bot_filters.py new file mode 100644 index 0000000..26b2abc --- /dev/null +++ b/tgbot/utils/misc/bot_filters.py @@ -0,0 +1,22 @@ +# - *- coding: utf- 8 - *- +from aiogram.filters import BaseFilter +from aiogram.types import Message + +from tgbot.data.config import get_admins +from tgbot.database.db_users import Userx +from tgbot.utils.misc.bot_logging import bot_logger + + +# Проверка на админа +class IsAdmin(BaseFilter): + async def __call__(self, message: Message) -> bool: + if message.from_user.id in get_admins(): + return True + else: + return False + +# Отправка сообщения пользователям по тендорам +def get_employees(): + get_users = Userx.gets(notif = "True") + # bot_logger.warning(f"employee {get_users}") + return get_users \ No newline at end of file diff --git a/tgbot/utils/misc/bot_logging.py b/tgbot/utils/misc/bot_logging.py new file mode 100644 index 0000000..a011c36 --- /dev/null +++ b/tgbot/utils/misc/bot_logging.py @@ -0,0 +1,32 @@ +# - *- coding: utf- 8 - *- +import logging as bot_logger + +import colorlog + +from tgbot.data.config import PATH_LOGS + +# Формат логгирования +log_formatter_file = bot_logger.Formatter("%(levelname)s | %(asctime)s | %(filename)s:%(lineno)d | %(message)s") +log_formatter_console = colorlog.ColoredFormatter( + "%(purple)s%(levelname)s %(blue)s|%(purple)s %(asctime)s %(blue)s|%(purple)s %(filename)s:%(lineno)d %(blue)s|%(purple)s %(message)s%(red)s", + datefmt="%d-%m-%Y %H:%M:%S", +) + +# Логгирование в файл logs.log +file_handler = bot_logger.FileHandler(PATH_LOGS, "w", "utf-8") +file_handler.setFormatter(log_formatter_file) +file_handler.setLevel(bot_logger.DEBUG) + +# Логгирование в консоль +console_handler = bot_logger.StreamHandler() +console_handler.setFormatter(log_formatter_console) +console_handler.setLevel(bot_logger.DEBUG) + +# Подключение настроек логгирования +bot_logger.basicConfig( + format="%(levelname)s | %(asctime)s | %(filename)s:%(lineno)d | %(message)s", + handlers=[ + file_handler, + console_handler + ] +) diff --git a/tgbot/utils/misc/bot_models.py b/tgbot/utils/misc/bot_models.py new file mode 100644 index 0000000..0e726de --- /dev/null +++ b/tgbot/utils/misc/bot_models.py @@ -0,0 +1,7 @@ +# - *- coding: utf- 8 - *- +from aiogram.fsm.context import FSMContext + +from tgbot.services.api_session import AsyncRequestSession + +FSM = FSMContext +ARS = AsyncRequestSession diff --git a/tgbot/utils/misc_functions.py b/tgbot/utils/misc_functions.py new file mode 100644 index 0000000..83f6ba4 --- /dev/null +++ b/tgbot/utils/misc_functions.py @@ -0,0 +1,123 @@ +# - *- coding: utf- 8 - *- +from aiogram import Bot +from aiogram.types import FSInputFile, BufferedInputFile + +from tgbot.data.config import get_admins, PATH_DATABASE, start_status +from tgbot.utils.const_functions import get_date, send_admins +from tgbot.utils.misc.bot_logging import bot_logger +from tgbot.services.parser_tendors import get_tenders_from_url, get_excel_from_tenders +from tgbot.utils.misc.bot_filters import get_employees +from tgbot.services.tender_plan import tenders_with_goods, search_in_tenderplan +import os +import io +import pandas as pd + +# Отправка сообщения пользователям по тендорам +# async def get_employees(bot: Bot, text: str, markup=None, not_me=0): +# get_users = Userx.gets(notif = "False") +# bot_logger.warning("employee {get_users}") + +# Отправка сообщения пользователям по тендорам +async def send_employees(bot: Bot, text: str, markup=None, not_me=0): + get_empl = get_employees() + bot_logger.warning(f"employee1 {get_empl}") + for empl in get_empl: + await bot.send_message( + empl.user_id, + text, + reply_markup=markup, + # disable_web_page_preview=True, + disable_notification=True, + ) + + # for admin in get_admins(): + # try: + # if str(admin) != str(not_me): + # await bot.send_message( + # admin, + # text, + # reply_markup=markup, + # disable_web_page_preview=True, + # ) + # except: + # ... + + +# Выполнение функции после запуска бота (рассылка админам о запуске бота) +async def startup_notify(bot: Bot): + if len(get_admins()) >= 1 and start_status: + await send_admins(bot, "✅ Bot was started") + + +# Автоматические бэкапы БД +async def autobackup_admin(bot: Bot): + for admin in get_admins(): + try: + await bot.send_document( + admin, + FSInputFile(PATH_DATABASE), + caption=f"📦 #AUTOBACKUP | {get_date()}", + ) + except: + pass + + +# поиск тендора по расписанию +async def tenders_sched(bot: Bot): + try: + tenders_id = await get_tenders_from_url() + bot_logger.warning(f"tenders_id: {tenders_id}") + if (len(str(tenders_id))>4000): + tenders_id = pd.DataFrame(tenders_id) + get_excel_from_tenders(tenders_id) + with io.BytesIO() as output: + tenders_id.to_excel(output) + excel_data = output.getvalue() + file_excel = io.BytesIO(excel_data) + get_empl = get_employees() + bot_logger.warning(f"employee1 {get_empl}") + for empl in get_empl: + await bot.send_document( + empl.user_id, + BufferedInputFile(file_excel.getvalue(), f"everyday_articles.xlsx"), + caption = f"Нашлось по расписанию", + disable_notification=True) + else: + answ = "" + for num, tend in enumerate(tenders_id): + answ += f"{num+1}. Наименование/артикул: {tend['article']}, id тендера: {tend['id_tender']}, прием до: {tend['date_until']}, url: {tend['url_tender']} \n \n" + mes = f"Автоматический поиск тендоров: \n \n" + if answ == "": + mes += "Ничего не найдено" + else: + mes += answ + await send_employees(bot, mes) + except: + send_admins(bot, f"Ошибка при автоматическом поиске") + + +# поиск тендора в автопитере по расписанию +async def tenders_sched_ap(bot: Bot): + bot_logger.warning(f"tenders_sched_ap start") + try: + tenders_with_goods(20) + await send_employees(bot, "поиск по автопитеру выполнен") + except: + send_admins(bot, f"Ошибка при поиск по автопитеру") + + +# Поиск тендеров в tenderplan по расписанию +async def tenders_sched_ap_in_tenderplan(bot: Bot): + bot_logger.warning(f"tenders_sched_ap_in_tenderplan") + await search_in_tenderplan() + get_empl = get_employees() + for empl in get_empl: + await bot.send_document( + empl.user_id, + FSInputFile('tgbot/data/tenders_tenderplan_from_art.xlsx'), + caption = f"Нашлось из tenderplan по расписанию", + disable_notification=True) + # await message.answer_document( + # FSInputFile('tgbot/data/tenders_tenderplan_from_art.xlsx'), + # caption=f"Тендеры из tenderplan.", + # ) \ No newline at end of file