45 Commits

Author SHA1 Message Date
firedotguy
6236c5981b chore: update version 2026-02-07 18:52:10 +03:00
firedotguy
27bd528883 fix: add __init__.py in routes and models 2026-02-07 18:51:31 +03:00
firedotguy
63dfedc917 chore: remove test file 2026-02-07 18:07:02 +03:00
firedotguy
006bda53c1 chore: update version; change name in readme 2026-02-07 18:01:38 +03:00
firedotguy
201f720843 Merge pull request #6 from firedotguy/v1
Версия 1
2026-02-07 20:59:49 +06:00
firedotguy
8aef43e11d feat: add pins 2026-02-07 17:47:09 +03:00
firedotguy
506e6a5d09 feat: add models final part 5 2026-02-07 17:23:42 +03:00
firedotguy
f33ed4f76a fix: revert str UUID 2026-02-06 22:19:56 +03:00
firedotguy
2d27507338 fix: fix type warnings 2026-02-06 22:15:16 +03:00
firedotguy
b2bd982c1b fix: remove tests 2026-02-06 21:43:40 +03:00
firedotguy
7d5275c880 Merge branch 'v1' of https://github.com/firedotguy/pyITDclient into v1 2026-02-06 21:43:12 +03:00
firedotguy
a2d019e1d6 fix: JSONDecodeError when view post 2026-02-06 21:43:03 +03:00
firedotguy
844671ab4e Merge pull request #5 from EpsilonRationes/like-and-restore 2026-02-07 00:42:21 +06:00
EpsilonRationes
38805156bf Delete tests/__init__.py 2026-02-06 21:40:10 +03:00
firedotguy
ae8d4c04d5 Merge pull request #7 from EpsilonRationes/str-or-UUID
Преобразование str в UUID
2026-02-06 01:49:47 +06:00
Rationess
65cd617a1f Преобразование str в UUID 2026-02-05 09:26:16 +03:00
firedotguy
55630bc23f feat: add models part 4 2026-02-05 00:20:26 +03:00
firedotguy
60ae5feb21 Merge branch 'v1' into like-and-restore 2026-02-05 02:50:35 +06:00
firedotguy
dd7b8c077e Merge branch 'v1' of https://github.com/firedotguy/pyITDclient into v1 2026-02-04 22:52:22 +03:00
firedotguy
2f026a32d7 feat: add cant follow yourself error 2026-02-04 22:52:10 +03:00
firedotguy
dc72c6fb4b Merge pull request #2 from EpsilonRationes/Typo 2026-02-05 01:32:01 +06:00
firedotguy
b82d4fc30b Merge branch 'v1' into Typo 2026-02-05 01:31:03 +06:00
firedotguy
295501462a Merge pull request #4 from EpsilonRationes/comment-attachments 2026-02-05 01:30:05 +06:00
Rationess
eb83c724cc restore и like постов 2026-02-04 11:15:19 +03:00
Rationess
a547bbfdfb mime_type не только png там может быть и jpg и gid. Ещё и аудио форматы 2026-02-03 22:22:27 +03:00
Rationess
6ac12c6f79 attachment_ids для комментариев 2026-02-03 22:20:33 +03:00
Rationess
0c212e41cb ошибка в названии verificate 2026-02-03 20:58:22 +03:00
Rationess
80a5a33443 ошибка в названиях get_hastags и get_posts_by_hastag 2026-02-03 20:55:33 +03:00
Rationess
7ce699e42a лишний аргумент в docstring 2026-02-03 20:48:08 +03:00
Rationess
3ad003a0b1 user limit 2026-02-03 20:45:06 +03:00
Rationess
2ccd26fc9b Ошибка в слове популярных 2026-02-03 20:42:55 +03:00
Rationess
af0b2a1acc Ошибка в слове verificate 2026-02-03 20:41:34 +03:00
Rationess
c9a5dcad10 Ошибка в слове Старый 2026-02-03 20:33:28 +03:00
Rationess
1db59149f4 попроваил get_hastags на get_hashtags 2026-02-03 20:32:43 +03:00
firedotguy
ba78457de5 feat: add models part 3 2026-02-01 17:20:37 +03:00
firedotguy
2a9f7da9a9 feat: add models part 2 2026-01-31 18:28:23 +03:00
firedotguy
8d03171925 chore: update version 2026-01-31 12:27:48 +03:00
firedotguy
a388426d8d feat: add models and partially custom error messages 2026-01-31 12:10:20 +03:00
firedotguy
c7e3812ee8 feat: add models and enum 2026-01-30 20:49:49 +03:00
firedotguy
1a606da55f feat: add verification; add auth - change password and logout 2026-01-30 16:12:05 +03:00
firedotguy
aa20199ebe feat: add get liked post and update privacy 2026-01-30 15:39:52 +03:00
firedotguy
49427a5535 refactor: move api calls to routes folder; feat: add update_banner user-friendly method; fix: change file data type 2026-01-30 15:16:29 +03:00
firedotguy
d27db1d905 docs: add note about cookie 2026-01-30 14:56:06 +03:00
firedotguy
70bb1e75d6 docs: add plans 2026-01-30 14:51:54 +03:00
firedotguy
97c6812819 docs: change package name in install guide; add banner change and name change examples 2026-01-29 23:54:11 +03:00
42 changed files with 1632 additions and 232 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,7 @@
test.py
like.py
venv/
__pycache__/
dist
itd_sdk.egg-info
nowkie.gif

