xync-bot 0.3.7.dev1__py3-none-any.whl → 0.3.24__py3-none-any.whl

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.

@@ -1,12 +1,10 @@
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 (
9
- User as TgUser,
7
+ User,
10
8
  ChatMemberUpdated,
11
9
  Message,
12
10
  InlineKeyboardMarkup,
@@ -15,12 +13,12 @@ from aiogram.types import (
15
13
  WebAppInfo,
16
14
  )
17
15
  from aiogram.utils.deep_linking import create_start_link
18
- from xync_schema.models import User, Order, Msg, Forum
16
+ from xync_schema import models
19
17
 
20
- from xync_bot.routers import user_upsert
21
18
  from xync_bot.shared import NavCallbackData
19
+ from xync_bot.store import Store
22
20
 
23
- main = Router()
21
+ mr = Router(name="main")
24
22
 
25
23
 
26
24
  class RrCallbackData(CallbackData, prefix="reg_res"): # registration response
@@ -31,6 +29,7 @@ class RrCallbackData(CallbackData, prefix="reg_res"): # registration response
31
29
  home_btns = InlineKeyboardMarkup(
32
30
  inline_keyboard=[
33
31
  [
32
+ InlineKeyboardButton(text="Transfer", callback_data=NavCallbackData(to="transfer").pack()),
34
33
  InlineKeyboardButton(text="Invite", callback_data=NavCallbackData(to="ref_link").pack()),
35
34
  InlineKeyboardButton(text="Get VPN", callback_data=NavCallbackData(to="get_vpn").pack()),
36
35
  ]
@@ -38,19 +37,19 @@ home_btns = InlineKeyboardMarkup(
38
37
  )
39
38
 
40
39
 
41
- @main.message(CommandStart(deep_link=True, deep_link_encoded=True))
40
+ @mr.message(CommandStart(deep_link=True, deep_link_encoded=True))
42
41
  async def start_handler(msg: Message, command: CommandObject):
43
- me: TgUser = msg.from_user
42
+ me: User = msg.from_user
44
43
  ref_id: int = command.args.isnumeric() and int(command.args)
45
- user = await User.get_or_none(id=me.id, blocked=False)
44
+ user = await models.User.get_or_none(id=me.id, blocked=False)
46
45
  rm = None
47
46
  logging.info(msg, {"src": "start"})
48
47
  if user:
49
48
  rs, rm = f"{me.full_name}, you have registered already😉", home_btns
50
- elif not (ref := await User.get_or_none(id=ref_id)):
49
+ elif not (ref := await models.User.get_or_none(id=ref_id)):
51
50
  rs = f"No registered user #{ref_id}😬"
52
51
  else: # new user created
53
- user, cr = await user_upsert(me, False)
52
+ user, cr = await models.User.tg2in(me, False)
54
53
  await user.update_from_dict({"ref": ref}).save()
55
54
  approve_btns = InlineKeyboardMarkup(
56
55
  inline_keyboard=[
@@ -67,36 +66,20 @@ async def start_handler(msg: Message, command: CommandObject):
67
66
  return await msg.answer(rs, reply_markup=rm)
68
67
 
69
68
 
70
- @main.callback_query(RrCallbackData.filter())
71
- async def phrases_input_request(cb: CallbackQuery, callback_data: RrCallbackData) -> None:
72
- protege = await User[callback_data.to]
73
- if callback_data.res:
74
- # protege.status = UserStatus.RESTRICTED
75
- await protege.save()
76
- rs = f"{cb.from_user.full_name}, теперь Вы несете ответветвенность за {protege.username}"
77
- else:
78
- rs = f'Вы отклонили запрос юзера "{protege.username}" на Вашу протекцию'
79
- res = {True: "одобрил", False: "отклонил"}
80
- txt = f"{cb.from_user.full_name} {res[callback_data.res]} вашу регистрацию"
81
- txt, rm = (f"Поздравляем! {txt}💥", home_btns) if callback_data.res else (f"К сожалению {txt}😢", None)
82
- await cb.bot.send_message(protege.id, txt, reply_markup=rm)
83
- await cb.answer("👌🏼")
84
- await cb.message.edit_text(rs)
85
-
86
-
87
- @main.message(CommandStart(deep_link=True)) # attempt to reg by fake link
69
+ @mr.message(CommandStart(deep_link=True)) # attempt to reg by fake link
88
70
  async def fraud_handler(msg: Message):
89
71
  logging.warning(f"Start: {msg.from_user.id}. Msg: {msg}")
90
72
  # todo: alert to admins! Fraud attempt!
91
73
  await msg.answer("🤔")
92
74
 
93
75
 
94
- @main.message(CommandStart())
95
- async def start_no_ref_handler(msg: Message):
76
+ @mr.message(CommandStart()) # обычный /start
77
+ async def home(msg: Message, store: Store):
96
78
  me = msg.from_user
97
- user, cr = await user_upsert(me, False)
79
+ user, is_new = await models.User.tg_upsert(me, False)
80
+
98
81
  rr = "сначала вы должны найти поручителя, и перейти по его реферальной ссылке.\nhttps://telegra.ph/XyncNet-02-13"
99
- if cr: # has ref and created now
82
+ if is_new: # has ref and created now
100
83
  await msg.answer(f"Здравствуйте {me.full_name}, что бы использовать возможности нашей сети, {rr}")
101
84
  elif not user.ref_id:
102
85
  await msg.answer(rr.capitalize())
@@ -104,10 +87,27 @@ async def start_no_ref_handler(msg: Message):
104
87
  await msg.answer(f"{me.full_name}, не балуйтесь, вы и так уже активный участник👌🏼", reply_markup=home_btns)
105
88
 
106
89
 
107
- @main.callback_query(NavCallbackData.filter(F.to == "ref_link"))
90
+ @mr.callback_query(RrCallbackData.filter())
91
+ async def phrases_input_request(cb: CallbackQuery, callback_data: RrCallbackData) -> None:
92
+ protege = await models.User[callback_data.to]
93
+ if callback_data.res:
94
+ # protege.status = UserStatus.RESTRICTED
95
+ await protege.save()
96
+ rs = f"{cb.from_user.full_name}, теперь Вы несете ответветвенность за {protege.username}"
97
+ else:
98
+ rs = f'Вы отклонили запрос юзера "{protege.username}" на Вашу протекцию'
99
+ res = {True: "одобрил", False: "отклонил"}
100
+ txt = f"{cb.from_user.full_name} {res[callback_data.res]} вашу регистрацию"
101
+ txt, rm = (f"Поздравляем! {txt}💥", home_btns) if callback_data.res else (f"К сожалению {txt}😢", None)
102
+ await cb.bot.send_message(protege.id, txt, reply_markup=rm)
103
+ await cb.answer("👌🏼")
104
+ await cb.message.edit_text(rs)
105
+
106
+
107
+ @mr.callback_query(NavCallbackData.filter(F.to.__eq__("ref_link")))
108
108
  async def ref_link_handler(cbq: CallbackQuery):
109
109
  me = cbq.from_user
110
- if not (u := await User.get_or_none(id=me.id, blocked=False).prefetch_related("ref")):
110
+ if not (u := await models.User.get_or_none(id=me.id, blocked=False).prefetch_related("ref")):
111
111
  return await cbq.answer(f"{me.full_name}, сначала сами получите одобрение поручителя😉")
112
112
  link = await create_start_link(cbq.bot, str(u.id), encode=True)
113
113
  logging.info(f"Start: {me.id}. Msg: {cbq}")
@@ -119,36 +119,36 @@ async def ref_link_handler(cbq: CallbackQuery):
119
119
  await cbq.answer("Wait for your protege request..")
120
120
 
121
121
 
122
- @main.my_chat_member(F.chat.type == "private") # my_chat_member is fired on add bot to any chat. filter for preventing
122
+ @mr.my_chat_member(F.chat.type == "private") # my_chat_member is fired on add bot to any chat. filter for preventing
123
123
  async def my_user_set_status(my_chat_member: ChatMemberUpdated):
124
124
  logging.info({"my_chat_member": my_chat_member.model_dump(exclude_none=True)})
125
- u: TgUser = my_chat_member.from_user
125
+ u: User = my_chat_member.from_user
126
126
  blocked = my_chat_member.new_chat_member.status in ("left", "kicked")
127
- await user_upsert(u, blocked)
127
+ await models.User.tg2in(u, blocked)
128
128
 
129
129
 
130
- @main.my_chat_member()
130
+ @mr.my_chat_member()
131
131
  async def user_set_status(my_chat_member: ChatMemberUpdated):
132
132
  if my_chat_member.new_chat_member.user.username == "XyncNetBot": # удалена группа где бот был добавлен админом
133
- if forum := await Forum.get_or_none(id=my_chat_member.chat.id):
133
+ if forum := await models.Forum.get_or_none(id=my_chat_member.chat.id):
134
134
  await forum.delete()
135
135
  res = f"I {my_chat_member.new_chat_member.status} from {my_chat_member.chat.id}:{my_chat_member.chat.title}"
136
136
  return logging.info(res)
137
137
  logging.info({"my_chat_member": my_chat_member.model_dump(exclude_none=True)})
138
- u: TgUser = my_chat_member.from_user
138
+ u: User = my_chat_member.from_user
139
139
  blocked = my_chat_member.new_chat_member.status in ("left", "kicked")
140
140
  if blocked:
141
- if forum := await Forum.get_or_none(id=my_chat_member.chat.id, user_id=u.id):
141
+ if forum := await models.Forum.get_or_none(id=my_chat_member.chat.id, user_id=u.id):
142
142
  if forum.joined:
143
143
  forum.joined = False
144
144
  await forum.save()
145
- await user_upsert(u, blocked)
145
+ return await models.User.tg2in(u, blocked)
146
146
 
147
147
 
148
- @main.chat_member(ChatMemberUpdatedFilter(LEAVE_TRANSITION)) # юзер покинул группу Ордеров
148
+ @mr.chat_member(ChatMemberUpdatedFilter(LEAVE_TRANSITION)) # юзер покинул группу Ордеров
149
149
  async def on_user_leave(member: ChatMemberUpdated):
150
150
  logging.info({"user_leave": member.model_dump(exclude_none=True)})
151
- if forum := await Forum[member.chat.id]:
151
+ if forum := await models.Forum[member.chat.id]:
152
152
  if forum.joined:
153
153
  forum.joined = False
154
154
  await forum.save()
@@ -156,10 +156,10 @@ async def on_user_leave(member: ChatMemberUpdated):
156
156
  return await member.bot.send_message(member.new_chat_member.user.id, resp)
157
157
 
158
158
 
159
- @main.chat_member(ChatMemberUpdatedFilter(JOIN_TRANSITION)) # Юзер добавился в группу Ордеров
159
+ @mr.chat_member(ChatMemberUpdatedFilter(JOIN_TRANSITION)) # Юзер добавился в группу Ордеров
160
160
  async def on_user_join(member: ChatMemberUpdated, app_url: str):
161
161
  logging.info({"user_join": member.model_dump(exclude_none=True)})
162
- if forum := await Forum.get_or_none(id=member.chat.id):
162
+ if forum := await models.Forum.get_or_none(id=member.chat.id):
163
163
  if not forum.joined:
164
164
  forum.joined = True
165
165
  await forum.save()
@@ -170,53 +170,20 @@ async def on_user_join(member: ChatMemberUpdated, app_url: str):
170
170
  return await member.bot.send_message(member.new_chat_member.user.id, resp, reply_markup=rm)
171
171
 
172
172
 
173
- @main.message(F.is_topic_message)
173
+ @mr.message(F.is_topic_message)
174
174
  async def order_msg(msg: Message):
175
- sender = await User[msg.from_user.id]
175
+ sender = await models.User[msg.from_user.id]
176
176
  cid = msg.chat.shifted_id
177
177
  assert sender.forum == cid, "sender is not client"
178
- if order := await Order.get_or_none(taker__user_id=sender.id, taker_topic=msg.message_thread_id):
178
+ if order := await models.Order.get_or_none(taker__user_id=sender.id, taker_topic=msg.message_thread_id):
179
179
  is_taker = True
180
- elif order := await Order.get_or_none(ad__agent__user_id=sender.id, maker_topic=msg.message_thread_id):
180
+ elif order := await models.Order.get_or_none(ad__agent__user_id=sender.id, maker_topic=msg.message_thread_id):
181
181
  is_taker = False
182
182
  else:
183
183
  return await msg.answer("No such order")
184
184
  # raise Exception("No such order")
185
- receiver: User = await (order.ad.maker.user if is_taker else order.taker.user)
185
+ receiver: models.User = await (order.ad.maker.user if is_taker else order.taker.user)
186
186
  rcv_topic = order.taker_topic if is_taker else order.maker_topic
187
- await Msg.create(tgid=msg.message_id, txt=msg.text, order_id=order.id, receiver=receiver)
187
+ await models.Msg.create(tgid=msg.message_id, txt=msg.text, order_id=order.id, receiver=receiver)
188
188
  logging.info(msg, {"src": "order_msg"})
189
189
  return await msg.send_copy(receiver.forum, message_thread_id=rcv_topic)
190
-
191
-
192
- @main.message(
193
- F.content_type.not_in(
194
- {
195
- ContentType.NEW_CHAT_MEMBERS,
196
- # ContentType.LEFT_CHAT_MEMBER,
197
- # ContentType.SUPERGROUP_CHAT_CREATED,
198
- # ContentType.NEW_CHAT_PHOTO,
199
- # ContentType.FORUM_TOPIC_CREATED,
200
- # ContentType.FORUM_TOPIC_EDITED,
201
- ContentType.FORUM_TOPIC_CLOSED,
202
- # ContentType.GENERAL_FORUM_TOPIC_HIDDEN, # deletable
203
- }
204
- )
205
- )
206
- async def del_cbq(msg: Message):
207
- try:
208
- await msg.delete()
209
- logging.info({"DELETED": msg.model_dump(exclude_none=True)})
210
- except TelegramBadRequest:
211
- logging.error({"NOT_DELETED": msg.model_dump(exclude_none=True)})
212
-
213
-
214
- @main.message()
215
- async def all_rest(msg: Message):
216
- logging.warning(
217
- {
218
- "NO_HANDLED": msg.model_dump(
219
- exclude_none=True,
220
- )
221
- }
222
- )
@@ -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}"