API for searching users and posts, user profile, timeline, with_replies and media

This commit is contained in:
Xie Yanbo 2021-10-01 00:43:18 +08:00
parent 13a4580ce2
commit 710be0b413
5 changed files with 171 additions and 15 deletions

15
src/restutils.nim Normal file
View 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
View 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)

View file

@ -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"))

View file

@ -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

View file

@ -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 & "\"")