View File

@@ -1,11 +1,11 @@
# pyITDclient
# itd-sdk
Клиент ITD для python
## Установка
```bash
pip install pyITDclient
pip install itd-sdk
```
## Пример
@@ -19,6 +19,41 @@ c = ITDClient('TOKEN', 'refresh_token=...; __ddg1_=...; __ddgid_=...; is_auth=1;
print(c.get_me())
```
> [!NOTE]
> Берите куки из запроса /auth/refresh. В остальных запросах нету refresh_token
> ![cookie](cookie-screen.png)
---
### Скрипт на обновление имени
Этот код сейчас работает на @itd_sdk (обновляется имя и пост)
```python
from itd import ITDClient
from time import sleep
from random import randint
from datetime import datetime
from datetime import timezone
c = ITDClient(None, '...')
while True:
c.update_profile(display_name=f'PYTHON ITD SDK | Рандом: {randint(1, 100)} | {datetime.now().strftime("%m.%d %H:%M:%S")}')
# редактирование поста
# c.edit_post('82ea8a4f-a49e-485e-b0dc-94d7da9df990', f'рил ща {datetime.now(timezone.utc).isoformat(" ")} по UTC (обновляется каждую секунду)')
sleep(1)
```
### Скрипт на смену баннера
```python
from itd import ITDClient
c = ITDClient(None, '...')
id = c.upload_file('любое-имя.png', open('реальное-имя-файла.png', 'rb'))['id']
c.update_profile(banner_id=id)
print('баннер обновлен')
```
### Встроенные запросы
Существуют встроенные эндпоинты для комментариев, хэштэгов, уведомлений, постов, репортов, поиска, пользователей, итд.
```python
@@ -42,7 +77,15 @@ fetch(c.token, 'метод', 'эндпоинт', {'данные': 'данные'
> [!NOTE]
> `xn--d1ah4a.com` - punycode от "итд.com"
## прочее
## Планы
- Форматированные сообщения об ошибках
- Логирование (через logging)
- Добавление ООП (отдеьные классы по типу User или Post вместо обычного JSON)
- Голосовые сообщения
## Прочее
Лицезия: [MIT](./LICENSE)
Идея (и часть эндпоинтов): https://github.com/FriceKa/ITD-SDK-js
- По сути этот проект является реворком, просто на другом языке

BIN
cookie-screen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +0,0 @@
from itd.request import fetch
def add_comment(token: str, post_id: str, content: str, reply_comment_id: str | None = None):
data = {'content': content}
if reply_comment_id:
data['replyTo'] = str(reply_comment_id)
return fetch(token, 'post', f'posts/{post_id}/comments', data)
def get_comments(token: str, post_id: str, limit: int = 20, cursor: int = 0, sort: str = 'popular'):
return fetch(token, 'get', f'posts/{post_id}/comments', {'limit': limit, 'sort': sort, 'cursor': cursor})
def like_comment(token: str, comment_id: str):
return fetch(token, 'post', f'comments/{comment_id}/like')
def unlike_comment(token: str, comment_id: str):
return fetch(token, 'delete', f'comments/{comment_id}/like')
def delete_comment(token: str, comment_id: str):
return fetch(token, 'delete', f'comments/{comment_id}')

34
itd/enums.py Normal file
View File

@@ -0,0 +1,34 @@
from enum import Enum
class NotificationType(Enum):
WALL_POST = 'wall_post'
REPLY = 'reply'
REPOST = 'repost'
COMMENT = 'comment'
FOLLOW = 'follow'
LIKE = 'like'
class NotificationTargetType(Enum):
POST = 'post'
class ReportTargetType(Enum):
POST = 'post'
USER = 'user'
COMMENT = 'comment'
class ReportTargetReason(Enum):
SPAM = 'spam' # спам
VIOLENCE = 'violence' # насилие
HATE = 'hate' # ненависть
ADULT = 'adult' # 18+
FRAUD = 'fraud' # обман\мошенничество
OTHER = 'other' # другое
class AttachType(Enum):
AUDIO = 'audio'
IMAGE = 'image'
VIDEO = 'video'
class PostsTab(Enum):
FOLLOWING = 'following'
POPULAR = 'popular'

101
itd/exceptions.py Normal file
View File

@@ -0,0 +1,101 @@
class NoCookie(Exception):
def __str__(self):
return 'No cookie for refresh-token required action'
class NoAuthData(Exception):
def __str__(self):
return 'No auth data. Provide token or cookies'
class InvalidCookie(Exception):
def __init__(self, code: str):
self.code = code
def __str__(self):
if self.code == 'SESSION_NOT_FOUND':
return 'Invalid cookie data: Session not found (incorrect refresh token)'
elif self.code == 'REFRESH_TOKEN_MISSING':
return 'Invalid cookie data: No refresh token'
elif self.code == 'SESSION_EXPIRED':
return 'Invalid cookie data: Session expired'
# SESSION_REVOKED
return 'Invalid cookie data: Session revoked (logged out)'
class InvalidToken(Exception):
def __str__(self):
return 'Invalid access token'
class SamePassword(Exception):
def __str__(self):
return 'Old and new password must not equals'
class InvalidOldPassword(Exception):
def __str__(self):
return 'Old password is incorrect'
class NotFound(Exception):
def __init__(self, obj: str):
self.obj = obj
def __str__(self):
return f'{self.obj} not found'
class UserBanned(Exception):
def __str__(self):
return 'User banned'
class ValidationError(Exception):
def __init__(self, name: str, value: str):
self.name = name
self.value = value
def __str__(self):
return f'Failed validation on {self.name}: "{self.value}"'
class PendingRequestExists(Exception):
def __str__(self):
return 'Pending verifiaction request already exists'
class RateLimitExceeded(Exception):
def __init__(self, retry_after: int):
self.retry_after = retry_after
def __str__(self):
return f'Rate limit exceeded - too much requests. Retry after {self.retry_after} seconds'
class Forbidden(Exception):
def __init__(self, action: str):
self.action = action
def __str__(self):
return f'Forbidden to {self.action}'
class UsernameTaken(Exception):
def __str__(self):
return 'Username is already taken'
class CantFollowYourself(Exception):
def __str__(self):
return 'Cannot follow yourself'
class Unauthorized(Exception):
def __str__(self):
return 'Auth required - refresh token'
class CantRepostYourPost(Exception):
def __str__(self):
return 'Cannot repost your own post'
class AlreadyReposted(Exception):
def __str__(self):
return 'Post already reposted'
class AlreadyReported(Exception):
def __init__(self, obj: str) -> None:
self.obj = obj
def __str__(self):
return f'{self.obj} already reported'
class TooLarge(Exception):
def __str__(self):
return 'Search query too large'
class PinNotOwned(Exception):
def __init__(self, pin: str) -> None:
self.pin = pin
def __str__(self):
return f'You do not own "{self.pin}" pin'

View File

@@ -1,5 +0,0 @@
from itd.request import fetch
def upload_file(token: str, name: str, data: bytes):
return fetch(token, 'post', 'files/upload', files={'file': (name, data)})

View File

@@ -1,7 +0,0 @@
from itd.request import fetch
def get_hastags(token: str, limit: int = 10):
return fetch(token, 'get', 'hashtags/trending', {'limit': limit})
def get_posts_by_hastag(token: str, hashtag: str, limit: int = 20, cursor: int = 0):
return fetch(token, 'get', f'hashtags/{hashtag}/posts', {'limit': limit, 'cursor': cursor})

0
itd/models/__init__.py Normal file
View File

21
itd/models/_text.py Normal file
View File

@@ -0,0 +1,21 @@
from uuid import UUID
from datetime import datetime
from pydantic import BaseModel, Field, field_validator
class TextObject(BaseModel):
id: UUID
content: str
created_at: datetime = Field(alias='createdAt')
model_config = {'populate_by_name': True}
@field_validator('created_at', mode='plain')
@classmethod
def validate_created_at(cls, v: str):
try:
return datetime.strptime(v + '00', '%Y-%m-%d %H:%M:%S.%f%z')
except ValueError:
return datetime.strptime(v, '%Y-%m-%dT%H:%M:%S.%fZ')

6
itd/models/clan.py Normal file
View File

@@ -0,0 +1,6 @@
from pydantic import BaseModel, Field
class Clan(BaseModel):
avatar: str
member_count: int = Field(0, alias='memberCount')

17
itd/models/comment.py Normal file
View File

@@ -0,0 +1,17 @@
from pydantic import Field
from itd.models._text import TextObject
from itd.models.user import UserPost
from itd.models.file import Attach
class Comment(TextObject):
author: UserPost
likes_count: int = Field(0, alias='likesCount')
replies_count: int = Field(0, alias='repliesCount')
is_liked: bool = Field(False, alias='isLiked')
attachments: list[Attach] = []
replies: list['Comment'] = []
reply_to: UserPost | None = None # author of replied comment, if this comment is reply

29
itd/models/file.py Normal file
View File

@@ -0,0 +1,29 @@
from uuid import UUID
from pydantic import BaseModel, Field
from itd.enums import AttachType
class File(BaseModel):
id: UUID
url: str
filename: str
mime_type: str = Field(alias='mimeType')
size: int
class PostAttach(BaseModel):
id: UUID
type: AttachType = AttachType.IMAGE
url: str
thumbnail_url: str | None = Field(None, alias='thumbnailUrl')
width: int | None = None
height: int | None = None
class Attach(PostAttach):
filename: str
mime_type: str = Field(alias='mimeType')
size: int
duration: int | None = None
order: int = 0

8
itd/models/hashtag.py Normal file
View File

@@ -0,0 +1,8 @@
from uuid import UUID
from pydantic import BaseModel, Field
class Hashtag(BaseModel):
id: UUID
name: str
posts_count: int = Field(0, alias='postsCount')

View File

@@ -0,0 +1,22 @@
from uuid import UUID
from datetime import datetime
from pydantic import BaseModel, Field
from itd.enums import NotificationType, NotificationTargetType
from itd.models.user import UserNotification
class Notification(BaseModel):
id: UUID
type: NotificationType
target_type: NotificationTargetType | None = Field(None, alias='targetType') # none - follows, other - NotificationTragetType.POST
target_id: UUID | None = Field(None, alias='targetId') # none - follows
preview: str | None = None # follow - none, comment/reply - content, repost - original post content, like - post content, wall_post - wall post content
read: bool = False
read_at: datetime | None = Field(None, alias='readAt')
created_at: datetime = Field(alias='createdAt')
actor: UserNotification

23
itd/models/pagination.py Normal file
View File

@@ -0,0 +1,23 @@
from uuid import UUID
from datetime import datetime
from pydantic import BaseModel, Field
class Pagination(BaseModel):
page: int | None = 1
limit: int = 20
total: int | None = None
has_more: bool = Field(True, alias='hasMore')
next_cursor: UUID | None = Field(None, alias='nextCursor')
class PostsPagintaion(BaseModel):
limit: int = 20
next_cursor: int | None = Field(1, alias='nextCursor')
has_more: bool = Field(True, alias='hasMore')
class LikedPostsPagintaion(BaseModel):
limit: int = 20
next_cursor: datetime | None = Field(None, alias='nextCursor')
has_more: bool = Field(True, alias='hasMore')

12
itd/models/pin.py Normal file
View File

@@ -0,0 +1,12 @@
from datetime import datetime
from pydantic import BaseModel, Field
class ShortPin(BaseModel):
slug: str
name: str
description: str
class Pin(ShortPin):
granted_at: datetime = Field(alias='grantedAt')

46
itd/models/post.py Normal file
View File

@@ -0,0 +1,46 @@
from uuid import UUID
from pydantic import Field, BaseModel
from itd.models.user import UserPost, UserNewPost
from itd.models._text import TextObject
from itd.models.file import PostAttach
from itd.models.comment import Comment
class _PostShort(TextObject):
likes_count: int = Field(0, alias='likesCount')
comments_count: int = Field(0, alias='commentsCount')
reposts_count: int = Field(0, alias='repostsCount')
views_count: int = Field(0, alias='viewsCount')
class PostShort(_PostShort):
author: UserPost
class OriginalPost(PostShort):
is_deleted: bool = Field(False, alias='isDeleted')
class _Post(_PostShort):
is_liked: bool = Field(False, alias='isLiked')
is_reposted: bool = Field(False, alias='isReposted')
is_viewed: bool = Field(False, alias='isViewed')
is_owner: bool = Field(False, alias='isOwner')
attachments: list[PostAttach] = []
comments: list[Comment] = []
original_post: OriginalPost | None = None
wall_recipient_id: UUID | None = Field(None, alias='wallRecipientId')
wall_recipient: UserPost | None = Field(None, alias='wallRecipient')
class Post(_Post, PostShort):
pass
class NewPost(_Post):
author: UserNewPost

19
itd/models/report.py Normal file
View File

@@ -0,0 +1,19 @@
from uuid import UUID
from datetime import datetime
from pydantic import BaseModel, Field
from itd.enums import ReportTargetType, ReportTargetReason
class NewReport(BaseModel):
id: UUID
created_at: datetime = Field(alias='createdAt')
class Report(NewReport):
reason: ReportTargetReason
description: str | None = None
target_type: ReportTargetType = Field(alias='targetType')
target_id: UUID

64
itd/models/user.py Normal file
View File

@@ -0,0 +1,64 @@
from uuid import UUID
from datetime import datetime
from pydantic import BaseModel, Field
from itd.models.pin import ShortPin
class UserPrivacy(BaseModel):
private: bool | None = Field(None, alias='isPrivate') # none for not me
wall_closed: bool = Field(False, alias='wallClosed')
model_config = {'populate_by_name': True}
class UserProfileUpdate(BaseModel):
id: UUID
username: str | None = None
display_name: str = Field(alias='displayName')
bio: str | None = None
updated_at: datetime | None = Field(None, alias='updatedAt')
class UserNewPost(BaseModel):
username: str | None = None
display_name: str = Field(alias='displayName')
avatar: str
pin: ShortPin | None = None
verified: bool = False
class UserNotification(UserNewPost):
id: UUID
class UserPost(UserNotification, UserNewPost):
pass
class UserWhoToFollow(UserPost):
followers_count: int = Field(0, alias='followersCount')
class UserFollower(UserPost):
is_following: bool = Field(False, alias='isFollowing') # none for me
class UserSearch(UserFollower, UserWhoToFollow):
pass
class User(UserSearch, UserPrivacy):
banner: str | None = None
bio: str | None = None
pinned_post_id: UUID | None = Field(None, alias='pinnedPostId')
following_count: int = Field(0, alias='followingCount')
posts_count: int = Field(0, alias='postsCount')
is_followed: bool | None = Field(None, alias='isFollowedBy') # none for me
created_at: datetime = Field(alias='createdAt')

View File

@@ -0,0 +1,23 @@
from uuid import UUID
from datetime import datetime
from pydantic import BaseModel, Field
class Verification(BaseModel):
id: UUID
user_id: UUID = Field(alias='userId')
video_url: str = Field(alias='videoUrl')
status: str # should be enum, but we dont know all statuses (what status for accepted?)
reject_reason: str | None = Field(None, alias='rejectionReason')
reviewer: str | None = Field(None, alias='reviewedBy')
reviewed_at: datetime | None = Field(None, alias='reviewedAt')
created_at: datetime = Field(alias='createdAt')
updated_at: datetime = Field(alias='updatedAt')
class VerificationStatus(BaseModel):
status: str # should be enum, but we dont know all statuses (what status for accepted?)
request_id: UUID = Field(alias='requestId')
submitted_at: datetime = Field(alias='submittedAt')

View File

@@ -1,16 +0,0 @@
from itd.request import fetch
def get_notifications(token: str, limit: int = 20, cursor: int = 0, type: str | None = None):
data = {'limit': str(limit), 'cursor': str(cursor)}
if type:
data['type'] = type
return fetch(token, 'get', 'notifications', data)
def mark_as_read(token: str, id: str):
return fetch(token, 'post', f'notification/{id}/read')
def mark_all_as_read(token: str):
return fetch(token, 'post', f'notification/read-all')
def get_unread_notifications_count(token: str):
return fetch(token, 'get', 'notifications/count')

View File

@@ -1,42 +0,0 @@
from itd.request import fetch
def create_post(token: str, content: str, wall_recipient_id: int | None = None, attach_ids: list[str] = []):
data: dict = {'content': content}
if wall_recipient_id:
data['wallRecipientId'] = wall_recipient_id
if attach_ids:
data['attachmentIds'] = attach_ids
return fetch(token, 'post', 'posts', data)
def get_posts(token: str, username: str | None = None, limit: int = 20, cursor: int = 0, sort: str = '', tab: str = ''):
data: dict = {'limit': limit, 'cursor': cursor}
if username:
data['username'] = username
if sort:
data['sort'] = sort
if tab:
data['tab'] = tab
return fetch(token, 'get', 'posts', data)
def get_post(token: str, id: str):
return fetch(token, 'get', f'posts/{id}')
def edit_post(token: str, id: str, content: str):
return fetch(token, 'put', f'posts/{id}', {'content': content})
def delete_post(token: str, id: str):
return fetch(token, 'delete', f'posts/{id}')
def pin_post(token: str, id: str):
return fetch(token, 'post', f'posts/{id}/pin')
def repost(token: str, id: str, content: str | None = None):
data = {}
if content:
data['content'] = content
return fetch(token, 'post', f'posts/{id}/repost', data)
def view_post(token: str, id: str):
return fetch(token, 'post', f'posts/{id}/view')

View File

@@ -1,4 +0,0 @@
from itd.request import fetch
def report(token: str, id: str, type: str = 'post', reason: str = 'other', description: str = ''):
return fetch(token, 'post', 'reports', {'targetId': id, 'targetType': type, 'reason': reason, 'description': description})

View File

@@ -1,9 +1,14 @@
from _io import BufferedReader
from requests import Session
from requests.exceptions import JSONDecodeError
from itd.exceptions import InvalidToken, InvalidCookie, RateLimitExceeded, Unauthorized
s = Session()
def fetch(token: str, method: str, url: str, params: dict = {}, files: dict[str, tuple[str, bytes]] = {}):
def fetch(token: str, method: str, url: str, params: dict = {}, files: dict[str, tuple[str, BufferedReader]] = {}):
base = f'https://xn--d1ah4a.com/api/{url}'
headers = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
@@ -25,18 +30,27 @@ def fetch(token: str, method: str, url: str, params: dict = {}, files: dict[str,
if method == "get":
res = s.get(base, timeout=120 if files else 20, params=params, headers=headers)
else:
res = s.request(method.upper(), base, timeout=20, json=params, headers=headers, files=files)
res = s.request(method.upper(), base, timeout=120 if files else 20, json=params, headers=headers, files=files)
try:
if res.json().get('error', {}).get('code') == 'RATE_LIMIT_EXCEEDED':
raise RateLimitExceeded(res.json()['error'].get('retryAfter', 0))
if res.json().get('error', {}).get('code') == 'UNAUTHORIZED':
raise Unauthorized()
except JSONDecodeError:
pass # todo
if not res.ok:
print(res.text)
return res
res.raise_for_status()
return res.json()
def set_cookies(cookies: str):
for cookie in cookies.split('; '):
s.cookies.set(cookie.split('=')[0], cookie.split('=')[-1], path='/', domain='xn--d1ah4a.com.com')
def refresh_auth(cookies: str):
print('refresh')
res = s.post(f'https://xn--d1ah4a.com/api/v1/auth/refresh', timeout=10, headers={
def auth_fetch(cookies: str, method: str, url: str, params: dict = {}, token: str | None = None):
headers = {
"Host": "xn--d1ah4a.com",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0",
"Accept": "*/*",
@@ -56,6 +70,26 @@ def refresh_auth(cookies: str):
"Cache-Control": "no-cache",
"Content-Length": "0",
"TE": "trailers",
})
res.raise_for_status()
return res.json()['accessToken']
}
if token:
headers['Authorization'] = 'Bearer ' + token
if method == 'get':
res = s.get(f'https://xn--d1ah4a.com/api/{url}', timeout=20, params=params, headers=headers)
else:
res = s.request(method, f'https://xn--d1ah4a.com/api/{url}', timeout=20, json=params, headers=headers)
# print(res.text)
if res.text == 'UNAUTHORIZED':
raise InvalidToken()
try:
if res.json().get('error', {}).get('code') == 'RATE_LIMIT_EXCEEDED':
raise RateLimitExceeded(res.json()['error'].get('retryAfter', 0))
if res.json().get('error', {}).get('code') in ('SESSION_NOT_FOUND', 'REFRESH_TOKEN_MISSING', 'SESSION_REVOKED', 'SESSION_EXPIRED'):
raise InvalidCookie(res.json()['error']['code'])
if res.json().get('error', {}).get('code') == 'UNAUTHORIZED':
raise Unauthorized()
except JSONDecodeError:
print('fail to parse json')
return res

0
itd/routes/__init__.py Normal file
View File

12
itd/routes/auth.py Normal file
View File

@@ -0,0 +1,12 @@
from requests import Response
from itd.request import auth_fetch
def refresh_token(cookies: str) -> Response:
return auth_fetch(cookies, 'post', 'v1/auth/refresh')
def change_password(cookies: str, token: str, old: str, new: str) -> Response:
return auth_fetch(cookies, 'post', 'v1/auth/change-password', {'newPassword': new, 'oldPassword': old}, token)
def logout(cookies: str) -> Response:
return auth_fetch(cookies, 'post', 'v1/auth/logout')

21
itd/routes/comments.py Normal file
View File

@@ -0,0 +1,21 @@
from uuid import UUID
from itd.request import fetch
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, "attachmentIds": list(map(str, attachment_ids))})
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), "attachmentIds": list(map(str, attachment_ids))})
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})
def like_comment(token: str, comment_id: UUID):
return fetch(token, 'post', f'comments/{comment_id}/like')
def unlike_comment(token: str, comment_id: UUID):
return fetch(token, 'delete', f'comments/{comment_id}/like')
def delete_comment(token: str, comment_id: UUID):
return fetch(token, 'delete', f'comments/{comment_id}')

