mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-24 15:20:52 -05:00
3e7b7ab787
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.
110 lines
5.4 KiB
Markdown
110 lines
5.4 KiB
Markdown
# GitHub connector (personal, webhook-driven)
|
|
|
|
> Lives in `app/integrations/github_connector.py` (provider key
|
|
> **`github_connector`**). This is **not** the OAuth-based connector at
|
|
> `app/integrations/github.py` (provider key `github`) — both can be
|
|
> active on the same install; pick the one that suits your workflow.
|
|
|
|
A small, per-user opt-in connector that turns a GitHub repository into a
|
|
TimeTracker task feed. Uses a **personal access token** (no OAuth dance)
|
|
and an HMAC-SHA256 webhook secret to drive task creation and (optionally)
|
|
auto-start timers when an issue is assigned.
|
|
|
|
## What it does
|
|
|
|
| Event | Action |
|
|
|-------|--------|
|
|
| `issues / opened` | Creates a Task in the configured default project with `external_ref = github_issue_{n}`, mapped priority (see below), `status="todo"`. |
|
|
| `issues / assigned` | If `auto_start_timer` is on **and** the assignee's GitHub login matches a TimeTracker user's `github_username`, starts a timer for that user on the default project. |
|
|
| `issues / closed` | Marks the existing task (matched by `external_ref`) as `status="done"`. |
|
|
| `ping` | Returns 200 `{"ok": true, "message": "Webhook received"}`. |
|
|
| Anything else | 422 (unhandled event). |
|
|
|
|
A manual **Sync now** also pulls the first 50 open issues from
|
|
`GET /repos/{owner}/{repo}/issues?state=open` and creates any tasks that
|
|
don't already exist.
|
|
|
|
## Priority mapping
|
|
|
|
Labels are scanned in order and the **first match** wins:
|
|
|
|
| GitHub label (lowercase) | TimeTracker priority |
|
|
|--------------------------|----------------------|
|
|
| `bug`, `critical` | `high` |
|
|
| `enhancement` | `medium` |
|
|
| anything else | `low` |
|
|
|
|
## Configuration
|
|
|
|
Open **Integrations → GitHub** (the personal-connector card under
|
|
*Personal connectors*) and fill in:
|
|
|
|
| Field | Stored as | Required | Notes |
|
|
|-------|-----------|----------|-------|
|
|
| **Personal access token** | `integration.config.github_token` (encrypted) | Yes | Needs `repo` scope to read issues. |
|
|
| **Owner** | `integration.config.repo_owner` | Yes | e.g. `octocat` |
|
|
| **Repository** | `integration.config.repo_name` | Yes | e.g. `Hello-World` |
|
|
| **Default project** | `integration.config.default_project_id` | Yes | Pick a TimeTracker project. |
|
|
| **Auto-start timer** | `integration.config.auto_start_timer` | No | Off by default. |
|
|
| **Label filter** | `integration.config.label_filter` (lowercased) | No | If set, manual sync only imports issues that carry this label. |
|
|
| **Webhook secret** | `integration.config.webhook_secret` (encrypted) | Yes (auto-generated) | Auto-filled with `secrets.token_urlsafe(32)` on first save. |
|
|
|
|
Tokens and the webhook secret are stored encrypted at rest when
|
|
`SETTINGS_ENCRYPTION_KEY` is configured (Fernet key, see the rest of
|
|
TimeTracker's secret handling). When the key is absent the connector
|
|
falls back to plain text and logs a warning.
|
|
|
|
## Wiring the GitHub webhook
|
|
|
|
1. Open the integration card and **Save** — this generates the webhook
|
|
secret if one isn't already stored and shows the receiver URL.
|
|
2. In GitHub, go to **Repo Settings → Webhooks → Add webhook** with:
|
|
- **Payload URL:** `{base_url}/api/integrations/github/webhook`
|
|
- **Content type:** `application/json`
|
|
- **Secret:** the webhook secret from the card
|
|
- **Events:** just **Issues** (or use *Send me everything* if you
|
|
prefer — the connector responds 422 to anything it doesn't handle).
|
|
3. The first delivery is a `ping`; the connector replies 200 with
|
|
`{"ok": true, "message": "Webhook received"}`.
|
|
|
|
The receiver verifies `X-Hub-Signature-256` with `hmac.compare_digest`
|
|
against `webhook_secret` and the **raw** request body, so don't add a
|
|
proxy that rewrites the body.
|
|
|
|
## Linking a TimeTracker user to a GitHub login
|
|
|
|
`users.github_username` is the join key. The 155 Alembic migration adds
|
|
the column; set it from **Profile → Settings** or by an admin updating
|
|
the user row. Auto-start-timer events are skipped silently if no
|
|
TimeTracker user matches the GitHub assignee.
|
|
|
|
## Endpoints
|
|
|
|
| Method | Path | Auth | Purpose |
|
|
|--------|------|------|---------|
|
|
| POST | `/api/integrations/github/webhook` | Signature (HMAC-SHA256) | Receives GitHub events. |
|
|
| POST | `/api/integrations/github/sync` | `@login_required`, admin only | Manual one-shot sync. |
|
|
| POST | `/api/integrations/github/config` | `@login_required` | Save the UI form. |
|
|
| POST | `/api/integrations/github/test` | `@login_required` | Calls `GET /user` to verify the token. |
|
|
| GET | `/api/integrations/github/status` | `@login_required` | Returns the current config snapshot (without secrets). |
|
|
|
|
All HTTP calls to `api.github.com` use a 10-second timeout and are
|
|
wrapped in `try/except requests.RequestException`.
|
|
|
|
## Operational notes
|
|
|
|
- The connector is **read-only on GitHub** — it never opens issues or
|
|
posts comments.
|
|
- `external_ref` on the `tasks` table is indexed; the connector
|
|
de-duplicates by `(project_id, external_ref)` so re-deliveries of the
|
|
same `issues / opened` event are safe.
|
|
- Manual sync also writes `integration.last_sync_at` /
|
|
`last_sync_status`, surfaced on the existing Integrations health
|
|
dashboard.
|
|
- The connector degrades gracefully: if the integration row is missing
|
|
or `is_active=False`, every method returns
|
|
`{"ok": false, "error": "Integration not configured"}` without
|
|
raising.
|
|
- Tokens never appear in logs — they're truncated to `prefix...` via an
|
|
internal helper.
|