commandless-relay 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.
- commandless_relay-0.1.0/PKG-INFO +78 -0
- commandless_relay-0.1.0/README.md +66 -0
- commandless_relay-0.1.0/pyproject.toml +26 -0
- commandless_relay-0.1.0/setup.cfg +4 -0
- commandless_relay-0.1.0/src/commandless_relay/__init__.py +9 -0
- commandless_relay-0.1.0/src/commandless_relay/client.py +143 -0
- commandless_relay-0.1.0/src/commandless_relay/discord_adapter.py +113 -0
- commandless_relay-0.1.0/src/commandless_relay.egg-info/PKG-INFO +78 -0
- commandless_relay-0.1.0/src/commandless_relay.egg-info/SOURCES.txt +10 -0
- commandless_relay-0.1.0/src/commandless_relay.egg-info/dependency_links.txt +1 -0
- commandless_relay-0.1.0/src/commandless_relay.egg-info/requires.txt +4 -0
- commandless_relay-0.1.0/src/commandless_relay.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: commandless-relay
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Commandless relay API
|
|
5
|
+
Author: Commandless
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: requests>=2.31.0
|
|
10
|
+
Provides-Extra: discord
|
|
11
|
+
Requires-Dist: discord.py>=2.4.0; extra == "discord"
|
|
12
|
+
|
|
13
|
+
# commandless-relay (Python)
|
|
14
|
+
|
|
15
|
+
Official Python SDK for the Commandless relay API.
|
|
16
|
+
|
|
17
|
+
This package gives Python bots a simple client and a ready-to-use `discord.py` adapter.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
Core client:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install commandless-relay
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
With `discord.py` adapter support:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install "commandless-relay[discord]"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quickstart (`discord.py`)
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import os
|
|
37
|
+
import discord
|
|
38
|
+
from commandless_relay import RelayClient, use_discord_adapter
|
|
39
|
+
|
|
40
|
+
TOKEN = os.getenv("BOT_TOKEN")
|
|
41
|
+
API_KEY = os.getenv("COMMANDLESS_API_KEY")
|
|
42
|
+
BASE_URL = os.getenv("COMMANDLESS_SERVICE_URL")
|
|
43
|
+
|
|
44
|
+
intents = discord.Intents.default()
|
|
45
|
+
intents.message_content = True
|
|
46
|
+
intents.messages = True
|
|
47
|
+
intents.guilds = True
|
|
48
|
+
|
|
49
|
+
client = discord.Client(intents=intents)
|
|
50
|
+
relay = RelayClient(api_key=API_KEY, base_url=BASE_URL)
|
|
51
|
+
|
|
52
|
+
use_discord_adapter(client, relay, mention_required=True)
|
|
53
|
+
|
|
54
|
+
@client.event
|
|
55
|
+
async def on_ready():
|
|
56
|
+
print(f"Logged in as {client.user}")
|
|
57
|
+
|
|
58
|
+
client.run(TOKEN)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Environment variables
|
|
62
|
+
|
|
63
|
+
- `BOT_TOKEN` - your Discord bot token
|
|
64
|
+
- `COMMANDLESS_API_KEY` - API key created in Commandless dashboard
|
|
65
|
+
- `COMMANDLESS_SERVICE_URL` - your Commandless backend URL
|
|
66
|
+
- `COMMANDLESS_HMAC_SECRET` - optional HMAC secret
|
|
67
|
+
|
|
68
|
+
## Included components
|
|
69
|
+
|
|
70
|
+
- `RelayClient`
|
|
71
|
+
- `send_event(event)` -> Decision dict or `None`
|
|
72
|
+
- `register_bot(...)` -> botId (optional flow)
|
|
73
|
+
- `heartbeat()` (optional flow)
|
|
74
|
+
- `use_discord_adapter(client, relay, mention_required=True, execute=None)`
|
|
75
|
+
- binds an `on_message` listener
|
|
76
|
+
- sends events to relay
|
|
77
|
+
- executes reply actions by default
|
|
78
|
+
- sends a clear message on billing rejection (402 / no subscription or credits)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# commandless-relay (Python)
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the Commandless relay API.
|
|
4
|
+
|
|
5
|
+
This package gives Python bots a simple client and a ready-to-use `discord.py` adapter.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Core client:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install commandless-relay
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
With `discord.py` adapter support:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install "commandless-relay[discord]"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quickstart (`discord.py`)
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import os
|
|
25
|
+
import discord
|
|
26
|
+
from commandless_relay import RelayClient, use_discord_adapter
|
|
27
|
+
|
|
28
|
+
TOKEN = os.getenv("BOT_TOKEN")
|
|
29
|
+
API_KEY = os.getenv("COMMANDLESS_API_KEY")
|
|
30
|
+
BASE_URL = os.getenv("COMMANDLESS_SERVICE_URL")
|
|
31
|
+
|
|
32
|
+
intents = discord.Intents.default()
|
|
33
|
+
intents.message_content = True
|
|
34
|
+
intents.messages = True
|
|
35
|
+
intents.guilds = True
|
|
36
|
+
|
|
37
|
+
client = discord.Client(intents=intents)
|
|
38
|
+
relay = RelayClient(api_key=API_KEY, base_url=BASE_URL)
|
|
39
|
+
|
|
40
|
+
use_discord_adapter(client, relay, mention_required=True)
|
|
41
|
+
|
|
42
|
+
@client.event
|
|
43
|
+
async def on_ready():
|
|
44
|
+
print(f"Logged in as {client.user}")
|
|
45
|
+
|
|
46
|
+
client.run(TOKEN)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Environment variables
|
|
50
|
+
|
|
51
|
+
- `BOT_TOKEN` - your Discord bot token
|
|
52
|
+
- `COMMANDLESS_API_KEY` - API key created in Commandless dashboard
|
|
53
|
+
- `COMMANDLESS_SERVICE_URL` - your Commandless backend URL
|
|
54
|
+
- `COMMANDLESS_HMAC_SECRET` - optional HMAC secret
|
|
55
|
+
|
|
56
|
+
## Included components
|
|
57
|
+
|
|
58
|
+
- `RelayClient`
|
|
59
|
+
- `send_event(event)` -> Decision dict or `None`
|
|
60
|
+
- `register_bot(...)` -> botId (optional flow)
|
|
61
|
+
- `heartbeat()` (optional flow)
|
|
62
|
+
- `use_discord_adapter(client, relay, mention_required=True, execute=None)`
|
|
63
|
+
- binds an `on_message` listener
|
|
64
|
+
- sends events to relay
|
|
65
|
+
- executes reply actions by default
|
|
66
|
+
- sends a clear message on billing rejection (402 / no subscription or credits)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "commandless-relay"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for Commandless relay API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Commandless" }
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"requests>=2.31.0"
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
discord = ["discord.py>=2.4.0"]
|
|
21
|
+
|
|
22
|
+
[tool.setuptools]
|
|
23
|
+
package-dir = { "" = "src" }
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
where = ["src"]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import hashlib
|
|
3
|
+
import hmac
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
DEFAULT_BASE = "https://commandless-app-production.up.railway.app"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class RelayError(Exception):
|
|
16
|
+
status: int
|
|
17
|
+
message: str
|
|
18
|
+
raw_error: Optional[str] = None
|
|
19
|
+
|
|
20
|
+
def __str__(self) -> str:
|
|
21
|
+
return f"Commandless error ({self.status}): {self.message}"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RelayClient:
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
api_key: str,
|
|
28
|
+
base_url: Optional[str] = None,
|
|
29
|
+
hmac_secret: Optional[str] = None,
|
|
30
|
+
timeout_ms: int = 15000,
|
|
31
|
+
max_retries: int = 3,
|
|
32
|
+
) -> None:
|
|
33
|
+
if not api_key:
|
|
34
|
+
raise ValueError("api_key is required")
|
|
35
|
+
self.api_key = api_key
|
|
36
|
+
base = base_url or DEFAULT_BASE
|
|
37
|
+
if not base.startswith(("http://", "https://")):
|
|
38
|
+
base = f"https://{base}"
|
|
39
|
+
self.base_url = base.rstrip("/")
|
|
40
|
+
self.hmac_secret = hmac_secret
|
|
41
|
+
self.timeout = max(timeout_ms / 1000.0, 1)
|
|
42
|
+
self.max_retries = max_retries
|
|
43
|
+
self.bot_id: Optional[str] = None
|
|
44
|
+
|
|
45
|
+
def register_bot(
|
|
46
|
+
self,
|
|
47
|
+
platform: str = "discord",
|
|
48
|
+
name: Optional[str] = None,
|
|
49
|
+
client_id: Optional[str] = None,
|
|
50
|
+
bot_id: Optional[int] = None,
|
|
51
|
+
) -> Optional[str]:
|
|
52
|
+
payload: Dict[str, Any] = {
|
|
53
|
+
"platform": platform,
|
|
54
|
+
"name": name,
|
|
55
|
+
"clientId": client_id,
|
|
56
|
+
"botId": bot_id,
|
|
57
|
+
}
|
|
58
|
+
res = self._post_json("/v1/relay/register", payload)
|
|
59
|
+
if res.get("ok") and isinstance(res.get("data"), dict) and res["data"].get("botId"):
|
|
60
|
+
self.bot_id = str(res["data"]["botId"])
|
|
61
|
+
return self.bot_id
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
def heartbeat(self) -> Optional[Dict[str, Any]]:
|
|
65
|
+
res = self._post_json("/v1/relay/heartbeat", {"botId": self.bot_id})
|
|
66
|
+
if res.get("ok"):
|
|
67
|
+
return res.get("data")
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
def send_event(self, event: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
71
|
+
payload = dict(event)
|
|
72
|
+
if self.bot_id and "botId" not in payload:
|
|
73
|
+
payload["botId"] = self.bot_id
|
|
74
|
+
idem = self._make_idempotency_key(payload)
|
|
75
|
+
|
|
76
|
+
last_error: Optional[RelayError] = None
|
|
77
|
+
for attempt in range(self.max_retries + 1):
|
|
78
|
+
res = self._post_json("/v1/relay/events", payload, idempotency_key=idem)
|
|
79
|
+
if res.get("ok"):
|
|
80
|
+
data = res.get("data") or {}
|
|
81
|
+
if isinstance(data, dict):
|
|
82
|
+
return data.get("decision")
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
status = int(res.get("status", 0))
|
|
86
|
+
err_text = str(res.get("error") or "unknown")
|
|
87
|
+
parsed_message = self._extract_error_message(err_text) or err_text
|
|
88
|
+
last_error = RelayError(status=status, message=parsed_message, raw_error=err_text)
|
|
89
|
+
time.sleep(0.2 * (attempt + 1))
|
|
90
|
+
|
|
91
|
+
if last_error:
|
|
92
|
+
raise last_error
|
|
93
|
+
raise RelayError(status=0, message="Unknown relay failure")
|
|
94
|
+
|
|
95
|
+
def _make_idempotency_key(self, event: Dict[str, Any]) -> str:
|
|
96
|
+
base = f"{event.get('type')}:{event.get('id')}:{int(int(event.get('timestamp', int(time.time() * 1000))) / 1000)}"
|
|
97
|
+
return base64.urlsafe_b64encode(base.encode("utf-8")).decode("utf-8").rstrip("=")
|
|
98
|
+
|
|
99
|
+
def _extract_error_message(self, text: str) -> Optional[str]:
|
|
100
|
+
try:
|
|
101
|
+
data = json.loads(text)
|
|
102
|
+
if isinstance(data, dict):
|
|
103
|
+
return str(data.get("message") or data.get("error") or "")
|
|
104
|
+
except Exception:
|
|
105
|
+
pass
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
def _post_json(
|
|
109
|
+
self,
|
|
110
|
+
path: str,
|
|
111
|
+
body: Dict[str, Any],
|
|
112
|
+
idempotency_key: Optional[str] = None,
|
|
113
|
+
) -> Dict[str, Any]:
|
|
114
|
+
url = f"{self.base_url}{path}"
|
|
115
|
+
json_body = json.dumps(body, separators=(",", ":"))
|
|
116
|
+
headers = {
|
|
117
|
+
"content-type": "application/json",
|
|
118
|
+
"x-api-key": self.api_key,
|
|
119
|
+
"x-commandless-key": self.api_key,
|
|
120
|
+
"x-timestamp": str(int(time.time() * 1000)),
|
|
121
|
+
}
|
|
122
|
+
if idempotency_key:
|
|
123
|
+
headers["x-idempotency-key"] = idempotency_key
|
|
124
|
+
if self.hmac_secret:
|
|
125
|
+
signature = hmac.new(
|
|
126
|
+
self.hmac_secret.encode("utf-8"),
|
|
127
|
+
json_body.encode("utf-8"),
|
|
128
|
+
hashlib.sha256,
|
|
129
|
+
).hexdigest()
|
|
130
|
+
headers["x-signature"] = signature
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
response = requests.post(
|
|
134
|
+
url,
|
|
135
|
+
data=json_body,
|
|
136
|
+
headers=headers,
|
|
137
|
+
timeout=self.timeout,
|
|
138
|
+
)
|
|
139
|
+
if not response.ok:
|
|
140
|
+
return {"ok": False, "status": response.status_code, "error": response.text}
|
|
141
|
+
return {"ok": True, "status": response.status_code, "data": response.json()}
|
|
142
|
+
except Exception as exc:
|
|
143
|
+
return {"ok": False, "status": 0, "error": str(exc)}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import time
|
|
3
|
+
from typing import Any, Awaitable, Callable, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from .client import RelayClient, RelayError
|
|
6
|
+
|
|
7
|
+
DecisionHandler = Callable[[Dict[str, Any], Any], Awaitable[None]]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DiscordAdapter:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
client: Any,
|
|
14
|
+
relay: RelayClient,
|
|
15
|
+
mention_required: bool = True,
|
|
16
|
+
execute: Optional[DecisionHandler] = None,
|
|
17
|
+
) -> None:
|
|
18
|
+
self.client = client
|
|
19
|
+
self.relay = relay
|
|
20
|
+
self.mention_required = mention_required
|
|
21
|
+
self.execute = execute
|
|
22
|
+
|
|
23
|
+
def bind(self) -> None:
|
|
24
|
+
self.client.add_listener(self._on_message, "on_message")
|
|
25
|
+
|
|
26
|
+
async def _on_message(self, message: Any) -> None:
|
|
27
|
+
if not message or not getattr(message, "author", None):
|
|
28
|
+
return
|
|
29
|
+
if getattr(message.author, "bot", False):
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
mentioned = False
|
|
33
|
+
try:
|
|
34
|
+
mentioned = bool(self.client.user and self.client.user in message.mentions)
|
|
35
|
+
except Exception:
|
|
36
|
+
mentioned = False
|
|
37
|
+
|
|
38
|
+
is_reply_to_bot = False
|
|
39
|
+
try:
|
|
40
|
+
if getattr(message, "reference", None) and getattr(message.reference, "resolved", None):
|
|
41
|
+
ref = message.reference.resolved
|
|
42
|
+
if getattr(ref, "author", None) and self.client.user:
|
|
43
|
+
is_reply_to_bot = ref.author.id == self.client.user.id
|
|
44
|
+
except Exception:
|
|
45
|
+
is_reply_to_bot = False
|
|
46
|
+
|
|
47
|
+
if self.mention_required and not mentioned and not is_reply_to_bot:
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
event = {
|
|
51
|
+
"type": "messageCreate",
|
|
52
|
+
"id": str(message.id),
|
|
53
|
+
"guildId": str(message.guild.id) if getattr(message, "guild", None) else None,
|
|
54
|
+
"channelId": str(message.channel.id),
|
|
55
|
+
"authorId": str(message.author.id),
|
|
56
|
+
"content": str(message.content or ""),
|
|
57
|
+
"timestamp": int((getattr(message, "created_at", None) or time.time()).timestamp() * 1000)
|
|
58
|
+
if getattr(message, "created_at", None)
|
|
59
|
+
else int(time.time() * 1000),
|
|
60
|
+
"botClientId": str(self.client.user.id) if getattr(self.client, "user", None) else None,
|
|
61
|
+
"isReplyToBot": is_reply_to_bot,
|
|
62
|
+
"referencedMessageId": str(message.reference.message_id) if getattr(message, "reference", None) and getattr(message.reference, "message_id", None) else None,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
async with message.channel.typing():
|
|
67
|
+
decision = await asyncio.to_thread(self.relay.send_event, event)
|
|
68
|
+
|
|
69
|
+
if not decision:
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
if self.execute:
|
|
73
|
+
await self.execute(decision, message)
|
|
74
|
+
else:
|
|
75
|
+
await self._default_execute(decision, message)
|
|
76
|
+
except RelayError as err:
|
|
77
|
+
msg = str(err)
|
|
78
|
+
if err.status == 402 or "SUBSCRIPTION_REQUIRED" in msg or "Payment Required" in msg:
|
|
79
|
+
try:
|
|
80
|
+
await message.reply(
|
|
81
|
+
"This bot's Commandless subscription is inactive or out of free credits. "
|
|
82
|
+
"Please ask the bot owner to update billing."
|
|
83
|
+
)
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
86
|
+
except Exception:
|
|
87
|
+
# Keep adapter resilient by default
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
async def _default_execute(self, decision: Dict[str, Any], message: Any) -> None:
|
|
91
|
+
actions = decision.get("actions") or []
|
|
92
|
+
for action in actions:
|
|
93
|
+
if action.get("kind") == "reply":
|
|
94
|
+
content = action.get("content")
|
|
95
|
+
if content:
|
|
96
|
+
await message.reply(str(content))
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def use_discord_adapter(
|
|
101
|
+
client: Any,
|
|
102
|
+
relay: RelayClient,
|
|
103
|
+
mention_required: bool = True,
|
|
104
|
+
execute: Optional[DecisionHandler] = None,
|
|
105
|
+
) -> DiscordAdapter:
|
|
106
|
+
adapter = DiscordAdapter(
|
|
107
|
+
client=client,
|
|
108
|
+
relay=relay,
|
|
109
|
+
mention_required=mention_required,
|
|
110
|
+
execute=execute,
|
|
111
|
+
)
|
|
112
|
+
adapter.bind()
|
|
113
|
+
return adapter
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: commandless-relay
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Commandless relay API
|
|
5
|
+
Author: Commandless
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: requests>=2.31.0
|
|
10
|
+
Provides-Extra: discord
|
|
11
|
+
Requires-Dist: discord.py>=2.4.0; extra == "discord"
|
|
12
|
+
|
|
13
|
+
# commandless-relay (Python)
|
|
14
|
+
|
|
15
|
+
Official Python SDK for the Commandless relay API.
|
|
16
|
+
|
|
17
|
+
This package gives Python bots a simple client and a ready-to-use `discord.py` adapter.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
Core client:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install commandless-relay
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
With `discord.py` adapter support:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install "commandless-relay[discord]"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quickstart (`discord.py`)
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import os
|
|
37
|
+
import discord
|
|
38
|
+
from commandless_relay import RelayClient, use_discord_adapter
|
|
39
|
+
|
|
40
|
+
TOKEN = os.getenv("BOT_TOKEN")
|
|
41
|
+
API_KEY = os.getenv("COMMANDLESS_API_KEY")
|
|
42
|
+
BASE_URL = os.getenv("COMMANDLESS_SERVICE_URL")
|
|
43
|
+
|
|
44
|
+
intents = discord.Intents.default()
|
|
45
|
+
intents.message_content = True
|
|
46
|
+
intents.messages = True
|
|
47
|
+
intents.guilds = True
|
|
48
|
+
|
|
49
|
+
client = discord.Client(intents=intents)
|
|
50
|
+
relay = RelayClient(api_key=API_KEY, base_url=BASE_URL)
|
|
51
|
+
|
|
52
|
+
use_discord_adapter(client, relay, mention_required=True)
|
|
53
|
+
|
|
54
|
+
@client.event
|
|
55
|
+
async def on_ready():
|
|
56
|
+
print(f"Logged in as {client.user}")
|
|
57
|
+
|
|
58
|
+
client.run(TOKEN)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Environment variables
|
|
62
|
+
|
|
63
|
+
- `BOT_TOKEN` - your Discord bot token
|
|
64
|
+
- `COMMANDLESS_API_KEY` - API key created in Commandless dashboard
|
|
65
|
+
- `COMMANDLESS_SERVICE_URL` - your Commandless backend URL
|
|
66
|
+
- `COMMANDLESS_HMAC_SECRET` - optional HMAC secret
|
|
67
|
+
|
|
68
|
+
## Included components
|
|
69
|
+
|
|
70
|
+
- `RelayClient`
|
|
71
|
+
- `send_event(event)` -> Decision dict or `None`
|
|
72
|
+
- `register_bot(...)` -> botId (optional flow)
|
|
73
|
+
- `heartbeat()` (optional flow)
|
|
74
|
+
- `use_discord_adapter(client, relay, mention_required=True, execute=None)`
|
|
75
|
+
- binds an `on_message` listener
|
|
76
|
+
- sends events to relay
|
|
77
|
+
- executes reply actions by default
|
|
78
|
+
- sends a clear message on billing rejection (402 / no subscription or credits)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/commandless_relay/__init__.py
|
|
4
|
+
src/commandless_relay/client.py
|
|
5
|
+
src/commandless_relay/discord_adapter.py
|
|
6
|
+
src/commandless_relay.egg-info/PKG-INFO
|
|
7
|
+
src/commandless_relay.egg-info/SOURCES.txt
|
|
8
|
+
src/commandless_relay.egg-info/dependency_links.txt
|
|
9
|
+
src/commandless_relay.egg-info/requires.txt
|
|
10
|
+
src/commandless_relay.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
commandless_relay
|