Files
TimeTracker/docs/integrations/SLACK.md
T
Dries Peeters 3e7b7ab787 feat: add per-user GitHub, Google Calendar, and Slack connectors
Three new opt-in integration connectors that plug into the existing
`app/integrations/base.py:BaseConnector` pattern and the integrations
settings UI. Each connector subclasses `BaseConnector`, persists its
state inside the existing `Integration.config` JSONB (no new tables),
encrypts every secret at rest via `app/utils/secret_crypto`, and
degrades gracefully when the integration row is missing or
`is_active=False` -- every method returns
`{"ok": false, "error": "Integration not configured"}` without
raising, so the timer, exports, and dashboards keep working when a
connector is disabled or broken.

All third-party HTTP calls go through `requests` with a 10-second
timeout and a `try/except requests.RequestException`. Tokens are
never written to logs in their raw form -- only short
`xoxb-...` / `ghp_...` truncations.

GitHub connector (`app/integrations/github_connector.py`, provider
key `github_connector`):
  - Webhook receiver at `POST /api/integrations/github/webhook`
    verifies `X-Hub-Signature-256` with HMAC-SHA256 against the
    per-integration webhook secret before reading the payload.
  - Handles `issues.opened` (creates a task with
    `external_ref="github_issue_{n}"`, mapped priority and `todo`
    status), `issues.assigned` (optionally starts a timer for the
    linked TimeTracker user when `users.github_username` matches),
    `issues.closed` (marks the existing task `done`), and `ping`.
  - Manual sync (`POST /api/integrations/github/sync`, admin only)
    pulls open issues from
    `GET /repos/{owner}/{repo}/issues?state=open&per_page=50` and
    upserts tasks by `external_ref`. Optional `label_filter`.

Google Calendar connector (`app/integrations/google_calendar_connector.py`,
provider key `google_calendar_connector`):
  - OAuth2 flow at `/integrations/google/{connect,callback,disconnect}`
    using raw `requests` against
    `https://oauth2.googleapis.com/token`. Tokens (`access_token`,
    `refresh_token`, `token_expiry`) are stored encrypted in
    `Integration.config`. `client_id`/`client_secret` come from
    Flask config (`GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`) and are
    never hardcoded.
  - `_refresh_token_if_needed()` refreshes within 5 minutes of expiry
    on every API call.
  - `sync()` supports `import` / `export` / `both`:
    * import: pulls dated events from the configured `calendar_id`
      over the last `sync_days_back` days (clamped 1-30), skips
      all-day events and anything tagged `[TT]` or already linked via
      `gcal:{event_id}` in the notes of an existing `TimeEntry`.
    * export: posts completed entries created since `last_sync_at`
      back to Google as `[TT] {project} -- {task or notes}` events
      with `timeZone: "UTC"`.
  - `revoke()` calls `https://oauth2.googleapis.com/revoke` and
    wipes the stored tokens.
  - APScheduler job `google_calendar_sync` runs every 30 minutes;
    each user is wrapped in `try/except` so one broken token cannot
    block the rest.

Slack connector (`app/integrations/slack_connector.py`, provider key
`slack_connector`):
  - Webhook receiver at `POST /api/integrations/slack/events`
    verifies `X-Slack-Signature` (HMAC-SHA256 of
    `v0:{timestamp}:{body}`) and rejects requests older than 5
    minutes. Replies to Slack's URL verification handshake
    immediately.
  - Slash command `/tt` supports `start [project]` (id or
    case-insensitive partial name match against the user's allowed
    projects), `stop`, `status`, `today` (via
    `notification_service.get_today_summary_for_user`), and an
    in-place help text fallback. Every reply is ephemeral JSON so it
    fits inside Slack's 3-second budget without touching
    `response_url`.
  - `notify_timer_started` / `notify_timer_stopped` post a
    stopwatch/checkmark message to the configured channel. Wired
    into both the page route (`app/routes/timer.py`) and the JSON
    API (`app/routes/api.py`) as a fire-and-forget hook: the import
    + call are wrapped in `try/except` and only log at `DEBUG` on
    failure, so Slack outages can't slow down the timer flow.
  - `post_daily_summary` posts a daily roll-up; APScheduler job
    `slack_daily_summary` runs every 30 minutes and matches each
    user's configured `HH:MM` against the window.

Plumbing and storage:
  - New blueprint `app/routes/integrations_webhooks.py` registers
    the webhook receivers (`csrf.exempt`, signature-verified) plus a
    uniform `config`/`status`/`test` API surface
    (`/api/integrations/{github,google,slack}/{config,status,test}`)
    used by the settings UI. Optional-registered in
    `app/blueprint_registry.py`.
  - Alembic revision `155_add_integration_columns`:
    * `users.github_username` (String(100), nullable) - GitHub login
      join key for the assignment auto-start-timer flow.
    * `tasks.external_ref` (String(200), nullable, indexed) -
      canonical external id for connector-created tasks; the new
      index lets webhook receivers de-duplicate cheaply.
    Both columns are added defensively (inspector-checked) so the
    migration is safe to re-run.
  - New cards in `app/templates/integrations/_connector_cards.html`
    (included by `templates/integrations/list.html`) drive the
    Personal connectors UI -- Tailwind CSS only, vanilla JS, per-card
    status fetch, save, test, and sync actions.

