parlon-bot 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.
- parlon_bot-0.1.0/LICENSE +21 -0
- parlon_bot-0.1.0/PKG-INFO +102 -0
- parlon_bot-0.1.0/README.md +84 -0
- parlon_bot-0.1.0/parlon_bot/__init__.py +34 -0
- parlon_bot-0.1.0/parlon_bot/client.py +168 -0
- parlon_bot-0.1.0/parlon_bot/components.py +66 -0
- parlon_bot-0.1.0/parlon_bot/errors.py +13 -0
- parlon_bot-0.1.0/parlon_bot/models.py +87 -0
- parlon_bot-0.1.0/parlon_bot.egg-info/PKG-INFO +102 -0
- parlon_bot-0.1.0/parlon_bot.egg-info/SOURCES.txt +13 -0
- parlon_bot-0.1.0/parlon_bot.egg-info/dependency_links.txt +1 -0
- parlon_bot-0.1.0/parlon_bot.egg-info/requires.txt +1 -0
- parlon_bot-0.1.0/parlon_bot.egg-info/top_level.txt +1 -0
- parlon_bot-0.1.0/pyproject.toml +28 -0
- parlon_bot-0.1.0/setup.cfg +4 -0
parlon_bot-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Parlon
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: parlon-bot
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for Parlon bots (api beta v0.1) — text messages and buttons.
|
|
5
|
+
Author: Parlon
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://parlon-app.vercel.app
|
|
8
|
+
Project-URL: Developer Portal, https://parlon-developers.vercel.app
|
|
9
|
+
Keywords: parlon,bot,chatbot,api,sdk
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Topic :: Communications :: Chat
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: requests>=2.28
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# parlon-bot
|
|
20
|
+
|
|
21
|
+
Official Python SDK for **Parlon** bots — **api beta v0.1**.
|
|
22
|
+
|
|
23
|
+
Supports text messages and buttons today; image messages are coming and will
|
|
24
|
+
first land on the [developer portal](https://parlon-developers.vercel.app),
|
|
25
|
+
then in this library.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install parlon-bot
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Get a token
|
|
34
|
+
|
|
35
|
+
1. Open the [Parlon Developer Portal](https://parlon-developers.vercel.app) and sign in
|
|
36
|
+
(same account as the main [Parlon](https://parlon-app.vercel.app) app).
|
|
37
|
+
2. Create an application. You get a token **once** — copy it immediately (you
|
|
38
|
+
can always reset it later, like Discord).
|
|
39
|
+
3. A persistent **Sandbox** server is created for you automatically and your
|
|
40
|
+
bot is already installed there — open the main Parlon app with the same
|
|
41
|
+
account to test.
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import os
|
|
47
|
+
from parlon_bot import Bot, ActionRow, Button
|
|
48
|
+
|
|
49
|
+
bot = Bot(token=os.environ["PARLON_BOT_TOKEN"])
|
|
50
|
+
|
|
51
|
+
@bot.on_message
|
|
52
|
+
def handle_message(message):
|
|
53
|
+
if message.content.strip() == "!ping":
|
|
54
|
+
message.reply(
|
|
55
|
+
"Pong! Click the button:",
|
|
56
|
+
components=[ActionRow(Button(label="Again", custom_id="ping_again", style="primary"))],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
@bot.on_button_click
|
|
60
|
+
def handle_click(interaction):
|
|
61
|
+
if interaction.custom_id == "ping_again":
|
|
62
|
+
interaction.respond("Pong again! 🏓")
|
|
63
|
+
|
|
64
|
+
bot.run()
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## API
|
|
68
|
+
|
|
69
|
+
- `Bot(token, *, poll_interval=2.0)` — create a client. `run()` blocks and polls for
|
|
70
|
+
events; `poll_once(since)` if you want to drive the loop yourself (e.g. inside
|
|
71
|
+
an existing asyncio/Flask app).
|
|
72
|
+
- `@bot.on_message` — called for every new message in a server this bot is installed
|
|
73
|
+
in (not its own messages). Handler receives a `Message`.
|
|
74
|
+
- `@bot.on_button_click` — called when a member clicks a button. Handler receives an
|
|
75
|
+
`Interaction`.
|
|
76
|
+
- `message.reply(content, components=None)` / `interaction.respond(content, components=None)`
|
|
77
|
+
— send a message into the same channel.
|
|
78
|
+
- `interaction.update_message(content=None, components=None)` — edit the original
|
|
79
|
+
message the button was attached to (e.g. pass `components=[]` to remove the buttons).
|
|
80
|
+
- `bot.send_message(channel_id, content, components=None)` / `bot.edit_message(message_id, ...)`
|
|
81
|
+
— lower-level, if you already have a channel/message id.
|
|
82
|
+
- `Button(label, custom_id, style="secondary", disabled=False)` — `style` is one of
|
|
83
|
+
`primary`, `secondary`, `success`, `danger`.
|
|
84
|
+
- `ActionRow(*buttons)` — up to 5 buttons per row; pass one or more rows as
|
|
85
|
+
`components=[...]`.
|
|
86
|
+
|
|
87
|
+
## Permissions & installation
|
|
88
|
+
|
|
89
|
+
A bot only acts on servers it's installed on, and only within the permissions
|
|
90
|
+
it was granted there (`read_messages`, `send_messages`, `manage_messages`).
|
|
91
|
+
Generate an "Add to server" link from the developer portal's application page —
|
|
92
|
+
whoever manages that server opens it and approves, exactly like adding a bot on
|
|
93
|
+
Discord.
|
|
94
|
+
|
|
95
|
+
## Notes on this beta
|
|
96
|
+
|
|
97
|
+
- Delivery is by short-interval HTTP polling (default every 2s), not a
|
|
98
|
+
persistent socket yet — simple to run anywhere, including from a basic
|
|
99
|
+
script or a free-tier host. This may be swapped for a real-time gateway in
|
|
100
|
+
a later version without breaking your bot's code.
|
|
101
|
+
- DMs aren't supported yet — bots operate in server channels only.
|
|
102
|
+
- Report issues / feature requests to the Parlon team.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# parlon-bot
|
|
2
|
+
|
|
3
|
+
Official Python SDK for **Parlon** bots — **api beta v0.1**.
|
|
4
|
+
|
|
5
|
+
Supports text messages and buttons today; image messages are coming and will
|
|
6
|
+
first land on the [developer portal](https://parlon-developers.vercel.app),
|
|
7
|
+
then in this library.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install parlon-bot
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Get a token
|
|
16
|
+
|
|
17
|
+
1. Open the [Parlon Developer Portal](https://parlon-developers.vercel.app) and sign in
|
|
18
|
+
(same account as the main [Parlon](https://parlon-app.vercel.app) app).
|
|
19
|
+
2. Create an application. You get a token **once** — copy it immediately (you
|
|
20
|
+
can always reset it later, like Discord).
|
|
21
|
+
3. A persistent **Sandbox** server is created for you automatically and your
|
|
22
|
+
bot is already installed there — open the main Parlon app with the same
|
|
23
|
+
account to test.
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import os
|
|
29
|
+
from parlon_bot import Bot, ActionRow, Button
|
|
30
|
+
|
|
31
|
+
bot = Bot(token=os.environ["PARLON_BOT_TOKEN"])
|
|
32
|
+
|
|
33
|
+
@bot.on_message
|
|
34
|
+
def handle_message(message):
|
|
35
|
+
if message.content.strip() == "!ping":
|
|
36
|
+
message.reply(
|
|
37
|
+
"Pong! Click the button:",
|
|
38
|
+
components=[ActionRow(Button(label="Again", custom_id="ping_again", style="primary"))],
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
@bot.on_button_click
|
|
42
|
+
def handle_click(interaction):
|
|
43
|
+
if interaction.custom_id == "ping_again":
|
|
44
|
+
interaction.respond("Pong again! 🏓")
|
|
45
|
+
|
|
46
|
+
bot.run()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## API
|
|
50
|
+
|
|
51
|
+
- `Bot(token, *, poll_interval=2.0)` — create a client. `run()` blocks and polls for
|
|
52
|
+
events; `poll_once(since)` if you want to drive the loop yourself (e.g. inside
|
|
53
|
+
an existing asyncio/Flask app).
|
|
54
|
+
- `@bot.on_message` — called for every new message in a server this bot is installed
|
|
55
|
+
in (not its own messages). Handler receives a `Message`.
|
|
56
|
+
- `@bot.on_button_click` — called when a member clicks a button. Handler receives an
|
|
57
|
+
`Interaction`.
|
|
58
|
+
- `message.reply(content, components=None)` / `interaction.respond(content, components=None)`
|
|
59
|
+
— send a message into the same channel.
|
|
60
|
+
- `interaction.update_message(content=None, components=None)` — edit the original
|
|
61
|
+
message the button was attached to (e.g. pass `components=[]` to remove the buttons).
|
|
62
|
+
- `bot.send_message(channel_id, content, components=None)` / `bot.edit_message(message_id, ...)`
|
|
63
|
+
— lower-level, if you already have a channel/message id.
|
|
64
|
+
- `Button(label, custom_id, style="secondary", disabled=False)` — `style` is one of
|
|
65
|
+
`primary`, `secondary`, `success`, `danger`.
|
|
66
|
+
- `ActionRow(*buttons)` — up to 5 buttons per row; pass one or more rows as
|
|
67
|
+
`components=[...]`.
|
|
68
|
+
|
|
69
|
+
## Permissions & installation
|
|
70
|
+
|
|
71
|
+
A bot only acts on servers it's installed on, and only within the permissions
|
|
72
|
+
it was granted there (`read_messages`, `send_messages`, `manage_messages`).
|
|
73
|
+
Generate an "Add to server" link from the developer portal's application page —
|
|
74
|
+
whoever manages that server opens it and approves, exactly like adding a bot on
|
|
75
|
+
Discord.
|
|
76
|
+
|
|
77
|
+
## Notes on this beta
|
|
78
|
+
|
|
79
|
+
- Delivery is by short-interval HTTP polling (default every 2s), not a
|
|
80
|
+
persistent socket yet — simple to run anywhere, including from a basic
|
|
81
|
+
script or a free-tier host. This may be swapped for a real-time gateway in
|
|
82
|
+
a later version without breaking your bot's code.
|
|
83
|
+
- DMs aren't supported yet — bots operate in server channels only.
|
|
84
|
+
- Report issues / feature requests to the Parlon team.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""parlon-bot — official Python SDK for Parlon bots (api beta v0.1).
|
|
2
|
+
|
|
3
|
+
pip install parlon-bot
|
|
4
|
+
|
|
5
|
+
from parlon_bot import Bot, Button, ActionRow
|
|
6
|
+
|
|
7
|
+
bot = Bot(token="parlon_...")
|
|
8
|
+
|
|
9
|
+
@bot.on_message
|
|
10
|
+
def handle_message(message):
|
|
11
|
+
if message.content == "!ping":
|
|
12
|
+
message.reply("pong", components=[ActionRow(Button(label="Again", custom_id="again"))])
|
|
13
|
+
|
|
14
|
+
bot.run()
|
|
15
|
+
|
|
16
|
+
Get a token at the Parlon developer portal (create an application there —
|
|
17
|
+
it's free and instant, and comes with a persistent sandbox server to test in).
|
|
18
|
+
"""
|
|
19
|
+
from .client import Bot
|
|
20
|
+
from .components import ActionRow, Button
|
|
21
|
+
from .errors import ParlonError
|
|
22
|
+
from .models import Channel, Interaction, Message, SentMessage
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
__all__ = [
|
|
26
|
+
"Bot",
|
|
27
|
+
"Button",
|
|
28
|
+
"ActionRow",
|
|
29
|
+
"Message",
|
|
30
|
+
"Interaction",
|
|
31
|
+
"Channel",
|
|
32
|
+
"SentMessage",
|
|
33
|
+
"ParlonError",
|
|
34
|
+
]
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""The Bot client: authenticate, send messages/buttons, and poll for events."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
import traceback
|
|
6
|
+
from typing import Callable, List, Optional
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from .components import ActionRow, components_to_payload
|
|
11
|
+
from .errors import ParlonError
|
|
12
|
+
from .models import Channel, Interaction, Message, SentMessage
|
|
13
|
+
|
|
14
|
+
# Public Supabase project backing Parlon. The anon/publishable key below is
|
|
15
|
+
# the same one shipped in the main site's browser bundle — it grants no
|
|
16
|
+
# access by itself, every bot_* RPC verifies YOUR bot token server-side.
|
|
17
|
+
# Point at a different backend (self-hosted / staging) via base_url + anon_key.
|
|
18
|
+
DEFAULT_BASE_URL = "https://zhqazyivekavtsnsfwgm.supabase.co"
|
|
19
|
+
DEFAULT_ANON_KEY = "sb_publishable_ODxl_hlVpNflwdo_M0aKqw_tzIjKCZd"
|
|
20
|
+
|
|
21
|
+
MessageHandler = Callable[[Message], None]
|
|
22
|
+
InteractionHandler = Callable[[Interaction], None]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Bot:
|
|
26
|
+
"""A Parlon bot.
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
bot = Bot(token="parlon_...")
|
|
30
|
+
|
|
31
|
+
@bot.on_message
|
|
32
|
+
def handle_message(message):
|
|
33
|
+
if message.content == "!ping":
|
|
34
|
+
message.reply("pong")
|
|
35
|
+
|
|
36
|
+
bot.run()
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
token: str,
|
|
42
|
+
*,
|
|
43
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
44
|
+
anon_key: str = DEFAULT_ANON_KEY,
|
|
45
|
+
poll_interval: float = 2.0,
|
|
46
|
+
):
|
|
47
|
+
if not token:
|
|
48
|
+
raise ValueError("token is required — create a bot at the Parlon developer portal")
|
|
49
|
+
self.token = token
|
|
50
|
+
self.base_url = base_url.rstrip("/")
|
|
51
|
+
self.anon_key = anon_key
|
|
52
|
+
self.poll_interval = poll_interval
|
|
53
|
+
|
|
54
|
+
self._session = requests.Session()
|
|
55
|
+
self._session.headers.update(
|
|
56
|
+
{
|
|
57
|
+
"apikey": anon_key,
|
|
58
|
+
"Authorization": f"Bearer {anon_key}",
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
self._message_handlers: List[MessageHandler] = []
|
|
64
|
+
self._button_handlers: List[InteractionHandler] = []
|
|
65
|
+
self._running = False
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------- rpc --
|
|
68
|
+
def _rpc(self, fn: str, payload: dict) -> dict:
|
|
69
|
+
resp = self._session.post(f"{self.base_url}/rest/v1/rpc/{fn}", json=payload, timeout=30)
|
|
70
|
+
if resp.status_code >= 400:
|
|
71
|
+
message = resp.text
|
|
72
|
+
code = None
|
|
73
|
+
try:
|
|
74
|
+
data = resp.json()
|
|
75
|
+
message = data.get("message", message)
|
|
76
|
+
code = data.get("code")
|
|
77
|
+
except ValueError:
|
|
78
|
+
pass
|
|
79
|
+
raise ParlonError(message, code=code)
|
|
80
|
+
if resp.text == "" or resp.text == "null":
|
|
81
|
+
return {}
|
|
82
|
+
return resp.json()
|
|
83
|
+
|
|
84
|
+
# ------------------------------------------------------------- actions --
|
|
85
|
+
def send_message(self, channel_id: str, content: str = "", components: Optional[List[ActionRow]] = None) -> SentMessage:
|
|
86
|
+
"""Send a text message (optionally with buttons) as this bot."""
|
|
87
|
+
row = self._rpc(
|
|
88
|
+
"bot_send_message",
|
|
89
|
+
{
|
|
90
|
+
"p_token": self.token,
|
|
91
|
+
"p_channel_id": channel_id,
|
|
92
|
+
"p_content": content,
|
|
93
|
+
"p_components": components_to_payload(components),
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
return SentMessage(id=row["id"], channel_id=row["channel_id"], content=row["content"], raw=row)
|
|
97
|
+
|
|
98
|
+
def edit_message(self, message_id: str, content: Optional[str] = None, components: Optional[List[ActionRow]] = None) -> SentMessage:
|
|
99
|
+
"""Edit a message this bot previously sent. Pass ``components=[]`` to remove its buttons."""
|
|
100
|
+
row = self._rpc(
|
|
101
|
+
"bot_edit_message",
|
|
102
|
+
{
|
|
103
|
+
"p_token": self.token,
|
|
104
|
+
"p_message_id": message_id,
|
|
105
|
+
"p_content": content,
|
|
106
|
+
"p_components": components_to_payload(components),
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
return SentMessage(id=row["id"], channel_id=row["channel_id"], content=row["content"], raw=row)
|
|
110
|
+
|
|
111
|
+
def channel(self, channel_id: str) -> Channel:
|
|
112
|
+
"""Get a :class:`Channel` handle to send messages into, by id."""
|
|
113
|
+
return Channel(self, channel_id)
|
|
114
|
+
|
|
115
|
+
# -------------------------------------------------------------- events --
|
|
116
|
+
def on_message(self, handler: MessageHandler) -> MessageHandler:
|
|
117
|
+
"""Decorator: called for every new message in a server this bot is installed in
|
|
118
|
+
(excluding the bot's own messages)."""
|
|
119
|
+
self._message_handlers.append(handler)
|
|
120
|
+
return handler
|
|
121
|
+
|
|
122
|
+
def on_button_click(self, handler: InteractionHandler) -> InteractionHandler:
|
|
123
|
+
"""Decorator: called when a member clicks a button this bot attached to a message."""
|
|
124
|
+
self._button_handlers.append(handler)
|
|
125
|
+
return handler
|
|
126
|
+
|
|
127
|
+
# ---------------------------------------------------------------- run --
|
|
128
|
+
def poll_once(self, since: Optional[str] = None) -> str:
|
|
129
|
+
"""Run a single poll cycle; returns the server_time cursor to pass as ``since`` next time."""
|
|
130
|
+
data = self._rpc("bot_poll", {"p_token": self.token, "p_since": since, "p_limit": 50})
|
|
131
|
+
for row in data.get("messages", []):
|
|
132
|
+
for handler in self._message_handlers:
|
|
133
|
+
try:
|
|
134
|
+
handler(Message(self, row))
|
|
135
|
+
except Exception: # noqa: BLE001 - keep the bot alive on handler bugs
|
|
136
|
+
traceback.print_exc()
|
|
137
|
+
for row in data.get("interactions", []):
|
|
138
|
+
for handler in self._button_handlers:
|
|
139
|
+
try:
|
|
140
|
+
handler(Interaction(self, row))
|
|
141
|
+
except Exception: # noqa: BLE001
|
|
142
|
+
traceback.print_exc()
|
|
143
|
+
return data.get("server_time", since)
|
|
144
|
+
|
|
145
|
+
def run(self) -> None:
|
|
146
|
+
"""Blocking loop: poll for new messages/button clicks every ``poll_interval`` seconds.
|
|
147
|
+
|
|
148
|
+
v0.1 beta uses simple HTTP polling (no persistent socket yet) — easy
|
|
149
|
+
to get started with, and swappable for push delivery later without
|
|
150
|
+
changing your bot's code.
|
|
151
|
+
"""
|
|
152
|
+
since = None
|
|
153
|
+
self._running = True
|
|
154
|
+
print(f"Parlon bot running (polling every {self.poll_interval}s)... Ctrl+C to stop.")
|
|
155
|
+
try:
|
|
156
|
+
while self._running:
|
|
157
|
+
try:
|
|
158
|
+
since = self.poll_once(since)
|
|
159
|
+
except ParlonError as e:
|
|
160
|
+
print(f"[parlon-bot] API error: {e}")
|
|
161
|
+
time.sleep(self.poll_interval)
|
|
162
|
+
except KeyboardInterrupt:
|
|
163
|
+
pass
|
|
164
|
+
finally:
|
|
165
|
+
self._running = False
|
|
166
|
+
|
|
167
|
+
def stop(self) -> None:
|
|
168
|
+
self._running = False
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Message components: buttons grouped into action rows.
|
|
2
|
+
|
|
3
|
+
Only text + buttons are supported in api beta v0.1 (images/attachments will
|
|
4
|
+
be added through the developer portal in a later version).
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
|
|
11
|
+
VALID_STYLES = ("primary", "secondary", "success", "danger")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Button:
|
|
16
|
+
"""A single clickable button attached to a bot message.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
label: Text shown on the button.
|
|
20
|
+
custom_id: Opaque string you choose — echoed back to your
|
|
21
|
+
``on_button_click`` handler when a member clicks it, so you know
|
|
22
|
+
which button was pressed. Must be unique within the message.
|
|
23
|
+
style: One of "primary", "secondary" (default), "success", "danger".
|
|
24
|
+
disabled: Render the button but make it unclickable.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
label: str
|
|
28
|
+
custom_id: str
|
|
29
|
+
style: str = "secondary"
|
|
30
|
+
disabled: bool = False
|
|
31
|
+
|
|
32
|
+
def __post_init__(self) -> None:
|
|
33
|
+
if self.style not in VALID_STYLES:
|
|
34
|
+
raise ValueError(f"button style must be one of {VALID_STYLES}, got {self.style!r}")
|
|
35
|
+
if not self.custom_id:
|
|
36
|
+
raise ValueError("custom_id is required")
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> dict:
|
|
39
|
+
return {
|
|
40
|
+
"type": "button",
|
|
41
|
+
"custom_id": self.custom_id,
|
|
42
|
+
"label": self.label,
|
|
43
|
+
"style": self.style,
|
|
44
|
+
"disabled": self.disabled,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class ActionRow:
|
|
50
|
+
"""A horizontal row of up to 5 buttons. Pass one or more to `components=`."""
|
|
51
|
+
|
|
52
|
+
buttons: List[Button] = field(default_factory=list)
|
|
53
|
+
|
|
54
|
+
def __init__(self, *buttons: Button):
|
|
55
|
+
if len(buttons) > 5:
|
|
56
|
+
raise ValueError("an action row can hold at most 5 buttons")
|
|
57
|
+
self.buttons = list(buttons)
|
|
58
|
+
|
|
59
|
+
def to_dict(self) -> dict:
|
|
60
|
+
return {"type": "action_row", "buttons": [b.to_dict() for b in self.buttons]}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def components_to_payload(components: Optional[List[ActionRow]]) -> Optional[list]:
|
|
64
|
+
if components is None:
|
|
65
|
+
return None
|
|
66
|
+
return [row.to_dict() for row in components]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ParlonError(Exception):
|
|
7
|
+
"""Raised when the Parlon API rejects a request (invalid token, missing
|
|
8
|
+
permission, bot not installed on that server, etc). ``str(error)``
|
|
9
|
+
contains the server-provided message."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, message: str, code: Optional[str] = None):
|
|
12
|
+
super().__init__(message)
|
|
13
|
+
self.code = code
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Data models returned to your event handlers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
6
|
+
|
|
7
|
+
from .components import ActionRow
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
10
|
+
from .client import Bot
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class SentMessage:
|
|
15
|
+
"""A message that was just sent or edited, returned by ``channel.send`` / ``bot.edit_message``."""
|
|
16
|
+
|
|
17
|
+
id: str
|
|
18
|
+
channel_id: str
|
|
19
|
+
content: str
|
|
20
|
+
raw: dict
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Channel:
|
|
24
|
+
"""Lightweight handle for sending messages into one channel."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, bot: "Bot", channel_id: str):
|
|
27
|
+
self._bot = bot
|
|
28
|
+
self.id = channel_id
|
|
29
|
+
|
|
30
|
+
def send(self, content: str = "", components: Optional[List[ActionRow]] = None) -> SentMessage:
|
|
31
|
+
"""Send a text message, optionally with buttons, into this channel."""
|
|
32
|
+
return self._bot.send_message(self.id, content, components=components)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Message:
|
|
36
|
+
"""A message received while polling (from a server the bot is installed in)."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, bot: "Bot", raw: dict):
|
|
39
|
+
self._bot = bot
|
|
40
|
+
self.raw = raw
|
|
41
|
+
self.id: str = raw["id"]
|
|
42
|
+
self.channel_id: str = raw["channel_id"]
|
|
43
|
+
self.author_id: Optional[str] = raw.get("author_id")
|
|
44
|
+
self.author_name: str = raw.get("author_name", "")
|
|
45
|
+
self.content: str = raw.get("content", "")
|
|
46
|
+
self.created_at: str = raw.get("created_at", "")
|
|
47
|
+
self.bot_id: Optional[str] = raw.get("bot_id")
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def channel(self) -> Channel:
|
|
51
|
+
return Channel(self._bot, self.channel_id)
|
|
52
|
+
|
|
53
|
+
def reply(self, content: str = "", components: Optional[List[ActionRow]] = None) -> SentMessage:
|
|
54
|
+
return self.channel.send(content, components=components)
|
|
55
|
+
|
|
56
|
+
def __repr__(self) -> str: # pragma: no cover
|
|
57
|
+
return f"<Message id={self.id!r} author={self.author_name!r} content={self.content!r}>"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Interaction:
|
|
61
|
+
"""A button click, delivered to your ``on_button_click`` handler."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, bot: "Bot", raw: dict):
|
|
64
|
+
self._bot = bot
|
|
65
|
+
self.raw = raw
|
|
66
|
+
self.id: str = raw["id"]
|
|
67
|
+
self.custom_id: str = raw["custom_id"]
|
|
68
|
+
self.message_id: str = raw["message_id"]
|
|
69
|
+
self.channel_id: str = raw["channel_id"]
|
|
70
|
+
self.server_id: str = raw["server_id"]
|
|
71
|
+
self.user_id: str = raw["user_id"]
|
|
72
|
+
self.created_at: str = raw.get("created_at", "")
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def channel(self) -> Channel:
|
|
76
|
+
return Channel(self._bot, self.channel_id)
|
|
77
|
+
|
|
78
|
+
def respond(self, content: str = "", components: Optional[List[ActionRow]] = None) -> SentMessage:
|
|
79
|
+
"""Send a new message into the same channel in response to this click."""
|
|
80
|
+
return self.channel.send(content, components=components)
|
|
81
|
+
|
|
82
|
+
def update_message(self, content: Optional[str] = None, components: Optional[List[ActionRow]] = None) -> SentMessage:
|
|
83
|
+
"""Edit the original message the button was attached to (e.g. to disable the button)."""
|
|
84
|
+
return self._bot.edit_message(self.message_id, content=content, components=components)
|
|
85
|
+
|
|
86
|
+
def __repr__(self) -> str: # pragma: no cover
|
|
87
|
+
return f"<Interaction custom_id={self.custom_id!r} user_id={self.user_id!r}>"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: parlon-bot
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for Parlon bots (api beta v0.1) — text messages and buttons.
|
|
5
|
+
Author: Parlon
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://parlon-app.vercel.app
|
|
8
|
+
Project-URL: Developer Portal, https://parlon-developers.vercel.app
|
|
9
|
+
Keywords: parlon,bot,chatbot,api,sdk
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Topic :: Communications :: Chat
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: requests>=2.28
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# parlon-bot
|
|
20
|
+
|
|
21
|
+
Official Python SDK for **Parlon** bots — **api beta v0.1**.
|
|
22
|
+
|
|
23
|
+
Supports text messages and buttons today; image messages are coming and will
|
|
24
|
+
first land on the [developer portal](https://parlon-developers.vercel.app),
|
|
25
|
+
then in this library.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install parlon-bot
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Get a token
|
|
34
|
+
|
|
35
|
+
1. Open the [Parlon Developer Portal](https://parlon-developers.vercel.app) and sign in
|
|
36
|
+
(same account as the main [Parlon](https://parlon-app.vercel.app) app).
|
|
37
|
+
2. Create an application. You get a token **once** — copy it immediately (you
|
|
38
|
+
can always reset it later, like Discord).
|
|
39
|
+
3. A persistent **Sandbox** server is created for you automatically and your
|
|
40
|
+
bot is already installed there — open the main Parlon app with the same
|
|
41
|
+
account to test.
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import os
|
|
47
|
+
from parlon_bot import Bot, ActionRow, Button
|
|
48
|
+
|
|
49
|
+
bot = Bot(token=os.environ["PARLON_BOT_TOKEN"])
|
|
50
|
+
|
|
51
|
+
@bot.on_message
|
|
52
|
+
def handle_message(message):
|
|
53
|
+
if message.content.strip() == "!ping":
|
|
54
|
+
message.reply(
|
|
55
|
+
"Pong! Click the button:",
|
|
56
|
+
components=[ActionRow(Button(label="Again", custom_id="ping_again", style="primary"))],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
@bot.on_button_click
|
|
60
|
+
def handle_click(interaction):
|
|
61
|
+
if interaction.custom_id == "ping_again":
|
|
62
|
+
interaction.respond("Pong again! 🏓")
|
|
63
|
+
|
|
64
|
+
bot.run()
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## API
|
|
68
|
+
|
|
69
|
+
- `Bot(token, *, poll_interval=2.0)` — create a client. `run()` blocks and polls for
|
|
70
|
+
events; `poll_once(since)` if you want to drive the loop yourself (e.g. inside
|
|
71
|
+
an existing asyncio/Flask app).
|
|
72
|
+
- `@bot.on_message` — called for every new message in a server this bot is installed
|
|
73
|
+
in (not its own messages). Handler receives a `Message`.
|
|
74
|
+
- `@bot.on_button_click` — called when a member clicks a button. Handler receives an
|
|
75
|
+
`Interaction`.
|
|
76
|
+
- `message.reply(content, components=None)` / `interaction.respond(content, components=None)`
|
|
77
|
+
— send a message into the same channel.
|
|
78
|
+
- `interaction.update_message(content=None, components=None)` — edit the original
|
|
79
|
+
message the button was attached to (e.g. pass `components=[]` to remove the buttons).
|
|
80
|
+
- `bot.send_message(channel_id, content, components=None)` / `bot.edit_message(message_id, ...)`
|
|
81
|
+
— lower-level, if you already have a channel/message id.
|
|
82
|
+
- `Button(label, custom_id, style="secondary", disabled=False)` — `style` is one of
|
|
83
|
+
`primary`, `secondary`, `success`, `danger`.
|
|
84
|
+
- `ActionRow(*buttons)` — up to 5 buttons per row; pass one or more rows as
|
|
85
|
+
`components=[...]`.
|
|
86
|
+
|
|
87
|
+
## Permissions & installation
|
|
88
|
+
|
|
89
|
+
A bot only acts on servers it's installed on, and only within the permissions
|
|
90
|
+
it was granted there (`read_messages`, `send_messages`, `manage_messages`).
|
|
91
|
+
Generate an "Add to server" link from the developer portal's application page —
|
|
92
|
+
whoever manages that server opens it and approves, exactly like adding a bot on
|
|
93
|
+
Discord.
|
|
94
|
+
|
|
95
|
+
## Notes on this beta
|
|
96
|
+
|
|
97
|
+
- Delivery is by short-interval HTTP polling (default every 2s), not a
|
|
98
|
+
persistent socket yet — simple to run anywhere, including from a basic
|
|
99
|
+
script or a free-tier host. This may be swapped for a real-time gateway in
|
|
100
|
+
a later version without breaking your bot's code.
|
|
101
|
+
- DMs aren't supported yet — bots operate in server channels only.
|
|
102
|
+
- Report issues / feature requests to the Parlon team.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
parlon_bot/__init__.py
|
|
5
|
+
parlon_bot/client.py
|
|
6
|
+
parlon_bot/components.py
|
|
7
|
+
parlon_bot/errors.py
|
|
8
|
+
parlon_bot/models.py
|
|
9
|
+
parlon_bot.egg-info/PKG-INFO
|
|
10
|
+
parlon_bot.egg-info/SOURCES.txt
|
|
11
|
+
parlon_bot.egg-info/dependency_links.txt
|
|
12
|
+
parlon_bot.egg-info/requires.txt
|
|
13
|
+
parlon_bot.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.28
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
parlon_bot
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "parlon-bot"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python SDK for Parlon bots (api beta v0.1) — text messages and buttons."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Parlon" }]
|
|
13
|
+
keywords = ["parlon", "bot", "chatbot", "api", "sdk"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Topic :: Communications :: Chat",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"requests>=2.28",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://parlon-app.vercel.app"
|
|
25
|
+
"Developer Portal" = "https://parlon-developers.vercel.app"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
include = ["parlon_bot*"]
|