xync-bot 0.3.7.dev4__tar.gz → 0.3.24__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.

Potentially problematic release.


This version of xync-bot might be problematic. Click here for more details.

Files changed (34) hide show
  1. {xync_bot-0.3.7.dev4/xync_bot.egg-info → xync_bot-0.3.24}/PKG-INFO +1 -1
  2. xync_bot-0.3.24/xync_bot/__main__.py +36 -0
  3. {xync_bot-0.3.7.dev4 → xync_bot-0.3.24}/xync_bot/loader.py +2 -0
  4. xync_bot-0.3.24/xync_bot/routers/__init__.py +41 -0
  5. {xync_bot-0.3.7.dev4 → xync_bot-0.3.24}/xync_bot/routers/cond/__init__.py +1 -7
  6. xync_bot-0.3.7.dev4/xync_bot/routers/main.py → xync_bot-0.3.24/xync_bot/routers/main/handler.py +25 -57
  7. xync_bot-0.3.24/xync_bot/routers/pay/cd.py +49 -0
  8. xync_bot-0.3.24/xync_bot/routers/pay/dep.py +270 -0
  9. xync_bot-0.3.24/xync_bot/routers/pay/handler.py +247 -0
  10. xync_bot-0.3.24/xync_bot/routers/pay/window.py +244 -0
  11. xync_bot-0.3.24/xync_bot/shared.py +24 -0
  12. xync_bot-0.3.24/xync_bot/store.py +150 -0
  13. {xync_bot-0.3.7.dev4 → xync_bot-0.3.24/xync_bot.egg-info}/PKG-INFO +1 -1
  14. {xync_bot-0.3.7.dev4 → xync_bot-0.3.24}/xync_bot.egg-info/SOURCES.txt +8 -3
  15. xync_bot-0.3.7.dev4/xync_bot/__init__.py +0 -23
  16. xync_bot-0.3.7.dev4/xync_bot/routers/pay/main.py +0 -133
  17. xync_bot-0.3.7.dev4/xync_bot/shared.py +0 -5
  18. {xync_bot-0.3.7.dev4 → xync_bot-0.3.24}/.env.dist +0 -0
  19. {xync_bot-0.3.7.dev4 → xync_bot-0.3.24}/.gitignore +0 -0
  20. {xync_bot-0.3.7.dev4 → xync_bot-0.3.24}/.pre-commit-config.yaml +0 -0
  21. {xync_bot-0.3.7.dev4 → xync_bot-0.3.24}/makefile +0 -0
  22. {xync_bot-0.3.7.dev4 → xync_bot-0.3.24}/pyproject.toml +0 -0
  23. {xync_bot-0.3.7.dev4 → xync_bot-0.3.24}/setup.cfg +0 -0
  24. {xync_bot-0.3.7.dev4 → xync_bot-0.3.24}/test_main.http +0 -0
  25. {xync_bot-0.3.7.dev4 → xync_bot-0.3.24}/xync_bot/routers/cond/func.py +0 -0
  26. {xync_bot-0.3.7.dev4/xync_bot/routers → xync_bot-0.3.24/xync_bot/routers/main}/__init__.py +0 -0
  27. {xync_bot-0.3.7.dev4 → xync_bot-0.3.24}/xync_bot/routers/order.py +0 -0
  28. {xync_bot-0.3.7.dev4 → xync_bot-0.3.24}/xync_bot/routers/photo.py +0 -0
  29. {xync_bot-0.3.7.dev4 → xync_bot-0.3.24}/xync_bot/routers/vpn.py +0 -0
  30. {xync_bot-0.3.7.dev4 → xync_bot-0.3.24}/xync_bot/routers/xicon.png +0 -0
  31. {xync_bot-0.3.7.dev4 → xync_bot-0.3.24}/xync_bot/typs.py +0 -0
  32. {xync_bot-0.3.7.dev4 → xync_bot-0.3.24}/xync_bot.egg-info/dependency_links.txt +0 -0
  33. {xync_bot-0.3.7.dev4 → xync_bot-0.3.24}/xync_bot.egg-info/requires.txt +0 -0
  34. {xync_bot-0.3.7.dev4 → xync_bot-0.3.24}/xync_bot.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xync-bot
