Skip to content

Commit

Permalink
Merge pull request #113 from MiddleRed/main
Browse files Browse the repository at this point in the history
Add `user_state` endpoint for better detection on account suspension
  • Loading branch information
d60 authored Jun 5, 2024
2 parents ea2b3cf + 9be7c51 commit cdde4b9
Show file tree
Hide file tree
Showing 6 changed files with 36 additions and 15 deletions.
3 changes: 0 additions & 3 deletions examples/download_tweet_media.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import asyncio
import time

from twikit.twikit_async import Client

AUTH_INFO_1 = '...'
Expand Down
14 changes: 12 additions & 2 deletions twikit/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from ._captcha import Capsolver
from .community import Community, CommunityMember
from .errors import (
AccountLocked,
AccountSuspended,
BadRequest,
CouldNotTweet,
Expand Down Expand Up @@ -122,7 +123,7 @@ def request(
if error_code == 326:
# Account unlocking
if self.captcha_solver is None:
raise TwitterException(
raise AccountLocked(
'Your account is locked. Visit '
'https://twitter.com/account/access to unlock it.'
)
Expand Down Expand Up @@ -151,6 +152,8 @@ def request(
elif status_code == 408:
raise RequestTimeout(message, headers=response.headers)
elif status_code == 429:
if self._get_user_state() == 'suspended':
raise AccountSuspended(message, headers=response.headers)
raise TooManyRequests(message, headers=response.headers)
elif 500 <= status_code < 600:
raise ServerError(message, headers=response.headers)
Expand Down Expand Up @@ -4936,7 +4939,7 @@ def _get_community_users(
items = find_dict(response, 'items_results')[0]
users = []
for item in items:
if not 'result' in item:
if 'result' not in item:
continue
if item['result'].get('__typename') != 'User':
continue
Expand Down Expand Up @@ -5192,3 +5195,10 @@ def _update_subscriptions(
session.topics -= unsubscribe

return _payload_from_data(response)

def _get_user_state(self) -> Literal['normal', 'bounced', 'suspended']:
response, _ = self.get(
Endpoint.USER_STATE,
headers=self._base_headers
)
return response['userState']
5 changes: 5 additions & 0 deletions twikit/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ class AccountSuspended(TwitterException):
Exception raised when the account is suspended.
"""

class AccountLocked(TwitterException):
"""
Exception raised when the account is locked (very likey is Arkose challenge).
"""

ERROR_CODE_TO_EXCEPTION: dict[int, TwitterException] = {
187: DuplicateTweet,
324: InvalidMedia
Expand Down
1 change: 0 additions & 1 deletion twikit/tweet.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from httpx import Response

from .client import Client
from .community import Community
from .utils import Result


Expand Down
14 changes: 12 additions & 2 deletions twikit/twikit_async/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from httpx import Response

from ..errors import (
AccountLocked,
AccountSuspended,
BadRequest,
CouldNotTweet,
Expand Down Expand Up @@ -124,7 +125,7 @@ async def request(
if error_code == 326:
# Account unlocking
if self.captcha_solver is None:
raise TwitterException(
raise AccountLocked(
'Your account is locked. Visit '
'https://twitter.com/account/access to unlock it.'
)
Expand Down Expand Up @@ -153,6 +154,8 @@ async def request(
elif status_code == 408:
raise RequestTimeout(message, headers=response.headers)
elif status_code == 429:
if await self._get_user_state() == 'suspended':
raise AccountSuspended(message, headers=response.headers)
raise TooManyRequests(message, headers=response.headers)
elif 500 <= status_code < 600:
raise ServerError(message, headers=response.headers)
Expand Down Expand Up @@ -4976,7 +4979,7 @@ async def _get_community_users(
items = find_dict(response, 'items_results')[0]
users = []
for item in items:
if not 'result' in item:
if 'result' not in item:
continue
if item['result'].get('__typename') != 'User':
continue
Expand Down Expand Up @@ -5236,3 +5239,10 @@ async def _update_subscriptions(
session.topics -= unsubscribe

return _payload_from_data(response)

async def _get_user_state(self) -> Literal['normal', 'bounced', 'suspended']:
response, _ = await self.get(
Endpoint.USER_STATE,
headers=self._base_headers
)
return response['userState']
14 changes: 7 additions & 7 deletions twikit/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ class Endpoint:
REVERSE_GEOCODE = 'https://api.twitter.com/1.1/geo/reverse_geocode.json'
SEARCH_GEO = 'https://api.twitter.com/1.1/geo/search.json'
PLACE_BY_ID = 'https://api.twitter.com/1.1/geo/id/{}.json'
USER_STATE = 'https://api.twitter.com/help-center/forms/api/prod/user_state.json'

T = TypeVar('T')

Expand Down Expand Up @@ -474,7 +475,6 @@ def build_tweet_data(raw_data: dict) -> dict:
'is_quote_status': raw_data.get('is_quote_status'),
'in_reply_to_status_id_str': raw_data.get('in_reply_to_status_id_str'),
'retweeted_status_result': raw_data.get('retweeted_status_result'),
'is_quote_status': raw_data.get('is_quote_status'),
'possibly_sensitive': raw_data.get('possibly_sensitive'),
'possibly_sensitive_editable': raw_data.get('possibly_sensitive_editable'),
'quote_count': raw_data.get('quote_count'),
Expand Down Expand Up @@ -674,13 +674,13 @@ def build_query(text: str, options: SearchOptions) -> str:
if until := options.get('until'):
text += f' until:{until}'

if options.get('positive') == True:
text += f' :)'
if options.get('positive') is True:
text += ' :)'

if options.get('negative') == True:
text += f' :('
if options.get('negative') is True:
text += ' :('

if options.get('question') == True:
text += f' ?'
if options.get('question') is True:
text += ' ?'

return text

0 comments on commit cdde4b9

Please sign in to comment.