xync-bot 0.2.9__tar.gz → 0.3.31.dev0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xync_bot-0.3.31.dev0/.env.dist +4 -0
- xync_bot-0.3.31.dev0/.gitignore +8 -0
- xync_bot-0.3.31.dev0/.pre-commit-config.yaml +36 -0
- xync_bot-0.3.31.dev0/PKG-INFO +21 -0
- xync_bot-0.3.31.dev0/makefile +24 -0
- xync_bot-0.3.31.dev0/pager.py +274 -0
- xync_bot-0.3.31.dev0/pyproject.toml +47 -0
- xync_bot-0.3.31.dev0/test_main.http +9 -0
- xync_bot-0.3.31.dev0/xync_bot/__init__.py +93 -0
- xync_bot-0.3.31.dev0/xync_bot/__main__.py +18 -0
- xync_bot-0.3.31.dev0/xync_bot/loader.py +23 -0
- xync_bot-0.3.31.dev0/xync_bot/routers/__init__.py +41 -0
- xync_bot-0.3.31.dev0/xync_bot/routers/cond/__init__.py +91 -0
- xync_bot-0.3.31.dev0/xync_bot/routers/cond/func.py +118 -0
- xync_bot-0.3.31.dev0/xync_bot/routers/hot/__init__.py +77 -0
- xync_bot-0.3.31.dev0/xync_bot/routers/main/__init__.py +0 -0
- xync_bot-0.3.31.dev0/xync_bot/routers/main/handler.py +191 -0
- xync_bot-0.3.31.dev0/xync_bot/routers/order.py +12 -0
- xync_bot-0.3.31.dev0/xync_bot/routers/pay/cd.py +49 -0
- xync_bot-0.3.31.dev0/xync_bot/routers/pay/dep.py +119 -0
- xync_bot-0.3.31.dev0/xync_bot/routers/pay/handler.py +256 -0
- xync_bot-0.3.31.dev0/xync_bot/routers/pay/window.py +249 -0
- xync_bot-0.3.31.dev0/xync_bot/routers/photo.py +36 -0
- xync_bot-0.3.31.dev0/xync_bot/routers/send/__init__.py +117 -0
- xync_bot-0.3.31.dev0/xync_bot/routers/xicon.png +0 -0
- xync_bot-0.3.31.dev0/xync_bot/shared.py +30 -0
- xync_bot-0.3.31.dev0/xync_bot/store.py +151 -0
- xync_bot-0.3.31.dev0/xync_bot/typs.py +0 -0
- xync_bot-0.3.31.dev0/xync_bot.egg-info/PKG-INFO +21 -0
- xync_bot-0.3.31.dev0/xync_bot.egg-info/SOURCES.txt +33 -0
- xync_bot-0.3.31.dev0/xync_bot.egg-info/requires.txt +12 -0
- xync_bot-0.2.9/PKG-INFO +0 -15
- xync_bot-0.2.9/pyproject.toml +0 -24
- xync_bot-0.2.9/xync_bot/__init__.py +0 -39
- xync_bot-0.2.9/xync_bot/handlers/__init__.py +0 -2
- xync_bot-0.2.9/xync_bot/handlers/main.py +0 -117
- xync_bot-0.2.9/xync_bot/shared.py +0 -5
- xync_bot-0.2.9/xync_bot.egg-info/PKG-INFO +0 -15
- xync_bot-0.2.9/xync_bot.egg-info/SOURCES.txt +0 -11
- xync_bot-0.2.9/xync_bot.egg-info/requires.txt +0 -4
- {xync_bot-0.2.9 → xync_bot-0.3.31.dev0}/setup.cfg +0 -0
- {xync_bot-0.2.9/xync_bot/handlers → xync_bot-0.3.31.dev0/xync_bot/routers}/vpn.py +0 -0
- {xync_bot-0.2.9 → xync_bot-0.3.31.dev0}/xync_bot.egg-info/dependency_links.txt +0 -0
- {xync_bot-0.2.9 → xync_bot-0.3.31.dev0}/xync_bot.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: local
|
|
3
|
+
hooks:
|
|
4
|
+
- id: tag
|
|
5
|
+
name: tag
|
|
6
|
+
### make tag with next ver only if "fix" in commit_msg or starts with "feat"
|
|
7
|
+
entry: bash -c 'grep -e ^feat -e fix .git/COMMIT_EDITMSG && make patch || exit 0'
|
|
8
|
+
language: system
|
|
9
|
+
verbose: true
|
|
10
|
+
pass_filenames: false
|
|
11
|
+
always_run: true
|
|
12
|
+
stages: [post-commit]
|
|
13
|
+
|
|
14
|
+
- id: build
|
|
15
|
+
name: build
|
|
16
|
+
### build & upload package only for "main" branch push
|
|
17
|
+
entry: bash -c 'echo $PRE_COMMIT_LOCAL_BRANCH | grep /main && make build || echo 0'
|
|
18
|
+
language: system
|
|
19
|
+
pass_filenames: false
|
|
20
|
+
verbose: true
|
|
21
|
+
require_serial: true
|
|
22
|
+
stages: [pre-push]
|
|
23
|
+
|
|
24
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
25
|
+
### Ruff version.
|
|
26
|
+
rev: v0.14.5
|
|
27
|
+
hooks:
|
|
28
|
+
### Run the linter.
|
|
29
|
+
- id: ruff
|
|
30
|
+
args: [--fix, --unsafe-fixes]
|
|
31
|
+
stages: [pre-commit]
|
|
32
|
+
### Run the formatter.
|
|
33
|
+
- id: ruff-format
|
|
34
|
+
types_or: [python, pyi]
|
|
35
|
+
verbose: true
|
|
36
|
+
stages: [pre-commit]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xync-bot
|
|
3
|
+
Version: 0.3.31.dev0
|
|
4
|
+
Summary: Telegram bot with web app for xync net
|
|
5
|
+
Author-email: Artemiev <mixartemev@gmail.com>
|
|
6
|
+
License-Expression: GPL-3.0-or-later
|
|
7
|
+
Project-URL: Homepage, https://gitlab.com/xync/back/tg-bot
|
|
8
|
+
Project-URL: Repository, https://gitlab.com/xync/back/tg-bot
|
|
9
|
+
Keywords: aiogram,cyrtranslit,xync-net
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Requires-Dist: cyrtranslit
|
|
12
|
+
Requires-Dist: xn-auth
|
|
13
|
+
Requires-Dist: xync-schema
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: PGram; extra == "dev"
|
|
16
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest; extra == "dev"
|
|
18
|
+
Requires-Dist: python-dotenv; extra == "dev"
|
|
19
|
+
Requires-Dist: setuptools-scm; extra == "dev"
|
|
20
|
+
Requires-Dist: build; extra == "dev"
|
|
21
|
+
Requires-Dist: twine; extra == "dev"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
include .env
|
|
2
|
+
PACKAGE := xync_bot
|
|
3
|
+
VPYTHON := $(VENV)/bin/python
|
|
4
|
+
|
|
5
|
+
.PHONY: all install pre-commit clean build twine patch
|
|
6
|
+
|
|
7
|
+
all:
|
|
8
|
+
make install clean build
|
|
9
|
+
|
|
10
|
+
install: $(VENV)
|
|
11
|
+
$(VPYTHON) -m pip install .[dev]; make pre-commit
|
|
12
|
+
pre-commit: .pre-commit-config.yaml
|
|
13
|
+
pre-commit install -t pre-commit -t post-commit -t pre-push
|
|
14
|
+
|
|
15
|
+
clean: dist $(PACKAGE).egg-info
|
|
16
|
+
rm -rf dist/* $(PACKAGE).egg-info $(PACKAGE)/__pycache__ dist/__pycache__
|
|
17
|
+
|
|
18
|
+
build: $(VENV)
|
|
19
|
+
$(VPYTHON) -m build; make twine
|
|
20
|
+
twine: dist
|
|
21
|
+
$(VPYTHON) -m twine upload dist/* --skip-existing
|
|
22
|
+
|
|
23
|
+
patch:
|
|
24
|
+
git tag `$(VPYTHON) -m setuptools_scm --strip-dev`; git push --tags --prune -f
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
from aiogram import Bot, Dispatcher, types
|
|
2
|
+
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
|
3
|
+
from aiogram.filters import Command
|
|
4
|
+
import asyncio
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from xync_bot.loader import TOKEN
|
|
8
|
+
|
|
9
|
+
# Пример списка из 200 вариантов (замените на ваши данные)
|
|
10
|
+
OPTIONS = (
|
|
11
|
+
[
|
|
12
|
+
f"Apple {i}"
|
|
13
|
+
for i in range(50) # 50 вариантов на A
|
|
14
|
+
]
|
|
15
|
+
+ [
|
|
16
|
+
"Banana" # 1 вариант на B
|
|
17
|
+
]
|
|
18
|
+
+ [
|
|
19
|
+
f"Dog {i}"
|
|
20
|
+
for i in range(30) # 30 вариантов на D
|
|
21
|
+
]
|
|
22
|
+
+ [
|
|
23
|
+
f"Elephant {i}"
|
|
24
|
+
for i in range(20) # 20 вариантов на E
|
|
25
|
+
]
|
|
26
|
+
+ [
|
|
27
|
+
f"Fish {i}"
|
|
28
|
+
for i in range(40) # 40 вариантов на F
|
|
29
|
+
]
|
|
30
|
+
+ [
|
|
31
|
+
f"Zebra {i}"
|
|
32
|
+
for i in range(59) # 59 вариантов на Z
|
|
33
|
+
]
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SmartAlphaPager:
|
|
38
|
+
def __init__(self, options, items_per_page=15):
|
|
39
|
+
self.options = sorted(options) # Сортируем по алфавиту
|
|
40
|
+
self.items_per_page = items_per_page
|
|
41
|
+
self.pages = self._create_balanced_pages()
|
|
42
|
+
|
|
43
|
+
def _create_balanced_pages(self):
|
|
44
|
+
"""Создаем страницы с примерно равным количеством элементов"""
|
|
45
|
+
pages = []
|
|
46
|
+
current_page = []
|
|
47
|
+
|
|
48
|
+
for option in self.options:
|
|
49
|
+
current_page.append(option)
|
|
50
|
+
|
|
51
|
+
# Если страница заполнена, создаем новую
|
|
52
|
+
if len(current_page) >= self.items_per_page:
|
|
53
|
+
pages.append(current_page)
|
|
54
|
+
current_page = []
|
|
55
|
+
|
|
56
|
+
# Добавляем последнюю страницу, если есть остатки
|
|
57
|
+
if current_page:
|
|
58
|
+
pages.append(current_page)
|
|
59
|
+
|
|
60
|
+
return pages
|
|
61
|
+
|
|
62
|
+
def _get_page_title(self, page_items):
|
|
63
|
+
"""Создает заголовок страницы на основе диапазона букв"""
|
|
64
|
+
if not page_items:
|
|
65
|
+
return "Пустая страница"
|
|
66
|
+
|
|
67
|
+
first_letter = page_items[0][0].upper()
|
|
68
|
+
last_letter = page_items[-1][0].upper()
|
|
69
|
+
|
|
70
|
+
if first_letter == last_letter:
|
|
71
|
+
return f"📋 Буква '{first_letter}'"
|
|
72
|
+
else:
|
|
73
|
+
return f"📋 Буквы '{first_letter}' - '{last_letter}'"
|
|
74
|
+
|
|
75
|
+
def get_overview_menu(self):
|
|
76
|
+
"""Возвращает обзорное меню со всеми страницами"""
|
|
77
|
+
if not self.pages:
|
|
78
|
+
return InlineKeyboardMarkup(inline_keyboard=[])
|
|
79
|
+
|
|
80
|
+
keyboard = InlineKeyboardMarkup(inline_keyboard=[])
|
|
81
|
+
|
|
82
|
+
# Создаем кнопки для каждой страницы
|
|
83
|
+
for i, page_items in enumerate(self.pages):
|
|
84
|
+
first_letter = page_items[0][0].upper()
|
|
85
|
+
last_letter = page_items[-1][0].upper()
|
|
86
|
+
count = len(page_items)
|
|
87
|
+
|
|
88
|
+
if first_letter == last_letter:
|
|
89
|
+
button_text = f"{first_letter} ({count})"
|
|
90
|
+
else:
|
|
91
|
+
button_text = f"{first_letter}-{last_letter} ({count})"
|
|
92
|
+
|
|
93
|
+
# Группируем по 3 кнопки в ряд
|
|
94
|
+
if i % 3 == 0:
|
|
95
|
+
keyboard.inline_keyboard.append([])
|
|
96
|
+
|
|
97
|
+
keyboard.inline_keyboard[-1].append(InlineKeyboardButton(text=button_text, callback_data=f"page_{i}"))
|
|
98
|
+
|
|
99
|
+
return keyboard
|
|
100
|
+
|
|
101
|
+
def get_page_keyboard(self, page_num):
|
|
102
|
+
"""Возвращает клавиатуру для конкретной страницы"""
|
|
103
|
+
if page_num < 0 or page_num >= len(self.pages):
|
|
104
|
+
return None, None
|
|
105
|
+
|
|
106
|
+
page_items = self.pages[page_num]
|
|
107
|
+
title = self._get_page_title(page_items)
|
|
108
|
+
|
|
109
|
+
keyboard = InlineKeyboardMarkup(inline_keyboard=[])
|
|
110
|
+
|
|
111
|
+
# Добавляем кнопки с вариантами
|
|
112
|
+
for option in page_items:
|
|
113
|
+
keyboard.inline_keyboard.append([InlineKeyboardButton(text=option, callback_data=f"select_{option}")])
|
|
114
|
+
|
|
115
|
+
# Навигационные кнопки
|
|
116
|
+
nav_row = []
|
|
117
|
+
|
|
118
|
+
# Кнопка "К обзору"
|
|
119
|
+
nav_row.append(InlineKeyboardButton(text="📖 Обзор", callback_data="back_to_overview"))
|
|
120
|
+
|
|
121
|
+
# Кнопки навигации между страницами
|
|
122
|
+
if len(self.pages) > 1:
|
|
123
|
+
if page_num > 0:
|
|
124
|
+
nav_row.append(InlineKeyboardButton(text="⬅️ Пред", callback_data=f"nav_page_{page_num - 1}"))
|
|
125
|
+
|
|
126
|
+
nav_row.append(
|
|
127
|
+
InlineKeyboardButton(text=f"{page_num + 1}/{len(self.pages)}", callback_data="current_page_info")
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if page_num < len(self.pages) - 1:
|
|
131
|
+
nav_row.append(InlineKeyboardButton(text="След ➡️", callback_data=f"nav_page_{page_num + 1}"))
|
|
132
|
+
|
|
133
|
+
keyboard.inline_keyboard.append(nav_row)
|
|
134
|
+
|
|
135
|
+
# Дополнительная информация в заголовке
|
|
136
|
+
items_count = len(page_items)
|
|
137
|
+
total_items = sum(len(page) for page in self.pages)
|
|
138
|
+
full_title = f"{title}\nСтраница {page_num + 1}/{len(self.pages)} • {items_count} из {total_items} вариантов"
|
|
139
|
+
|
|
140
|
+
return keyboard, full_title
|
|
141
|
+
|
|
142
|
+
def get_stats(self):
|
|
143
|
+
"""Возвращает статистику распределения"""
|
|
144
|
+
stats = {}
|
|
145
|
+
for page_num, page_items in enumerate(self.pages):
|
|
146
|
+
first_letter = page_items[0][0].upper()
|
|
147
|
+
last_letter = page_items[-1][0].upper()
|
|
148
|
+
|
|
149
|
+
if first_letter == last_letter:
|
|
150
|
+
range_text = f"'{first_letter}'"
|
|
151
|
+
else:
|
|
152
|
+
range_text = f"'{first_letter}'-'{last_letter}'"
|
|
153
|
+
|
|
154
|
+
stats[f"Страница {page_num + 1}"] = {"range": range_text, "count": len(page_items)}
|
|
155
|
+
|
|
156
|
+
return stats
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# Инициализация бота
|
|
160
|
+
bot = Bot(token=TOKEN)
|
|
161
|
+
dp = Dispatcher()
|
|
162
|
+
|
|
163
|
+
pager = SmartAlphaPager(OPTIONS)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dp.message(Command("start"))
|
|
167
|
+
async def start_handler(message: types.Message):
|
|
168
|
+
"""Стартовая команда - показываем обзор страниц"""
|
|
169
|
+
keyboard = pager.get_overview_menu()
|
|
170
|
+
total_options = len(pager.options)
|
|
171
|
+
total_pages = len(pager.pages)
|
|
172
|
+
|
|
173
|
+
text = (
|
|
174
|
+
f"🔍 Выберите диапазон для просмотра:\n\n"
|
|
175
|
+
f"📊 Всего вариантов: {total_options}\n"
|
|
176
|
+
f"📄 Всего страниц: {total_pages}"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
await message.answer(text, reply_markup=keyboard)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dp.message(Command("stats"))
|
|
183
|
+
async def stats_handler(message: types.Message):
|
|
184
|
+
"""Показать статистику распределения"""
|
|
185
|
+
stats = pager.get_stats()
|
|
186
|
+
|
|
187
|
+
text = "📊 Статистика распределения по страницам:\n\n"
|
|
188
|
+
for page_name, info in stats.items():
|
|
189
|
+
text += f"{page_name}: {info['range']} — {info['count']} вариантов\n"
|
|
190
|
+
|
|
191
|
+
await message.answer(text)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@dp.callback_query(lambda c: c.data == "back_to_overview")
|
|
195
|
+
async def back_to_overview(callback: types.CallbackQuery):
|
|
196
|
+
"""Возврат к обзору страниц"""
|
|
197
|
+
keyboard = pager.get_overview_menu()
|
|
198
|
+
total_options = len(pager.options)
|
|
199
|
+
total_pages = len(pager.pages)
|
|
200
|
+
|
|
201
|
+
text = (
|
|
202
|
+
f"🔍 Выберите диапазон для просмотра:\n\n"
|
|
203
|
+
f"📊 Всего вариантов: {total_options}\n"
|
|
204
|
+
f"📄 Всего страниц: {total_pages}"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
await callback.message.edit_text(text, reply_markup=keyboard)
|
|
208
|
+
await callback.answer()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@dp.callback_query(lambda c: c.data.startswith("page_"))
|
|
212
|
+
async def show_page(callback: types.CallbackQuery):
|
|
213
|
+
"""Показываем конкретную страницу"""
|
|
214
|
+
page_num = int(callback.data.split("_")[1])
|
|
215
|
+
keyboard, text = pager.get_page_keyboard(page_num)
|
|
216
|
+
|
|
217
|
+
if keyboard:
|
|
218
|
+
await callback.message.edit_text(text, reply_markup=keyboard)
|
|
219
|
+
else:
|
|
220
|
+
await callback.answer("Ошибка: страница не найдена")
|
|
221
|
+
|
|
222
|
+
await callback.answer()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@dp.callback_query(lambda c: c.data.startswith("nav_page_"))
|
|
226
|
+
async def navigate_pages(callback: types.CallbackQuery):
|
|
227
|
+
"""Навигация между страницами"""
|
|
228
|
+
page_num = int(callback.data.split("_")[2])
|
|
229
|
+
keyboard, text = pager.get_page_keyboard(page_num)
|
|
230
|
+
|
|
231
|
+
if keyboard:
|
|
232
|
+
await callback.message.edit_text(text, reply_markup=keyboard)
|
|
233
|
+
else:
|
|
234
|
+
await callback.answer("Ошибка навигации")
|
|
235
|
+
|
|
236
|
+
await callback.answer()
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@dp.callback_query(lambda c: c.data.startswith("select_"))
|
|
240
|
+
async def option_selected(callback: types.CallbackQuery):
|
|
241
|
+
"""Обработка выбора варианта"""
|
|
242
|
+
selected = callback.data.replace("select_", "")
|
|
243
|
+
|
|
244
|
+
await callback.message.edit_text(
|
|
245
|
+
f"✅ Вы выбрали: {selected}\n\n"
|
|
246
|
+
f"Используйте /start для нового выбора\n"
|
|
247
|
+
f"Используйте /stats для просмотра статистики"
|
|
248
|
+
)
|
|
249
|
+
await callback.answer(f"Выбран: {selected}")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@dp.callback_query(lambda c: c.data == "current_page_info")
|
|
253
|
+
async def current_page_info(callback: types.CallbackQuery):
|
|
254
|
+
"""Информация о текущей странице"""
|
|
255
|
+
await callback.answer("Информация о текущей странице")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
async def main():
|
|
259
|
+
"""Запуск бота"""
|
|
260
|
+
logging.basicConfig(level=logging.INFO)
|
|
261
|
+
print("Бот запущен...")
|
|
262
|
+
|
|
263
|
+
# Выводим статистику при запуске
|
|
264
|
+
stats = pager.get_stats()
|
|
265
|
+
print("\n📊 Статистика распределения:")
|
|
266
|
+
for page_name, info in stats.items():
|
|
267
|
+
print(f"{page_name}: {info['range']} — {info['count']} вариантов")
|
|
268
|
+
print()
|
|
269
|
+
|
|
270
|
+
await dp.start_polling(bot)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
if __name__ == "__main__":
|
|
274
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "xync-bot"
|
|
3
|
+
requires-python = ">=3.12"
|
|
4
|
+
authors = [
|
|
5
|
+
{name = "Artemiev", email = "mixartemev@gmail.com"},
|
|
6
|
+
]
|
|
7
|
+
keywords = ["aiogram", "cyrtranslit", "xync-net"]
|
|
8
|
+
description = "Telegram bot with web app for xync net"
|
|
9
|
+
#readme = "README.md"
|
|
10
|
+
license = "GPL-3.0-or-later"
|
|
11
|
+
dynamic = ["version"]
|
|
12
|
+
dependencies = [
|
|
13
|
+
"cyrtranslit",
|
|
14
|
+
"xn-auth",
|
|
15
|
+
"xync-schema",
|
|
16
|
+
]
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
dev = [
|
|
19
|
+
"PGram",
|
|
20
|
+
"pre-commit",
|
|
21
|
+
"pytest",
|
|
22
|
+
"python-dotenv",
|
|
23
|
+
"setuptools-scm",
|
|
24
|
+
"build",
|
|
25
|
+
"twine",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://gitlab.com/xync/back/tg-bot"
|
|
30
|
+
Repository = "https://gitlab.com/xync/back/tg-bot"
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["setuptools>=64", "setuptools-scm[toml]>=8"]
|
|
34
|
+
build-backend = "setuptools.build_meta"
|
|
35
|
+
[tool.setuptools_scm]
|
|
36
|
+
version_scheme = "python-simplified-semver" # if "feature" in `branch_name` SEMVER_MINOR++ else SEMVER_PATCH++
|
|
37
|
+
local_scheme = "no-local-version"
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
asyncio_mode = "auto"
|
|
41
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
42
|
+
|
|
43
|
+
[tool.ruff]
|
|
44
|
+
line-length = 120
|
|
45
|
+
|
|
46
|
+
[tool.setuptools]
|
|
47
|
+
packages = ["xync_bot"]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from datetime import timedelta
|
|
3
|
+
from itertools import groupby
|
|
4
|
+
|
|
5
|
+
from PGram import Bot
|
|
6
|
+
from aiogram.client.default import DefaultBotProperties
|
|
7
|
+
from aiogram.enums import UpdateType
|
|
8
|
+
from aiogram.types import InlineKeyboardButton
|
|
9
|
+
from tortoise.timezone import now
|
|
10
|
+
from xync_schema.enums import HotStatus
|
|
11
|
+
|
|
12
|
+
from xync_bot.shared import BoolCd
|
|
13
|
+
from xync_schema.models import Order, MyAd, User, Hot, CurEx
|
|
14
|
+
|
|
15
|
+
from xync_bot.store import Store
|
|
16
|
+
from xync_bot.routers import last
|
|
17
|
+
from xync_bot.routers.main.handler import mr
|
|
18
|
+
from xync_bot.routers.pay.handler import pr
|
|
19
|
+
from xync_bot.routers.cond import cr
|
|
20
|
+
from xync_bot.routers.send import sd
|
|
21
|
+
from xync_bot.routers.hot import hot
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
au = [
|
|
25
|
+
UpdateType.MESSAGE,
|
|
26
|
+
UpdateType.CALLBACK_QUERY,
|
|
27
|
+
UpdateType.CHAT_MEMBER,
|
|
28
|
+
UpdateType.MY_CHAT_MEMBER,
|
|
29
|
+
] # , UpdateType.CHAT_JOIN_REQUEST
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class XyncBot(Bot):
|
|
33
|
+
def __init__(self, token, cn):
|
|
34
|
+
super().__init__(token, cn, [hot, sd, cr, pr, mr, last], Store(), DefaultBotProperties(parse_mode="HTML"))
|
|
35
|
+
|
|
36
|
+
async def start(self, wh_host: str = None):
|
|
37
|
+
self.dp.workflow_data["xbt"] = self # todo: refact?
|
|
38
|
+
# self.dp.workflow_data["store"].glob = await Store.Global() # todo: refact store loading
|
|
39
|
+
await super().start(au, wh_host)
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
# итерация: предоставляем юзеру кнопки для перехода на hot объявления
|
|
43
|
+
async def go_hot(self, username_id: int, ex_ids: list[int]):
|
|
44
|
+
ex_ids = ex_ids or [4]
|
|
45
|
+
adq = MyAd.hot_mads_query(ex_ids)
|
|
46
|
+
if not (ads := await adq.filter(ad__pair_side__is_sell=False)):
|
|
47
|
+
raise ValueError({1: ex_ids})
|
|
48
|
+
user = await User.get(username_id=username_id)
|
|
49
|
+
hot_db, _ = await Hot.get_or_create(
|
|
50
|
+
{}, user=user, updated_at__gt=now() - timedelta(hours=1), status=HotStatus.opened
|
|
51
|
+
)
|
|
52
|
+
ads = {cur: random.choice(list(g)) for cur, g in groupby(ads, key=lambda x: x.ad.pair_side.pair.cur_id)}
|
|
53
|
+
await hot_db.ads_shared.add(*ads.values())
|
|
54
|
+
btns = [
|
|
55
|
+
[
|
|
56
|
+
InlineKeyboardButton(text=ad.ad.pair_side.pair.cur.ticker + " 🌐", url=ad.get_url()[0]),
|
|
57
|
+
InlineKeyboardButton(text=ad.ad.pair_side.pair.cur.ticker + " 📱", url=ad.get_url()[1]),
|
|
58
|
+
]
|
|
59
|
+
for ad in ads.values()
|
|
60
|
+
]
|
|
61
|
+
await self.send(username_id, "🟥Sell USDT", btns)
|
|
62
|
+
|
|
63
|
+
if balances := await user.balances():
|
|
64
|
+
curexs = await CurEx.filter(cur_id__in=balances.keys(), ex_id__in=ex_ids)
|
|
65
|
+
minimums = {cx.cur_id: cx.minimum for cx in curexs}
|
|
66
|
+
balances = {cur_id: b for cur_id, b in balances.items() if b >= minimums[cur_id]}
|
|
67
|
+
if ads := await adq.filter(
|
|
68
|
+
ad__pair_side__is_sell=True, ad__pair_side__pair__cur_id__in=balances.keys()
|
|
69
|
+
).all():
|
|
70
|
+
ads = {cur: random.choice(list(g)) for cur, g in groupby(ads, key=lambda x: x.ad.pair_side.pair.cur_id)}
|
|
71
|
+
await hot_db.ads_shared.add(*ads.values())
|
|
72
|
+
btns = [
|
|
73
|
+
[
|
|
74
|
+
InlineKeyboardButton(text=rng + ad.ad.pair_side.pair.cur.ticker + " 🌐", url=ad.get_url()[0]),
|
|
75
|
+
InlineKeyboardButton(text=rng + ad.ad.pair_side.pair.cur.ticker + " 📱", url=ad.get_url()[1]),
|
|
76
|
+
]
|
|
77
|
+
for cur_id, ad in ads.items()
|
|
78
|
+
if (rng := f"≤ {balances[cur_id]} ")
|
|
79
|
+
]
|
|
80
|
+
await self.send(username_id, "🟩Buy USDT", btns)
|
|
81
|
+
|
|
82
|
+
async def def_actor(self, uid: int, order: Order):
|
|
83
|
+
"""
|
|
84
|
+
Если актор прилетевшего ордера по прогреву еще не связан ни с одним юзером, спрашиваем hot-юзера он ли это
|
|
85
|
+
"""
|
|
86
|
+
txt = f"{order.taker.name}:{order.taker.exid} is you?"
|
|
87
|
+
btns = [
|
|
88
|
+
[
|
|
89
|
+
InlineKeyboardButton(text="Yes", callback_data=BoolCd(req="is_you", res=True, xtr=order.id).pack()),
|
|
90
|
+
InlineKeyboardButton(text="No", callback_data=BoolCd(req="is_you", res=False, xtr=order.id).pack()),
|
|
91
|
+
]
|
|
92
|
+
]
|
|
93
|
+
await self.send(uid, txt, btns)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from asyncio import run
|
|
3
|
+
|
|
4
|
+
from x_model import init_db
|
|
5
|
+
|
|
6
|
+
from xync_bot import XyncBot
|
|
7
|
+
from xync_bot.loader import TOKEN
|
|
8
|
+
|
|
9
|
+
if __name__ == "__main__":
|
|
10
|
+
from xync_bot.loader import TORM
|
|
11
|
+
|
|
12
|
+
logging.basicConfig(level=logging.INFO)
|
|
13
|
+
|
|
14
|
+
async def main() -> None:
|
|
15
|
+
cn = await init_db(TORM)
|
|
16
|
+
await XyncBot(TOKEN, cn).start()
|
|
17
|
+
|
|
18
|
+
run(main())
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from dotenv import load_dotenv
|
|
2
|
+
from os import getenv as env
|
|
3
|
+
|
|
4
|
+
from xync_schema import models
|
|
5
|
+
|
|
6
|
+
load_dotenv()
|
|
7
|
+
|
|
8
|
+
PG_DSN = (
|
|
9
|
+
f"postgres://{env('POSTGRES_USER')}:{env('POSTGRES_PASSWORD')}@{env('POSTGRES_HOST', 'dbs')}"
|
|
10
|
+
f":{env('POSTGRES_PORT', 5432)}/{env('POSTGRES_DB', env('POSTGRES_USER'))}"
|
|
11
|
+
)
|
|
12
|
+
TOKEN = env("TOKEN")
|
|
13
|
+
API_URL = "https://" + env("API_DOMAIN")
|
|
14
|
+
WH_URL = API_URL + "/wh/" + TOKEN
|
|
15
|
+
|
|
16
|
+
TORM = {
|
|
17
|
+
"connections": {"default": PG_DSN},
|
|
18
|
+
"apps": {"models": {"models": [models, "aerich.models"]}},
|
|
19
|
+
"use_tz": False,
|
|
20
|
+
"timezone": "UTC",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
glob = object
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from aiogram import Router, F
|
|
4
|
+
from aiogram.enums import ContentType
|
|
5
|
+
from aiogram.exceptions import TelegramBadRequest
|
|
6
|
+
from aiogram.types import Message
|
|
7
|
+
|
|
8
|
+
last = Router(name="last")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@last.message(
|
|
12
|
+
F.content_type.not_in(
|
|
13
|
+
{
|
|
14
|
+
ContentType.NEW_CHAT_MEMBERS,
|
|
15
|
+
# ContentType.LEFT_CHAT_MEMBER,
|
|
16
|
+
# ContentType.SUPERGROUP_CHAT_CREATED,
|
|
17
|
+
# ContentType.NEW_CHAT_PHOTO,
|
|
18
|
+
# ContentType.FORUM_TOPIC_CREATED,
|
|
19
|
+
# ContentType.FORUM_TOPIC_EDITED,
|
|
20
|
+
ContentType.FORUM_TOPIC_CLOSED,
|
|
21
|
+
# ContentType.GENERAL_FORUM_TOPIC_HIDDEN, # deletable
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
async def del_cbq(msg: Message):
|
|
26
|
+
try:
|
|
27
|
+
await msg.delete()
|
|
28
|
+
logging.info({"DELETED": msg.model_dump(exclude_none=True)})
|
|
29
|
+
except TelegramBadRequest:
|
|
30
|
+
logging.error({"NOT_DELETED": msg.model_dump(exclude_none=True)})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@last.message()
|
|
34
|
+
async def all_rest(msg: Message):
|
|
35
|
+
logging.warning(
|
|
36
|
+
{
|
|
37
|
+
"NO_HANDLED": msg.model_dump(
|
|
38
|
+
exclude_none=True,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
)
|