markanm 0.1.0a1__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 MarkanM Developer Team
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 all
13
+ 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 THE
21
+ SOFTWARE.
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: markanm
3
+ Version: 0.1.0a1
4
+ Summary: Official Python SDK for MarkanM Chat Bot Platform & REST API (v0.1.0-alpha)
5
+ Author-email: MarkanM Developer Team <developers@markanm.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 MarkanM Developer Team
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://chat.markanm.com/developers
29
+ Project-URL: Documentation, https://chat.markanm.com/developers/docs
30
+ Project-URL: Repository, https://github.com/markanm/markanm-python-sdk
31
+ Keywords: markanm,bot,chat,sdk,api,oauth,webhooks
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.8
37
+ Classifier: Programming Language :: Python :: 3.9
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Topic :: Communications :: Chat
42
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
43
+ Requires-Python: >=3.8
44
+ Description-Content-Type: text/markdown
45
+ License-File: LICENSE
46
+ Requires-Dist: httpx>=0.24.0
47
+ Dynamic: license-file
48
+
49
+ # MarkanM Official Python SDK (`markanm 0.1.0a1`)
50
+
51
+ Official Python library (v0.1.0-alpha) for building developer bots, automation, and AI agents on **MarkanM Chat** (`https://chat.markanm.com`).
52
+
53
+ ---
54
+
55
+ ## ⚠️ Alpha Release Notice
56
+
57
+ This is **v0.1.0-alpha** (`markanm 0.1.0a1`). It is intended for testing against the MarkanM Bot API v1.
58
+
59
+ ---
60
+
61
+ ## 🚀 Installation
62
+
63
+ Install via `pip`:
64
+
65
+ ```bash
66
+ pip install markanm
67
+ ```
68
+
69
+ ---
70
+
71
+ ## ⚡ Quick Start
72
+
73
+ ### 1. Simple Hello Bot (`hello_bot.py`)
74
+
75
+ ```python
76
+ import os
77
+ from markanm import Bot
78
+
79
+ bot = Bot(os.environ["MARKANM_BOT_TOKEN"])
80
+
81
+ @bot.command("hello")
82
+ async def hello(ctx):
83
+ await ctx.reply("Hello 👋 Welcome to MarkanM!")
84
+
85
+ @bot.command("start")
86
+ async def start(ctx):
87
+ await ctx.reply("🚀 Bot activated and ready!")
88
+
89
+ if __name__ == "__main__":
90
+ bot.run_polling()
91
+ ```
92
+
93
+ ---
94
+
95
+ ### 2. Event Listeners
96
+
97
+ ```python
98
+ @bot.on("message.created")
99
+ async def on_message(event):
100
+ print(f"New message received: {event.payload}")
101
+
102
+ @bot.on("member.joined")
103
+ def on_member_joined(event):
104
+ print(f"User joined room: {event.payload.get('user')}")
105
+ ```
106
+
107
+ ---
108
+
109
+ ### 3. AI Assistant Bot (`ai_bot.py`)
110
+
111
+ ```python
112
+ import os
113
+ from markanm import Bot
114
+ from markanm.ai import AI
115
+
116
+ bot = Bot(os.environ["MARKANM_BOT_TOKEN"])
117
+ ai = AI(provider="openai", api_key=os.environ["OPENAI_API_KEY"])
118
+
119
+ @bot.command("ask")
120
+ async def ask(ctx):
121
+ prompt = " ".join(ctx.args)
122
+ if not prompt:
123
+ await ctx.reply("Usage: /ask <question>")
124
+ return
125
+
126
+ answer = await ai.generate(prompt)
127
+ await ctx.reply(answer)
128
+
129
+ bot.run_polling()
130
+ ```
131
+
132
+ ---
133
+
134
+ ### 4. Webhook Verification (Flask / FastAPI)
135
+
136
+ ```python
137
+ from markanm import WebhookHandler, MarkanMWebhookError
138
+
139
+ handler = WebhookHandler(secret="whsec_YOUR_SECRET", timestamp_tolerance=300)
140
+
141
+ try:
142
+ # Verifies HMAC-SHA256 signature header and 300s timestamp tolerance
143
+ event = handler.process_event(request_body_text, request_headers.get("X-MarkanM-Signature"))
144
+ print(f"Event verified: {event.id} ({event.type})")
145
+ except MarkanMWebhookError as err:
146
+ print(f"Webhook error: {err}")
147
+ ```
148
+
149
+ ---
150
+
151
+ ## 🧪 Testing
152
+
153
+ Run tests with `pytest`:
154
+
155
+ ```bash
156
+ pytest sdk/python/tests/
157
+ ```
158
+
159
+ ---
160
+
161
+ ## 📚 API Reference
162
+ For complete API endpoints and permission scope guidelines, visit:
163
+ **https://chat.markanm.com/developers/docs**
@@ -0,0 +1,115 @@
1
+ # MarkanM Official Python SDK (`markanm 0.1.0a1`)
2
+
3
+ Official Python library (v0.1.0-alpha) for building developer bots, automation, and AI agents on **MarkanM Chat** (`https://chat.markanm.com`).
4
+
5
+ ---
6
+
7
+ ## ⚠️ Alpha Release Notice
8
+
9
+ This is **v0.1.0-alpha** (`markanm 0.1.0a1`). It is intended for testing against the MarkanM Bot API v1.
10
+
11
+ ---
12
+
13
+ ## 🚀 Installation
14
+
15
+ Install via `pip`:
16
+
17
+ ```bash
18
+ pip install markanm
19
+ ```
20
+
21
+ ---
22
+
23
+ ## ⚡ Quick Start
24
+
25
+ ### 1. Simple Hello Bot (`hello_bot.py`)
26
+
27
+ ```python
28
+ import os
29
+ from markanm import Bot
30
+
31
+ bot = Bot(os.environ["MARKANM_BOT_TOKEN"])
32
+
33
+ @bot.command("hello")
34
+ async def hello(ctx):
35
+ await ctx.reply("Hello 👋 Welcome to MarkanM!")
36
+
37
+ @bot.command("start")
38
+ async def start(ctx):
39
+ await ctx.reply("🚀 Bot activated and ready!")
40
+
41
+ if __name__ == "__main__":
42
+ bot.run_polling()
43
+ ```
44
+
45
+ ---
46
+
47
+ ### 2. Event Listeners
48
+
49
+ ```python
50
+ @bot.on("message.created")
51
+ async def on_message(event):
52
+ print(f"New message received: {event.payload}")
53
+
54
+ @bot.on("member.joined")
55
+ def on_member_joined(event):
56
+ print(f"User joined room: {event.payload.get('user')}")
57
+ ```
58
+
59
+ ---
60
+
61
+ ### 3. AI Assistant Bot (`ai_bot.py`)
62
+
63
+ ```python
64
+ import os
65
+ from markanm import Bot
66
+ from markanm.ai import AI
67
+
68
+ bot = Bot(os.environ["MARKANM_BOT_TOKEN"])
69
+ ai = AI(provider="openai", api_key=os.environ["OPENAI_API_KEY"])
70
+
71
+ @bot.command("ask")
72
+ async def ask(ctx):
73
+ prompt = " ".join(ctx.args)
74
+ if not prompt:
75
+ await ctx.reply("Usage: /ask <question>")
76
+ return
77
+
78
+ answer = await ai.generate(prompt)
79
+ await ctx.reply(answer)
80
+
81
+ bot.run_polling()
82
+ ```
83
+
84
+ ---
85
+
86
+ ### 4. Webhook Verification (Flask / FastAPI)
87
+
88
+ ```python
89
+ from markanm import WebhookHandler, MarkanMWebhookError
90
+
91
+ handler = WebhookHandler(secret="whsec_YOUR_SECRET", timestamp_tolerance=300)
92
+
93
+ try:
94
+ # Verifies HMAC-SHA256 signature header and 300s timestamp tolerance
95
+ event = handler.process_event(request_body_text, request_headers.get("X-MarkanM-Signature"))
96
+ print(f"Event verified: {event.id} ({event.type})")
97
+ except MarkanMWebhookError as err:
98
+ print(f"Webhook error: {err}")
99
+ ```
100
+
101
+ ---
102
+
103
+ ## 🧪 Testing
104
+
105
+ Run tests with `pytest`:
106
+
107
+ ```bash
108
+ pytest sdk/python/tests/
109
+ ```
110
+
111
+ ---
112
+
113
+ ## 📚 API Reference
114
+ For complete API endpoints and permission scope guidelines, visit:
115
+ **https://chat.markanm.com/developers/docs**
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "markanm"
7
+ version = "0.1.0a1"
8
+ description = "Official Python SDK for MarkanM Chat Bot Platform & REST API (v0.1.0-alpha)"
9
+ readme = "README.md"
10
+ authors = [
11
+ { name = "MarkanM Developer Team", email = "developers@markanm.com" }
12
+ ]
13
+ license = { file = "LICENSE" }
14
+ requires-python = ">=3.8"
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.8",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Topic :: Communications :: Chat",
26
+ "Topic :: Software Development :: Libraries :: Python Modules"
27
+ ]
28
+ keywords = ["markanm", "bot", "chat", "sdk", "api", "oauth", "webhooks"]
29
+ dependencies = [
30
+ "httpx>=0.24.0"
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://chat.markanm.com/developers"
35
+ Documentation = "https://chat.markanm.com/developers/docs"
36
+ Repository = "https://github.com/markanm/markanm-python-sdk"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,8 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="markanm",
5
+ version="0.1.0a1",
6
+ packages=find_packages(where="src"),
7
+ package_dir={"": "src"},
8
+ )
@@ -0,0 +1,41 @@
1
+ """
2
+ MarkanM Official Python SDK (v0.1.0-alpha)
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
+
5
+ Official Python library for building developer bots, automation, and AI agents on MarkanM Chat.
6
+ """
7
+
8
+ __version__ = "0.1.0a1"
9
+ __author__ = "MarkanM Developer Team"
10
+
11
+ from .bot import Bot
12
+ from .context import CommandContext
13
+ from .events import Event
14
+ from .webhook import WebhookHandler
15
+ from .ai import AI
16
+ from .exceptions import (
17
+ MarkanMError,
18
+ MarkanMAPIError,
19
+ MarkanMAuthError,
20
+ MarkanMPermissionError,
21
+ MarkanMNotFoundError,
22
+ MarkanMRateLimitError,
23
+ MarkanMWebhookError,
24
+ MarkanMValidationError
25
+ )
26
+
27
+ __all__ = [
28
+ "Bot",
29
+ "CommandContext",
30
+ "Event",
31
+ "WebhookHandler",
32
+ "AI",
33
+ "MarkanMError",
34
+ "MarkanMAPIError",
35
+ "MarkanMAuthError",
36
+ "MarkanMPermissionError",
37
+ "MarkanMNotFoundError",
38
+ "MarkanMRateLimitError",
39
+ "MarkanMWebhookError",
40
+ "MarkanMValidationError"
41
+ ]
@@ -0,0 +1,73 @@
1
+ import json
2
+ import asyncio
3
+ from typing import Optional
4
+ from .exceptions import MarkanMError, _mask_sensitive
5
+
6
+ try:
7
+ import httpx
8
+ HAS_HTTPX = True
9
+ except ImportError:
10
+ HAS_HTTPX = False
11
+ import urllib.request
12
+ import urllib.error
13
+
14
+ class AI:
15
+ """
16
+ AI Integration Layer for MarkanM Developer Bots.
17
+ Allows developers to integrate OpenAI or custom HTTP AI model endpoints using their own credentials.
18
+ """
19
+ def __init__(self, provider: str = "openai", api_key: str = "", model: str = "gpt-3.5-turbo", base_url: Optional[str] = None):
20
+ self.provider = provider.lower().strip()
21
+ self.api_key = api_key
22
+ self.model = model
23
+ self.base_url = base_url or "https://api.openai.com/v1"
24
+
25
+ async def generate(self, prompt: str, system_prompt: str = "You are a helpful MarkanM Bot assistant.") -> str:
26
+ """
27
+ Generate text response from configured AI provider
28
+ """
29
+ if not prompt or not isinstance(prompt, str):
30
+ raise MarkanMError("Prompt must be a non-empty string")
31
+
32
+ if self.provider == "openai":
33
+ if not self.api_key:
34
+ raise MarkanMError("OpenAI API Key is required for OpenAI AI provider")
35
+
36
+ headers = {
37
+ "Authorization": f"Bearer {self.api_key}",
38
+ "Content-Type": "application/json"
39
+ }
40
+ payload = {
41
+ "model": self.model,
42
+ "messages": [
43
+ {"role": "system", "content": system_prompt},
44
+ {"role": "user", "content": prompt}
45
+ ]
46
+ }
47
+
48
+ if HAS_HTTPX:
49
+ async with httpx.AsyncClient(timeout=15.0) as client:
50
+ try:
51
+ res = await client.post(f"{self.base_url.rstrip('/')}/chat/completions", json=payload, headers=headers)
52
+ if res.is_success:
53
+ data = res.json()
54
+ return data["choices"][0]["message"]["content"].strip()
55
+ else:
56
+ raise MarkanMError(f"AI Provider HTTP {res.status_code}: {res.text}")
57
+ except Exception as exc:
58
+ raise MarkanMError(f"AI Provider request failed: {_mask_sensitive(str(exc))}")
59
+ else:
60
+ def _sync_ai_request():
61
+ req_data = json.dumps(payload).encode("utf-8")
62
+ req = urllib.request.Request(f"{self.base_url.rstrip('/')}/chat/completions", data=req_data, headers=headers, method="POST")
63
+ try:
64
+ with urllib.request.urlopen(req, timeout=15) as resp:
65
+ res_text = resp.read().decode("utf-8")
66
+ data = json.loads(res_text)
67
+ return data["choices"][0]["message"]["content"].strip()
68
+ except Exception as exc:
69
+ raise MarkanMError(f"AI Provider request failed: {_mask_sensitive(str(exc))}")
70
+
71
+ return await asyncio.to_thread(_sync_ai_request)
72
+
73
+ raise MarkanMError(f"Unsupported AI provider '{self.provider}'. Supported providers: 'openai'")
@@ -0,0 +1,188 @@
1
+ import asyncio
2
+ import inspect
3
+ import signal
4
+ import sys
5
+ from typing import Callable, Dict, List, Any, Optional
6
+ from .client import APIClient
7
+ from .context import CommandContext
8
+ from .events import Event
9
+ from .webhook import WebhookHandler
10
+ from .exceptions import MarkanMError, _mask_sensitive
11
+
12
+ class Bot:
13
+ """
14
+ Official MarkanM Bot Client (v0.1.0-alpha)
15
+ """
16
+ def __init__(
17
+ self,
18
+ token: str,
19
+ base_url: str = "https://chat.markanm.com/api/bot/v1",
20
+ timeout: float = 10.0,
21
+ webhook_secret: Optional[str] = None
22
+ ):
23
+ if not token or not isinstance(token, str):
24
+ raise MarkanMError("Bot token must be a non-empty string starting with 'mkbot_...'")
25
+
26
+ self.token = token
27
+ self.client = APIClient(token=token, base_url=base_url, timeout=timeout)
28
+ self.webhook_secret = webhook_secret
29
+ self.commands: Dict[str, Callable] = {}
30
+ self.listeners: Dict[str, List[Callable]] = {}
31
+ self._is_running = False
32
+
33
+ def command(self, name: str):
34
+ """
35
+ Decorator to register a bot command handler.
36
+ Usage:
37
+ @bot.command("hello")
38
+ async def hello_handler(ctx):
39
+ await ctx.reply("Hello 👋")
40
+ """
41
+ clean_name = name.lstrip("/").strip().lower()
42
+ def decorator(func: Callable):
43
+ self.commands[clean_name] = func
44
+ return func
45
+ return decorator
46
+
47
+ def on(self, event_type: str):
48
+ """
49
+ Decorator to register an event listener. Supports multiple listeners per event.
50
+ Usage:
51
+ @bot.on("message.created")
52
+ async def on_message(evt):
53
+ print(evt.type)
54
+ """
55
+ clean_type = event_type.strip()
56
+ def decorator(func: Callable):
57
+ if clean_type not in self.listeners:
58
+ self.listeners[clean_type] = []
59
+ self.listeners[clean_type].append(func)
60
+ return func
61
+ return decorator
62
+
63
+ async def get_me() -> Dict[str, Any]:
64
+ """Fetch bot's profile"""
65
+ return await self.client.get("/me")
66
+
67
+ async def get_room(self, room_id: str) -> Dict[str, Any]:
68
+ """Fetch room details"""
69
+ return await self.client.get(f"/rooms/{room_id}")
70
+
71
+ async def get_room_members(self, room_id: str) -> Dict[str, Any]:
72
+ """Fetch room members"""
73
+ return await self.client.get(f"/rooms/{room_id}/members")
74
+
75
+ async def send_message(self, room_id: str, text: str, card: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
76
+ """Send a text or interactive card message to a room"""
77
+ payload = {"text": text}
78
+ if card:
79
+ payload["card"] = card
80
+ payload["type"] = "card"
81
+ return await self.client.post(f"/rooms/{room_id}/messages", json_data=payload)
82
+
83
+ async def reply(self, message_id: str, text: str) -> Dict[str, Any]:
84
+ """Reply directly to a message"""
85
+ return await self.client.post(f"/messages/{message_id}/reply", json_data={"text": text})
86
+
87
+ async def react(self, message_id: str, emoji: str = "👍") -> Dict[str, Any]:
88
+ """Toggle emoji reaction on a message"""
89
+ return await self.client.post(f"/messages/{message_id}/react", json_data={"emoji": emoji})
90
+
91
+ async def delete_message(self, message_id: str) -> Dict[str, Any]:
92
+ """Delete a message"""
93
+ return await self.client.delete(f"/messages/{message_id}")
94
+
95
+ async def dispatch_event(self, event: Event):
96
+ """
97
+ Dispatch incoming Event to registered event listeners & command handlers
98
+ """
99
+ evt_type = event.type
100
+
101
+ # 1. Execute general event listeners
102
+ if evt_type in self.listeners:
103
+ for listener in self.listeners[evt_type]:
104
+ await self._invoke_handler(listener, event)
105
+
106
+ # 2. Execute command handler if event is 'command.received'
107
+ if evt_type == "command.received" or (isinstance(event.payload, dict) and "command" in event.payload):
108
+ cmd_payload = event.payload if isinstance(event.payload, dict) else {}
109
+ cmd_name = str(cmd_payload.get("command", "")).lstrip("/").strip().lower()
110
+
111
+ if cmd_name in self.commands:
112
+ ctx = CommandContext(self, cmd_payload)
113
+ handler = self.commands[cmd_name]
114
+ await self._invoke_handler(handler, ctx)
115
+
116
+ async def _invoke_handler(self, handler: Callable, arg: Any):
117
+ """Helper to invoke sync or async handler safely"""
118
+ try:
119
+ if inspect.iscoroutinefunction(handler):
120
+ await handler(arg)
121
+ else:
122
+ loop = asyncio.get_running_loop()
123
+ await loop.run_in_executor(None, handler, arg)
124
+ except Exception as exc:
125
+ safe_msg = _mask_sensitive(str(exc))
126
+ print(f"⚠️ Error executing handler {handler.__name__}: {safe_msg}")
127
+
128
+ def verify_webhook(self, payload_body: str, signature_header: str, secret: Optional[str] = None) -> bool:
129
+ """
130
+ Verify incoming webhook signature
131
+ """
132
+ sec = secret or self.webhook_secret
133
+ if not sec:
134
+ raise MarkanMError("Webhook secret must be specified to verify signatures")
135
+ handler = WebhookHandler(secret=sec)
136
+ return handler.verify(payload_body, signature_header)
137
+
138
+ async def handle_webhook(self, payload_body: str, signature_header: str, secret: Optional[str] = None) -> Event:
139
+ """
140
+ Verify signature, parse Event, and dispatch to handlers
141
+ """
142
+ sec = secret or self.webhook_secret
143
+ if not sec:
144
+ raise MarkanMError("Webhook secret must be specified to process webhooks")
145
+
146
+ wh_handler = WebhookHandler(secret=sec)
147
+ event = wh_handler.process_event(payload_body, signature_header)
148
+ await self.dispatch_event(event)
149
+ return event
150
+
151
+ def run_polling(self, interval: float = 2.0):
152
+ """
153
+ Run development polling event loop
154
+ """
155
+ print(f"🤖 MarkanM Bot running in development polling mode (interval: {interval}s)...")
156
+ self._is_running = True
157
+ loop = asyncio.get_event_loop()
158
+
159
+ try:
160
+ loop.run_until_complete(self._polling_loop(interval))
161
+ except (KeyboardInterrupt, SystemExit):
162
+ print("\n🛑 Shutting down MarkanM Bot...")
163
+ finally:
164
+ self._is_running = False
165
+
166
+ def run(self, mode: str = "polling", interval: float = 2.0):
167
+ """
168
+ Main entry point (alias for run_polling in alpha)
169
+ """
170
+ if mode == "polling":
171
+ self.run_polling(interval=interval)
172
+ else:
173
+ raise MarkanMError(f"Unsupported run mode '{mode}'. For webhooks, use framework Integration or bot.handle_webhook()")
174
+
175
+ async def _polling_loop(self, interval: float):
176
+ while self._is_running:
177
+ try:
178
+ res = await self.client.post("/polling")
179
+ events_data = res.get("events", [])
180
+ for evt_raw in events_data:
181
+ event = Event(evt_raw)
182
+ await self.dispatch_event(event)
183
+ except MarkanMError as err:
184
+ print(f"⚠️ MarkanM Polling Error: {err.message}")
185
+ except Exception as exc:
186
+ print(f"⚠️ Unexpected Polling Error: {_mask_sensitive(str(exc))}")
187
+
188
+ await asyncio.sleep(interval)
@@ -0,0 +1,128 @@
1
+ import json
2
+ import asyncio
3
+ from typing import Dict, Any, Optional
4
+ from .exceptions import (
5
+ MarkanMAPIError,
6
+ MarkanMAuthError,
7
+ MarkanMPermissionError,
8
+ MarkanMNotFoundError,
9
+ MarkanMRateLimitError,
10
+ MarkanMError
11
+ )
12
+
13
+ try:
14
+ import httpx
15
+ HAS_HTTPX = True
16
+ except ImportError:
17
+ HAS_HTTPX = False
18
+ import urllib.request
19
+ import urllib.error
20
+
21
+ class APIClient:
22
+ """
23
+ Async HTTP Client for MarkanM Bot API v1.
24
+ Uses httpx when installed, with automatic fallback to urllib.request.
25
+ """
26
+ def __init__(self, token: str, base_url: str = "https://chat.markanm.com/api/bot/v1", timeout: float = 10.0):
27
+ self.token = token
28
+ self.base_url = base_url.rstrip("/")
29
+ self.timeout = timeout
30
+
31
+ def _get_headers(self) -> Dict[str, str]:
32
+ return {
33
+ "Authorization": f"Bearer {self.token}",
34
+ "Content-Type": "application/json",
35
+ "User-Agent": "markanm-python-sdk/0.1.0a1"
36
+ }
37
+
38
+ async def request(
39
+ self,
40
+ method: str,
41
+ endpoint: str,
42
+ params: Optional[Dict[str, Any]] = None,
43
+ json_data: Optional[Dict[str, Any]] = None
44
+ ) -> Dict[str, Any]:
45
+ url = f"{self.base_url}/{endpoint.lstrip('/')}"
46
+
47
+ if params:
48
+ from urllib.parse import urlencode
49
+ url += f"?{urlencode(params)}"
50
+
51
+ if HAS_HTTPX:
52
+ async with httpx.AsyncClient(timeout=self.timeout) as client:
53
+ try:
54
+ response = await client.request(
55
+ method=method.upper(),
56
+ url=url,
57
+ headers=self._get_headers(),
58
+ json=json_data
59
+ )
60
+ status_code = response.status_code
61
+ response_text = response.text
62
+ request_id = response.headers.get("X-MarkanM-Request-ID")
63
+ try:
64
+ res_json = response.json()
65
+ except Exception:
66
+ res_json = {}
67
+ except httpx.TimeoutException:
68
+ raise MarkanMError(f"HTTP Request to {url} timed out after {self.timeout}s")
69
+ except httpx.RequestError as exc:
70
+ raise MarkanMError(f"Network error communicating with MarkanM API: {str(exc)}")
71
+ else:
72
+ # Fallback to urllib.request in executor thread
73
+ def _sync_urllib_request():
74
+ data_bytes = json.dumps(json_data).encode("utf-8") if json_data else None
75
+ req = urllib.request.Request(url, data=data_bytes, headers=self._get_headers(), method=method.upper())
76
+ try:
77
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
78
+ body_bytes = resp.read()
79
+ headers_dict = dict(resp.info())
80
+ return resp.status, body_bytes.decode("utf-8"), headers_dict
81
+ except urllib.error.HTTPError as err:
82
+ body_bytes = err.read()
83
+ headers_dict = dict(err.headers)
84
+ return err.code, body_bytes.decode("utf-8"), headers_dict
85
+ except Exception as exc:
86
+ raise MarkanMError(f"Network error: {str(exc)}")
87
+
88
+ status_code, response_text, resp_headers = await asyncio.to_thread(_sync_urllib_request)
89
+ request_id = resp_headers.get("X-MarkanM-Request-ID") or resp_headers.get("x-markanm-request-id")
90
+ try:
91
+ res_json = json.loads(response_text)
92
+ except Exception:
93
+ res_json = {}
94
+
95
+ if 200 <= status_code < 300:
96
+ return res_json
97
+
98
+ # Parse API Error Payload
99
+ error_data = res_json.get("error", {})
100
+ if isinstance(error_data, dict):
101
+ err_code = error_data.get("code", "api_error")
102
+ err_msg = error_data.get("message", response_text or "API Error")
103
+ else:
104
+ err_code = "api_error"
105
+ err_msg = str(error_data) or response_text
106
+
107
+ if status_code == 401:
108
+ raise MarkanMAuthError(status_code, err_code, err_msg, request_id)
109
+ elif status_code == 403:
110
+ raise MarkanMPermissionError(status_code, err_code, err_msg, request_id)
111
+ elif status_code == 404:
112
+ raise MarkanMNotFoundError(status_code, err_code, err_msg, request_id)
113
+ elif status_code == 429:
114
+ raise MarkanMRateLimitError(status_code, err_code, err_msg, 60, request_id)
115
+ else:
116
+ raise MarkanMAPIError(status_code, err_code, err_msg, request_id)
117
+
118
+ async def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
119
+ return await self.request("GET", endpoint, params=params)
120
+
121
+ async def post(self, endpoint: str, json_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
122
+ return await self.request("POST", endpoint, json_data=json_data)
123
+
124
+ async def patch(self, endpoint: str, json_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
125
+ return await self.request("PATCH", endpoint, json_data=json_data)
126
+
127
+ async def delete(self, endpoint: str) -> Dict[str, Any]:
128
+ return await self.request("DELETE", endpoint)
@@ -0,0 +1,54 @@
1
+ from typing import Dict, Any, List
2
+
3
+ class UserContext:
4
+ def __init__(self, data: Dict[str, Any]):
5
+ self.id = data.get("id")
6
+ self.username = data.get("username", "")
7
+ self.display_name = data.get("display_name", "")
8
+ self.avatar_url = data.get("avatar_url", "")
9
+
10
+ class RoomContext:
11
+ def __init__(self, data: Dict[str, Any]):
12
+ self.id = data.get("id")
13
+ self.name = data.get("name", "")
14
+
15
+ class MessageContext:
16
+ def __init__(self, data: Dict[str, Any]):
17
+ self.id = data.get("id")
18
+ self.text = data.get("text", "") or data.get("content", "")
19
+ self.type = data.get("type", "text")
20
+
21
+ class CommandContext:
22
+ """
23
+ Context passed to command handlers: @bot.command("name")
24
+ """
25
+ def __init__(self, bot, payload: Dict[str, Any]):
26
+ self.bot = bot
27
+ self.raw_data = payload or {}
28
+ self.command = payload.get("command", "").lstrip("/")
29
+
30
+ # Parse command arguments
31
+ raw_text = payload.get("text") or payload.get("message", {}).get("text", "")
32
+ self.text = raw_text
33
+ if "args" in payload and isinstance(payload["args"], list):
34
+ self.args = payload["args"]
35
+ else:
36
+ parts = raw_text.split()
37
+ self.args = parts[1:] if len(parts) > 1 else []
38
+
39
+ self.user = UserContext(payload.get("user") or {})
40
+ self.room = RoomContext(payload.get("room") or {"id": payload.get("conversation_id") or payload.get("room_id")})
41
+ self.message = MessageContext(payload.get("message") or {})
42
+
43
+ @property
44
+ def room_id(self):
45
+ return self.room.id
46
+
47
+ async def reply(self, content: str):
48
+ """Reply to room context"""
49
+ if not self.room_id:
50
+ raise ValueError("Cannot reply: missing room ID in context")
51
+ return await self.bot.send_message(self.room_id, content)
52
+
53
+ def __repr__(self) -> str:
54
+ return f"<CommandContext command='{self.command}' args={self.args} room_id={self.room_id}>"
@@ -0,0 +1,30 @@
1
+ from typing import Dict, Any, Optional
2
+
3
+ class Event:
4
+ """
5
+ MarkanM Platform Event Model
6
+ """
7
+ def __init__(self, data: Dict[str, Any]):
8
+ self.raw_data = data or {}
9
+ self.id = data.get("event_id") or data.get("id", "")
10
+ self.type = data.get("event_type") or data.get("event", "")
11
+ self.timestamp = data.get("timestamp") or data.get("created_at", "")
12
+ self.payload = data.get("payload") if "payload" in data else data
13
+
14
+ @property
15
+ def event_id(self) -> str:
16
+ """Alias for backward compatibility"""
17
+ return self.id
18
+
19
+ @property
20
+ def event_type(self) -> str:
21
+ """Alias for backward compatibility"""
22
+ return self.type
23
+
24
+ @property
25
+ def created_at(self) -> str:
26
+ """Alias for timestamp"""
27
+ return str(self.timestamp)
28
+
29
+ def __repr__(self) -> str:
30
+ return f"<MarkanM Event type='{self.type}' id='{self.id}'>"
@@ -0,0 +1,53 @@
1
+ import re
2
+
3
+ def _mask_sensitive(text: str) -> str:
4
+ """Mask sensitive tokens or keys in strings"""
5
+ if not isinstance(text, str):
6
+ return str(text)
7
+ text = re.sub(r'mkbot_[a-zA-Z0-9_-]+', 'mkbot_***MASKED***', text)
8
+ text = re.sub(r'mkm_sec_[a-zA-Z0-9_-]+', 'mkm_sec_***MASKED***', text)
9
+ text = re.sub(r'whsec_[a-zA-Z0-9_-]+', 'whsec_***MASKED***', text)
10
+ return text
11
+
12
+ class MarkanMError(Exception):
13
+ """Base exception class for all MarkanM SDK errors"""
14
+ def __init__(self, message: str = "An error occurred with MarkanM SDK"):
15
+ self.message = _mask_sensitive(message)
16
+ super().__init__(self.message)
17
+
18
+ class MarkanMAPIError(MarkanMError):
19
+ """Raised when MarkanM Bot API returns an HTTP error status"""
20
+ def __init__(self, status_code: int, error_code: str, message: str, request_id: str = None):
21
+ self.status_code = status_code
22
+ self.error_code = error_code
23
+ self.request_id = request_id
24
+ msg = f"HTTP {status_code} [{error_code}]: {message}"
25
+ if request_id:
26
+ msg += f" (Request-ID: {request_id})"
27
+ super().__init__(msg)
28
+
29
+ class MarkanMAuthError(MarkanMAPIError):
30
+ """Raised on HTTP 401 Unauthorized errors"""
31
+ pass
32
+
33
+ class MarkanMPermissionError(MarkanMAPIError):
34
+ """Raised on HTTP 403 Forbidden scope errors"""
35
+ pass
36
+
37
+ class MarkanMNotFoundError(MarkanMAPIError):
38
+ """Raised on HTTP 404 Not Found errors"""
39
+ pass
40
+
41
+ class MarkanMRateLimitError(MarkanMAPIError):
42
+ """Raised on HTTP 429 Rate Limit Exceeded errors"""
43
+ def __init__(self, status_code: int, error_code: str, message: str, retry_after: int = 60, request_id: str = None):
44
+ self.retry_after = retry_after
45
+ super().__init__(status_code, error_code, f"{message} (Retry after {retry_after}s)", request_id)
46
+
47
+ class MarkanMWebhookError(MarkanMError):
48
+ """Raised on HMAC signature or timestamp verification failure"""
49
+ pass
50
+
51
+ class MarkanMValidationError(MarkanMError):
52
+ """Raised on SDK validation failure"""
53
+ pass
@@ -0,0 +1,78 @@
1
+ import hmac
2
+ import hashlib
3
+ import time
4
+ import json
5
+ from typing import Dict, Any, Tuple
6
+ from .events import Event
7
+ from .exceptions import MarkanMWebhookError
8
+
9
+ class WebhookHandler:
10
+ """
11
+ HMAC-SHA256 Webhook Verification & Parsing Helper
12
+ """
13
+ def __init__(self, secret: str, timestamp_tolerance: int = 300):
14
+ self.secret = secret
15
+ self.timestamp_tolerance = timestamp_tolerance
16
+
17
+ def parse_header(self, signature_header: str) -> Tuple[int, str]:
18
+ """
19
+ Parse X-MarkanM-Signature header: "t=1788000000,v1=abcdef..."
20
+ """
21
+ if not signature_header or not isinstance(signature_header, str):
22
+ raise MarkanMWebhookError("Missing or invalid X-MarkanM-Signature header")
23
+
24
+ t_val = None
25
+ v1_val = None
26
+
27
+ for part in signature_header.split(","):
28
+ part = part.strip()
29
+ if "=" in part:
30
+ k, v = part.split("=", 1)
31
+ if k.strip() == "t":
32
+ try:
33
+ t_val = int(v.strip())
34
+ except ValueError:
35
+ raise MarkanMWebhookError("Invalid timestamp in signature header")
36
+ elif k.strip() == "v1":
37
+ v1_val = v.strip()
38
+
39
+ if t_val is None or not v1_val:
40
+ raise MarkanMWebhookError("Malformed X-MarkanM-Signature header. Expected format: 't=TIMESTAMP,v1=SIGNATURE'")
41
+
42
+ return t_val, v1_val
43
+
44
+ def verify(self, payload_body: str, signature_header: str) -> bool:
45
+ """
46
+ Verify HMAC-SHA256 signature and timestamp freshness
47
+ """
48
+ t_val, expected_sig = self.parse_header(signature_header)
49
+
50
+ # Check timestamp tolerance for replay protection
51
+ current_time = int(time.time())
52
+ if abs(current_time - t_val) > self.timestamp_tolerance:
53
+ raise MarkanMWebhookError(f"Webhook timestamp expired or out of tolerance window ({abs(current_time - t_val)}s > {self.timestamp_tolerance}s)")
54
+
55
+ # Compute HMAC signature
56
+ signed_payload = f"{t_val}.{payload_body}".encode("utf-8")
57
+ computed_sig = hmac.new(
58
+ self.secret.encode("utf-8"),
59
+ signed_payload,
60
+ hashlib.sha256
61
+ ).hexdigest()
62
+
63
+ if not hmac.compare_digest(computed_sig, expected_sig):
64
+ raise MarkanMWebhookError("Invalid HMAC-SHA256 webhook signature")
65
+
66
+ return True
67
+
68
+ def process_event(self, payload_body: str, signature_header: str) -> Event:
69
+ """
70
+ Verify signature and parse into typed Event model
71
+ """
72
+ self.verify(payload_body, signature_header)
73
+ try:
74
+ data = json.loads(payload_body)
75
+ except json.JSONDecodeError as exc:
76
+ raise MarkanMWebhookError(f"Invalid JSON payload in webhook body: {str(exc)}")
77
+
78
+ return Event(data)
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: markanm
3
+ Version: 0.1.0a1
4
+ Summary: Official Python SDK for MarkanM Chat Bot Platform & REST API (v0.1.0-alpha)
5
+ Author-email: MarkanM Developer Team <developers@markanm.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 MarkanM Developer Team
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://chat.markanm.com/developers
29
+ Project-URL: Documentation, https://chat.markanm.com/developers/docs
30
+ Project-URL: Repository, https://github.com/markanm/markanm-python-sdk
31
+ Keywords: markanm,bot,chat,sdk,api,oauth,webhooks
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.8
37
+ Classifier: Programming Language :: Python :: 3.9
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Topic :: Communications :: Chat
42
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
43
+ Requires-Python: >=3.8
44
+ Description-Content-Type: text/markdown
45
+ License-File: LICENSE
46
+ Requires-Dist: httpx>=0.24.0
47
+ Dynamic: license-file
48
+
49
+ # MarkanM Official Python SDK (`markanm 0.1.0a1`)
50
+
51
+ Official Python library (v0.1.0-alpha) for building developer bots, automation, and AI agents on **MarkanM Chat** (`https://chat.markanm.com`).
52
+
53
+ ---
54
+
55
+ ## ⚠️ Alpha Release Notice
56
+
57
+ This is **v0.1.0-alpha** (`markanm 0.1.0a1`). It is intended for testing against the MarkanM Bot API v1.
58
+
59
+ ---
60
+
61
+ ## 🚀 Installation
62
+
63
+ Install via `pip`:
64
+
65
+ ```bash
66
+ pip install markanm
67
+ ```
68
+
69
+ ---
70
+
71
+ ## ⚡ Quick Start
72
+
73
+ ### 1. Simple Hello Bot (`hello_bot.py`)
74
+
75
+ ```python
76
+ import os
77
+ from markanm import Bot
78
+
79
+ bot = Bot(os.environ["MARKANM_BOT_TOKEN"])
80
+
81
+ @bot.command("hello")
82
+ async def hello(ctx):
83
+ await ctx.reply("Hello 👋 Welcome to MarkanM!")
84
+
85
+ @bot.command("start")
86
+ async def start(ctx):
87
+ await ctx.reply("🚀 Bot activated and ready!")
88
+
89
+ if __name__ == "__main__":
90
+ bot.run_polling()
91
+ ```
92
+
93
+ ---
94
+
95
+ ### 2. Event Listeners
96
+
97
+ ```python
98
+ @bot.on("message.created")
99
+ async def on_message(event):
100
+ print(f"New message received: {event.payload}")
101
+
102
+ @bot.on("member.joined")
103
+ def on_member_joined(event):
104
+ print(f"User joined room: {event.payload.get('user')}")
105
+ ```
106
+
107
+ ---
108
+
109
+ ### 3. AI Assistant Bot (`ai_bot.py`)
110
+
111
+ ```python
112
+ import os
113
+ from markanm import Bot
114
+ from markanm.ai import AI
115
+
116
+ bot = Bot(os.environ["MARKANM_BOT_TOKEN"])
117
+ ai = AI(provider="openai", api_key=os.environ["OPENAI_API_KEY"])
118
+
119
+ @bot.command("ask")
120
+ async def ask(ctx):
121
+ prompt = " ".join(ctx.args)
122
+ if not prompt:
123
+ await ctx.reply("Usage: /ask <question>")
124
+ return
125
+
126
+ answer = await ai.generate(prompt)
127
+ await ctx.reply(answer)
128
+
129
+ bot.run_polling()
130
+ ```
131
+
132
+ ---
133
+
134
+ ### 4. Webhook Verification (Flask / FastAPI)
135
+
136
+ ```python
137
+ from markanm import WebhookHandler, MarkanMWebhookError
138
+
139
+ handler = WebhookHandler(secret="whsec_YOUR_SECRET", timestamp_tolerance=300)
140
+
141
+ try:
142
+ # Verifies HMAC-SHA256 signature header and 300s timestamp tolerance
143
+ event = handler.process_event(request_body_text, request_headers.get("X-MarkanM-Signature"))
144
+ print(f"Event verified: {event.id} ({event.type})")
145
+ except MarkanMWebhookError as err:
146
+ print(f"Webhook error: {err}")
147
+ ```
148
+
149
+ ---
150
+
151
+ ## 🧪 Testing
152
+
153
+ Run tests with `pytest`:
154
+
155
+ ```bash
156
+ pytest sdk/python/tests/
157
+ ```
158
+
159
+ ---
160
+
161
+ ## 📚 API Reference
162
+ For complete API endpoints and permission scope guidelines, visit:
163
+ **https://chat.markanm.com/developers/docs**
@@ -0,0 +1,22 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ setup.py
5
+ src/markanm/__init__.py
6
+ src/markanm/ai.py
7
+ src/markanm/bot.py
8
+ src/markanm/client.py
9
+ src/markanm/context.py
10
+ src/markanm/events.py
11
+ src/markanm/exceptions.py
12
+ src/markanm/webhook.py
13
+ src/markanm.egg-info/PKG-INFO
14
+ src/markanm.egg-info/SOURCES.txt
15
+ src/markanm.egg-info/dependency_links.txt
16
+ src/markanm.egg-info/requires.txt
17
+ src/markanm.egg-info/top_level.txt
18
+ tests/test_ai.py
19
+ tests/test_bot.py
20
+ tests/test_context.py
21
+ tests/test_events.py
22
+ tests/test_webhook.py
@@ -0,0 +1 @@
1
+ httpx>=0.24.0
@@ -0,0 +1 @@
1
+ markanm
@@ -0,0 +1,14 @@
1
+ import asyncio
2
+ from markanm.ai import AI
3
+ from markanm.exceptions import MarkanMError
4
+
5
+ async def test_unsupported_ai_provider():
6
+ ai = AI(provider="unsupported_provider", api_key="test_key")
7
+ caught = False
8
+ try:
9
+ await ai.generate("Hello")
10
+ except MarkanMError as exc:
11
+ caught = True
12
+ assert "Unsupported AI provider" in str(exc)
13
+
14
+ assert caught is True, "Expected MarkanMError for unsupported AI provider"
@@ -0,0 +1,39 @@
1
+ import asyncio
2
+ from markanm.bot import Bot
3
+ from markanm.events import Event
4
+
5
+ async def test_bot_command_and_event_dispatching():
6
+ bot = Bot("mkbot_test_token_123456789")
7
+
8
+ executed_command = False
9
+ executed_listener = False
10
+
11
+ @bot.command("start")
12
+ async def handle_start(ctx):
13
+ nonlocal executed_command
14
+ executed_command = True
15
+ assert ctx.command == "start"
16
+
17
+ @bot.on("message.created")
18
+ def handle_msg(evt):
19
+ nonlocal executed_listener
20
+ executed_listener = True
21
+ assert evt.type == "message.created"
22
+
23
+ # Dispatch command event
24
+ cmd_event = Event({
25
+ "event_id": "evt_cmd",
26
+ "event_type": "command.received",
27
+ "payload": {"command": "/start", "user": {"username": "alex"}, "room": {"id": 10}}
28
+ })
29
+ await bot.dispatch_event(cmd_event)
30
+ assert executed_command is True
31
+
32
+ # Dispatch message event
33
+ msg_event = Event({
34
+ "event_id": "evt_msg",
35
+ "event_type": "message.created",
36
+ "payload": {"text": "hello"}
37
+ })
38
+ await bot.dispatch_event(msg_event)
39
+ assert executed_listener is True
@@ -0,0 +1,15 @@
1
+ from markanm.context import CommandContext
2
+
3
+ def test_command_context_parsing():
4
+ payload = {
5
+ "command": "/ask",
6
+ "text": "/ask What is Python?",
7
+ "user": {"id": 1, "username": "alex", "display_name": "Alex"},
8
+ "room": {"id": 42, "name": "General Room"}
9
+ }
10
+ ctx = CommandContext(None, payload)
11
+ assert ctx.command == "ask"
12
+ assert ctx.args == ["What", "is", "Python?"]
13
+ assert ctx.user.username == "alex"
14
+ assert ctx.room.id == 42
15
+ assert ctx.room_id == 42
@@ -0,0 +1,16 @@
1
+ from markanm.events import Event
2
+
3
+ def test_event_properties():
4
+ raw_data = {
5
+ "event_id": "evt_12345",
6
+ "event_type": "message.created",
7
+ "timestamp": 1788000000,
8
+ "payload": {"text": "hello"}
9
+ }
10
+ evt = Event(raw_data)
11
+ assert evt.id == "evt_12345"
12
+ assert evt.event_id == "evt_12345"
13
+ assert evt.type == "message.created"
14
+ assert evt.event_type == "message.created"
15
+ assert evt.payload == {"text": "hello"}
16
+ assert repr(evt) == "<MarkanM Event type='message.created' id='evt_12345'>"
@@ -0,0 +1,42 @@
1
+ import hmac
2
+ import hashlib
3
+ import time
4
+ from markanm.webhook import WebhookHandler
5
+ from markanm.exceptions import MarkanMWebhookError
6
+
7
+ def test_webhook_verification():
8
+ secret = "whsec_test_secret_12345"
9
+ handler = WebhookHandler(secret=secret, timestamp_tolerance=300)
10
+
11
+ timestamp = int(time.time())
12
+ body = '{"event_id":"evt_1","event_type":"message.created"}'
13
+ signed_payload = f"{timestamp}.{body}".encode("utf-8")
14
+ sig = hmac.new(secret.encode("utf-8"), signed_payload, hashlib.sha256).hexdigest()
15
+ header = f"t={timestamp},v1={sig}"
16
+
17
+ # Valid signature check
18
+ assert handler.verify(body, header) is True
19
+
20
+ # Process event check
21
+ event = handler.process_event(body, header)
22
+ assert event.id == "evt_1"
23
+ assert event.type == "message.created"
24
+
25
+ def test_expired_webhook():
26
+ secret = "whsec_test_secret_12345"
27
+ handler = WebhookHandler(secret=secret, timestamp_tolerance=10)
28
+
29
+ expired_timestamp = int(time.time()) - 500 # 500 seconds old
30
+ body = '{"event_id":"evt_1"}'
31
+ signed_payload = f"{expired_timestamp}.{body}".encode("utf-8")
32
+ sig = hmac.new(secret.encode("utf-8"), signed_payload, hashlib.sha256).hexdigest()
33
+ header = f"t={expired_timestamp},v1={sig}"
34
+
35
+ caught = False
36
+ try:
37
+ handler.verify(body, header)
38
+ except MarkanMWebhookError as exc:
39
+ caught = True
40
+ assert "timestamp expired" in str(exc)
41
+
42
+ assert caught is True, "Expected MarkanMWebhookError for expired timestamp"