7
itd/routes/files.py Normal file
View File

@@ -0,0 +1,7 @@
from _io import BufferedReader
from itd.request import fetch
def upload_file(token: str, name: str, data: BufferedReader):
return fetch(token, 'post', 'files/upload', files={'file': (name, data)})

17
itd/routes/hashtags.py Normal file
View File

@@ -0,0 +1,17 @@
from warnings import deprecated
from uuid import UUID
from itd.request import fetch
@deprecated("get_hastags устарела используйте get_hashtags")
def get_hastags(token: str, limit: int = 10):
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):
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

@@ -0,0 +1,15 @@
from uuid import UUID
from itd.request import fetch
def get_notifications(token: str, limit: int = 20, offset: int = 0):
return fetch(token, 'get', 'notifications', {'limit': limit, 'offset': offset})
def mark_as_read(token: str, id: UUID):
return fetch(token, 'post', f'notifications/{id}/read')
def mark_all_as_read(token: str):
return fetch(token, 'post', f'notifications/read-all')
def get_unread_notifications_count(token: str):
return fetch(token, 'get', 'notifications/count')

10
itd/routes/pins.py Normal file
View File

@@ -0,0 +1,10 @@
from itd.request import fetch
def get_pins(token: str):
return fetch(token, 'get', 'users/me/pins')
def remove_pin(token: str):
return fetch(token, 'delete', 'users/me/pin')
def set_pin(token: str, slug: str):
return fetch(token, 'put', 'users/me/pin', {'slug': slug})

