Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/restutils.nim
Original file line number Diff line number Diff line change
@@ -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 changes: 36 additions & 0 deletions src/routes/rest.nim
Original file line number Diff line number Diff line change
@@ -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)
37 changes: 32 additions & 5 deletions src/routes/search.nim
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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"))

Expand Down
40 changes: 32 additions & 8 deletions src/routes/timeline.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand All @@ -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]

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

Expand All @@ -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)

Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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())
Expand All @@ -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
58 changes: 56 additions & 2 deletions src/types.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import times, sequtils, options, tables
import times, sequtils, options, tables, json
import prefs_impl

genPrefsType()
Expand Down Expand Up @@ -126,7 +126,7 @@ type
videoDirectMessage = "video_direct_message"
imageDirectMessage = "image_direct_message"
audiospace = "audiospace"

Card* = object
kind*: CardKind
id*: string
Expand Down Expand Up @@ -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 & "\"")