mirror of
https://github.com/zedeus/nitter.git
synced 2025-04-21 08:24:05 +00:00
feat: search api (#5)
This commit is contained in:
commit
65be1ab472
5 changed files with 114 additions and 31 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -10,6 +10,7 @@ nitter
|
|||
/public/css/style.css
|
||||
/public/md/*.html
|
||||
/tools/venv
|
||||
/scripts
|
||||
/nimbledeps
|
||||
nitter.conf
|
||||
guest_accounts.json*
|
||||
|
|
38
API.md
38
API.md
|
@ -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
|
||||
|
|
|
@ -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
40
src/jsons/search.nim
Normal 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"))
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue