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.
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.postMessageto 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
/ttcommand 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 vianotification_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_summaryAPScheduler 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
- Create a Slack app (https://api.slack.com/apps) — From scratch is fine.
- 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), andusers:read(optional, for nicer status output). - Slash Commands → Create New Command:
- Command:
/tt - Request URL:
{base_url}/api/integrations/slack/events - Short description: TimeTracker timer control
- Command:
- 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
challengefield immediately) if you decide to subscribe to events later. - 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 intry/except. - The notification hook in
start_timer/stop_timeris 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.