itd-iter-api 1.0.2__tar.gz
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.
- itd_iter_api-1.0.2/LICENSE +21 -0
- itd_iter_api-1.0.2/PKG-INFO +107 -0
- itd_iter_api-1.0.2/README.md +93 -0
- itd_iter_api-1.0.2/itd_iter_api.egg-info/PKG-INFO +107 -0
- itd_iter_api-1.0.2/itd_iter_api.egg-info/SOURCES.txt +13 -0
- itd_iter_api-1.0.2/itd_iter_api.egg-info/dependency_links.txt +1 -0
- itd_iter_api-1.0.2/itd_iter_api.egg-info/requires.txt +1 -0
- itd_iter_api-1.0.2/itd_iter_api.egg-info/top_level.txt +1 -0
- itd_iter_api-1.0.2/iter/__init__.py +14 -0
- itd_iter_api-1.0.2/iter/client.py +168 -0
- itd_iter_api-1.0.2/iter/manual_auth.py +146 -0
- itd_iter_api-1.0.2/iter/request.py +75 -0
- itd_iter_api-1.0.2/pyproject.toml +18 -0
- itd_iter_api-1.0.2/setup.cfg +4 -0
- itd_iter_api-1.0.2/setup.py +13 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 firedotguy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: itd-iter-api
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Summary: ITD client for python
|
|
5
|
+
Author: sharkow
|
|
6
|
+
Author-email: firedotguy <nta16022013@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: requests
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
Dynamic: requires-python
|
|
14
|
+
|
|
15
|
+
# pyITDclient
|
|
16
|
+
Клиент ITD для python
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## Установка
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install itd-sdk
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Пример
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from itd import ITDClient
|
|
29
|
+
|
|
30
|
+
c = ITDClient('TOKEN', 'refresh_token=...; __ddg1_=...; __ddgid_=...; is_auth=1; __ddg2_=...; ddg_last_challenge=...; __ddg8_=...; __ddg10_=...; __ddg9_=...')
|
|
31
|
+
# можно указать только токен, тогда после просрочки перестанет работать, либо только куки чтобы токен сразу подтянулся, либо оба сразу
|
|
32
|
+
|
|
33
|
+
print(c.get_me())
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> [!NOTE]
|
|
37
|
+
> Берите куки из запроса /auth/refresh. В остальных запросах нету refresh_token
|
|
38
|
+
> 
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
### Скрипт на обновление имени
|
|
42
|
+
Этот код сейчас работает на @itd_sdk (обновляется имя и пост)
|
|
43
|
+
```python
|
|
44
|
+
from itd import ITDClient
|
|
45
|
+
from time import sleep
|
|
46
|
+
from random import randint
|
|
47
|
+
from datetime import datetime
|
|
48
|
+
from datetime import timezone
|
|
49
|
+
|
|
50
|
+
c = ITDClient(None, '...')
|
|
51
|
+
|
|
52
|
+
while True:
|
|
53
|
+
c.update_profile(display_name=f'PYTHON ITD SDK | Рандом: {randint(1, 100)} | {datetime.now().strftime("%m.%d %H:%M:%S")}')
|
|
54
|
+
# редактирование поста
|
|
55
|
+
# c.edit_post('82ea8a4f-a49e-485e-b0dc-94d7da9df990', f'рил ща {datetime.now(timezone.utc).isoformat(" ")} по UTC (обновляется каждую секунду)')
|
|
56
|
+
sleep(1)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Скрипт на смену баннера
|
|
60
|
+
```python
|
|
61
|
+
from itd import ITDClient
|
|
62
|
+
|
|
63
|
+
c = ITDClient(None, '...')
|
|
64
|
+
|
|
65
|
+
id = c.upload_file('любое-имя.png', open('реальное-имя-файла.png', 'rb'))['id']
|
|
66
|
+
c.update_profile(banner_id=id)
|
|
67
|
+
print('баннер обновлен')
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Встроенные запросы
|
|
72
|
+
Существуют встроенные эндпоинты для комментариев, хэштэгов, уведомлений, постов, репортов, поиска, пользователей, итд.
|
|
73
|
+
```python
|
|
74
|
+
c.get_user('ITD_API') # получение данных пользователя
|
|
75
|
+
c.get_me() # получение своих данных (me)
|
|
76
|
+
c.update_profile(display_name='22:26') # изменение данных профиля, например имя, био итд
|
|
77
|
+
c.create_post('тест1') # создание постов
|
|
78
|
+
# итд
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Кастомные запросы
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from iter.request import fetch
|
|
85
|
+
|
|
86
|
+
fetch(c.token, 'метод', 'эндпоинт', {'данные': 'данные'})
|
|
87
|
+
```
|
|
88
|
+
Из методов поддерживается `get`, `post`, `put` итд, которые есть в `requests`
|
|
89
|
+
К названию эндпоинта добавляется домен итд и `api`, то есть в этом примере отпарвится `https://xn--d1ah4a.com/api/эндпоинт`.
|
|
90
|
+
|
|
91
|
+
> [!NOTE]
|
|
92
|
+
> `xn--d1ah4a.com` - punycode от "итд.com"
|
|
93
|
+
|
|
94
|
+
## Планы
|
|
95
|
+
|
|
96
|
+
- Форматированные сообщения об ошибках
|
|
97
|
+
- Логирование (через logging)
|
|
98
|
+
- Добавление ООП (отдеьные классы по типу User или Post вместо обычного JSON)
|
|
99
|
+
- Голосовые сообщения
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## Прочее
|
|
103
|
+
Лицезия: [MIT](./LICENSE)
|
|
104
|
+
Идея (и часть эндпоинтов): https://github.com/FriceKa/ITD-SDK-js
|
|
105
|
+
- По сути этот проект является реворком, просто на другом языке
|
|
106
|
+
|
|
107
|
+
Автор: [itd_sdk](https://xn--d1ah4a.com/itd_sdk) (в итд) [@desicars](https://t.me/desicars) (в тг)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# pyITDclient
|
|
2
|
+
Клиент ITD для python
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
## Установка
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install itd-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Пример
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from itd import ITDClient
|
|
15
|
+
|
|
16
|
+
c = ITDClient('TOKEN', 'refresh_token=...; __ddg1_=...; __ddgid_=...; is_auth=1; __ddg2_=...; ddg_last_challenge=...; __ddg8_=...; __ddg10_=...; __ddg9_=...')
|
|
17
|
+
# можно указать только токен, тогда после просрочки перестанет работать, либо только куки чтобы токен сразу подтянулся, либо оба сразу
|
|
18
|
+
|
|
19
|
+
print(c.get_me())
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
> [!NOTE]
|
|
23
|
+
> Берите куки из запроса /auth/refresh. В остальных запросах нету refresh_token
|
|
24
|
+
> 
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
### Скрипт на обновление имени
|
|
28
|
+
Этот код сейчас работает на @itd_sdk (обновляется имя и пост)
|
|
29
|
+
```python
|
|
30
|
+
from itd import ITDClient
|
|
31
|
+
from time import sleep
|
|
32
|
+
from random import randint
|
|
33
|
+
from datetime import datetime
|
|
34
|
+
from datetime import timezone
|
|
35
|
+
|
|
36
|
+
c = ITDClient(None, '...')
|
|
37
|
+
|
|
38
|
+
while True:
|
|
39
|
+
c.update_profile(display_name=f'PYTHON ITD SDK | Рандом: {randint(1, 100)} | {datetime.now().strftime("%m.%d %H:%M:%S")}')
|
|
40
|
+
# редактирование поста
|
|
41
|
+
# c.edit_post('82ea8a4f-a49e-485e-b0dc-94d7da9df990', f'рил ща {datetime.now(timezone.utc).isoformat(" ")} по UTC (обновляется каждую секунду)')
|
|
42
|
+
sleep(1)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Скрипт на смену баннера
|
|
46
|
+
```python
|
|
47
|
+
from itd import ITDClient
|
|
48
|
+
|
|
49
|
+
c = ITDClient(None, '...')
|
|
50
|
+
|
|
51
|
+
id = c.upload_file('любое-имя.png', open('реальное-имя-файла.png', 'rb'))['id']
|
|
52
|
+
c.update_profile(banner_id=id)
|
|
53
|
+
print('баннер обновлен')
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Встроенные запросы
|
|
58
|
+
Существуют встроенные эндпоинты для комментариев, хэштэгов, уведомлений, постов, репортов, поиска, пользователей, итд.
|
|
59
|
+
```python
|
|
60
|
+
c.get_user('ITD_API') # получение данных пользователя
|
|
61
|
+
c.get_me() # получение своих данных (me)
|
|
62
|
+
c.update_profile(display_name='22:26') # изменение данных профиля, например имя, био итд
|
|
63
|
+
c.create_post('тест1') # создание постов
|
|
64
|
+
# итд
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Кастомные запросы
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from iter.request import fetch
|
|
71
|
+
|
|
72
|
+
fetch(c.token, 'метод', 'эндпоинт', {'данные': 'данные'})
|
|
73
|
+
```
|
|
74
|
+
Из методов поддерживается `get`, `post`, `put` итд, которые есть в `requests`
|
|
75
|
+
К названию эндпоинта добавляется домен итд и `api`, то есть в этом примере отпарвится `https://xn--d1ah4a.com/api/эндпоинт`.
|
|
76
|
+
|
|
77
|
+
> [!NOTE]
|
|
78
|
+
> `xn--d1ah4a.com` - punycode от "итд.com"
|
|
79
|
+
|
|
80
|
+
## Планы
|
|
81
|
+
|
|
82
|
+
- Форматированные сообщения об ошибках
|
|
83
|
+
- Логирование (через logging)
|
|
84
|
+
- Добавление ООП (отдеьные классы по типу User или Post вместо обычного JSON)
|
|
85
|
+
- Голосовые сообщения
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
## Прочее
|
|
89
|
+
Лицезия: [MIT](./LICENSE)
|
|
90
|
+
Идея (и часть эндпоинтов): https://github.com/FriceKa/ITD-SDK-js
|
|
91
|
+
- По сути этот проект является реворком, просто на другом языке
|
|
92
|
+
|
|
93
|
+
Автор: [itd_sdk](https://xn--d1ah4a.com/itd_sdk) (в итд) [@desicars](https://t.me/desicars) (в тг)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: itd-iter-api
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Summary: ITD client for python
|
|
5
|
+
Author: sharkow
|
|
6
|
+
Author-email: firedotguy <nta16022013@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: requests
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
Dynamic: requires-python
|
|
14
|
+
|
|
15
|
+
# pyITDclient
|
|
16
|
+
Клиент ITD для python
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## Установка
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install itd-sdk
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Пример
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from itd import ITDClient
|
|
29
|
+
|
|
30
|
+
c = ITDClient('TOKEN', 'refresh_token=...; __ddg1_=...; __ddgid_=...; is_auth=1; __ddg2_=...; ddg_last_challenge=...; __ddg8_=...; __ddg10_=...; __ddg9_=...')
|
|
31
|
+
# можно указать только токен, тогда после просрочки перестанет работать, либо только куки чтобы токен сразу подтянулся, либо оба сразу
|
|
32
|
+
|
|
33
|
+
print(c.get_me())
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> [!NOTE]
|
|
37
|
+
> Берите куки из запроса /auth/refresh. В остальных запросах нету refresh_token
|
|
38
|
+
> 
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
### Скрипт на обновление имени
|
|
42
|
+
Этот код сейчас работает на @itd_sdk (обновляется имя и пост)
|
|
43
|
+
```python
|
|
44
|
+
from itd import ITDClient
|
|
45
|
+
from time import sleep
|
|
46
|
+
from random import randint
|
|
47
|
+
from datetime import datetime
|
|
48
|
+
from datetime import timezone
|
|
49
|
+
|
|
50
|
+
c = ITDClient(None, '...')
|
|
51
|
+
|
|
52
|
+
while True:
|
|
53
|
+
c.update_profile(display_name=f'PYTHON ITD SDK | Рандом: {randint(1, 100)} | {datetime.now().strftime("%m.%d %H:%M:%S")}')
|
|
54
|
+
# редактирование поста
|
|
55
|
+
# c.edit_post('82ea8a4f-a49e-485e-b0dc-94d7da9df990', f'рил ща {datetime.now(timezone.utc).isoformat(" ")} по UTC (обновляется каждую секунду)')
|
|
56
|
+
sleep(1)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Скрипт на смену баннера
|
|
60
|
+
```python
|
|
61
|
+
from itd import ITDClient
|
|
62
|
+
|
|
63
|
+
c = ITDClient(None, '...')
|
|
64
|
+
|
|
65
|
+
id = c.upload_file('любое-имя.png', open('реальное-имя-файла.png', 'rb'))['id']
|
|
66
|
+
c.update_profile(banner_id=id)
|
|
67
|
+
print('баннер обновлен')
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Встроенные запросы
|
|
72
|
+
Существуют встроенные эндпоинты для комментариев, хэштэгов, уведомлений, постов, репортов, поиска, пользователей, итд.
|
|
73
|
+
```python
|
|
74
|
+
c.get_user('ITD_API') # получение данных пользователя
|
|
75
|
+
c.get_me() # получение своих данных (me)
|
|
76
|
+
c.update_profile(display_name='22:26') # изменение данных профиля, например имя, био итд
|
|
77
|
+
c.create_post('тест1') # создание постов
|
|
78
|
+
# итд
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Кастомные запросы
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from iter.request import fetch
|
|
85
|
+
|
|
86
|
+
fetch(c.token, 'метод', 'эндпоинт', {'данные': 'данные'})
|
|
87
|
+
```
|
|
88
|
+
Из методов поддерживается `get`, `post`, `put` итд, которые есть в `requests`
|
|
89
|
+
К названию эндпоинта добавляется домен итд и `api`, то есть в этом примере отпарвится `https://xn--d1ah4a.com/api/эндпоинт`.
|
|
90
|
+
|
|
91
|
+
> [!NOTE]
|
|
92
|
+
> `xn--d1ah4a.com` - punycode от "итд.com"
|
|
93
|
+
|
|
94
|
+
## Планы
|
|
95
|
+
|
|
96
|
+
- Форматированные сообщения об ошибках
|
|
97
|
+
- Логирование (через logging)
|
|
98
|
+
- Добавление ООП (отдеьные классы по типу User или Post вместо обычного JSON)
|
|
99
|
+
- Голосовые сообщения
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## Прочее
|
|
103
|
+
Лицезия: [MIT](./LICENSE)
|
|
104
|
+
Идея (и часть эндпоинтов): https://github.com/FriceKa/ITD-SDK-js
|
|
105
|
+
- По сути этот проект является реворком, просто на другом языке
|
|
106
|
+
|
|
107
|
+
Автор: [itd_sdk](https://xn--d1ah4a.com/itd_sdk) (в итд) [@desicars](https://t.me/desicars) (в тг)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
itd_iter_api.egg-info/PKG-INFO
|
|
6
|
+
itd_iter_api.egg-info/SOURCES.txt
|
|
7
|
+
itd_iter_api.egg-info/dependency_links.txt
|
|
8
|
+
itd_iter_api.egg-info/requires.txt
|
|
9
|
+
itd_iter_api.egg-info/top_level.txt
|
|
10
|
+
iter/__init__.py
|
|
11
|
+
iter/client.py
|
|
12
|
+
iter/manual_auth.py
|
|
13
|
+
iter/request.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
iter
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from iter.client import Client as Client
|
|
2
|
+
from iter.types.responses import (
|
|
3
|
+
SearchResponse,
|
|
4
|
+
CommentsResponse,
|
|
5
|
+
PostFeedResponse,
|
|
6
|
+
HashtagFeedResponse,
|
|
7
|
+
UserListResponse,
|
|
8
|
+
LikeResponse,
|
|
9
|
+
FollowResponse,
|
|
10
|
+
PostUpdateResponse,
|
|
11
|
+
PinResponse,
|
|
12
|
+
ProfileUpdateResponse,
|
|
13
|
+
PrivacyUpdateResponse
|
|
14
|
+
)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from _io import BufferedReader
|
|
4
|
+
from typing import cast, Optional
|
|
5
|
+
|
|
6
|
+
from requests.exceptions import HTTPError
|
|
7
|
+
|
|
8
|
+
# Import your routes
|
|
9
|
+
from iter.routes.users import get_user, update_profile, follow, unfollow, get_followers, get_following, update_privacy
|
|
10
|
+
from iter.routes.etc import get_top_clans, get_who_to_follow, get_platform_status
|
|
11
|
+
from iter.routes.comments import get_comments, add_comment, delete_comment, like_comment, unlike_comment
|
|
12
|
+
from iter.routes.hashtags import get_hastags, get_posts_by_hastag
|
|
13
|
+
from iter.routes.notifications import get_notifications, mark_as_read, get_unread_notifications_count
|
|
14
|
+
from iter.routes.posts import create_post, get_posts, get_post, edit_post, delete_post, pin_post, repost, view_post, get_liked_posts
|
|
15
|
+
from iter.routes.reports import report
|
|
16
|
+
from iter.routes.search import search
|
|
17
|
+
from iter.routes.files import upload_file
|
|
18
|
+
from iter.routes.auth import refresh_token, change_password, logout
|
|
19
|
+
from iter.routes.verification import verificate, get_verification_status
|
|
20
|
+
|
|
21
|
+
from iter.manual_auth import auth
|
|
22
|
+
|
|
23
|
+
def refresh_on_error(func):
|
|
24
|
+
def wrapper(self, *args, **kwargs):
|
|
25
|
+
try:
|
|
26
|
+
return func(self, *args, **kwargs)
|
|
27
|
+
except HTTPError as e:
|
|
28
|
+
# If Access Token is expired (401)
|
|
29
|
+
if e.response is not None and e.response.status_code == 401:
|
|
30
|
+
print("Access token expired, attempting refresh")
|
|
31
|
+
self.auth()
|
|
32
|
+
return func(self, *args, **kwargs)
|
|
33
|
+
raise e
|
|
34
|
+
return wrapper
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Client:
|
|
38
|
+
def __init__(self, token: Optional[str] = None, cookies: Optional[str] = None, session_file: Optional[str] = "session.json", email: Optional[str] = None, password: Optional[str] = None, use_manual_login: bool = True):
|
|
39
|
+
self.token = token.replace('Bearer ', '') if token else None
|
|
40
|
+
self.cookies = cookies
|
|
41
|
+
|
|
42
|
+
self.manual_login = use_manual_login
|
|
43
|
+
self.session_file = session_file
|
|
44
|
+
|
|
45
|
+
self.email = email
|
|
46
|
+
self.password = password
|
|
47
|
+
|
|
48
|
+
is_auth = self.auth()
|
|
49
|
+
if not is_auth:
|
|
50
|
+
raise RuntimeError('Cannot login')
|
|
51
|
+
|
|
52
|
+
def auth(self):
|
|
53
|
+
if (self.session_file
|
|
54
|
+
and not self.token
|
|
55
|
+
and not self.cookies):
|
|
56
|
+
self._load_session()
|
|
57
|
+
|
|
58
|
+
if (self.manual_login
|
|
59
|
+
and not self.token
|
|
60
|
+
and not self.cookies):
|
|
61
|
+
self._manual_login()
|
|
62
|
+
elif self.cookies and not self.token:
|
|
63
|
+
self._refresh_auth()
|
|
64
|
+
|
|
65
|
+
return self.token and self.cookies
|
|
66
|
+
|
|
67
|
+
def _save_session(self):
|
|
68
|
+
"""Saves current credentials to a JSON file."""
|
|
69
|
+
data = {
|
|
70
|
+
"token": self.token,
|
|
71
|
+
"cookies": self.cookies
|
|
72
|
+
}
|
|
73
|
+
with open(self.session_file, 'w', encoding='utf-8') as f:
|
|
74
|
+
json.dump(data, f, indent=4)
|
|
75
|
+
|
|
76
|
+
def _load_session(self):
|
|
77
|
+
"""Loads credentials from the session file."""
|
|
78
|
+
if os.path.exists(self.session_file):
|
|
79
|
+
try:
|
|
80
|
+
with open(self.session_file, 'r', encoding='utf-8') as f:
|
|
81
|
+
data = json.load(f)
|
|
82
|
+
self.token = data.get("token")
|
|
83
|
+
self.cookies = data.get("cookies")
|
|
84
|
+
except Exception as e:
|
|
85
|
+
print(f"Failed to load session file: {e}")
|
|
86
|
+
|
|
87
|
+
def _manual_login(self):
|
|
88
|
+
"""Triggers the manual authentication flow."""
|
|
89
|
+
print("Starting manual login...")
|
|
90
|
+
new_data = auth(self.email, self.password)
|
|
91
|
+
if new_data:
|
|
92
|
+
self.token = new_data.get('token', '').replace('Bearer ', '')
|
|
93
|
+
self.cookies = new_data.get('cookies')
|
|
94
|
+
self._save_session()
|
|
95
|
+
return True
|
|
96
|
+
print("Manual login failed")
|
|
97
|
+
|
|
98
|
+
def _refresh_auth(self):
|
|
99
|
+
"""Attempts to get a new access token using cookies. Falls back to manual login if cookies expired."""
|
|
100
|
+
if not self.cookies:
|
|
101
|
+
return self._manual_login()
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
print("Refreshing access token...")
|
|
105
|
+
self.token = refresh_token(self.cookies).replace('Bearer ', '')
|
|
106
|
+
self._save_session()
|
|
107
|
+
return self.token
|
|
108
|
+
except HTTPError as e:
|
|
109
|
+
if e.response is not None and e.response.status_code in [401, 403]:
|
|
110
|
+
print("Refresh token expired. Manual login required.")
|
|
111
|
+
return self._manual_login()
|
|
112
|
+
raise e
|
|
113
|
+
|
|
114
|
+
@refresh_on_error
|
|
115
|
+
def logout(self):
|
|
116
|
+
if not self.cookies:
|
|
117
|
+
print('no cookies')
|
|
118
|
+
return
|
|
119
|
+
res = logout(self.cookies)
|
|
120
|
+
self.token = None
|
|
121
|
+
self.cookies = None
|
|
122
|
+
if os.path.exists(self.session_file):
|
|
123
|
+
os.remove(self.session_file)
|
|
124
|
+
return res
|
|
125
|
+
|
|
126
|
+
@refresh_on_error
|
|
127
|
+
def get_user(self, username: str) -> dict:
|
|
128
|
+
return get_user(self.token, username)
|
|
129
|
+
|
|
130
|
+
@refresh_on_error
|
|
131
|
+
def get_me(self) -> dict:
|
|
132
|
+
return self.get_user('me')
|
|
133
|
+
|
|
134
|
+
@refresh_on_error
|
|
135
|
+
def update_profile(self, username: str | None = None, display_name: str | None = None, bio: str | None = None, banner_id: str | None = None) -> dict:
|
|
136
|
+
return update_profile(self.token, bio, display_name, username, banner_id)
|
|
137
|
+
|
|
138
|
+
@refresh_on_error
|
|
139
|
+
def update_privacy(self, wall_closed: bool = False, private: bool = False):
|
|
140
|
+
return update_privacy(self.token, wall_closed, private)
|
|
141
|
+
|
|
142
|
+
@refresh_on_error
|
|
143
|
+
def follow(self, username: str) -> dict:
|
|
144
|
+
return follow(self.token, username)
|
|
145
|
+
|
|
146
|
+
@refresh_on_error
|
|
147
|
+
def unfollow(self, username: str) -> dict:
|
|
148
|
+
return unfollow(self.token, username)
|
|
149
|
+
|
|
150
|
+
@refresh_on_error
|
|
151
|
+
def get_followers(self, username: str) -> dict:
|
|
152
|
+
return get_followers(self.token, username)
|
|
153
|
+
|
|
154
|
+
@refresh_on_error
|
|
155
|
+
def get_following(self, username: str) -> dict:
|
|
156
|
+
return get_following(self.token, username)
|
|
157
|
+
|
|
158
|
+
@refresh_on_error
|
|
159
|
+
def add_comment(self, post_id: str, content: str, reply_comment_id: str | None = None):
|
|
160
|
+
return add_comment(self.token, post_id, content, reply_comment_id)
|
|
161
|
+
|
|
162
|
+
@refresh_on_error
|
|
163
|
+
def create_post(self, content: str, wall_recipient_id: int | None = None, attach_ids: list[str] = []):
|
|
164
|
+
return create_post(self.token, content, wall_recipient_id, attach_ids)
|
|
165
|
+
|
|
166
|
+
@refresh_on_error
|
|
167
|
+
def upload_file(self, name: str, data: BufferedReader):
|
|
168
|
+
return upload_file(self.token, name, data)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import zipfile
|
|
3
|
+
import platform
|
|
4
|
+
import requests
|
|
5
|
+
import logging
|
|
6
|
+
import stat
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from DrissionPage import ChromiumPage, ChromiumOptions
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
class BrowserManager:
|
|
13
|
+
def __init__(self, folder_name="browser"):
|
|
14
|
+
package_dir = Path(__file__).resolve().parent
|
|
15
|
+
self.base_dir = package_dir / folder_name
|
|
16
|
+
|
|
17
|
+
self.system = platform.system()
|
|
18
|
+
self.machine = platform.machine().lower()
|
|
19
|
+
self._set_platform_configs()
|
|
20
|
+
|
|
21
|
+
def _set_platform_configs(self):
|
|
22
|
+
"""Maps system architecture to Chrome for Testing API keys and paths."""
|
|
23
|
+
if self.system == "Windows":
|
|
24
|
+
self.platform_key = "win64"
|
|
25
|
+
self.exec_name = "chrome.exe"
|
|
26
|
+
self.relative_path = Path("chrome-win64") / self.exec_name
|
|
27
|
+
elif self.system == "Linux":
|
|
28
|
+
self.platform_key = "linux64"
|
|
29
|
+
self.exec_name = "chrome"
|
|
30
|
+
self.relative_path = Path("chrome-linux64") / self.exec_name
|
|
31
|
+
elif self.system == "Darwin":
|
|
32
|
+
self.platform_key = "mac-arm64" if "arm" in self.machine or "apple" in self.machine else "mac-x64"
|
|
33
|
+
self.exec_name = "Google Chrome for Testing"
|
|
34
|
+
folder_name = f"chrome-{self.platform_key}"
|
|
35
|
+
self.relative_path = Path(folder_name) / "Google Chrome for Testing.app" / "Contents" / "MacOS" / "Google Chrome for Testing"
|
|
36
|
+
else:
|
|
37
|
+
raise OSError(f"Unsupported operating system: {self.system}")
|
|
38
|
+
|
|
39
|
+
self.chrome_exe = self.base_dir / self.relative_path
|
|
40
|
+
|
|
41
|
+
def get_chrome_path(self):
|
|
42
|
+
"""Returns the path to the executable, downloading it if necessary."""
|
|
43
|
+
if not self.chrome_exe.exists():
|
|
44
|
+
logger.info(f"Chrome not found for {self.system}. Starting download...")
|
|
45
|
+
self._download_chrome()
|
|
46
|
+
|
|
47
|
+
# On Linux/Mac, we must ensure the binary is executable
|
|
48
|
+
if self.system != "Windows":
|
|
49
|
+
self._ensure_executable(self.chrome_exe)
|
|
50
|
+
|
|
51
|
+
return str(self.chrome_exe.absolute())
|
|
52
|
+
|
|
53
|
+
def _ensure_executable(self, path):
|
|
54
|
+
"""Sets chmod +x on the binary."""
|
|
55
|
+
st = os.stat(path)
|
|
56
|
+
os.chmod(path, st.st_mode | stat.S_IEXEC)
|
|
57
|
+
|
|
58
|
+
def _download_chrome(self):
|
|
59
|
+
# 1. Get latest stable download URL
|
|
60
|
+
api_url = "https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json"
|
|
61
|
+
try:
|
|
62
|
+
resp = requests.get(api_url).json()
|
|
63
|
+
downloads = resp['channels']['Stable']['downloads']['chrome']
|
|
64
|
+
|
|
65
|
+
download_url = next(item['url'] for item in downloads if item['platform'] == self.platform_key)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
raise RuntimeError(f"Failed to fetch download URL from Google API: {e}")
|
|
68
|
+
|
|
69
|
+
self.base_dir.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
zip_path = self.base_dir / "chrome_temp.zip"
|
|
71
|
+
|
|
72
|
+
# 2. Download with progress bar
|
|
73
|
+
logger.info(f"Downloading Chrome ({self.platform_key}) from: {download_url}")
|
|
74
|
+
r = requests.get(download_url, stream=True)
|
|
75
|
+
|
|
76
|
+
with open(zip_path, 'wb') as f:
|
|
77
|
+
for data in r.iter_content(chunk_size=1024):
|
|
78
|
+
f.write(data)
|
|
79
|
+
|
|
80
|
+
# 3. Extract
|
|
81
|
+
logger.info("Extracting browser...")
|
|
82
|
+
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
|
83
|
+
zip_ref.extractall(self.base_dir)
|
|
84
|
+
|
|
85
|
+
# 4. Cleanup
|
|
86
|
+
if zip_path.exists():
|
|
87
|
+
os.remove(zip_path)
|
|
88
|
+
logger.info(f"Browser successfully installed to {self.chrome_exe}")
|
|
89
|
+
|
|
90
|
+
def auth(email = None, password = None):
|
|
91
|
+
auth_data = None
|
|
92
|
+
page = None
|
|
93
|
+
try:
|
|
94
|
+
bm = BrowserManager()
|
|
95
|
+
chrome_path = bm.get_chrome_path()
|
|
96
|
+
|
|
97
|
+
co = ChromiumOptions()
|
|
98
|
+
co.set_browser_path(chrome_path)
|
|
99
|
+
|
|
100
|
+
target_url = 'https://xn--d1ah4a.com/login'
|
|
101
|
+
co.set_argument(f'--app={target_url}')
|
|
102
|
+
|
|
103
|
+
co.incognito()
|
|
104
|
+
|
|
105
|
+
width, height = 450, 700
|
|
106
|
+
co.set_argument(f'--window-size={width},{height}')
|
|
107
|
+
co.set_argument('--window-position=500,200')
|
|
108
|
+
|
|
109
|
+
page = ChromiumPage(co)
|
|
110
|
+
|
|
111
|
+
page.listen.start('auth/sign-in')
|
|
112
|
+
page.get(target_url)
|
|
113
|
+
|
|
114
|
+
logger.info(f"Attempting to autofill login for: {email}")
|
|
115
|
+
|
|
116
|
+
if email:
|
|
117
|
+
email_field = page.ele('#login-email')
|
|
118
|
+
email_field.input(email)
|
|
119
|
+
|
|
120
|
+
if password:
|
|
121
|
+
pass_field = page.ele('#login-password')
|
|
122
|
+
pass_field.input(password)
|
|
123
|
+
|
|
124
|
+
logger.info("Waiting for authentication response")
|
|
125
|
+
|
|
126
|
+
res = None
|
|
127
|
+
try:
|
|
128
|
+
res = page.listen.wait()
|
|
129
|
+
except Exception: pass
|
|
130
|
+
if res:
|
|
131
|
+
target_data = res.response.body
|
|
132
|
+
if isinstance(target_data, dict) and 'accessToken' in target_data:
|
|
133
|
+
token = target_data['accessToken']
|
|
134
|
+
all_raw_cookies = page.run_cdp('Network.getAllCookies')['cookies']
|
|
135
|
+
auth_data = {"token": token, "cookies": all_raw_cookies}
|
|
136
|
+
logger.info("Successfully authenticated!")
|
|
137
|
+
else:
|
|
138
|
+
logger.error("Login failed or response structure changed.")
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.exception(f"Error during auth: {e}")
|
|
142
|
+
finally:
|
|
143
|
+
if page:
|
|
144
|
+
page.quit()
|
|
145
|
+
|
|
146
|
+
return auth_data
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from _io import BufferedReader
|
|
2
|
+
|
|
3
|
+
from requests import Session
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
s = Session()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def fetch(token: str, method: str, url: str, params: dict = {}, files: dict[str, tuple[str, BufferedReader]] = {}, response_schema: Optional[BaseModel] = None):
|
|
11
|
+
base = f'https://xn--d1ah4a.com/api/{url}'
|
|
12
|
+
headers = {
|
|
13
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
14
|
+
"Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
|
|
15
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
16
|
+
"Authorization": 'Bearer ' + token,
|
|
17
|
+
"Sec-GPC": "1",
|
|
18
|
+
"Upgrade-Insecure-Requests": "1",
|
|
19
|
+
"Sec-Fetch-Dest": "document",
|
|
20
|
+
"Sec-Fetch-Mode": "navigate",
|
|
21
|
+
"Sec-Fetch-Site": "none",
|
|
22
|
+
"Sec-Fetch-User": "?1",
|
|
23
|
+
"Priority": "u=0, i",
|
|
24
|
+
"Pragma": "no-cache",
|
|
25
|
+
"Cache-Control": "no-cache",
|
|
26
|
+
"TE": "trailers"
|
|
27
|
+
}
|
|
28
|
+
method = method.lower()
|
|
29
|
+
if method == "get":
|
|
30
|
+
res = s.get(base, timeout=120 if files else 20, params=params, headers=headers)
|
|
31
|
+
else:
|
|
32
|
+
res = s.request(method.upper(), base, timeout=20, json=params, headers=headers, files=files)
|
|
33
|
+
|
|
34
|
+
res.raise_for_status()
|
|
35
|
+
|
|
36
|
+
if res and res.ok and response_schema:
|
|
37
|
+
return response_schema.model_validate(res.json())
|
|
38
|
+
|
|
39
|
+
return res
|
|
40
|
+
|
|
41
|
+
def set_cookies(cookies: str):
|
|
42
|
+
for cookie in cookies.split('; '):
|
|
43
|
+
s.cookies.set(cookie.split('=')[0], cookie.split('=')[-1], path='/', domain='xn--d1ah4a.com.com')
|
|
44
|
+
|
|
45
|
+
def auth_fetch(cookies: str, method: str, url: str, params: dict = {}, token: str | None = None):
|
|
46
|
+
headers = {
|
|
47
|
+
"Host": "xn--d1ah4a.com",
|
|
48
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0",
|
|
49
|
+
"Accept": "*/*",
|
|
50
|
+
"Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
|
|
51
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
52
|
+
"Referer": "https://xn--d1ah4a.com/",
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
"Origin": "https://xn--d1ah4a.com",
|
|
55
|
+
"Sec-GPC": "1",
|
|
56
|
+
"Connection": "keep-alive",
|
|
57
|
+
"Cookie": cookies,
|
|
58
|
+
"Sec-Fetch-Dest": "empty",
|
|
59
|
+
"Sec-Fetch-Mode": "cors",
|
|
60
|
+
"Sec-Fetch-Site": "same-origin",
|
|
61
|
+
"Priority": "u=4",
|
|
62
|
+
"Pragma": "no-cache",
|
|
63
|
+
"Cache-Control": "no-cache",
|
|
64
|
+
"Content-Length": "0",
|
|
65
|
+
"TE": "trailers",
|
|
66
|
+
}
|
|
67
|
+
if token:
|
|
68
|
+
headers['Authorization'] = 'Bearer ' + token
|
|
69
|
+
|
|
70
|
+
if method == 'get':
|
|
71
|
+
res = s.get(f'https://xn--d1ah4a.com/api/{url}', timeout=20, params=params, headers=headers)
|
|
72
|
+
else:
|
|
73
|
+
res = s.request(method, f'https://xn--d1ah4a.com/api/{url}', timeout=20, json=params, headers=headers)
|
|
74
|
+
res.raise_for_status()
|
|
75
|
+
return res.json()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "itd-iter-api"
|
|
7
|
+
version = 'v1.0.2'
|
|
8
|
+
description = "ITD client for python"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "firedotguy", email = "nta16022013@gmail.com" },
|
|
12
|
+
{ name = "sharkow" }
|
|
13
|
+
]
|
|
14
|
+
license = "MIT"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"requests"
|
|
17
|
+
]
|
|
18
|
+
requires-python = ">=3.9"
|