|
1 | 1 | from os.path import expandvars |
2 | | -from typing import Literal |
| 2 | +from typing import Literal, Optional |
3 | 3 |
|
4 | | -from pydantic import BaseModel, Field, field_validator |
| 4 | +from pydantic import BaseModel, Field, field_validator, model_validator |
5 | 5 |
|
6 | 6 |
|
7 | 7 | class Program(BaseModel): |
@@ -68,19 +68,106 @@ def password(self): |
68 | 68 | return expandvars(self.password_) |
69 | 69 |
|
70 | 70 |
|
| 71 | +class NotificationProvider(BaseModel): |
| 72 | + """Configuration for a single notification provider.""" |
| 73 | + |
| 74 | + type: str = Field(..., description="Provider type (telegram, discord, bark, etc.)") |
| 75 | + enabled: bool = Field(True, description="Whether this provider is enabled") |
| 76 | + |
| 77 | + # Common fields (with env var expansion) |
| 78 | + token_: Optional[str] = Field(None, alias="token", description="Auth token") |
| 79 | + chat_id_: Optional[str] = Field(None, alias="chat_id", description="Chat/channel ID") |
| 80 | + |
| 81 | + # Provider-specific fields |
| 82 | + webhook_url_: Optional[str] = Field( |
| 83 | + None, alias="webhook_url", description="Webhook URL for discord/wecom" |
| 84 | + ) |
| 85 | + server_url_: Optional[str] = Field( |
| 86 | + None, alias="server_url", description="Server URL for gotify/bark" |
| 87 | + ) |
| 88 | + device_key_: Optional[str] = Field( |
| 89 | + None, alias="device_key", description="Device key for bark" |
| 90 | + ) |
| 91 | + user_key_: Optional[str] = Field( |
| 92 | + None, alias="user_key", description="User key for pushover" |
| 93 | + ) |
| 94 | + api_token_: Optional[str] = Field( |
| 95 | + None, alias="api_token", description="API token for pushover" |
| 96 | + ) |
| 97 | + template: Optional[str] = Field( |
| 98 | + None, description="Custom template for webhook provider" |
| 99 | + ) |
| 100 | + url_: Optional[str] = Field( |
| 101 | + None, alias="url", description="URL for generic webhook provider" |
| 102 | + ) |
| 103 | + |
| 104 | + @property |
| 105 | + def token(self) -> str: |
| 106 | + return expandvars(self.token_) if self.token_ else "" |
| 107 | + |
| 108 | + @property |
| 109 | + def chat_id(self) -> str: |
| 110 | + return expandvars(self.chat_id_) if self.chat_id_ else "" |
| 111 | + |
| 112 | + @property |
| 113 | + def webhook_url(self) -> str: |
| 114 | + return expandvars(self.webhook_url_) if self.webhook_url_ else "" |
| 115 | + |
| 116 | + @property |
| 117 | + def server_url(self) -> str: |
| 118 | + return expandvars(self.server_url_) if self.server_url_ else "" |
| 119 | + |
| 120 | + @property |
| 121 | + def device_key(self) -> str: |
| 122 | + return expandvars(self.device_key_) if self.device_key_ else "" |
| 123 | + |
| 124 | + @property |
| 125 | + def user_key(self) -> str: |
| 126 | + return expandvars(self.user_key_) if self.user_key_ else "" |
| 127 | + |
| 128 | + @property |
| 129 | + def api_token(self) -> str: |
| 130 | + return expandvars(self.api_token_) if self.api_token_ else "" |
| 131 | + |
| 132 | + @property |
| 133 | + def url(self) -> str: |
| 134 | + return expandvars(self.url_) if self.url_ else "" |
| 135 | + |
| 136 | + |
71 | 137 | class Notification(BaseModel): |
72 | | - enable: bool = Field(False, description="Enable notification") |
73 | | - type: str = Field("telegram", description="Notification type") |
74 | | - token_: str = Field("", alias="token", description="Notification token") |
75 | | - chat_id_: str = Field("", alias="chat_id", description="Notification chat id") |
| 138 | + """Notification configuration supporting multiple providers.""" |
| 139 | + |
| 140 | + enable: bool = Field(False, description="Enable notification system") |
| 141 | + providers: list[NotificationProvider] = Field( |
| 142 | + default_factory=list, description="List of notification providers" |
| 143 | + ) |
| 144 | + |
| 145 | + # Legacy fields for backward compatibility (deprecated) |
| 146 | + type: Optional[str] = Field(None, description="[Deprecated] Use providers instead") |
| 147 | + token_: Optional[str] = Field(None, alias="token", description="[Deprecated]") |
| 148 | + chat_id_: Optional[str] = Field(None, alias="chat_id", description="[Deprecated]") |
76 | 149 |
|
77 | 150 | @property |
78 | | - def token(self): |
79 | | - return expandvars(self.token_) |
| 151 | + def token(self) -> str: |
| 152 | + return expandvars(self.token_) if self.token_ else "" |
80 | 153 |
|
81 | 154 | @property |
82 | | - def chat_id(self): |
83 | | - return expandvars(self.chat_id_) |
| 155 | + def chat_id(self) -> str: |
| 156 | + return expandvars(self.chat_id_) if self.chat_id_ else "" |
| 157 | + |
| 158 | + @model_validator(mode="after") |
| 159 | + def migrate_legacy_config(self) -> "Notification": |
| 160 | + """Auto-migrate old single-provider config to new format.""" |
| 161 | + if self.type and not self.providers: |
| 162 | + # Old format detected, migrate to new format |
| 163 | + legacy_provider = NotificationProvider( |
| 164 | + type=self.type, |
| 165 | + enabled=True, |
| 166 | + token=self.token_ or "", |
| 167 | + chat_id=self.chat_id_ or "", |
| 168 | + ) |
| 169 | + self.providers = [legacy_provider] |
| 170 | + return self |
84 | 171 |
|
85 | 172 |
|
86 | 173 | class ExperimentalOpenAI(BaseModel): |
|
0 commit comments