aiodot 1.0.0__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.
- aiodot-1.0.0/LICENSE +21 -0
- aiodot-1.0.0/PKG-INFO +150 -0
- aiodot-1.0.0/README.md +125 -0
- aiodot-1.0.0/aiodot/__init__.py +37 -0
- aiodot-1.0.0/aiodot/auth.py +153 -0
- aiodot-1.0.0/aiodot/client.py +307 -0
- aiodot-1.0.0/aiodot/models.py +185 -0
- aiodot-1.0.0/aiodot.egg-info/PKG-INFO +150 -0
- aiodot-1.0.0/aiodot.egg-info/SOURCES.txt +13 -0
- aiodot-1.0.0/aiodot.egg-info/dependency_links.txt +1 -0
- aiodot-1.0.0/aiodot.egg-info/requires.txt +2 -0
- aiodot-1.0.0/aiodot.egg-info/top_level.txt +1 -0
- aiodot-1.0.0/pyproject.toml +29 -0
- aiodot-1.0.0/setup.cfg +4 -0
- aiodot-1.0.0/setup.py +43 -0
aiodot-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 karbaladev.ir
|
|
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.
|
aiodot-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aiodot
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Async Python client for MyDot.one social platform
|
|
5
|
+
Home-page: https://github.com/karbaladev/aiodot
|
|
6
|
+
Author: karbaladev.ir
|
|
7
|
+
Author-email: "karbaladev.ir" <saleh.mohammadi.az@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://karbaladev.ir
|
|
10
|
+
Project-URL: Repository, https://github.com/karbaladevir/aiodot
|
|
11
|
+
Project-URL: Issues, https://github.com/karbaladevir/aiodot/issues
|
|
12
|
+
Keywords: mydot,aiodot,mydot-bot,social-media,async,bot
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: aiohttp>=3.8.0
|
|
20
|
+
Requires-Dist: requests>=2.28.0
|
|
21
|
+
Dynamic: author
|
|
22
|
+
Dynamic: home-page
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
Dynamic: requires-python
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<img src="https://abrehamrahi.ir/o/public/rNf0ej8l/" alt="aiodot logo" width="200">
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
<h1 align="center">aiodot 🚀</h1>
|
|
31
|
+
<p align="center">
|
|
32
|
+
<b>Async Python client for MyDot.one social platform</b><br>
|
|
33
|
+
Build bots, automation, and tools with ease.
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
<p align="center">
|
|
37
|
+
<a href="https://pypi.org/project/aiodot/">
|
|
38
|
+
<img src="https://badge.fury.io/py/aiodot.svg" alt="PyPI version">
|
|
39
|
+
</a>
|
|
40
|
+
<a href="https://pypi.org/project/aiodot/">
|
|
41
|
+
<img src="https://img.shields.io/pypi/pyversions/aiodot.svg" alt="Python">
|
|
42
|
+
</a>
|
|
43
|
+
<a href="https://github.com/karbaladev/aiodot/blob/main/LICENSE">
|
|
44
|
+
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT">
|
|
45
|
+
</a>
|
|
46
|
+
</p>
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
**`aiodot`** is an asynchronous Python library for [MyDot.one](https://mydot.one). Built with `aiohttp` — fast, async-first, session-based like aiogram.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## ✨ Features
|
|
55
|
+
|
|
56
|
+
- ⚡ **Async-first** — Built on aiohttp
|
|
57
|
+
- 💾 **Session persistence** — Login once, auto-loads next time
|
|
58
|
+
- 🔄 **Auto token refresh** — Handles 401 gracefully
|
|
59
|
+
- 📦 **50+ endpoints** — Full API coverage
|
|
60
|
+
- 🎯 **Type hints** — Full type annotations
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 📦 Installation
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install aiodot
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 💡 Quick Start
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import asyncio
|
|
76
|
+
from aiodot import MyDotClient
|
|
77
|
+
|
|
78
|
+
TOKEN = "your-token" # F12 → Cookies → __Secure-access_token
|
|
79
|
+
|
|
80
|
+
async def main():
|
|
81
|
+
async with MyDotClient(token=TOKEN, session_file="user.session.json") as client:
|
|
82
|
+
me = await client.get_me()
|
|
83
|
+
print(f"@{me.username}")
|
|
84
|
+
|
|
85
|
+
dot = await client.create_dot("Hello aiodot! 🚀")
|
|
86
|
+
print(dot.url)
|
|
87
|
+
|
|
88
|
+
asyncio.run(main())
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 🤖 Echo Bot
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
import asyncio
|
|
97
|
+
from aiodot import MyDotClient
|
|
98
|
+
|
|
99
|
+
TOKEN = "your-token"
|
|
100
|
+
|
|
101
|
+
async def main():
|
|
102
|
+
processed = set()
|
|
103
|
+
async with MyDotClient(token=TOKEN, session_file="bot.session.json") as c:
|
|
104
|
+
print(f"🤖 @{(await c.get_me()).username} started!")
|
|
105
|
+
while True:
|
|
106
|
+
for n in (await c.get_notifications(10)).get("results", []):
|
|
107
|
+
if n.get("id") in processed: continue
|
|
108
|
+
processed.add(n.get("id"))
|
|
109
|
+
if n.get("type") == "mention":
|
|
110
|
+
d = n.get("dot", {})
|
|
111
|
+
if d.get("id"):
|
|
112
|
+
await c.reply(d["id"], "Hello! 🤖")
|
|
113
|
+
await asyncio.sleep(15)
|
|
114
|
+
|
|
115
|
+
asyncio.run(main())
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 📖 API Reference
|
|
121
|
+
|
|
122
|
+
| Category | Methods |
|
|
123
|
+
|----------|---------|
|
|
124
|
+
| **Profile** | `get_me()`, `update_profile()`, `set_visibility()` |
|
|
125
|
+
| **Dots** | `create_dot()`, `get_dot()`, `reply()`, `repost()`, `quote()`, `like()`, `bookmark()`, `edit_dot()`, `delete_dot()` |
|
|
126
|
+
| **Social** | `follow()`, `unfollow()`, `block()`, `unblock()`, `mute()`, `unmute()` |
|
|
127
|
+
| **Users** | `search_users()`, `get_user_followers()`, `get_user_following()`, `get_user_dots()` |
|
|
128
|
+
| **Feed** | `home_feed()`, `following_feed()`, `follow_suggestions()` |
|
|
129
|
+
| **Notifications** | `get_notifications()`, `mark_all_read()`, `get_notification_preferences()` |
|
|
130
|
+
| **Trending** | `get_trending_hashtags()`, `get_trending_media()` |
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## ⚠️ Disclaimer
|
|
135
|
+
|
|
136
|
+
Unofficial library. Use responsibly per MyDot.one Terms of Service.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 📡 Community
|
|
141
|
+
|
|
142
|
+
- 📢 **Telegram:** [@aiodotlib](https://t.me/aiodotlib)
|
|
143
|
+
- 💬 **Bale:** [ble.ir/aiodot](https://ble.ir/aiodot)
|
|
144
|
+
- 🌐 **Website:** [karbaladev.ir](https://karbaladev.ir)
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 📄 License
|
|
149
|
+
|
|
150
|
+
MIT © [karbaladev.ir](https://karbaladev.ir)
|
aiodot-1.0.0/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://abrehamrahi.ir/o/public/rNf0ej8l/" alt="aiodot logo" width="200">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">aiodot 🚀</h1>
|
|
6
|
+
<p align="center">
|
|
7
|
+
<b>Async Python client for MyDot.one social platform</b><br>
|
|
8
|
+
Build bots, automation, and tools with ease.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://pypi.org/project/aiodot/">
|
|
13
|
+
<img src="https://badge.fury.io/py/aiodot.svg" alt="PyPI version">
|
|
14
|
+
</a>
|
|
15
|
+
<a href="https://pypi.org/project/aiodot/">
|
|
16
|
+
<img src="https://img.shields.io/pypi/pyversions/aiodot.svg" alt="Python">
|
|
17
|
+
</a>
|
|
18
|
+
<a href="https://github.com/karbaladev/aiodot/blob/main/LICENSE">
|
|
19
|
+
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT">
|
|
20
|
+
</a>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
**`aiodot`** is an asynchronous Python library for [MyDot.one](https://mydot.one). Built with `aiohttp` — fast, async-first, session-based like aiogram.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## ✨ Features
|
|
30
|
+
|
|
31
|
+
- ⚡ **Async-first** — Built on aiohttp
|
|
32
|
+
- 💾 **Session persistence** — Login once, auto-loads next time
|
|
33
|
+
- 🔄 **Auto token refresh** — Handles 401 gracefully
|
|
34
|
+
- 📦 **50+ endpoints** — Full API coverage
|
|
35
|
+
- 🎯 **Type hints** — Full type annotations
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 📦 Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install aiodot
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 💡 Quick Start
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import asyncio
|
|
51
|
+
from aiodot import MyDotClient
|
|
52
|
+
|
|
53
|
+
TOKEN = "your-token" # F12 → Cookies → __Secure-access_token
|
|
54
|
+
|
|
55
|
+
async def main():
|
|
56
|
+
async with MyDotClient(token=TOKEN, session_file="user.session.json") as client:
|
|
57
|
+
me = await client.get_me()
|
|
58
|
+
print(f"@{me.username}")
|
|
59
|
+
|
|
60
|
+
dot = await client.create_dot("Hello aiodot! 🚀")
|
|
61
|
+
print(dot.url)
|
|
62
|
+
|
|
63
|
+
asyncio.run(main())
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 🤖 Echo Bot
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import asyncio
|
|
72
|
+
from aiodot import MyDotClient
|
|
73
|
+
|
|
74
|
+
TOKEN = "your-token"
|
|
75
|
+
|
|
76
|
+
async def main():
|
|
77
|
+
processed = set()
|
|
78
|
+
async with MyDotClient(token=TOKEN, session_file="bot.session.json") as c:
|
|
79
|
+
print(f"🤖 @{(await c.get_me()).username} started!")
|
|
80
|
+
while True:
|
|
81
|
+
for n in (await c.get_notifications(10)).get("results", []):
|
|
82
|
+
if n.get("id") in processed: continue
|
|
83
|
+
processed.add(n.get("id"))
|
|
84
|
+
if n.get("type") == "mention":
|
|
85
|
+
d = n.get("dot", {})
|
|
86
|
+
if d.get("id"):
|
|
87
|
+
await c.reply(d["id"], "Hello! 🤖")
|
|
88
|
+
await asyncio.sleep(15)
|
|
89
|
+
|
|
90
|
+
asyncio.run(main())
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 📖 API Reference
|
|
96
|
+
|
|
97
|
+
| Category | Methods |
|
|
98
|
+
|----------|---------|
|
|
99
|
+
| **Profile** | `get_me()`, `update_profile()`, `set_visibility()` |
|
|
100
|
+
| **Dots** | `create_dot()`, `get_dot()`, `reply()`, `repost()`, `quote()`, `like()`, `bookmark()`, `edit_dot()`, `delete_dot()` |
|
|
101
|
+
| **Social** | `follow()`, `unfollow()`, `block()`, `unblock()`, `mute()`, `unmute()` |
|
|
102
|
+
| **Users** | `search_users()`, `get_user_followers()`, `get_user_following()`, `get_user_dots()` |
|
|
103
|
+
| **Feed** | `home_feed()`, `following_feed()`, `follow_suggestions()` |
|
|
104
|
+
| **Notifications** | `get_notifications()`, `mark_all_read()`, `get_notification_preferences()` |
|
|
105
|
+
| **Trending** | `get_trending_hashtags()`, `get_trending_media()` |
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## ⚠️ Disclaimer
|
|
110
|
+
|
|
111
|
+
Unofficial library. Use responsibly per MyDot.one Terms of Service.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 📡 Community
|
|
116
|
+
|
|
117
|
+
- 📢 **Telegram:** [@aiodotlib](https://t.me/aiodotlib)
|
|
118
|
+
- 💬 **Bale:** [ble.ir/aiodot](https://ble.ir/aiodot)
|
|
119
|
+
- 🌐 **Website:** [karbaladev.ir](https://karbaladev.ir)
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 📄 License
|
|
124
|
+
|
|
125
|
+
MIT © [karbaladev.ir](https://karbaladev.ir)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# ==========================================
|
|
2
|
+
# aiodot/__init__.py
|
|
3
|
+
# ==========================================
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
aiodot - Python client for MyDot.one social platform.
|
|
7
|
+
Build bots, automation, and tools with ease.
|
|
8
|
+
|
|
9
|
+
Author: karbaladev.ir
|
|
10
|
+
License: MIT
|
|
11
|
+
Version: 1.0.0
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
from aiodot import MyDotClient
|
|
15
|
+
|
|
16
|
+
client = MyDotClient(token="your_token")
|
|
17
|
+
me = client.me
|
|
18
|
+
dot = client.create_dot("Hello from aiodot!")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from .client import MyDotClient
|
|
22
|
+
from .auth import MyDotAuth, Session
|
|
23
|
+
from .models import Dot, User, ThreadView, Notification, PaginatedResponse
|
|
24
|
+
|
|
25
|
+
__version__ = "1.0.0"
|
|
26
|
+
__author__ = "karbaladev.ir"
|
|
27
|
+
__license__ = "MIT"
|
|
28
|
+
__all__ = [
|
|
29
|
+
"MyDotClient",
|
|
30
|
+
"MyDotAuth",
|
|
31
|
+
"Session",
|
|
32
|
+
"Dot",
|
|
33
|
+
"User",
|
|
34
|
+
"ThreadView",
|
|
35
|
+
"Notification",
|
|
36
|
+
"PaginatedResponse",
|
|
37
|
+
]
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# ==========================================
|
|
2
|
+
# aiodot/auth.py
|
|
3
|
+
# ==========================================
|
|
4
|
+
|
|
5
|
+
"""Authentication module for MyDot.one. Token-based with session file."""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional, Dict, Any
|
|
11
|
+
from dataclasses import dataclass, asdict, field
|
|
12
|
+
|
|
13
|
+
import aiohttp
|
|
14
|
+
import requests
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Session:
|
|
19
|
+
"""Session data saved to disk."""
|
|
20
|
+
token: str = ""
|
|
21
|
+
user_id: str = ""
|
|
22
|
+
username: str = ""
|
|
23
|
+
display_name: str = ""
|
|
24
|
+
phone: str = ""
|
|
25
|
+
created_at: float = field(default_factory=time.time)
|
|
26
|
+
|
|
27
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
28
|
+
return asdict(self)
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Session":
|
|
32
|
+
valid = {k: v for k, v in data.items() if k in cls.__dataclass_fields__}
|
|
33
|
+
return cls(**valid)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class MyDotAuth:
|
|
37
|
+
"""Auth handler for MyDot.one."""
|
|
38
|
+
|
|
39
|
+
BASE_URL = "https://api.mydot.one/mydot/api/v1"
|
|
40
|
+
|
|
41
|
+
def __init__(self, session_file: Optional[str] = None):
|
|
42
|
+
if session_file is None:
|
|
43
|
+
self.session_file = Path.home() / ".mydot" / "session.json"
|
|
44
|
+
else:
|
|
45
|
+
self.session_file = Path(session_file)
|
|
46
|
+
|
|
47
|
+
self.session_file.parent.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
self.session_data = Session()
|
|
49
|
+
self._loaded = False
|
|
50
|
+
|
|
51
|
+
if self.session_file.exists():
|
|
52
|
+
self._load()
|
|
53
|
+
|
|
54
|
+
def _load(self) -> bool:
|
|
55
|
+
try:
|
|
56
|
+
with open(self.session_file, "r", encoding="utf-8") as f:
|
|
57
|
+
data = json.load(f)
|
|
58
|
+
self.session_data = Session.from_dict(data)
|
|
59
|
+
self._loaded = bool(self.session_data.token)
|
|
60
|
+
return True
|
|
61
|
+
except Exception:
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
def save(self) -> None:
|
|
65
|
+
with open(self.session_file, "w", encoding="utf-8") as f:
|
|
66
|
+
json.dump(self.session_data.to_dict(), f, indent=2, ensure_ascii=False)
|
|
67
|
+
|
|
68
|
+
def clear(self) -> None:
|
|
69
|
+
self.session_data = Session()
|
|
70
|
+
if self.session_file.exists():
|
|
71
|
+
self.session_file.unlink()
|
|
72
|
+
|
|
73
|
+
# ============ Sync Login (called once during init) ============
|
|
74
|
+
|
|
75
|
+
def login_with_token_sync(self, token: str) -> bool:
|
|
76
|
+
"""Sync token validation. Called once during MyDotClient init."""
|
|
77
|
+
self.session_data.token = token
|
|
78
|
+
resp = requests.get(
|
|
79
|
+
f"{self.BASE_URL}/auth/profile/",
|
|
80
|
+
cookies={"__Secure-access_token": token},
|
|
81
|
+
headers=self._get_headers_sync(),
|
|
82
|
+
)
|
|
83
|
+
if resp.ok:
|
|
84
|
+
p = resp.json()
|
|
85
|
+
self.session_data.user_id = p.get("user_id", "")
|
|
86
|
+
self.session_data.username = p.get("username", "")
|
|
87
|
+
self.session_data.display_name = p.get("display_name", "")
|
|
88
|
+
self._loaded = True
|
|
89
|
+
self.save()
|
|
90
|
+
print(f"✅ Token saved! @{self.username}")
|
|
91
|
+
return True
|
|
92
|
+
print("❌ Invalid token")
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
# ============ Async Token Refresh ============
|
|
96
|
+
|
|
97
|
+
async def refresh(self) -> bool:
|
|
98
|
+
"""Async token refresh using aiohttp."""
|
|
99
|
+
headers = self._get_headers_async()
|
|
100
|
+
headers["Cookie"] = f"__Secure-access_token={self.token}"
|
|
101
|
+
async with aiohttp.ClientSession() as s:
|
|
102
|
+
async with s.post(f"{self.BASE_URL}/auth/token/refresh/", headers=headers, json={}) as resp:
|
|
103
|
+
if resp.status == 200:
|
|
104
|
+
for key, cookie in resp.cookies.items():
|
|
105
|
+
if key == "__Secure-access_token":
|
|
106
|
+
self.session_data.token = cookie.value
|
|
107
|
+
self.save()
|
|
108
|
+
print("🔄 Token refreshed")
|
|
109
|
+
return True
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
# ============ Headers ============
|
|
113
|
+
|
|
114
|
+
def _get_headers_sync(self) -> Dict[str, str]:
|
|
115
|
+
return {
|
|
116
|
+
"Accept": "application/json, text/plain, */*",
|
|
117
|
+
"Origin": "https://mydot.one",
|
|
118
|
+
"Referer": "https://mydot.one/",
|
|
119
|
+
"User-Agent": "Mozilla/5.0",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
def _get_headers_async(self) -> Dict[str, str]:
|
|
123
|
+
return {
|
|
124
|
+
"Accept": "application/json, text/plain, */*",
|
|
125
|
+
"Origin": "https://mydot.one",
|
|
126
|
+
"Referer": "https://mydot.one/",
|
|
127
|
+
"User-Agent": "Mozilla/5.0",
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# ============ Properties ============
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def token(self) -> Optional[str]:
|
|
134
|
+
return self.session_data.token
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def user_id(self) -> Optional[str]:
|
|
138
|
+
return self.session_data.user_id
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def username(self) -> Optional[str]:
|
|
142
|
+
return self.session_data.username
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def display_name(self) -> Optional[str]:
|
|
146
|
+
return self.session_data.display_name
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def is_logged_in(self) -> bool:
|
|
150
|
+
return self._loaded and bool(self.token)
|
|
151
|
+
|
|
152
|
+
def __repr__(self) -> str:
|
|
153
|
+
return f"MyDotAuth(@{self.username})" if self.username else "MyDotAuth(not logged in)"
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# ==========================================
|
|
2
|
+
# aiodot/client.py
|
|
3
|
+
# ==========================================
|
|
4
|
+
|
|
5
|
+
"""Main MyDot.one API Client. Async version built on aiohttp."""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, Dict, List, Any
|
|
8
|
+
import aiohttp
|
|
9
|
+
from .models import Dot, User, ThreadView, Notification, PaginatedResponse
|
|
10
|
+
from .auth import MyDotAuth
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MyDotClient:
|
|
14
|
+
"""Async API client for MyDot.one."""
|
|
15
|
+
|
|
16
|
+
BASE_URL = "https://api.mydot.one/mydot/api/v1"
|
|
17
|
+
|
|
18
|
+
def __init__(self, token: Optional[str] = None, session_file: Optional[str] = None, auth: Optional[MyDotAuth] = None):
|
|
19
|
+
if auth:
|
|
20
|
+
self.auth = auth
|
|
21
|
+
elif token:
|
|
22
|
+
self.auth = MyDotAuth(session_file=session_file)
|
|
23
|
+
self.auth.login_with_token_sync(token)
|
|
24
|
+
else:
|
|
25
|
+
self.auth = MyDotAuth(session_file=session_file)
|
|
26
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
27
|
+
|
|
28
|
+
async def __aenter__(self):
|
|
29
|
+
self._session = aiohttp.ClientSession()
|
|
30
|
+
return self
|
|
31
|
+
|
|
32
|
+
async def __aexit__(self, *args):
|
|
33
|
+
if self._session:
|
|
34
|
+
await self._session.close()
|
|
35
|
+
self._session = None
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def http(self) -> aiohttp.ClientSession:
|
|
39
|
+
if self._session is None:
|
|
40
|
+
raise RuntimeError("Use 'async with MyDotClient(...) as client:' block.")
|
|
41
|
+
return self._session
|
|
42
|
+
|
|
43
|
+
def _headers(self) -> Dict[str, str]:
|
|
44
|
+
h = {
|
|
45
|
+
"Accept": "application/json, text/plain, */*",
|
|
46
|
+
"Origin": "https://mydot.one",
|
|
47
|
+
"Referer": "https://mydot.one/",
|
|
48
|
+
"User-Agent": "Mozilla/5.0",
|
|
49
|
+
}
|
|
50
|
+
if self.auth.token:
|
|
51
|
+
h["Cookie"] = f"__Secure-access_token={self.auth.token}"
|
|
52
|
+
return h
|
|
53
|
+
|
|
54
|
+
async def _get(self, path: str, params: Dict = None) -> Any:
|
|
55
|
+
url = f"{self.BASE_URL}{path}"
|
|
56
|
+
async with self.http.get(url, params=params, headers=self._headers()) as resp:
|
|
57
|
+
if resp.status == 401 and await self.auth.refresh():
|
|
58
|
+
async with self.http.get(url, params=params, headers=self._headers()) as r:
|
|
59
|
+
r.raise_for_status()
|
|
60
|
+
ct = r.content_type
|
|
61
|
+
return await r.json() if "json" in ct else await r.text()
|
|
62
|
+
resp.raise_for_status()
|
|
63
|
+
ct = resp.content_type
|
|
64
|
+
return await resp.json() if "json" in ct else await resp.text()
|
|
65
|
+
|
|
66
|
+
async def _post(self, path: str, json: Dict = None) -> Any:
|
|
67
|
+
url = f"{self.BASE_URL}{path}"
|
|
68
|
+
async with self.http.post(url, json=json, headers=self._headers()) as resp:
|
|
69
|
+
if resp.status == 401 and await self.auth.refresh():
|
|
70
|
+
async with self.http.post(url, json=json, headers=self._headers()) as r:
|
|
71
|
+
r.raise_for_status()
|
|
72
|
+
ct = r.content_type
|
|
73
|
+
return await r.json() if "json" in ct else await r.text()
|
|
74
|
+
resp.raise_for_status()
|
|
75
|
+
ct = resp.content_type
|
|
76
|
+
return await resp.json() if "json" in ct else await resp.text()
|
|
77
|
+
|
|
78
|
+
async def _patch(self, path: str, json: Dict = None) -> Any:
|
|
79
|
+
url = f"{self.BASE_URL}{path}"
|
|
80
|
+
async with self.http.patch(url, json=json, headers=self._headers()) as resp:
|
|
81
|
+
if resp.status == 401 and await self.auth.refresh():
|
|
82
|
+
async with self.http.patch(url, json=json, headers=self._headers()) as r:
|
|
83
|
+
r.raise_for_status()
|
|
84
|
+
ct = r.content_type
|
|
85
|
+
return await r.json() if "json" in ct else await r.text()
|
|
86
|
+
resp.raise_for_status()
|
|
87
|
+
ct = resp.content_type
|
|
88
|
+
return await resp.json() if "json" in ct else await resp.text()
|
|
89
|
+
|
|
90
|
+
async def _delete(self, path: str) -> Any:
|
|
91
|
+
url = f"{self.BASE_URL}{path}"
|
|
92
|
+
async with self.http.delete(url, headers=self._headers()) as resp:
|
|
93
|
+
if resp.status == 401 and await self.auth.refresh():
|
|
94
|
+
async with self.http.delete(url, headers=self._headers()) as r:
|
|
95
|
+
r.raise_for_status()
|
|
96
|
+
ct = r.content_type
|
|
97
|
+
return await r.json() if "json" in ct else await r.text()
|
|
98
|
+
resp.raise_for_status()
|
|
99
|
+
ct = resp.content_type
|
|
100
|
+
return await resp.json() if "json" in ct else await resp.text()
|
|
101
|
+
|
|
102
|
+
# ============ Profile (4) ============
|
|
103
|
+
async def get_me(self) -> User:
|
|
104
|
+
data = await self._get("/auth/profile/")
|
|
105
|
+
return User.from_dict(data)
|
|
106
|
+
|
|
107
|
+
async def update_profile(self, **kwargs) -> User:
|
|
108
|
+
data = await self._patch("/auth/profile/", json=kwargs)
|
|
109
|
+
return User.from_dict(data)
|
|
110
|
+
|
|
111
|
+
async def get_profile_visibility(self) -> Dict:
|
|
112
|
+
return await self._get("/auth/profile/visibility/")
|
|
113
|
+
|
|
114
|
+
async def set_profile_visibility(self, visibility: str) -> Dict:
|
|
115
|
+
return await self._patch("/auth/profile/visibility/", json={"profile_visibility": visibility})
|
|
116
|
+
|
|
117
|
+
# ============ User Actions (6) ============
|
|
118
|
+
async def follow(self, user_id: str) -> bool:
|
|
119
|
+
await self._post("/auth/follow/", json={"target_id": user_id})
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
async def unfollow(self, user_id: str) -> bool:
|
|
123
|
+
await self._post("/auth/unfollow/", json={"target_id": user_id})
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
async def block(self, user_id: str) -> bool:
|
|
127
|
+
await self._post("/auth/block/", json={"target_id": user_id})
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
async def unblock(self, user_id: str) -> bool:
|
|
131
|
+
await self._post("/auth/unblock/", json={"target_id": user_id})
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
async def mute(self, user_id: str) -> bool:
|
|
135
|
+
await self._post("/auth/mute/", json={"target_id": user_id})
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
async def unmute(self, user_id: str) -> bool:
|
|
139
|
+
await self._post("/auth/unmute/", json={"target_id": user_id})
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
# ============ User Info (3) ============
|
|
143
|
+
async def get_user_followers(self, username: str, page_size: int = 20) -> Dict:
|
|
144
|
+
return await self._get(f"/auth/{username}/followers/", params={"page_size": page_size})
|
|
145
|
+
|
|
146
|
+
async def get_user_following(self, username: str, page_size: int = 20) -> Dict:
|
|
147
|
+
return await self._get(f"/auth/{username}/following/", params={"page_size": page_size})
|
|
148
|
+
|
|
149
|
+
async def search_users(self, query: str) -> Any:
|
|
150
|
+
return await self._get("/auth/profile/search", params={"q": query})
|
|
151
|
+
|
|
152
|
+
# ============ Dots - Read (9) ============
|
|
153
|
+
async def get_dot(self, dot_id: str) -> Dot:
|
|
154
|
+
data = await self._get(f"/dots/{dot_id}/")
|
|
155
|
+
return Dot.from_dict(data)
|
|
156
|
+
|
|
157
|
+
async def get_thread_view(self, dot_id: str) -> ThreadView:
|
|
158
|
+
data = await self._get(f"/dots/{dot_id}/thread-view/")
|
|
159
|
+
return ThreadView.from_dict(data)
|
|
160
|
+
|
|
161
|
+
async def get_replies(self, dot_id: str, page: int = 1, limit: int = 20) -> Dict:
|
|
162
|
+
return await self._get(f"/dots/{dot_id}/replies/", params={"page": page, "limit": limit})
|
|
163
|
+
|
|
164
|
+
async def get_reposts(self, dot_id: str) -> Dict:
|
|
165
|
+
return await self._get(f"/dots/{dot_id}/reposts/")
|
|
166
|
+
|
|
167
|
+
async def get_quotes(self, dot_id: str) -> Dict:
|
|
168
|
+
return await self._get(f"/dots/{dot_id}/quotes/")
|
|
169
|
+
|
|
170
|
+
async def get_dot_likes(self, dot_id: str) -> Any:
|
|
171
|
+
return await self._get(f"/dots/{dot_id}/likes/")
|
|
172
|
+
|
|
173
|
+
async def get_user_dots(self, user_id: str, dot_type: str = "dot,quote,repost", limit: int = 20) -> Dict:
|
|
174
|
+
return await self._get(f"/dots/user/{user_id}/", params={"dot_type": dot_type, "limit": limit})
|
|
175
|
+
|
|
176
|
+
async def get_user_replies(self, user_id: str) -> Dict:
|
|
177
|
+
return await self._get(f"/dots/user/{user_id}/", params={"dot_type": "reply"})
|
|
178
|
+
|
|
179
|
+
async def get_user_likes(self, user_id: str) -> Dict:
|
|
180
|
+
return await self._get(f"/dots/user/{user_id}/activity/", params={"action_type": "like"})
|
|
181
|
+
|
|
182
|
+
# ============ Dots - Create (4) ============
|
|
183
|
+
async def create_dot(self, content: str, dot_type: str = "dot", reply_to: str = None,
|
|
184
|
+
repost_of: str = None, quote_of: str = None,
|
|
185
|
+
media_ids: List[str] = None, reply_permission: str = "everyone") -> Dot:
|
|
186
|
+
body = {k: v for k, v in {
|
|
187
|
+
"dot_type": dot_type, "content": content,
|
|
188
|
+
"reply_to": reply_to, "repost_of": repost_of, "quote_of": quote_of,
|
|
189
|
+
"media_ids": media_ids or [], "reply_permission": reply_permission
|
|
190
|
+
}.items() if v is not None}
|
|
191
|
+
data = await self._post("/dots/", json=body)
|
|
192
|
+
return Dot.from_dict(data)
|
|
193
|
+
|
|
194
|
+
async def reply(self, dot_id: str, content: str) -> Dot:
|
|
195
|
+
return await self.create_dot(content=content, dot_type="reply", reply_to=dot_id)
|
|
196
|
+
|
|
197
|
+
async def repost(self, dot_id: str, media_ids: List[str] = None) -> Dot:
|
|
198
|
+
return await self.create_dot(content="", dot_type="repost", repost_of=dot_id, media_ids=media_ids)
|
|
199
|
+
|
|
200
|
+
async def quote(self, dot_id: str, content: str) -> Dot:
|
|
201
|
+
return await self.create_dot(content=content, dot_type="quote", quote_of=dot_id)
|
|
202
|
+
|
|
203
|
+
# ============ Dots - Actions (10) ============
|
|
204
|
+
async def like(self, dot_id: str) -> bool:
|
|
205
|
+
await self._post(f"/dots/{dot_id}/like/")
|
|
206
|
+
return True
|
|
207
|
+
|
|
208
|
+
async def unlike(self, dot_id: str) -> bool:
|
|
209
|
+
await self._delete(f"/dots/{dot_id}/like/")
|
|
210
|
+
return True
|
|
211
|
+
|
|
212
|
+
async def bookmark(self, dot_id: str) -> bool:
|
|
213
|
+
await self._post(f"/dots/{dot_id}/bookmark/")
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
async def unbookmark(self, dot_id: str) -> bool:
|
|
217
|
+
await self._delete(f"/dots/{dot_id}/bookmark/")
|
|
218
|
+
return True
|
|
219
|
+
|
|
220
|
+
async def do_repost(self, dot_id: str) -> bool:
|
|
221
|
+
await self._post(f"/dots/{dot_id}/repost/")
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
async def undo_repost(self, dot_id: str) -> bool:
|
|
225
|
+
await self._delete(f"/dots/{dot_id}/repost/")
|
|
226
|
+
return True
|
|
227
|
+
|
|
228
|
+
async def edit_dot(self, dot_id: str, content: str) -> Dot:
|
|
229
|
+
data = await self._patch(f"/dots/{dot_id}/", json={"content": content})
|
|
230
|
+
return Dot.from_dict(data)
|
|
231
|
+
|
|
232
|
+
async def delete_dot(self, dot_id: str) -> bool:
|
|
233
|
+
await self._delete(f"/dots/{dot_id}/")
|
|
234
|
+
return True
|
|
235
|
+
|
|
236
|
+
# ============ Feed (3) ============
|
|
237
|
+
async def home_feed(self, page_size: int = 40) -> Dict:
|
|
238
|
+
return await self._get("/feed/suggestions/timeline/", params={"page_size": page_size})
|
|
239
|
+
|
|
240
|
+
async def following_feed(self, **params) -> Dict:
|
|
241
|
+
return await self._get("/feed/following/", params=params)
|
|
242
|
+
|
|
243
|
+
async def follow_suggestions(self, cursor: str = None, page_size: int = 20) -> Dict:
|
|
244
|
+
p = {"page_size": page_size}
|
|
245
|
+
if cursor:
|
|
246
|
+
p["cursor"] = cursor
|
|
247
|
+
return await self._get("/feed/suggestions/follow/", params=p)
|
|
248
|
+
|
|
249
|
+
# ============ Notifications (4) ============
|
|
250
|
+
async def get_notifications(self, page_size: int = 50) -> Dict:
|
|
251
|
+
return await self._get("/notifications/", params={"page_size": page_size})
|
|
252
|
+
|
|
253
|
+
async def get_notification_preferences(self) -> Dict:
|
|
254
|
+
return await self._get("/notifications/preferences/")
|
|
255
|
+
|
|
256
|
+
async def update_notification_preferences(self, **kwargs) -> Dict:
|
|
257
|
+
return await self._patch("/notifications/preferences/", json=kwargs)
|
|
258
|
+
|
|
259
|
+
async def mark_all_notifications_read(self) -> bool:
|
|
260
|
+
await self._post("/notifications/read-all/")
|
|
261
|
+
return True
|
|
262
|
+
|
|
263
|
+
# ============ Bookmarks (1) ============
|
|
264
|
+
async def get_bookmarks(self, limit: int = 20) -> Dict:
|
|
265
|
+
return await self._get("/dots/bookmarks/", params={"limit": limit})
|
|
266
|
+
|
|
267
|
+
# ============ Trending (2) ============
|
|
268
|
+
async def get_trending_hashtags(self) -> Any:
|
|
269
|
+
return await self._get("/dots/hashtags/trending/")
|
|
270
|
+
|
|
271
|
+
async def get_trending_media(self, limit: int = 24) -> Dict:
|
|
272
|
+
return await self._get("/dots/media/trending/", params={"limit": limit})
|
|
273
|
+
|
|
274
|
+
# ============ Media Upload (1) ============
|
|
275
|
+
async def upload_media(self, file_path: str) -> Dict:
|
|
276
|
+
url = f"{self.BASE_URL}/posts/media/"
|
|
277
|
+
with open(file_path, "rb") as f:
|
|
278
|
+
form = aiohttp.FormData()
|
|
279
|
+
form.add_field("file", f)
|
|
280
|
+
async with self.http.post(url, data=form, headers=self._headers()) as resp:
|
|
281
|
+
resp.raise_for_status()
|
|
282
|
+
return await resp.json()
|
|
283
|
+
|
|
284
|
+
# ============ Other (6) ============
|
|
285
|
+
async def get_alerts(self) -> Any:
|
|
286
|
+
return await self._get("/utils/alerts/active/")
|
|
287
|
+
|
|
288
|
+
async def get_invites(self) -> Dict:
|
|
289
|
+
return await self._get("/auth/invites/")
|
|
290
|
+
|
|
291
|
+
async def get_wallets(self) -> Dict:
|
|
292
|
+
return await self._get("/auth/wallets/")
|
|
293
|
+
|
|
294
|
+
async def get_2fa_state(self) -> Dict:
|
|
295
|
+
return await self._get("/auth/2fa/state/")
|
|
296
|
+
|
|
297
|
+
async def get_star_settings(self) -> Dict:
|
|
298
|
+
return await self._get("/reward/default-stars/")
|
|
299
|
+
|
|
300
|
+
async def get_star_transactions(self) -> Dict:
|
|
301
|
+
return await self._get("/reward/stars-transaction/")
|
|
302
|
+
|
|
303
|
+
# ============ Convenience (1) ============
|
|
304
|
+
async def get_dot_url(self, short_id: str, username: str = None) -> str:
|
|
305
|
+
if username is None:
|
|
306
|
+
username = self.auth.username
|
|
307
|
+
return f"https://mydot.one/@{username}/status/{short_id}"
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# ==========================================
|
|
2
|
+
# aiodot/models.py
|
|
3
|
+
# ==========================================
|
|
4
|
+
|
|
5
|
+
"""Data models for MyDot API responses."""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, Dict, List, Any
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class User:
|
|
13
|
+
"""MyDot user profile."""
|
|
14
|
+
user_id: str = ""
|
|
15
|
+
username: str = ""
|
|
16
|
+
display_name: str = ""
|
|
17
|
+
bio: str = ""
|
|
18
|
+
location: str = ""
|
|
19
|
+
website: str = ""
|
|
20
|
+
avatar_url: str = ""
|
|
21
|
+
header_url: str = ""
|
|
22
|
+
followers_count: int = 0
|
|
23
|
+
following_count: int = 0
|
|
24
|
+
posts_count: int = 0
|
|
25
|
+
is_verified: bool = False
|
|
26
|
+
is_dotone_staff: bool = False
|
|
27
|
+
account_type: str = "standard"
|
|
28
|
+
profile_visibility: str = "public"
|
|
29
|
+
created_at: str = ""
|
|
30
|
+
email: str = ""
|
|
31
|
+
phone_number: str = ""
|
|
32
|
+
kyc_status: str = "not_requested"
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_dict(cls, data: Dict[str, Any]) -> "User":
|
|
36
|
+
return cls(
|
|
37
|
+
user_id=data.get("user_id", data.get("id", "")),
|
|
38
|
+
username=data.get("username", ""),
|
|
39
|
+
display_name=data.get("display_name", ""),
|
|
40
|
+
bio=data.get("bio", ""),
|
|
41
|
+
location=data.get("location", ""),
|
|
42
|
+
website=data.get("website", ""),
|
|
43
|
+
avatar_url=data.get("avatar_url", ""),
|
|
44
|
+
header_url=data.get("header_url", ""),
|
|
45
|
+
followers_count=data.get("followers_count", 0),
|
|
46
|
+
following_count=data.get("following_count", 0),
|
|
47
|
+
posts_count=data.get("posts_count", 0),
|
|
48
|
+
is_verified=data.get("is_verified", False),
|
|
49
|
+
is_dotone_staff=data.get("is_dotone_staff", False),
|
|
50
|
+
account_type=data.get("account_type", "standard"),
|
|
51
|
+
profile_visibility=data.get("profile_visibility", "public"),
|
|
52
|
+
created_at=data.get("created_at", ""),
|
|
53
|
+
email=data.get("email", ""),
|
|
54
|
+
phone_number=data.get("phone_number", ""),
|
|
55
|
+
kyc_status=data.get("kyc_status", "not_requested"),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def profile_url(self) -> str:
|
|
60
|
+
return f"https://mydot.one/@{self.username}"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class Dot:
|
|
65
|
+
"""MyDot dot (post/tweet)."""
|
|
66
|
+
id: str = ""
|
|
67
|
+
short_id: str = ""
|
|
68
|
+
content: str = ""
|
|
69
|
+
dot_type: str = "dot"
|
|
70
|
+
author: Dict[str, Any] = field(default_factory=dict)
|
|
71
|
+
reply_to: Optional[str] = None
|
|
72
|
+
repost_of: Optional[str] = None
|
|
73
|
+
quote_of: Optional[str] = None
|
|
74
|
+
likes_count: int = 0
|
|
75
|
+
replies_count: int = 0
|
|
76
|
+
reposts_count: int = 0
|
|
77
|
+
quotes_count: int = 0
|
|
78
|
+
bookmarks_count: int = 0
|
|
79
|
+
view_count: int = 0
|
|
80
|
+
stars: int = 0
|
|
81
|
+
is_liked: bool = False
|
|
82
|
+
is_reposted: bool = False
|
|
83
|
+
is_bookmarked: bool = False
|
|
84
|
+
created_at: Optional[str] = None
|
|
85
|
+
edited_at: Optional[str] = None
|
|
86
|
+
media: List[Dict] = field(default_factory=list)
|
|
87
|
+
entities: Dict = field(default_factory=dict)
|
|
88
|
+
sensitive_type: str = "normal"
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Dot":
|
|
92
|
+
vc = data.get("viewer_context", {})
|
|
93
|
+
return cls(
|
|
94
|
+
id=data.get("id", ""),
|
|
95
|
+
short_id=data.get("short_id", ""),
|
|
96
|
+
content=data.get("content", ""),
|
|
97
|
+
dot_type=data.get("dot_type", "dot"),
|
|
98
|
+
author=data.get("author", {}),
|
|
99
|
+
reply_to=data.get("reply_to"),
|
|
100
|
+
repost_of=data.get("repost_of"),
|
|
101
|
+
quote_of=data.get("quote_of"),
|
|
102
|
+
likes_count=data.get("likes_count", 0),
|
|
103
|
+
replies_count=data.get("replies_count", 0),
|
|
104
|
+
reposts_count=data.get("reposts_count", 0),
|
|
105
|
+
quotes_count=data.get("quotes_count", 0),
|
|
106
|
+
bookmarks_count=data.get("bookmarks_count", 0),
|
|
107
|
+
view_count=data.get("view_count", 0),
|
|
108
|
+
stars=data.get("stars", 0),
|
|
109
|
+
is_liked=vc.get("is_liked", False),
|
|
110
|
+
is_reposted=vc.get("is_reposted", False),
|
|
111
|
+
is_bookmarked=vc.get("is_bookmarked", False),
|
|
112
|
+
created_at=data.get("created_at"),
|
|
113
|
+
edited_at=data.get("edited_at"),
|
|
114
|
+
media=data.get("media", []),
|
|
115
|
+
entities=data.get("entities", {}),
|
|
116
|
+
sensitive_type=data.get("sensitive_type", "normal"),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def author_username(self) -> str:
|
|
121
|
+
return self.author.get("username", "")
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def url(self) -> str:
|
|
125
|
+
return f"https://mydot.one/@{self.author_username}/status/{self.short_id}"
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def is_reply(self) -> bool:
|
|
129
|
+
return self.dot_type == "reply"
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def is_repost(self) -> bool:
|
|
133
|
+
return self.dot_type == "repost"
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def is_quote(self) -> bool:
|
|
137
|
+
return self.dot_type == "quote"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass
|
|
141
|
+
class ThreadView:
|
|
142
|
+
"""Thread view (parent, dot, children)."""
|
|
143
|
+
above: List[Dot] = field(default_factory=list)
|
|
144
|
+
dot: Optional[Dot] = None
|
|
145
|
+
below: Dict = field(default_factory=dict)
|
|
146
|
+
|
|
147
|
+
@classmethod
|
|
148
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ThreadView":
|
|
149
|
+
return cls(
|
|
150
|
+
above=[Dot.from_dict(d) for d in data.get("above", [])],
|
|
151
|
+
dot=Dot.from_dict(data.get("dot", {})),
|
|
152
|
+
below=data.get("below", {}),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass
|
|
157
|
+
class Notification:
|
|
158
|
+
"""MyDot notification."""
|
|
159
|
+
id: str = ""
|
|
160
|
+
type: str = ""
|
|
161
|
+
created_at: str = ""
|
|
162
|
+
is_read: bool = False
|
|
163
|
+
actor: Optional[Dict] = None
|
|
164
|
+
dot: Optional[Dot] = None
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Notification":
|
|
168
|
+
dot = Dot.from_dict(data["dot"]) if data.get("dot") else None
|
|
169
|
+
return cls(
|
|
170
|
+
id=data.get("id", ""),
|
|
171
|
+
type=data.get("type", ""),
|
|
172
|
+
created_at=data.get("created_at", ""),
|
|
173
|
+
is_read=data.get("is_read", False),
|
|
174
|
+
actor=data.get("actor"),
|
|
175
|
+
dot=dot,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass
|
|
180
|
+
class PaginatedResponse:
|
|
181
|
+
"""Generic paginated response."""
|
|
182
|
+
count: int = 0
|
|
183
|
+
next: Optional[str] = None
|
|
184
|
+
previous: Optional[str] = None
|
|
185
|
+
results: List[Any] = field(default_factory=list)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aiodot
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Async Python client for MyDot.one social platform
|
|
5
|
+
Home-page: https://github.com/karbaladev/aiodot
|
|
6
|
+
Author: karbaladev.ir
|
|
7
|
+
Author-email: "karbaladev.ir" <saleh.mohammadi.az@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://karbaladev.ir
|
|
10
|
+
Project-URL: Repository, https://github.com/karbaladevir/aiodot
|
|
11
|
+
Project-URL: Issues, https://github.com/karbaladevir/aiodot/issues
|
|
12
|
+
Keywords: mydot,aiodot,mydot-bot,social-media,async,bot
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: aiohttp>=3.8.0
|
|
20
|
+
Requires-Dist: requests>=2.28.0
|
|
21
|
+
Dynamic: author
|
|
22
|
+
Dynamic: home-page
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
Dynamic: requires-python
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<img src="https://abrehamrahi.ir/o/public/rNf0ej8l/" alt="aiodot logo" width="200">
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
<h1 align="center">aiodot 🚀</h1>
|
|
31
|
+
<p align="center">
|
|
32
|
+
<b>Async Python client for MyDot.one social platform</b><br>
|
|
33
|
+
Build bots, automation, and tools with ease.
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
<p align="center">
|
|
37
|
+
<a href="https://pypi.org/project/aiodot/">
|
|
38
|
+
<img src="https://badge.fury.io/py/aiodot.svg" alt="PyPI version">
|
|
39
|
+
</a>
|
|
40
|
+
<a href="https://pypi.org/project/aiodot/">
|
|
41
|
+
<img src="https://img.shields.io/pypi/pyversions/aiodot.svg" alt="Python">
|
|
42
|
+
</a>
|
|
43
|
+
<a href="https://github.com/karbaladev/aiodot/blob/main/LICENSE">
|
|
44
|
+
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT">
|
|
45
|
+
</a>
|
|
46
|
+
</p>
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
**`aiodot`** is an asynchronous Python library for [MyDot.one](https://mydot.one). Built with `aiohttp` — fast, async-first, session-based like aiogram.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## ✨ Features
|
|
55
|
+
|
|
56
|
+
- ⚡ **Async-first** — Built on aiohttp
|
|
57
|
+
- 💾 **Session persistence** — Login once, auto-loads next time
|
|
58
|
+
- 🔄 **Auto token refresh** — Handles 401 gracefully
|
|
59
|
+
- 📦 **50+ endpoints** — Full API coverage
|
|
60
|
+
- 🎯 **Type hints** — Full type annotations
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 📦 Installation
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install aiodot
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 💡 Quick Start
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import asyncio
|
|
76
|
+
from aiodot import MyDotClient
|
|
77
|
+
|
|
78
|
+
TOKEN = "your-token" # F12 → Cookies → __Secure-access_token
|
|
79
|
+
|
|
80
|
+
async def main():
|
|
81
|
+
async with MyDotClient(token=TOKEN, session_file="user.session.json") as client:
|
|
82
|
+
me = await client.get_me()
|
|
83
|
+
print(f"@{me.username}")
|
|
84
|
+
|
|
85
|
+
dot = await client.create_dot("Hello aiodot! 🚀")
|
|
86
|
+
print(dot.url)
|
|
87
|
+
|
|
88
|
+
asyncio.run(main())
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 🤖 Echo Bot
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
import asyncio
|
|
97
|
+
from aiodot import MyDotClient
|
|
98
|
+
|
|
99
|
+
TOKEN = "your-token"
|
|
100
|
+
|
|
101
|
+
async def main():
|
|
102
|
+
processed = set()
|
|
103
|
+
async with MyDotClient(token=TOKEN, session_file="bot.session.json") as c:
|
|
104
|
+
print(f"🤖 @{(await c.get_me()).username} started!")
|
|
105
|
+
while True:
|
|
106
|
+
for n in (await c.get_notifications(10)).get("results", []):
|
|
107
|
+
if n.get("id") in processed: continue
|
|
108
|
+
processed.add(n.get("id"))
|
|
109
|
+
if n.get("type") == "mention":
|
|
110
|
+
d = n.get("dot", {})
|
|
111
|
+
if d.get("id"):
|
|
112
|
+
await c.reply(d["id"], "Hello! 🤖")
|
|
113
|
+
await asyncio.sleep(15)
|
|
114
|
+
|
|
115
|
+
asyncio.run(main())
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 📖 API Reference
|
|
121
|
+
|
|
122
|
+
| Category | Methods |
|
|
123
|
+
|----------|---------|
|
|
124
|
+
| **Profile** | `get_me()`, `update_profile()`, `set_visibility()` |
|
|
125
|
+
| **Dots** | `create_dot()`, `get_dot()`, `reply()`, `repost()`, `quote()`, `like()`, `bookmark()`, `edit_dot()`, `delete_dot()` |
|
|
126
|
+
| **Social** | `follow()`, `unfollow()`, `block()`, `unblock()`, `mute()`, `unmute()` |
|
|
127
|
+
| **Users** | `search_users()`, `get_user_followers()`, `get_user_following()`, `get_user_dots()` |
|
|
128
|
+
| **Feed** | `home_feed()`, `following_feed()`, `follow_suggestions()` |
|
|
129
|
+
| **Notifications** | `get_notifications()`, `mark_all_read()`, `get_notification_preferences()` |
|
|
130
|
+
| **Trending** | `get_trending_hashtags()`, `get_trending_media()` |
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## ⚠️ Disclaimer
|
|
135
|
+
|
|
136
|
+
Unofficial library. Use responsibly per MyDot.one Terms of Service.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 📡 Community
|
|
141
|
+
|
|
142
|
+
- 📢 **Telegram:** [@aiodotlib](https://t.me/aiodotlib)
|
|
143
|
+
- 💬 **Bale:** [ble.ir/aiodot](https://ble.ir/aiodot)
|
|
144
|
+
- 🌐 **Website:** [karbaladev.ir](https://karbaladev.ir)
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 📄 License
|
|
149
|
+
|
|
150
|
+
MIT © [karbaladev.ir](https://karbaladev.ir)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
aiodot/__init__.py
|
|
6
|
+
aiodot/auth.py
|
|
7
|
+
aiodot/client.py
|
|
8
|
+
aiodot/models.py
|
|
9
|
+
aiodot.egg-info/PKG-INFO
|
|
10
|
+
aiodot.egg-info/SOURCES.txt
|
|
11
|
+
aiodot.egg-info/dependency_links.txt
|
|
12
|
+
aiodot.egg-info/requires.txt
|
|
13
|
+
aiodot.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aiodot
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aiodot"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Async Python client for MyDot.one social platform"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "karbaladev.ir", email = "saleh.mohammadi.az@gmail.com"}
|
|
13
|
+
]
|
|
14
|
+
keywords = ["mydot", "aiodot", "mydot-bot", "social-media", "async", "bot"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
]
|
|
20
|
+
requires-python = ">=3.8"
|
|
21
|
+
dependencies = [
|
|
22
|
+
"aiohttp>=3.8.0",
|
|
23
|
+
"requests>=2.28.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://karbaladev.ir"
|
|
28
|
+
Repository = "https://github.com/karbaladevir/aiodot"
|
|
29
|
+
Issues = "https://github.com/karbaladevir/aiodot/issues"
|
aiodot-1.0.0/setup.cfg
ADDED
aiodot-1.0.0/setup.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""aiodot - Async Python client for MyDot.one. Install with: pip install aiodot"""
|
|
2
|
+
|
|
3
|
+
from setuptools import setup, find_packages
|
|
4
|
+
|
|
5
|
+
with open("README.md", "r", encoding="utf-8") as fh:
|
|
6
|
+
long_description = fh.read()
|
|
7
|
+
|
|
8
|
+
setup(
|
|
9
|
+
name="aiodot",
|
|
10
|
+
version="1.0.0",
|
|
11
|
+
author="karbaladev.ir",
|
|
12
|
+
author_email="saleh.mohammadi.az@gmail.com",
|
|
13
|
+
description="Async Python client for MyDot.one social platform",
|
|
14
|
+
long_description=long_description,
|
|
15
|
+
long_description_content_type="text/markdown",
|
|
16
|
+
url="https://github.com/karbaladev/aiodot",
|
|
17
|
+
project_urls={
|
|
18
|
+
"Website": "https://karbaladev.ir",
|
|
19
|
+
"Bug Tracker": "https://github.com/karbaladev/aiodot/issues",
|
|
20
|
+
"Source Code": "https://github.com/karbaladev/aiodot",
|
|
21
|
+
},
|
|
22
|
+
packages=find_packages(),
|
|
23
|
+
classifiers=[
|
|
24
|
+
"Development Status :: 4 - Beta",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"License :: OSI Approved :: MIT License",
|
|
27
|
+
"Operating System :: OS Independent",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Programming Language :: Python :: 3.8",
|
|
30
|
+
"Programming Language :: Python :: 3.9",
|
|
31
|
+
"Programming Language :: Python :: 3.10",
|
|
32
|
+
"Programming Language :: Python :: 3.11",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Programming Language :: Python :: 3.13",
|
|
35
|
+
"Framework :: AsyncIO",
|
|
36
|
+
],
|
|
37
|
+
python_requires=">=3.8",
|
|
38
|
+
install_requires=[
|
|
39
|
+
"aiohttp>=3.8.0",
|
|
40
|
+
"requests>=2.28.0",
|
|
41
|
+
],
|
|
42
|
+
keywords="mydot, aiodot, mydot-bot, social-media, async, bot, user-bot",
|
|
43
|
+
)
|