Merge branch 'v1' into like-and-restore

This commit is contained in:
firedotguy
2026-02-05 02:50:35 +06:00
committed by GitHub
8 changed files with 102 additions and 36 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
test.py
like.py
venv/ venv/
__pycache__/ __pycache__/
dist dist

View File

@@ -1,20 +1,21 @@
from warnings import deprecated
from uuid import UUID from uuid import UUID
from _io import BufferedReader from _io import BufferedReader
from typing import cast from typing import cast
from requests.exceptions import HTTPError, ConnectionError from requests.exceptions import ConnectionError, HTTPError
from itd.routes.users import get_user, update_profile, follow, unfollow, get_followers, get_following, update_privacy from itd.routes.users import get_user, update_profile, follow, unfollow, get_followers, get_following, update_privacy
from itd.routes.etc import get_top_clans, get_who_to_follow, get_platform_status from itd.routes.etc import get_top_clans, get_who_to_follow, get_platform_status
from itd.routes.comments import get_comments, add_comment, delete_comment, like_comment, unlike_comment, add_reply_comment from itd.routes.comments import get_comments, add_comment, delete_comment, like_comment, unlike_comment, add_reply_comment
from itd.routes.hashtags import get_hastags, get_posts_by_hastag from itd.routes.hashtags import get_hashtags, get_posts_by_hashtag
from itd.routes.notifications import get_notifications, mark_as_read, mark_all_as_read, get_unread_notifications_count from itd.routes.notifications import get_notifications, mark_as_read, mark_all_as_read, get_unread_notifications_count
from itd.routes.posts import create_post, get_posts, get_post, edit_post, delete_post, pin_post, repost, view_post, get_liked_posts, restore_post, like_post, delete_like_post from itd.routes.posts import create_post, get_posts, get_post, edit_post, delete_post, pin_post, repost, view_post, get_liked_posts, restore_post, like_post, delete_like_post
from itd.routes.reports import report from itd.routes.reports import report
from itd.routes.search import search from itd.routes.search import search
from itd.routes.files import upload_file from itd.routes.files import upload_file
from itd.routes.auth import refresh_token, change_password, logout from itd.routes.auth import refresh_token, change_password, logout
from itd.routes.verification import verificate, get_verification_status from itd.routes.verification import verify, get_verification_status
from itd.models.comment import Comment from itd.models.comment import Comment
from itd.models.notification import Notification from itd.models.notification import Notification
@@ -26,20 +27,18 @@ from itd.models.pagination import Pagination
from itd.models.verification import Verification, VerificationStatus from itd.models.verification import Verification, VerificationStatus
from itd.request import set_cookies from itd.request import set_cookies
from itd.exceptions import NoCookie, NoAuthData, SamePassword, InvalidOldPassword, NotFound, ValidationError, UserBanned, PendingRequestExists, Forbidden, UsernameTaken from itd.exceptions import NoCookie, NoAuthData, SamePassword, InvalidOldPassword, NotFound, ValidationError, UserBanned, PendingRequestExists, Forbidden, UsernameTaken, CantFollowYourself, Unauthorized
def refresh_on_error(func): def refresh_on_error(func):
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
try: if self.cookies:
return func(self, *args, **kwargs) try:
except HTTPError as e: return func(self, *args, **kwargs)
if '401' in str(e): except (Unauthorized, ConnectionError, HTTPError):
self.refresh_auth() self.refresh_auth()
return func(self, *args, **kwargs) return func(self, *args, **kwargs)
raise e else:
except ConnectionError:
self.refresh_auth()
return func(self, *args, **kwargs) return func(self, *args, **kwargs)
return wrapper return wrapper
@@ -80,7 +79,7 @@ class Client:
"""Смена пароля """Смена пароля
Args: Args:
old (str): Страый пароль old (str): Старый пароль
new (str): Новый пароль new (str): Новый пароль
Raises: Raises:
@@ -203,6 +202,7 @@ class Client:
Raises: Raises:
NotFound: Пользователь не найден NotFound: Пользователь не найден
CantFollowYourself: Невозможно подписаться на самого себе
Returns: Returns:
int: Число подписчиков после подписки int: Число подписчиков после подписки
@@ -210,6 +210,8 @@ class Client:
res = follow(self.token, username) res = follow(self.token, username)
if res.json().get('error', {}).get('code') == 'NOT_FOUND': if res.json().get('error', {}).get('code') == 'NOT_FOUND':
raise NotFound('User') raise NotFound('User')
if res.json().get('error', {}).get('code') == 'VALIDATION_ERROR' and res.status_code == 400:
raise CantFollowYourself()
res.raise_for_status() res.raise_for_status()
return res.json()['followersCount'] return res.json()['followersCount']
@@ -280,7 +282,7 @@ class Client:
return [UserFollower.model_validate(user) for user in res.json()['data']['users']], Pagination.model_validate(res.json()['data']['pagination']) return [UserFollower.model_validate(user) for user in res.json()['data']['users']], Pagination.model_validate(res.json()['data']['pagination'])
@deprecated("verificate устарел используйте verify")
@refresh_on_error @refresh_on_error
def verificate(self, file_url: str) -> Verification: def verificate(self, file_url: str) -> Verification:
"""Отправить запрос на верификацию """Отправить запрос на верификацию
@@ -294,7 +296,22 @@ class Client:
Returns: Returns:
Verification: Верификация Verification: Верификация
""" """
res = verificate(self.token, file_url) return self.verify(file_url)
@refresh_on_error
def verify(self, file_url: str) -> Verification:
"""Отправить запрос на верификацию
Args:
file_url (str): Ссылка на видео
Raises:
PendingRequestExists: Запрос уже отправлен
Returns:
Verification: Верификация
"""
res = verify(self.token, file_url)
if res.json().get('error', {}).get('code') == 'PENDING_REQUEST_EXISTS': if res.json().get('error', {}).get('code') == 'PENDING_REQUEST_EXISTS':
raise PendingRequestExists() raise PendingRequestExists()
res.raise_for_status() res.raise_for_status()
@@ -313,10 +330,9 @@ class Client:
return VerificationStatus.model_validate(res.json()) return VerificationStatus.model_validate(res.json())
@refresh_on_error @refresh_on_error
def get_who_to_follow(self) -> list[UserWhoToFollow]: def get_who_to_follow(self) -> list[UserWhoToFollow]:
"""Получить список популярнык пользователей (кого читать) """Получить список популярных пользователей (кого читать)
Returns: Returns:
list[UserWhoToFollow]: Список пользователей list[UserWhoToFollow]: Список пользователей
@@ -352,12 +368,13 @@ class Client:
@refresh_on_error @refresh_on_error
def add_comment(self, post_id: UUID, content: str) -> Comment: def add_comment(self, post_id: UUID, content: str, attachment_ids: list[UUID] = []) -> Comment:
"""Добавить комментарий """Добавить комментарий
Args: Args:
post_id (str): UUID поста post_id (str): UUID поста
content (str): Содержание content (str): Содержание
attachment_ids (list[UUID]): Список UUID прикреплённых файлов
reply_comment_id (UUID | None, optional): ID коммента для ответа. Defaults to None. reply_comment_id (UUID | None, optional): ID коммента для ответа. Defaults to None.
Raises: Raises:
@@ -367,7 +384,7 @@ class Client:
Returns: Returns:
Comment: Комментарий Comment: Комментарий
""" """
res = add_comment(self.token, post_id, content) res = add_comment(self.token, post_id, content, attachment_ids)
if res.status_code == 422 and 'found' in res.json(): if res.status_code == 422 and 'found' in res.json():
raise ValidationError(*list(res.json()['found'].items())[0]) raise ValidationError(*list(res.json()['found'].items())[0])
if res.json().get('error', {}).get('code') == 'NOT_FOUND': if res.json().get('error', {}).get('code') == 'NOT_FOUND':
@@ -378,13 +395,14 @@ class Client:
@refresh_on_error @refresh_on_error
def add_reply_comment(self, comment_id: UUID, content: str, author_id: UUID) -> Comment: def add_reply_comment(self, comment_id: UUID, content: str, author_id: UUID, attachment_ids: list[UUID] = []) -> Comment:
"""Добавить ответный комментарий """Добавить ответный комментарий
Args: Args:
comment_id (str): UUID комментария comment_id (str): UUID комментария
content (str): Содержание content (str): Содержание
author_id (UUID | None, optional): ID пользователя, отправившего комментарий. Defaults to None. author_id (UUID | None, optional): ID пользователя, отправившего комментарий. Defaults to None.
attachment_ids (list[UUID]): Список UUID прикреплённых файлов
Raises: Raises:
ValidationError: Ошибка валидации ValidationError: Ошибка валидации
@@ -393,7 +411,7 @@ class Client:
Returns: Returns:
Comment: Комментарий Comment: Комментарий
""" """
res = add_reply_comment(self.token, comment_id, content, author_id) res = add_reply_comment(self.token, comment_id, content, author_id, attachment_ids)
if res.status_code == 500 and 'Failed query' in res.text: if res.status_code == 500 and 'Failed query' in res.text:
raise NotFound('User') raise NotFound('User')
if res.status_code == 422 and 'found' in res.json(): if res.status_code == 422 and 'found' in res.json():
@@ -490,7 +508,7 @@ class Client:
raise Forbidden('delete comment') raise Forbidden('delete comment')
res.raise_for_status() res.raise_for_status()
@deprecated("get_hastags устарел используйте get_hashtags")
@refresh_on_error @refresh_on_error
def get_hastags(self, limit: int = 10) -> list[Hashtag]: def get_hastags(self, limit: int = 10) -> list[Hashtag]:
"""Получить список популярных хэштэгов """Получить список популярных хэштэгов
@@ -501,7 +519,19 @@ class Client:
Returns: Returns:
list[Hashtag]: Список хэштэгов list[Hashtag]: Список хэштэгов
""" """
res = get_hastags(self.token, limit) return self.get_hashtags(limit)
@refresh_on_error
def get_hashtags(self, limit: int = 10) -> list[Hashtag]:
"""Получить список популярных хэштэгов
Args:
limit (int, optional): Лимит. Defaults to 10.
Returns:
list[Hashtag]: Список хэштэгов
"""
res = get_hashtags(self.token, limit)
res.raise_for_status() res.raise_for_status()
return [Hashtag.model_validate(hashtag) for hashtag in res.json()['data']['hashtags']] return [Hashtag.model_validate(hashtag) for hashtag in res.json()['data']['hashtags']]
@@ -520,7 +550,7 @@ class Client:
list[Post]: Посты list[Post]: Посты
Pagination: Пагинация Pagination: Пагинация
""" """
res = get_posts_by_hastag(self.token, hashtag, limit, cursor) res = get_posts_by_hashtag(self.token, hashtag, limit, cursor)
res.raise_for_status() res.raise_for_status()
data = res.json()['data'] data = res.json()['data']

