xync-client 0.0.141__py3-none-any.whl → 0.0.156.dev18__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 (40) hide show
  1. xync_client/Abc/AdLoader.py +5 -0
  2. xync_client/Abc/Agent.py +354 -8
  3. xync_client/Abc/Ex.py +432 -25
  4. xync_client/Abc/HasAbotUid.py +10 -0
  5. xync_client/Abc/InAgent.py +0 -11
  6. xync_client/Abc/PmAgent.py +34 -26
  7. xync_client/Abc/xtype.py +57 -3
  8. xync_client/Bybit/InAgent.py +233 -409
  9. xync_client/Bybit/agent.py +844 -777
  10. xync_client/Bybit/etype/__init__.py +0 -0
  11. xync_client/Bybit/etype/ad.py +54 -86
  12. xync_client/Bybit/etype/cred.py +29 -9
  13. xync_client/Bybit/etype/order.py +75 -103
  14. xync_client/Bybit/ex.py +35 -48
  15. xync_client/Gmail/__init__.py +119 -98
  16. xync_client/Htx/agent.py +213 -40
  17. xync_client/Htx/etype/ad.py +40 -16
  18. xync_client/Htx/etype/order.py +194 -0
  19. xync_client/Htx/ex.py +17 -19
  20. xync_client/Mexc/agent.py +268 -0
  21. xync_client/Mexc/api.py +1255 -0
  22. xync_client/Mexc/etype/ad.py +52 -1
  23. xync_client/Mexc/etype/order.py +354 -0
  24. xync_client/Mexc/ex.py +34 -22
  25. xync_client/Okx/1.py +14 -0
  26. xync_client/Okx/agent.py +39 -0
  27. xync_client/Okx/ex.py +8 -8
  28. xync_client/Pms/Payeer/agent.py +396 -0
  29. xync_client/Pms/Payeer/login.py +1 -59
  30. xync_client/Pms/Payeer/trade.py +58 -0
  31. xync_client/Pms/Volet/__init__.py +82 -63
  32. xync_client/Pms/Volet/api.py +5 -4
  33. xync_client/loader.py +2 -0
  34. xync_client/pm_unifier.py +1 -1
  35. {xync_client-0.0.141.dist-info → xync_client-0.0.156.dev18.dist-info}/METADATA +5 -1
  36. {xync_client-0.0.141.dist-info → xync_client-0.0.156.dev18.dist-info}/RECORD +38 -29
  37. xync_client/Pms/Payeer/__init__.py +0 -253
  38. xync_client/Pms/Payeer/api.py +0 -25
  39. {xync_client-0.0.141.dist-info → xync_client-0.0.156.dev18.dist-info}/WHEEL +0 -0
  40. {xync_client-0.0.141.dist-info → xync_client-0.0.156.dev18.dist-info}/top_level.txt +0 -0
xync_client/Abc/Ex.py CHANGED
@@ -1,27 +1,48 @@
1
1
  import logging
2
+ import re
2
3
  from abc import abstractmethod
3
4
  from asyncio import sleep
5
+ from collections import defaultdict
6
+ from difflib import SequenceMatcher
4
7
 
5
8
  from aiohttp import ClientSession, ClientResponse
9
+ from google.protobuf.internal.wire_format import INT32_MAX
6
10
  from msgspec import Struct
11
+ from pydantic import BaseModel
7
12
  from pyro_client.client.file import FileClient
8
- from tortoise.exceptions import MultipleObjectsReturned, IntegrityError
13
+ from tortoise.exceptions import MultipleObjectsReturned, IntegrityError, OperationalError
9
14
  from x_client.aiohttp import Client as HttpClient
15
+ from xync_client.Bybit.etype.ad import AdsReq
10
16
  from xync_schema import models
11
17
  from xync_schema.enums import FileType
12
18
  from xync_schema.xtype import CurEx, CoinEx, BaseAd, BaseAdIn
13
19
 
14
- from xync_client.Abc.xtype import PmEx, MapOfIdsList
20
+ from xync_client.Abc.AdLoader import AdLoader
21
+ from xync_client.Abc.xtype import PmEx, MapOfIdsList, GetAds
15
22
  from xync_client.pm_unifier import PmUnifier, PmUni
16
23
 
17
24
 
18
- class BaseExClient(HttpClient):
25
+ class BaseExClient(HttpClient, AdLoader):
26
+ host: str = None
19
27
  cur_map: dict[int, str] = {}
20
28
  unifier_class: type = PmUnifier
21
29
  logo_pre_url: str
22
30
  bot: FileClient
23
31
  ex: models.Ex
24
32
 
