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/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)