View File

@@ -7,14 +7,20 @@ class NoAuthData(Exception):
return 'No auth data. Provide token or cookies' return 'No auth data. Provide token or cookies'
class InvalidCookie(Exception): class InvalidCookie(Exception):
def __init__(self, code: str):
self.code = code
def __str__(self): def __str__(self):
return f'Invalid cookie data' if self.code == 'SESSION_NOT_FOUND':
return f'Invalid cookie data: Session not found (incorrect refresh token)'
elif self.code == 'REFRESH_TOKEN_MISSING':
return f'Invalid cookie data: No refresh token'
# SESSION_REVOKED
return f'Invalid cookie data: Session revoked (logged out)'
class InvalidToken(Exception): class InvalidToken(Exception):
def __str__(self): def __str__(self):
return f'Invalid access token' return f'Invalid access token'
class SamePassword(Exception): class SamePassword(Exception):
def __str__(self): def __str__(self):
return 'Old and new password must not equals' return 'Old and new password must not equals'
@@ -59,3 +65,11 @@ class Forbidden(Exception):
class UsernameTaken(Exception): class UsernameTaken(Exception):
def __str__(self): def __str__(self):
return 'Username is already taken' return 'Username is already taken'
class CantFollowYourself(Exception):
def __str__(self):
return 'Cannot follow yourself'
class Unauthorized(Exception):
def __str__(self) -> str:
return 'Auth required - refresh token'

