133 lines
3.7 KiB
Python
133 lines
3.7 KiB
Python
"""
|
|
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."""
|
|
durationPattern = re.compile(r"(?P<days>\d+d)?(?P<hours>\d+h)?(?P<minutes>\d+m)?")
|
|
matches = durationPattern.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)
|