33
+ coin_x2e: dict[int, tuple[str, int]] = {}
34
+ coin_e2x: dict[str, int] = {}
35
+ cur_x2e: dict[int, tuple[str, int, int]] = {}
36
+ cur_e2x: dict[str, int] = {}
37
+ pm_x2e: dict[int, str] = {}
38
+ pm_e2x: dict[str, int] = {}
39
+ pairs_e2x: dict[int, dict[int, tuple[int, int]]] = defaultdict(defaultdict)
40
+ pairs_x2e: dict[int, tuple[int, int, int]] = defaultdict(defaultdict)
41
+ actor_x2e: dict[int, int] = {}
42
+ actor_e2x: dict[int, int] = {}
43
+ ad_x2e: dict[int, int] = {}
44
+ ad_e2x: dict[int, int] = {}
45
+
25
46
  def __init__(
26
47
  self,
27
48
  ex: models.Ex,
@@ -33,7 +54,7 @@ class BaseExClient(HttpClient):
33
54
  ):
34
55
  self.ex = ex
35
56
  self.bot = bot
36
- super().__init__(getattr(ex, attr), headers, cookies, proxy and proxy.str())
57
+ super().__init__(self.host or getattr(ex, attr), headers, cookies, proxy and proxy.str())
37
58
 
38
59
  @abstractmethod
39
60
  def pm_type_map(self, typ: models.PmEx) -> str: ...
@@ -62,18 +83,115 @@ class BaseExClient(HttpClient):
62
83
  @abstractmethod
63
84
  async def pairs(self) -> tuple[MapOfIdsList, MapOfIdsList]: ...
64
85
 
86
+ # converters
87
+ async def x2e_coin(self, coin_id: int) -> tuple[str, int]: # coinex.exid
88
+ if not self.coin_x2e.get(coin_id):
89
+ exid, scale = await models.CoinEx.get(coin_id=coin_id, ex=self.ex).values_list("exid", "scale")
90
+ self.coin_x2e[coin_id] = exid, scale
91
+ self.coin_e2x[exid] = coin_id
92
+ return self.coin_x2e[coin_id]
93
+
94
+ async def e2x_coin(self, exid: str) -> int: # coin.id
95
+ if not self.coin_e2x.get(exid):
96
+ coin_id, scale = await models.CoinEx.get(exid=exid, ex=self.ex).values_list("coin_id", "scale")
97
+ self.coin_x2e[coin_id] = exid, scale
98
+ self.coin_e2x[exid] = coin_id
99
+ return self.coin_e2x[exid]
100
+
101
+ async def x2e_cur(self, cur_id: int) -> tuple[str, int, int]: # curex.exid
102
+ if not self.cur_x2e.get(cur_id):
103
+ exid, scale, mnm = await models.CurEx.get(cur_id=cur_id, ex=self.ex).values_list("exid", "scale", "minimum")
104
+ self.cur_x2e[cur_id] = exid, scale, mnm
105
+ self.cur_e2x[exid] = cur_id
106
+ return self.cur_x2e[cur_id]
107
+
108
+ async def e2x_cur(self, exid: str) -> int: # cur.id
109
+ if not self.cur_e2x.get(exid):
110
+ cur_id, scale, mnm = await models.CurEx.get(exid=exid, ex=self.ex).values_list("cur_id", "scale", "minimum")
111
+ self.cur_e2x[exid] = cur_id
112
+ self.cur_x2e[cur_id] = exid, scale, mnm
113
+ return self.cur_e2x[exid]
114
+
115
+ async def x2e_pm(self, pm_id: int) -> str: # pmex.exid
116
+ if not self.pm_x2e.get(pm_id):
117
+ self.pm_x2e[pm_id] = (await models.PmEx.get(pm_id=pm_id, ex=self.ex)).exid
118
+ self.pm_e2x[self.pm_x2e[pm_id]] = pm_id
119
+ return self.pm_x2e[pm_id]
120
+
121
+ async def e2x_pm(self, exid: str) -> int: # pm.id
122
+ if not self.pm_e2x.get(exid):
123
+ self.pm_e2x[exid] = (await models.PmEx.get(exid=exid, ex=self.ex)).pm_id
124
+ self.pm_x2e[self.pm_e2x[exid]] = exid
125
+ return self.pm_e2x[exid]
126
+
127
+ # pair_side
128
+ async def ccs2pair(self, coin_id: int, cur_id: int, is_sell: bool) -> int: # ex cur+coin+is_sale -> x pair.id
129
+ if not self.pairs_e2x.get(coin_id, {}).get(cur_id):
130
+ self.pairs_e2x[coin_id][cur_id] = (
131
+ await models.PairSide.filter(pair__coin_id=coin_id, pair__cur_id=coin_id)
132
+ .order_by("is_sell")
133
+ .values_list("id", flat=True)
134
+ )
135
+ return self.pairs_e2x[coin_id][cur_id][int(is_sell)]
136
+
137
+ async def pair2ccs(self, xid: int) -> tuple[int, int, int]: # coinex.exid
138
+ if not self.pairs_x2e.get(xid):
139
+ ps = await models.PairSide.get(id=xid).prefetch_related("pair")
140
+ self.pairs_x2e[xid] = ps.pair.coin_id, ps.pair.cur_id, ps.is_sell
141
+ return self.pairs_x2e[xid]
142
+
143
+ async def e2x_pair(self, coin_exid: str, cur_exid: str, is_sell: bool) -> int: # ex cur+coin+is_sale -> x pair.id
144
+ coin_id = await self.e2x_coin(coin_exid)
145
+ cur_id = await self.e2x_cur(cur_exid)
146
+ return await self.ccs2pair(coin_id, cur_id, is_sell)
147
+
148
+ async def x2e_pair(self, pair_side_id: int) -> tuple[str, str, int]: # coinex.exid
149
+ coin_id, cur_id, is_sell = await self.pair2ccs(pair_side_id)
150
+ coin_exid = await self.x2e_coin(coin_id)
151
+ cur_exid = await self.x2e_cur(cur_id)
152
+ return coin_exid[0], cur_exid[0], is_sell
153
+
154
+ async def x2e_actor(self, actor_id: int) -> int: # actor.exid
155
+ if not self.actor_x2e.get(actor_id):
156
+ self.actor_x2e[actor_id] = (await models.Actor[actor_id]).exid
157
+ self.actor_e2x[self.actor_x2e[actor_id]] = actor_id
158
+ return self.actor_x2e[actor_id]
159
+
160
+ async def e2x_actor(self, exid: int) -> int: # actor.id
161
+ if not self.actor_e2x.get(exid):
162
+ self.actor_e2x[exid] = (await models.Actor.get(exid=exid, ex=self.ex)).id
163
+ self.actor_x2e[self.actor_e2x[exid]] = exid
164
+ return self.actor_e2x[exid]
165
+
166
+ async def x2e_ad(self, ad_id: int) -> int: # ad.exid
167
+ if not self.ad_x2e.get(ad_id):
168
+ self.ad_x2e[ad_id] = (await models.Ad[ad_id]).exid
169
+ self.ad_e2x[self.ad_x2e[ad_id]] = ad_id
170
+ return self.ad_x2e[ad_id]
171
+
172
+ async def e2x_ad(self, exid: int) -> int: # ad.id
173
+ if not self.ad_e2x.get(exid):
174
+ self.ad_e2x[exid] = (await models.Ad.get(exid=exid, maker__ex=self.ex)).id
175
+ self.ad_x2e[self.ad_e2x[exid]] = exid
176
+ return self.ad_e2x[exid]
177
+
65
178
  # 24: Список объяв по (buy/sell, cur, coin, pm)
