mirror of
https://github.com/zedeus/nitter.git
synced 2024-12-14 03:56:29 +00:00
API for searching users and posts, user profile, timeline, with_replies and media
This commit is contained in:
parent
13a4580ce2
commit
710be0b413
5 changed files with 171 additions and 15 deletions
15
src/restutils.nim
Normal file
15
src/restutils.nim
Normal file
|
@ -0,0 +1,15 @@
|
|||
import uri
|
||||
import jester
|
||||
import types, query
|
||||
|
||||
proc getLinkHeader*(results: Result, req: Request): string =
|
||||
let
|
||||
cursor = results.bottom
|
||||
query = results.query
|
||||
var url = if req.secure: "https://" else: "http://"
|
||||
url &= req.host & req.path
|
||||
var links = newLinkHeader()
|
||||
links["first"] = url & "?" & genQueryUrl(query)
|
||||
if results.content.len > 0 and results.bottom.len > 0:
|
||||
links["next"] = url & "?" & genQueryUrl(query) & "&cursor=" & encodeUrl(cursor)
|
||||
result = $links
|
36
src/routes/rest.nim
Normal file
36
src/routes/rest.nim
Normal file
|
@ -0,0 +1,36 @@
|
|||
import json
|
||||
import jester
|
||||
import ".."/[types, restutils]
|
||||
|
||||
export restutils
|
||||
|
||||
template rest*(code: HttpCode; message: string): untyped =
|
||||
## Response of RESTful API
|
||||
mixin resp
|
||||
resp code, @{"Content-Type": "application/json"}, message
|
||||
|
||||
template rest*(code: HttpCode; message: JsonNode): untyped =
|
||||
mixin rest
|
||||
rest code, $message
|
||||
|
||||
template rest*(code: HttpCode; profile: Profile): untyped =
|
||||
mixin rest
|
||||
rest code, %profile
|
||||
|
||||
template rest*(code: HttpCode; timeline: Timeline): untyped =
|
||||
mixin rest
|
||||
rest code, %timeline
|
||||
|
||||
template rest*[T](code: HttpCode; results: Result[T]): untyped =
|
||||
mixin rest
|
||||
rest code, %results
|
||||
|
||||
template rest*[T](code: HttpCode; results: Result[T];
|
||||
request: Request): untyped =
|
||||
mixin resp
|
||||
resp code, @{"Content-Type": "application/json",
|
||||
"Link": $getLinkHeader(results, request)}, $ %results
|
||||
|
||||
template restError*(code: HttpCode; message: string): untyped =
|
||||
mixin rest
|
||||
rest code, %newRestApiError(message)
|
|
@ -1,20 +1,26 @@
|
|||
import strutils, sequtils, uri
|
||||
import strutils, sequtils, uri, json
|
||||
|
||||
import jester
|
||||
|
||||
import router_utils
|
||||
import ".."/[query, types, api]
|
||||
import rest
|
||||
import ".."/[query, types, api, restutils]
|
||||
import ../views/[general, search]
|
||||
|
||||
include "../views/opensearch.nimf"
|
||||
|
||||
export search
|
||||
export rest
|
||||
|
||||
const
|
||||
searchLimit* = 500
|
||||
|
||||
proc createSearchRouter*(cfg: Config) =
|
||||
router search:
|
||||
get "/search/?":
|
||||
if @"q".len > 500:
|
||||
resp Http400, showError("Search input too long.", cfg)
|
||||
if @"q".len > searchLimit:
|
||||
resp Http400, showError("Search input too long, max limit: " &
|
||||
$searchLimit, cfg)
|
||||
|
||||
let
|
||||
prefs = cookiePrefs()
|
||||
|
@ -31,10 +37,31 @@ proc createSearchRouter*(cfg: Config) =
|
|||
tweets = await getSearch[Tweet](query, getCursor())
|
||||
rss = "/search/rss?" & genQueryUrl(query)
|
||||
resp renderMain(renderTweetSearch(tweets, prefs, getPath()),
|
||||
request, cfg, prefs, rss=rss)
|
||||
request, cfg, prefs, rss = rss)
|
||||
else:
|
||||
resp Http404, showError("Invalid search", cfg)
|
||||
|
||||
get "/api/search/?":
|
||||
if @"q".len > searchLimit:
|
||||
restError Http400, "Search input too long, max limit: " & $searchLimit
|
||||
let
|
||||
prefs = cookiePrefs()
|
||||
query = initQuery(params(request))
|
||||
|
||||
case query.kind
|
||||
of users:
|
||||
if "," in @"q":
|
||||
redirect("/" & @"q")
|
||||
let users = await getSearch[Profile](query, getCursor())
|
||||
rest Http200, users, request
|
||||
of tweets:
|
||||
let
|
||||
tweets = await getSearch[Tweet](query, getCursor())
|
||||
rss = "/search/rss?" & genQueryUrl(query)
|
||||
rest Http200, tweets, request
|
||||
else:
|
||||
restError Http404, "Invalid search type: " & $query.kind
|
||||
|
||||
get "/hashtag/@hash":
|
||||
redirect("/search?q=" & encodeUrl("#" & @"hash"))
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import asyncdispatch, strutils, sequtils, uri, options, times
|
|||
import jester, karax/vdom
|
||||
|
||||
import router_utils
|
||||
import rest
|
||||
import ".."/[types, redis_cache, formatters, query, api]
|
||||
import ../views/[general, profile, timeline, status, search]
|
||||
|
||||
|
@ -10,15 +11,16 @@ export uri, sequtils
|
|||
export router_utils
|
||||
export redis_cache, formatters, query, api
|
||||
export profile, timeline, status
|
||||
export rest
|
||||
|
||||
proc getQuery*(request: Request; tab, name: string): Query =
|
||||
case tab
|
||||
of "with_replies": getReplyQuery(name)
|
||||
of "media": getMediaQuery(name)
|
||||
of "search": initQuery(params(request), name=name)
|
||||
of "search": initQuery(params(request), name = name)
|
||||
else: Query(fromUser: @[name])
|
||||
|
||||
proc fetchSingleTimeline*(after: string; query: Query; skipRail=false):
|
||||
proc fetchSingleTimeline*(after: string; query: Query; skipRail = false):
|
||||
Future[(Profile, Timeline, PhotoRail)] {.async.} =
|
||||
let name = query.fromUser[0]
|
||||
|
||||
|
@ -33,7 +35,7 @@ proc fetchSingleTimeline*(after: string; query: Query; skipRail=false):
|
|||
else: profile.id
|
||||
|
||||
if profileId.len > 0:
|
||||
await cacheProfileId(profile.username, profileId)
|
||||
await cacheProfileId(profile.username, profileId)
|
||||
|
||||
fetched = true
|
||||
|
||||
|
@ -54,7 +56,7 @@ proc fetchSingleTimeline*(after: string; query: Query; skipRail=false):
|
|||
var timeline =
|
||||
case query.kind
|
||||
of posts: await getTimeline(profileId, after)
|
||||
of replies: await getTimeline(profileId, after, replies=true)
|
||||
of replies: await getTimeline(profileId, after, replies = true)
|
||||
of media: await getMediaTimeline(profileId, after)
|
||||
else: await getSearch[Tweet](query, after)
|
||||
|
||||
|
@ -86,7 +88,7 @@ proc showTimeline*(request: Request; query: Query; cfg: Config; prefs: Prefs;
|
|||
let
|
||||
timeline = await getSearch[Tweet](query, after)
|
||||
html = renderTweetSearch(timeline, prefs, getPath())
|
||||
return renderMain(html, request, cfg, prefs, "Multi", rss=rss)
|
||||
return renderMain(html, request, cfg, prefs, "Multi", rss = rss)
|
||||
|
||||
var (p, t, r) = await fetchSingleTimeline(after, query)
|
||||
|
||||
|
@ -95,8 +97,8 @@ proc showTimeline*(request: Request; query: Query; cfg: Config; prefs: Prefs;
|
|||
|
||||
let pHtml = renderProfile(p, t, r, prefs, getPath())
|
||||
result = renderMain(pHtml, request, cfg, prefs, pageTitle(p), pageDesc(p),
|
||||
rss=rss, images = @[p.getUserpic("_400x400")],
|
||||
banner=p.banner)
|
||||
rss = rss, images = @[p.getUserpic("_400x400")],
|
||||
banner = p.banner)
|
||||
|
||||
template respTimeline*(timeline: typed) =
|
||||
let t = timeline
|
||||
|
@ -126,7 +128,8 @@ proc createTimelineRouter*(cfg: Config) =
|
|||
timeline.beginning = true
|
||||
resp $renderTweetSearch(timeline, prefs, getPath())
|
||||
else:
|
||||
var (_, timeline, _) = await fetchSingleTimeline(after, query, skipRail=true)
|
||||
var (_, timeline, _) = await fetchSingleTimeline(after, query,
|
||||
skipRail = true)
|
||||
if timeline.content.len == 0: resp Http404
|
||||
timeline.beginning = true
|
||||
resp $renderTimelineTweets(timeline, prefs, getPath())
|
||||
|
@ -138,3 +141,24 @@ proc createTimelineRouter*(cfg: Config) =
|
|||
rss &= "?" & genQueryUrl(query)
|
||||
|
||||
respTimeline(await showTimeline(request, query, cfg, prefs, rss, after))
|
||||
|
||||
get "/api/@name/@tab/?":
|
||||
cond '.' notin @"name"
|
||||
cond @"name" notin ["pic", "gif", "video"]
|
||||
cond @"tab" in ["profile", "timeline", "with_replies", "media", "search", ""]
|
||||
let
|
||||
prefs = cookiePrefs()
|
||||
after = getCursor()
|
||||
names = getNames(@"name")
|
||||
|
||||
var query = request.getQuery(@"tab", @"name")
|
||||
if names.len != 1:
|
||||
query.fromUser = names
|
||||
|
||||
var (profile, timeline, _) = await fetchSingleTimeline(after, query,
|
||||
skipRail = true)
|
||||
|
||||
if @"tab" in ["profile", ""]:
|
||||
rest Http200, profile
|
||||
else:
|
||||
rest Http200, timeline
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import times, sequtils, options, tables
|
||||
import times, sequtils, options, tables, json
|
||||
import prefs_impl
|
||||
|
||||
genPrefsType()
|
||||
|
@ -126,7 +126,7 @@ type
|
|||
videoDirectMessage = "video_direct_message"
|
||||
imageDirectMessage = "image_direct_message"
|
||||
audiospace = "audiospace"
|
||||
|
||||
|
||||
Card* = object
|
||||
kind*: CardKind
|
||||
id*: string
|
||||
|
@ -227,5 +227,59 @@ type
|
|||
Rss* = object
|
||||
feed*, cursor*: string
|
||||
|
||||
RestApiError* = object
|
||||
message*: string
|
||||
|
||||
LinkHeader* = object
|
||||
links*: TableRef[string, string]
|
||||
|
||||
proc contains*(thread: Chain; tweet: Tweet): bool =
|
||||
thread.content.anyIt(it.id == tweet.id)
|
||||
|
||||
proc `%`*[T](p: Result[T]): JsonNode =
|
||||
result = %p.content
|
||||
|
||||
proc `%`*(t: Time): JsonNode =
|
||||
result = JsonNode(kind: JString, str: format(t, "yyyy-MM-dd'T'HH:mm:sszzz", utc()))
|
||||
|
||||
proc `%`*(t: Tweet): JsonNode =
|
||||
let p = t.profile
|
||||
result = %* {
|
||||
"id": t.id,
|
||||
"threadId": t.threadId,
|
||||
"replyId": t.replyId,
|
||||
"profile": {"id": p.id, "username": p.username, "fullname": p.fullname},
|
||||
"text": t.text,
|
||||
"time": t.time,
|
||||
"reply": t.reply,
|
||||
"pinned": t.pinned,
|
||||
"hasThread": t.hasThread,
|
||||
"available": t.available,
|
||||
"tombstone": t.tombstone,
|
||||
"location": t.location,
|
||||
"stats": t.stats,
|
||||
"retweet": t.retweet,
|
||||
"attribution": t.attribution,
|
||||
"mediaTags": t.mediaTags,
|
||||
"quote": t.quote,
|
||||
"card": t.card,
|
||||
"poll": t.poll,
|
||||
"gif": t.gif,
|
||||
"video": t.video,
|
||||
"photos": t.photos,
|
||||
}
|
||||
|
||||
proc newRestApiError*(message: string): RestApiError =
|
||||
result.message = message
|
||||
|
||||
proc newLinkHeader*(): LinkHeader =
|
||||
result.links = newTable[string, string]()
|
||||
|
||||
proc `[]=`*(links: LinkHeader; rel: string; url: sink string) =
|
||||
links.links[rel] = url
|
||||
|
||||
proc `$`*(links: LinkHeader): string =
|
||||
for rel, url in links.links:
|
||||
if len(result) > 0:
|
||||
add(result, ", ")
|
||||
add(result, "<" & url & ">; rel=\"" & rel & "\"")
|
||||
|
|
Loading…
Reference in a new issue