xync-client 0.0.114__py3-none-any.whl → 0.0.155__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.
Files changed (39) hide show
  1. xync_client/Abc/AdLoader.py +299 -0
  2. xync_client/Abc/Agent.py +94 -10
  3. xync_client/Abc/Ex.py +27 -22
  4. xync_client/Abc/HasAbotUid.py +10 -0
  5. xync_client/Abc/InAgent.py +0 -11
  6. xync_client/Abc/PmAgent.py +42 -35
  7. xync_client/Abc/xtype.py +24 -2
  8. xync_client/Binance/ex.py +2 -2
  9. xync_client/BingX/ex.py +2 -2
  10. xync_client/BitGet/ex.py +2 -2
  11. xync_client/Bybit/InAgent.py +229 -114
  12. xync_client/Bybit/agent.py +584 -572
  13. xync_client/Bybit/etype/ad.py +11 -56
  14. xync_client/Bybit/etype/cred.py +29 -9
  15. xync_client/Bybit/etype/order.py +55 -62
  16. xync_client/Bybit/ex.py +17 -4
  17. xync_client/Gate/ex.py +2 -2
  18. xync_client/Gmail/__init__.py +119 -98
  19. xync_client/Htx/agent.py +162 -31
  20. xync_client/Htx/etype/ad.py +18 -11
  21. xync_client/Htx/ex.py +9 -11
  22. xync_client/KuCoin/ex.py +2 -2
  23. xync_client/Mexc/agent.py +85 -0
  24. xync_client/Mexc/api.py +636 -0
  25. xync_client/Mexc/etype/order.py +639 -0
  26. xync_client/Mexc/ex.py +12 -10
  27. xync_client/Okx/ex.py +2 -2
  28. xync_client/Pms/Payeer/__init__.py +147 -43
  29. xync_client/Pms/Payeer/login.py +29 -2
  30. xync_client/Pms/Volet/__init__.py +148 -94
  31. xync_client/Pms/Volet/api.py +17 -13
  32. xync_client/TgWallet/ex.py +2 -2
  33. xync_client/details.py +44 -0
  34. xync_client/loader.py +2 -1
  35. xync_client/pm_unifier.py +1 -1
  36. {xync_client-0.0.114.dist-info → xync_client-0.0.155.dist-info}/METADATA +6 -1
  37. {xync_client-0.0.114.dist-info → xync_client-0.0.155.dist-info}/RECORD +39 -33
  38. {xync_client-0.0.114.dist-info → xync_client-0.0.155.dist-info}/WHEEL +0 -0
  39. {xync_client-0.0.114.dist-info → xync_client-0.0.155.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  from enum import StrEnum
2
2
  from typing import List, Optional, Any, Literal
3
- from pydantic import BaseModel, Field, field_serializer
4
- from xync_schema import xtype
3
+ from pydantic import BaseModel, Field
4
+ from xync_client.Bybit.etype.cred import MyPaymentTerm
5
5
  from xync_schema.xtype import BaseAd
6
6
 
7
7
  from xync_client.Abc.xtype import BaseAdUpdate
@@ -113,7 +113,7 @@ class Ad(BaseAd):
113
113
  recommend: bool = None # for initial actualize
114
114
  recommendTag: str = None # for initial actualize
115
115
  remark: str = Field(serialization_alias="auto_msg")
116
- side: Literal[0, 1] = None # for initial actualize # 0 - покупка, 1 - продажа (для мейкера, т.е КАКАЯ объява)
116
+ side: Literal[0, 1] = None # for initial actualize # 0 - покупка, 1 - продажа (для мейкера, т.е КАКАЯ объява)
117
117
  status: Literal[10, 20, 30] # 10: online; 20: offline; 30: completed
118
118
  symbolInfo: SymbolInfo = None # for initial actualize
119
119
  tokenId: str = None # for initial actualize
@@ -126,59 +126,14 @@ class Ad(BaseAd):
126
126
  verificationOrderLabels: List[Any] = None # for initial actualize
127
127
  verificationOrderSwitch: bool = None # for initial actualize
128
128
  version: int = None # for initial actualize
129
- #
130
- #
131
- # class Ad(BaseAd):
132
- # accountId: str = None # for initial actualize
133
- # authStatus: int = None # for initial actualize
134
- # authTag: List[str] = None # for initial actualize
135
- # ban: bool = None # for initial actualize
136
- # baned: bool = None # for initial actualize
137
- # blocked: str = None # for initial actualize
138
- # createDate: str = None # for initial actualize
139
- # currencyId: str = None # for initial actualize
140
- # executedQuantity: str = None # for initial actualize
141
- # fee: str = None # for initial actualize
142
- # finishNum: int = None # for initial actualize
143
- # frozenQuantity: str = None # for initial actualize
144
- # exid: str = Field(serialization_alias="id")
145
- # isOnline: bool = None # for initial actualize
146
- # itemType: str = None # for initial actualize
147
- # lastLogoutTime: str = None # for initial actualize
148
- # quantity: str = Field(serialization_alias="lastQuantity")
149
- # makerContact: bool = None # for initial actualize
150
- # max_fiat: str = Field(serialization_alias="maxAmount")
151
- # min_fiat: str = Field(serialization_alias="minAmount")
152
- # nickName: str = None # for initial actualize
153
- # orderNum: int = None # for initial actualize
154
- # paymentPeriod: int = None # for initial actualize
155
- # payments: List[str] = None # for initial actualize
156
- # premium: str = None # for initial actualize
157
- # price: str = None # for initial actualize
158
- # priceType: Literal[0, 1] = None # for initial actualize # 0 - fix rate, 1 - floating
159
- # allQuantity: str = Field(serialization_alias="quantity") # for initial actualize
160
- # recentExecuteRate: int = None # for initial actualize
161
- # recentOrderNum: int = None # for initial actualize
162
- # recommend: bool = None # for initial actualize
163
- # recommendTag: str = None # for initial actualize
164
- # auto_msg: str = Field(serialization_alias="remark")
165
- # is_sell: Literal[0, 1] = Field(serialization_alias="side") # for initial actualize # 0 - покупка, 1 - продажа (для мейкера, т.е КАКАЯ объява)
166
- # status: Literal[10, 20, 30] # 10: online; 20: offline; 30: completed
167
- # symbolInfo: SymbolInfo = None # for initial actualize
168
- # tokenId: str = None # for initial actualize
169
- # tokenName: str = None # for initial actualize
170
- # tradingPreferenceSet: TradingPreferenceSet | None = None # for initial actualize
171
- # userId: str = Field(serialization_alias="maker__exid")
172
- # userMaskId: str = None # for initial actualize
173
- # userType: str = None # for initial actualize
174
- # verificationOrderAmount: str = None # for initial actualize
175
- # verificationOrderLabels: List[Any] = None # for initial actualize
176
- # verificationOrderSwitch: bool = None # for initial actualize
177
- # version: int = None # for initial actualize
178
-
179
- @field_serializer("status")
180
- def status(self, status, _info) -> xtype.AdStatus:
181
- return {10: xtype.AdStatus.active, 20: xtype.AdStatus.defActive, 30: xtype.AdStatus.soldOut}[status]
129
+
130
+ # @field_serializer("status")
131
+ # def status(self, status, _info) -> xtype.AdStatus:
132
+ # return {10: xtype.AdStatus.active, 20: xtype.AdStatus.defActive, 30: xtype.AdStatus.soldOut}[status]
133
+
134
+
135
+ class MyAd(Ad):
136
+ paymentTerms: List[MyPaymentTerm]
182
137
 
183
138
 
184
139
  class AdPostRequest(BaseModel):
@@ -1,5 +1,3 @@
1
- from typing import Literal
2
-
3
1
  from pydantic import BaseModel
4
2
 
5
3
  from xync_client.Abc.xtype import CredExOut
@@ -15,18 +13,31 @@ class PaymentItem(BaseModel):
15
13
  required: bool
16
14
 
17
15
 
18
- class PaymentConfigVo(BaseModel):
19
- paymentType: str
16
+ class BasePaymentConf(BaseModel):
17
+ paymentType: int
18
+ paymentName: str
19
+
20
+
21
+ class PaymentConfig(BasePaymentConf):
22
+ class PaymentTemplateItem(BaseModel):
23
+ labelDialect: str
24
+ placeholderDialect: str
25
+ fieldName: str
26
+
27
+ paymentDialect: str
28
+ paymentTemplateItem: list[PaymentTemplateItem]
29
+
30
+
31
+ class PaymentConfigVo(BasePaymentConf):
20
32
  checkType: int
21
33
  sort: int
22
- paymentName: str
23
34
  addTips: str
24
35
  itemTips: str
25
- online: Literal[0, 1] # Non-balance coin purchase (0 Offline), balance coin purchase (1 Online)
26
- items: list[PaymentItem]
36
+ online: int
37
+ items: list[dict[str, str | bool]]
27
38
 
28
39
 
29
- class CredEpyd(CredExOut):
40
+ class PaymentTerm(CredExOut):
30
41
  id: str # int
31
42
  realName: str
32
43
  paymentType: int # int
@@ -44,7 +55,7 @@ class CredEpyd(CredExOut):
44
55
  mobile: str
45
56
  businessName: str
46
57
  concept: str
47
- online: str
58
+ online: str = None
48
59
  paymentExt1: str
49
60
  paymentExt2: str
50
61
  paymentExt3: str
@@ -54,6 +65,15 @@ class CredEpyd(CredExOut):
54
65
  paymentTemplateVersion: int
55
66
 
56
67
 
68
+ class MyPaymentTerm(PaymentTerm):
69
+ paymentConfig: PaymentConfig
70
+ realNameVerified: bool
71
+
72
+
73
+ class CredEpyd(PaymentTerm):
74
+ securityRiskToken: str = ""
75
+
76
+
57
77
  class MyCredEpyd(CredEpyd): # todo: заменить везде где надо CredEpyd -> MyCredEpyd
58
78
  countNo: str
59
79
  hasPaymentTemplateChanged: bool
@@ -1,10 +1,9 @@
1
- from datetime import datetime
2
1
  from enum import IntEnum
3
2
  from typing import Literal
4
3
 
5
4
  from pydantic import BaseModel
6
5
 
7
- from xync_client.Bybit.etype.cred import CredEpyd
6
+ from xync_client.Bybit.etype.cred import CredEpyd, PaymentTerm as CredPaymentTerm, PaymentConfigVo
8
7
 
9
8
 
10
9
  class Topic(IntEnum):
@@ -16,7 +15,7 @@ class Topic(IntEnum):
16
15
 
17
16
  class Status(IntEnum):
18
17
  deleted = 40 # order canceled
19
- # created = 10 # waiting for buyer to pay
18
+ created = 10
20
19
  appealable = 20 # waiting for seller to release
21
20
  canceled = 40
22
21
  completed = 50 # order finished
@@ -26,17 +25,34 @@ class Status(IntEnum):
26
25
  paid = 50
27
26
  rejected = 40
28
27
  request_canceled = 40
29
- requested = 10
30
28
  seller_appeal_disputed_by_buyer = 30
31
29
 
32
30
 
33
31
  class StatusApi(IntEnum):
34
32
  created = 1
33
+ _web3 = 5
35
34
  wait_for_buyer = 10 # ws_canceled
36
35
  wait_for_seller = 20
37
36
  appealed = 30
38
37
  canceled = 40
39
38
  completed = 50
39
+ _paying_online = 60
40
+ _pay_fail_online = 70
41
+ hotswap_cancelled = 80
42
+ _buyer_sel_tokenId = 90
43
+ objectioning = 100
44
+ waiting_for_objection = 110
45
+
46
+
47
+ class TakeAdReq(BaseModel):
48
+ ad_id: int | str
49
+ amount: float
50
+ is_sell: bool
51
+ pm_id: int
52
+ coin_id: int
53
+ cur_id: int
54
+ quantity: float | None = None
55
+ price: float | None = None
40
56
 
41
57
 
42
58
  class OrderRequest(BaseModel):
@@ -54,33 +70,46 @@ class OrderRequest(BaseModel):
54
70
  flag: Literal["amount", "quantity"]
55
71
  version: str = "1.0"
56
72
  securityRiskToken: str = ""
73
+ isFromAi: bool = False
74
+
75
+
76
+ class OrderSellRequest(OrderRequest):
77
+ paymentId: str
78
+ paymentType: str
57
79
 
58
80
 
59
81
  class PreOrderResp(BaseModel):
60
- price: str # float
61
- curPrice: str
62
- totalAmount: float
63
- minAmount: float
64
- maxAmount: float
65
- minQuantity: float
66
- maxQuantity: float
67
- payments: list[str] # list[int]
68
- status: Literal[10, 20]
69
- paymentTerms: list
70
- paymentPeriod: Literal[15]
71
- lastQuantity: float
72
- lastPrice: float
82
+ id: str # bigint
83
+ price: str # float .cur.scale
84
+ lastQuantity: str # float .coin.scale
85
+ curPrice: str # hex 32
86
+ lastPrice: str # float .cur.scale # future
73
87
  isOnline: bool
74
- lastLogoutTime: datetime
75
- itemPriceAvailableTime: datetime
76
- itemPriceValidTime: int # 45000
88
+ lastLogoutTime: str # timestamp(0)+0
89
+ payments: list[str] # list[int]
90
+ status: Literal[10, 15, 20]
91
+ paymentTerms: list # empty
92
+ paymentPeriod: Literal[15, 30, 60]
93
+ totalAmount: str # float .cur.scale
94
+ minAmount: str # float .cur.scale
95
+ maxAmount: str # float .cur.scale
96
+ minQuantity: str # float .coin.scale
97
+ maxQuantity: str # float .coin.scale
98
+ itemPriceAvailableTime: str # timestamp(0)+0
99
+ itemPriceValidTime: Literal["45000"]
77
100
  itemType: Literal["ORIGIN"]
101
+ shareItem: bool # False
78
102
 
79
103
 
80
104
  class OrderResp(BaseModel):
81
105
  orderId: str
82
106
  isNeedConfirm: bool
107
+ confirmId: str = ""
83
108
  success: bool
109
+ securityRiskToken: str = ""
110
+ riskTokenType: Literal["challenge", ""] = ""
111
+ riskVersion: Literal["1", "2", ""] = ""
112
+ needSecurityRisk: bool
84
113
  isBulkOrder: bool
85
114
  confirmed: str = None
86
115
  delayTime: str
@@ -115,43 +144,7 @@ class AppraiseInfo(BaseModel):
115
144
  updateDate: str
116
145
 
117
146
 
118
- class PaymentConfigVo(BaseModel):
119
- paymentType: str
120
- checkType: int
121
- sort: int
122
- paymentName: str
123
- addTips: str
124
- itemTips: str
125
- online: int
126
- items: list[dict[str, str | bool]]
127
-
128
-
129
- class PaymentTerm(BaseModel):
130
- id: str
131
- realName: str
132
- paymentType: int
133
- bankName: str
134
- branchName: str
135
- accountNo: str
136
- qrcode: str
137
- visible: int
138
- payMessage: str
139
- firstName: str
140
- lastName: str
141
- secondLastName: str
142
- clabe: str
143
- debitCardNumber: str
144
- mobile: str
145
- businessName: str
146
- concept: str
147
- online: str
148
- paymentExt1: str
149
- paymentExt2: str
150
- paymentExt3: str
151
- paymentExt4: str
152
- paymentExt5: str
153
- paymentExt6: str
154
- paymentTemplateVersion: int
147
+ class PaymentTerm(CredPaymentTerm):
155
148
  paymentConfigVo: PaymentConfigVo
156
149
  ruPaymentPrompt: bool
157
150
 
@@ -288,18 +281,18 @@ class Message(BaseModel):
288
281
  nickName: str
289
282
  read: Literal[0, 1]
290
283
  fileName: str
291
- onlyForCustomer: int
284
+ onlyForCustomer: int | None = None
292
285
 
293
286
 
294
287
  class _BaseChange(BaseModel):
295
288
  userId: int
296
289
  makerUserId: int
297
290
  id: str
298
- status: StatusApi
299
291
  createDate: int
300
292
  side: int
301
293
  appealedTimes: int
302
294
  totalAppealedTimes: int
295
+ status: StatusApi | None = None
303
296
 
304
297
 
305
298
  class StatusChange(_BaseChange):
@@ -318,18 +311,18 @@ class _BaseMsg(BaseModel):
318
311
  msgUuId: str
319
312
  createDate: str
320
313
  contentType: str
321
- roleType: Literal["user", "sys", "alarm"]
314
+ roleType: Literal["user", "sys", "alarm", "customer_support"]
322
315
 
323
316
 
324
317
  class Receive(_BaseMsg):
325
318
  id: int
326
319
  msgCode: int
327
- onlyForCustomer: int
320
+ onlyForCustomer: int | None = None
328
321
 
329
322
 
330
323
  class Read(_BaseMsg):
331
324
  readAmount: int
332
- read: Literal["111", "101"]
325
+ read: Literal["101", "110", "11", "111"]
333
326
  orderStatus: StatusApi
334
327
 
335
328
 
xync_client/Bybit/ex.py CHANGED
@@ -2,26 +2,39 @@ import json
2
2
  from asyncio import run
3
3
 
4
4
  from pyro_client.client.file import FileClient
5
+ from x_client import df_hdrs
5
6
  from x_model import init_db
6
7
  from xync_schema import models, xtype
8
+ from xync_schema.enums import AdStatus, AgentStatus
7
9
  from xync_schema.models import Ex, Agent
8
10
 
9
11
  from xync_client.Abc.Ex import BaseExClient
10
- from xync_client.loader import TOKEN
12
+ from xync_client.loader import NET_TOKEN
11
13
  from xync_client.Bybit.etype import ad
12
14
  from xync_client.Abc.xtype import PmEx, MapOfIdsList
13
15
  from xync_client.loader import TORM
14
16
 
15
17
 
16
18
  class ExClient(BaseExClient): # Bybit client
17
- headers = {"cookie": ";"} # rewrite token for public methods
19
+ host = "api2.bybit.com"
20
+ headers = df_hdrs # rewrite token for public methods
18
21
  agent: Agent = None
19
22
 
20
23
  async def _get_auth_cks(self) -> dict[str, str]:
21
24
  if not self.agent:
22
- self.agent = await Agent.get(actor__ex=self.ex).prefetch_related("actor")
25
+ self.agent = (
26
+ await Agent.filter(actor__ex=self.ex, status__gt=AgentStatus.off).prefetch_related("actor").first()
27
+ )
23
28
  return self.agent.auth["cookies"]
24
29
 
30
+ @staticmethod
31
+ def ad_status(status: int) -> AdStatus:
32
+ return {
33
+ 10: AdStatus.active,
34
+ 20: AdStatus.defActive,
35
+ 30: AdStatus.soldOut,
36
+ }[status]
37
+
25
38
  async def _get_config(self):
26
39
  resp = await self._get("/fiat/p2p/config/initial")
27
40
  return resp["result"] # todo: tokens, pairs, ...
@@ -114,7 +127,7 @@ class ExClient(BaseExClient): # Bybit client
114
127
  async def main():
115
128
  _ = await init_db(TORM, True)
116
129
  ex = await Ex.get(name="Bybit")
117
- bot: FileClient = FileClient(TOKEN)
130
+ bot: FileClient = FileClient(NET_TOKEN)
118
131
  # await bot.start()
119
132
  cl = ExClient(ex, bot)
120
133
  await cl.set_pms()
xync_client/Gate/ex.py CHANGED
@@ -6,7 +6,7 @@ from xync_schema.models import Ex
6
6
  from xync_schema import xtype
7
7
 
8
8
  from xync_client.Abc.Ex import BaseExClient
9
- from xync_client.loader import TOKEN, TORM
9
+ from xync_client.loader import NET_TOKEN, TORM
10
10
  from xync_client.Abc.xtype import PmEx, MapOfIdsList
11
11
  from xync_client.Gate.etype import ad
12
12
 
@@ -93,7 +93,7 @@ class ExClient(BaseExClient):
93
93
  async def main():
94
94
  _ = await init_db(TORM, True)
95
95
  gt = await Ex.get(name="Gate")
96
- async with FileClient(TOKEN) as b:
96
+ async with FileClient(NET_TOKEN) as b:
97
97
  cl = ExClient(gt, b)
98
98
  await cl.set_pairs()
99
99
  pms = await cl.set_coins()
@@ -1,113 +1,134 @@
1
- from playwright.async_api import Page, Playwright
2
- from pyro_client.client.bot import BotClient
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 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",
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 = await BotClient(TOKEN)
87
- self.bot = await 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__":