onbot 0.1.0.dev0__py3-none-any.whl
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.
- onbot/__init__.py +7 -0
- onbot/__main__.py +6 -0
- onbot/app.py +171 -0
- onbot/auth/__init__.py +19 -0
- onbot/auth/token_provider.py +153 -0
- onbot/cli.py +80 -0
- onbot/clients/__init__.py +5 -0
- onbot/clients/authentik.py +123 -0
- onbot/clients/base.py +243 -0
- onbot/clients/mas_admin.py +65 -0
- onbot/clients/matrix.py +457 -0
- onbot/clients/synapse_admin.py +159 -0
- onbot/clients/versions.py +61 -0
- onbot/config.py +653 -0
- onbot/events.py +51 -0
- onbot/healthcheck.py +134 -0
- onbot/identity.py +44 -0
- onbot/lifecycle/__init__.py +2 -0
- onbot/lifecycle/accounts.py +309 -0
- onbot/logging.py +26 -0
- onbot/media.py +46 -0
- onbot/models.py +74 -0
- onbot/onboarding/__init__.py +5 -0
- onbot/onboarding/listener.py +103 -0
- onbot/onboarding/welcome.py +116 -0
- onbot/reconciler/__init__.py +2 -0
- onbot/reconciler/effectors.py +78 -0
- onbot/reconciler/engine.py +348 -0
- onbot/reconciler/membership.py +54 -0
- onbot/reconciler/power_levels.py +96 -0
- onbot/reconciler/rooms.py +127 -0
- onbot/reconciler/state.py +77 -0
- onbot/utils.py +61 -0
- onbot-0.1.0.dev0.dist-info/METADATA +241 -0
- onbot-0.1.0.dev0.dist-info/RECORD +38 -0
- onbot-0.1.0.dev0.dist-info/WHEEL +4 -0
- onbot-0.1.0.dev0.dist-info/entry_points.txt +5 -0
- onbot-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
onbot/config.py
ADDED
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
"""Configuration model (pydantic-settings).
|
|
2
|
+
|
|
3
|
+
Ported and refined from the legacy ``onbot/config.py`` (AD-1, reuse-the-config decision).
|
|
4
|
+
Changes from legacy:
|
|
5
|
+
|
|
6
|
+
* pydantic v2 / ``pydantic-settings`` v2 (``SettingsConfigDict``) and modern ``X | None`` typing.
|
|
7
|
+
* Removed the duplicated top-level ``DeactivateDisabledAuthentikUsersInMatrix`` block — the
|
|
8
|
+
lifecycle settings live solely under :class:`SyncAuthentikUsersWithMatrix` where they are read.
|
|
9
|
+
* Fixed legacy type/default bugs (``sync_only_users_of_groups_with_id`` defaulted to ``None`` on a
|
|
10
|
+
non-optional ``list``; ``only_groups_with_attributes`` / ``only_for_groupnames_starting_with``
|
|
11
|
+
carried list defaults on non-list fields).
|
|
12
|
+
* No ``matrix-nio`` references — the Matrix client library is a Phase 6 decision (AD, BATTLE_PLAN §5).
|
|
13
|
+
* Phase 8: dropped the vestigial ``storage_dir`` / ``storage_encryption_key`` fields — they backed
|
|
14
|
+
the libolm key store, which ADR-0009 removed (the bot operates outside encrypted rooms and keeps
|
|
15
|
+
no on-disk crypto state). They were unused everywhere in the codebase; the lifecycle ledger lives
|
|
16
|
+
in Matrix account data (ADR-0001, no DB). Every field carries a description + example so the
|
|
17
|
+
psyplus-generated config reference and YAML template are self-documenting (BATTLE_PLAN §5 Phase 8).
|
|
18
|
+
|
|
19
|
+
Loading: :func:`load_config` reads the YAML at ``ONBOT_CONFIG_FILE_PATH`` (env overrides still apply
|
|
20
|
+
via the ``ONBOT_`` prefix and ``__`` nesting delimiter); :func:`generate_example_config` dumps the
|
|
21
|
+
default model to YAML (G11.2).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import inspect
|
|
27
|
+
import os
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Annotated, Any, Literal
|
|
30
|
+
|
|
31
|
+
import yaml
|
|
32
|
+
from pydantic import BaseModel, Field
|
|
33
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
34
|
+
|
|
35
|
+
CONFIG_FILE_ENV_VAR = "ONBOT_CONFIG_FILE_PATH"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MatrixOAuth2(BaseModel):
|
|
39
|
+
"""OAuth2 client-credentials auth against MAS (AD-6, Phase 6).
|
|
40
|
+
|
|
41
|
+
The forward-looking alternative to a static compatibility token: the bot is a confidential
|
|
42
|
+
OAuth2 client of MAS and mints short-lived access tokens that refresh transparently. When this
|
|
43
|
+
block is set it takes precedence over ``bot_access_token``.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
token_endpoint: Annotated[
|
|
47
|
+
str,
|
|
48
|
+
Field(
|
|
49
|
+
description="MAS OAuth2 token endpoint (the ``token_endpoint`` from MAS discovery).",
|
|
50
|
+
examples=["https://auth.company.org/oauth2/token"],
|
|
51
|
+
),
|
|
52
|
+
]
|
|
53
|
+
client_id: Annotated[
|
|
54
|
+
str,
|
|
55
|
+
Field(description="OAuth2 client id registered for the bot in MAS."),
|
|
56
|
+
]
|
|
57
|
+
client_secret: Annotated[
|
|
58
|
+
str,
|
|
59
|
+
Field(description="OAuth2 client secret for the bot client. Provide the bare secret."),
|
|
60
|
+
]
|
|
61
|
+
scope: Annotated[
|
|
62
|
+
str | None,
|
|
63
|
+
Field(
|
|
64
|
+
description="Optional space-separated scopes to request (e.g. the Synapse admin scope).",
|
|
65
|
+
examples=["urn:matrix:org.matrix.msc2967.client:api:* urn:synapse:admin:*"],
|
|
66
|
+
),
|
|
67
|
+
] = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class MasAdmin(BaseModel):
|
|
71
|
+
"""MAS admin API credentials for lifecycle enforcement (ADR-0005/0006, §7 Q1).
|
|
72
|
+
|
|
73
|
+
Under MAS the Matrix token is owned by MAS, so the Synapse admin API cannot revoke a live
|
|
74
|
+
session — only MAS can (lock/deactivate). When this block is set, the lifecycle module enforces
|
|
75
|
+
lockout through the MAS admin API using an OAuth2 ``client_credentials`` token with the
|
|
76
|
+
``urn:mas:admin`` scope (the client must be listed in MAS ``policy.data.admin_clients``). Leave
|
|
77
|
+
unset to fall back to the Synapse-admin effectors (which do NOT revoke MAS sessions).
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
url: Annotated[
|
|
81
|
+
str,
|
|
82
|
+
Field(
|
|
83
|
+
description="Base URL of the Matrix Authentication Service.",
|
|
84
|
+
examples=["https://auth.company.org"],
|
|
85
|
+
),
|
|
86
|
+
]
|
|
87
|
+
client_id: Annotated[
|
|
88
|
+
str,
|
|
89
|
+
Field(description="OAuth2 client id of the bot's MAS admin client (client_credentials)."),
|
|
90
|
+
]
|
|
91
|
+
client_secret: Annotated[
|
|
92
|
+
str,
|
|
93
|
+
Field(description="OAuth2 client secret of the bot's MAS admin client."),
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class SynapseServer(BaseModel):
|
|
98
|
+
server_name: Annotated[
|
|
99
|
+
str,
|
|
100
|
+
Field(
|
|
101
|
+
description=inspect.cleandoc(
|
|
102
|
+
"""Synapse's public facing domain
|
|
103
|
+
https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html#server_name
|
|
104
|
+
This is not necessarily the domain under which the Synapse server is reachable."""
|
|
105
|
+
),
|
|
106
|
+
examples=["company.org"],
|
|
107
|
+
),
|
|
108
|
+
]
|
|
109
|
+
server_url: Annotated[
|
|
110
|
+
str,
|
|
111
|
+
Field(
|
|
112
|
+
description=inspect.cleandoc(
|
|
113
|
+
"""URL to reach the Synapse server. This can (and should) be an internal URL, so the
|
|
114
|
+
Synapse admin API need not be public. The bot works with a public URL too."""
|
|
115
|
+
),
|
|
116
|
+
examples=["https://internal.matrix"],
|
|
117
|
+
),
|
|
118
|
+
]
|
|
119
|
+
bot_user_id: Annotated[
|
|
120
|
+
str,
|
|
121
|
+
Field(
|
|
122
|
+
description="Full Matrix user ID of an existing account; the bot acts as this user.",
|
|
123
|
+
examples=["@welcome-bot:company.org"],
|
|
124
|
+
),
|
|
125
|
+
]
|
|
126
|
+
bot_access_token: Annotated[
|
|
127
|
+
str | None,
|
|
128
|
+
Field(
|
|
129
|
+
description=inspect.cleandoc(
|
|
130
|
+
"""Access token authorising the bot against the Synapse APIs. Under MAS this is a
|
|
131
|
+
compatibility token issued via ``mas-cli manage issue-compatibility-token`` (AD-6).
|
|
132
|
+
Provide the bare token; do not prefix it with ``Bearer`` (the client adds that).
|
|
133
|
+
Leave unset (``null``) when using ``oauth2`` instead."""
|
|
134
|
+
),
|
|
135
|
+
examples=["syt_ONLY_AN_EXAMPLE_TOKEN_sadaw4"],
|
|
136
|
+
),
|
|
137
|
+
] = None
|
|
138
|
+
oauth2: Annotated[
|
|
139
|
+
MatrixOAuth2 | None,
|
|
140
|
+
Field(
|
|
141
|
+
description=inspect.cleandoc(
|
|
142
|
+
"""Optional OAuth2 client-credentials auth against MAS (AD-6). When set, it is used
|
|
143
|
+
instead of ``bot_access_token`` and tokens refresh automatically. Provide exactly one
|
|
144
|
+
of ``bot_access_token`` or ``oauth2``."""
|
|
145
|
+
),
|
|
146
|
+
),
|
|
147
|
+
] = None
|
|
148
|
+
bot_avatar_url: Annotated[
|
|
149
|
+
str | None,
|
|
150
|
+
Field(
|
|
151
|
+
description="HTTP URL to a picture; the bot sets it as its own avatar on start.",
|
|
152
|
+
examples=["https://sillyimages.com/face.png"],
|
|
153
|
+
),
|
|
154
|
+
] = None
|
|
155
|
+
admin_api_path: Annotated[
|
|
156
|
+
str,
|
|
157
|
+
Field(
|
|
158
|
+
description="Sub-path the Synapse admin API is served under. Keep the default if unsure.",
|
|
159
|
+
examples=["_synapse/admin/"],
|
|
160
|
+
),
|
|
161
|
+
] = "_synapse/admin/"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class AuthentikServer(BaseModel):
|
|
165
|
+
url: Annotated[
|
|
166
|
+
str,
|
|
167
|
+
Field(
|
|
168
|
+
description="URL to reach your Authentik server.",
|
|
169
|
+
examples=["https://authentik.company.org/"],
|
|
170
|
+
),
|
|
171
|
+
]
|
|
172
|
+
api_key: Annotated[
|
|
173
|
+
str,
|
|
174
|
+
Field(
|
|
175
|
+
description=inspect.cleandoc(
|
|
176
|
+
"""API token for your Authentik server. Generate one at
|
|
177
|
+
``https://<authentik>/if/admin/#/core/tokens``. Provide the bare token; the client
|
|
178
|
+
adds the ``Bearer`` prefix."""
|
|
179
|
+
),
|
|
180
|
+
examples=["yEl4tFqeIBQwoHAd9hajmkm2PBjSAirY_THIS_IS_JUST_AN_EXAMPLE_i57e"],
|
|
181
|
+
),
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class DeactivateDisabledAuthentikUsersInMatrix(BaseModel):
|
|
186
|
+
"""Lifecycle settings (Phase 5 — quarantined, dry-run/audit default; AD-5)."""
|
|
187
|
+
|
|
188
|
+
enabled: Annotated[
|
|
189
|
+
bool,
|
|
190
|
+
Field(
|
|
191
|
+
description="Lock out Matrix accounts whose Authentik account was disabled/deleted.",
|
|
192
|
+
),
|
|
193
|
+
] = True
|
|
194
|
+
dry_run: Annotated[
|
|
195
|
+
bool,
|
|
196
|
+
Field(
|
|
197
|
+
description=inspect.cleandoc(
|
|
198
|
+
"""Quarantine switch (AD-5): while ``true`` the bot only records bookkeeping and logs
|
|
199
|
+
what it *would* do to the ``onbot.lifecycle.audit`` channel — no session is revoked
|
|
200
|
+
and no account is deactivated. Set ``false`` to actually perform destructive
|
|
201
|
+
lifecycle actions. Defaults to ``true`` so destructive offboarding is opt-in."""
|
|
202
|
+
),
|
|
203
|
+
),
|
|
204
|
+
] = True
|
|
205
|
+
deactivate_after_n_sec: Annotated[
|
|
206
|
+
int,
|
|
207
|
+
Field(
|
|
208
|
+
description="Cooldown before deactivation, to absorb accidental upstream disables.",
|
|
209
|
+
),
|
|
210
|
+
] = 60 * 60 * 24
|
|
211
|
+
delete_after_n_sec: Annotated[
|
|
212
|
+
int | None,
|
|
213
|
+
Field(
|
|
214
|
+
description="Further cooldown before erase/delete. ``null`` disables deletion.",
|
|
215
|
+
),
|
|
216
|
+
] = 60 * 60 * 24 * 365
|
|
217
|
+
include_user_media_on_delete: Annotated[
|
|
218
|
+
bool,
|
|
219
|
+
Field(
|
|
220
|
+
description="Also delete media uploaded by the user on account deletion (data protection).",
|
|
221
|
+
),
|
|
222
|
+
] = False
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class SyncAuthentikUsersWithMatrix(BaseModel):
|
|
226
|
+
"""How Authentik users/groups are projected into Matrix room membership (the core sync)."""
|
|
227
|
+
|
|
228
|
+
enabled: Annotated[
|
|
229
|
+
bool,
|
|
230
|
+
Field(description="Master switch for projecting Authentik group membership into Matrix rooms."),
|
|
231
|
+
] = True
|
|
232
|
+
authentik_username_mapping_attribute: Annotated[
|
|
233
|
+
str,
|
|
234
|
+
Field(
|
|
235
|
+
description=inspect.cleandoc(
|
|
236
|
+
"""Source of the localpart of the Matrix ID (``@<localpart>:server``). A dotted path
|
|
237
|
+
into the Authentik user object (e.g. ``username`` or ``attributes.matrix_name``).
|
|
238
|
+
Under MAS this MUST agree with the localpart template MAS derives from the upstream
|
|
239
|
+
claim, or provisioned users will not match (AD-6)."""
|
|
240
|
+
),
|
|
241
|
+
),
|
|
242
|
+
] = "username"
|
|
243
|
+
kick_matrix_room_members_not_in_mapped_authentik_group_anymore: Annotated[
|
|
244
|
+
bool,
|
|
245
|
+
Field(
|
|
246
|
+
description=inspect.cleandoc(
|
|
247
|
+
"""When a user leaves an Authentik group, kick them from the corresponding Matrix
|
|
248
|
+
room so membership stays a faithful mirror. Disable to let the bot only ever *add*
|
|
249
|
+
members (never remove)."""
|
|
250
|
+
),
|
|
251
|
+
),
|
|
252
|
+
] = True
|
|
253
|
+
sync_only_users_in_authentik_pathes: Annotated[
|
|
254
|
+
list[str] | None,
|
|
255
|
+
Field(
|
|
256
|
+
description=inspect.cleandoc(
|
|
257
|
+
"""Restrict syncing to users under these Authentik directory paths. ``null`` syncs
|
|
258
|
+
users regardless of path."""
|
|
259
|
+
),
|
|
260
|
+
examples=[["users", "users/staff"]],
|
|
261
|
+
),
|
|
262
|
+
] = None
|
|
263
|
+
sync_only_users_with_authentik_attributes: Annotated[
|
|
264
|
+
dict[str, Any] | None,
|
|
265
|
+
Field(
|
|
266
|
+
description=inspect.cleandoc(
|
|
267
|
+
"""Only sync users carrying all of these Authentik attributes (exact match).
|
|
268
|
+
``null`` syncs every user."""
|
|
269
|
+
),
|
|
270
|
+
examples=[{"is_chat_user": True}],
|
|
271
|
+
),
|
|
272
|
+
] = None
|
|
273
|
+
sync_only_users_of_groups_with_id: Annotated[
|
|
274
|
+
list[str] | None,
|
|
275
|
+
Field(
|
|
276
|
+
description=inspect.cleandoc(
|
|
277
|
+
"""Only sync users who belong to at least one of these Authentik groups (by pk).
|
|
278
|
+
``null`` applies no group filter."""
|
|
279
|
+
),
|
|
280
|
+
examples=[["1120a6e1124f309bbe96c8be5fb09eab"]],
|
|
281
|
+
),
|
|
282
|
+
] = None
|
|
283
|
+
deactivate_disabled_authentik_users_in_matrix: Annotated[
|
|
284
|
+
DeactivateDisabledAuthentikUsersInMatrix,
|
|
285
|
+
Field(
|
|
286
|
+
description="Quarantined lifecycle settings: lock out Matrix accounts disabled upstream.",
|
|
287
|
+
),
|
|
288
|
+
] = Field(default_factory=DeactivateDisabledAuthentikUsersInMatrix)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class CreateMatrixSpaceIfNotExists(BaseModel):
|
|
292
|
+
enabled: bool = Field(
|
|
293
|
+
default=True,
|
|
294
|
+
description="Create the parent space if it does not exist.",
|
|
295
|
+
)
|
|
296
|
+
name: str = Field(default="OnBotSpace", description="Display name of the space.")
|
|
297
|
+
topic: str = Field(
|
|
298
|
+
default="Space for authentik group rooms",
|
|
299
|
+
description="Matrix topic (tagline) for the space.",
|
|
300
|
+
)
|
|
301
|
+
avatar_url: str | None = Field(
|
|
302
|
+
default=None,
|
|
303
|
+
description="HTTP(S) URL to a picture used as the space avatar.",
|
|
304
|
+
)
|
|
305
|
+
space_params: dict[str, Any] = Field(
|
|
306
|
+
default_factory=lambda: {"preset": "private_chat", "visibility": "private"},
|
|
307
|
+
description="Extra parameters passed to the space-creation call.",
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class CreateMatrixRoomsInAMatrixSpace(BaseModel):
|
|
312
|
+
enabled: bool = Field(
|
|
313
|
+
default=True,
|
|
314
|
+
description="Gather all Authentik-group rooms under a dedicated parent space.",
|
|
315
|
+
)
|
|
316
|
+
alias: str = Field(
|
|
317
|
+
default="OnBotSpace",
|
|
318
|
+
description='Localpart of the space canonical alias (e.g. "#<alias>:server").',
|
|
319
|
+
examples=["myspace", "companyspace"],
|
|
320
|
+
)
|
|
321
|
+
create_matrix_space_if_not_exists: CreateMatrixSpaceIfNotExists = Field(
|
|
322
|
+
default_factory=CreateMatrixSpaceIfNotExists,
|
|
323
|
+
description="Whether/how the parent space is created.",
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class SyncMatrixRoomsBasedOnAuthentikGroups(BaseModel):
|
|
328
|
+
"""Which Authentik groups become Matrix rooms, and how their power levels are derived."""
|
|
329
|
+
|
|
330
|
+
enabled: Annotated[
|
|
331
|
+
bool,
|
|
332
|
+
Field(description="Master switch for creating/maintaining one Matrix room per Authentik group."),
|
|
333
|
+
] = True
|
|
334
|
+
only_for_children_of_groups_with_uid: Annotated[
|
|
335
|
+
list[str] | None,
|
|
336
|
+
Field(
|
|
337
|
+
description=inspect.cleandoc(
|
|
338
|
+
"""Only mirror Authentik groups that are children of one of these parent groups (by
|
|
339
|
+
uid/pk). ``null`` considers all groups."""
|
|
340
|
+
),
|
|
341
|
+
examples=[["a1b2c3d4parentgroupuid"]],
|
|
342
|
+
),
|
|
343
|
+
] = None
|
|
344
|
+
only_groups_with_attributes: Annotated[
|
|
345
|
+
dict[str, Any] | None,
|
|
346
|
+
Field(
|
|
347
|
+
description=inspect.cleandoc(
|
|
348
|
+
"""Only mirror Authentik groups carrying these custom attributes. If unset, all
|
|
349
|
+
groups become rooms. https://goauthentik.io/docs/user-group/group#attributes"""
|
|
350
|
+
),
|
|
351
|
+
examples=[{"is_chatroom": True}],
|
|
352
|
+
),
|
|
353
|
+
] = None
|
|
354
|
+
room_avatar_url_attribute: Annotated[
|
|
355
|
+
str | None,
|
|
356
|
+
Field(
|
|
357
|
+
description="Authentik group attribute holding a URL used as the room avatar.",
|
|
358
|
+
examples=["chatroom_avatar_url"],
|
|
359
|
+
),
|
|
360
|
+
] = "chatroom_avatar_url"
|
|
361
|
+
only_for_groupnames_starting_with: Annotated[
|
|
362
|
+
str | None,
|
|
363
|
+
Field(
|
|
364
|
+
description=inspect.cleandoc(
|
|
365
|
+
"""Only mirror Authentik groups whose name starts with this prefix — a lightweight way
|
|
366
|
+
to opt specific groups into chat without custom attributes. ``null`` disables the
|
|
367
|
+
filter."""
|
|
368
|
+
),
|
|
369
|
+
examples=["chat-", "matrix_"],
|
|
370
|
+
),
|
|
371
|
+
] = None
|
|
372
|
+
disable_rooms_when_mapped_authentik_group_disappears: Annotated[
|
|
373
|
+
bool,
|
|
374
|
+
Field(
|
|
375
|
+
description=inspect.cleandoc(
|
|
376
|
+
"""If a mapped Authentik group disappears (deleted or lost its matching attribute),
|
|
377
|
+
kick all members and block the room."""
|
|
378
|
+
),
|
|
379
|
+
),
|
|
380
|
+
] = False
|
|
381
|
+
delete_disabled_rooms: Annotated[
|
|
382
|
+
bool,
|
|
383
|
+
Field(
|
|
384
|
+
description=inspect.cleandoc(
|
|
385
|
+
"""When a room is disabled (its Authentik group disappeared, see
|
|
386
|
+
``disable_rooms_when_mapped_authentik_group_disappears``), also delete it via the
|
|
387
|
+
Synapse admin API rather than only blocking it. Irreversible — leave ``false`` unless
|
|
388
|
+
you are sure."""
|
|
389
|
+
),
|
|
390
|
+
),
|
|
391
|
+
] = False
|
|
392
|
+
make_authentik_superusers_matrix_room_admin: Annotated[
|
|
393
|
+
bool,
|
|
394
|
+
Field(
|
|
395
|
+
description=inspect.cleandoc(
|
|
396
|
+
"""Grant Authentik superusers the Matrix admin power level (100) in the rooms they are
|
|
397
|
+
members of. Takes precedence over ``authentik_group_attr_for_matrix_power_level``."""
|
|
398
|
+
),
|
|
399
|
+
),
|
|
400
|
+
] = True
|
|
401
|
+
authentik_group_attr_for_matrix_power_level: Annotated[
|
|
402
|
+
str,
|
|
403
|
+
Field(
|
|
404
|
+
description=inspect.cleandoc(
|
|
405
|
+
"""Authentik group attribute (dotted path) holding an integer 0-100. Members of the
|
|
406
|
+
group get that Matrix power level in their onbot rooms. Superusers made admin (see
|
|
407
|
+
``make_authentik_superusers_matrix_room_admin``) ignore this. On conflicting values
|
|
408
|
+
across multiple group memberships the highest wins."""
|
|
409
|
+
),
|
|
410
|
+
examples=["matrix-userpowerlevel", "synapse-options.chat-powerlevel"],
|
|
411
|
+
),
|
|
412
|
+
] = "chat-systemwide-powerlevel"
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
class MatrixDynamicRoomSettings(BaseModel):
|
|
416
|
+
"""Template for how a Matrix room's identity (alias/name/topic/encryption/create params) is
|
|
417
|
+
derived from its Authentik group. Used as the default for all rooms and overridable per group
|
|
418
|
+
via ``per_authentik_group_pk_matrix_room_settings``."""
|
|
419
|
+
|
|
420
|
+
alias_prefix: Annotated[
|
|
421
|
+
str | None,
|
|
422
|
+
Field(
|
|
423
|
+
description="Prefix prepended to the room's canonical alias localpart. ``null`` for none.",
|
|
424
|
+
examples=["authentik-", "grp-"],
|
|
425
|
+
),
|
|
426
|
+
] = None
|
|
427
|
+
matrix_alias_from_authentik_attribute: Annotated[
|
|
428
|
+
str,
|
|
429
|
+
Field(
|
|
430
|
+
description=inspect.cleandoc(
|
|
431
|
+
"""Authentik group attribute (dotted path) used as the room alias localpart. The
|
|
432
|
+
default ``pk`` is the most stable choice (it never changes when a group is renamed)."""
|
|
433
|
+
),
|
|
434
|
+
examples=["pk", "attributes.chatroom_alias"],
|
|
435
|
+
),
|
|
436
|
+
] = "pk"
|
|
437
|
+
name_prefix: Annotated[
|
|
438
|
+
str | None,
|
|
439
|
+
Field(
|
|
440
|
+
description="Prefix prepended to the room's display name. ``null`` for none.",
|
|
441
|
+
examples=["[Chat] "],
|
|
442
|
+
),
|
|
443
|
+
] = None
|
|
444
|
+
matrix_name_from_authentik_attribute: Annotated[
|
|
445
|
+
str,
|
|
446
|
+
Field(
|
|
447
|
+
description="Authentik group attribute (dotted path) used as the room display name.",
|
|
448
|
+
examples=["name", "attributes.chatroom_name"],
|
|
449
|
+
),
|
|
450
|
+
] = "name"
|
|
451
|
+
topic_prefix: Annotated[
|
|
452
|
+
str | None,
|
|
453
|
+
Field(description="Prefix prepended to the room topic. ``null`` for none."),
|
|
454
|
+
] = None
|
|
455
|
+
matrix_topic_from_authentik_attribute: Annotated[
|
|
456
|
+
str | None,
|
|
457
|
+
Field(
|
|
458
|
+
description=inspect.cleandoc(
|
|
459
|
+
"""Authentik group attribute (dotted path) used as the room topic. ``null`` leaves the
|
|
460
|
+
topic unset."""
|
|
461
|
+
),
|
|
462
|
+
examples=["attributes.chatroom_topic"],
|
|
463
|
+
),
|
|
464
|
+
] = "attributes.chatroom_topic"
|
|
465
|
+
end2end_encryption_enabled: Annotated[
|
|
466
|
+
bool,
|
|
467
|
+
Field(
|
|
468
|
+
description=inspect.cleandoc(
|
|
469
|
+
"""Enable end-to-end encryption in the group-mapped Matrix rooms. The bot itself stays
|
|
470
|
+
outside encryption (ADR-0009): it writes room *state* (membership, power levels) but
|
|
471
|
+
does not read/post message content in encrypted rooms — welcome DMs are plaintext."""
|
|
472
|
+
),
|
|
473
|
+
),
|
|
474
|
+
] = True
|
|
475
|
+
default_room_create_params: Annotated[
|
|
476
|
+
dict[str, Any] | None,
|
|
477
|
+
Field(
|
|
478
|
+
description=inspect.cleandoc(
|
|
479
|
+
"""Parameters merged into the Matrix ``createRoom`` call for group rooms (preset,
|
|
480
|
+
visibility, federation, …). See the Client-Server API ``POST /createRoom``."""
|
|
481
|
+
),
|
|
482
|
+
examples=[{"preset": "private_chat", "visibility": "private"}],
|
|
483
|
+
),
|
|
484
|
+
] = Field(default_factory=lambda: {"preset": "private_chat", "visibility": "private"})
|
|
485
|
+
matrix_room_create_params_from_authentik_attribute: Annotated[
|
|
486
|
+
str | None,
|
|
487
|
+
Field(
|
|
488
|
+
description=inspect.cleandoc(
|
|
489
|
+
"""Authentik group attribute (dotted path) holding a dict of extra ``createRoom``
|
|
490
|
+
params, merged over ``default_room_create_params`` for that group. ``null`` to
|
|
491
|
+
disable per-group overrides."""
|
|
492
|
+
),
|
|
493
|
+
examples=["attributes.chatroom_params"],
|
|
494
|
+
),
|
|
495
|
+
] = "attributes.chatroom_params"
|
|
496
|
+
keep_updating_matrix_attributes_from_authentik: Annotated[
|
|
497
|
+
bool,
|
|
498
|
+
Field(
|
|
499
|
+
description="Keep room name/topic in sync with Authentik, overwriting drift.",
|
|
500
|
+
),
|
|
501
|
+
] = True
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
class OnbotConfig(BaseSettings):
|
|
505
|
+
model_config = SettingsConfigDict(env_prefix="ONBOT_", env_nested_delimiter="__")
|
|
506
|
+
|
|
507
|
+
log_level: Annotated[
|
|
508
|
+
Literal["INFO", "DEBUG"],
|
|
509
|
+
Field(
|
|
510
|
+
description="Logging verbosity. ``DEBUG`` is noisy but useful while wiring up the bot.",
|
|
511
|
+
examples=["INFO", "DEBUG"],
|
|
512
|
+
),
|
|
513
|
+
] = "INFO"
|
|
514
|
+
server_tick_rate_sec: Annotated[
|
|
515
|
+
int,
|
|
516
|
+
Field(
|
|
517
|
+
description=inspect.cleandoc(
|
|
518
|
+
"""How often (seconds) the reconciler re-converges Authentik→Matrix state, in
|
|
519
|
+
addition to on-demand triggers. The reconcile is idempotent (AD-2), so this is a
|
|
520
|
+
safety net for drift, not the only path — onboarding still reacts to live events."""
|
|
521
|
+
),
|
|
522
|
+
examples=[20, 300],
|
|
523
|
+
),
|
|
524
|
+
] = 20
|
|
525
|
+
|
|
526
|
+
synapse_server: Annotated[
|
|
527
|
+
SynapseServer,
|
|
528
|
+
Field(
|
|
529
|
+
title="Synapse Server Configuration",
|
|
530
|
+
description="Authorization/connection data for the Matrix CS and Synapse admin APIs.",
|
|
531
|
+
),
|
|
532
|
+
]
|
|
533
|
+
authentik_server: Annotated[
|
|
534
|
+
AuthentikServer,
|
|
535
|
+
Field(description="Connection data and API token for the upstream Authentik IdP (source of truth)."),
|
|
536
|
+
]
|
|
537
|
+
|
|
538
|
+
mas_admin: Annotated[
|
|
539
|
+
MasAdmin | None,
|
|
540
|
+
Field(
|
|
541
|
+
description=inspect.cleandoc(
|
|
542
|
+
"""Optional MAS admin API credentials. Required for the lifecycle module to actually
|
|
543
|
+
revoke sessions / deactivate accounts under MAS — the Synapse admin API cannot
|
|
544
|
+
(ADR-0005, BATTLE_PLAN §7 Q1). Leave unset on non-MAS deployments."""
|
|
545
|
+
),
|
|
546
|
+
),
|
|
547
|
+
] = None
|
|
548
|
+
|
|
549
|
+
welcome_new_users_messages: Annotated[
|
|
550
|
+
list[str] | None,
|
|
551
|
+
Field(
|
|
552
|
+
description=inspect.cleandoc(
|
|
553
|
+
"""Messages the bot sends, in order, in the 1:1 welcome DM to each newly onboarded
|
|
554
|
+
user. Each message is sent once (content-hashed, idempotent). ``null`` or an empty
|
|
555
|
+
list disables the welcome DM entirely."""
|
|
556
|
+
),
|
|
557
|
+
),
|
|
558
|
+
] = Field(
|
|
559
|
+
default_factory=lambda: [
|
|
560
|
+
"Welcome to the company chat. I am the company bot. I will invite you to the groups you "
|
|
561
|
+
"are assigned to. If you have any technical questions write a message to "
|
|
562
|
+
"@admin-person:matrix.company.org.",
|
|
563
|
+
"If you need guidance on how to use this chat have a look at the official documentation: "
|
|
564
|
+
"https://matrix.org/docs/chat_basics/matrix-for-im/ and https://element.io/user-guide",
|
|
565
|
+
"🛑 🔐 The chat software will ask you to set up a 'Security Key Backup'. This is very "
|
|
566
|
+
"important. Save the file in a secure location, otherwise you could lose access to older "
|
|
567
|
+
"encrypted messages later. Please follow the request.",
|
|
568
|
+
]
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
place_onboarding_rooms_in_space: Annotated[
|
|
572
|
+
bool,
|
|
573
|
+
Field(
|
|
574
|
+
description=inspect.cleandoc(
|
|
575
|
+
"""Gather the 1:1 onboarding/welcome rooms under the managed parent space (G4.5).
|
|
576
|
+
Off by default — whether direct rooms belong in a space is a matter of taste. Only
|
|
577
|
+
applies when the parent space is enabled."""
|
|
578
|
+
),
|
|
579
|
+
),
|
|
580
|
+
] = False
|
|
581
|
+
|
|
582
|
+
sync_authentik_users_with_matrix_rooms: Annotated[
|
|
583
|
+
SyncAuthentikUsersWithMatrix,
|
|
584
|
+
Field(description="User→room-membership projection (the core Authentik→Matrix sync)."),
|
|
585
|
+
] = Field(default_factory=SyncAuthentikUsersWithMatrix)
|
|
586
|
+
create_matrix_rooms_in_a_matrix_space: Annotated[
|
|
587
|
+
CreateMatrixRoomsInAMatrixSpace,
|
|
588
|
+
Field(description="Configure the designated parent space for Authentik-group rooms."),
|
|
589
|
+
] = Field(default_factory=CreateMatrixRoomsInAMatrixSpace)
|
|
590
|
+
sync_matrix_rooms_based_on_authentik_groups: Annotated[
|
|
591
|
+
SyncMatrixRoomsBasedOnAuthentikGroups,
|
|
592
|
+
Field(description="Group→room projection rules (which groups become rooms, power levels)."),
|
|
593
|
+
] = Field(default_factory=SyncMatrixRoomsBasedOnAuthentikGroups)
|
|
594
|
+
matrix_room_default_settings: Annotated[
|
|
595
|
+
MatrixDynamicRoomSettings,
|
|
596
|
+
Field(description="Default room identity template applied to every group room."),
|
|
597
|
+
] = Field(default_factory=MatrixDynamicRoomSettings)
|
|
598
|
+
per_authentik_group_pk_matrix_room_settings: Annotated[
|
|
599
|
+
dict[str, MatrixDynamicRoomSettings],
|
|
600
|
+
Field(
|
|
601
|
+
description="Per-group room-setting overrides, keyed by Authentik group primary key (pk).",
|
|
602
|
+
),
|
|
603
|
+
] = Field(default_factory=dict)
|
|
604
|
+
|
|
605
|
+
matrix_user_ignore_list: Annotated[
|
|
606
|
+
list[str],
|
|
607
|
+
Field(examples=[["@admin:company.org", "@root:company.org"]]),
|
|
608
|
+
] = Field(default_factory=list)
|
|
609
|
+
authentik_user_ignore_list: Annotated[
|
|
610
|
+
list[str],
|
|
611
|
+
Field(examples=[["admin", "internal_account_alex"]]),
|
|
612
|
+
] = Field(default_factory=list)
|
|
613
|
+
authentik_group_id_ignore_list: Annotated[
|
|
614
|
+
list[str],
|
|
615
|
+
Field(examples=[["1120a6e1124f309bbe96c8be5fb09eab"]]),
|
|
616
|
+
] = Field(default_factory=list)
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def get_config_file_path(*, not_exists_ok: bool = False) -> Path | None:
|
|
620
|
+
"""Return the YAML config path from ``ONBOT_CONFIG_FILE_PATH`` (default ``config.yml``)."""
|
|
621
|
+
yaml_file = Path(os.environ.get(CONFIG_FILE_ENV_VAR, "config.yml"))
|
|
622
|
+
if yaml_file.is_file() or not_exists_ok:
|
|
623
|
+
return yaml_file
|
|
624
|
+
return None
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def load_config() -> OnbotConfig:
|
|
628
|
+
"""Load config from YAML if present; env vars (``ONBOT_*``) override either way."""
|
|
629
|
+
yaml_file = get_config_file_path()
|
|
630
|
+
if yaml_file is not None:
|
|
631
|
+
with yaml_file.open() as reader:
|
|
632
|
+
data = yaml.safe_load(reader) or {}
|
|
633
|
+
return OnbotConfig.model_validate(data)
|
|
634
|
+
# No file: required fields are supplied from the environment by pydantic-settings.
|
|
635
|
+
return OnbotConfig() # type: ignore[call-arg]
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def generate_example_config() -> str:
|
|
639
|
+
"""Render a YAML document of the default config model (G11.2).
|
|
640
|
+
|
|
641
|
+
Required fields that have no default are emitted as ``null`` placeholders so the result is a
|
|
642
|
+
fillable template rather than a validation error.
|
|
643
|
+
"""
|
|
644
|
+
model = OnbotConfig(
|
|
645
|
+
synapse_server=SynapseServer(server_name="", server_url="", bot_user_id="", bot_access_token=""),
|
|
646
|
+
authentik_server=AuthentikServer(url="", api_key=""),
|
|
647
|
+
)
|
|
648
|
+
data = model.model_dump(mode="json")
|
|
649
|
+
for key in ("server_name", "server_url", "bot_user_id", "bot_access_token"):
|
|
650
|
+
data["synapse_server"][key] = None
|
|
651
|
+
data["authentik_server"]["url"] = None
|
|
652
|
+
data["authentik_server"]["api_key"] = None
|
|
653
|
+
return yaml.safe_dump(data, sort_keys=False, allow_unicode=True)
|