from typing import Any import os import httpx # Constants RAINDROP_API_BASE = "https://api.raindrop.io/rest/v1" RAINDROP_TOKEN = os.environ.get("RAINDROP_TOKEN") async def make_raindrop_request(url: str, token: str, method: str = "GET", data: dict = None) -> dict[str, Any] | None: """Make a request to the Raindrop.io API with proper error handling.""" headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json" } async with httpx.AsyncClient() as client: try: if method.upper() == "GET": response = await client.get(url, headers=headers, timeout=30.0) elif method.upper() == "POST": response = await client.post(url, headers=headers, json=data, timeout=30.0) else: print(f"Unsupported method: {method}") return None response.raise_for_status() return response.json() except Exception as e: print(f"Error making Raindrop request: {e}") return None def format_bookmark(item: dict) -> str: """Format a Raindrop bookmark into a readable string.""" return f""" Title: {item.get('title', 'Untitled')} URL: {item.get('link', 'No URL')} Tags: {', '.join(item.get('tags', [])) or 'No tags'} Created: {item.get('created', 'Unknown date')} Description: {item.get('excerpt', 'No description available')} """ async def get_latest_feed(count: int = 10) -> str: """Get latest bookmarks from Raindrop.io feed. Args: count: Number of bookmarks to fetch (default: 10) """ url = f"{RAINDROP_API_BASE}/raindrops/0?perpage={count}&sort=-created" data = await make_raindrop_request(url, RAINDROP_TOKEN) if not data or "items" not in data: return "Unable to fetch bookmarks or no bookmarks found." if not data["items"]: return "No bookmarks found in your collection." bookmarks = [format_bookmark(item) for item in data["items"]] return "\n---\n".join(bookmarks) async def add_bookmark(url: str, title: str = "", description: str = "", tags: str = "", collection_id: int = 0) -> str: """Add a new bookmark to Raindrop.io Args: url: The URL to bookmark (required) title: Title for the bookmark (optional, will be extracted from URL if not provided) description: Description/excerpt for the bookmark (optional) tags: Comma-separated tags to apply to the bookmark (optional) collection_id: ID of the collection to add the bookmark to (default: 0 for Unsorted) """ if not url: return "Error: URL is required" raindrop_data = { "link": url, "collection": {"$id": collection_id} } if title: raindrop_data["title"] = title if description: raindrop_data["excerpt"] = description if tags: # Convert comma-separated string to list tag_list = [tag.strip() for tag in tags.split(",") if tag.strip()] raindrop_data["tags"] = tag_list endpoint = f"{RAINDROP_API_BASE}/raindrop" response = await make_raindrop_request(endpoint, RAINDROP_TOKEN, method="POST", data=raindrop_data) if not response or "item" not in response: return "Failed to add bookmark. Please check the URL and try again." return f"Bookmark successfully added:\n{format_bookmark(response['item'])}" async def search_by_tag(tag: str, collection_id: int = 0, count: int = 10, from_date: str = "", to_date: str = "") -> str: """Search for bookmarks with a specific tag in Raindrop.io with optional date range filtering Args: tag: The tag to search for (required) collection_id: ID of the collection to search in (default: 0 for all collections) count: Maximum number of bookmarks to return (default: 10) from_date: Start date in YYYY-MM-DD format (optional) to_date: End date in YYYY-MM-DD format (optional) """ if not tag: return "Error: Tag is required" # Build search filters search_filters = [f"-tags:\"{tag}\""] # Add date range filters if provided if from_date and from_date.strip(): search_filters.append(f"created>={from_date.strip()}") if to_date and to_date.strip(): search_filters.append(f"created<={to_date.strip()}") # Combine all search filters search_query = " ".join(search_filters) url = f"{RAINDROP_API_BASE}/raindrops/{collection_id}?perpage={count}&search={search_query}&sort=-created" data = await make_raindrop_request(url, RAINDROP_TOKEN) if not data or "items" not in data: return "Unable to fetch bookmarks or no bookmarks found with this tag." if not data["items"]: return f"No bookmarks found with tag '{tag}' within the specified criteria." bookmarks = [format_bookmark(item) for item in data["items"]] result_msg = f"Found {len(data['items'])} bookmarks with tag '{tag}'" if from_date or to_date: date_range = f" (Date range: {from_date or 'any'} to {to_date or 'any'})" result_msg += date_range return result_msg + ":\n\n" + "\n---\n".join(bookmarks) async def search_bookmarks(query: str, collection_id: int = 0, count: int = 10, from_date: str = "", to_date: str = "") -> str: """Search for bookmarks by keyword/text in Raindrop.io with optional date range filtering Args: query: The search term to look for in bookmarks (required) collection_id: ID of the collection to search in (default: 0 for all collections) count: Maximum number of bookmarks to return (default: 10) from_date: Start date in YYYY-MM-DD format (optional) to_date: End date in YYYY-MM-DD format (optional) """ if not query: return "Error: Search query is required" # Build search filters search_filters = [f"\"{query}\""] # Add date range filters if provided if from_date and from_date.strip(): search_filters.append(f"created>={from_date.strip()}") if to_date and to_date.strip(): search_filters.append(f"created<={to_date.strip()}") # Combine all search filters search_query = " ".join(search_filters) url = f"{RAINDROP_API_BASE}/raindrops/{collection_id}?perpage={count}&search={search_query}&sort=-created" data = await make_raindrop_request(url, RAINDROP_TOKEN) if not data or "items" not in data: return "Unable to fetch bookmarks or no bookmarks found for your search." if not data["items"]: return f"No bookmarks found matching '{query}' within the specified criteria." bookmarks = [format_bookmark(item) for item in data["items"]] result_msg = f"Found {len(data['items'])} bookmarks matching '{query}'" if from_date or to_date: date_range = f" (Date range: {from_date or 'any'} to {to_date or 'any'})" result_msg += date_range return result_msg + ":\n\n" + "\n---\n".join(bookmarks)