Source code for src.bot.state
# src/bot/state.py
from dataclasses import dataclass, field
from threading import Lock
from typing import Optional, Set
[docs]
@dataclass
class BotState:
"""Thread-safe per-workspace bot configuration and runtime state."""
_lock: Lock
_daily_target_time: Optional[str] = None
_active_channel: Optional[str] = None
_selected_preset: Optional[str] = None
_selected_mode: Optional[str] = None
_random_start_time: Optional[str] = None
_random_end_time: Optional[str] = None
_static_time: Optional[str] = None
_active_days: Set[str] = field(default_factory=lambda: {
"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"
})
_active_token: Optional[str] = None
_pending_topic: Optional[str] = None
_active_tags: Set[str] = field(default_factory=set) # empty = all tags allowed
_last_prompt_ts: Optional[str] = None
_reminder_sent: bool = False
_reminder_enabled: bool = False
_pending_custom_prompt: Optional[str] = None # user-authored prompt text
_user_prompt_creator_used_today: bool = False # only invite one user to create a prompt
_social_connector_used_today: bool = False # only run social connector once per day
_mentor_checkin_week: Optional[int] = None # ISO week number of last mentor check-in sent
_prompt_response_type: str = "image" # "image", "text", or "any"
_last_prompt_channel: Optional[str] = None # channel where the last prompt was posted
[docs]
def set_last_prompt_ts(self, ts: Optional[str], channel: Optional[str] = None) -> None:
"""Store the timestamp of the last prompt and reset the reminder flag."""
with self._lock:
self._last_prompt_ts = ts
self._reminder_sent = False
if channel:
self._last_prompt_channel = channel
[docs]
def get_last_prompt_ts(self) -> Optional[str]:
"""Return the Slack message timestamp of the last posted prompt."""
with self._lock:
return self._last_prompt_ts
[docs]
def get_reminder_sent(self) -> bool:
"""Return whether a reminder has already been sent for the current prompt."""
with self._lock:
return self._reminder_sent
[docs]
def set_reminder_sent(self, value: bool) -> None:
"""Mark whether a reminder has been sent for the current prompt."""
with self._lock:
self._reminder_sent = value
[docs]
def get_reminder_enabled(self) -> bool:
"""Return whether prompt reminders are enabled for this workspace."""
with self._lock:
return self._reminder_enabled
[docs]
def set_reminder_enabled(self, value: bool) -> None:
"""Enable or disable prompt reminders for this workspace."""
with self._lock:
self._reminder_enabled = value
[docs]
def set_pending_topic(self, topic: Optional[str]) -> None:
"""Queue a topic to be used as context for the next prompt."""
with self._lock:
self._pending_topic = topic
[docs]
def get_and_clear_pending_topic(self) -> Optional[str]:
"""Returns the pending topic and clears it so it only applies once."""
with self._lock:
topic = self._pending_topic
self._pending_topic = None
return topic
[docs]
def set_daily_target_time(self, t: Optional[str]) -> None:
"""Set the resolved daily target time (HH:MM) for the current day."""
with self._lock:
self._daily_target_time = t
[docs]
def set_active_channel(self, channel: Optional[str]) -> None:
"""Set the Slack channel ID where prompts will be posted."""
with self._lock:
self._active_channel = channel
[docs]
def get_active_channel(self) -> Optional[str]:
"""Return the Slack channel ID where prompts will be posted."""
with self._lock:
return self._active_channel
[docs]
def set_active_token(self, token: Optional[str]) -> None:
"""Store the Slack bot token for this workspace."""
with self._lock:
self._active_token = token
[docs]
def get_active_token(self) -> Optional[str]:
"""Return the Slack bot token for this workspace."""
with self._lock:
return self._active_token
[docs]
def get_daily_target_time(self) -> Optional[str]:
"""Return the resolved daily target time (HH:MM) for the current day."""
with self._lock:
return self._daily_target_time
[docs]
def set_selected_preset(self, value: Optional[str]) -> None:
"""Set the active scheduling preset name."""
with self._lock:
self._selected_preset = value
[docs]
def get_selected_preset(self) -> Optional[str]:
"""Return the active scheduling preset name."""
with self._lock:
return self._selected_preset
[docs]
def set_selected_mode(self, value: Optional[str]) -> None:
"""Set the scheduling mode (e.g. 'random', 'static')."""
with self._lock:
self._selected_mode = value
[docs]
def get_selected_mode(self) -> Optional[str]:
"""Return the scheduling mode (e.g. 'random', 'static')."""
with self._lock:
return self._selected_mode
[docs]
def set_random_start_time(self, value: Optional[str]) -> None:
"""Set the earliest time (HH:MM) for random prompt scheduling."""
with self._lock:
self._random_start_time = value
[docs]
def get_random_start_time(self) -> Optional[str]:
"""Return the earliest time (HH:MM) for random prompt scheduling."""
with self._lock:
return self._random_start_time
[docs]
def set_random_end_time(self, value: Optional[str]) -> None:
"""Set the latest time (HH:MM) for random prompt scheduling."""
with self._lock:
self._random_end_time = value
[docs]
def get_random_end_time(self) -> Optional[str]:
"""Return the latest time (HH:MM) for random prompt scheduling."""
with self._lock:
return self._random_end_time
[docs]
def set_static_time(self, value: Optional[str]) -> None:
"""Set the fixed daily time (HH:MM) used in static scheduling mode."""
with self._lock:
self._static_time = value
[docs]
def get_static_time(self) -> Optional[str]:
"""Return the fixed daily time (HH:MM) used in static scheduling mode."""
with self._lock:
return self._static_time
[docs]
def set_active_days(self, days: Set[str]) -> None:
"""Replace the set of weekday names on which prompts are posted."""
with self._lock:
self._active_days = days
[docs]
def get_active_days(self) -> Set[str]:
"""Return a snapshot of the weekday names on which prompts are posted."""
with self._lock:
return set(self._active_days)
[docs]
def is_today_active(self) -> bool:
"""Return True if today's weekday is in the active days set."""
from datetime import date
today = date.today().strftime("%A") # e.g. "Monday"
with self._lock:
return today in self._active_days
[docs]
def set_pending_custom_prompt(self, text: Optional[str]) -> None:
"""Queue a user-authored prompt text to be used instead of a generated one."""
with self._lock:
self._pending_custom_prompt = text
[docs]
def get_and_clear_pending_custom_prompt(self) -> Optional[str]:
"""Return the pending custom prompt text and clear it so it is used only once."""
with self._lock:
text = self._pending_custom_prompt
self._pending_custom_prompt = None
return text
[docs]
def set_user_prompt_creator_used_today(self, value: bool) -> None:
"""Mark whether a user has already been invited to create a prompt today."""
with self._lock:
self._user_prompt_creator_used_today = value
[docs]
def get_user_prompt_creator_used_today(self) -> bool:
"""Return whether a user has already been invited to create a prompt today."""
with self._lock:
return self._user_prompt_creator_used_today
[docs]
def set_social_connector_used_today(self, value: bool) -> None:
"""Mark whether the social connector flow has already run today."""
with self._lock:
self._social_connector_used_today = value
[docs]
def get_social_connector_used_today(self) -> bool:
"""Return whether the social connector flow has already run today."""
with self._lock:
return self._social_connector_used_today
[docs]
def set_mentor_checkin_week(self, week: Optional[int]) -> None:
"""Store the ISO week number when the last mentor check-in was sent."""
with self._lock:
self._mentor_checkin_week = week
[docs]
def get_mentor_checkin_week(self) -> Optional[int]:
"""Return the ISO week number when the last mentor check-in was sent."""
with self._lock:
return self._mentor_checkin_week
[docs]
def set_prompt_response_type(self, value: str) -> None:
"""Set the expected response type for prompts: 'image', 'text', or 'any'."""
with self._lock:
self._prompt_response_type = value
[docs]
def get_prompt_response_type(self) -> str:
"""Return the expected response type for prompts: 'image', 'text', or 'any'."""
with self._lock:
return self._prompt_response_type
[docs]
def set_last_prompt_channel(self, channel: Optional[str]) -> None:
"""Set the channel ID where the most recent prompt was posted."""
with self._lock:
self._last_prompt_channel = channel
[docs]
def get_last_prompt_channel(self) -> Optional[str]:
"""Return the channel ID where the most recent prompt was posted."""
with self._lock:
return self._last_prompt_channel
[docs]
def get_team_id(body: dict) -> Optional[str]:
"""Extract team_id from a Slack request body, handling both slash commands and block actions."""
return body.get("team_id") or (body.get("team") or {}).get("id")
[docs]
def create_state(default_channel: Optional[str] = None) -> BotState:
"""Create a new BotState with a fresh lock, optionally pre-setting the active channel."""
state = BotState(_lock=Lock())
if default_channel:
state.set_active_channel(default_channel)
return state
[docs]
class StateManager:
"""Registry that maps workspace team IDs to their BotState instances."""
def __init__(self):
self._states = {}
self._lock = Lock()
[docs]
def get_state(self, team_id: str) -> BotState:
"""Return the BotState for the given team, creating one if it does not exist."""
with self._lock:
if team_id not in self._states:
self._states[team_id] = BotState(_lock=Lock())
return self._states[team_id]
[docs]
def all_states(self) -> dict:
"""Return a shallow copy of the team_id → BotState mapping."""
with self._lock:
return dict(self._states)