commit 96ea504b10d3784395f68e006ef81c4ebb3919b0 Author: MoonDev Date: Wed May 21 07:55:46 2025 +0300 first commit diff --git a/__pycache__/callback.cpython-311.pyc b/__pycache__/callback.cpython-311.pyc new file mode 100644 index 0000000..978838f Binary files /dev/null and b/__pycache__/callback.cpython-311.pyc differ diff --git a/callback.py b/callback.py new file mode 100644 index 0000000..d98da23 --- /dev/null +++ b/callback.py @@ -0,0 +1,51 @@ +import os +import importlib +import inspect +import asyncio +from typing import Dict, Any + +# Словарь для хранения загруженных функций +_action_functions = {} + +# Загружаем все файлы из папки ./callbacks +def load_callbacks(): + callbacks_dir = "./callbacks" + if not os.path.exists(callbacks_dir): + return + + print("\nLoaded callbacks:") + for filename in os.listdir(callbacks_dir): + if filename.endswith(".py") and filename != "__init__.py": + module_name = filename[:-3] # Удаляем .py + try: + module = importlib.import_module(f"callbacks.{module_name}") + # Получаем все функции из модуля + for func_name, func in inspect.getmembers(module, inspect.isfunction): + # Сохраняем с префиксом имени файла + _action_functions[f"{module_name}.{func_name}"] = func + print(f"{module_name}.{func_name}") + # Получаем все асинхронные функции + for func_name, func in inspect.getmembers(module, inspect.iscoroutinefunction): + _action_functions[f"{module_name}.{func_name}"] = func + except Exception as e: + print(f"Error loading module {module_name}: {str(e)}") + + +# Загружаем функции при старте +load_callbacks() + +async def call_action(name: str, args: Dict[str, Any]): + """ + Вызывает функцию с заданным именем в формате filename.function_name, передавая ей аргументы. + Если функция асинхронная, использует await. + """ + if name not in _action_functions: + raise ValueError(f"Action '{name}' not found") + + func = _action_functions[name] + + # Проверяем, является ли функция асинхронной + if inspect.iscoroutinefunction(func): + return await func(args) + else: + return func(args) \ No newline at end of file diff --git a/callbacks/__pycache__/example.cpython-311.pyc b/callbacks/__pycache__/example.cpython-311.pyc new file mode 100644 index 0000000..2d4b93a Binary files /dev/null and b/callbacks/__pycache__/example.cpython-311.pyc differ diff --git a/callbacks/__pycache__/lock.cpython-311.pyc b/callbacks/__pycache__/lock.cpython-311.pyc new file mode 100644 index 0000000..f9cb454 Binary files /dev/null and b/callbacks/__pycache__/lock.cpython-311.pyc differ diff --git a/callbacks/__pycache__/media.cpython-311.pyc b/callbacks/__pycache__/media.cpython-311.pyc new file mode 100644 index 0000000..20cd019 Binary files /dev/null and b/callbacks/__pycache__/media.cpython-311.pyc differ diff --git a/callbacks/__pycache__/screen.cpython-311.pyc b/callbacks/__pycache__/screen.cpython-311.pyc new file mode 100644 index 0000000..be194e6 Binary files /dev/null and b/callbacks/__pycache__/screen.cpython-311.pyc differ diff --git a/callbacks/__pycache__/system.cpython-311.pyc b/callbacks/__pycache__/system.cpython-311.pyc new file mode 100644 index 0000000..6e7f879 Binary files /dev/null and b/callbacks/__pycache__/system.cpython-311.pyc differ diff --git a/callbacks/__pycache__/v2ray.cpython-311.pyc b/callbacks/__pycache__/v2ray.cpython-311.pyc new file mode 100644 index 0000000..2d605c6 Binary files /dev/null and b/callbacks/__pycache__/v2ray.cpython-311.pyc differ diff --git a/callbacks/__pycache__/web.cpython-311.pyc b/callbacks/__pycache__/web.cpython-311.pyc new file mode 100644 index 0000000..ae2c7ed Binary files /dev/null and b/callbacks/__pycache__/web.cpython-311.pyc differ diff --git a/callbacks/example.py b/callbacks/example.py new file mode 100644 index 0000000..4d1c0f9 --- /dev/null +++ b/callbacks/example.py @@ -0,0 +1,5 @@ +async def example_action(args): + return {"message": "Example action executed", "args": args} + +def another_action(args): + return {"message": "Another action executed synchronously", "args": args} \ No newline at end of file diff --git a/callbacks/lock.py b/callbacks/lock.py new file mode 100644 index 0000000..6a3b534 --- /dev/null +++ b/callbacks/lock.py @@ -0,0 +1,21 @@ +import ctypes +from typing import Dict, Any + +async def lock_computer(args: Dict[str, Any]) -> Dict[str, Any]: + """ + Locks the Windows computer screen using the LockWorkStation API. + Args: + args: Dictionary of arguments (not used in this implementation). + Returns: + Dictionary with status message and input arguments. + Raises: + OSError: If the lock operation fails or the platform is not Windows. + """ + try: + # Use Windows API to lock the screen + result = ctypes.WinDLL('user32.dll').LockWorkStation() + if not result: + raise OSError("Failed to lock the Windows screen") + return {"message": "Windows screen locked successfully", "args": args} + except Exception as e: + raise OSError(f"Error while locking the Windows screen: {str(e)}") \ No newline at end of file diff --git a/callbacks/media.py b/callbacks/media.py new file mode 100644 index 0000000..dc82a24 --- /dev/null +++ b/callbacks/media.py @@ -0,0 +1,109 @@ +import sys +import os +import asyncio +import comtypes +import ctypes +from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume +from ctypes import cast, POINTER +from typing import Dict, Any + +from winrt.windows.media.control import GlobalSystemMediaTransportControlsSessionManager as MediaManager + + +# Добавляем родительскую директорию в sys.path +parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +if parent_dir not in sys.path: + sys.path.append(parent_dir) + + + + +async def set_volume(args: Dict[str, Any]) -> Dict[str, Any]: + """ + Sets the Windows system volume using the Core Audio API. + + Args: + args: Dictionary containing 'level' (float from 0.0 to 1.0) for volume level. + + Returns: + Dictionary with status message and input arguments. + + Raises: + OSError: If volume setting fails or the platform is not Windows. + """ + try: + # Initialize COM + comtypes.CoInitialize() + + # Get volume level from args + level = args.get('level', 0.5) # Default to 50% if not specified + + # Validate input level + if not isinstance(level, (int, float)) or not 0.0 <= level <= 1.0: + raise ValueError("Volume level must be a float between 0.0 and 1.0") + + # Get audio endpoint interface + devices = AudioUtilities.GetSpeakers() + interface = devices.Activate(IAudioEndpointVolume._iid_, comtypes.CLSCTX_ALL, None) + volume = cast(interface, POINTER(IAudioEndpointVolume)) + + # Set volume level + volume.SetMasterVolumeLevelScalar(float(level), None) + + return { + "message": f"Volume set to {level*100:.0f}% successfully", + "args": args + } + + except Exception as e: + raise OSError(f"Error while setting volume: {str(e)}") + + finally: + # Release COM resources + comtypes.CoUninitialize() + +async def play(args): + # Получаем менеджер медиасессий + sessions = await MediaManager.request_async() + current_session = sessions.get_current_session() + + if current_session: + # Воспроизведение + await current_session.try_play_async() + else: + print("No media session is active.") + +async def pause(args): + #print(await read_config("media.yaml")) + # Получаем менеджер медиасессий + sessions = await MediaManager.request_async() + current_session = sessions.get_current_session() + + if current_session: + # пауза + await current_session.try_pause_async() + else: + print("No media session is active.") + +async def next(args): + # Получаем менеджер медиасессий + sessions = await MediaManager.request_async() + current_session = sessions.get_current_session() + + if current_session: + # вперед + await current_session.try_skip_next_async() + # await show_notification({"title":"Test","message":"test"}) + else: + print("No media session is active.") + +async def prev(args): + # Получаем менеджер медиасессий + sessions = await MediaManager.request_async() + current_session = sessions.get_current_session() + + if current_session: + # назад + await current_session.try_skip_previous_async() + else: + print("No media session is active.") \ No newline at end of file diff --git a/callbacks/screen.py b/callbacks/screen.py new file mode 100644 index 0000000..70cf0a8 --- /dev/null +++ b/callbacks/screen.py @@ -0,0 +1,28 @@ +import monitorcontrol + +def get_monitors(): + """Retrieve a list of connected monitors.""" + return monitorcontrol.get_monitors() + +def set_brightness(monitor, brightness): + """Set the brightness of a specific monitor.""" + if not 0 <= brightness <= 100: + raise ValueError("Brightness must be between 0 and 100") + with monitor: + monitor.set_luminance(brightness) + print(f"Set brightness to {brightness}%") + + +def chenge_brightness(args): + try: + level = args.get('level', 10) + monitors = get_monitors() + if not monitors: + raise OSError(f"No DDC/CI compatible monitors found.") + return + + for i, monitor in enumerate(monitors): + set_brightness(monitor, level) + + except Exception as e: + raise OSError(f"An error occurred: {e}") diff --git a/callbacks/system.py b/callbacks/system.py new file mode 100644 index 0000000..61a1bc3 --- /dev/null +++ b/callbacks/system.py @@ -0,0 +1,86 @@ +import asyncio +import psutil +import pynvml +import time +from typing import Dict, Any + +async def get_system_metrics(args: Dict[str, Any]) -> Dict[str, Any]: + """ + Retrieves system metrics including CPU usage, RAM usage, GPU usage (for NVIDIA GPUs), + system uptime, and network traffic of the primary network interface. + + Args: + args: Dictionary of arguments (not used in this implementation). + + Returns: + Dictionary with CPU usage (%), RAM used (GB), RAM total (GB), GPU usage (%), + system uptime (seconds), network traffic (Mbit/s), and input arguments. + + Raises: + OSError: If retrieving metrics fails. + """ + try: + # Get CPU usage (averaged over 0.1 seconds for responsiveness) + cpu_usage = await asyncio.to_thread(psutil.cpu_percent, interval=0.1) + + # Get RAM usage + memory = psutil.virtual_memory() + ram_used = memory.used / (1024 ** 3) # Convert bytes to GB + ram_total = memory.total / (1024 ** 3) # Convert bytes to GB + + # Get system uptime + uptime = time.time() - psutil.boot_time() # Time since system boot in seconds + + # Get network traffic for primary interface + network_traffic = 0.0 + net_io = psutil.net_io_counters(pernic=True) + primary_interface = None + max_bytes = 0 + + # Find primary network interface (with most bytes sent/received) + for interface, stats in net_io.items(): + total_bytes = stats.bytes_sent + stats.bytes_recv + if total_bytes > max_bytes: + max_bytes = total_bytes + primary_interface = interface + + if primary_interface: + # Measure traffic over 1 second + initial_stats = net_io[primary_interface] + await asyncio.sleep(1) + final_stats = psutil.net_io_counters(pernic=True)[primary_interface] + + # Calculate traffic in Mbit/s + bytes_diff = (final_stats.bytes_sent - initial_stats.bytes_sent + + final_stats.bytes_recv - initial_stats.bytes_recv) + network_traffic = (bytes_diff * 8) / (1024 ** 2) # Convert bytes to Mbit/s + + # Try to get GPU usage (NVIDIA only) + gpu_usage = 0.0 + try: + pynvml.nvmlInit() + device_count = pynvml.nvmlDeviceGetCount() + if device_count > 0: + handle = pynvml.nvmlDeviceGetHandleByIndex(0) # Use first GPU + utilization = pynvml.nvmlDeviceGetUtilizationRates(handle) + gpu_usage = utilization.gpu + pynvml.nvmlShutdown() + except pynvml.NVMLError: + gpu_usage = -1 # Indicate GPU metrics unavailable + + return { + "message": "System metrics retrieved successfully", + "metrics": { + "cpu_usage_percent": round(cpu_usage, 2), + "ram_used_gb": round(ram_used, 2), + "ram_total_gb": round(ram_total, 2), + "gpu_usage_percent": gpu_usage, + "uptime_seconds": round(uptime, 2), + "network_traffic_mbps": round(network_traffic, 2) + }, + "args": args + } + + except Exception as e: + raise OSError(f"Error while retrieving system metrics: {str(e)}") + diff --git a/callbacks/v2ray.py b/callbacks/v2ray.py new file mode 100644 index 0000000..6c00be2 --- /dev/null +++ b/callbacks/v2ray.py @@ -0,0 +1,135 @@ +import aiohttp +import sys +import os + + + +# Добавляем родительскую директорию в sys.path +parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +if parent_dir not in sys.path: + sys.path.append(parent_dir) + + +from utils.windows_toast import show_notification +from utils.config import read_config + + +class V2rayAController: + def __init__(self, host='http://localhost:2017', username='admin', password='admin'): + self.host = host + self.username = username + self.password = password + self.token = None + self.session = None + + async def authenticate(self): + """Аутентификация и получение токена""" + self.session = aiohttp.ClientSession() + auth_url = f"{self.host}/api/login" + credentials = { + 'username': self.username, + 'password': self.password + } + + try: + async with self.session.post(auth_url, json=credentials) as response: + if response.status == 200: + data = await response.json() + self.token = data.get('data').get('token') + print("V2RayA auth OK") + return True + else: + print(f"Ошибка аутентификации: {response.status}") + return False + except Exception as e: + raise OSError("Failed to authenticate v2ray") + print(f"Ошибка при аутентификации: {e}") + return False + + async def enable_proxy(self): + """Включение прокси""" + if not self.token: + print("Не выполнен вход. Пожалуйста, сначала выполните аутентификацию") + return False + + url = f"{self.host}/api/v2ray" + headers = {'Authorization': f"Bearer {self.token}"} + data = {} + + try: + async with self.session.post(url, headers=headers, json=data) as response: + if response.status == 200: + print("Прокси успешно включен") + await show_notification({"title":"✅ V2Ray proxy","message":"Прокси успешно включен"}) + return True + else: + print(f"Ошибка при включении прокси: {response.status}") + return False + except Exception as e: + raise OSError("Failed to enable proxy") + print(f"Ошибка при включении прокси: {e}") + return False + + async def disable_proxy(self): + """Выключение прокси""" + if not self.token: + print("Не выполнен вход. Пожалуйста, сначала выполните аутентификацию") + return False + + url = f"{self.host}/api/v2ray" + headers = {'Authorization': f"Bearer {self.token}"} + data = {'operation': 'stop'} + + try: + async with self.session.delete(url, headers=headers, json=data) as response: + if response.status == 200: + print("Прокси успешно выключен") + await show_notification({"title":"🔴 V2Ray proxy","message":"Прокси успешно выключен"}) + return True + else: + print(f"Ошибка при выключении прокси: {response.status}") + return False + except Exception as e: + raise OSError("Failed to disable proxy") + print(f"Ошибка при выключении прокси: {e}") + return False + + async def close(self): + """Закрытие сессии""" + if self.session: + await self.session.close() + +async def enable_vpn(args): + # Инициализация контроллера + config = await read_config("v2ray.yaml") + if config.get("username") and config.get("password"): + controller = V2rayAController( + host='http://localhost:2017', + username=config.get("username"), + password=config.get("password") + ) + # Аутентификация + if await controller.authenticate(): + await controller.enable_proxy() + + # Закрытие сессии + await controller.close() + else: + raise OSError("Config unset") +async def disable_vpn(args): + # Инициализация контроллера + config = await read_config("v2ray.yaml") + if config.get("username") and config.get("password"): + controller = V2rayAController( + host='http://localhost:2017', + username=config.get("username"), + password=config.get("password") + ) + # Аутентификация + if await controller.authenticate(): + await controller.disable_proxy() + + # Закрытие сессии + await controller.close() + else: + raise OSError("Config unset") diff --git a/callbacks/web.py b/callbacks/web.py new file mode 100644 index 0000000..63670e7 --- /dev/null +++ b/callbacks/web.py @@ -0,0 +1,39 @@ +import asyncio +import webbrowser +from typing import Dict, Any + +async def open_url(args: Dict[str, Any]) -> Dict[str, Any]: + """ + Opens a website in the default browser on Windows. + + Args: + args: Dictionary containing 'url' (string) of the website to open. + + Returns: + Dictionary with status message and input arguments. + + Raises: + OSError: If opening the website fails. + """ + try: + # Get URL from args + url = args.get('url') + if not url: + raise ValueError("URL must be provided in args") + + # Ensure URL has a scheme (http:// or https://) + if not url.startswith(('http://', 'https://')): + url = 'https://' + url + + # Open website in default browser + success = await asyncio.to_thread(webbrowser.open, url) + if not success: + raise OSError("Failed to open the website") + + return { + "message": f"Website {url} opened successfully", + "args": args + } + + except Exception as e: + raise OSError(f"Error while opening website: {str(e)}") \ No newline at end of file diff --git a/config/v2ray.yaml b/config/v2ray.yaml new file mode 100644 index 0000000..0cb0208 --- /dev/null +++ b/config/v2ray.yaml @@ -0,0 +1,2 @@ +username: admin123 +password: admin123 \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..6153638 --- /dev/null +++ b/main.py @@ -0,0 +1,39 @@ +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +from fastapi.staticfiles import StaticFiles +from fastapi.responses import HTMLResponse +from typing import Optional, Dict, Any +import json +from callback import call_action +from fastapi.responses import JSONResponse + +app = FastAPI() + +app.mount("/static", StaticFiles(directory="static"), name="static") + +class ActionArgs(BaseModel): + args: Optional[Dict[str, Any]] = None + + +@app.get("/") +async def read_index(): + with open("static/index.html", "r", encoding="utf-8") as f: + html_content = f.read() + return HTMLResponse(content=html_content, status_code=200) + +@app.post("/action/{name:path}") +async def handle_action(name: str, action_args: ActionArgs): + try: + # Вызываем функцию из callback.py, передавая имя действия и аргументы + result = await call_action(name, action_args.args or {}) + return JSONResponse(content={"status": "success", "result": result}) + except ValueError as e: + # Если функция не найдена, возвращаем 404 + raise HTTPException(status_code=404, detail=str(e)) + except Exception as e: + # Обработка остальных ошибок + raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/static/icons/cloud.png b/static/icons/cloud.png new file mode 100644 index 0000000..0029f83 Binary files /dev/null and b/static/icons/cloud.png differ diff --git a/static/icons/steam-svgrepo-com.svg b/static/icons/steam-svgrepo-com.svg new file mode 100644 index 0000000..fd9d6f6 --- /dev/null +++ b/static/icons/steam-svgrepo-com.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/icons/telegram-logo-svgrepo-com.svg b/static/icons/telegram-logo-svgrepo-com.svg new file mode 100644 index 0000000..25999d5 --- /dev/null +++ b/static/icons/telegram-logo-svgrepo-com.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/static/icons/vpn.png b/static/icons/vpn.png new file mode 100644 index 0000000..9336d45 Binary files /dev/null and b/static/icons/vpn.png differ diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..8c72c01 --- /dev/null +++ b/static/index.html @@ -0,0 +1,196 @@ + + + + + + + Dynamic Glassmorphism Tile Panel + + + + + +
+
+
+ + + + \ No newline at end of file diff --git a/test.py b/test.py new file mode 100644 index 0000000..a71dc3e --- /dev/null +++ b/test.py @@ -0,0 +1,104 @@ +import aiohttp +import asyncio +import json + +class V2rayAController: + def __init__(self, host='http://localhost:2017', username='admin', password='admin'): + self.host = host + self.username = username + self.password = password + self.token = None + self.session = None + + async def authenticate(self): + """Аутентификация и получение токена""" + self.session = aiohttp.ClientSession() + auth_url = f"{self.host}/api/login" + credentials = { + 'username': self.username, + 'password': self.password + } + + try: + async with self.session.post(auth_url, json=credentials) as response: + if response.status == 200: + data = await response.json() + self.token = data.get('data').get('token') + print("V2RayA auth OK") + return True + else: + print(f"Ошибка аутентификации: {response.status}") + return False + except Exception as e: + print(f"Ошибка при аутентификации: {e}") + return False + + async def enable_proxy(self): + """Включение прокси""" + if not self.token: + print("Не выполнен вход. Пожалуйста, сначала выполните аутентификацию") + return False + + url = f"{self.host}/api/v2ray" + headers = {'Authorization': f"Bearer {self.token}"} + data = {} + + try: + async with self.session.post(url, headers=headers, json=data) as response: + if response.status == 200: + print("Прокси успешно включен") + return True + else: + print(f"Ошибка при включении прокси: {response.status}") + return False + except Exception as e: + print(f"Ошибка при включении прокси: {e}") + return False + + async def disable_proxy(self): + """Выключение прокси""" + if not self.token: + print("Не выполнен вход. Пожалуйста, сначала выполните аутентификацию") + return False + + url = f"{self.host}/api/v2ray" + headers = {'Authorization': f"Bearer {self.token}"} + data = {'operation': 'stop'} + + try: + async with self.session.delete(url, headers=headers, json=data) as response: + if response.status == 200: + print("Прокси успешно выключен") + return True + else: + print(f"Ошибка при выключении прокси: {response.status}") + return False + except Exception as e: + print(f"Ошибка при выключении прокси: {e}") + return False + + async def close(self): + """Закрытие сессии""" + if self.session: + await self.session.close() + +async def main(): + # Инициализация контроллера + controller = V2rayAController( + host='http://localhost:2017', + username='admin123', + password='admin123' + ) + + # Аутентификация + if await controller.authenticate(): + # Примеры использования + await controller.enable_proxy() + await asyncio.sleep(20) # Пауза для демонстрации + await controller.disable_proxy() + + # Закрытие сессии + await controller.close() + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/utils/__pycache__/config.cpython-311.pyc b/utils/__pycache__/config.cpython-311.pyc new file mode 100644 index 0000000..48202f6 Binary files /dev/null and b/utils/__pycache__/config.cpython-311.pyc differ diff --git a/utils/__pycache__/windows_toast.cpython-311.pyc b/utils/__pycache__/windows_toast.cpython-311.pyc new file mode 100644 index 0000000..d3a5be6 Binary files /dev/null and b/utils/__pycache__/windows_toast.cpython-311.pyc differ diff --git a/utils/config.py b/utils/config.py new file mode 100644 index 0000000..5821554 --- /dev/null +++ b/utils/config.py @@ -0,0 +1,41 @@ +import os +import yaml +from typing import Dict, Any +import asyncio + +async def read_config(config_file): + """ + Reads and parses a YAML configuration file from the parent directory. + Args: + 'config_file' (str) with the name of the YAML file. + If not provided, defaults to 'config.yaml'. + Returns: + Dictionary with the parsed configuration and input arguments. + Raises: + OSError: If the file cannot be read or parsed. + """ + try: + # Get the config file name from args, default to 'config.yaml' + config_file = "./config/"+config_file + + # Construct path to the parent directory + parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) + config_path = os.path.join(parent_dir, config_file) + + # Check if file exists + if not os.path.exists(config_path): + raise OSError(f"Configuration file '{config_file}' not found in parent directory") + + # Read and parse YAML file + with open(config_path, 'r', encoding='utf-8') as f: + config_data = yaml.safe_load(f) + + if config_data is None: + raise OSError(f"Configuration file '{config_file}' is empty or invalid") + + return config_data + + except yaml.YAMLError as e: + raise OSError(f"Error parsing YAML file: {str(e)}") + except Exception as e: + raise OSError(f"Error reading configuration file: {str(e)}") \ No newline at end of file diff --git a/utils/windows_toast.py b/utils/windows_toast.py new file mode 100644 index 0000000..479f019 --- /dev/null +++ b/utils/windows_toast.py @@ -0,0 +1,45 @@ +from win10toast import ToastNotifier +import asyncio +from typing import Dict, Any + +async def show_notification(args: Dict[str, Any]) -> Dict[str, Any]: + """ + Displays a system notification in the Windows system tray (bottom-right corner). + Args: + args: Dictionary containing 'title' (str), 'message' (str), and optional 'duration' (int, in seconds). + Returns: + Dictionary with status message and input arguments. + Raises: + ValueError: If required 'title' or 'message' arguments are missing. + OSError: If the notification fails to display. + """ + try: + # Check for required arguments + title = args.get('title') + message = args.get('message') + if not title or not message: + raise ValueError("Both 'title' and 'message' are required in args") + + # Optional duration (default to 5 seconds) + duration = args.get('duration', 5) + if not isinstance(duration, int) or duration < 1: + raise ValueError("'duration' must be a positive integer") + + # Initialize the notifier + toaster = ToastNotifier() + + # Show notification (run in a separate thread to avoid blocking) + await asyncio.get_event_loop().run_in_executor( + None, + lambda: toaster.show_toast( + title=title, + msg=message, + duration=duration, + threaded=True + ) + ) + + return {"message": "Notification displayed successfully", "args": args} + + except Exception as e: + raise OSError(f"Error displaying notification: {str(e)}") \ No newline at end of file