179
+ async def ads(self, xreq: GetAds, **kwargs) -> list[BaseAd]:
180
+ self.ex.etype().ad.AdsReq
181
+ ereq = AdsReq(
182
+ coin_id=(await self.x2e_coin(xreq.coin_id))[0],
183
+ cur_id=(await self.x2e_cur(xreq.cur_id))[0],
184
+ is_sell=str(int(xreq.is_sell)),
185
+ pm_ids=[await self.x2e_pm(pid) for pid in xreq.pm_ids],
186
+ # size=str(xreq.limit),
187
+ # page=str(xreq.page),
188
+ )
189
+ if xreq.amount:
190
+ ereq.amount = str(xreq.amount)
191
+ return await self._ads(ereq, **kwargs)
192
+
66
193
  @abstractmethod
67
- async def ads(
68
- self,
69
- coin_exid: str,
70
- cur_exid: str,
71
- is_sell: bool,
72
- pm_exids: list[str | int] = None,
73
- amount: int = None,
74
- lim: int = None,
75
- ) -> list[BaseAd]: # {ad.id: ad}
76
- ...
194
+ async def _ads(self, ereq: BaseModel, **kwargs) -> list[BaseAd]: ...
77
195
 
78
196
  # 42: Чужая объява по id
79
197
  @abstractmethod
@@ -250,14 +368,14 @@ class BaseExClient(HttpClient):
250
368
  return True
251
369
 
252
370
  # Сохранение чужого объявления (с Pm-ами) в бд
253
- async def ad_pydin2db(self, ad_pydin: BaseAdIn) -> models.Ad:
254
- dct = ad_pydin.model_dump()
255
- dct["exid"] = dct.pop("id")
256
- ad_in = models.Ad.validate(dct)
257
- ad_db, _ = await models.Ad.update_or_create(**ad_in.df_unq())
258
- await ad_db.credexs.add(*getattr(ad_pydin, "credexs_", []))
259
- await ad_db.pmexs.add(*getattr(ad_pydin, "pmexs_", []))
260
- return ad_db
371
+ # async def ad_pydin2db(self, ad_pydin: BaseAdIn) -> models.Ad:
372
+ # dct = ad_pydin.model_dump()
373
+ # dct["exid"] = dct.pop("id")
374
+ # ad_in = models.Ad.validate(dct)
375
+ # ad_db, _ = await models.Ad.update_or_create(**ad_in.df_unq())
376
+ # await ad_db.credexs.add(*getattr(ad_pydin, "credexs_", []))
377
+ # await ad_db.pmexs.add(*getattr(ad_pydin, "pmexs_", []))
378
+ # return ad_db
261
379
 
