python-yappy 0.1.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.
- python_yappy-0.1.0/PKG-INFO +176 -0
- python_yappy-0.1.0/README.md +165 -0
- python_yappy-0.1.0/pyproject.toml +21 -0
- python_yappy-0.1.0/pyproject.toml.orig +21 -0
- python_yappy-0.1.0/src/yappy/__init__.py +1 -0
- python_yappy-0.1.0/src/yappy/api.py +44 -0
- python_yappy-0.1.0/src/yappy/app.py +204 -0
- python_yappy-0.1.0/src/yappy/consts.py +1 -0
- python_yappy-0.1.0/src/yappy/core.py +105 -0
- python_yappy-0.1.0/src/yappy/errors.py +8 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: python-yappy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: API Bindings for Yapper!
|
|
5
|
+
Author: H1387Lmao
|
|
6
|
+
Requires-Dist: aiohttp>=3.14.5
|
|
7
|
+
Requires-Dist: load-dotenv>=0.1.0
|
|
8
|
+
Requires-Dist: uv-build>=0.13.1
|
|
9
|
+
Requires-Python: >=3.14
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# YapPy
|
|
13
|
+
|
|
14
|
+
**A Python API binding for Yapper.**
|
|
15
|
+
|
|
16
|
+
YapPy brings a Python-friendly interface to [Yapper](https://yapper.website), a chat platform similar to Discord. It provides familiar models, event handlers, and command decorators so you can build Yapper bots with straightforward asynchronous Python.
|
|
17
|
+
|
|
18
|
+
> **Project status:** Early development. The current implementation includes `Context`, `Message`, `Channel`, and `User` models, along with bot event and command handling. The API may change as development continues.
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- **Python-first API** — build bot logic using async functions and decorators.
|
|
23
|
+
- **Event handlers** — respond to Yapper events such as incoming messages.
|
|
24
|
+
- **Commands** — register named commands with descriptions.
|
|
25
|
+
- **Context-based interactions** — reply to messages through a context object.
|
|
26
|
+
- **Core models** — work with `Context`, `Message`, `Channel`, and `User` objects.
|
|
27
|
+
- **Optional logging** — enable verbose output when creating the application.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
Install YapPy using your project's preferred method.
|
|
32
|
+
|
|
33
|
+
If the package is available on PyPI:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install yappy
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
If you are working from a local checkout, install the project from its repository directory:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install .
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The example below uses [`python-dotenv`](https://pypi.org/project/python-dotenv/) to load the bot token from a `.env` file:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install python-dotenv
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Quick start
|
|
52
|
+
|
|
53
|
+
Create a file named `bot.py`:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import os
|
|
57
|
+
|
|
58
|
+
from dotenv import load_dotenv
|
|
59
|
+
from yappy import App
|
|
60
|
+
|
|
61
|
+
load_dotenv()
|
|
62
|
+
|
|
63
|
+
bot = App(verbose=True)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@bot.event("message")
|
|
67
|
+
async def on_message(ctx):
|
|
68
|
+
if ctx.is_bot:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
await ctx.reply("Hi!")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@bot.command(
|
|
75
|
+
name="ping",
|
|
76
|
+
description="replies with 'Pong!'",
|
|
77
|
+
)
|
|
78
|
+
async def ping(ctx):
|
|
79
|
+
await ctx.reply("Pong!")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
bot.run(os.getenv("TOKEN"))
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Create a `.env` file beside `bot.py`:
|
|
86
|
+
|
|
87
|
+
```dotenv
|
|
88
|
+
TOKEN=your_yapper_bot_token
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Then run the bot:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
python bot.py
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
This example registers a `message` event that replies with `Hi!` to messages not sent by bots, and a `ping` command that replies with `Pong!`.
|
|
98
|
+
|
|
99
|
+
**Keep your token private.** Do not commit your `.env` file or publish your bot token.
|
|
100
|
+
|
|
101
|
+
## Core concepts
|
|
102
|
+
|
|
103
|
+
### `App`
|
|
104
|
+
|
|
105
|
+
`App` is the main entry point for creating and running a YapPy bot.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from yappy import App
|
|
109
|
+
|
|
110
|
+
bot = App(verbose=True)
|
|
111
|
+
bot.run("YOUR_TOKEN")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Set `verbose=True` to enable verbose output while developing. Keep credentials in environment variables rather than hard-coding them in source files.
|
|
115
|
+
|
|
116
|
+
### Events
|
|
117
|
+
|
|
118
|
+
Register an event handler with `@bot.event(...)`. Event handlers are asynchronous functions.
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
@bot.event("message")
|
|
122
|
+
async def on_message(ctx):
|
|
123
|
+
if ctx.is_bot:
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
await ctx.reply("Hello!")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The event name and handler arguments should match the event interface supported by the installed YapPy version.
|
|
130
|
+
|
|
131
|
+
### Commands
|
|
132
|
+
|
|
133
|
+
Register a command using `@bot.command(...)`:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
@bot.command(
|
|
137
|
+
name="ping",
|
|
138
|
+
description="replies with 'Pong!'",
|
|
139
|
+
)
|
|
140
|
+
async def ping(ctx):
|
|
141
|
+
await ctx.reply("Pong!")
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The `name` identifies the command, and `description` describes its purpose. Command registration and invocation behavior depend on the current implementation.
|
|
145
|
+
|
|
146
|
+
### Context and models
|
|
147
|
+
|
|
148
|
+
YapPy currently provides these core models:
|
|
149
|
+
|
|
150
|
+
| Model | Purpose |
|
|
151
|
+
| --- | --- |
|
|
152
|
+
| `Context` | Context for handling an interaction or message event. |
|
|
153
|
+
| `Message` | Represents a message. |
|
|
154
|
+
| `Channel` | Represents a channel. |
|
|
155
|
+
| `User` | Represents a user. |
|
|
156
|
+
|
|
157
|
+
The example uses the context object (`ctx`) to check `ctx.is_bot` and send a reply with `ctx.reply(...)`. Consult the source code for the currently available attributes and methods of each model.
|
|
158
|
+
|
|
159
|
+
## Project status
|
|
160
|
+
|
|
161
|
+
YapPy is under active development. Current implemented areas include:
|
|
162
|
+
|
|
163
|
+
- `Context`, `Message`, `Channel`, and `User` models
|
|
164
|
+
- Bot event registration and handling
|
|
165
|
+
- Command registration
|
|
166
|
+
- Message replies through the context API
|
|
167
|
+
|
|
168
|
+
Other features may be planned, incomplete, or unsupported. Check the source code and release notes for the actual capabilities of the version you are using.
|
|
169
|
+
|
|
170
|
+
## Contributing
|
|
171
|
+
|
|
172
|
+
Contributions, bug reports, and suggestions are welcome. Before opening a pull request, describe the change clearly and include a minimal reproduction or test where appropriate.
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
This project is licensed under the MIT License.
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# YapPy
|
|
2
|
+
|
|
3
|
+
**A Python API binding for Yapper.**
|
|
4
|
+
|
|
5
|
+
YapPy brings a Python-friendly interface to [Yapper](https://yapper.website), a chat platform similar to Discord. It provides familiar models, event handlers, and command decorators so you can build Yapper bots with straightforward asynchronous Python.
|
|
6
|
+
|
|
7
|
+
> **Project status:** Early development. The current implementation includes `Context`, `Message`, `Channel`, and `User` models, along with bot event and command handling. The API may change as development continues.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Python-first API** — build bot logic using async functions and decorators.
|
|
12
|
+
- **Event handlers** — respond to Yapper events such as incoming messages.
|
|
13
|
+
- **Commands** — register named commands with descriptions.
|
|
14
|
+
- **Context-based interactions** — reply to messages through a context object.
|
|
15
|
+
- **Core models** — work with `Context`, `Message`, `Channel`, and `User` objects.
|
|
16
|
+
- **Optional logging** — enable verbose output when creating the application.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
Install YapPy using your project's preferred method.
|
|
21
|
+
|
|
22
|
+
If the package is available on PyPI:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install yappy
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
If you are working from a local checkout, install the project from its repository directory:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install .
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The example below uses [`python-dotenv`](https://pypi.org/project/python-dotenv/) to load the bot token from a `.env` file:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install python-dotenv
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
Create a file named `bot.py`:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import os
|
|
46
|
+
|
|
47
|
+
from dotenv import load_dotenv
|
|
48
|
+
from yappy import App
|
|
49
|
+
|
|
50
|
+
load_dotenv()
|
|
51
|
+
|
|
52
|
+
bot = App(verbose=True)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@bot.event("message")
|
|
56
|
+
async def on_message(ctx):
|
|
57
|
+
if ctx.is_bot:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
await ctx.reply("Hi!")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@bot.command(
|
|
64
|
+
name="ping",
|
|
65
|
+
description="replies with 'Pong!'",
|
|
66
|
+
)
|
|
67
|
+
async def ping(ctx):
|
|
68
|
+
await ctx.reply("Pong!")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
bot.run(os.getenv("TOKEN"))
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Create a `.env` file beside `bot.py`:
|
|
75
|
+
|
|
76
|
+
```dotenv
|
|
77
|
+
TOKEN=your_yapper_bot_token
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Then run the bot:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
python bot.py
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
This example registers a `message` event that replies with `Hi!` to messages not sent by bots, and a `ping` command that replies with `Pong!`.
|
|
87
|
+
|
|
88
|
+
**Keep your token private.** Do not commit your `.env` file or publish your bot token.
|
|
89
|
+
|
|
90
|
+
## Core concepts
|
|
91
|
+
|
|
92
|
+
### `App`
|
|
93
|
+
|
|
94
|
+
`App` is the main entry point for creating and running a YapPy bot.
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from yappy import App
|
|
98
|
+
|
|
99
|
+
bot = App(verbose=True)
|
|
100
|
+
bot.run("YOUR_TOKEN")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Set `verbose=True` to enable verbose output while developing. Keep credentials in environment variables rather than hard-coding them in source files.
|
|
104
|
+
|
|
105
|
+
### Events
|
|
106
|
+
|
|
107
|
+
Register an event handler with `@bot.event(...)`. Event handlers are asynchronous functions.
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
@bot.event("message")
|
|
111
|
+
async def on_message(ctx):
|
|
112
|
+
if ctx.is_bot:
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
await ctx.reply("Hello!")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The event name and handler arguments should match the event interface supported by the installed YapPy version.
|
|
119
|
+
|
|
120
|
+
### Commands
|
|
121
|
+
|
|
122
|
+
Register a command using `@bot.command(...)`:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
@bot.command(
|
|
126
|
+
name="ping",
|
|
127
|
+
description="replies with 'Pong!'",
|
|
128
|
+
)
|
|
129
|
+
async def ping(ctx):
|
|
130
|
+
await ctx.reply("Pong!")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The `name` identifies the command, and `description` describes its purpose. Command registration and invocation behavior depend on the current implementation.
|
|
134
|
+
|
|
135
|
+
### Context and models
|
|
136
|
+
|
|
137
|
+
YapPy currently provides these core models:
|
|
138
|
+
|
|
139
|
+
| Model | Purpose |
|
|
140
|
+
| --- | --- |
|
|
141
|
+
| `Context` | Context for handling an interaction or message event. |
|
|
142
|
+
| `Message` | Represents a message. |
|
|
143
|
+
| `Channel` | Represents a channel. |
|
|
144
|
+
| `User` | Represents a user. |
|
|
145
|
+
|
|
146
|
+
The example uses the context object (`ctx`) to check `ctx.is_bot` and send a reply with `ctx.reply(...)`. Consult the source code for the currently available attributes and methods of each model.
|
|
147
|
+
|
|
148
|
+
## Project status
|
|
149
|
+
|
|
150
|
+
YapPy is under active development. Current implemented areas include:
|
|
151
|
+
|
|
152
|
+
- `Context`, `Message`, `Channel`, and `User` models
|
|
153
|
+
- Bot event registration and handling
|
|
154
|
+
- Command registration
|
|
155
|
+
- Message replies through the context API
|
|
156
|
+
|
|
157
|
+
Other features may be planned, incomplete, or unsupported. Check the source code and release notes for the actual capabilities of the version you are using.
|
|
158
|
+
|
|
159
|
+
## Contributing
|
|
160
|
+
|
|
161
|
+
Contributions, bug reports, and suggestions are welcome. Before opening a pull request, describe the change clearly and include a minimal reproduction or test where appropriate.
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
This project is licensed under the MIT License.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "python-yappy"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "API Bindings for Yapper!"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"aiohttp>=3.14.5",
|
|
9
|
+
"load-dotenv>=0.1.0",
|
|
10
|
+
"uv-build>=0.13.1",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[[project.authors]]
|
|
14
|
+
name = "H1387Lmao"
|
|
15
|
+
|
|
16
|
+
[tool.uv.build-backend]
|
|
17
|
+
module-name = "yappy"
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["uv_build>=0.12.1,<0.13.1"]
|
|
21
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "python-yappy"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "API Bindings for Yapper!"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "H1387Lmao"}
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.14"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"aiohttp>=3.14.5",
|
|
12
|
+
"load-dotenv>=0.1.0",
|
|
13
|
+
"uv-build>=0.13.1",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[tool.uv.build-backend]
|
|
17
|
+
module-name = "yappy"
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["uv_build>=0.12.1,<0.13.1"]
|
|
21
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .app import App
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from .consts import BASE_URL
|
|
2
|
+
import aiohttp
|
|
3
|
+
|
|
4
|
+
async def api_push(session: aiohttp.ClientSession, method: str, path: str, body=None, tries=3):
|
|
5
|
+
headers = {
|
|
6
|
+
"Authorization": f"Bot {session._bearer_token}",
|
|
7
|
+
"Content-Type": "application/json"
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
async with session.request(method, BASE_URL + path, json=body, headers=headers, timeout=40) as r:
|
|
12
|
+
r.raise_for_status()
|
|
13
|
+
return await r.json()
|
|
14
|
+
except aiohttp.ClientResponseError as e:
|
|
15
|
+
try:
|
|
16
|
+
err = await e.history[-1].json() if e.history else {}
|
|
17
|
+
except Exception:
|
|
18
|
+
err = {}
|
|
19
|
+
|
|
20
|
+
msg = err.get("error") if isinstance(err, dict) else None
|
|
21
|
+
busy = e.status == 503 and isinstance(err, dict) and "retry_after" in err
|
|
22
|
+
|
|
23
|
+
wait = int(e.headers.get("Retry-After") or 0) if e.status == 429 or busy else 0
|
|
24
|
+
|
|
25
|
+
if 0 < wait <= 60 and tries > 1:
|
|
26
|
+
print(f"{'busy' if busy else 'rate limited'}, retrying in {wait}s")
|
|
27
|
+
await asyncio.sleep(wait * (1 + random.random() * 0.5))
|
|
28
|
+
return await api(session, method, path, body, tries - 1)
|
|
29
|
+
|
|
30
|
+
raise RuntimeError(f"{method} {path} failed ({e.status}): {msg or e.message}") from None
|
|
31
|
+
|
|
32
|
+
class api:
|
|
33
|
+
@staticmethod
|
|
34
|
+
async def put(session, path="", body=None, tries=3):
|
|
35
|
+
await api_push(session, "PUT", path, body, tries)
|
|
36
|
+
@staticmethod
|
|
37
|
+
async def get(session, path="", body=None, tries=3):
|
|
38
|
+
return await api_push(session, "GET", path, body, tries)
|
|
39
|
+
@staticmethod
|
|
40
|
+
async def post(session, path="", body=None, tries=3):
|
|
41
|
+
await api_push(session, "POST", path, body, tries)
|
|
42
|
+
@staticmethod
|
|
43
|
+
async def send(session, channel_id=0, data=None, tries=3):
|
|
44
|
+
await api_push(session, "POST", f"/channels/{channel_id}/messages", data, tries)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
from .core import *
|
|
2
|
+
from .consts import BASE_URL
|
|
3
|
+
from .api import *
|
|
4
|
+
from .errors import ImproperToken, InvalidSignature
|
|
5
|
+
|
|
6
|
+
import inspect
|
|
7
|
+
import asyncio
|
|
8
|
+
import aiohttp
|
|
9
|
+
from functools import wraps
|
|
10
|
+
from typing import Callable
|
|
11
|
+
|
|
12
|
+
class App:
|
|
13
|
+
def __init__(self, verbose=False):
|
|
14
|
+
self.__commands = {}
|
|
15
|
+
self.__event_handlers = {}
|
|
16
|
+
|
|
17
|
+
self.user = None
|
|
18
|
+
self._token = None
|
|
19
|
+
|
|
20
|
+
self._verbose = verbose
|
|
21
|
+
|
|
22
|
+
self.__cache = {"users":{}}
|
|
23
|
+
|
|
24
|
+
async def _load_bot_info(self):
|
|
25
|
+
my_info = await api.get(self.session, "/me")
|
|
26
|
+
|
|
27
|
+
if self._verbose:
|
|
28
|
+
print(my_info)
|
|
29
|
+
|
|
30
|
+
return Bot(
|
|
31
|
+
name=my_info["username"],
|
|
32
|
+
display_name=my_info["display_name"],
|
|
33
|
+
id=my_info["id"],
|
|
34
|
+
profile=Profile(
|
|
35
|
+
avatar=my_info["avatar_url"],
|
|
36
|
+
banner=my_info["banner_url"],
|
|
37
|
+
),
|
|
38
|
+
bio=my_info["bio"],
|
|
39
|
+
created_at=my_info["created_at"],
|
|
40
|
+
|
|
41
|
+
owner_id=my_info["bot"]["owner_id"],
|
|
42
|
+
is_bot=True
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def get_user(
|
|
46
|
+
self,
|
|
47
|
+
id,
|
|
48
|
+
_data
|
|
49
|
+
):
|
|
50
|
+
if id in self.__cache["users"]:
|
|
51
|
+
return self.__cache["users"]
|
|
52
|
+
user = User(
|
|
53
|
+
name=_data["author"],
|
|
54
|
+
id=id
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
self.__cache[id]=user
|
|
58
|
+
return user
|
|
59
|
+
|
|
60
|
+
def _build_context(self, ev):
|
|
61
|
+
d = ev["data"]
|
|
62
|
+
|
|
63
|
+
channel=Channel(
|
|
64
|
+
self,
|
|
65
|
+
id=d["channel_id"],
|
|
66
|
+
name=d["channel_name"]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return Context(
|
|
70
|
+
self,
|
|
71
|
+
is_bot=ev.get("author_bot", False),
|
|
72
|
+
message=Message(
|
|
73
|
+
self,
|
|
74
|
+
id=d["id"],
|
|
75
|
+
author=self.get_user(
|
|
76
|
+
d["author_id"], d
|
|
77
|
+
),
|
|
78
|
+
channel=channel,
|
|
79
|
+
created_at=d["created_at"],
|
|
80
|
+
content=d["content"]
|
|
81
|
+
),
|
|
82
|
+
channel=channel
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
async def _handle_event(self, session: aiohttp.ClientSession, ev):
|
|
86
|
+
if self._verbose:
|
|
87
|
+
print(ev)
|
|
88
|
+
d = ev["data"]
|
|
89
|
+
|
|
90
|
+
if ev["type"] in self.__event_handlers:
|
|
91
|
+
if (cmd_name:=d.get("command")):
|
|
92
|
+
if cmd_name in self.__commands:
|
|
93
|
+
await self.__commands[cmd_name].func(
|
|
94
|
+
self._build_context(
|
|
95
|
+
ev
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
elif cmd_name in self.__aliases:
|
|
99
|
+
await self.__aliases[cmd_name](
|
|
100
|
+
self._build_context(
|
|
101
|
+
ev
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
raise UnknownCommand(
|
|
106
|
+
f"Unhandled command '{cmd_name}'!"
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
await self.__event_handlers[ev["type"]](
|
|
110
|
+
self._build_context(
|
|
111
|
+
ev
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
async def _init_commands(self):
|
|
116
|
+
_commands = []
|
|
117
|
+
for name, func in self.__commands.items():
|
|
118
|
+
_commands.append(
|
|
119
|
+
{"name": name, "description": func.description}
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
await api.put(
|
|
123
|
+
self.session,
|
|
124
|
+
"/commands",
|
|
125
|
+
{"commands": _commands}
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
async def _event_loop(self):
|
|
129
|
+
async with aiohttp.ClientSession() as self.session:
|
|
130
|
+
self.session._bearer_token=self._token
|
|
131
|
+
|
|
132
|
+
self.user = await self._load_bot_info()
|
|
133
|
+
await self._init_commands()
|
|
134
|
+
|
|
135
|
+
events_init = await api.get(self.session, "/events?after=0&wait=0")
|
|
136
|
+
last = events_init["last"]
|
|
137
|
+
fails = 0
|
|
138
|
+
|
|
139
|
+
while True:
|
|
140
|
+
try:
|
|
141
|
+
out = await api.get(self.session, f"/events?after={last}&wait=25")
|
|
142
|
+
fails = 0
|
|
143
|
+
except Exception as e:
|
|
144
|
+
fails += 1
|
|
145
|
+
if "(401)" in str(e):
|
|
146
|
+
raise ImproperToken("Please check if your token is correctly used!")
|
|
147
|
+
wait = min(30, 2 ** fails)
|
|
148
|
+
|
|
149
|
+
print(f"retrying in {wait}s:", e)
|
|
150
|
+
|
|
151
|
+
await asyncio.sleep(wait)
|
|
152
|
+
continue
|
|
153
|
+
for ev in out["events"]:
|
|
154
|
+
if ev["id"] <= last:
|
|
155
|
+
continue
|
|
156
|
+
last = ev["id"]
|
|
157
|
+
try:
|
|
158
|
+
await self._handle_event(self.session, ev)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
print("error handling", ev["type"], "event:", e)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def event(self, event_type: str):
|
|
164
|
+
def wrapper(func):
|
|
165
|
+
if not inspect.iscoroutinefunction(func):
|
|
166
|
+
raise InvalidSignature(f"Expected event handler for '{event_type}' to be a coroutine!")
|
|
167
|
+
self.__event_handlers[
|
|
168
|
+
event_type
|
|
169
|
+
] = func
|
|
170
|
+
return wrapper
|
|
171
|
+
|
|
172
|
+
def command(
|
|
173
|
+
self,
|
|
174
|
+
name, *,
|
|
175
|
+
aliases=[],
|
|
176
|
+
description=""
|
|
177
|
+
):
|
|
178
|
+
def wrapper(
|
|
179
|
+
func
|
|
180
|
+
):
|
|
181
|
+
if not inspect.iscoroutinefunction(func):
|
|
182
|
+
raise InvalidSignature(f"Expected command handler for '{name}' to be a coroutine!")
|
|
183
|
+
self.__commands[name]=Command(
|
|
184
|
+
name=name,
|
|
185
|
+
aliases=aliases,
|
|
186
|
+
description=description,
|
|
187
|
+
func=func
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
for alias in aliases:
|
|
191
|
+
self.__aliases[alias] = self.__commands[name]
|
|
192
|
+
|
|
193
|
+
return wrapper
|
|
194
|
+
|
|
195
|
+
def run(self, token: str):
|
|
196
|
+
self._token = token
|
|
197
|
+
|
|
198
|
+
self._yappy_loop = asyncio.new_event_loop()
|
|
199
|
+
asyncio.set_event_loop(self._yappy_loop)
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
self._yappy_loop.run_until_complete(self._event_loop())
|
|
203
|
+
finally:
|
|
204
|
+
self._yappy_loop.close()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
BASE_URL = "https://www.yapper.website/api/bot/v1"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from .api import *
|
|
2
|
+
from typing import Callable
|
|
3
|
+
|
|
4
|
+
class Profile:
|
|
5
|
+
def __init__(self,
|
|
6
|
+
avatar: str = "",
|
|
7
|
+
banner: str = ""
|
|
8
|
+
):
|
|
9
|
+
self.avatar = avatar
|
|
10
|
+
self.banner = banner
|
|
11
|
+
|
|
12
|
+
class User:
|
|
13
|
+
def __init__(self,
|
|
14
|
+
name: str ="",
|
|
15
|
+
display_name: str = "",
|
|
16
|
+
id=0,
|
|
17
|
+
profile: Profile | None = None,
|
|
18
|
+
created_at: str = "",
|
|
19
|
+
bio: str = "",
|
|
20
|
+
is_bot: bool = True
|
|
21
|
+
):
|
|
22
|
+
self.name = name
|
|
23
|
+
self.id = id
|
|
24
|
+
self.profile = profile
|
|
25
|
+
self.display_name = display_name
|
|
26
|
+
self.is_bot = is_bot
|
|
27
|
+
self.bio = bio
|
|
28
|
+
|
|
29
|
+
class Bot(User):
|
|
30
|
+
def __init__(self, owner_id=None, **kwargs):
|
|
31
|
+
super().__init__(**kwargs)
|
|
32
|
+
self.owner_id = owner_id
|
|
33
|
+
|
|
34
|
+
class Command:
|
|
35
|
+
def __init__(self,
|
|
36
|
+
name: str,
|
|
37
|
+
aliases: list[str] = [],
|
|
38
|
+
description: str = "",
|
|
39
|
+
func: Callable = None,
|
|
40
|
+
):
|
|
41
|
+
self.name = name
|
|
42
|
+
self.aliases = aliases
|
|
43
|
+
self.description = description
|
|
44
|
+
self.func = func
|
|
45
|
+
|
|
46
|
+
class Channel:
|
|
47
|
+
def __init__(self,
|
|
48
|
+
bot,
|
|
49
|
+
id=0,
|
|
50
|
+
name: str = ""
|
|
51
|
+
):
|
|
52
|
+
self.id = id
|
|
53
|
+
self.bot = bot
|
|
54
|
+
self.name = name
|
|
55
|
+
|
|
56
|
+
class Message:
|
|
57
|
+
def __init__(self,
|
|
58
|
+
bot,
|
|
59
|
+
id=0,
|
|
60
|
+
channel: Channel | None = None,
|
|
61
|
+
author: User | None = None,
|
|
62
|
+
created_at: str = "",
|
|
63
|
+
content: str = ""
|
|
64
|
+
):
|
|
65
|
+
self.id = id
|
|
66
|
+
self.channel = channel
|
|
67
|
+
self.bot = bot
|
|
68
|
+
self.created_at = created_at
|
|
69
|
+
self.content = content
|
|
70
|
+
|
|
71
|
+
self.author = author
|
|
72
|
+
|
|
73
|
+
class Context:
|
|
74
|
+
def __init__(self,
|
|
75
|
+
bot,
|
|
76
|
+
is_bot: bool = False,
|
|
77
|
+
message: Message | None = None,
|
|
78
|
+
channel: Channel | None = None
|
|
79
|
+
):
|
|
80
|
+
self.bot = bot
|
|
81
|
+
self.is_bot = is_bot
|
|
82
|
+
self.message = message
|
|
83
|
+
self.channel = channel
|
|
84
|
+
|
|
85
|
+
async def reply(
|
|
86
|
+
self,
|
|
87
|
+
content: str = "Hi"
|
|
88
|
+
):
|
|
89
|
+
await api.send(
|
|
90
|
+
self.bot.session, self.channel.id, data={
|
|
91
|
+
"content": content,
|
|
92
|
+
"reply_to": self.message.id
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
async def send(
|
|
97
|
+
self,
|
|
98
|
+
content: str = "Hi"
|
|
99
|
+
):
|
|
100
|
+
await api.send(
|
|
101
|
+
self.bot.session, self.channel.id, data={
|
|
102
|
+
"content": content,
|
|
103
|
+
"reply_to": None
|
|
104
|
+
}
|
|
105
|
+
)
|