View File

@@ -8,7 +8,7 @@ class File(BaseModel):
id: UUID id: UUID
url: str url: str
filename: str filename: str
mime_type: str = Field('image/png', alias='mimeType') mime_type: str = Field(alias='mimeType')
size: int size: int

View File

@@ -3,7 +3,7 @@ from _io import BufferedReader
from requests import Session from requests import Session
from requests.exceptions import JSONDecodeError from requests.exceptions import JSONDecodeError
from itd.exceptions import InvalidToken, InvalidCookie, RateLimitExceeded from itd.exceptions import InvalidToken, InvalidCookie, RateLimitExceeded, Unauthorized
s = Session() s = Session()
@@ -35,10 +35,13 @@ def fetch(token: str, method: str, url: str, params: dict = {}, files: dict[str,
try: try:
if res.json().get('error', {}).get('code') == 'RATE_LIMIT_EXCEEDED': if res.json().get('error', {}).get('code') == 'RATE_LIMIT_EXCEEDED':
raise RateLimitExceeded(res.json()['error'].get('retryAfter', 0)) raise RateLimitExceeded(res.json()['error'].get('retryAfter', 0))
if res.json().get('error', {}).get('code') == 'UNAUTHORIZED':
raise Unauthorized()
except JSONDecodeError: except JSONDecodeError:
pass pass
print(res.text) if not res.ok:
print(res.text)
return res return res
@@ -82,8 +85,10 @@ def auth_fetch(cookies: str, method: str, url: str, params: dict = {}, token: st
try: try:
if res.json().get('error', {}).get('code') == 'RATE_LIMIT_EXCEEDED': if res.json().get('error', {}).get('code') == 'RATE_LIMIT_EXCEEDED':
raise RateLimitExceeded(res.json()['error'].get('retryAfter', 0)) raise RateLimitExceeded(res.json()['error'].get('retryAfter', 0))
if res.json().get('error', {}).get('code') in ('SESSION_NOT_FOUND', 'REFRESH_TOKEN_MISSING'): if res.json().get('error', {}).get('code') in ('SESSION_NOT_FOUND', 'REFRESH_TOKEN_MISSING', 'SESSION_REVOKED'):
raise InvalidCookie() raise InvalidCookie(res.json()['error']['code'])
if res.json().get('error', {}).get('code') == 'UNAUTHORIZED':
raise Unauthorized()
except JSONDecodeError: except JSONDecodeError:
pass pass

View File

@@ -2,11 +2,11 @@ from uuid import UUID
from itd.request import fetch from itd.request import fetch
def add_comment(token: str, post_id: UUID, content: str): def add_comment(token: str, post_id: UUID, content: str, attachment_ids: list[UUID] = []):
return fetch(token, 'post', f'posts/{post_id}/comments', {'content': content}) return fetch(token, 'post', f'posts/{post_id}/comments', {'content': content, "attachmentIds": list(map(str, attachment_ids))})
def add_reply_comment(token: str, comment_id: UUID, content: str, author_id: UUID): def add_reply_comment(token: str, comment_id: UUID, content: str, author_id: UUID, attachment_ids: list[UUID] = []):
return fetch(token, 'post', f'comments/{comment_id}/replies', {'content': content, 'replyToUserId': str(author_id)}) return fetch(token, 'post', f'comments/{comment_id}/replies', {'content': content, 'replyToUserId': str(author_id), "attachmentIds": list(map(str, attachment_ids))})
def get_comments(token: str, post_id: UUID, limit: int = 20, cursor: int = 0, sort: str = 'popular'): def get_comments(token: str, post_id: UUID, limit: int = 20, cursor: int = 0, sort: str = 'popular'):
return fetch(token, 'get', f'posts/{post_id}/comments', {'limit': limit, 'sort': sort, 'cursor': cursor}) return fetch(token, 'get', f'posts/{post_id}/comments', {'limit': limit, 'sort': sort, 'cursor': cursor})

View File

@@ -1,8 +1,17 @@
from warnings import deprecated
from uuid import UUID from uuid import UUID
from itd.request import fetch from itd.request import fetch
@deprecated("get_hastags устарела используйте get_hashtags")
def get_hastags(token: str, limit: int = 10): def get_hastags(token: str, limit: int = 10):
return fetch(token, 'get', 'hashtags/trending', {'limit': limit}) return fetch(token, 'get', 'hashtags/trending', {'limit': limit})
def get_hashtags(token: str, limit: int = 10):
return fetch(token, 'get', 'hashtags/trending', {'limit': limit})
@deprecated("get_posts_by_hastag устерла используй get_posts_by_hashtag")
def get_posts_by_hastag(token: str, hashtag: str, limit: int = 20, cursor: UUID | None = None): def get_posts_by_hastag(token: str, hashtag: str, limit: int = 20, cursor: UUID | None = None):
return fetch(token, 'get', f'hashtags/{hashtag}/posts', {'limit': limit, 'cursor': cursor}) return fetch(token, 'get', f'hashtags/{hashtag}/posts', {'limit': limit, 'cursor': cursor})
def get_posts_by_hashtag(token: str, hashtag: str, limit: int = 20, cursor: UUID | None = None):
return fetch(token, 'get', f'hashtags/{hashtag}/posts', {'limit': limit, 'cursor': cursor})

View File

@@ -1,8 +1,14 @@
from warnings import deprecated
from itd.request import fetch from itd.request import fetch
def verificate(token: str, file_url: str): def verify(token: str, file_url: str):
# {"success":true,"request":{"id":"fc54e54f-8586-4d8c-809e-df93161f99da","userId":"9096a85b-c319-483e-8940-6921be427ad0","videoUrl":"https://943701f000610900cbe86b72234e451d.bckt.ru/videos/354f28a6-9ac7-48a6-879a-a454062b1d6b.mp4","status":"pending","rejectionReason":null,"reviewedBy":null,"reviewedAt":null,"createdAt":"2026-01-30T12:58:14.228Z","updatedAt":"2026-01-30T12:58:14.228Z"}} # {"success":true,"request":{"id":"fc54e54f-8586-4d8c-809e-df93161f99da","userId":"9096a85b-c319-483e-8940-6921be427ad0","videoUrl":"https://943701f000610900cbe86b72234e451d.bckt.ru/videos/354f28a6-9ac7-48a6-879a-a454062b1d6b.mp4","status":"pending","rejectionReason":null,"reviewedBy":null,"reviewedAt":null,"createdAt":"2026-01-30T12:58:14.228Z","updatedAt":"2026-01-30T12:58:14.228Z"}}
return fetch(token, 'post', 'verification/submit', {'videoUrl': file_url}) return fetch(token, 'post', 'verification/submit', {'videoUrl': file_url})
@deprecated("verificate устарела используйте verify")
def verificate(token: str, file_url: str):
return verify(token, file_url)
def get_verification_status(token: str): def get_verification_status(token: str):
return fetch(token, 'get', 'verification/status') return fetch(token, 'get', 'verification/status')