262
380
  async def file_upsert(self, url: str, ss: ClientSession = None) -> models.File:
263
381
  if not (file := await models.File.get_or_none(name__startswith=url.split("?")[0])):
@@ -271,12 +389,301 @@ class BaseExClient(HttpClient):
271
389
  # fr = await pbot.get_file(file.ref) # check
272
390
  return file
273
391
 
274
- async def _proc(self, resp: ClientResponse, data_key: str = None, bp: dict | str = None) -> dict | str:
392
+ async def _proc(self, resp: ClientResponse, bp: dict | str = None) -> dict | str:
275
393
  if resp.status in (403,):
276
394
  proxy = await models.Proxy.filter(valid=True, country__short__not="US").order_by("-updated_at").first()
277
395
  cookies = self.session.cookie_jar.filter_cookies(self.session._base_url)
278
396
  self.session = ClientSession(
279
397
  self.session._base_url, headers=self.session.headers, cookies=cookies or None, proxy=proxy.str()
280
398
  )
281
- return await self.METHS[resp.method](self, resp.url.path, bp, data_key=data_key)
282
- return await super()._proc(resp, data_key, bp)
399
+ return await self.METHS[resp.method](self, resp.url.path, bp)
400
+ return await super()._proc(resp, bp)
401
+
402
+ # ad cond loader
403
+ all_conds: dict[int, tuple[str, set[int]]] = {}
404
+ cond_sims: dict[int, int] = defaultdict(set)
405
+ rcond_sims: dict[int, set[int]] = defaultdict(set) # backward
406
+ tree: dict = {}
407
+
408
+ async def old_conds_load(self):
409
+ # пока не порешали рейс-кондишн, очищаем сиротские условия при каждом запуске
410
+ # [await c.delete() for c in await Cond.filter(ads__isnull=True)]
411
+ self.all_conds = {
412
+ c.id: (c.raw_txt, {a.maker.exid for a in c.ads})
413
+ for c in await models.Cond.all().prefetch_related("ads__maker")
414
+ }
415
+ for curr, old in await models.CondSim.filter().values_list("cond_id", "cond_rel_id"):
416
+ self.cond_sims[curr] = old
417
+ self.rcond_sims[old] |= {curr}
418
+
419
+ self.build_tree()
420
+ a = set()
421
+
422
+ def check_tree(tre):
423
+ for p, c in tre.items():
424
+ a.add(p)
425
+ check_tree(c)
426
+
427
+ for pr, ch in self.tree.items():
428
+ check_tree(ch)
429
+ if ct := set(self.tree.keys()) & a:
430
+ logging.exception(f"cycle cids: {ct}")
431
+
432
+ async def person_name_update(self, name: str, exid: int) -> models.Person:
433
+ if actor := await models.Actor.get_or_none(exid=exid, ex=self.ex).prefetch_related("person"):
434
+ actor.person.name = name
435
+ await actor.person.save()
436
+ return actor.person
437
+ # tmp dirty fix
438
+ note = f"{self.ex.id}:{exid}"
439
+ if person := await models.Person.get_or_none(note__startswith=note):
440
+ person.name = name
441
+ await person.save()
442
+ return person
443
+ try:
444
+ return await models.Person.create(name=name, note=note)
445
+ except OperationalError as e:
446
+ raise e
447
+ await models.Actor.create(person=person, exid=exid, ex=self.ex)
448
+ return person
449
+ # person = await models.Person.create(note=f'{actor.ex_id}:{actor.exid}:{name}') # no person for just ads with no orders
450
+ # raise ValueError(f"Agent #{exid} not found")
451
+
452
+ async def ad_load(
453
+ self,
454
+ pad: BaseAd,
455
+ cid: int = None,
456
+ ps: models.PairSide = None,
457
+ maker: models.Actor = None,
458
+ coinex: models.CoinEx = None,
459
+ curex: models.CurEx = None,
460
+ rname: str = None,
461
+ ) -> models.Ad:
462
+ self.e2x_actor()
463
+ if not maker:
464
+ if not (maker := await models.Actor.get_or_none(exid=pad.userId, ex=self.ex)):
465
+ person = await models.Person.create(name=rname, note=f"{self.ex.id}:{pad.userId}:{pad.nickName}")
466
+ maker = await models.Actor.create(name=pad.nickName, person=person, exid=pad.userId, ex=self.ex)
467
+ if rname:
468
+ await self.person_name_update(rname, int(pad.userId))
469
+ ps = ps or await models.PairSide.get_or_none(
470
+ is_sell=pad.side,
471
+ pair__coin__ticker=pad.tokenId,
472
+ pair__cur__ticker=pad.currencyId,
473
+ ).prefetch_related("pair")
474
+ # if not ps or not ps.pair:
475
+ # ... # THB/USDC: just for initial filling
476
+ ad_upd = models.Ad.validate(pad.model_dump(by_alias=True))
477
+ cur_scale = 10 ** (curex or await models.CurEx.get(cur_id=ps.pair.cur_id, ex=self.ex)).scale
478
+ coin_scale = 10 ** (coinex or await models.CoinEx.get(coin_id=ps.pair.coin_id, ex=self.ex)).scale
479
+ amt = int(float(pad.quantity) * float(pad.price) * cur_scale)
480
+ mxf = pad.maxAmount and int(float(pad.maxAmount) * cur_scale)
481
+ df_unq = ad_upd.df_unq(
482
+ maker_id=maker.id,
483
+ pair_side_id=ps.id,
484
+ amount=min(amt, INT32_MAX),
485
+ quantity=int(float(pad.quantity) * coin_scale),
486
+ min_fiat=int(float(pad.minAmount) * cur_scale),
487
+ max_fiat=min(mxf, INT32_MAX),
488
+ price=int(float(pad.price) * cur_scale),
489
+ premium=int(float(pad.premium) * 100),
490
+ cond_id=cid,
491
+ status=self.ad_status(ad_upd.status),
492
+ )
493
+ try:
494
+ ad_db, _ = await models.Ad.update_or_create(**df_unq)
495
+ except OperationalError as e:
496
+ raise e
497
+ await ad_db.pms.add(*(await models.Pm.filter(pmexs__ex=self.ex, pmexs__exid__in=pad.payments)))
498
+ return ad_db
499
+
500
+ async def cond_load( # todo: refact from Bybit Ad format to universal
501
+ self,
502
+ ad: BaseAd,
503
+ ps: models.PairSide = None,
504
+ force: bool = False,
505
+ rname: str = None,
506
+ coinex: models.CoinEx = None,
507
+ curex: models.CurEx = None,
508
+ pms_from_cond: bool = False,
509
+ ) -> tuple[models.Ad, bool]:
510
+ _sim, cid = None, None
511
+ ad_db = await models.Ad.get_or_none(exid=ad.id, maker__ex=self.ex).prefetch_related("cond")
512
+ # если точно такое условие уже есть в бд
513
+ if not (cleaned := clean(ad.remark)) or (cid := {oc[0]: ci for ci, oc in self.all_conds.items()}.get(cleaned)):
514
+ # и объява с таким ид уже есть, но у нее другое условие
515
+ if ad_db and ad_db.cond_id != cid:
516
+ # то обновляем ид ее условия
517
+ ad_db.cond_id = cid
518
+ await ad_db.save()
519
+ logging.info(f"{ad.nickName} upd cond#{ad_db.cond_id}->{cid}")
520
+ # old_cid = ad_db.cond_id # todo: solve race-condition, а пока что очищаем при каждом запуске
521
+ # if not len((old_cond := await Cond.get(id=old_cid).prefetch_related('ads')).ads):
522
+ # await old_cond.delete()
523
+ # logging.warning(f"Cond#{old_cid} deleted!")
524
+ return (ad_db or force and await self.ad_load(ad, cid, ps, coinex=coinex, curex=curex, rname=rname)), False
525
+ # если эта объява в таким ид уже есть в бд, но с другим условием (или без), а текущего условия еще нет в бд
526
+ if ad_db:
527
+ await ad_db.fetch_related("cond__ads", "maker")
528
+ if not ad_db.cond_id or (
529
+ # у измененного условия этой объявы есть другие объявы?
530
+ (rest_ads := set(ad_db.cond.ads) - {ad_db})
531
+ and
532
+ # другие объявы этого условия принадлежат другим юзерам
533
+ {ra.maker_id for ra in rest_ads} - {ad_db.maker_id}
534
+ ):
535
+ # создадим новое условие и присвоим его только текущей объяве
536
+ cond = await self.cond_new(cleaned, {int(ad.userId)})
537
+ ad_db.cond_id = cond.id
538
+ await ad_db.save()
539
+ ad_db.cond = cond
540
+ return ad_db, True
541
+ # а если других объяв со старым условием этой обявы нет, либо они все этого же юзера
542
+ # обновляем условие (в тч во всех ЕГО объявах)
543
+ ad_db.cond.last_ver = ad_db.cond.raw_txt
544
+ ad_db.cond.raw_txt = cleaned
545
+ try:
546
+ await ad_db.cond.save()
547
+ except IntegrityError as e:
548
+ raise e
549
+ await self.cond_upd(ad_db.cond, {ad_db.maker.exid})
550
+ # и подправим коэфициенты похожести нового текста
551
+ await self.fix_rel_sims(ad_db.cond_id, cleaned)
552
+ return ad_db, False
553
+
554
+ cond = await self.cond_new(cleaned, {int(ad.userId)})
555
+ ad_db = await self.ad_load(ad, cond.id, ps, coinex=coinex, curex=curex, rname=rname)
556
+ ad_db.cond = cond
557
+ return ad_db, True
558
+
559
+ async def cond_new(self, txt: str, uids: set[int]) -> models.Cond:
560
+ new_cond, _ = await models.Cond.update_or_create(raw_txt=txt)
561
+ # и максимально похожую связь для нового условия (если есть >= 60%)
562
+ await self.cond_upd(new_cond, uids)
563
+ return new_cond
564
+
565
+ async def cond_upd(self, cond: models.Cond, uids: set[int]):
566
+ self.all_conds[cond.id] = cond.raw_txt, uids
567
+ # и максимально похожую связь для нового условия (если есть >= 60%)
568
+ old_cid, sim = await self.cond_get_max_sim(cond.id, cond.raw_txt, uids)
569
+ await self.actual_sim(cond.id, old_cid, sim)
570
+
571
+ def find_in_tree(self, cid: int, old_cid: int) -> bool:
572
+ if p := self.cond_sims.get(old_cid):
573
+ if p == cid:
574
+ return True
575
+ return self.find_in_tree(cid, p)
576
+ return False
577
+
578
+ async def cond_get_max_sim(self, cid: int, txt: str, uids: set[int]) -> tuple[int | None, int | None]:
579
+ # находим все старые тексты похожие на 90% и более
580
+ if len(txt) < 15:
581
+ return None, None
582
+ sims: dict[int, int] = {}
583
+ for old_cid, (old_txt, old_uids) in self.all_conds.items():
584
+ if len(old_txt) < 15 or uids == old_uids:
585
+ continue
586
+ elif not self.can_add_sim(cid, old_cid):
587
+ continue
588
+ if sim := get_sim(txt, old_txt):
589
+ sims[old_cid] = sim
590
+ # если есть, берем самый похожий из них
591
+ if sims:
592
+ old_cid, sim = max(sims.items(), key=lambda x: x[1])
593
+ await sleep(0.3)
594
+ return old_cid, sim
595
+ return None, None
596
+
597
+ def can_add_sim(self, cid: int, old_cid: int) -> bool:
598
+ if cid == old_cid:
599
+ return False
600
+ elif self.cond_sims.get(cid) == old_cid:
601
+ return False
602
+ elif self.find_in_tree(cid, old_cid):
603
+ return False
604
+ elif self.cond_sims.get(old_cid) == cid:
605
+ return False
606
+ elif cid in self.rcond_sims.get(old_cid, {}):
607
+ return False
608
+ elif old_cid in self.rcond_sims.get(cid, {}):
609
+ return False
610
+ return True
611
+
612
+ async def fix_rel_sims(self, cid: int, new_txt: str):
613
+ for rel_sim in await models.CondSim.filter(cond_rel_id=cid).prefetch_related("cond"):
614
+ if sim := get_sim(new_txt, rel_sim.cond.raw_txt):
615
+ rel_sim.similarity = sim
616
+ await rel_sim.save()
617
+ else:
618
+ await rel_sim.delete()
619
+
620
+ async def actual_cond(self):
621
+ for curr, old in await models.CondSim.all().values_list("cond_id", "cond_rel_id"):
622
+ self.cond_sims[curr] = old
623
+ self.rcond_sims[old] |= {curr}
624
+ for cid, (txt, uids) in self.all_conds.items():
625
+ old_cid, sim = await self.cond_get_max_sim(cid, txt, uids)
626
+ await self.actual_sim(cid, old_cid, sim)
627
+ # хз бля чо это ваще
628
+ # for ad_db in await models.Ad.filter(direction__pairex__ex=self.ex).prefetch_related("cond", "maker"):
629
+ # ad = Ad(id=str(ad_db.exid), userId=str(ad_db.maker.exid), remark=ad_db.cond.raw_txt)
630
+ # await self.cond_upsert(ad, force=True)
631
+
632
+ async def actual_sim(self, cid: int, old_cid: int, sim: int):
633
+ if not sim:
634
+ return
635
+ if old_sim := await models.CondSim.get_or_none(cond_id=cid):
636
+ if old_sim.cond_rel_id != old_cid:
637
+ if sim > old_sim.similarity:
638
+ logging.warning(f"R {cid}: {old_sim.similarity}->{sim} ({old_sim.cond_rel_id}->{old_cid})")
639
+ await old_sim.update_from_dict({"similarity": sim, "old_rel_id": old_cid}).save()
640
+ self._cond_sim_upd(cid, old_cid)
641
+ elif sim != old_sim.similarity:
642
+ logging.info(f"{cid}: {old_sim.similarity}->{sim}")
643
+ await old_sim.update_from_dict({"similarity": sim}).save()
644
+ else:
645
+ await models.CondSim.create(cond_id=cid, cond_rel_id=old_cid, similarity=sim)
646
+ self._cond_sim_upd(cid, old_cid)
647
+
648
+ def _cond_sim_upd(self, cid: int, old_cid: int):
649
+ if old_old_cid := self.cond_sims.get(cid): # если старый cid уже был в дереве:
650
+ self.rcond_sims[old_old_cid].remove(cid) # удаляем из обратного
651
+ self.cond_sims[cid] = old_cid # а в прямом он автоматом переопределится, даже если и был
652
+ self.rcond_sims[old_cid] |= {cid} # ну и в обратное добавим новый
653
+
654
+ def build_tree(self):
655
+ set(self.cond_sims.keys()) | set(self.cond_sims.values())
656
+ tree = defaultdict(dict)
657
+ # Группируем родителей по детям
658
+ for child, par in self.cond_sims.items():
659
+ tree[par] |= {child: {}} # todo: make from self.rcond_sim
660
+
661
+ # Строим дерево снизу вверх
662
+ def subtree(node):
663
+ if not node:
664
+ return node
665
+ for key in node:
666
+ subnode = tree.pop(key, {})
667
+ d = subtree(subnode)
668
+ node[key] |= d # actual tree rebuilding here!
669
+ return node # todo: refact?
670
+
671
+ # Находим корни / без родителей
672
+ roots = set(self.cond_sims.values()) - set(self.cond_sims.keys())
673
+ for root in roots:
674
+ _ = subtree(tree[root])
675
+
676
+ self.tree = tree
677
+
678
+
679
+ def get_sim(s1, s2) -> int:
680
+ sim = SequenceMatcher(None, s1, s2).ratio() - 0.6
681
+ return int(sim * 10_000) if sim > 0 else 0
682
+
683
+
684
+ def clean(s) -> str:
685
+ clear = r"[^\w\s.,!?;:()\-]"
686
+ repeat = r"(.)\1{2,}"
687
+ s = re.sub(clear, "", s).lower()
688
+ s = re.sub(repeat, r"\1", s)
689
+ return s.replace("\n\n", "\n").replace(" ", " ").strip(" \n/.,!?-")
@@ -0,0 +1,10 @@
1
+ from PGram import Bot
2
+ from aiogram.types import Message
3
+
4
+
5
+ class HasAbotUid:
6
+ abot: Bot
7
+ uid: int
8
+
9
+ async def receive(self, text: str, photo: bytes = None, video: bytes = None) -> Message:
10
+ return await self.abot.send(self.uid, txt=text, photo=photo, video=video)
@@ -1,18 +1,7 @@
1
1
  from abc import abstractmethod
