Skip to main content
Glama
ticktick_rest_api.py•26 kB
#!/usr/bin/env python3 """ TickTick REST API Client - Pure OAuth Implementation Direct REST API integration with TickTick using official Open API. No hacky libraries, just clean HTTP requests with OAuth 2.0. """ import os import json import webbrowser from datetime import datetime, timedelta from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import parse_qs, urlparse, urlencode import requests from dotenv import load_dotenv # Load environment variables load_dotenv() class OAuthCallbackHandler(BaseHTTPRequestHandler): """HTTP server handler to receive OAuth callback""" def do_GET(self): """Handle OAuth callback redirect""" query = urlparse(self.path).query params = parse_qs(query) if 'code' in params: self.server.auth_code = params['code'][0] self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(b""" <html> <body> <h1>Authorization Successful!</h1> <p>You can close this window and return to the terminal.</p> </body> </html> """) else: self.send_response(400) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(b"<html><body><h1>Authorization Failed</h1></body></html>") def log_message(self, format, *args): """Suppress server logs""" pass class TickTickClient: """TickTick REST API Client using OAuth 2.0""" BASE_URL = "https://api.ticktick.com/open/v1" AUTH_URL = "https://ticktick.com/oauth/authorize" TOKEN_URL = "https://ticktick.com/oauth/token" def __init__(self, client_id, client_secret, redirect_uri="http://127.0.0.1:8080"): self.client_id = client_id self.client_secret = client_secret self.redirect_uri = redirect_uri self.access_token = None self.refresh_token = None self.token_file = ".ticktick-token.json" # Try to load existing token self._load_token() def _load_token(self): """Load existing OAuth token from file""" if os.path.exists(self.token_file): try: with open(self.token_file, 'r') as f: data = json.load(f) self.access_token = data.get('access_token') self.refresh_token = data.get('refresh_token') print(f"āœ“ Loaded existing OAuth token from {self.token_file}") return True except Exception as e: print(f"Warning: Could not load token file: {e}") return False def _save_token(self, token_data): """Save OAuth token to file""" with open(self.token_file, 'w') as f: json.dump(token_data, f, indent=2) print(f"āœ“ Saved OAuth token to {self.token_file}") def authorize(self): """Perform OAuth 2.0 authorization flow""" print("\n" + "=" * 60) print("OAuth 2.0 Authorization") print("=" * 60) # Build authorization URL auth_params = { 'client_id': self.client_id, 'redirect_uri': self.redirect_uri, 'response_type': 'code', 'scope': 'tasks:read tasks:write', 'state': 'health_marriage_tracker' } auth_url = f"{self.AUTH_URL}?{urlencode(auth_params)}" print("\n1. Opening browser for authorization...") print(f" URL: {auth_url}\n") webbrowser.open(auth_url) print("2. Starting local callback server on http://127.0.0.1:8080") print(" Waiting for authorization...\n") # Start local HTTP server to receive callback server = HTTPServer(('127.0.0.1', 8080), OAuthCallbackHandler) server.auth_code = None server.timeout = 120 # 2 minute timeout # Wait for callback server.handle_request() if not server.auth_code: raise Exception("Authorization failed - no code received") print("āœ“ Authorization code received!\n") # Exchange code for token print("3. Exchanging authorization code for access token...") token_data = { 'client_id': self.client_id, 'client_secret': self.client_secret, 'code': server.auth_code, 'grant_type': 'authorization_code', 'redirect_uri': self.redirect_uri } response = requests.post( self.TOKEN_URL, data=token_data, headers={'Content-Type': 'application/x-www-form-urlencoded'} ) if response.status_code != 200: raise Exception(f"Token exchange failed: {response.status_code} - {response.text}") token_response = response.json() self.access_token = token_response['access_token'] self.refresh_token = token_response.get('refresh_token') # Save token self._save_token(token_response) print("āœ“ Access token obtained and saved!\n") print("=" * 60) def _request(self, method, endpoint, **kwargs): """Make authenticated API request""" if not self.access_token: raise Exception("Not authenticated - call authorize() first") url = f"{self.BASE_URL}/{endpoint}" headers = kwargs.pop('headers', {}) headers['Authorization'] = f"Bearer {self.access_token}" response = requests.request(method, url, headers=headers, **kwargs) if response.status_code == 401: # Token expired - try to refresh if self.refresh_token: print("Access token expired, refreshing...") self._refresh_token() # Retry request with new token headers['Authorization'] = f"Bearer {self.access_token}" response = requests.request(method, url, headers=headers, **kwargs) else: raise Exception("Access token expired and no refresh token available") return response def _refresh_token(self): """Refresh the access token""" token_data = { 'client_id': self.client_id, 'client_secret': self.client_secret, 'refresh_token': self.refresh_token, 'grant_type': 'refresh_token' } response = requests.post( self.TOKEN_URL, data=token_data, headers={'Content-Type': 'application/x-www-form-urlencoded'} ) if response.status_code != 200: raise Exception(f"Token refresh failed: {response.status_code} - {response.text}") token_response = response.json() self.access_token = token_response['access_token'] self.refresh_token = token_response.get('refresh_token', self.refresh_token) self._save_token(token_response) print("āœ“ Access token refreshed") # Project methods def get_projects(self): """Get all projects""" response = self._request('GET', 'project') response.raise_for_status() return response.json() def create_project(self, name, color=None): """Create a new project""" data = {'name': name} if color: data['color'] = color response = self._request('POST', 'project', json=data) response.raise_for_status() return response.json() # Task methods def get_tasks(self): """Get all tasks""" response = self._request('GET', 'task') response.raise_for_status() return response.json() def create_task(self, title, **kwargs): """ Create a new task Args: title (str): Task title **kwargs: Optional fields: - projectId (str): Project ID - content (str): Task description - startDate (str): ISO 8601 format - dueDate (str): ISO 8601 format - timeZone (str): IANA timezone - priority (int): 0, 1, 3, or 5 - isAllDay (bool): All-day task - repeat (str): RRULE format - reminders (list): List of TRIGGER strings - items (list): Checklist items """ data = {'title': title} data.update(kwargs) response = self._request('POST', 'task', json=data) response.raise_for_status() return response.json() def complete_task(self, task_id): """Mark task as complete""" response = self._request('POST', f'task/{task_id}/complete') response.raise_for_status() return response.json() def delete_task(self, project_id, task_id): """Delete a task""" response = self._request('DELETE', f'task/{project_id}/{task_id}') response.raise_for_status() return True class HealthMarriageTracker: """Health & Marriage tracking system using TickTick REST API""" def __init__(self): # Get credentials from environment client_id = os.getenv('TICKTICK_CLIENT_ID') client_secret = os.getenv('TICKTICK_CLIENT_SECRET') redirect_uri = os.getenv('TICKTICK_REDIRECT_URI', 'http://127.0.0.1:8080') if not client_id or not client_secret: raise ValueError( "Required environment variables:\n" " TICKTICK_CLIENT_ID\n" " TICKTICK_CLIENT_SECRET\n" "Get these from: https://developer.ticktick.com/manage" ) self.client = TickTickClient(client_id, client_secret, redirect_uri) # Ensure we're authenticated if not self.client.access_token: print("No existing token found. Starting OAuth flow...\n") self.client.authorize() self.projects = {} def setup_complete_system(self): """Create entire tracking system in TickTick""" print("\n" + "=" * 60) print("Setting up Health & Marriage Tracking System") print("=" * 60) # Create projects print("\nCreating projects...") self.projects['Marriage'] = self.client.create_project("Marriage", "#E91E63") self.projects['Health'] = self.client.create_project("Health", "#4CAF50") self.projects['Milestones'] = self.client.create_project("Milestones", "#FF9800") print("āœ“ Projects created") # Create marriage tasks print("\nCreating Marriage tasks...") self._create_marriage_tasks() # Create health tasks print("\nCreating Health tasks...") self._create_health_tasks() # Create milestone tasks print("\nCreating Milestone tasks...") self._create_milestone_tasks() print("\n" + "=" * 60) print("āœ“ Setup Complete!") print("=" * 60) print("\nYour TickTick is now configured with:") print(" • Marriage project with weekly check-ins") print(" • Health project with daily/weekly tracking") print(" • Milestone project with 30/90/180-day targets") print("\nOpen TickTick to start tracking!") def _create_marriage_tasks(self): """Create marriage-related tasks - Week 1 specific actions""" project_id = self.projects['Marriage']['id'] # Get next occurrence of each day today = datetime.now() # Monday: Download app next_monday = self._get_next_weekday(0) # 0 = Monday self.client.create_task( title="Download Blueheart App", content="""Download Blueheart from App Store - Research-backed intimacy app - Based on Sensate Focus therapy (gold standard) - 83% effective in clinical studies - Addresses body image, communication, desire mismatches Why: Science shows structured intimacy reduces cortisol (stress) and increases connection.""", projectId=project_id, dueDate=next_monday.strftime("%Y-%m-%dT09:00:00+0000"), timeZone="America/Los_Angeles", priority=5 ) print(" āœ“ Download Blueheart App (Monday)") # Tuesday: Pitch to Nicole next_tuesday = self._get_next_weekday(1) # 1 = Tuesday self.client.create_task( title="Pitch Blueheart to Nicole", content="""Have the conversation: "I found a research-backed intimacy app called Blueheart. It's like couples therapy on your phone - guides you through timed touch exercises. Science shows it reduces stress and increases connection. Want to try one 30-minute session together this week?" Be ready to: - Show her the app - Explain it's structured/guided (not awkward guessing) - Emphasize: helps with body confidence and communication - No pressure - just one session to try Goal: Schedule a specific day/time for the session.""", projectId=project_id, dueDate=next_tuesday.strftime("%Y-%m-%dT19:00:00+0000"), timeZone="America/Los_Angeles", priority=5 ) print(" āœ“ Pitch Blueheart to Nicole (Tuesday)") # Friday: Do Blueheart session (placeholder - adjust day as needed) next_friday = self._get_next_weekday(4) # 4 = Friday self.client.create_task( title="Blueheart Guided Session (30 min)", content="""Do the first Blueheart guided session together: - 30 minutes, phones off, no interruptions - Let the app guide you - don't overthink it - Focus on being present, not performance - Goal: Connection, not orgasm Remember: - This is an experiment - Awkward is normal the first time - Science shows this reduces stress for both of you - You're rebuilding foundation together Adjust this task's date to whatever day you and Nicole agreed on.""", projectId=project_id, dueDate=next_friday.strftime("%Y-%m-%dT20:00:00+0000"), timeZone="America/Los_Angeles", priority=5 ) print(" āœ“ Blueheart Session (Friday - adjust as needed)") # Sunday: Review next_sunday = self._get_next_sunday(20, 0) # 8pm self.client.create_task( title="Week 1 Marriage Review", content="""Debrief the week: - Did you do the Blueheart session? - How did it feel? Awkward? Good? Weird? - Did Nicole feel safe/comfortable? - Want to try again next week? No judgment - first time is always weird. Track: - Connection this week (1-10): ___ - Intimacy this week: ___ times - What worked: ___ - What to adjust: ___ Next week: Another Blueheart session or different approach?""", projectId=project_id, dueDate=next_sunday.strftime("%Y-%m-%dT20:00:00+0000"), timeZone="America/Los_Angeles", priority=3 ) print(" āœ“ Week 1 Marriage Review (Sunday)") def _create_health_tasks(self): """Create health-related tasks - Week 1 spine-safe plan""" project_id = self.projects['Health']['id'] # Monday: Strength/Stretch next_monday = self._get_next_weekday(0) self.client.create_task( title="Monday: 30 min Strength/Stretch", content="""SPINE-SAFE ROUTINE: Warm-up (5 min): - Light movement, arm circles, gentle twists Stretches (15 min): - Cat/cow stretch: 10 reps (~2 min) - Child's pose: Hold 60 seconds - Hip flexor stretch: 30 sec each side - Hamstring stretch: 30 sec each side - Spinal twist: 30 sec each side - Piriformis stretch: 30 sec each side - Lower back stretch: 60 seconds - Repeat circuit 2x Strength (10 min): - Bodyweight squats: 3 sets of 10 - Plank: 3 sets, 20-30 sec each - Bird dogs: 3 sets of 10 each side - Glute bridges: 3 sets of 12 - Rest 30 sec between sets Track: How does spine feel after? (1-10)""", projectId=project_id, dueDate=next_monday.strftime("%Y-%m-%dT07:00:00+0000"), timeZone="America/Los_Angeles", priority=5 ) print(" āœ“ Monday: Strength/Stretch") # Tuesday: Rest/Walk next_tuesday = self._get_next_weekday(1) self.client.create_task( title="Tuesday: Rest or Light Walk", content="""Optional: 20-minute easy walk - Listen to your body - If spine feels tight, walk - If tired, rest completely - Track mood: ___/10""", projectId=project_id, dueDate=next_tuesday.strftime("%Y-%m-%dT07:00:00+0000"), timeZone="America/Los_Angeles", priority=1 ) print(" āœ“ Tuesday: Rest/Walk") # Wednesday: Cycling next_wednesday = self._get_next_weekday(2) self.client.create_task( title="Wednesday: Cycling 45 min", content="""Indoor trainer session: - 5 min warm-up (easy spin) - 35 min Zone 2 (conversational pace) - 5 min cool down Zone 2 check: Can you talk in full sentences? Track: - Mood after: ___/10 - Spine feel: ___/10""", projectId=project_id, dueDate=next_wednesday.strftime("%Y-%m-%dT07:00:00+0000"), timeZone="America/Los_Angeles", priority=5 ) print(" āœ“ Wednesday: Cycling") # Thursday: Strength/Stretch next_thursday = self._get_next_weekday(3) self.client.create_task( title="Thursday: 30 min Strength/Stretch", content="""SPINE-SAFE ROUTINE (same as Monday): Warm-up (5 min): - Light movement, arm circles, gentle twists Stretches (15 min): - Cat/cow stretch: 10 reps (~2 min) - Child's pose: Hold 60 seconds - Hip flexor stretch: 30 sec each side - Hamstring stretch: 30 sec each side - Spinal twist: 30 sec each side - Piriformis stretch: 30 sec each side - Lower back stretch: 60 seconds - Repeat circuit 2x Strength (10 min): - Bodyweight squats: 3 sets of 10 - Plank: 3 sets, 20-30 sec each - Bird dogs: 3 sets of 10 each side - Glute bridges: 3 sets of 12 - Rest 30 sec between sets Track: How does spine feel after? (1-10)""", projectId=project_id, dueDate=next_thursday.strftime("%Y-%m-%dT07:00:00+0000"), timeZone="America/Los_Angeles", priority=5 ) print(" āœ“ Thursday: Strength/Stretch") # Friday: Rest/Walk next_friday = self._get_next_weekday(4) self.client.create_task( title="Friday: Rest or Light Walk", content="""Optional: 20-minute easy walk - Listen to your body - If spine feels tight, walk - If tired, rest completely - Track mood: ___/10""", projectId=project_id, dueDate=next_friday.strftime("%Y-%m-%dT07:00:00+0000"), timeZone="America/Los_Angeles", priority=1 ) print(" āœ“ Friday: Rest/Walk") # Saturday: Cycling next_saturday = self._get_next_weekday(5) self.client.create_task( title="Saturday: Cycling 45 min", content="""Indoor trainer session: - 5 min warm-up (easy spin) - 35 min Zone 2 (conversational pace) - 5 min cool down Zone 2 check: Can you talk in full sentences? Track: - Mood after: ___/10 - Spine feel: ___/10""", projectId=project_id, dueDate=next_saturday.strftime("%Y-%m-%dT07:00:00+0000"), timeZone="America/Los_Angeles", priority=5 ) print(" āœ“ Saturday: Cycling") # Sunday: Weekly Check-in next_sunday = self._get_next_sunday(8, 0) self.client.create_task( title="Sunday: Weekly Health Check-in", content="""Morning weigh-in: - Same time, same scale - Weight: _____ - Track trend, not daily fluctuation Week 1 Review: - Exercise days completed: ___/6 planned - 2x cycling? Yes/No - 2x strength/stretch? Yes/No - Average mood: ___/10 - Spine feeling: Better/Same/Worse - Energy level: ___/10 What worked: What to adjust for Week 2: Goal: Consistency over intensity. Did you show up?""", projectId=project_id, dueDate=next_sunday.strftime("%Y-%m-%dT08:00:00+0000"), timeZone="America/Los_Angeles", priority=3 ) print(" āœ“ Sunday: Weekly Health Check-in") def _create_milestone_tasks(self): """Create milestone tracking tasks""" project_id = self.projects['Milestones']['id'] # 30-day milestone due_30 = (datetime.now() + timedelta(days=30)).strftime("%Y-%m-%dT09:00:00+0000") self.client.create_task( title="30-Day Milestone Check", content="""30-Day Targets: Marriage: - [ ] 4 consecutive marriage check-ins - [ ] Intimacy: 2x/week average - [ ] Connection score: 6 → 7 Health: - [ ] Weight: -5 lbs (230 → 225) - [ ] Exercise: 20/30 days - [ ] Mood: Average 6+""", projectId=project_id, dueDate=due_30, timeZone="America/Los_Angeles", priority=5 ) print(" āœ“ 30-Day Milestone") # 90-day milestone due_90 = (datetime.now() + timedelta(days=90)).strftime("%Y-%m-%dT09:00:00+0000") self.client.create_task( title="90-Day Milestone Check", content="""90-Day Targets: Marriage: - [ ] 12 consecutive check-ins - [ ] Marriage quality: 8/10 - [ ] Depression days: <5/month Health: - [ ] Weight: -15 lbs (230 → 215) - [ ] Run 5K without stopping - [ ] Exercise habit established""", projectId=project_id, dueDate=due_90, timeZone="America/Los_Angeles", priority=5 ) print(" āœ“ 90-Day Milestone") # 180-day vision due_180 = (datetime.now() + timedelta(days=180)).strftime("%Y-%m-%dT09:00:00+0000") self.client.create_task( title="6-Month Vision Check", content="""6-Month Vision: - [ ] Weight: 195 lbs - [ ] 10K capability restored - [ ] Marriage: Consistent 8+/10 - [ ] Aging together, not alone - [ ] Stress managed proactively""", projectId=project_id, dueDate=due_180, timeZone="America/Los_Angeles", priority=5 ) print(" āœ“ 6-Month Vision") def _get_next_sunday(self, hour=0, minute=0): """Get next Sunday at specified time""" today = datetime.now() days_until_sunday = (6 - today.weekday()) % 7 if days_until_sunday == 0 and today.hour >= hour: days_until_sunday = 7 next_sunday = today + timedelta(days=days_until_sunday) return next_sunday.replace(hour=hour, minute=minute, second=0, microsecond=0) def _get_next_weekday(self, weekday, hour=7, minute=0): """Get next occurrence of specified weekday (0=Mon, 6=Sun)""" today = datetime.now() days_ahead = weekday - today.weekday() if days_ahead <= 0: # Target day already happened this week days_ahead += 7 next_day = today + timedelta(days=days_ahead) return next_day.replace(hour=hour, minute=minute, second=0, microsecond=0) def view_status(self): """View current tasks and projects""" print("\n" + "=" * 60) print("Current Status") print("=" * 60) projects = self.client.get_projects() tasks = self.client.get_tasks() # Group tasks by project by_project = {} for task in tasks: pid = task.get('projectId', 'inbox') if pid not in by_project: by_project[pid] = [] by_project[pid].append(task) # Display for project in projects: pid = project['id'] pname = project['name'] ptasks = by_project.get(pid, []) print(f"\n{pname} ({len(ptasks)} tasks):") for task in ptasks[:5]: # Show first 5 status = "āœ“" if task.get('status') == 2 else "ā—‹" print(f" {status} {task['title']}") def main(): import argparse parser = argparse.ArgumentParser( description="Health & Marriage TickTick Tracking System (REST API)" ) parser.add_argument( '--setup', action='store_true', help='Initial setup - create all projects and tasks' ) parser.add_argument( '--status', action='store_true', help='View current status' ) parser.add_argument( '--auth', action='store_true', help='Test authentication only' ) parser.add_argument( '--test', action='store_true', help='Create one test task to verify API works' ) args = parser.parse_args() try: tracker = HealthMarriageTracker() if args.setup: tracker.setup_complete_system() elif args.status: tracker.view_status() elif args.auth: print("\nāœ“ Authentication successful!") print(f"Access token: {tracker.client.access_token[:20]}...") elif args.test: print("\n" + "=" * 60) print("Creating Test Task") print("=" * 60) # Create one simple task task = tracker.client.create_task( title="šŸŽÆ Test Task - Health & Marriage Tracker", content="If you can see this task in TickTick, the API is working perfectly!", priority=1 ) print(f"\nāœ“ Test task created successfully!") print(f" Task ID: {task['id']}") print(f" Title: {task['title']}") print(f"\nšŸ‘‰ Check your TickTick Inbox - you should see the test task!") print("=" * 60) else: parser.print_help() except Exception as e: print(f"\nāŒ Error: {e}") import traceback traceback.print_exc() if __name__ == "__main__": main()

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kbadinger/ticktickmcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server