50
itd/routes/posts.py Normal file
View File

@@ -0,0 +1,50 @@
from datetime import datetime
from uuid import UUID
from itd.request import fetch
from itd.enums import PostsTab
def create_post(token: str, content: str, wall_recipient_id: UUID | None = None, attachment_ids: list[UUID] = []):
data: dict = {'content': content}
if wall_recipient_id:
data['wallRecipientId'] = str(wall_recipient_id)
if attachment_ids:
data['attachmentIds'] = list(map(str, attachment_ids))
return fetch(token, 'post', 'posts', data)
def get_posts(token: str, cursor: int = 0, tab: PostsTab = PostsTab.POPULAR):
return fetch(token, 'get', 'posts', {'cursor': cursor, 'tab': tab.value})
def get_post(token: str, id: UUID):
return fetch(token, 'get', f'posts/{id}')
def edit_post(token: str, id: UUID, content: str):
return fetch(token, 'put', f'posts/{id}', {'content': content})
def delete_post(token: str, id: UUID):
return fetch(token, 'delete', f'posts/{id}')
def pin_post(token: str, id: UUID):
return fetch(token, 'post', f'posts/{id}/pin')
def repost(token: str, id: UUID, content: str | None = None):
data = {}
if content:
data['content'] = content
return fetch(token, 'post', f'posts/{id}/repost', data)
def view_post(token: str, id: UUID):
return fetch(token, 'post', f'posts/{id}/view')
def get_liked_posts(token: str, username_or_id: str | UUID, limit: int = 20, cursor: datetime | None = None):
return fetch(token, 'get', f'posts/user/{username_or_id}/liked', {'limit': limit, 'cursor': cursor})
def restore_post(token: str, post_id: UUID):
return fetch(token, "post", f"posts/{post_id}/restore",)
def like_post(token: str, post_id: UUID):
return fetch(token, "post", f"posts/{post_id}/like")
def unlike_post(token: str, post_id: UUID):
return fetch(token, "delete", f"posts/{post_id}/like")