2
2
 
3
- from pyro_client.client.file import FileClient
4
- from xync_client.Abc.PmAgent import PmAgentClient
5
- from xync_schema.models import Actor
6
-
7
- from xync_client.Abc.Agent import BaseAgentClient
8
-
9
3
 
10
4
  class BaseInAgentClient:
11
- pmacs: dict[int, PmAgentClient] = {}
12
-
13
- def __init__(self, actor: Actor, bot: FileClient):
14
- self.agent_client: BaseAgentClient = actor.client(bot)
15
-
16
5
  @abstractmethod
17
6
  async def start_listen(self) -> bool: ...
18
7
 
@@ -4,52 +4,53 @@ from datetime import datetime, timedelta
4
4
  from decimal import Decimal
5
5
  from enum import StrEnum
6
6
 
7
- from playwright.async_api import Page, Playwright
7
+ from PGram import Bot
8
+ from playwright.async_api import Page, Browser
8
9
  from pyro_client.client.file import FileClient
9
10
  from pyro_client.client.user import UserClient
11
+ from tortoise.timezone import now
10
12
 
11
- from xync_client.loader import NET_TOKEN
12
13
  from xync_schema.enums import UserStatus
13
- from xync_schema.models import PmAgent, User
14
+ from xync_schema.models import PmAgent, User, Transfer
15
+
16
+ from xync_client.Abc.HasAbotUid import HasAbotUid
14
17
 
