Skip to content

Commit 83ffede

Browse files
committed
ui_metrics
1 parent bd2aa7b commit 83ffede

File tree

8 files changed

+88
-55
lines changed

8 files changed

+88
-55
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,10 @@ client = Client('en-US')
8383

8484
async def main():
8585
await client.login(
86-
auth_info_1=USERNAME ,
86+
auth_info_1=USERNAME,
8787
auth_info_2=EMAIL,
88-
password=PASSWORD
88+
password=PASSWORD,
89+
cookies_file='cookies.json'
8990
)
9091

9192
asyncio.run(main())

docs/requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ beautifulsoup4
55
pyotp
66
lxml
77
webvtt-py
8-
m3u8
8+
m3u8
9+
js2py

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ beautifulsoup4
44
pyotp
55
lxml
66
webvtt-py
7-
m3u8
7+
m3u8
8+
js2py

twikit/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
A Python library for interacting with the Twitter API.
88
"""
99

10-
__version__ = '2.3.0'
10+
__version__ = '2.3.1'
1111

1212
import asyncio
1313
import os

twikit/client/client.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import io
55
import json
66
import os
7-
import re
87

98
import warnings
109
from functools import partial
@@ -40,13 +39,13 @@
4039
)
4140
from ..geo import Place, _places_from_response
4241
from ..group import Group, GroupMessage
43-
from ..js_metrics import run_js_metrics
4442
from ..list import List
4543
from ..message import Message
4644
from ..notification import Notification
4745
from ..streaming import Payload, StreamingSession, _payload_from_data
4846
from ..trend import Location, PlaceTrend, PlaceTrends, Trend
4947
from ..tweet import CommunityNote, Poll, ScheduledTweet, Tweet, tweet_from_data
48+
from ..ui_metrics import solve_ui_metrics
5049
from ..user import User
5150
from ..utils import (
5251
Flow,
@@ -291,7 +290,7 @@ async def login(
291290
password: str,
292291
totp_secret: str | None = None,
293292
cookies_file: str | None = None,
294-
enable_ui_metrics: bool = False
293+
enable_ui_metrics: bool = True
295294
) -> dict:
296295
"""
297296
Logs into the account using the specified login information.
@@ -319,11 +318,9 @@ async def login(
319318
The file path used for storing and loading cookies.
320319
If the specified file exists, cookies will be loaded from it, potentially bypassing the login process.
321320
After a successful login, cookies will be saved to this file for future use.
322-
enable_ui_metrics : :class:`bool`, default=False
323-
If set to True, obfuscated ui_metrics function will be executed using JSDom,
324-
and the results will be sent to the API. Enabling this may reduce the risk of account suspension.
325-
To use this feature, Node.js and JSDom must be installed.
326-
If Node.js is available in your environment, enabling this option is recommended.
321+
enable_ui_metrics : :class:`bool`, default=True
322+
If set to True, obfuscated ui_metrics function will be executed using js2py,
323+
and the result will be sent to the API. Enabling this may reduce the risk of account suspension.
327324
328325
Examples
329326
--------
@@ -399,7 +396,7 @@ async def login(
399396
await flow.sso_init('apple')
400397

401398
if enable_ui_metrics:
402-
ui_metrics_response = run_js_metrics(
399+
ui_metrics_response = solve_ui_metrics(
403400
await self._ui_metrics()
404401
)
405402
else:

twikit/js_metrics/__init__.py

Lines changed: 0 additions & 41 deletions
This file was deleted.

twikit/ui_metrics/__init__.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import re
2+
3+
import js2py
4+
5+
from .dom import MockDocument
6+
7+
FUNCTION_PATTERN = re.compile(r'function [a-zA-Z]+\(\) ({.+})')
8+
EQUAL_PATTERN = re.compile(r'(![a-zA-Z]{5}\|\|[a-zA-Z]{5})==([a-zA-Z]{5})')
9+
10+
11+
def solve_ui_metrics(ui_metrics: str) -> str:
12+
match = FUNCTION_PATTERN.search(ui_metrics)
13+
if not match:
14+
raise ValueError()
15+
inner_function = match.group(1)
16+
# Replace '==' with '===' to ensure proper object comparison in js2py
17+
inner_function = EQUAL_PATTERN.sub(r'\1===\2', inner_function)
18+
context = js2py.EvalJs()
19+
context.document = MockDocument()
20+
function = 'function main()' + inner_function
21+
context.eval(function)
22+
return str(context.main())

twikit/ui_metrics/dom.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from __future__ import annotations
2+
3+
from typing import Callable
4+
5+
6+
class MockElement:
7+
def __init__(self, tag_name, document: MockDocument) -> None:
8+
self.tagName = tag_name
9+
self.document = document
10+
self.parentNode: MockElement | None = None
11+
12+
def appendChild(self, child: MockElement) -> None:
13+
child.parentNode = self
14+
15+
def remove(self) -> None:
16+
self.document.element_seq.remove(self)
17+
18+
def removeChild(self, child: MockElement) -> None:
19+
child.remove()
20+
21+
@property
22+
def lastElementChild(self) -> MockElement:
23+
return self.children[-1]
24+
25+
def setAttribute(self, name: str, value: str) -> None:
26+
pass
27+
28+
@property
29+
def children(self) -> list[MockElement]:
30+
return self.document._filter_elements(lambda x: x.parentNode == self)
31+
32+
33+
class MockDocument:
34+
def __init__(self) -> None:
35+
self.element_seq: list[MockElement] = []
36+
self.createElement('body')
37+
38+
def createElement(self, tag_name) -> MockElement:
39+
element = MockElement(tag_name, self)
40+
self.element_seq.append(element)
41+
return element
42+
43+
def _filter_elements(self, function: Callable[[MockElement], list[MockElement]]) -> list[MockElement]:
44+
return list(
45+
filter(
46+
function,
47+
self.element_seq
48+
)
49+
)
50+
51+
def getElementsByTagName(self, tag_name):
52+
return self._filter_elements(lambda x: x.tagName == tag_name)

0 commit comments

Comments
 (0)