9
itd/routes/reports.py Normal file
View File

@@ -0,0 +1,9 @@
from uuid import UUID
from itd.request import fetch
from itd.enums import ReportTargetReason, ReportTargetType
def report(token: str, id: UUID, type: ReportTargetType = ReportTargetType.POST, reason: ReportTargetReason = ReportTargetReason.OTHER, description: str | None = None):
if description is None:
description = ''
return fetch(token, 'post', 'reports', {'targetId': str(id), 'targetType': type.value, 'reason': reason.value, 'description': description})

View File

@@ -1,10 +1,12 @@
from uuid import UUID
from itd.request import fetch
def get_user(token: str, username: str):
return fetch(token, 'get', f'users/{username}')
def update_profile(token: str, bio: str | None = None, display_name: str | None = None, username: str | None = None, banner_id: str | None = None):
def update_profile(token: str, bio: str | None = None, display_name: str | None = None, username: str | None = None, banner_id: UUID | None = None):
data = {}
if bio:
data['bio'] = bio
@@ -13,9 +15,17 @@ def update_profile(token: str, bio: str | None = None, display_name: str | None
if username:
data['username'] = username
if banner_id:
data['bannerId'] = banner_id
data['bannerId'] = str(banner_id)
return fetch(token, 'put', 'users/me', data)
def update_privacy(token: str, wall_closed: bool = False, private: bool = False):
data = {}
if wall_closed is not None:
data['wallClosed'] = wall_closed
if private is not None:
data['isPrivate'] = private
return fetch(token, 'put', 'users/me/privacy', data)
def follow(token: str, username: str):
return fetch(token, 'post', f'users/{username}/follow')
@@ -27,3 +37,4 @@ def get_followers(token: str, username: str, limit: int = 30, page: int = 1):
def get_following(token: str, username: str, limit: int = 30, page: int = 1):
return fetch(token, 'get', f'users/{username}/following', {'limit': limit, 'page': page})

View File

@@ -0,0 +1,14 @@
from warnings import deprecated
from itd.request import fetch
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"}}
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):
return fetch(token, 'get', 'verification/status')

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "itd-sdk"
version = "0.1.0"
version = "1.0.1"
description = "ITD client for python"
readme = "README.md"
authors = [
@@ -12,6 +12,6 @@ authors = [
]
license = "MIT"
dependencies = [
"requests"
"requests", "pydantic"
]
requires-python = ">=3.9"

View File

@@ -0,0 +1,2 @@
pydantic==2.11.9
requests==2.32.3

View File

@@ -2,10 +2,10 @@ from setuptools import setup, find_packages
setup(
name='itd-sdk',
version='0.1.0',
version='1.0.1',
packages=find_packages(),
install_requires=[
'requests'
'requests', 'pydantic'
],
python_requires=">=3.9"
)