15
18
 
16
19
  class LoginFailedException(Exception): ...
17
20
 
18
21
 
19
- class PmAgentClient(metaclass=ABCMeta):
22
+ class PmAgentClient(HasAbotUid, metaclass=ABCMeta):
20
23
  class Pages(StrEnum):
21
24
  base = "https://host"
22
25
  LOGIN = base + "login"
23
26
  SEND = base + "send"
24
27
  OTP_LOGIN = base + "login/otp"
25
28
 
29
+ browser: Browser
26
30
  norm: str
27
31
  agent: PmAgent
28
- bot: FileClient | UserClient
32
+ ubot: FileClient | UserClient = None
29
33
  page: Page
30
34
  pages: type(StrEnum) = Pages
31
- last_page: int = 0
32
- last_active: datetime = datetime.now()
35
+ last_active: datetime = now()
36
+ with_userbot: bool = False
33
37
  _is_started: bool = False
34
38
 
35
- async def start(self, pw: Playwright, headed: bool = False, userbot: bool = False) -> "PmAgentClient":
36
- bot = FileClient(NET_TOKEN)
37
- self.bot = UserClient(self.uid, bot) if userbot else bot
38
- await self.bot.start()
39
-
40
- self.browser = await pw.chromium.launch(
41
- channel="chromium" if headed else "chromium-headless-shell", headless=not headed
42
- )
39
+ async def start(self) -> "PmAgentClient":
40
+ if self.with_userbot:
41
+ self.ubot = UserClient(self.uid)
42
+ await self.ubot.start()
43
+ # noinspection PyTypeChecker
43
44
  context = await self.browser.new_context(storage_state=self.agent.state)