Documentation:
  - `docs/integrations/README.md` indexes all built-in connectors.
  - `docs/integrations/GITHUB_CONNECTOR.md`,
    `docs/integrations/GOOGLE_CALENDAR.md`, and
    `docs/integrations/SLACK.md` cover setup, OAuth/webhook wiring,
    config fields, endpoints, and operational notes for each
    connector.
  - `docs/api/REST_API.md` lists the new endpoints under a new
    "Personal integration connectors" subsection.
  - `CHANGELOG.md` notes the feature under the [Unreleased] section.

`LLMService`, `TimeTrackingService`, `ForecastService`, and the
`Integration` model schema are intentionally untouched -- only
`users` and `tasks` gain columns via migration.
2026-05-15 10:45:30 +02:00

5.3 KiB

Slack connector

Lives in app/integrations/slack_connector.py (provider key slack_connector). Per-user, opt-in. Posts notifications when timers start/stop, handles /tt slash commands, and (optionally) posts a daily summary at a chosen local time.

What it does

  • Timer notifications — Fires chat.postMessage to the configured channel when the linked user starts or stops a timer. Wired into both the page route (app/routes/timer.py) and the JSON API (app/routes/api.py) as a fire-and-forget hook — failures only emit a debug log; they never break the timer flow.
  • Slash commands — A single /tt command supports:
    • /tt start [project] — starts a timer (project is matched by id or case-insensitive partial name).
    • /tt stop — stops the running timer and replies with the duration.
    • /tt status — reports the current timer.
    • /tt today — reports today's hours via notification_service.get_today_summary_for_user.
    • Anything else — returns the in-place help text. Every reply is an ephemeral JSON response, so it's only visible to the user who invoked the command. The endpoint returns within Slack's 3-second budget.
  • Daily summary — Optional once-a-day post at a user-configured local time, driven by the slack_daily_summary APScheduler job (every 30 minutes; the connector matches the configured time against the window).

Configuration

Open Integrations → Slack (under Personal connectors).

Field Stored as Notes
Bot token integration.config.bot_token (encrypted) xoxb-... from your Slack app's OAuth & Permissions page.
Signing secret integration.config.signing_secret (encrypted) From Basic Information; used to verify slash commands.
Channel ID integration.config.channel_id Either a public channel like #general or the raw ID (C1234567890). The bot must be in the channel.
Notify on start integration.config.notify_on_start Boolean, default true.
Notify on stop integration.config.notify_on_stop Boolean, default true.
Daily summary integration.config.daily_summary Toggle.
Daily summary time integration.config.daily_summary_time HH:MM (24h, user's local time). Default 18:00.
Slack user ID integration.config.linked_slack_user_id Required for slash commands. Looks like U0ABC1234.

The connector degrades gracefully when not configured: if the integration row is missing or is_active=False, all notify_for_user(...) / post_daily_summary(...) calls quietly return {"ok": false, "error": "Integration not configured"}.

Slack app setup

  1. Create a Slack app (https://api.slack.com/apps) — From scratch is fine.
  2. OAuth & Permissions → Bot Token Scopes add at least: chat:write, chat:write.public (so the bot can post in channels it hasn't been invited to), commands (for the slash command), and users:read (optional, for nicer status output).
  3. Slash Commands → Create New Command:
    • Command: /tt
    • Request URL: {base_url}/api/integrations/slack/events
    • Short description: TimeTracker timer control
  4. Event Subscriptions is optional — the connector only needs the slash command URL today, but the same endpoint will respond to Slack's URL verification handshake (returns the challenge field immediately) if you decide to subscribe to events later.
  5. Install the app to your workspace and copy the Bot User OAuth Token + Signing Secret into the TimeTracker card.

Endpoints

Method Path Auth Purpose
POST /api/integrations/slack/events HMAC-SHA256 signature Slash command + URL verification handler.
POST /api/integrations/slack/config @login_required Save the UI form.
POST /api/integrations/slack/test @login_required Posts a test message to the configured channel via chat.postMessage.
GET /api/integrations/slack/status @login_required Returns the current config snapshot (no secrets) plus connection state.

The /events endpoint is csrf.exempt and rejects (401) any request whose X-Slack-Request-Timestamp is more than 5 minutes old or whose X-Slack-Signature doesn't match the HMAC-SHA256 of v0:{timestamp}:{raw_body}.

Notification format

Timer start:

:stopwatch: *{user.display_name or username}* started a timer
 *Project:* {project_name}{ — task_name if any}
 *Started at:* {HH:MM}

Timer stop:

:white_check_mark: *{user.display_name}* stopped a timer
 *Project:* {project_name}
 *Duration:* {duration}
 *Billable:* Yes|No

Daily summary:

:bar_chart: *Daily summary for {user.display_name}*
 *Hours logged:* {hours}h across {projects} projects
 Have a great evening!

Operational notes

  • Tokens never appear in logs — only xoxb-... truncations.
  • All Slack Web API calls (chat.postMessage, auth.test) use a 10-second timeout and are wrapped in try/except.
  • The notification hook in start_timer / stop_timer is fire-and-forget; Slack outages won't slow down the UI.
  • The same Slack app can be installed by multiple TimeTracker users — each user gets their own integration row, channel, and command-user binding.