3
- Version: 0.3.7.dev4
3
+ Version: 0.3.24
4
4
  Summary: Telegram bot with web app for xync net
5
5
  Author-email: Artemiev <mixartemev@gmail.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -0,0 +1,36 @@
1
+ import logging
2
+ from asyncio import run
3
+
4
+ from PGram import Bot
5
+ from aiogram.client.default import DefaultBotProperties
6
+ from aiogram.enums import UpdateType
7
+ from x_model import init_db
8
+
9
+ from xync_bot.routers.main.handler import mr
10
+ from xync_bot.routers.cond import cr
11
+ from xync_bot.routers.pay.handler import pr
12
+ from xync_bot.routers import last
13
+ from xync_bot.routers.pay.dep import Store
14
+
15
+ au = [
16
+ UpdateType.MESSAGE,
17
+ UpdateType.CALLBACK_QUERY,
18
+ UpdateType.CHAT_MEMBER,
19
+ UpdateType.MY_CHAT_MEMBER,
20
+ ] # , UpdateType.CHAT_JOIN_REQUEST
21
+ bot = Bot([cr, pr, mr, last], Store(), au, default=DefaultBotProperties(parse_mode="HTML"))
22
+
23
+ if __name__ == "__main__":
24
+ from xync_bot.loader import TOKEN, TORM
25
+
26
+ logging.basicConfig(level=logging.INFO)
27
+
28
+ async def main() -> None:
29
+ cn = await init_db(TORM)
30
+ bot.dp.workflow_data["store"].glob = await Store.Global() # todo: refact store loading
31
+ await bot.start(
32
+ TOKEN,
33
+ cn,
34
+ )
35
+
36
+ run(main())
@@ -19,3 +19,5 @@ TORM = {
19
19
  "use_tz": False,
20
20
  "timezone": "UTC",
21
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
+ )
@@ -8,7 +8,7 @@ from xync_schema.enums import SynonymType
8
8
 
9
9
  from xync_bot.routers.cond.func import wrap_cond, get_val, btns, rkm, ikm, SynTypeCd, CondCd
10
10
 
11
- cr = Router()
11
+ cr = Router(name="cond")
12
12
 
13
13
 
14
14
  @cr.message(Command("cond"))