44
45
  self.page = await context.new_page()
45
- await self.page.goto(self.pages.SEND) # Оптимистично переходим сразу на страницу отправки
46
+ await self.page.goto(self.pages.SEND, wait_until="commit") # Оптимистично переходим сразу на страницу отправки
46
47
  if self.page.url.startswith(self.pages.LOGIN): # Если перебросило на страницу логина
47
48
  await self._login() # Логинимся
48
49
  if not self.page.url.startswith(self.pages.SEND): # Если в итоге не удалось попасть на отправку
49
- await self.bot.send(self.norm + " not logged in!", self.uid, photo=await self.page.screenshot())
50
+ await self.receive(self.norm + " not logged in!", photo=await self.page.screenshot())
50
51
  raise LoginFailedException(f"User {self.agent.user_id} has not logged in")
51
52
  loop = get_running_loop()
52
- self.last_active = datetime.now()
53
+ self.last_active = now()
53
54
  loop.create_task(self._idle()) # Бесконечно пасёмся в фоне на странице отправки, что бы куки не протухли
54
55
  self._is_started = True
55
56
  return self
@@ -59,18 +60,21 @@ class PmAgentClient(metaclass=ABCMeta):
59
60
  async def _idle(self): # todo: не мешать другим процессам, обновлять на другой вкладке?
