nexobots 0.1.0__py3-none-any.whl
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.
- nexobots/__init__.py +100 -0
- nexobots-0.1.0.dist-info/METADATA +58 -0
- nexobots-0.1.0.dist-info/RECORD +4 -0
- nexobots-0.1.0.dist-info/WHEEL +4 -0
nexobots/__init__.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Post stats and check votes on Nexobots, the Discord bot directory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
import json
|
|
8
|
+
from typing import Any, Dict, List, Optional, Union
|
|
9
|
+
|
|
10
|
+
__all__ = ["Nexobots", "NexobotsError", "verify_webhook", "parse_webhook"]
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
|
|
13
|
+
DEFAULT_BASE_URL = "https://api.nexobots.app/v1"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NexobotsError(Exception):
|
|
17
|
+
"""An error response from the Nexobots API."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, status: int, code: str, message: str) -> None:
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.status = status
|
|
22
|
+
self.code = code
|
|
23
|
+
self.message = message
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Nexobots:
|
|
27
|
+
"""A client for one bot. Use it with ``async with``, or pass your own aiohttp session."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, token: str, bot_id: Union[int, str], *, base_url: str = DEFAULT_BASE_URL, session: Any = None) -> None:
|
|
30
|
+
if not token:
|
|
31
|
+
raise ValueError("A Nexobots API token is required.")
|
|
32
|
+
if not bot_id:
|
|
33
|
+
raise ValueError("Your bot's client ID is required.")
|
|
34
|
+
self._token = token
|
|
35
|
+
self._bot_id = str(bot_id)
|
|
36
|
+
self._base_url = base_url.rstrip("/")
|
|
37
|
+
self._session = session
|
|
38
|
+
self._owns_session = False
|
|
39
|
+
|
|
40
|
+
async def __aenter__(self) -> "Nexobots":
|
|
41
|
+
if self._session is None:
|
|
42
|
+
import aiohttp
|
|
43
|
+
|
|
44
|
+
self._session = aiohttp.ClientSession()
|
|
45
|
+
self._owns_session = True
|
|
46
|
+
return self
|
|
47
|
+
|
|
48
|
+
async def __aexit__(self, *_: Any) -> None:
|
|
49
|
+
await self.close()
|
|
50
|
+
|
|
51
|
+
async def close(self) -> None:
|
|
52
|
+
if self._owns_session and self._session is not None:
|
|
53
|
+
await self._session.close()
|
|
54
|
+
self._session = None
|
|
55
|
+
self._owns_session = False
|
|
56
|
+
|
|
57
|
+
async def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
58
|
+
if self._session is None:
|
|
59
|
+
raise RuntimeError('Use "async with Nexobots(...)" or pass a session.')
|
|
60
|
+
headers = {"Authorization": self._token}
|
|
61
|
+
async with self._session.request(method, f"{self._base_url}{path}", headers=headers, **kwargs) as res:
|
|
62
|
+
try:
|
|
63
|
+
body = await res.json(content_type=None)
|
|
64
|
+
except ValueError:
|
|
65
|
+
body = {}
|
|
66
|
+
if res.status >= 400:
|
|
67
|
+
raise NexobotsError(res.status, body.get("error", "error"), body.get("message", f"Nexobots returned {res.status}"))
|
|
68
|
+
return body
|
|
69
|
+
|
|
70
|
+
async def post_stats(self, server_count: int, shard_count: Optional[int] = None) -> None:
|
|
71
|
+
"""Posts your server count. The API allows one post per 5 minutes."""
|
|
72
|
+
payload: Dict[str, int] = {"server_count": server_count}
|
|
73
|
+
if shard_count is not None:
|
|
74
|
+
payload["shard_count"] = shard_count
|
|
75
|
+
await self._request("POST", f"/bots/{self._bot_id}/stats", json=payload)
|
|
76
|
+
|
|
77
|
+
async def has_voted(self, user_id: Union[int, str]) -> bool:
|
|
78
|
+
"""Whether this user voted for your bot in the last 12 hours."""
|
|
79
|
+
body = await self._request("GET", f"/bots/{self._bot_id}/votes/check", params={"userId": str(user_id)})
|
|
80
|
+
return body.get("voted") == 1
|
|
81
|
+
|
|
82
|
+
async def recent_voters(self) -> List[Dict[str, Any]]:
|
|
83
|
+
"""The most recent voters, up to 1,000."""
|
|
84
|
+
return await self._request("GET", f"/bots/{self._bot_id}/votes")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def verify_webhook(secret: str, raw_body: Union[bytes, str], signature: Optional[str]) -> bool:
|
|
88
|
+
"""Checks a vote webhook really came from Nexobots. Pass the raw request body and the X-Nexo-Signature header."""
|
|
89
|
+
if not signature:
|
|
90
|
+
return False
|
|
91
|
+
body = raw_body.encode() if isinstance(raw_body, str) else raw_body
|
|
92
|
+
expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
|
|
93
|
+
return hmac.compare_digest(expected, signature)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def parse_webhook(secret: str, raw_body: Union[bytes, str], signature: Optional[str]) -> Optional[Dict[str, Any]]:
|
|
97
|
+
"""Verifies a webhook and returns the vote, or None if the signature is wrong."""
|
|
98
|
+
if not verify_webhook(secret, raw_body, signature):
|
|
99
|
+
return None
|
|
100
|
+
return json.loads(raw_body)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: nexobots
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Post stats and check votes on Nexobots, the Discord bot directory.
|
|
5
|
+
Project-URL: Homepage, https://nexobots.app
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Requires-Dist: aiohttp>=3.8
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# nexobots
|
|
12
|
+
|
|
13
|
+
Post stats and check votes on [Nexobots](https://nexobots.app), the Discord bot directory.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install nexobots
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Get your API token from your bot's page in the Nexobots dashboard, and keep it in your `.env` file.
|
|
20
|
+
|
|
21
|
+
## Post your server count
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import os
|
|
25
|
+
|
|
26
|
+
from discord.ext import tasks
|
|
27
|
+
from nexobots import Nexobots
|
|
28
|
+
|
|
29
|
+
@tasks.loop(minutes=30)
|
|
30
|
+
async def post_stats():
|
|
31
|
+
async with Nexobots(os.environ["NEXOBOTS_TOKEN"], bot.user.id) as nexobots:
|
|
32
|
+
await nexobots.post_stats(server_count=len(bot.guilds), shard_count=bot.shard_count)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The API allows one post per 5 minutes.
|
|
36
|
+
|
|
37
|
+
## Check a vote
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
async with Nexobots(os.environ["NEXOBOTS_TOKEN"], bot.user.id) as nexobots:
|
|
41
|
+
if await nexobots.has_voted(user.id):
|
|
42
|
+
... # Thank them.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Receive vote webhooks
|
|
46
|
+
|
|
47
|
+
Pass the raw request body, before any JSON parsing.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from nexobots import parse_webhook
|
|
51
|
+
|
|
52
|
+
vote = parse_webhook(os.environ["NEXOBOTS_WEBHOOK_SECRET"], raw_body, request.headers.get("X-Nexo-Signature"))
|
|
53
|
+
if vote is None:
|
|
54
|
+
return Response(status=401)
|
|
55
|
+
print(f"{vote['user']} voted")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Errors from the API are raised as `NexobotsError`, with `status`, `code` and `message`.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
nexobots/__init__.py,sha256=qxXLOGTTfzh-K0TQW5ge8Aj1onpGV_kBYd5CgzmRaDc,4043
|
|
2
|
+
nexobots-0.1.0.dist-info/METADATA,sha256=RiKTMQj4Ls-Xo9ciciBGqKFJtd06PDzZW7GGY_Me0hw,1488
|
|
3
|
+
nexobots-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
4
|
+
nexobots-0.1.0.dist-info/RECORD,,
|