@@ -89,9 +89,3 @@ async def got_action(cbq: CallbackQuery, callback_data: CondCd, state: FSMContex
89
89
  await (await state.get_value("cmsg")).delete()
90
90
  await show_cond(cbq.message, cond)
91
91
  return await cbq.answer(callback_data.act)
92
-
93
-
94
- @cr.message()
95
- async def unknown(msg: Message):
96
- # user = await User.get(username_id=msg.from_user.id)
97
- await msg.delete()
@@ -1,8 +1,6 @@
1
1
  import logging
2
2
 
3
3
  from aiogram import Router, F
4
- from aiogram.enums import ContentType
5
- from aiogram.exceptions import TelegramBadRequest
6
4
  from aiogram.filters import CommandStart, CommandObject, ChatMemberUpdatedFilter, JOIN_TRANSITION, LEAVE_TRANSITION
7
5
  from aiogram.filters.callback_data import CallbackData
8
6
  from aiogram.types import (
@@ -18,8 +16,9 @@ from aiogram.utils.deep_linking import create_start_link
18
16
  from xync_schema import models
19
17
 
20
18
  from xync_bot.shared import NavCallbackData
19
+ from xync_bot.store import Store
21
20
 
22
- mr = Router()
21
+ mr = Router(name="main")
23
22
 
24
23
 
25
24
  class RrCallbackData(CallbackData, prefix="reg_res"): # registration response
@@ -30,6 +29,7 @@ class RrCallbackData(CallbackData, prefix="reg_res"): # registration response
30
29
  home_btns = InlineKeyboardMarkup(
31
30
  inline_keyboard=[
32
31
  [
32
+ InlineKeyboardButton(text="Transfer", callback_data=NavCallbackData(to="transfer").pack()),
33
33
  InlineKeyboardButton(text="Invite", callback_data=NavCallbackData(to="ref_link").pack()),
34
34
  InlineKeyboardButton(text="Get VPN", callback_data=NavCallbackData(to="get_vpn").pack()),
35
35
  ]
@@ -66,6 +66,27 @@ async def start_handler(msg: Message, command: CommandObject):
66
66
  return await msg.answer(rs, reply_markup=rm)
67
67
 
68
68
 
69
+ @mr.message(CommandStart(deep_link=True)) # attempt to reg by fake link
70
+ async def fraud_handler(msg: Message):
71
+ logging.warning(f"Start: {msg.from_user.id}. Msg: {msg}")
72
+ # todo: alert to admins! Fraud attempt!
73
+ await msg.answer("🤔")
74
+
75
+
76
+ @mr.message(CommandStart()) # обычный /start
77
+ async def home(msg: Message, store: Store):
78
+ me = msg.from_user
79
+ user, is_new = await models.User.tg_upsert(me, False)
80
+
81
+ rr = "сначала вы должны найти поручителя, и перейти по его реферальной ссылке.\nhttps://telegra.ph/XyncNet-02-13"
82
+ if is_new: # has ref and created now
83
+ await msg.answer(f"Здравствуйте {me.full_name}, что бы использовать возможности нашей сети, {rr}")
84
+ elif not user.ref_id:
85
+ await msg.answer(rr.capitalize())
86
+ else:
87
+ await msg.answer(f"{me.full_name}, не балуйтесь, вы и так уже активный участник👌🏼", reply_markup=home_btns)
88
+
89
+
69
90
  @mr.callback_query(RrCallbackData.filter())
70
91
  async def phrases_input_request(cb: CallbackQuery, callback_data: RrCallbackData) -> None:
71
92
  protege = await models.User[callback_data.to]
@@ -83,27 +104,7 @@ async def phrases_input_request(cb: CallbackQuery, callback_data: RrCallbackData
83
104
  await cb.message.edit_text(rs)
84
105
 
85
106
 
86
- @mr.message(CommandStart(deep_link=True)) # attempt to reg by fake link
87
- async def fraud_handler(msg: Message):
88
- logging.warning(f"Start: {msg.from_user.id}. Msg: {msg}")
89
- # todo: alert to admins! Fraud attempt!
90
- await msg.answer("🤔")
91
-
92
-
93
- @mr.message(CommandStart())
94
- async def start_no_ref_handler(msg: Message):
95
- me = msg.from_user
96
- user, cr = await models.User.tg2in(me, False)
97
- rr = "сначала вы должны найти поручителя, и перейти по его реферальной ссылке.\nhttps://telegra.ph/XyncNet-02-13"
98
- if cr: # has ref and created now
99
- await msg.answer(f"Здравствуйте {me.full_name}, что бы использовать возможности нашей сети, {rr}")
100
- elif not user.ref_id:
101
- await msg.answer(rr.capitalize())
102
- else:
103
- await msg.answer(f"{me.full_name}, не балуйтесь, вы и так уже активный участник👌🏼", reply_markup=home_btns)
104
-
105
-
106
- @mr.callback_query(NavCallbackData.filter(F.to == "ref_link"))
107
+ @mr.callback_query(NavCallbackData.filter(F.to.__eq__("ref_link")))
107
108
  async def ref_link_handler(cbq: CallbackQuery):
108
109
  me = cbq.from_user
109
110
  if not (u := await models.User.get_or_none(id=me.id, blocked=False).prefetch_related("ref")):
@@ -186,36 +187,3 @@ async def order_msg(msg: Message):
186
187
  await models.Msg.create(tgid=msg.message_id, txt=msg.text, order_id=order.id, receiver=receiver)
187
188
  logging.info(msg, {"src": "order_msg"})
188
189
  return await msg.send_copy(receiver.forum, message_thread_id=rcv_topic)
189
-
190
-
191
- @mr.message(
192
- F.content_type.not_in(
193
- {
194
- ContentType.NEW_CHAT_MEMBERS,
195
- # ContentType.LEFT_CHAT_MEMBER,
196
- # ContentType.SUPERGROUP_CHAT_CREATED,
197
- # ContentType.NEW_CHAT_PHOTO,
198
- # ContentType.FORUM_TOPIC_CREATED,
199
- # ContentType.FORUM_TOPIC_EDITED,
200
- ContentType.FORUM_TOPIC_CLOSED,
201
- # ContentType.GENERAL_FORUM_TOPIC_HIDDEN, # deletable
202
- }
203
- )
204
- )
205
- async def del_cbq(msg: Message):
206
- try:
207
- await msg.delete()
208
- logging.info({"DELETED": msg.model_dump(exclude_none=True)})
209
- except TelegramBadRequest:
210
- logging.error({"NOT_DELETED": msg.model_dump(exclude_none=True)})
211
-
212
-
213
- @mr.message()
214
- async def all_rest(msg: Message):
215
- logging.warning(
216
- {
217
- "NO_HANDLED": msg.model_dump(
218
- exclude_none=True,
219
- )
220
- }
221
- )
@@ -0,0 +1,49 @@
1
+ from aiogram.filters.callback_data import CallbackData
2
+
3
+ from xync_bot.routers.pay.dep import PayStep, ActionType
4
+
5
+
6
+ class MoneyType(CallbackData, prefix="target"):
7
+ is_fiat: int # bool
8
+ is_target: int # bool
9
+
10
+
11
+ class Cur(CallbackData, prefix="cur"):
12
+ id: int
13
+ is_target: int # bool
14
+
15
+
16
+ class Coin(CallbackData, prefix="coin"):
17
+ id: int
18
+ is_target: int # bool
19
+
20
+
21
+ class Cred(CallbackData, prefix="cred"):
22
+ id: int
23
+
24
+
25
+ class Ex(CallbackData, prefix="ex"):
26
+ id: int
27
+ is_target: int # bool
28
+
29
+
30
+ class Pm(CallbackData, prefix="pm"):
31
+ pmcur_id: int
32
+ is_target: bool
33
+
34
+
35
+ class Ppo(CallbackData, prefix="ppo"):
36
+ num: int
37
+ is_target: int # bool
38
+
39
+
40
+ class PayNav(CallbackData, prefix="pay_nav"):
41
+ to: PayStep
42
+
43
+
44
+ class Time(CallbackData, prefix="time"):
45
+ minutes: int # время в минутах
46
+
47
+
48
+ class Action(CallbackData, prefix="action"):
49
+ act: ActionType # "received" или "not_received"
@@ -0,0 +1,270 @@
1
+ import logging
2
+ from asyncio import gather
3
+ from enum import IntEnum
4
+
5
+ from aiogram.exceptions import TelegramBadRequest
6
+ from aiogram.fsm.state import StatesGroup, State
7
+ from aiogram.types import Message, InlineKeyboardMarkup
8
+ from pyrogram.types import CallbackQuery
9
+ from tortoise.functions import Min
10
+ from x_auth.enums import Role
11
+ from x_model.func import ArrayAgg
12
+ from xync_schema import models
13
+
14
+ from xync_bot.shared import flags
15
+
16
+
17
+ class Report(StatesGroup):
18
+ text = State()
19
+
20
+
21
+ class CredState(StatesGroup):
22
+ detail = State()
23
+ name = State()
24
+
25
+
26
+ class PaymentState(StatesGroup):
27
+ amount = State()
28
+ timer = State()
29
+ timer_active = State()
30
+
31
+
32
+ class ActionType(IntEnum):
33
+ """Цель (назначение) платежа (target)"""
34
+
35
+ sent = 1 # Отправил
36
+ received = 2 # Получил
37
+ not_received = 3 # Не получил
38
+
39
+
40
+ class PayStep(IntEnum):
41
+ """Цель (назначение) платежа (target)"""
42
+
43
+ t_type = 1 # Выбор типа
44
+ t_cur = 2 # Выбор валюты
45
+ t_coin = 3 # Выбор монеты
46
+ t_pm = 4 # Выбор платежки
47
+ t_ex = 5 # Выбор биржи
48
+ t_cred_dtl = 6 # Ввод номера карты
49
+ t_cred_name = 7 # Ввод имени
50
+ # t_addr = 8 # todo: позже добавим: Выбор/ввод крипто кошелька
51
+ t_amount = 9 # Ввод суммы
52
+ """ Источник платежа (source) """
53
+ s_type = 10 # Выбор типа
54
+ s_cur = 11 # Выбор типа
55
+ s_pm = 12 # Выбор типа
56
+ s_coin = 13 # Выбор типа
57
+ s_ex = 14 # Выбор типа
58
+ ppo = 15 # Выбор возможности разбивки платежа
59
+ urgency = 16 # Выбор срочности получения платежа
60
+ pending_send = 17 # Ожидание отправки (если мы платим фиатом)
61
+ pending_confirm = 18 # Ожидание пока на той стороне подтвердят получение нашего фиата (если мы платим фиатом)
62
+ pending_receive = 19 # Ожидание поступления (если мы получаем фиат)
63
+
64
+
65
+ class SingleStore(type):
66
+ _store = None
67
+
68
+ async def __call__(cls):
69
+ if not cls._store:
70
+ cls._store = super(SingleStore, cls).__call__()
71
+ cls._store.coins = {k: v for k, v in await models.Coin.all().order_by("ticker").values_list("id", "ticker")}
72
+ curs = {c.id: c for c in await models.Cur.filter(ticker__in=flags.keys()).order_by("ticker")}
73
+ cls._store.curs = curs
74
+ cls._store.exs = {k: v for k, v in await models.Ex.all().values_list("id", "name")}
75
+ cls._store.pmcurs = {
76
+ k: v
77
+ for k, v in await models.Pmex.filter(pm__pmcurs__cur_id__in=cls._store.curs.keys())
78
+ .annotate(sname=Min("name"))
79
+ .group_by("pm__pmcurs__id")
80
+ .values_list("pm__pmcurs__id", "sname")
81
+ }
82
+ cls._store.coinexs = {
83
+ c.id: [ex.ex_id for ex in c.coinexs] for c in await models.Coin.all().prefetch_related("coinexs")
84
+ }
85
+ cls._store.curpms = {
86
+ cur_id: ids
87
+ for cur_id, ids in await models.Pmcur.filter(cur_id__in=curs.keys())
88
+ .annotate(ids=ArrayAgg("id"))
89
+ .group_by("cur_id")
90
+ .values_list("cur_id", "ids")
91
+ }
92
+ cls._store.curpms = {
93
+ cur_id: ids
94
+ for cur_id, ids in await models.Pmcur.filter(cur_id__in=curs.keys())
95
+ .annotate(ids=ArrayAgg("id"))
96
+ .group_by("cur_id")
97
+ .values_list("cur_id", "ids")
98
+ }
99
+
100
+ return cls._store
101
+
102
+
103
+ class Store:
104
+ class Global(metaclass=SingleStore):
105
+ coins: dict[int, str] # id:ticker
106
+ curs: dict[int, models.Cur] # id:Cur
107
+ exs: dict[int, str] # id:name
108
+ coinexs: dict[int, list[int]] # id:[ex_ids]
109
+ pmcurs: dict[int, str] # pmcur_id:name
110
+ curpms: dict[int, list[int]] # id:[pmcur_ids]
111
+
112
+ class Permanent:
113
+ msg_id: int = None
114
+ user: models.User = None
115
+ actors: dict[int, int] = None # key=ex_id
116
+ creds: dict[int, models.Cred] = None # key=cred_id
117
+ cur_creds: dict[int, list[int]] = None # pmcur_id:[cred_ids]
118
+
119
+ class Current:
120
+ is_target: bool = True
121
+ is_fiat: bool = None
122
+ msg_to_del: Message = None
123
+
124
+ class Payment:
125
+ t_cur_id: int = None
126
+ s_cur_id: int = None
127
+ t_coin_id: int = None
128
+ s_coin_id: int = None
129
+ t_pmcur_id: int = None
130
+ s_pmcur_id: int = None
131
+ t_ex_id: int = None
132
+ s_ex_id: int = None
133
+ amount: int | float = None
134
+ ppo: int = 1
135
+ addr_id: int = None
136
+ cred_dtl: str = None
137
+ cred_id: int = None
138
+ urg: int = 5
139
+ pr_id: int = None
140
+
141
+ glob: Global
142
+ perm: Permanent = Permanent()
143
+ pay: Payment = Payment()
144
+ curr: Current = Current()
145
+
146
+ async def xync_have_coin_amount(self) -> bool:
147
+ assets = await models.Asset.filter(
148
+ addr__coin_id=self.pay.t_coin_id, addr__ex_id=self.pay.t_ex_id, addr__actor__user__role__in=Role.ADMIN
149
+ )
150
+ return self.pay.amount <= sum(a.free for a in assets)
151
+
152
+ async def client_have_coin_amount(self) -> bool:
153
+ assets = await models.Asset.filter(
154
+ addr__coin_id=self.pay.t_coin_id, addr__actor_id__in=self.perm.actors.values()
155
+ )
156
+ return self.pay.amount <= sum(a.free for a in assets)
157
+
158
+ async def need_ppo(self):
159
+ cur_id = getattr(self.pay, ("t" if self.curr.is_target else "s") + "_cur_id")
160
+ usd_amount = self.pay.amount * self.glob.curs[cur_id].rate
161
+ if usd_amount < 50:
162
+ return 0
163
+ elif usd_amount > 100:
164
+ return 2
165
+ else:
166
+ return 1
167
+
168
+ async def client_target_repr(self) -> tuple[models.Addr | models.Cred, str]:
169
+ if self.pay.t_ex_id:
170
+ addr_to = (
171
+ await models.Addr.filter(
172
+ actor__ex_id=self.pay.t_ex_id, coin_id=self.pay.t_coin_id, actor__user=self.perm.user
173
+ )
174
+ .prefetch_related("actor")
175
+ .first()
176
+ )
177
+ ex, coin = self.glob.exs[self.pay.s_ex_id], self.glob.coins[self.pay.s_coin_id]
178
+ if not addr_to:
179
+ logging.error(f"No {coin} addr in {ex} for user: {self.perm.user.username_id}")
180
+ return addr_to, f"{coin} на {ex} по id: `{addr_to.actor.exid}`"
181
+ # иначе: реквизиты для фиата
182
+ cur, pm = self.glob.curs[self.pay.t_cur_id], self.glob.pmcurs[self.pay.t_pmcur_id]
183
+ cred = self.perm.creds[self.pay.cred_id]
184
+ return cred, f"{cur.ticker} на {pm} по номеру: {cred.repr()}"
185
+
186
+ async def get_merch_target(self) -> tuple[models.Addr | models.Cred, str]:
187
+ if self.pay.s_ex_id:
188
+ addr_in = (
189
+ await models.Addr.filter(
190
+ actor__ex_id=self.pay.s_ex_id, coin_id=self.pay.s_coin_id, actor__user__role__gte=Role.ADMIN
191
+ )
192
+ .prefetch_related("actor")
193
+ .first()
194
+ )
195
+ ex, coin = self.glob.exs[self.pay.s_ex_id], self.glob.coins[self.pay.s_coin_id]
196
+ if not addr_in:
197
+ logging.error(f"No {coin} addr in {ex}")
198
+ return addr_in, f"{coin} на {ex} по id: `{addr_in.actor.exid}`"
199
+ # иначе: реквизиты для фиатной оплаты
200
+ s_pmcur = await models.Pmcur.get(id=self.pay.s_pmcur_id).prefetch_related("pm__grp")
201
+ cred = await models.Cred.filter(
202
+ **({"pmcur__pm__grp": s_pmcur.pm.grp} if s_pmcur.pm.grp else {"pmcur_id": self.pay.s_pmcur_id}),
203
+ person__user__role__gte=Role.ADMIN,
204
+ ).first() # todo: order by fiat.target-fiat.amount
205
+ cur, pm = self.glob.curs[self.pay.s_cur_id], self.glob.pmcurs[self.pay.s_pmcur_id]
206
+ if not cred:
207
+ logging.error(f"No {cur.ticker} cred for {pm}")
208
+ return cred, f"{cur.ticker} на {pm} по номеру: {cred.repr()}"
209
+
210
+
211
+ async def fill_creds(person_id: int) -> tuple[dict[int, models.Cred], dict[int, list[int]]]:
212
+ cq = models.Cred.filter(person_id=person_id)
213
+ creds = {c.id: c for c in await cq}
214
+ cur_creds = {
215
+ pci: ids
216
+ for pci, ids in await cq.annotate(ids=ArrayAgg("id")).group_by("pmcur_id").values_list("pmcur_id", "ids")
217
+ }
218
+ return creds, cur_creds
219
+
220
+
221
+ async def fill_actors(person_id: int) -> dict[int, int]:
222
+ ex_actors = {
223
+ # todo: check len(ids) == 1
224
+ exi: ids[0]
225
+ for exi, ids in await models.Actor.filter(person_id=person_id)
226
+ .annotate(ids=ArrayAgg("id"))
227
+ .group_by("ex_id")
228
+ .values_list("ex_id", "ids")
229
+ }
230
+ return ex_actors
231
+
232
+
233
+ async def edit(msg: Message, txt: str, rm: InlineKeyboardMarkup):
234
+ await gather(msg.edit_text(txt), msg.edit_reply_markup(reply_markup=rm))
235
+
236
+
237
+ async def ans(cbq: CallbackQuery, txt: str = None):
238
+ await cbq.answer(txt, cache_time=0)
239
+
240
+
241
+ async def dlt(msg: Message):
242
+ await msg.delete()
243
+
244
+
245
+ async def edt(msg: Message, txt: str, rm: InlineKeyboardMarkup):
246
+ if msg.message_id == msg.bot.store.perm.msg_id:
247
+ await msg.edit_text(txt, reply_markup=rm)
248
+ else: # окно вызвано в ответ на текст, а не кнопку
249
+ try:
250
+ await msg.bot.edit_message_text(
251
+ txt, chat_id=msg.chat.id, message_id=msg.bot.store.perm.msg_id, reply_markup=rm
252
+ )
253
+ except TelegramBadRequest as e:
254
+ print(msg.bot.store.perm.msg_id, e)
255
+
256
+
257
+ def fmt_sec(sec: int):
258
+ days = sec // (24 * 3600)
259
+ sec %= 24 * 3600
260
+ hours = sec // 3600
261
+ sec %= 3600
262
+ minutes = sec // 60
263
+ sec %= 60
264
+
265
+ if days > 0:
266
+ return f"{days}д {hours:02d}:{minutes:02d}:{sec:02d}"
267
+ elif hours > 0:
268
+ return f"{hours:02d}:{minutes:02d}:{sec:02d}"
269
+ else:
270
+ return f"{minutes:02d}:{sec:02d}"