1
0
Fork 0
mirror of https://github.com/zedeus/nitter.git synced 2025-04-21 08:24:05 +00:00

feat: search api ()

This commit is contained in:
guanbinrui 2025-04-02 21:35:13 +08:00 committed by GitHub
commit 65be1ab472
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 114 additions and 31 deletions

1
.gitignore vendored
View file

@ -10,6 +10,7 @@ nitter
/public/css/style.css
/public/md/*.html
/tools/venv
/scripts
/nimbledeps
nitter.conf
guest_accounts.json*

38
API.md
View file

@ -1,4 +1,4 @@
# Nitter JSON API Documentation
# Nitter JSON API Documentation
This document describes all available JSON API endpoints in the Nitter application.
@ -77,6 +77,42 @@ Get members of a specific list.
| pagination | object | Pagination information |
| users | array | Array of user objects |
## Search
### GET /api/search
Search for tweets or users based on a query.
**Parameters:**
| Parameter | Type | Description |
|-----------|--------|--------------------------------|
| q | string | Search query (max 500 chars) |
**Response:**
| Field | Type | Description |
|------------|--------|--------------------------------|
| pagination | object | Pagination information |
| timeline | array | Array of tweets (for tweet search) |
| users | array | Array of users (for user search) |
**Notes:**
- The search type is determined by the query format
- For user search, if the query contains a comma, it will redirect to the user profile page
- Returns error if search input is too long (>500 characters)
- Returns error for invalid search types
### GET /api/hashtag/@hash
Redirect to search results for a specific hashtag.
**Parameters:**
| Parameter | Type | Description |
|-----------|--------|--------------------------------|
| hash | string | Hashtag to search for |
**Response:**
Redirects to `/search?q=#hashtag`
## User Profile
### GET /api/@name/profile

View file

@ -19,6 +19,21 @@ proc formatListAsJson*(list: List): JsonNode =
"banner": list.banner
}
proc formatUsersAsJson*(results: Result[User]): JsonNode =
var users = newJArray()
for user in results.content:
users.add(formatUserAsJson(user))
return %*{
"pagination": %*{
"beginning": results.beginning,
"top": results.top,
"bottom": results.bottom,
},
"users": users
}
proc createJsonApiListRouter*(cfg: Config) =
router jsonapi_list:
get "/api/@name/lists/@slug/?":
@ -34,21 +49,21 @@ proc createJsonApiListRouter*(cfg: Config) =
get "/api/i/lists/@id/?":
cond cfg.enableJsonApi
cond '.' notin @"id"
let list = await getCachedList(id=(@"id"))
let list = await getCachedList(id = (@"id"))
respJson formatListAsJson(list)
get "/api/i/lists/@id/timeline/?":
cond cfg.enableJsonApi
cond '.' notin @"id"
let
list = await getCachedList(id=(@"id"))
timeline = await getGraphListTweets(list.id, getCursor())
list = await getCachedList(id = (@"id"))
timeline = await getGraphListTweets(list.id, getCursor())
respJson formatTimelineAsJson(timeline)
get "/api/i/lists/@id/members/?":
cond cfg.enableJsonApi
cond '.' notin @"id"
let
list = await getCachedList(id=(@"id"))
members = await getGraphListMembers(list, getCursor())
respJson formatUsersAsJson(members)
list = await getCachedList(id = (@"id"))
members = await getGraphListMembers(list, getCursor())
respJson formatUsersAsJson(members)

40
src/jsons/search.nim Normal file
View file

@ -0,0 +1,40 @@
# SPDX-License-Identifier: AGPL-3.0-only
import strutils, uri
import jester
import ".."/routes/[router_utils, timeline]
import ".."/[query, types, api, formatters]
import ../views/[general, search]
proc createJsonApiSearchRouter*(cfg: Config) =
router jsonapi_search:
get "/api/search?":
let q = @"q"
if q.len > 500:
respJsonError "Search input too long."
let
prefs = cookiePrefs()
query = initQuery(params(request))
title = "Search" & (if q.len > 0: " (" & q & ")" else: "")
case query.kind
of users:
if "," in q:
redirect("/" & q)
var users: Result[User]
try:
users = await getGraphUserSearch(query, getCursor())
except InternalError:
users = Result[User](beginning: true, query: query)
respJsonSuccess formatUsersAsJson(users)
of tweets:
let
tweets = await getGraphTweetSearch(query, getCursor())
respJsonSuccess formatTweetsAsJson(tweets)
else:
respJsonError "Invalid search"
get "/api/hashtag/@hash":
redirect("/search?q=" & encodeUrl("#" & @"hash"))

View file

@ -54,9 +54,12 @@ proc formatTweetAsJson*(tweet: Tweet): JsonNode =
"likes": tweet.stats.likes,
"quotes": tweet.stats.quotes
},
"retweet": if tweet.retweet.isSome: formatTweetAsJson(get(tweet.retweet)) else: newJNull(),
"attribution": if tweet.attribution.isSome: formatUserAsJson(get(tweet.attribution)) else: newJNull(),
"quote": if tweet.quote.isSome: formatTweetAsJson(get(tweet.quote)) else: newJNull(),
"retweet": if tweet.retweet.isSome: formatTweetAsJson(get(
tweet.retweet)) else: newJNull(),
"attribution": if tweet.attribution.isSome: formatUserAsJson(get(
tweet.attribution)) else: newJNull(),
"quote": if tweet.quote.isSome: formatTweetAsJson(get(
tweet.quote)) else: newJNull(),
"poll": if tweet.poll.isSome: %*get(tweet.poll) else: newJNull(),
"gif": if tweet.gif.isSome: %*get(tweet.gif) else: newJNull(),
"video": if tweet.video.isSome: %*get(tweet.video) else: newJNull(),
@ -94,21 +97,6 @@ proc formatTimelineAsJson*(results: Timeline): JsonNode =
"timeline": timeline
}
proc formatUsersAsJson*(results: Result[User]): JsonNode =
var users = newJArray()
for user in results.content:
users.add(formatUserAsJson(user))
return %*{
"pagination": %*{
"beginning": results.beginning,
"top": results.top,
"bottom": results.bottom,
},
"users": users
}
proc formatUserName*(username: string): JsonNode =
return %*{
"username": username
@ -118,7 +106,8 @@ proc formatProfileAsJson*(profile: Profile): JsonNode =
return %*{
"user": formatUserAsJson(profile.user),
"photoRail": %profile.photoRail,
"pinned": if profile.pinned.isSome: formatTweetAsJson(get(profile.pinned)) else: newJNull()
"pinned": if profile.pinned.isSome: formatTweetAsJson(get(
profile.pinned)) else: newJNull()
}
proc createJsonApiTimelineRouter*(cfg: Config) =
@ -132,7 +121,8 @@ proc createJsonApiTimelineRouter*(cfg: Config) =
respJsonError "User not found"
get "/api/@name/profile":
cond @"name" notin ["pic", "gif", "video", "search", "settings", "login", "intent", "i"]
cond @"name" notin ["pic", "gif", "video", "search", "settings", "login",
"intent", "i"]
let
prefs = cookiePrefs()
names = getNames(@"name")
@ -141,14 +131,15 @@ proc createJsonApiTimelineRouter*(cfg: Config) =
if names.len != 1:
query.fromUser = names
var profile = await fetchProfile("", query, skipRail=false)
var profile = await fetchProfile("", query, skipRail = false)
if profile.user.username.len == 0: respJsonError "User not found"
respJsonSuccess formatProfileAsJson(profile)
get "/api/@name/?@tab?/?":
cond '.' notin @"name"
cond @"name" notin ["pic", "gif", "video", "search", "settings", "login", "intent", "i"]
cond @"name" notin ["pic", "gif", "video", "search", "settings", "login",
"intent", "i"]
cond @"tab" in ["with_replies", "media", "search", ""]
let
prefs = cookiePrefs()
@ -165,7 +156,7 @@ proc createJsonApiTimelineRouter*(cfg: Config) =
timeline.beginning = true
respJsonSuccess formatTimelineAsJson(timeline)
else:
var profile = await fetchProfile(after, query, skipRail=true)
var profile = await fetchProfile(after, query, skipRail = true)
if profile.tweets.content.len == 0: respJsonError "User not found"
profile.tweets.beginning = true
respJsonSuccess formatTimelineAsJson(profile.tweets)
respJsonSuccess formatTimelineAsJson(profile.tweets)