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.
Files changed (44) hide show
  1. xync_bot-0.3.31.dev0/.env.dist +4 -0
  2. xync_bot-0.3.31.dev0/.gitignore +8 -0
  3. xync_bot-0.3.31.dev0/.pre-commit-config.yaml +36 -0
  4. xync_bot-0.3.31.dev0/PKG-INFO +21 -0
  5. xync_bot-0.3.31.dev0/makefile +24 -0
  6. xync_bot-0.3.31.dev0/pager.py +274 -0
  7. xync_bot-0.3.31.dev0/pyproject.toml +47 -0
  8. xync_bot-0.3.31.dev0/test_main.http +9 -0
  9. xync_bot-0.3.31.dev0/xync_bot/__init__.py +93 -0
  10. xync_bot-0.3.31.dev0/xync_bot/__main__.py +18 -0
  11. xync_bot-0.3.31.dev0/xync_bot/loader.py +23 -0
  12. xync_bot-0.3.31.dev0/xync_bot/routers/__init__.py +41 -0
  13. xync_bot-0.3.31.dev0/xync_bot/routers/cond/__init__.py +91 -0
  14. xync_bot-0.3.31.dev0/xync_bot/routers/cond/func.py +118 -0
  15. xync_bot-0.3.31.dev0/xync_bot/routers/hot/__init__.py +77 -0
  16. xync_bot-0.3.31.dev0/xync_bot/routers/main/__init__.py +0 -0
  17. xync_bot-0.3.31.dev0/xync_bot/routers/main/handler.py +191 -0
  18. xync_bot-0.3.31.dev0/xync_bot/routers/order.py +12 -0
  19. xync_bot-0.3.31.dev0/xync_bot/routers/pay/cd.py +49 -0
  20. xync_bot-0.3.31.dev0/xync_bot/routers/pay/dep.py +119 -0
  21. xync_bot-0.3.31.dev0/xync_bot/routers/pay/handler.py +256 -0
  22. xync_bot-0.3.31.dev0/xync_bot/routers/pay/window.py +249 -0
  23. xync_bot-0.3.31.dev0/xync_bot/routers/photo.py +36 -0
  24. xync_bot-0.3.31.dev0/xync_bot/routers/send/__init__.py +117 -0
  25. xync_bot-0.3.31.dev0/xync_bot/routers/xicon.png +0 -0
  26. xync_bot-0.3.31.dev0/xync_bot/shared.py +30 -0
  27. xync_bot-0.3.31.dev0/xync_bot/store.py +151 -0
  28. xync_bot-0.3.31.dev0/xync_bot/typs.py +0 -0
  29. xync_bot-0.3.31.dev0/xync_bot.egg-info/PKG-INFO +21 -0
  30. xync_bot-0.3.31.dev0/xync_bot.egg-info/SOURCES.txt +33 -0
  31. xync_bot-0.3.31.dev0/xync_bot.egg-info/requires.txt +12 -0
  32. xync_bot-0.2.9/PKG-INFO +0 -15
  33. xync_bot-0.2.9/pyproject.toml +0 -24
  34. xync_bot-0.2.9/xync_bot/__init__.py +0 -39
  35. xync_bot-0.2.9/xync_bot/handlers/__init__.py +0 -2
  36. xync_bot-0.2.9/xync_bot/handlers/main.py +0 -117
  37. xync_bot-0.2.9/xync_bot/shared.py +0 -5
  38. xync_bot-0.2.9/xync_bot.egg-info/PKG-INFO +0 -15
  39. xync_bot-0.2.9/xync_bot.egg-info/SOURCES.txt +0 -11
  40. xync_bot-0.2.9/xync_bot.egg-info/requires.txt +0 -4
  41. {xync_bot-0.2.9 → xync_bot-0.3.31.dev0}/setup.cfg +0 -0
  42. {xync_bot-0.2.9/xync_bot/handlers → xync_bot-0.3.31.dev0/xync_bot/routers}/vpn.py +0 -0
  43. {xync_bot-0.2.9 → xync_bot-0.3.31.dev0}/xync_bot.egg-info/dependency_links.txt +0 -0
  44. {xync_bot-0.2.9 → xync_bot-0.3.31.dev0}/xync_bot.egg-info/top_level.txt +0 -0
@@ -0,0 +1,4 @@
1
+ WH_DOMAIN=https://
2
+ TWA_DOMAIN=https://
3
+ TOKEN=
4
+ DB_URL=postgres://user:pwd@host/db_name
@@ -0,0 +1,8 @@
1
+ /.idea
2
+ /venv
3
+ /.env
4
+ __pycache__
5
+ *.pyc
6
+ *.egg-info
7
+ /build
8
+ /dist
@@ -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,9 @@
1
+ # Test your FastAPI endpoints
2
+
3
+ GET http://127.0.0.1:8000/
4
+ Accept: application/json
5
+ ###
6
+
7
+ GET http://127.0.0.1:8000/hello/User
8
+ Accept: application/json
9
+ ###
@@ -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
+ )