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.
@@ -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
+ 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*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+