diff --git a/src/jsons/health.nim b/src/jsons/health.nim index 2a0ee7dd1..e74fcf4d0 100644 --- a/src/jsons/health.nim +++ b/src/jsons/health.nim @@ -11,5 +11,11 @@ proc createJsonApiHealthRouter*(cfg: Config) = router jsonapi_health: get "/api/health": cond cfg.enableJsonApi - let headers = {"Content-Type": "application/json; charset=utf-8"} + let origin = corsOrigin() + let headers = { + "Content-Type": "application/json; charset=utf-8", + "Vary": "Origin", + "Access-Control-Allow-Origin": origin, + "Access-Control-Allow-Credentials": "true" + } resp Http200, headers, """{"message": "OK"}""" diff --git a/src/jsons/search.nim b/src/jsons/search.nim index 5518b0f56..4216bb708 100644 --- a/src/jsons/search.nim +++ b/src/jsons/search.nim @@ -16,9 +16,7 @@ proc createJsonApiSearchRouter*(cfg: Config) = if q.len > 500: respJsonError("Search input too long.", "invalid_input", Http400) - let - prefs = requestPrefs() - query = initQuery(params(request)) + let query = initQuery(params(request)) case query.kind of users: diff --git a/src/jsons/timeline.nim b/src/jsons/timeline.nim index 47408159c..d1acdd986 100644 --- a/src/jsons/timeline.nim +++ b/src/jsons/timeline.nim @@ -1,9 +1,6 @@ # SPDX-License-Identifier: AGPL-3.0-only import json, asyncdispatch, strutils, sequtils, uri, options, times -import options -import times - import jester, karax/vdom import ".."/routes/[router_utils, timeline] @@ -32,6 +29,33 @@ proc formatUserAsJson*(user: User): JsonNode = "joinDate": user.joinDate.toTime.toUnix() } +proc formatMediaAsJson*(m: Media): JsonNode = + case m.kind + of photoMedia: + return %*{"type": "photo", "url": m.photo.url, "altText": m.photo.altText} + of videoMedia: + var variants = newJArray() + for v in m.video.variants: + variants.add %*{ + "contentType": $v.contentType, + "url": v.url, + "bitrate": v.bitrate, + "resolution": v.resolution + } + return %*{ + "type": "video", + "durationMs": m.video.durationMs, + "url": m.video.url, + "thumb": m.video.thumb, + "available": m.video.available, + "reason": m.video.reason, + "title": m.video.title, + "description": m.video.description, + "variants": variants + } + of gifMedia: + return %*{"type": "gif", "url": m.gif.url, "thumb": m.gif.thumb, "altText": m.gif.altText} + proc formatTweetAsJson*(tweet: Tweet): JsonNode = return %*{ "id": $tweet.id, @@ -51,7 +75,8 @@ proc formatTweetAsJson*(tweet: Tweet): JsonNode = "replies": tweet.stats.replies, "retweets": tweet.stats.retweets, "likes": tweet.stats.likes, - "quotes": tweet.stats.quotes + "quotes": tweet.stats.quotes, + "views": tweet.stats.views }, "retweet": if tweet.retweet.isSome: formatTweetAsJson(get( tweet.retweet)) else: newJNull(), @@ -63,9 +88,11 @@ proc formatTweetAsJson*(tweet: Tweet): JsonNode = tweet.quote)) else: newJNull(), "card": if tweet.card.isSome: %*get(tweet.card) else: newJNull(), "poll": if tweet.poll.isSome: %*get(tweet.poll) else: newJNull(), - "photos": (if tweet.hasPhotos: %tweet.getPhotos else: newJNull()), - "videos": (if tweet.hasVideos: %tweet.getVideos else: newJNull()), - "gifs": (if tweet.hasGifs: %tweet.media.filterIt(it.kind == gifMedia).mapIt(it.gif) else: newJNull()) + "media": (if tweet.media.len > 0: %tweet.media.map(formatMediaAsJson) else: newJNull()), + "history": (if tweet.history.len > 0: %tweet.history else: newJNull()), + "note": (if tweet.note.len > 0: %tweet.note else: newJNull()), + "isAd": %tweet.isAd, + "isAI": %tweet.isAI } proc formatTimelineAsJson*(results: Timeline): JsonNode = diff --git a/src/nitter.nim b/src/nitter.nim index f7c3cb014..2e26245d3 100644 --- a/src/nitter.nim +++ b/src/nitter.nim @@ -1,5 +1,5 @@ # SPDX-License-Identifier: AGPL-3.0-only -import asyncdispatch, strformat, logging +import asyncdispatch, strformat, logging, re from net import Port from htmlgen import a from os import getEnv @@ -75,6 +75,17 @@ settings: reusePort = true routes: + options re"/api/.*": + let origin = if request.headers.hasKey("Origin"): request.headers["Origin"] else: "*" + resp Http204, { + "Vary": "Origin", + "Access-Control-Allow-Origin": origin, + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization, DNT", + "Access-Control-Allow-Credentials": "true", + "Access-Control-Max-Age": "300" + }, "" + before: # skip all file URLs cond "." notin request.path diff --git a/src/routes/router_utils.nim b/src/routes/router_utils.nim index 5f351247d..15ef87489 100644 --- a/src/routes/router_utils.nim +++ b/src/routes/router_utils.nim @@ -58,25 +58,51 @@ template applyUrlPrefs*() {.dirty.} = else: redirect(request.path) +template corsOrigin*(): string {.dirty.} = + if request.headers.hasKey("Origin"): request.headers["Origin"] else: "*" + template respJson*(node: JsonNode) = - resp $node, "application/json" + let origin = corsOrigin() + resp Http200, { + "Content-Type": "application/json", + "Vary": "Origin", + "Access-Control-Allow-Origin": origin, + "Access-Control-Allow-Credentials": "true" + }, $node template respJsonSuccess*(data: JsonNode) = + let origin = corsOrigin() let successResponse = %*{ "code": 0, "data": data } - resp $successResponse, "application/json" + resp Http200, { + "Content-Type": "application/json", + "Vary": "Origin", + "Access-Control-Allow-Origin": origin, + "Access-Control-Allow-Credentials": "true" + }, $successResponse template respJsonError*(message: string, errorType: string = "", httpCode: HttpCode = Http200) = + let origin = corsOrigin() var errorResponse = %*{ "code": -1, "error": message } if errorType.len > 0: errorResponse["error_type"] = %errorType - resp httpCode, $errorResponse, "application/json" + resp httpCode, { + "Content-Type": "application/json", + "Vary": "Origin", + "Access-Control-Allow-Origin": origin, + "Access-Control-Allow-Credentials": "true" + }, $errorResponse template respJsonNull*() = - let nullResponse = newJNull() - resp $nullResponse, "application/json" + let origin = corsOrigin() + resp Http200, { + "Content-Type": "application/json", + "Vary": "Origin", + "Access-Control-Allow-Origin": origin, + "Access-Control-Allow-Credentials": "true" + }, $newJNull()