|
46 | 46 | SEARXNG_URL = os.getenv("KHOJ_SEARXNG_URL") |
47 | 47 | # Exa API credentials |
48 | 48 | EXA_API_KEY = os.getenv("EXA_API_KEY") |
| 49 | +# Xquik API credentials (X/Twitter search) |
| 50 | +XQUIK_API_KEY = os.getenv("XQUIK_API_KEY") |
49 | 51 |
|
50 | 52 | # Whether to automatically read web pages from search results |
51 | 53 | AUTO_READ_WEBPAGE = is_env_var_true("KHOJ_AUTO_READ_WEBPAGE") |
@@ -114,6 +116,9 @@ async def search_online( |
114 | 116 | if SEARXNG_URL: |
115 | 117 | search_engine = "Searxng" |
116 | 118 | search_engines.append((search_engine, search_with_searxng)) |
| 119 | + if XQUIK_API_KEY: |
| 120 | + search_engine = "Xquik" |
| 121 | + search_engines.append((search_engine, search_with_xquik)) |
117 | 122 |
|
118 | 123 | if send_status_func: |
119 | 124 | subqueries_str = "\n- " + "\n- ".join(subqueries) |
@@ -358,6 +363,70 @@ async def search_with_searxng(query: str, location: LocationData) -> Tuple[str, |
358 | 363 | return query, {} |
359 | 364 |
|
360 | 365 |
|
| 366 | +async def search_with_xquik(query: str, location: LocationData) -> Tuple[str, Dict[str, List[Dict]]]: |
| 367 | + """ |
| 368 | + Search X/Twitter using Xquik API. |
| 369 | + Returns real-time social media perspectives, expert opinions, and community sentiment. |
| 370 | +
|
| 371 | + Args: |
| 372 | + query: The search query string |
| 373 | + location: Location data (unused — X/Twitter search is global) |
| 374 | +
|
| 375 | + Returns: |
| 376 | + Tuple containing the original query and a dictionary of search results |
| 377 | + """ |
| 378 | + xquik_api_url = "https://xquik.com/api/v1/x/tweets/search" |
| 379 | + headers = {"X-API-Key": XQUIK_API_KEY, "Accept": "application/json"} |
| 380 | + params = {"q": query, "limit": "10", "queryType": "Top"} |
| 381 | + |
| 382 | + async with aiohttp.ClientSession() as session: |
| 383 | + try: |
| 384 | + async with session.get( |
| 385 | + xquik_api_url, headers=headers, params=params, timeout=WEBPAGE_REQUEST_TIMEOUT |
| 386 | + ) as response: |
| 387 | + if response.status != 200: |
| 388 | + error_text = await response.text() |
| 389 | + logger.error(f"Xquik search failed: {error_text[:200]}") |
| 390 | + return query, {} |
| 391 | + |
| 392 | + response_json = await response.json() |
| 393 | + tweets = response_json.get("tweets", []) |
| 394 | + |
| 395 | + if is_none_or_empty(tweets): |
| 396 | + return query, {} |
| 397 | + |
| 398 | + organic_results = [] |
| 399 | + for tweet in tweets: |
| 400 | + author = tweet.get("author", {}) |
| 401 | + username = author.get("username", "unknown") |
| 402 | + text = tweet.get("text", "") |
| 403 | + tweet_id = tweet.get("id", "") |
| 404 | + |
| 405 | + likes = tweet.get("likeCount", 0) |
| 406 | + retweets = tweet.get("retweetCount", 0) |
| 407 | + views = tweet.get("viewCount", 0) |
| 408 | + |
| 409 | + engagement = f"{likes} likes, {retweets} RTs" |
| 410 | + if views: |
| 411 | + engagement += f", {views} views" |
| 412 | + |
| 413 | + organic_results.append( |
| 414 | + { |
| 415 | + "title": f"@{username}: {text[:120]}", |
| 416 | + "link": f"https://x.com/{username}/status/{tweet_id}", |
| 417 | + "snippet": text, |
| 418 | + "content": f"Tweet by @{username}:\n\n{text}\n\nEngagement: {engagement}\n\n" |
| 419 | + f"Source: X/Twitter (social media post, represents real-time opinion)", |
| 420 | + } |
| 421 | + ) |
| 422 | + |
| 423 | + return query, {"organic": organic_results} |
| 424 | + |
| 425 | + except Exception as e: |
| 426 | + logger.error(f"Error searching with Xquik: {str(e)}") |
| 427 | + return query, {} |
| 428 | + |
| 429 | + |
361 | 430 | async def search_with_google(query: str, location: LocationData) -> Tuple[str, Dict[str, List[Dict]]]: |
362 | 431 | country_code = location.country_code.lower() if location and location.country_code else "us" |
363 | 432 | base_url = "https://www.googleapis.com/customsearch/v1" |
|
0 commit comments