Skip to content

Commit dd838cc

Browse files
committed
refactor all routers; add db, identity middlewares
1 parent fef1431 commit dd838cc

File tree

11 files changed

+1715
-1473
lines changed

11 files changed

+1715
-1473
lines changed

CLAUDE.md

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,243 @@ Multi-user Telegram bot that manages per-user Letta agents through an identity-b
5656
- Each `.edgeql` file generates a corresponding Python module with type-safe async functions
5757
- Examples: `register_user`, `create_auth_request`, `get_identity`, `set_selected_agent`
5858

59+
### Middleware System
60+
61+
The bot uses **Aiogram's middleware system** for dependency injection and access control. Middlewares are called before handlers and can inject data, perform checks, or block handler execution.
62+
63+
**DBMiddleware** (`middlewares.py`):
64+
65+
- Injects `gel_client` (Gel database client) into handler data for all Message and CallbackQuery events
66+
- Registered as outer middleware for both message and callback query routers
67+
- Provides database access to all handlers without manual client passing
68+
69+
**IdentityMiddleware** (`middlewares.py`):
70+
71+
- Checks if handler requires identity verification by inspecting `require_identity` flag
72+
- Only processes handlers marked with `flags={'require_identity': True}`
73+
- Performs identity authorization check using `get_allowed_identity_query()`
74+
- If authorization succeeds:
75+
- Fetches user's identity using `get_identity_query()`
76+
- Injects `identity` object into handler data
77+
- Allows handler execution
78+
- If authorization fails:
79+
- Sends error message to user: "You need to request bot access first using /botaccess"
80+
- Blocks handler execution (returns None)
81+
82+
**Marking Handlers that Require Identity**:
83+
84+
Handlers that need identity access should use `flags={'require_identity': True}` parameter:
85+
86+
```python
87+
# Command handler requiring identity
88+
@router.message(Command('switch'), flags={'require_identity': True})
89+
async def switch(message: Message, identity: GetIdentityResult) -> None:
90+
# Handler receives identity as injected parameter
91+
pass
92+
93+
# Callback query handler requiring identity
94+
@router.callback_query(
95+
SwitchAssistantCallback.filter(),
96+
flags={'require_identity': True}
97+
)
98+
async def handle_switch(
99+
callback: CallbackQuery,
100+
callback_data: SwitchAssistantCallback,
101+
identity: GetIdentityResult,
102+
) -> None:
103+
# Handler receives identity as injected parameter
104+
pass
105+
106+
# Message handler (catch-all) requiring identity
107+
@router.message(flags={'require_identity': True})
108+
async def message_handler(
109+
message: Message,
110+
identity: GetIdentityResult
111+
) -> None:
112+
# Handler receives identity as injected parameter
113+
pass
114+
```
115+
116+
**Key Points**:
117+
118+
- Handlers with `require_identity` flag automatically receive `identity: GetIdentityResult` parameter
119+
- No need to manually check identity or pass `gel_client` - middleware handles this
120+
- Middleware setup via `setup_middlewares(dp, gel_client)` in `main.py`
121+
- Replaces legacy `@require_identity(gel_client)` decorator pattern
122+
123+
**Creating Custom Middlewares**:
124+
125+
Aiogram middlewares inherit from `BaseMiddleware` and implement `__call__` method:
126+
127+
```python
128+
from aiogram import BaseMiddleware
129+
from aiogram.types import TelegramObject
130+
from collections.abc import Awaitable, Callable
131+
132+
class CustomMiddleware(BaseMiddleware):
133+
async def __call__(
134+
self,
135+
handler: Callable[[TelegramObject, dict[str, object]], Awaitable[object]],
136+
event: TelegramObject,
137+
data: dict[str, object],
138+
) -> object | None:
139+
# Pre-processing: inject data, perform checks
140+
data['custom_key'] = 'custom_value'
141+
142+
# Optional: check conditions and block handler
143+
if some_condition:
144+
await event.answer('Access denied')
145+
return None # Block handler execution
146+
147+
# Call handler
148+
result = await handler(event, data)
149+
150+
# Post-processing (optional)
151+
# ... do something after handler
152+
153+
return result
154+
```
155+
156+
**Middleware Types**:
157+
158+
- **Outer middleware**: Runs before inner middleware and filters
159+
- Use: `dp.message.outer_middleware.register(CustomMiddleware())`
160+
- Example: `DBMiddleware` (injects database client)
161+
- **Inner middleware**: Runs after outer middleware but before handlers
162+
- Use: `dp.message.middleware(CustomMiddleware())`
163+
- Example: `IdentityMiddleware` (checks identity access)
164+
165+
**Registration Order Matters**:
166+
167+
```python
168+
# DBMiddleware must run first (outer) to inject gel_client
169+
dp.message.outer_middleware.register(DBMiddleware(gel_client))
170+
dp.callback_query.outer_middleware.register(DBMiddleware(gel_client))
171+
172+
# IdentityMiddleware runs second (inner) and uses gel_client
173+
dp.message.middleware(IdentityMiddleware())
174+
dp.callback_query.middleware(IdentityMiddleware())
175+
```
176+
177+
### Filters
178+
179+
The bot uses **Aiogram's filter system** for access control. Filters determine whether a handler should execute based on specific conditions. Unlike middleware, filters are declarative and specific to individual handlers.
180+
181+
**AdminOnlyFilter** (`filters.py`):
182+
183+
- Magic filter that restricts handler execution to admin users only
184+
- Implementation: `MagicData(F.event_from_user.id.in_(CONFIG.admin_ids))`
185+
- Checks if `event.from_user.id` is in the list of configured admin IDs
186+
- If `CONFIG.admin_ids` is `None`, filter always fails (no admins configured)
187+
188+
**Usage Pattern**:
189+
190+
```python
191+
from letta_bot.filters import AdminOnlyFilter
192+
193+
# Apply filter to command handler
194+
@router.message(Command('pending'), AdminOnlyFilter)
195+
async def pending(message: Message, gel_client: AsyncIOExecutor) -> None:
196+
# Only admins can access this handler
197+
pass
198+
```
199+
200+
**Admin Commands Using This Filter**:
201+
202+
- `/pending` - View pending authorization requests
203+
- `/allow <request_uuid>` - Approve authorization request
204+
- `/deny <request_uuid> [reason]` - Deny authorization request with optional reason
205+
- `/revoke <telegram_id>` - Revoke user's identity access
206+
- `/users` - List all users with their allowed resources
207+
208+
**Key Points**:
209+
210+
- Filter automatically blocks non-admin users from accessing admin commands
211+
- No error message sent to non-admin users (handler simply doesn't execute)
212+
- Admin IDs must be configured via `ADMIN_IDS` environment variable (comma-separated list)
213+
- If no admin IDs configured, admin commands are inaccessible to everyone
214+
215+
**Creating Custom Filters**:
216+
217+
Aiogram supports multiple filter types:
218+
219+
**1. Magic Filters (Recommended for simple conditions)**:
220+
221+
Using Aiogram's `F` (magic filter) for concise, readable conditions:
222+
223+
```python
224+
from aiogram import F
225+
from aiogram.filters.magic_data import MagicData
226+
227+
# Check user ID
228+
UserIsJohn = MagicData(F.event_from_user.id == 12345)
229+
230+
# Check multiple IDs
231+
PremiumUsersFilter = MagicData(F.event_from_user.id.in_([123, 456, 789]))
232+
233+
# Check message text pattern
234+
HasKeywordFilter = MagicData(F.message.text.contains('keyword'))
235+
236+
# Combine conditions with & (AND) and | (OR)
237+
SpecialFilter = MagicData(
238+
(F.event_from_user.id == 123) & (F.message.text.startswith('/'))
239+
)
240+
```
241+
242+
**2. Custom Filter Classes (For complex logic)**:
243+
244+
```python
245+
from aiogram.filters import Filter
246+
from aiogram.types import Message
247+
248+
class HasAttachmentFilter(Filter):
249+
async def __call__(self, message: Message) -> bool:
250+
# Return True if handler should execute
251+
return bool(
252+
message.photo
253+
or message.document
254+
or message.video
255+
or message.audio
256+
)
257+
258+
# Usage
259+
@router.message(HasAttachmentFilter())
260+
async def handle_attachment(message: Message) -> None:
261+
pass
262+
```
263+
264+
**3. Filter with Data Injection**:
265+
266+
Filters can inject data into handlers by returning a dict:
267+
268+
```python
269+
from aiogram.filters import Filter
270+
from aiogram.types import Message
271+
272+
class ExtractMentionFilter(Filter):
273+
async def __call__(self, message: Message) -> bool | dict[str, object]:
274+
if not message.entities:
275+
return False
276+
277+
for entity in message.entities:
278+
if entity.type == 'mention':
279+
mention = message.text[entity.offset:entity.offset + entity.length]
280+
return {'mentioned_user': mention}
281+
282+
return False
283+
284+
# Handler receives injected data
285+
@router.message(ExtractMentionFilter())
286+
async def handle_mention(message: Message, mentioned_user: str) -> None:
287+
await message.answer(f'You mentioned: {mentioned_user}')
288+
```
289+
290+
**Filter vs Middleware Decision**:
291+
292+
- **Use Filter when**: Condition is specific to a handler (e.g., admin-only command)
293+
- **Use Middleware when**: Logic applies to many handlers (e.g., database injection, identity checks)
294+
- **Combine both**: Middleware for common setup, filters for specific conditions
295+
59296
### Authorization Flow
60297

61298
**Phase 1: User Registration**
@@ -257,6 +494,8 @@ Current module organization:
257494
letta_bot/
258495
main.py # Bot entry point with webhook/polling modes, /start handler
259496
config.py # Configuration management (Pydantic settings)
497+
middlewares.py # Middleware for database client injection and identity checks
498+
filters.py # Filters for admin access control
260499
auth.py # Admin authorization handlers (/pending, /allow, /deny, /users, /revoke)
261500
agent.py # Agent request handlers, message routing, and Letta API integration
262501
client.py # Shared Letta client instance and Letta API operations

letta_bot/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Letta Telegram Bot - Multi-user bot with identity-based agent isolation."""
22

3-
__version__ = '0.1.0'
3+
__version__ = '1.0.0'

0 commit comments

Comments
 (0)