libervia-backend/libervia/cli/cmd_notifications.py
2024-06-19 18:44:57 +02:00

373 lines
11 KiB
Python

#!/usr/bin/env python3
# Libervia CLI
# Copyright (C) 2009-2023 Jérôme Poisson (goffi@goffi.org)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from libervia.backend.core.i18n import _
from libervia.backend.memory.memory import (
NotificationPriority,
NotificationStatus,
NotificationType,
)
from libervia.backend.tools.common import data_format, date_utils
from libervia.cli.constants import Const as C
from rich.live import Live
from rich.table import Table
from rich.text import Text
from . import base
__commands__ = ["Notification"]
class Add(base.CommandBase):
"""Create and broadcast a notification"""
def __init__(self, host):
super(Add, self).__init__(
host, "add", use_verbose=True, help=_("create and broadcast a notification")
)
def add_parser_options(self):
self.parser.add_argument(
"type",
choices=[e.name for e in NotificationType],
help=_("notification type (default: %(default)s)"),
)
self.parser.add_argument(
"body_plain", help=_("plain text body of the notification")
)
# TODO:
# self.parser.add_argument(
# "-r", "--body-rich", default="", help=_("rich text body of the notification")
# )
self.parser.add_argument(
"-t", "--title", default="", help=_("title of the notification")
)
self.parser.add_argument(
"-g",
"--is-global",
action="store_true",
help=_("indicates if the notification is for all profiles"),
)
# TODO:
# self.parser.add_argument(
# "--requires-action",
# action="store_true",
# help=_("indicates if the notification requires action"),
# )
self.parser.add_argument(
"-P",
"--priority",
default="MEDIUM",
choices=[p.name for p in NotificationPriority],
help=_("priority level of the notification (default: %(default)s)"),
)
self.parser.add_argument(
"-e",
"--expire-at",
type=base.date_decoder,
default=0,
help=_(
"expiration timestamp for the notification (optional, can be 0 for none)"
),
)
async def start(self):
try:
await self.host.bridge.notification_add(
self.args.type,
self.args.body_plain,
"", # TODO: self.args.body_rich or "",
self.args.title or "",
self.args.is_global,
False, # TODO: self.args.requires_action,
self.args.priority,
self.args.expire_at,
"",
self.profile,
)
except Exception as e:
self.disp(f"can't add notification: {e}", error=True)
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
else:
self.disp("Notification added.")
self.host.quit()
class Get(base.CommandBase):
"""Get available notifications"""
def __init__(self, host):
super(Get, self).__init__(
host,
"get",
use_output=C.OUTPUT_LIST_DICT,
extra_outputs={"default": self.default_output},
help=_("display notifications"),
)
def add_parser_options(self):
self.parser.add_argument(
"-f",
"--follow",
action="store_true",
help=_("wait and print incoming notifications"),
)
self.parser.add_argument(
"-t",
"--type",
type=str,
choices=[t.name for t in NotificationType],
help=_("filter by type of the notification"),
)
self.parser.add_argument(
"-s",
"--status",
type=str,
choices=[s.name for s in NotificationStatus],
help=_("filter by status of the notification"),
)
self.parser.add_argument(
"-a",
"--requires-action",
type=C.bool,
default=None,
help=_(
"filter notifications that require (or not) user action, true by "
"default, don't filter if omitted"
),
)
self.parser.add_argument(
"-P",
"--min-priority",
type=str,
choices=[p.name for p in NotificationPriority],
help=_("filter notifications with at least the specified priority"),
)
def create_table(self):
table = Table(box=None, show_header=False, collapse_padding=True)
table.add_column("is_new")
table.add_column("type")
table.add_column("id")
table.add_column("priority")
table.add_column("timestamp")
table.add_column("body")
return table
def default_output(self, notifs):
if self.args.follow:
if self.live is None:
self.table = table = self.create_table()
self.live = Live(table, auto_refresh=False, console=self.console)
self.host.add_on_quit_callback(self.live.stop)
self.live.start()
else:
table = self.table
else:
table = self.create_table()
for notif in notifs:
emoji_mapper = {
"chat": "💬",
"blog": "📝",
"calendar": "📅",
"file": "📂",
"call": "📞",
"service": "📢",
"other": "🟣",
}
emoji = emoji_mapper[notif.get("type", "other")]
notif_id = Text(notif["id"])
created = date_utils.date_fmt(notif["timestamp"], tz_info=date_utils.TZ_LOCAL)
priority_name = NotificationPriority(notif["priority"]).name.lower()
priority = Text(f"[{priority_name}]", style=f"priority_{priority_name}")
body_parts = []
title = notif.get("title")
if title:
body_parts.append((f"{title}\n", "notif_title"))
body_parts.append(notif["body_plain"])
body = Text.assemble(*body_parts)
new_flag = "🌟 " if notif.get("new") else ""
table.add_row(new_flag, emoji, notif_id, created, priority, body)
if self.args.follow:
self.live.refresh()
else:
self.print(table)
async def on_notification_new(
self,
id_: str,
timestamp: float,
type_: str,
body_plain: str,
body_rich: str,
title: str,
requires_action: bool,
priority: int,
expire_at: float,
extra: str,
profile: str,
) -> None:
"""Callback when a new notification is emitted."""
notification_data = {
"id": id_,
"timestamp": timestamp,
"type": type_,
"body_plain": body_plain,
"body_rich": body_rich,
"title": title,
"requires_action": requires_action,
"priority": priority,
"expire_at": expire_at,
"extra": data_format.deserialise(extra),
"profile": profile,
"new": True,
}
await self.output([notification_data])
async def start(self):
keys = ["type", "status", "requires_action", "min_priority"]
filters = {
key: getattr(self.args, key) for key in keys if getattr(self.args, key)
}
try:
notifications = data_format.deserialise(
await self.host.bridge.notifications_get(
data_format.serialise(filters),
self.profile,
),
type_check=list,
)
except Exception as e:
self.disp(f"can't get notifications: {e}", error=True)
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
else:
self.live = None
await self.output(notifications)
if self.args.follow:
self.host.bridge.register_signal(
"notification_new", self.on_notification_new, "core"
)
else:
self.host.quit()
class Delete(base.CommandBase):
"""Delete a notification"""
def __init__(self, host):
super(Delete, self).__init__(
host, "delete", use_verbose=True, help=_("delete a notification")
)
def add_parser_options(self):
self.parser.add_argument(
"id",
help=_("ID of the notification to delete"),
)
self.parser.add_argument(
"-g",
"--is-global",
action="store_true",
help=_("true if the notification is a global one"),
)
self.parser.add_argument(
"--profile-key",
default="@ALL@",
help=_("Profile key (use '@ALL@' for all profiles, default: %(default)s)"),
)
async def start(self):
try:
await self.host.bridge.notification_delete(
self.args.id, self.args.is_global, self.profile
)
except Exception as e:
self.disp(f"can't delete notification: {e}", error=True)
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
else:
self.disp("Notification deleted.")
self.host.quit()
class Expire(base.CommandBase):
"""Clean expired notifications"""
def __init__(self, host):
super(Expire, self).__init__(
host, "expire", use_verbose=True, help=_("clean expired notifications")
)
def add_parser_options(self):
self.parser.add_argument(
"-l",
"--limit",
type=base.date_decoder,
metavar="TIME_PATTERN",
help=_("time limit for older notifications. default: no limit used)"),
)
self.parser.add_argument(
"-a",
"--all",
action="store_true",
help=_(
"expire notifications for all profiles (default: use current profile)"
),
)
async def start(self):
try:
await self.host.bridge.notifications_expired_clean(
-1.0 if self.args.limit is None else self.args.limit,
C.PROF_KEY_NONE if self.args.all else self.profile,
)
except Exception as e:
self.disp(f"can't clean expired notifications: {e}", error=True)
self.host.quit(C.EXIT_BRIDGE_ERRBACK)
else:
self.disp("Expired notifications cleaned.")
self.host.quit()
class Notification(base.CommandBase):
subcommands = (Add, Get, Delete, Expire)
def __init__(self, host):
super(Notification, self).__init__(
host, "notification", use_profile=False, help=_("Notifications handling")
)