mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-12 23:39:17 -05:00
0ec6b8e9d6
This commit implements a comprehensive refactoring of the integration system to support both global (shared) and per-user integrations, adds new integrations, and improves the overall architecture. Key changes: - Add global integrations support: most integrations are now shared across all users (Jira, Slack, GitHub, Asana, Trello, GitLab, Microsoft Teams, Outlook Calendar, Xero) - Add new integrations: GitLab, Microsoft Teams, Outlook Calendar, and Xero - Database migrations: * Migration 081: Add OAuth credential columns for all integrations to Settings model * Migration 082: Add is_global flag to Integration model and make user_id nullable - Update Integration model to support global integrations with nullable user_id - Refactor IntegrationService to handle both global and per-user integrations - Create dedicated admin setup pages for each integration - Update Trello connector to use API key setup instead of OAuth flow - Enhance all existing integrations (Jira, Slack, GitHub, Google Calendar, Asana, Trello) with global support - Update routes, templates, and services to support the new global/per-user distinction - Improve integration management UI with better separation of global vs per-user integrations - Update scheduled tasks to work with the new integration architecture
293 lines
11 KiB
Python
293 lines
11 KiB
Python
"""
|
|
Trello integration connector.
|
|
Sync boards, lists, and cards with Trello.
|
|
"""
|
|
|
|
from typing import Dict, Any, Optional, List
|
|
from datetime import datetime, timedelta
|
|
from app.integrations.base import BaseConnector
|
|
import requests
|
|
import os
|
|
import hmac
|
|
import hashlib
|
|
import base64
|
|
|
|
|
|
class TrelloConnector(BaseConnector):
|
|
"""Trello integration connector."""
|
|
|
|
display_name = "Trello"
|
|
description = "Sync boards and cards with Trello"
|
|
icon = "trello"
|
|
|
|
BASE_URL = "https://api.trello.com/1"
|
|
|
|
@property
|
|
def provider_name(self) -> str:
|
|
return "trello"
|
|
|
|
def get_authorization_url(self, redirect_uri: str, state: str = None) -> str:
|
|
"""Get Trello OAuth authorization URL."""
|
|
from app.models import Settings
|
|
|
|
settings = Settings.get_settings()
|
|
creds = settings.get_integration_credentials("trello")
|
|
api_key = creds.get("api_key") or os.getenv("TRELLO_API_KEY")
|
|
|
|
if not api_key:
|
|
raise ValueError("TRELLO_API_KEY not configured")
|
|
|
|
auth_url = "https://trello.com/1/OAuthAuthorizeToken"
|
|
|
|
params = {
|
|
"key": api_key,
|
|
"name": "TimeTracker Integration",
|
|
"response_type": "token",
|
|
"scope": "read,write",
|
|
"expiration": "never",
|
|
"redirect_uri": redirect_uri
|
|
}
|
|
|
|
query_string = "&".join([f"{k}={v}" for k, v in params.items()])
|
|
return f"{auth_url}?{query_string}"
|
|
|
|
def exchange_code_for_tokens(self, code: str, redirect_uri: str) -> Dict[str, Any]:
|
|
"""Exchange authorization code for tokens (Trello uses token directly)."""
|
|
# Trello uses token-based auth, not OAuth flow
|
|
# The token is returned directly from the authorization URL
|
|
from app.models import Settings
|
|
|
|
settings = Settings.get_settings()
|
|
creds = settings.get_integration_credentials("trello")
|
|
api_key = creds.get("api_key") or os.getenv("TRELLO_API_KEY")
|
|
|
|
if not api_key:
|
|
raise ValueError("Trello API key not configured")
|
|
|
|
# For Trello, the 'code' parameter is actually the token
|
|
token = code
|
|
|
|
# Verify token by getting user info
|
|
user_info = {}
|
|
try:
|
|
response = requests.get(
|
|
f"{self.BASE_URL}/members/me",
|
|
params={"key": api_key, "token": token}
|
|
)
|
|
if response.status_code == 200:
|
|
user_data = response.json()
|
|
user_info = {
|
|
"id": user_data.get("id"),
|
|
"username": user_data.get("username"),
|
|
"fullName": user_data.get("fullName"),
|
|
"email": user_data.get("email")
|
|
}
|
|
except Exception:
|
|
pass
|
|
|
|
return {
|
|
"access_token": token,
|
|
"refresh_token": None, # Trello tokens don't expire
|
|
"expires_at": None,
|
|
"token_type": "Bearer",
|
|
"extra_data": user_info
|
|
}
|
|
|
|
def refresh_access_token(self) -> Dict[str, Any]:
|
|
"""Refresh access token (Trello tokens don't expire)."""
|
|
# Trello tokens don't expire, so just return current token
|
|
return {
|
|
"access_token": self.credentials.access_token if self.credentials else None,
|
|
"expires_at": None
|
|
}
|
|
|
|
def test_connection(self) -> Dict[str, Any]:
|
|
"""Test connection to Trello."""
|
|
try:
|
|
from app.models import Settings
|
|
settings = Settings.get_settings()
|
|
creds = settings.get_integration_credentials("trello")
|
|
api_key = creds.get("api_key") or os.getenv("TRELLO_API_KEY")
|
|
|
|
headers = {"Authorization": f"Bearer {self.get_access_token()}"}
|
|
response = requests.get(
|
|
f"{self.BASE_URL}/members/me",
|
|
params={"key": api_key, "token": self.get_access_token()}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
user_data = response.json()
|
|
return {
|
|
"success": True,
|
|
"message": f"Connected to Trello as {user_data.get('fullName', 'Unknown')}"
|
|
}
|
|
else:
|
|
return {
|
|
"success": False,
|
|
"message": f"Connection test failed: {response.status_code}"
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
"success": False,
|
|
"message": f"Connection test failed: {str(e)}"
|
|
}
|
|
|
|
def sync_data(self, sync_type: str = "full") -> Dict[str, Any]:
|
|
"""Sync boards and cards with Trello."""
|
|
from app.models import Project, Task
|
|
from app import db
|
|
|
|
try:
|
|
from app.models import Settings
|
|
settings = Settings.get_settings()
|
|
creds = settings.get_integration_credentials("trello")
|
|
api_key = creds.get("api_key") or os.getenv("TRELLO_API_KEY")
|
|
|
|
token = self.get_access_token()
|
|
if not token or not api_key:
|
|
return {"success": False, "message": "Trello credentials not configured"}
|
|
|
|
synced_count = 0
|
|
errors = []
|
|
|
|
# Get boards
|
|
boards_response = requests.get(
|
|
f"{self.BASE_URL}/members/me/boards",
|
|
params={"key": api_key, "token": token, "filter": "open"}
|
|
)
|
|
|
|
if boards_response.status_code == 200:
|
|
boards = boards_response.json()
|
|
|
|
for board in boards:
|
|
try:
|
|
# Create or update project from board
|
|
project = Project.query.filter_by(
|
|
user_id=self.integration.user_id,
|
|
name=board.get("name")
|
|
).first()
|
|
|
|
if not project:
|
|
project = Project(
|
|
name=board.get("name"),
|
|
description=board.get("desc", ""),
|
|
user_id=self.integration.user_id,
|
|
status="active"
|
|
)
|
|
db.session.add(project)
|
|
db.session.flush()
|
|
|
|
# Store Trello board ID in metadata
|
|
if not hasattr(project, 'metadata') or not project.metadata:
|
|
project.metadata = {}
|
|
project.metadata['trello_board_id'] = board.get("id")
|
|
|
|
# Sync cards as tasks
|
|
cards_response = requests.get(
|
|
f"{self.BASE_URL}/boards/{board.get('id')}/cards",
|
|
params={"key": api_key, "token": token, "filter": "open"}
|
|
)
|
|
|
|
if cards_response.status_code == 200:
|
|
cards = cards_response.json()
|
|
|
|
for card in cards:
|
|
# Find or create task
|
|
task = Task.query.filter_by(
|
|
project_id=project.id,
|
|
name=card.get("name")
|
|
).first()
|
|
|
|
if not task:
|
|
task = Task(
|
|
project_id=project.id,
|
|
name=card.get("name"),
|
|
description=card.get("desc", ""),
|
|
status=self._map_trello_list_to_status(card.get("idList"))
|
|
)
|
|
db.session.add(task)
|
|
db.session.flush()
|
|
|
|
# Store Trello card ID in metadata
|
|
if not hasattr(task, 'metadata') or not task.metadata:
|
|
task.metadata = {}
|
|
task.metadata['trello_card_id'] = card.get("id")
|
|
|
|
synced_count += 1
|
|
except Exception as e:
|
|
errors.append(f"Error syncing board {board.get('name')}: {str(e)}")
|
|
|
|
db.session.commit()
|
|
|
|
return {
|
|
"success": True,
|
|
"synced_count": synced_count,
|
|
"errors": errors
|
|
}
|
|
|
|
except Exception as e:
|
|
return {
|
|
"success": False,
|
|
"message": f"Sync failed: {str(e)}"
|
|
}
|
|
|
|
def _map_trello_list_to_status(self, list_id: str) -> str:
|
|
"""Map Trello list to task status."""
|
|
from app.models import Settings
|
|
settings = Settings.get_settings()
|
|
creds = settings.get_integration_credentials("trello")
|
|
api_key = creds.get("api_key") or os.getenv("TRELLO_API_KEY")
|
|
token = self.get_access_token()
|
|
|
|
if not token or not api_key:
|
|
return "todo"
|
|
|
|
try:
|
|
# Fetch list name
|
|
list_response = requests.get(
|
|
f"{self.BASE_URL}/lists/{list_id}",
|
|
params={"key": api_key, "token": token}
|
|
)
|
|
|
|
if list_response.status_code == 200:
|
|
list_data = list_response.json()
|
|
list_name = list_data.get("name", "").lower()
|
|
|
|
# Map common list names to statuses
|
|
if "done" in list_name or "completed" in list_name or "closed" in list_name:
|
|
return "completed"
|
|
elif "in progress" in list_name or "doing" in list_name or "active" in list_name:
|
|
return "in_progress"
|
|
elif "todo" in list_name or "to do" in list_name or "backlog" in list_name:
|
|
return "todo"
|
|
except Exception:
|
|
pass
|
|
|
|
return "todo"
|
|
|
|
def get_config_schema(self) -> Dict[str, Any]:
|
|
"""Get configuration schema."""
|
|
return {
|
|
"fields": [
|
|
{
|
|
"name": "board_ids",
|
|
"type": "array",
|
|
"label": "Board IDs",
|
|
"description": "Trello board IDs to sync (leave empty to sync all)"
|
|
},
|
|
{
|
|
"name": "sync_direction",
|
|
"type": "select",
|
|
"label": "Sync Direction",
|
|
"options": [
|
|
{"value": "trello_to_timetracker", "label": "Trello → TimeTracker"},
|
|
{"value": "timetracker_to_trello", "label": "TimeTracker → Trello"},
|
|
{"value": "bidirectional", "label": "Bidirectional"}
|
|
],
|
|
"default": "trello_to_timetracker"
|
|
}
|
|
],
|
|
"required": []
|
|
}
|
|
|