from __future__ import annotations import re from dataclasses import dataclass from typing import Any _SLACK_ID_RE = re.compile(r"^[UGCD][A-Z0-9]{8,}$") @dataclass class SlackTarget: raw: str target_type: str = "" target_id: str = "" @property def is_user(self) -> bool: return self.target_type == "user" @property def is_channel(self) -> bool: return self.target_type == "channel" @property def is_valid(self) -> bool: return bool(self.target_type and self.target_id) def parse_slack_target(raw: str) -> SlackTarget: raw = raw.strip() if not raw: return SlackTarget(raw=raw) mention_match = re.match(r"^<@([A-Z0-9]+)>$", raw) if mention_match: return SlackTarget(raw=raw, target_type="user", target_id=mention_match.group(1)) channel_match = re.match(r"^<#([A-Z0-9]+)\|?[^>]*>$", raw) if channel_match: return SlackTarget(raw=raw, target_type="channel", target_id=channel_match.group(1)) if _SLACK_ID_RE.match(raw): if raw.startswith("U"): return SlackTarget(raw=raw, target_type="user", target_id=raw) if raw.startswith("C") or raw.startswith("G"): return SlackTarget(raw=raw, target_type="channel", target_id=raw) if raw.startswith("D"): return SlackTarget(raw=raw, target_type="channel", target_id=raw) if "@" in raw: return SlackTarget(raw=raw, target_type="email", target_id=raw) if raw.startswith("#"): return SlackTarget(raw=raw, target_type="channel_name", target_id=raw[1:]) return SlackTarget(raw=raw, target_type="user_name", target_id=raw) def parse_slack_targets(raw: str) -> list[SlackTarget]: parts = re.split(r"[,\s]+", raw) return [parse_slack_target(p) for p in parts if p.strip()] @dataclass class BatchResolveResult: targets: list[SlackTarget] resolved: list[dict[str, Any]] = None unresolved: list[str] = None errors: list[str] = None def __post_init__(self): self.resolved = self.resolved or [] self.unresolved = self.unresolved or [] self.errors = self.errors or [] @property def count(self) -> int: return len(self.targets) @property def resolved_count(self) -> int: return len(self.resolved) @property def unresolved_count(self) -> int: return len(self.unresolved) async def resolve_targets( client, targets: list[SlackTarget], ) -> BatchResolveResult: result = BatchResolveResult(targets=targets) for target in targets: if not target.is_valid: result.unresolved.append(target.raw) continue try: if target.target_type == "user": resp = await client.users_info(user=target.target_id) if resp.get("ok"): user = resp.get("user", {}) result.resolved.append( { "raw": target.raw, "type": "user", "id": user.get("id", target.target_id), "name": user.get("name", ""), "display_name": user.get("profile", {}).get("display_name", ""), } ) else: result.unresolved.append(target.raw) elif target.target_type == "channel": resp = await client.conversations_info(channel=target.target_id) if resp.get("ok"): ch = resp.get("channel", {}) result.resolved.append( { "raw": target.raw, "type": "channel", "id": ch.get("id", target.target_id), "name": ch.get("name", ""), } ) else: result.unresolved.append(target.raw) elif target.target_type == "email": resp = await client.users_lookupByEmail(email=target.target_id) if resp.get("ok"): user = resp.get("user", {}) result.resolved.append( { "raw": target.raw, "type": "user", "id": user.get("id", ""), "name": user.get("name", ""), "display_name": user.get("profile", {}).get("display_name", ""), } ) else: result.unresolved.append(target.raw) elif target.target_type == "channel_name": found = False cursor = "" while True: resp = await client.conversations_list( types="public_channel,private_channel", cursor=cursor or None, limit=200, ) if resp.get("ok"): for ch in resp.get("channels", []): if ch.get("name") == target.target_id: result.resolved.append( { "raw": target.raw, "type": "channel", "id": ch.get("id", ""), "name": ch.get("name", ""), } ) found = True break cursor = resp.get("response_metadata", {}).get("next_cursor", "") if not cursor or found: break if not found: result.unresolved.append(target.raw) elif target.target_type == "user_name": found = False cursor = "" while True: resp = await client.users_list(cursor=cursor or None, limit=200) if resp.get("ok"): for user in resp.get("members", []): if user.get("name") == target.target_id or user.get("real_name") == target.target_id: result.resolved.append( { "raw": target.raw, "type": "user", "id": user.get("id", ""), "name": user.get("name", ""), "display_name": user.get("profile", {}).get("display_name", ""), } ) found = True break cursor = resp.get("response_metadata", {}).get("next_cursor", "") if not cursor or found: break if not found: result.unresolved.append(target.raw) except Exception as e: result.errors.append(f"{target.raw}: {e}") result.unresolved.append(target.raw) return result