xync-client 0.0.144__py3-none-any.whl → 0.0.147__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-client might be problematic. Click here for more details.

@@ -24,13 +24,14 @@ from tortoise.signals import post_save
24
24
  from urllib3.exceptions import ReadTimeoutError
25
25
  from x_model import init_db
26
26
  from x_model.func import ArrayAgg
27
+ from xync_bot import XyncBot
27
28
  from xync_schema import models
28
29
  from xync_schema.enums import OrderStatus
29
30
 
30
- from xync_schema.models import Actor, Cond, CondSim, PmCur, PairSide
31
+ from xync_schema.models import Actor, Cond, CondSim, PmCur, PairSide, Agent
31
32
 
32
33
  from xync_client.Abc.Agent import BaseAgentClient
33
- from xync_client.Abc.xtype import BaseOrderReq, FlatDict
34
+ from xync_client.Abc.xtype import FlatDict, BaseOrderReq
34
35
  from xync_client.Bybit.etype.ad import AdPostRequest, AdUpdateRequest, Ad, AdStatus
35
36
  from xync_client.Bybit.etype.cred import CredEpyd
36
37
  from xync_client.Bybit.etype.order import (
@@ -42,8 +43,10 @@ from xync_client.Bybit.etype.order import (
42
43
  OrderFull,
43
44
  Message,
44
45
  Status,
46
+ OrderSellRequest,
47
+ TakeAdReq,
45
48
  )
46
- from xync_client.loader import TORM, NET_TOKEN
49
+ from xync_client.loader import TORM, NET_TOKEN, PAY_TOKEN
47
50
 
48
51
 
49
52
  class NoMakerException(Exception):
@@ -87,9 +90,9 @@ class AgentClient(BaseAgentClient): # Bybit client
87
90
  rcond_sims: dict[int, set[int]] = defaultdict(set) # backward
88
91
  tree: dict = {}
89
92
 
90
- def __init__(self, actor: Actor, bot: FileClient, **kwargs):
91
- super().__init__(actor, bot, **kwargs)
92
- self.api = P2P(testnet=False, api_key=actor.agent.auth["key"], api_secret=actor.agent.auth["sec"])
93
+ def __init__(self, agent: Agent, fbot: FileClient, bbot: XyncBot, **kwargs):
94
+ super().__init__(agent, fbot, bbot, **kwargs)
95
+ self.api = P2P(testnet=False, api_key=agent.auth["key"], api_secret=agent.auth["sec"])
93
96
  self.hist: dict = None
94
97
  self.completed_orders: list[int] = None
95
98
 
@@ -272,20 +275,39 @@ class AgentClient(BaseAgentClient): # Bybit client
272
275
  security_risk_token = data["result"]["securityRiskToken"]
273
276
  return security_risk_token
274
277
 
275
- def _check_2fa(self, risk_token):
276
- # 2fa code
277
- bybit_secret = self.agent.auth["2fa"]
278
- totp = pyotp.TOTP(bybit_secret)
279
- totp_code = totp.now()
280
-
281
- res = self._post(
282
- "/user/public/risk/verify", {"risk_token": risk_token, "component_list": {"google2fa": totp_code}}
278
+ async def _check_2fa(self, risk_token) -> int:
279
+ cres = await self._post("/user/public/risk/components", {"risk_token": risk_token})
280
+ if cres["ret_msg"] != "success":
281
+ raise HTTPException("get")
282
+ cres = cres["result"]["component_list"]
283
+ res = await self._post(
284
+ "/user/public/risk/verify",
285
+ {
286
+ "risk_token": risk_token,
287
+ "component_list": {c["component_id"]: self.__get_2fa(c["component_id"]) for c in cres},
288
+ },
283
289
  )
284
290
  if res["ret_msg"] != "success":
285
- print("Wrong 2fa, wait 5 secs and retry..")
286
- sleep(5)
287
- self._check_2fa(risk_token)
288
- return res
291
+ logging.error("Wrong 2fa, wait 5 secs and retry..")
292
+ await sleep(5)
293
+ await self._check_2fa(risk_token)
294
+ return res["ret_code"]
295
+
296
+ async def __get_2fa(self, typ: Literal["google2fa", "email_verify", "payment_password_verify"], rt: str = None):
297
+ if typ == "google2fa":
298
+ bybit_secret = self.agent.auth["2fa"]
299
+ totp = pyotp.TOTP(bybit_secret)
300
+ return totp.now()
301
+ elif typ == "email_verify":
302
+ res = await self._post("/user/public/risk/send/code", {"risk_token": rt, "component_id": "email_verify"})
303
+ if res["ret_msg"] != "success":
304
+ return self.gmail.bybit_code()
305
+ elif cool_down := int(res["result"]["cool_down"]):
306
+ await sleep(cool_down)
307
+ return self.gmail.bybit_code()
308
+ elif typ == "payment_password_verify":
309
+ return self.agent.auth["pass"]
310
+ raise Exception("2fa fail")
289
311
 
290
312
  def _post_ad(self, risk_token: str):
291
313
  self.create_ad_body.update({"securityRiskToken": risk_token})
@@ -339,27 +361,56 @@ class AgentClient(BaseAgentClient): # Bybit client
339
361
  data = self.api.remove_ad(itemId=ad_id)
340
362
  return data
341
363
 
342
- async def order_request(self, br: BaseOrderReq) -> OrderResp:
343
- res0 = await self._post("/fiat/otc/item/simple", data={"item_id": str(br.ad_id)})
344
- if res0["ret_code"] == 0:
345
- res0 = res0["result"]
346
- res0 = PreOrderResp.model_validate(res0)
364
+ async def __preorder_request(self, ad_id: int) -> PreOrderResp:
365
+ res = await self._post("/fiat/otc/item/simple", data={"item_id": str(ad_id)})
366
+ if res["ret_code"] == 0:
367
+ res = res["result"]
368
+ return PreOrderResp.model_validate(res)
369
+
370
+ async def __order_request_build(self, por: PreOrderResp, br: BaseOrderReq) -> OrderRequest:
347
371
  req = OrderRequest(
348
- itemId=br.ad_id,
372
+ itemId=por.id,
349
373
  tokenId=br.coin_exid,
350
374
  currencyId=br.cur_exid,
351
- side=str(OrderRequest.Side(int(br.is_sell))),
352
- amount=str(br.fiat_amount or br.asset_amount * float(res0.price)),
353
- curPrice=res0.curPrice,
354
- quantity=str(br.asset_amount or round(br.fiat_amount / float(res0.price), br.coin_scale)),
355
- flag="amount" if br.amount_is_fiat else "quantity",
375
+ side="1" if br.is_sell else "0",
376
+ amount=str(br.fiat_amount),
377
+ curPrice=por.curPrice,
378
+ quantity=str(round(br.fiat_amount / float(por.price), br.coin_scale)),
379
+ flag="amount",
380
+ # paymentType="51",
381
+ # paymentId="20399134",
382
+ # online="0"
356
383
  )
384
+ if br.is_sell:
385
+ credex = await models.CredEx.get(
386
+ cred__person_id=self.actor.person_id,
387
+ pmcur__pm__pmexs__exid=por.payments[0],
388
+ pmcur__pm__pmexs__ex_id=self.ex_client.ex.id,
389
+ pmcur__cur_id=br.cur_exid,
390
+ )
391
+ req = OrderSellRequest(**req.model_dump(), paymentType=por.payments[0], paymentId=str(credex.exid))
392
+ return req
393
+
394
+ async def _order_request(self, bor: BaseOrderReq) -> OrderResp:
395
+ por: PreOrderResp = await self.__preorder_request(bor.ad_id)
396
+ req: OrderRequest | OrderSellRequest = await self.__order_request_build(por, bor)
357
397
  # вот непосредственно сам запрос на ордер
358
- res = await self._post("/fiat/otc/order/create", data=req.model_dump())
398
+ return await self.__order_create(req, bor)
399
+
400
+ async def __order_create(self, req: OrderRequest | OrderSellRequest, bor: BaseOrderReq) -> OrderResp:
401
+ res: dict = await self._post("/fiat/otc/order/create", data=req.model_dump())
359
402
  if res["ret_code"] == 0:
360
- return OrderResp.model_validate(res["result"])
403
+ resp = OrderResp.model_validate(res["result"])
361
404
  elif res["ret_code"] == 912120030 or res["ret_msg"] == "The price has changed, please try again later.":
362
- return await self.order_request(br)
405
+ resp = await self._order_request(bor)
406
+ if not resp.orderId and resp.needSecurityRisk:
407
+ if rc := await self._check_2fa(resp.securityRiskToken):
408
+ await self.bbot.send(self.actor.person.user.username_id, f"Bybit 2fa: {rc}")
409
+ raise Exception(f"Bybit 2fa: {rc}")
410
+ # еще раз уже с токеном
411
+ req.securityRiskToken = resp.securityRiskToken
412
+ resp = await self.__order_create(req, bor)
413
+ return resp
363
414
 
364
415
  async def cancel_order(self, order_id: str) -> bool:
365
416
  cr = CancelOrderReq(orderId=order_id)
@@ -545,7 +596,7 @@ class AgentClient(BaseAgentClient): # Bybit client
545
596
  to = ((odb.payed_at or odb.created_at) + timedelta(minutes=180 + 30)).isoformat(sep=" ").split("+")[0]
546
597
  tsa = [
547
598
  t
548
- for tid, t in self.hist.items()
599
+ for tid, t in (self.hist.items() if self.hist else [])
549
600
  if (ecredex.accountNo == t["to"] and t["from"] != "@merchant" and frm < t["date"] < to)
550
601
  ]
551
602
  buyer_person = (
@@ -1184,6 +1235,20 @@ class AgentClient(BaseAgentClient): # Bybit client
1184
1235
 
1185
1236
  self.tree = tree
1186
1237
 
1238
+ async def take_ad(self, req: TakeAdReq):
1239
+ ad: Ad = Ad.model_validate(self.api.get_ad_details(itemId=req.ad_id))
1240
+ bor = BaseOrderReq(
1241
+ ad_id=str(ad.id),
1242
+ fiat_amount=req.amount,
1243
+ is_sell=bool(ad.side),
1244
+ cur_exid=ad.currencyId,
1245
+ coin_exid=ad.tokenId,
1246
+ coin_scale=ad.token.scale,
1247
+ pm_id=req.pm_id,
1248
+ )
1249
+ resp: OrderResp = await self._order_request(bor)
1250
+ return resp
1251
+
1187
1252
 
1188
1253
  def ms2utc(msk_ts_str: str):
1189
1254
  return datetime.fromtimestamp(int(msk_ts_str) / 1000, timezone(timedelta(hours=3), name="MSK"))
@@ -1248,7 +1313,7 @@ class ExcCode(IntEnum):
1248
1313
 
1249
1314
  async def main():
1250
1315
  logging.basicConfig(level=logging.INFO)
1251
- _ = await init_db(TORM)
1316
+ cn = await init_db(TORM)
1252
1317
 
1253
1318
  @post_save(models.Race)
1254
1319
  async def race_upserted(
@@ -1261,18 +1326,20 @@ async def main():
1261
1326
  else: # параметры гонки изменены
1262
1327
  ...
1263
1328
 
1264
- actor = (
1265
- await models.Actor.filter(ex_id=4, agent__isnull=False).prefetch_related("ex", "agent", "person__user").first()
1329
+ agent = (
1330
+ await models.Agent.filter(actor__ex_id=4, auth__isnull=False, active=True)
1331
+ .prefetch_related("actor__ex", "actor__person__user__gmail")
1332
+ .first()
1266
1333
  )
1267
1334
  filebot = FileClient(NET_TOKEN)
1268
1335
  await filebot.start()
1269
1336
  # b.add_handler(MessageHandler(cond_start_handler, command("cond")))
1270
- cl: AgentClient = actor.client(filebot)
1337
+ cl: AgentClient = agent.client(filebot, XyncBot(PAY_TOKEN, cn))
1271
1338
 
1272
1339
  # await cl.ex_client.set_pairs()
1273
1340
  # await cl.ex_client.set_pms()
1274
- # await cl.set_creds()
1275
- # await cl.export_my_ads()
1341
+ await cl.set_creds()
1342
+ await cl.export_my_ads()
1276
1343
 
1277
1344
  # создание гонок по мои активным объявам:
1278
1345
  # for ma in cl.my_ads():
@@ -1320,7 +1387,7 @@ async def main():
1320
1387
  # )
1321
1388
  # await cl.get_api_orders() # 43, 1741294800000, 1749157199999)
1322
1389
 
1323
- races = await models.Race.filter(started=True).prefetch_related(
1390
+ races = await models.Race.filter(started=True, road__ad__maker_id=agent.actor_id).prefetch_related(
1324
1391
  "road__ad__pair_side__pair__cur",
1325
1392
  "road__ad__pms",
1326
1393
  )
@@ -1332,20 +1399,10 @@ async def main():
1332
1399
  # cl.get_api_orders(), # 10, 1738357200000, 1742504399999
1333
1400
  )
1334
1401
  except Exception as e:
1335
- await filebot.send("🤬Bybit agent CRASHED!!!🤬", actor.person.user.username_id)
1336
- await filebot.send(e.__repr__(), actor.person.user.username_id)
1402
+ await filebot.send("🤬Bybit agent CRASHED!!!🤬", agent.actor.person.user.username_id)
1403
+ await filebot.send(e.__repr__(), agent.actor.person.user.username_id)
1337
1404
  raise e
1338
- # bor = BaseOrderReq(
1339
- # ad_id="1861440060199632896",
1340
- # # asset_amount=40,
1341
- # fiat_amount=3000,
1342
- # amount_is_fiat=True,
1343
- # is_sell=False,
1344
- # cur_exid=rub.exid,
1345
- # coin_exid=usdt.exid,
1346
- # coin_scale=usdt.coin.scale,
1347
- # )
1348
- # res: OrderResp = await cl.order_request(bor)
1405
+
1349
1406
  # await cl.cancel_order(res.orderId)
1350
1407
  await filebot.stop()
1351
1408
  await cl.close()
@@ -1,4 +1,3 @@
1
- from datetime import datetime
2
1
  from enum import IntEnum
3
2
  from typing import Literal
4
3
 
@@ -45,6 +44,12 @@ class StatusApi(IntEnum):
45
44
  waiting_for_objection = 110
46
45
 
47
46
 
47
+ class TakeAdReq(BaseModel):
48
+ ad_id: int | str
49
+ amount: float
50
+ pm_id: int = None
51
+
52
+
48
53
  class OrderRequest(BaseModel):
49
54
  class Side(IntEnum):
50
55
  BUY = 0
@@ -60,33 +65,46 @@ class OrderRequest(BaseModel):
60
65
  flag: Literal["amount", "quantity"]
61
66
  version: str = "1.0"
62
67
  securityRiskToken: str = ""
68
+ isFromAi: bool = False
69
+
70
+
71
+ class OrderSellRequest(OrderRequest):
72
+ paymentId: str
73
+ paymentType: str
63
74
 
64
75
 
65
76
  class PreOrderResp(BaseModel):
66
- price: str # float
67
- curPrice: str
68
- totalAmount: float
69
- minAmount: float
70
- maxAmount: float
71
- minQuantity: float
72
- maxQuantity: float
77
+ id: str # bigint
78
+ price: str # float .cur.scale
79
+ lastQuantity: str # float .coin.scale
80
+ curPrice: str # hex 32
81
+ lastPrice: str # float .cur.scale # future
82
+ isOnline: bool
83
+ lastLogoutTime: str # timestamp(0)+0
73
84
  payments: list[str] # list[int]
74
85
  status: Literal[10, 20]
75
- paymentTerms: list
76
- paymentPeriod: Literal[15]
77
- lastQuantity: float
78
- lastPrice: float
79
- isOnline: bool
80
- lastLogoutTime: datetime
81
- itemPriceAvailableTime: datetime
82
- itemPriceValidTime: int # 45000
86
+ paymentTerms: list # empty
87
+ paymentPeriod: Literal[15, 30, 60]
88
+ totalAmount: str # float .cur.scale
89
+ minAmount: str # float .cur.scale
90
+ maxAmount: str # float .cur.scale
91
+ minQuantity: str # float .coin.scale
92
+ maxQuantity: str # float .coin.scale
93
+ itemPriceAvailableTime: str # timestamp(0)+0
94
+ itemPriceValidTime: Literal["45000"]
83
95
  itemType: Literal["ORIGIN"]
96
+ shareItem: bool # False
84
97
 
85
98
 
86
99
  class OrderResp(BaseModel):
87
100
  orderId: str
88
101
  isNeedConfirm: bool
102
+ confirmId: str = ""
89
103
  success: bool
104
+ securityRiskToken: str = ""
105
+ riskTokenType: Literal["challenge"] = None
106
+ riskVersion: Literal["1", "2"] = None
107
+ needSecurityRisk: bool
90
108
  isBulkOrder: bool
91
109
  confirmed: str = None
92
110
  delayTime: str
@@ -1,113 +1,134 @@
1
- from playwright.async_api import Page, Playwright
2
- from pyro_client.client.file import FileClient
3
- from pyro_client.client.user import UserClient
1
+ import logging
2
+ import pickle
3
+ import re
4
+ from base64 import urlsafe_b64decode
5
+ from datetime import datetime
6
+
7
+ from google.auth.transport.requests import Request
8
+ from google_auth_oauthlib.flow import InstalledAppFlow
9
+ from googleapiclient.discovery import Resource, build
10
+ from requests import get
4
11
  from xync_schema.models import User, Gmail
5
12
 
6
- from xync_client.loader import NET_TOKEN
13
+ from xync_client.Abc.HasAbotUid import HasAbotUid
14
+ from xync_client.loader import TORM
15
+
16
+ # Область доступа
17
+ SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"]
18
+
19
+
20
+ class GmClient(HasAbotUid):
21
+ service: Resource
22
+
23
+ def __init__(self, user: User):
24
+ """Авторизация и создание сервиса Gmail API"""
25
+ creds = None
26
+ # Файл token.pickle хранит токены доступа пользователя
27
+ if user.gmail.token:
28
+ creds = pickle.loads(user.gmail.token)
29
+
30
+ # Если нет валидных credentials, запрашиваем авторизацию
31
+ if not creds or not creds.valid:
32
+ if creds and creds.expired and creds.refresh_token:
33
+ creds.refresh(Request())
34
+ else:
35
+ flow = InstalledAppFlow.from_client_config(user.gmail.auth, SCOPES)
36
+ creds = flow.run_local_server(port=0)
7
37
 
38
+ # Сохраняем credentials для следующего запуска
39
+ user.gmail.token = pickle.dumps(creds)
8
40
 
9
- class GmClient:
10
- uid: int
11
- user: User
12
- page: Page
13
- bot: UserClient = None
14
- HOME = "https://mail.google.com/mail/u/0/"
41
+ self.service = build("gmail", "v1", credentials=creds)
42
+ self.uid = user.username_id
15
43
 
16
- def __init__(self, uid: int):
17
- self.uid = uid
44
+ def _get_last_email(self, sender_email, subject_keyword=None):
45
+ """
46
+ Получить последнее письмо от определенного отправителя
18
47
 
19
- async def start(self, pw: Playwright, headed: bool = False):
20
- self.user = await User.get(username_id=self.uid).prefetch_related("gmail")
21
- if not self.user.gmail:
22
- self.user.gmail = await Gmail.create(
23
- login=input(f"{self.user.last_name} gmail login:"), password=input("password:")
48
+ Args:
49
+ sender_email: email отправителя (например, 'example@gmail.com')
50
+ subject_keyword: ключевое слово в теме (опционально)
51
+ """
52
+
53
+ def _get_email_body(payload):
54
+ """Извлечь текст письма из payload"""
55
+ if "body" in payload and "data" in payload["body"]:
56
+ return urlsafe_b64decode(payload["body"]["data"]).decode("utf-8")
57
+ return ""
58
+
59
+ # Формируем поисковый запрос
60
+ query = f"from:{sender_email}"
61
+ if subject_keyword:
62
+ query += f" subject:{subject_keyword}"
63
+
64
+ # Ищем письма с этим запросом
65
+ results = (
66
+ self.service.users()
67
+ .messages()
68
+ .list(
69
+ userId="me",
70
+ q=query,
71
+ maxResults=1, # Только последнее письмо
24
72
  )
25
- browser = await pw.chromium.launch(
26
- channel="chrome" if headed else "chromium-headless-shell",
27
- headless=not headed,
28
- args=[
29
- "--disable-blink-features=AutomationControlled",
30
- "--no-sandbox",
31
- "--disable-web-security",
32
- "--disable-infobars",
33
- "--disable-extensions",
34
- "--start-maximized",
35
- ],
73
+ .execute()
36
74
  )
37
- context = await browser.new_context(storage_state=self.user.gmail.auth, locale="en")
38
- self.page = await context.new_page()
39
- # go home
40
- await self.page.goto(self.HOME, timeout=62000)
41
- if not self.page.url.startswith(self.HOME):
42
- if await ( # ваще с 0 заходим
43
- sgn_btn := self.page.locator(
44
- 'header a[href^="https://accounts.google.com/AccountChooser/signinchooser"]:visible',
45
- has_text="sign",
46
- )
47
- ).count():
48
- await sgn_btn.click()
49
- if self.page.url.startswith("https://accounts.google.com/v3/signin/accountchooser"): # надо выбрать акк
50
- await self.page.locator("li").first.click()
51
- # если предлагает залогиниться
52
- elif await self.page.locator("h1#headingText", has_text="Sign In").count():
53
- await self.page.fill("input[type=email]", self.user.gmail.login)
54
- await self.page.locator("button", has_text="Next").click()
55
- # осталось ввести пороль:
56
- await self.page.fill("input[type=password]", self.user.gmail.password)
57
- await self.page.locator("#passwordNext").click()
58
- await self.page.wait_for_timeout(3000)
59
- if self.page.url.startswith("https://accounts.google.com/v3/signin/challenge/dp"):
60
- await self.load_bot()
61
- await self.bot.receive("Аппрувни гмейл, у тебя 1.5 минуты", photo=await self.page.screenshot())
62
- await self.page.wait_for_url(lambda u: u.startswith(self.HOME), timeout=90 * 1000) # убеждаемся что мы в почте
63
- self.user.gmail.auth = await self.page.context.storage_state()
64
- await self.user.gmail.save()
65
-
66
- async def mail_confirm(self):
67
- lang = await self.page.get_attribute("html", "lang")
68
- labs = {
69
- "ru": "Оповещения",
70
- "en-US": "Updates",
71
- }
72
- tab = self.page.get_by_role("heading").get_by_label(labs[lang]).last
73
- await tab.click()
74
- rows = self.page.locator("tbody>>nth=4 >> tr")
75
- row = rows.get_by_text("Volet.com").and_(rows.get_by_text("Please Confirm Withdrawal"))
76
- if not await row.count():
77
- await self.bot.receive("А нет запросов от волета", photo=await self.page.screenshot())
78
-
79
- await row.click()
80
- await self.page.wait_for_load_state()
81
- btn = self.page.locator('a[href^="https://account.volet.com/verify/"]', has_text="confirm").first
82
- await btn.click()
83
-
84
- async def load_bot(self):
85
- if not self.bot:
86
- bot = FileClient(NET_TOKEN)
87
- self.bot = UserClient(self.uid, bot)
88
- if not self.bot.is_connected:
89
- await self.bot.start()
90
-
91
- async def stop(self):
92
- if self.bot and self.bot.is_connected: # todo: do not stop if
93
- await self.bot.stop(False)
94
- await self.page.context.close()
95
- await self.page.context.browser.close()
75
+
76
+ if not (messages := results.get("messages", [])):
77
+ logging.warning("Письма не найдены")
78
+ return None
79
+
80
+ # Получаем полную информацию о письме
81
+ message_id = messages[0]["id"]
82
+ message = self.service.users().messages().get(userId="me", id=message_id, format="full").execute()
83
+
84
+ # Извлекаем заголовки
85
+ headers = message["payload"]["headers"]
86
+ subject = next((h["value"] for h in headers if h["name"] == "Subject"), "Нет темы")
87
+ from_email = next((h["value"] for h in headers if h["name"] == "From"), "Неизвестно")
88
+ date = next((h["value"] for h in headers if h["name"] == "Date"), "Неизвестно")
89
+
90
+ # Извлекаем текст письма
91
+ body = _get_email_body(message["payload"])
92
+
93
+ return {"id": message_id, "subject": subject, "from": from_email, "date": date, "body": body}
94
+
95
+ async def volet_confirm(self, amount: float, dt: datetime):
96
+ if email := self._get_last_email("noreply@volet.com", "Please Confirm Withdrawal"): # "Volet.com"
97
+ date = datetime.strptime(email["date"].split(",")[1].split(" +")[0], "%d %b %Y %H:%M:%S")
98
+ if match := re.search(r"Amount: <b>([\d.]+) [A-Z]{3}</b>", email["body"]):
99
+ amt = float(match.group(1))
100
+ if match := re.search(r"https://account\.volet\.com/verify/([a-f0-9-]+)", email["body"]):
101
+ token = match.group(1)
102
+
103
+ if email and amount == amt and date > dt and token:
104
+ get(f"https://account.volet.com/verify/{token}")
105
+ return True
106
+
107
+ await self.receive("А нет запросов от волета")
108
+ return False
109
+
110
+ async def bybit_code(self, dt: datetime) -> str | None:
111
+ if email := self._get_last_email("Bybit", "[Bybit]Security Code for Your Bybit Account"):
112
+ date = datetime.strptime(email["date"].split(",")[1].split(" +")[0], "%d %b %Y %H:%M:%S")
113
+ if match := re.search(r'<span style="font-size:28pt;color:#ff9c2e">(\d{6})</span>', email["body"]):
114
+ code = match.group(1)
115
+
116
+ if email and date > dt and code:
117
+ get(f"https://account.volet.com/verify/{code}")
118
+ return code
119
+
120
+ await self.receive("А нет запросов от волета")
121
+ return None
96
122
 
97
123
 
98
124
  async def _test():
99
125
  from x_model import init_db
100
- from xync_schema import TORM
101
-
102
- _ = await init_db(TORM, True)
103
- uid = 193017646
104
- gmc = GmClient(uid)
105
- try:
106
- await gmc.start(True)
107
- except TimeoutError as te:
108
- raise te
109
- finally:
110
- await gmc.stop()
126
+
127
+ _ = await init_db(TORM)
128
+
129
+ gm = await Gmail.get(id=1).prefetch_related("user__username")
130
+ gmc = GmClient(gm)
131
+ await gmc.volet_confirm(amount=90, dt=datetime.now())
111
132
 
112
133
 
113
134
  if __name__ == "__main__":