4134
|
1 #!/usr/bin/env python3 |
|
2 |
|
3 # Libervia CLI |
|
4 # Copyright (C) 2009-2023 Jérôme Poisson (goffi@goffi.org) |
|
5 |
|
6 # This program is free software: you can redistribute it and/or modify |
|
7 # it under the terms of the GNU Affero General Public License as published by |
|
8 # the Free Software Foundation, either version 3 of the License, or |
|
9 # (at your option) any later version. |
|
10 |
|
11 # This program is distributed in the hope that it will be useful, |
|
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
14 # GNU Affero General Public License for more details. |
|
15 |
|
16 # You should have received a copy of the GNU Affero General Public License |
|
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
18 |
|
19 from libervia.backend.core.i18n import _ |
|
20 from libervia.backend.memory.memory import ( |
|
21 NotificationPriority, |
|
22 NotificationStatus, |
|
23 NotificationType, |
|
24 ) |
|
25 from libervia.backend.tools.common import data_format, date_utils |
|
26 from libervia.cli.constants import Const as C |
|
27 from rich.live import Live |
|
28 from rich.table import Table |
|
29 from rich.text import Text |
|
30 |
|
31 from . import base |
|
32 |
|
33 __commands__ = ["Notification"] |
|
34 |
|
35 |
|
36 class Add(base.CommandBase): |
|
37 """Create and broadcast a notification""" |
|
38 |
|
39 def __init__(self, host): |
|
40 super(Add, self).__init__( |
|
41 host, "add", use_verbose=True, help=_("create and broadcast a notification") |
|
42 ) |
|
43 |
|
44 def add_parser_options(self): |
|
45 self.parser.add_argument( |
|
46 "type", |
|
47 choices=[e.name for e in NotificationType], |
|
48 help=_("notification type (default: %(default)s)"), |
|
49 ) |
|
50 |
|
51 self.parser.add_argument( |
|
52 "body_plain", help=_("plain text body of the notification") |
|
53 ) |
|
54 |
|
55 # TODO: |
|
56 # self.parser.add_argument( |
|
57 # "-r", "--body-rich", default="", help=_("rich text body of the notification") |
|
58 # ) |
|
59 |
|
60 self.parser.add_argument( |
|
61 "-t", "--title", default="", help=_("title of the notification") |
|
62 ) |
|
63 |
|
64 self.parser.add_argument( |
|
65 "-g", |
|
66 "--is-global", |
|
67 action="store_true", |
|
68 help=_("indicates if the notification is for all profiles"), |
|
69 ) |
|
70 |
|
71 # TODO: |
|
72 # self.parser.add_argument( |
|
73 # "--requires-action", |
|
74 # action="store_true", |
|
75 # help=_("indicates if the notification requires action"), |
|
76 # ) |
|
77 |
|
78 self.parser.add_argument( |
|
79 "-P", |
|
80 "--priority", |
|
81 default="MEDIUM", |
|
82 choices=[p.name for p in NotificationPriority], |
|
83 help=_("priority level of the notification (default: %(default)s)"), |
|
84 ) |
|
85 |
|
86 self.parser.add_argument( |
|
87 "-e", |
|
88 "--expire-at", |
|
89 type=base.date_decoder, |
|
90 default=0, |
|
91 help=_( |
|
92 "expiration timestamp for the notification (optional, can be 0 for none)" |
|
93 ), |
|
94 ) |
|
95 |
|
96 async def start(self): |
|
97 try: |
|
98 await self.host.bridge.notification_add( |
|
99 self.args.type, |
|
100 self.args.body_plain, |
|
101 "", # TODO: self.args.body_rich or "", |
|
102 self.args.title or "", |
|
103 self.args.is_global, |
|
104 False, # TODO: self.args.requires_action, |
|
105 self.args.priority, |
|
106 self.args.expire_at, |
|
107 "", |
|
108 self.profile, |
|
109 ) |
|
110 except Exception as e: |
|
111 self.disp(f"can't add notification: {e}", error=True) |
|
112 self.host.quit(C.EXIT_BRIDGE_ERRBACK) |
|
113 else: |
|
114 self.disp("Notification added.") |
|
115 |
|
116 self.host.quit() |
|
117 |
|
118 |
|
119 class Get(base.CommandBase): |
|
120 """Get available notifications""" |
|
121 |
|
122 def __init__(self, host): |
|
123 super(Get, self).__init__( |
|
124 host, |
|
125 "get", |
|
126 use_output=C.OUTPUT_LIST_DICT, |
|
127 extra_outputs={"default": self.default_output}, |
|
128 help=_("display notifications"), |
|
129 ) |
|
130 |
|
131 def add_parser_options(self): |
|
132 self.parser.add_argument( |
|
133 "-f", |
|
134 "--follow", |
|
135 action="store_true", |
|
136 help=_("wait and print incoming notifications"), |
|
137 ) |
|
138 |
|
139 self.parser.add_argument( |
|
140 "-t", |
|
141 "--type", |
|
142 type=str, |
|
143 choices=[t.name for t in NotificationType], |
|
144 help=_("filter by type of the notification"), |
|
145 ) |
|
146 |
|
147 self.parser.add_argument( |
|
148 "-s", |
|
149 "--status", |
|
150 type=str, |
|
151 choices=[s.name for s in NotificationStatus], |
|
152 help=_("filter by status of the notification"), |
|
153 ) |
|
154 |
|
155 self.parser.add_argument( |
|
156 "-a", |
|
157 "--requires-action", |
|
158 type=C.bool, |
|
159 default=None, |
|
160 help=_( |
|
161 "filter notifications that require (or not) user action, true by " |
|
162 "default, don't filter if omitted" |
|
163 ), |
|
164 ) |
|
165 |
|
166 self.parser.add_argument( |
|
167 "-P", |
|
168 "--min-priority", |
|
169 type=str, |
|
170 choices=[p.name for p in NotificationPriority], |
|
171 help=_("filter notifications with at least the specified priority"), |
|
172 ) |
|
173 |
|
174 def create_table(self): |
|
175 table = Table(box=None, show_header=False, collapse_padding=True) |
|
176 table.add_column("is_new") |
|
177 table.add_column("type") |
|
178 table.add_column("id") |
|
179 table.add_column("priority") |
|
180 table.add_column("timestamp") |
|
181 table.add_column("body") |
|
182 return table |
|
183 |
|
184 def default_output(self, notifs): |
|
185 if self.args.follow: |
|
186 if self.live is None: |
|
187 self.table = table = self.create_table() |
|
188 self.live = Live(table, auto_refresh=False, console=self.console) |
|
189 self.host.add_on_quit_callback(self.live.stop) |
|
190 self.live.start() |
|
191 else: |
|
192 table = self.table |
|
193 else: |
|
194 table = self.create_table() |
|
195 |
|
196 for notif in notifs: |
|
197 emoji_mapper = { |
|
198 "chat": "💬", |
|
199 "blog": "📝", |
|
200 "calendar": "📅", |
|
201 "file": "📂", |
|
202 "call": "📞", |
|
203 "service": "📢", |
|
204 "other": "🟣", |
|
205 } |
|
206 emoji = emoji_mapper[notif.get("type", "other")] |
|
207 notif_id = Text(notif["id"]) |
|
208 created = date_utils.date_fmt(notif["timestamp"], tz_info=date_utils.TZ_LOCAL) |
|
209 |
|
210 priority_name = NotificationPriority(notif["priority"]).name.lower() |
|
211 |
|
212 priority = Text(f"[{priority_name}]", style=f"priority_{priority_name}") |
|
213 |
|
214 body_parts = [] |
|
215 title = notif.get("title") |
|
216 if title: |
|
217 body_parts.append((f"{title}\n", "notif_title")) |
|
218 body_parts.append(notif["body_plain"]) |
|
219 body = Text.assemble(*body_parts) |
|
220 |
|
221 new_flag = "🌟 " if notif.get("new") else "" |
|
222 table.add_row(new_flag, emoji, notif_id, created, priority, body) |
|
223 |
|
224 if self.args.follow: |
|
225 self.live.refresh() |
|
226 else: |
|
227 self.print(table) |
|
228 |
|
229 async def on_notification_new( |
|
230 self, |
|
231 id_: str, |
|
232 timestamp: float, |
|
233 type_: str, |
|
234 body_plain: str, |
|
235 body_rich: str, |
|
236 title: str, |
|
237 requires_action: bool, |
|
238 priority: int, |
|
239 expire_at: float, |
|
240 extra: str, |
|
241 profile: str, |
|
242 ) -> None: |
|
243 """Callback when a new notification is emitted.""" |
|
244 notification_data = { |
|
245 "id": id_, |
|
246 "timestamp": timestamp, |
|
247 "type": type_, |
|
248 "body_plain": body_plain, |
|
249 "body_rich": body_rich, |
|
250 "title": title, |
|
251 "requires_action": requires_action, |
|
252 "priority": priority, |
|
253 "expire_at": expire_at, |
|
254 "extra": data_format.deserialise(extra), |
|
255 "profile": profile, |
|
256 "new": True, |
|
257 } |
|
258 |
|
259 await self.output([notification_data]) |
|
260 |
|
261 async def start(self): |
|
262 keys = ["type", "status", "requires_action", "min_priority"] |
|
263 filters = { |
|
264 key: getattr(self.args, key) for key in keys if getattr(self.args, key) |
|
265 } |
|
266 try: |
|
267 notifications = data_format.deserialise( |
|
268 await self.host.bridge.notifications_get( |
|
269 data_format.serialise(filters), |
|
270 self.profile, |
|
271 ), |
|
272 type_check=list, |
|
273 ) |
|
274 except Exception as e: |
|
275 self.disp(f"can't get notifications: {e}", error=True) |
|
276 self.host.quit(C.EXIT_BRIDGE_ERRBACK) |
|
277 else: |
|
278 self.live = None |
|
279 await self.output(notifications) |
|
280 if self.args.follow: |
|
281 self.host.bridge.register_signal( |
|
282 "notification_new", self.on_notification_new, "core" |
|
283 ) |
|
284 else: |
|
285 self.host.quit() |
|
286 |
|
287 |
|
288 class Delete(base.CommandBase): |
|
289 """Delete a notification""" |
|
290 |
|
291 def __init__(self, host): |
|
292 super(Delete, self).__init__( |
|
293 host, "delete", use_verbose=True, help=_("delete a notification") |
|
294 ) |
|
295 |
|
296 def add_parser_options(self): |
|
297 self.parser.add_argument( |
|
298 "id", |
|
299 help=_("ID of the notification to delete"), |
|
300 ) |
|
301 |
|
302 self.parser.add_argument( |
|
303 "-g", "--is-global", |
|
304 action="store_true", |
|
305 help=_("true if the notification is a global one"), |
|
306 ) |
|
307 |
|
308 self.parser.add_argument( |
|
309 "--profile-key", |
|
310 default="@ALL@", |
|
311 help=_("Profile key (use '@ALL@' for all profiles, default: %(default)s)"), |
|
312 ) |
|
313 |
|
314 async def start(self): |
|
315 try: |
|
316 await self.host.bridge.notification_delete( |
|
317 self.args.id, self.args.is_global, self.profile |
|
318 ) |
|
319 except Exception as e: |
|
320 self.disp(f"can't delete notification: {e}", error=True) |
|
321 self.host.quit(C.EXIT_BRIDGE_ERRBACK) |
|
322 else: |
|
323 self.disp("Notification deleted.") |
|
324 self.host.quit() |
|
325 |
|
326 |
|
327 class Expire(base.CommandBase): |
|
328 """Clean expired notifications""" |
|
329 |
|
330 def __init__(self, host): |
|
331 super(Expire, self).__init__( |
|
332 host, "expire", use_verbose=True, help=_("clean expired notifications") |
|
333 ) |
|
334 |
|
335 def add_parser_options(self): |
|
336 self.parser.add_argument( |
|
337 "-l", |
|
338 "--limit", |
|
339 type=base.date_decoder, |
|
340 metavar="TIME_PATTERN", |
|
341 help=_("time limit for older notifications. default: no limit used)"), |
|
342 ) |
|
343 self.parser.add_argument( |
|
344 "-a", |
|
345 "--all", |
|
346 action="store_true", |
|
347 help=_( |
|
348 "expire notifications for all profiles (default: use current profile)" |
|
349 ), |
|
350 ) |
|
351 |
|
352 async def start(self): |
|
353 try: |
|
354 await self.host.bridge.notifications_expired_clean( |
|
355 -1.0 if self.args.limit is None else self.args.limit, |
|
356 C.PROF_KEY_NONE if self.args.all else self.profile, |
|
357 ) |
|
358 except Exception as e: |
|
359 self.disp(f"can't clean expired notifications: {e}", error=True) |
|
360 self.host.quit(C.EXIT_BRIDGE_ERRBACK) |
|
361 else: |
|
362 self.disp("Expired notifications cleaned.") |
|
363 self.host.quit() |
|
364 |
|
365 |
|
366 class Notification(base.CommandBase): |
|
367 subcommands = (Add, Get, Delete, Expire) |
|
368 |
|
369 def __init__(self, host): |
|
370 super(Notification, self).__init__( |
|
371 host, "notification", use_profile=False, help=_("Notifications handling") |
|
372 ) |