60
61
  while (await User.get(username_id=self.uid)).status >= UserStatus.ACTIVE:
61
62
  await self.page.wait_for_timeout(30 * 1000)
62
- if self.last_active < datetime.now() - timedelta(minutes=1):
63
- await self.page.reload()
64
- self.last_active = datetime.now()
65
- await self.bot.send(self.norm + " stoped", self.uid)
63
+ if self.last_active < now() - timedelta(minutes=1):
64
+ await self.page.reload(wait_until="commit")
65
+ self.last_active = now()
66
+ await self.receive(self.norm + " stoped")
66
67
  await self.stop()
67
68
 
68
69
  async def stop(self):
69
70
  # save state
71
+ # noinspection PyTypeChecker
70
72
  self.agent.state = await self.page.context.storage_state()
71
73
  await self.agent.save()
72
74
  # closing
73
- await self.bot.stop()
75
+ await self.abot.stop()
76
+ if self.ubot:
77
+ await self.ubot.stop()
74
78
  await self.page.context.close()
75
79
  await self.page.context.browser.close()
76
80
  self._is_started = False
@@ -79,15 +83,19 @@ class PmAgentClient(metaclass=ABCMeta):
79
83
  async def _login(self): ...
80
84
 
81
85
  @abstractmethod
82
- async def send(self, dest, amount: int, cur: str) -> tuple[int, bytes, float]: ...
86
+ async def send(self, t: Transfer) -> tuple[int, bytes] | float: ...
83
87
 
84
88
  @abstractmethod # проверка поступления определенной суммы за последние пол часа (минимум), return точную сумму
85
- async def check_in(self, amount: int | Decimal | float, cur: str, tid: str | int = None) -> float | None: ...
89
+ async def check_in(
90
+ self, amount: int | Decimal | float, cur: str, dt: datetime, tid: str | int = None
91
+ ) -> float | None: ...
86
92
 
87
93
  @abstractmethod # видео входа в аккаунт, и переход в историю поступлений за последние сутки (минимум)
88
94
  async def proof(self) -> bytes: ...
89
95
 
90
- def __init__(self, agent: PmAgent):
96
+ def __init__(self, agent: PmAgent, browser: Browser, abot: Bot):
91
97
  self.agent = agent
98
+ self.browser = browser
99
+ self.abot = abot
92
100
  self.uid = agent.user.username_id
93
101
  self.norm = agent.pm.norm