Dotfiles_ArchAP/greetd/src/avatar_backend.py

101 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/python3
"""Publie l'avatar du compte courant via AccountsService, sans privilège local."""
import json
import os
from pathlib import Path
import pwd
import stat
import sys
from urllib.parse import unquote, urlsplit
from PIL import Image, ImageOps, UnidentifiedImageError
from avatar_icon import system_icon
import gi
from gi.repository import Gio, GLib
MAX_SOURCE_BYTES = 12 * 1024 * 1024
MAX_SOURCE_PIXELS = 16_000_000
Image.MAX_IMAGE_PIXELS = MAX_SOURCE_PIXELS
class AvatarError(Exception):
pass
def source_path(value):
"""CONTRACT: seule une image locale régulière du compte appelant est acceptée."""
url = urlsplit(value)
if url.scheme not in ("", "file") or url.netloc not in ("", "localhost"):
raise AvatarError("Choisissez un fichier image local.")
path = Path(unquote(url.path) if url.scheme else value).resolve(strict=True)
info = path.stat()
if not stat.S_ISREG(info.st_mode) or not 0 < info.st_size <= MAX_SOURCE_BYTES:
raise AvatarError("L’image doit être un fichier de 12 Mio au maximum.")
return path
def render_png(source, target):
"""WHY: conversion bornée en PNG carré pour le contrat SetIconFile."""
try:
with Image.open(source) as opened:
if opened.width * opened.height > MAX_SOURCE_PIXELS:
raise AvatarError("Image trop grande.")
opened.load()
normalized = ImageOps.exif_transpose(opened)
square = ImageOps.fit(normalized.convert("RGBA"), (256, 256), method=Image.Resampling.LANCZOS)
square.save(target, format="PNG", optimize=True)
except (OSError, UnidentifiedImageError, Image.DecompressionBombError, Image.DecompressionBombWarning) as exc:
raise AvatarError("Image illisible ou trop grande.") from exc
def set_avatar(value):
source = source_path(value)
username = pwd.getpwuid(os.getuid()).pw_name
data_home = Path(os.environ.get("XDG_DATA_HOME") or Path.home() / ".local/share")
directory = data_home / "labfy-greeter"
directory.mkdir(mode=0o700, parents=True, exist_ok=True)
destination = directory / "avatar.png"
temporary = directory / f".avatar-{os.getpid()}.png"
try:
with temporary.open("xb") as output:
os.fchmod(output.fileno(), 0o600)
render_png(source, output)
output.flush()
os.fsync(output.fileno())
os.replace(temporary, destination)
# AccountsService ingère le fichier comme appelant puis publie sa copie root-owned.
bus = Gio.bus_get_sync(Gio.BusType.SYSTEM)
user_path = bus.call_sync("org.freedesktop.Accounts", "/org/freedesktop/Accounts",
"org.freedesktop.Accounts", "FindUserById",
GLib.Variant("(x)", (os.getuid(),)), GLib.VariantType.new("(o)"),
Gio.DBusCallFlags.NONE, 10000).unpack()[0]
bus.call_sync("org.freedesktop.Accounts", user_path, "org.freedesktop.Accounts.User",
"SetIconFile", GLib.Variant("(s)", (str(destination),)), None,
Gio.DBusCallFlags.NONE, 15000)
finally:
temporary.unlink(missing_ok=True)
published = system_icon(username)
if published is None:
raise AvatarError("La copie système de l’avatar est indisponible.")
return str(published)
def main(argv):
try:
if argv == ["current"]:
icon = system_icon(pwd.getpwuid(os.getuid()).pw_name)
print(json.dumps({"path": str(icon) if icon else ""}))
elif len(argv) == 2 and argv[0] == "set":
print(json.dumps({"path": set_avatar(argv[1])}))
else:
raise AvatarError("Commande d’avatar invalide.")
except (AvatarError, OSError, GLib.Error, ValueError) as exc:
# Les erreurs D-Bus ne sont pas reproduites dans l'interface utilisateur.
message = str(exc) if isinstance(exc, AvatarError) else "Impossible de modifier l’avatar."
print(json.dumps({"error": message}))
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))