""" slck: Slack Status CLI Facilitates setting Slack status text/emojis via the command-line. """ import datetime import pathlib import re import click import pydantic import slack_sdk import yaml class ProfilePayload(pydantic.BaseModel): """Profile payload sent to Slack's API.""" status_text: str = "" status_emoji: str | None = "" status_expiration: int | None = 0 class Preset(pydantic.BaseModel): """Represents a set of value used as a preset.""" text: str emoji: str | None = "" duration: str | None = "" class Configuration(pydantic.BaseModel): """Tool configuration.""" token: str presets: dict[str, Preset] | None def get_client(token: str) -> slack_sdk.WebClient: """Returns an authenticated API client.""" return slack_sdk.WebClient(token=token) def parse_duration(duration: str) -> int: """Parses duration descriptors of the form xdyhzm (x,y,z integers) into timestamps relative to present.""" duration_pattern = re.compile(r"(?P\d+d)?(?P\d+h)?(?P\d+m)?") matches = duration_pattern.search(duration) delta = datetime.timedelta() if matches.group("days"): delta += datetime.timedelta(days=int(matches.group("days").rstrip("d"))) if matches.group("hours"): delta += datetime.timedelta(hours=int(matches.group("hours").rstrip("h"))) if matches.group("minutes"): delta += datetime.timedelta(minutes=int(matches.group("minutes").rstrip("m"))) return (datetime.datetime.now() + delta).timestamp() def get_configuration(path: str) -> Configuration: """Loads configuration from file.""" conf_path = pathlib.Path(path) if not conf_path.exists(): raise RuntimeError(f"Configuration file not found: {path}") with open(conf_path, "r", encoding="utf8") as conf_file: return Configuration(**yaml.safe_load(conf_file)) @click.command() @click.option("--text", "-t", help="Status text.") @click.option("--emoji", "-e", help="Emoji attached to the status.") @click.option("--duration", "-d", help="Duration of the status.") @click.option("--preset", "-p", help="Preset for text/emoji combinations.") @click.option("--config", "-c", "config_path", help="Path to configuration.") def cli( *, text: str = None, emoji: str = None, duration: str = None, preset: str = None, config_path: str = None, ): if text is None and preset is None: raise RuntimeError( "Must specify either status text via --text/-t or a preset via --preset/-p." ) conf = get_configuration(config_path) status_text, status_emoji, status_exp = None, None, 0 if preset is not None and preset in conf.presets: preset_data = conf.presets[preset] status_text = preset_data.text if preset_data.text else status_text status_emoji = preset_data.emoji if preset_data.emoji else status_emoji status_exp = ( parse_duration(preset_data.duration) if preset_data.duration else status_exp ) elif preset is not None: raise RuntimeError(f"Unknown preset: {preset}") if text is not None: status_text = text if emoji is not None: status_emoji = emoji if duration is not None: status_exp = parse_duration(duration) payload = ProfilePayload( status_text=status_text, status_emoji=status_emoji, status_expiration=int(status_exp), ) client = get_client(conf.token) api_response = client.users_profile_set(profile=payload.model_dump()) if not api_response.get("ok", False): raise RuntimeError("Failed to set status!") def run(): """Entrypoint.""" try: cli() except Exception as e: click.echo(e)