trd-utils 0.0.1__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 trd-utils might be problematic. Click here for more details.
- trd_utils/__init__.py +3 -0
- trd_utils/bx_ultra/__init__.py +6 -0
- trd_utils/bx_ultra/bx_types.py +719 -0
- trd_utils/bx_ultra/bx_ultra_client.py +436 -0
- trd_utils/bx_ultra/common_utils.py +51 -0
- trd_utils/cipher/__init__.py +44 -0
- trd_utils/html_utils/__init__.py +26 -0
- trd_utils/html_utils/html_formats.py +72 -0
- trd_utils/tradingview/__init__.py +8 -0
- trd_utils/tradingview/tradingview_client.py +130 -0
- trd_utils/tradingview/tradingview_types.py +185 -0
- trd_utils/types_helper/__init__.py +4 -0
- trd_utils/types_helper/base_model.py +150 -0
- trd_utils-0.0.1.dist-info/LICENSE +21 -0
- trd_utils-0.0.1.dist-info/METADATA +28 -0
- trd_utils-0.0.1.dist-info/RECORD +17 -0
- trd_utils-0.0.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"""BxUltra exchange subclass"""
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import uuid
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from .common_utils import do_ultra_ss
|
|
14
|
+
from .bx_types import (
|
|
15
|
+
AssetsInfoResponse,
|
|
16
|
+
ContractsListResponse,
|
|
17
|
+
CopyTraderTradePositionsResponse,
|
|
18
|
+
HintListResponse,
|
|
19
|
+
HomePageResponse,
|
|
20
|
+
HotSearchResponse,
|
|
21
|
+
QuotationRankResponse,
|
|
22
|
+
SearchCopyTraderCondition,
|
|
23
|
+
SearchCopyTradersResponse,
|
|
24
|
+
UserFavoriteQuotationResponse,
|
|
25
|
+
ZenDeskABStatusResponse,
|
|
26
|
+
ZoneModuleListResponse,
|
|
27
|
+
)
|
|
28
|
+
from ..cipher import AESCipher
|
|
29
|
+
|
|
30
|
+
PLATFORM_ID_ANDROID = "10"
|
|
31
|
+
PLATFORM_ID_WEB = "30"
|
|
32
|
+
PLATFORM_ID_TG = "100"
|
|
33
|
+
|
|
34
|
+
ANDROID_DEVICE_BRAND = "SM-N976N"
|
|
35
|
+
WEB_DEVICE_BRAND = "Windows 10_Chrome_127.0.0.0"
|
|
36
|
+
EDGE_DEVICE_BRAND = "Windows 10_Edge_131.0.0.0"
|
|
37
|
+
|
|
38
|
+
ANDROID_APP_VERSION = "4.28.3"
|
|
39
|
+
WEB_APP_VERSION = "4.78.12"
|
|
40
|
+
TG_APP_VERSION = "5.0.15"
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class BXUltraClient:
|
|
46
|
+
###########################################################
|
|
47
|
+
# region client parameters
|
|
48
|
+
we_api_base_host: str = "\u0061pi-\u0061pp.w\u0065-\u0061pi.com"
|
|
49
|
+
we_api_base_url: str = "https://\u0061pi-\u0061pp.w\u0065-\u0061pi.com/\u0061pi"
|
|
50
|
+
|
|
51
|
+
original_base_host: str = "https://\u0062ing\u0078.co\u006d"
|
|
52
|
+
|
|
53
|
+
qq_os_base_host: str = "https://\u0061pi-\u0061pp.\u0071\u0071-os.com"
|
|
54
|
+
qq_os_base_url: str = "https://\u0061pi-\u0061pp.\u0071\u0071-os.com/\u0061pi"
|
|
55
|
+
|
|
56
|
+
device_id: str = None
|
|
57
|
+
trace_id: str = None
|
|
58
|
+
app_version: str = "4.28.3"
|
|
59
|
+
platform_id: str = "10"
|
|
60
|
+
install_channel: str = "officialAPK"
|
|
61
|
+
channel_header: str = "officialAPK"
|
|
62
|
+
origin_header: str = "https://\u0062ing\u0078.co\u006d"
|
|
63
|
+
authorization_token: str = None
|
|
64
|
+
app_id: str = "30004"
|
|
65
|
+
main_app_id: str = "10009"
|
|
66
|
+
trade_env: str = "real"
|
|
67
|
+
timezone: str = "3"
|
|
68
|
+
os_version: str = "7.1.2"
|
|
69
|
+
device_brand: str = "SM-N976N"
|
|
70
|
+
platform_lang: str = "en"
|
|
71
|
+
sys_lang: str = "en"
|
|
72
|
+
user_agent: str = "okhttp/4.12.0"
|
|
73
|
+
x_requested_with: str = None
|
|
74
|
+
httpx_client: httpx.AsyncClient = None
|
|
75
|
+
account_name: str = "default"
|
|
76
|
+
|
|
77
|
+
_fav_letter: str = "^"
|
|
78
|
+
|
|
79
|
+
# endregion
|
|
80
|
+
###########################################################
|
|
81
|
+
# region client constructor
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
account_name: str = "default",
|
|
85
|
+
platform_id: str = PLATFORM_ID_ANDROID,
|
|
86
|
+
device_brand: str = ANDROID_DEVICE_BRAND,
|
|
87
|
+
app_version: str = ANDROID_APP_VERSION,
|
|
88
|
+
http_verify: bool = True,
|
|
89
|
+
fav_letter: str = "^",
|
|
90
|
+
):
|
|
91
|
+
self.httpx_client = httpx.AsyncClient(
|
|
92
|
+
verify=http_verify, http2=True, http1=False
|
|
93
|
+
)
|
|
94
|
+
self.account_name = account_name
|
|
95
|
+
self.platform_id = platform_id
|
|
96
|
+
self.device_brand = device_brand
|
|
97
|
+
self.app_version = app_version
|
|
98
|
+
self._fav_letter = fav_letter
|
|
99
|
+
|
|
100
|
+
self.read_from_session_file(f"{self.account_name}.bx")
|
|
101
|
+
|
|
102
|
+
# endregion
|
|
103
|
+
###########################################################
|
|
104
|
+
# region api/coin/v1
|
|
105
|
+
async def get_zone_module_info(
|
|
106
|
+
self, only_one_position: int = 0, biz_type: int = 10
|
|
107
|
+
) -> ZoneModuleListResponse:
|
|
108
|
+
"""
|
|
109
|
+
Fetches and returns zone module info from the API.
|
|
110
|
+
Available zones are: All, Forex, Indices, MEME, Elon-inspired,
|
|
111
|
+
Innovation, AI Agent, BTC Ecosystem, TON Ecosystem, Commodities,
|
|
112
|
+
GameFi, Fan Tokens , Layer1 & Layer2, SOL Ecosystem, RWA, LST, DePin, AI
|
|
113
|
+
"""
|
|
114
|
+
params = {
|
|
115
|
+
"bizType": f"{biz_type}",
|
|
116
|
+
}
|
|
117
|
+
headers = self.get_headers(params)
|
|
118
|
+
headers["Only_one_position"] = f"{only_one_position}"
|
|
119
|
+
response = await self.httpx_client.get(
|
|
120
|
+
f"{self.we_api_base_url}/coin/v1/zone/module-info",
|
|
121
|
+
headers=headers,
|
|
122
|
+
params=params,
|
|
123
|
+
)
|
|
124
|
+
return ZoneModuleListResponse.deserialize(response.json(parse_float=Decimal))
|
|
125
|
+
|
|
126
|
+
async def get_user_favorite_quotation(
|
|
127
|
+
self, only_one_position: int = 0, biz_type: int = 1
|
|
128
|
+
):
|
|
129
|
+
params = {
|
|
130
|
+
"bizType": f"{biz_type}",
|
|
131
|
+
}
|
|
132
|
+
headers = self.get_headers(params)
|
|
133
|
+
headers["Only_one_position"] = f"{only_one_position}"
|
|
134
|
+
response = await self.httpx_client.get(
|
|
135
|
+
f"{self.we_api_base_url}/coin/v1/user/favorite/quotation",
|
|
136
|
+
headers=headers,
|
|
137
|
+
params=params,
|
|
138
|
+
)
|
|
139
|
+
return UserFavoriteQuotationResponse.deserialize(
|
|
140
|
+
response.json(parse_float=Decimal)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
async def get_quotation_rank(self, only_one_position: int = 0, order_flag: int = 0):
|
|
144
|
+
params = {
|
|
145
|
+
"orderFlag": f"{order_flag}",
|
|
146
|
+
}
|
|
147
|
+
headers = self.get_headers(params)
|
|
148
|
+
headers["Only_one_position"] = f"{only_one_position}"
|
|
149
|
+
response = await self.httpx_client.get(
|
|
150
|
+
f"{self.we_api_base_url}/coin/v1/rank/quotation-rank",
|
|
151
|
+
headers=headers,
|
|
152
|
+
params=params,
|
|
153
|
+
)
|
|
154
|
+
return QuotationRankResponse.deserialize(response.json(parse_float=Decimal))
|
|
155
|
+
|
|
156
|
+
async def get_hot_search(self, only_one_position: int = 0, biz_type: int = 30):
|
|
157
|
+
params = {
|
|
158
|
+
"bizType": f"{biz_type}",
|
|
159
|
+
}
|
|
160
|
+
headers = self.get_headers(params)
|
|
161
|
+
headers["Only_one_position"] = f"{only_one_position}"
|
|
162
|
+
response = await self.httpx_client.get(
|
|
163
|
+
f"{self.we_api_base_url}/coin/v1/quotation/hot-search",
|
|
164
|
+
headers=headers,
|
|
165
|
+
params=params,
|
|
166
|
+
)
|
|
167
|
+
return HotSearchResponse.deserialize(response.json(parse_float=Decimal))
|
|
168
|
+
|
|
169
|
+
async def get_homepage(self, only_one_position: int = 0, biz_type: int = 30):
|
|
170
|
+
params = {
|
|
171
|
+
"biz-type": f"{biz_type}",
|
|
172
|
+
}
|
|
173
|
+
headers = self.get_headers(params)
|
|
174
|
+
headers["Only_one_position"] = f"{only_one_position}"
|
|
175
|
+
response = await self.httpx_client.get(
|
|
176
|
+
f"{self.we_api_base_url}/coin/v1/discovery/homepage",
|
|
177
|
+
headers=headers,
|
|
178
|
+
params=params,
|
|
179
|
+
)
|
|
180
|
+
return HomePageResponse.deserialize(response.json(parse_float=Decimal))
|
|
181
|
+
|
|
182
|
+
# endregion
|
|
183
|
+
###########################################################
|
|
184
|
+
# region customer
|
|
185
|
+
async def get_zendesk_ab_status(self):
|
|
186
|
+
headers = self.get_headers()
|
|
187
|
+
response = await self.httpx_client.get(
|
|
188
|
+
f"{self.we_api_base_url}/customer/v1/zendesk/ab-status",
|
|
189
|
+
headers=headers,
|
|
190
|
+
)
|
|
191
|
+
return ZenDeskABStatusResponse.deserialize(response.json(parse_float=Decimal))
|
|
192
|
+
|
|
193
|
+
# endregion
|
|
194
|
+
###########################################################
|
|
195
|
+
# region platform-tool
|
|
196
|
+
async def get_hint_list(self) -> HintListResponse:
|
|
197
|
+
headers = self.get_headers()
|
|
198
|
+
response = await self.httpx_client.get(
|
|
199
|
+
f"{self.we_api_base_url}/platform-tool/v1/hint/list",
|
|
200
|
+
headers=headers,
|
|
201
|
+
)
|
|
202
|
+
return HintListResponse.deserialize(response.json(parse_float=Decimal))
|
|
203
|
+
|
|
204
|
+
# endregion
|
|
205
|
+
###########################################################
|
|
206
|
+
# region asset-manager
|
|
207
|
+
async def get_assets_info(self) -> AssetsInfoResponse:
|
|
208
|
+
headers = self.get_headers(needs_auth=True)
|
|
209
|
+
response = await self.httpx_client.get(
|
|
210
|
+
f"{self.we_api_base_url}/asset-manager/v1/assets/account-total-overview",
|
|
211
|
+
headers=headers,
|
|
212
|
+
)
|
|
213
|
+
return AssetsInfoResponse.deserialize(response.json(parse_float=Decimal))
|
|
214
|
+
|
|
215
|
+
# endregion
|
|
216
|
+
###########################################################
|
|
217
|
+
# region contract
|
|
218
|
+
async def get_contract_list(
|
|
219
|
+
self,
|
|
220
|
+
quotation_coin_id: int = -1,
|
|
221
|
+
margin_type: int = -1,
|
|
222
|
+
page_size: int = 20,
|
|
223
|
+
page_id: int = 0,
|
|
224
|
+
margin_coin_name: str = "",
|
|
225
|
+
create_type: str = -1,
|
|
226
|
+
) -> ContractsListResponse:
|
|
227
|
+
params = {
|
|
228
|
+
"quotationCoinId": f"{quotation_coin_id}",
|
|
229
|
+
"marginType": f"{margin_type}",
|
|
230
|
+
"pageSize": f"{page_size}",
|
|
231
|
+
"pageId": f"{page_id}",
|
|
232
|
+
"createType": f"{create_type}",
|
|
233
|
+
}
|
|
234
|
+
if margin_coin_name:
|
|
235
|
+
params["marginCoinName"] = margin_coin_name
|
|
236
|
+
headers = self.get_headers(params, needs_auth=True)
|
|
237
|
+
response = await self.httpx_client.get(
|
|
238
|
+
f"{self.we_api_base_url}/v4/contract/order/hold",
|
|
239
|
+
headers=headers,
|
|
240
|
+
params=params,
|
|
241
|
+
)
|
|
242
|
+
return ContractsListResponse.deserialize(response.json(parse_float=Decimal))
|
|
243
|
+
|
|
244
|
+
# endregion
|
|
245
|
+
###########################################################
|
|
246
|
+
# region copy-trade-facade
|
|
247
|
+
async def get_copy_trade_trader_positions(
|
|
248
|
+
self,
|
|
249
|
+
uid: str,
|
|
250
|
+
api_identity: str,
|
|
251
|
+
page_size: int = 20,
|
|
252
|
+
page_id: int = 0,
|
|
253
|
+
copy_trade_label_type: int = 1,
|
|
254
|
+
) -> CopyTraderTradePositionsResponse:
|
|
255
|
+
params = {
|
|
256
|
+
"uid": f"{uid}",
|
|
257
|
+
"apiIdentity": f"{api_identity}",
|
|
258
|
+
"pageSize": f"{page_size}",
|
|
259
|
+
"pageId": f"{page_id}",
|
|
260
|
+
"copyTradeLabelType": f"{copy_trade_label_type}",
|
|
261
|
+
}
|
|
262
|
+
headers = self.get_headers(params)
|
|
263
|
+
response = await self.httpx_client.get(
|
|
264
|
+
f"{self.we_api_base_url}/copy-trade-facade/v2/real/trader/positions",
|
|
265
|
+
headers=headers,
|
|
266
|
+
params=params,
|
|
267
|
+
)
|
|
268
|
+
return CopyTraderTradePositionsResponse.deserialize(
|
|
269
|
+
response.json(parse_float=Decimal)
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
async def search_copy_traders(
|
|
273
|
+
self,
|
|
274
|
+
exchange_id: int = 2,
|
|
275
|
+
nick_name: str = "",
|
|
276
|
+
conditions: list[SearchCopyTraderCondition] = None,
|
|
277
|
+
page_id: int = 0,
|
|
278
|
+
page_size: int = 20,
|
|
279
|
+
sort: str = "comprehensive",
|
|
280
|
+
order: str = "desc",
|
|
281
|
+
) -> SearchCopyTradersResponse:
|
|
282
|
+
params = {
|
|
283
|
+
"pageId": f"{page_id}",
|
|
284
|
+
"pageSize": f"{page_size}",
|
|
285
|
+
"sort": sort,
|
|
286
|
+
"order": order,
|
|
287
|
+
}
|
|
288
|
+
if conditions is None:
|
|
289
|
+
conditions = [
|
|
290
|
+
{"key": "exchangeId", "selected": "2", "type": "singleSelect"}
|
|
291
|
+
]
|
|
292
|
+
else:
|
|
293
|
+
conditions = [x.to_dict() for x in conditions]
|
|
294
|
+
|
|
295
|
+
payload = {
|
|
296
|
+
"conditions": conditions,
|
|
297
|
+
"exchangeId": f"{exchange_id}",
|
|
298
|
+
"nickName": nick_name,
|
|
299
|
+
}
|
|
300
|
+
headers = self.get_headers(payload)
|
|
301
|
+
response = await self.httpx_client.post(
|
|
302
|
+
f"{self.we_api_base_url}/v6/copy-trade/search/search",
|
|
303
|
+
headers=headers,
|
|
304
|
+
params=params,
|
|
305
|
+
content=json.dumps(payload, separators=(",", ":"), sort_keys=True),
|
|
306
|
+
)
|
|
307
|
+
return SearchCopyTradersResponse.deserialize(response.json(parse_float=Decimal))
|
|
308
|
+
|
|
309
|
+
# endregion
|
|
310
|
+
###########################################################
|
|
311
|
+
# region welfare
|
|
312
|
+
async def do_daily_check_in(self):
|
|
313
|
+
headers = self.get_headers(needs_auth=True)
|
|
314
|
+
response = await self.httpx_client.post(
|
|
315
|
+
f"{self.original_base_host}/api/act-operation/v1/welfare/sign-in/do",
|
|
316
|
+
headers=headers,
|
|
317
|
+
content="",
|
|
318
|
+
)
|
|
319
|
+
return response.json()
|
|
320
|
+
|
|
321
|
+
# endregion
|
|
322
|
+
###########################################################
|
|
323
|
+
# region client helper methods
|
|
324
|
+
def get_headers(self, payload=None, needs_auth: bool = False) -> dict:
|
|
325
|
+
the_timestamp = int(time.time() * 1000)
|
|
326
|
+
the_headers = {
|
|
327
|
+
"Host": self.we_api_base_host,
|
|
328
|
+
"Content-Type": "application/json",
|
|
329
|
+
"Mainappid": self.main_app_id,
|
|
330
|
+
"Accept": "application/json",
|
|
331
|
+
"Origin": self.origin_header,
|
|
332
|
+
"Traceid": self.trace_id,
|
|
333
|
+
"App_version": self.app_version,
|
|
334
|
+
"Platformid": self.platform_id,
|
|
335
|
+
"Device_id": self.device_id,
|
|
336
|
+
"Device_brand": self.device_brand,
|
|
337
|
+
"Channel": self.channel_header,
|
|
338
|
+
"Appid": self.app_id,
|
|
339
|
+
"Trade_env": self.trade_env,
|
|
340
|
+
"Timezone": self.timezone,
|
|
341
|
+
"Lang": self.platform_lang,
|
|
342
|
+
"Syslang": self.sys_lang,
|
|
343
|
+
"Sign": do_ultra_ss(
|
|
344
|
+
e_param=None,
|
|
345
|
+
se_param=None,
|
|
346
|
+
le_param=None,
|
|
347
|
+
timestamp=the_timestamp,
|
|
348
|
+
trace_id=self.trace_id,
|
|
349
|
+
device_id=self.device_id,
|
|
350
|
+
platform_id=self.platform_id,
|
|
351
|
+
app_version=self.app_version,
|
|
352
|
+
payload_data=payload,
|
|
353
|
+
),
|
|
354
|
+
"Timestamp": f"{the_timestamp}",
|
|
355
|
+
# 'Accept-Encoding': 'gzip, deflate',
|
|
356
|
+
"User-Agent": self.user_agent,
|
|
357
|
+
"Connection": "close",
|
|
358
|
+
"appsiteid": "0",
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if self.x_requested_with:
|
|
362
|
+
the_headers["X-Requested-With"] = self.x_requested_with
|
|
363
|
+
|
|
364
|
+
if needs_auth:
|
|
365
|
+
the_headers["Authorization"] = f"Bearer {self.authorization_token}"
|
|
366
|
+
return the_headers
|
|
367
|
+
|
|
368
|
+
async def aclose(self) -> None:
|
|
369
|
+
await self.httpx_client.aclose()
|
|
370
|
+
logger.info("BXUltraClient closed")
|
|
371
|
+
return True
|
|
372
|
+
|
|
373
|
+
def read_from_session_file(self, file_path: str) -> None:
|
|
374
|
+
"""
|
|
375
|
+
Reads from session file; if it doesn't exist, creates it.
|
|
376
|
+
"""
|
|
377
|
+
# check if path exists
|
|
378
|
+
target_path = Path(file_path)
|
|
379
|
+
if not target_path.exists():
|
|
380
|
+
return self._save_session_file(file_path=file_path)
|
|
381
|
+
|
|
382
|
+
aes = AESCipher(key=f"bx_{self.account_name}_bx", fav_letter=self._fav_letter)
|
|
383
|
+
content = aes.decrypt(target_path.read_text()).decode("utf-8")
|
|
384
|
+
json_data: dict = json.loads(content)
|
|
385
|
+
|
|
386
|
+
self.device_id = json_data.get("device_id", self.device_id)
|
|
387
|
+
self.trace_id = json_data.get("trace_id", self.trace_id)
|
|
388
|
+
self.app_version = json_data.get("app_version", self.app_version)
|
|
389
|
+
self.platform_id = json_data.get("platform_id", self.platform_id)
|
|
390
|
+
self.install_channel = json_data.get("install_channel", self.install_channel)
|
|
391
|
+
self.channel_header = json_data.get("channel_header", self.channel_header)
|
|
392
|
+
self.authorization_token = json_data.get(
|
|
393
|
+
"authorization_token", self.authorization_token
|
|
394
|
+
)
|
|
395
|
+
self.app_id = json_data.get("app_id", self.app_id)
|
|
396
|
+
self.trade_env = json_data.get("trade_env", self.trade_env)
|
|
397
|
+
self.timezone = json_data.get("timezone", self.timezone)
|
|
398
|
+
self.os_version = json_data.get("os_version", self.os_version)
|
|
399
|
+
self.device_brand = json_data.get("device_brand", self.device_brand)
|
|
400
|
+
self.platform_lang = json_data.get("platform_lang", self.platform_lang)
|
|
401
|
+
self.sys_lang = json_data.get("sys_lang", self.sys_lang)
|
|
402
|
+
self.user_agent = json_data.get("user_agent", self.user_agent)
|
|
403
|
+
|
|
404
|
+
def _save_session_file(self, file_path: str) -> None:
|
|
405
|
+
"""
|
|
406
|
+
Saves current information to the session file.
|
|
407
|
+
"""
|
|
408
|
+
if not self.device_id:
|
|
409
|
+
self.device_id = uuid.uuid4().hex.replace("-", "") + "##"
|
|
410
|
+
|
|
411
|
+
if not self.trace_id:
|
|
412
|
+
self.trace_id = uuid.uuid4().hex.replace("-", "")
|
|
413
|
+
|
|
414
|
+
json_data = {
|
|
415
|
+
"device_id": self.device_id,
|
|
416
|
+
"trace_id": self.trace_id,
|
|
417
|
+
"app_version": self.app_version,
|
|
418
|
+
"platform_id": self.platform_id,
|
|
419
|
+
"install_channel": self.install_channel,
|
|
420
|
+
"channel_header": self.channel_header,
|
|
421
|
+
"authorization_token": self.authorization_token,
|
|
422
|
+
"app_id": self.app_id,
|
|
423
|
+
"trade_env": self.trade_env,
|
|
424
|
+
"timezone": self.timezone,
|
|
425
|
+
"os_version": self.os_version,
|
|
426
|
+
"device_brand": self.device_brand,
|
|
427
|
+
"platform_lang": self.platform_lang,
|
|
428
|
+
"sys_lang": self.sys_lang,
|
|
429
|
+
"user_agent": self.user_agent,
|
|
430
|
+
}
|
|
431
|
+
aes = AESCipher(key=f"bx_{self.account_name}_bx", fav_letter=self._fav_letter)
|
|
432
|
+
target_path = Path(file_path)
|
|
433
|
+
target_path.write_text(aes.encrypt(json.dumps(json_data)))
|
|
434
|
+
|
|
435
|
+
# endregion
|
|
436
|
+
###########################################################
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
default_e: str = (
|
|
6
|
+
"\u0039\u0035\u0064\u0036\u0035\u0063\u0037\u0033\u0064\u0063\u0035"
|
|
7
|
+
+ "\u0063\u0034\u0033\u0037"
|
|
8
|
+
)
|
|
9
|
+
default_se: str = "\u0030\u0061\u0065\u0039\u0030\u0031\u0038\u0066\u0062\u0037"
|
|
10
|
+
default_le: str = "\u0066\u0032\u0065\u0061\u0062\u0036\u0039"
|
|
11
|
+
|
|
12
|
+
long_accept_header1: str = (
|
|
13
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,"
|
|
14
|
+
+ "image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def do_ultra_ss(
|
|
19
|
+
e_param: str,
|
|
20
|
+
se_param: str,
|
|
21
|
+
le_param: str,
|
|
22
|
+
timestamp: int,
|
|
23
|
+
trace_id: str,
|
|
24
|
+
device_id: str,
|
|
25
|
+
platform_id: str,
|
|
26
|
+
app_version: str,
|
|
27
|
+
payload_data: str = None,
|
|
28
|
+
) -> str:
|
|
29
|
+
if not e_param:
|
|
30
|
+
e_param = default_e
|
|
31
|
+
|
|
32
|
+
if not se_param:
|
|
33
|
+
se_param = default_se
|
|
34
|
+
|
|
35
|
+
if not le_param:
|
|
36
|
+
le_param = default_le
|
|
37
|
+
|
|
38
|
+
first_part = f"{e_param}{se_param}{le_param}{timestamp}{trace_id}"
|
|
39
|
+
if not payload_data:
|
|
40
|
+
payload_data = "{}"
|
|
41
|
+
elif not isinstance(payload_data, str):
|
|
42
|
+
# convert to json
|
|
43
|
+
payload_data = json.dumps(payload_data, separators=(",", ":"), sort_keys=True)
|
|
44
|
+
|
|
45
|
+
if not trace_id:
|
|
46
|
+
trace_id = uuid.uuid4().hex.replace("-", "")
|
|
47
|
+
|
|
48
|
+
whole_parts = f"{first_part}{device_id}{platform_id}{app_version}{payload_data}"
|
|
49
|
+
|
|
50
|
+
# do SHA256
|
|
51
|
+
return hashlib.sha256(whole_parts.encode()).hexdigest().upper()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
|
|
2
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
3
|
+
from cryptography.hazmat.backends import default_backend
|
|
4
|
+
from cryptography.hazmat.primitives import padding
|
|
5
|
+
from base64 import b64encode, b64decode
|
|
6
|
+
from os import urandom
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AESCipher:
|
|
10
|
+
def __init__(self, key: str, fav_letter: str):
|
|
11
|
+
if len(key) > 32:
|
|
12
|
+
raise ValueError("Key length must be 32 bytes or less")
|
|
13
|
+
elif len(key) < 32:
|
|
14
|
+
key = key.ljust(len(key) + (32 - len(key) % 32), fav_letter)
|
|
15
|
+
|
|
16
|
+
key = key.encode('utf-8')
|
|
17
|
+
if len(key) != 32:
|
|
18
|
+
raise ValueError("Key length must be 32 bytes")
|
|
19
|
+
|
|
20
|
+
self.key = key
|
|
21
|
+
self.backend = default_backend()
|
|
22
|
+
|
|
23
|
+
def encrypt(self, plaintext):
|
|
24
|
+
if isinstance(plaintext, str):
|
|
25
|
+
plaintext = plaintext.encode('utf-8')
|
|
26
|
+
|
|
27
|
+
iv = urandom(16)
|
|
28
|
+
cipher = Cipher(algorithms.AES(self.key), modes.CBC(iv), backend=self.backend)
|
|
29
|
+
padder = padding.PKCS7(128).padder()
|
|
30
|
+
padded_data = padder.update(plaintext) + padder.finalize()
|
|
31
|
+
encryptor = cipher.encryptor()
|
|
32
|
+
ciphertext = encryptor.update(padded_data) + encryptor.finalize()
|
|
33
|
+
return b64encode(iv + ciphertext).decode('utf-8')
|
|
34
|
+
|
|
35
|
+
def decrypt(self, b64_encrypted_data):
|
|
36
|
+
encrypted_data = b64decode(b64_encrypted_data)
|
|
37
|
+
iv = encrypted_data[:16]
|
|
38
|
+
ciphertext = encrypted_data[16:]
|
|
39
|
+
cipher = Cipher(algorithms.AES(self.key), modes.CBC(iv), backend=self.backend)
|
|
40
|
+
unpadder = padding.PKCS7(128).unpadder()
|
|
41
|
+
decryptor = cipher.decryptor()
|
|
42
|
+
padded_plaintext = decryptor.update(ciphertext) + decryptor.finalize()
|
|
43
|
+
plaintext = unpadder.update(padded_plaintext) + unpadder.finalize()
|
|
44
|
+
return plaintext
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
|
|
2
|
+
from .html_formats import (
|
|
3
|
+
get_html_normal,
|
|
4
|
+
html_normal,
|
|
5
|
+
html_mono,
|
|
6
|
+
html_in_parenthesis,
|
|
7
|
+
html_bold,
|
|
8
|
+
html_italic,
|
|
9
|
+
html_link,
|
|
10
|
+
html_code_snippets,
|
|
11
|
+
html_pre,
|
|
12
|
+
html_spoiler
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"get_html_normal",
|
|
17
|
+
"html_normal",
|
|
18
|
+
"html_mono",
|
|
19
|
+
"html_in_parenthesis",
|
|
20
|
+
"html_bold",
|
|
21
|
+
"html_italic",
|
|
22
|
+
"html_link",
|
|
23
|
+
"html_code_snippets",
|
|
24
|
+
"html_pre",
|
|
25
|
+
"html_spoiler"
|
|
26
|
+
]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import html
|
|
2
|
+
|
|
3
|
+
def camel_to_snake(str_value: str) -> str:
|
|
4
|
+
"""
|
|
5
|
+
Convert CamelCase to snake_case.
|
|
6
|
+
https://stackoverflow.com/a/44969381/16518789
|
|
7
|
+
"""
|
|
8
|
+
return ''.join(['_'+c.lower() if c.isupper() else c for c in str_value]).lstrip('_')
|
|
9
|
+
|
|
10
|
+
def to_camel_case(snake_str: str) -> str:
|
|
11
|
+
"""
|
|
12
|
+
Convert snake_case to CamelCase.
|
|
13
|
+
https://stackoverflow.com/a/19053800/16518789
|
|
14
|
+
"""
|
|
15
|
+
return "".join(x.capitalize() for x in snake_str.lower().split("_"))
|
|
16
|
+
|
|
17
|
+
def to_lower_camel_case(snake_str: str) -> str:
|
|
18
|
+
# We capitalize the first letter of each component except the first one
|
|
19
|
+
# with the 'capitalize' method and join them together.
|
|
20
|
+
camel_string = to_camel_case(snake_str)
|
|
21
|
+
return snake_str[0].lower() + camel_string[1:]
|
|
22
|
+
|
|
23
|
+
def get_html_normal(*argv) -> str:
|
|
24
|
+
if argv is None or len(argv) == 0:
|
|
25
|
+
return ""
|
|
26
|
+
|
|
27
|
+
my_str = ""
|
|
28
|
+
for value in argv:
|
|
29
|
+
if not value:
|
|
30
|
+
continue
|
|
31
|
+
if isinstance(value, str):
|
|
32
|
+
my_str += value
|
|
33
|
+
else:
|
|
34
|
+
my_str += str(value)
|
|
35
|
+
|
|
36
|
+
return my_str
|
|
37
|
+
|
|
38
|
+
def html_normal(value, *argv) -> str:
|
|
39
|
+
my_str = html.escape(str(value))
|
|
40
|
+
for value in argv:
|
|
41
|
+
if isinstance(value, str):
|
|
42
|
+
my_str += value
|
|
43
|
+
return my_str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def html_mono(value, *argv) -> str:
|
|
47
|
+
return f"<code>{html.escape(str(value))}</code>" + get_html_normal(*argv)
|
|
48
|
+
|
|
49
|
+
def html_in_parenthesis(value) -> str:
|
|
50
|
+
if not value:
|
|
51
|
+
return ": "
|
|
52
|
+
return f" ({html.escape(str(value))}): "
|
|
53
|
+
|
|
54
|
+
def html_bold(value, *argv) -> str:
|
|
55
|
+
return f"<b>{html.escape(str(value))}</b>" + get_html_normal(*argv)
|
|
56
|
+
|
|
57
|
+
def html_italic(value, *argv) -> str:
|
|
58
|
+
return f"<i>{html.escape(str(value))}</i>" + get_html_normal(*argv)
|
|
59
|
+
|
|
60
|
+
def html_link(value, link: str, *argv) -> str:
|
|
61
|
+
if not isinstance(link, str) or len(link) == 0:
|
|
62
|
+
return html_mono(value, *argv)
|
|
63
|
+
return f"<a href={html.escape(link)}>{html.escape(str(value))}</a>" + get_html_normal(*argv)
|
|
64
|
+
|
|
65
|
+
def html_code_snippets(value, language: str, *argv):
|
|
66
|
+
return html_pre(value, language, *argv)
|
|
67
|
+
|
|
68
|
+
def html_pre(value, language: str, *argv):
|
|
69
|
+
return f"<pre language={html.escape(language)}>{html.escape(str(value))}</pre>" + get_html_normal(*argv)
|
|
70
|
+
|
|
71
|
+
def html_spoiler(value, *argv):
|
|
72
|
+
return f"<spoiler>{html.escape(str(value))}</spoiler>" + get_html_normal(*argv)
|