add my fork changes
This commit is contained in:
parent
cdff5e9b1c
commit
f2443a0da2
7 changed files with 188 additions and 34 deletions
|
@ -1,25 +0,0 @@
|
||||||
FROM alpine:3.18 as nim
|
|
||||||
LABEL maintainer="setenforce@protonmail.com"
|
|
||||||
|
|
||||||
RUN apk --no-cache add libsass-dev pcre gcc git libc-dev "nim=1.6.14-r0" "nimble=0.13.1-r2"
|
|
||||||
|
|
||||||
WORKDIR /src/nitter
|
|
||||||
|
|
||||||
COPY nitter.nimble .
|
|
||||||
RUN nimble install -y --depsOnly
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
RUN nimble build -d:danger -d:lto -d:strip \
|
|
||||||
&& nimble scss \
|
|
||||||
&& nimble md
|
|
||||||
|
|
||||||
FROM alpine:3.18
|
|
||||||
WORKDIR /src/
|
|
||||||
RUN apk --no-cache add pcre ca-certificates openssl1.1-compat
|
|
||||||
COPY --from=nim /src/nitter/nitter ./
|
|
||||||
COPY --from=nim /src/nitter/nitter.example.conf ./nitter.conf
|
|
||||||
COPY --from=nim /src/nitter/public ./public
|
|
||||||
EXPOSE 8080
|
|
||||||
RUN adduser -h /src/ -D -s /bin/sh nitter
|
|
||||||
USER nitter
|
|
||||||
CMD ./nitter
|
|
|
@ -1,14 +1,19 @@
|
||||||
version: "3"
|
version: "3"
|
||||||
|
|
||||||
services:
|
networks:
|
||||||
|
|
||||||
nitter:
|
nitter:
|
||||||
image: zedeus/nitter:latest
|
|
||||||
|
services:
|
||||||
|
nitter:
|
||||||
|
build: .
|
||||||
container_name: nitter
|
container_name: nitter
|
||||||
|
hostname: nitter
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8080:8080" # Replace with "8080:8080" if you don't use a reverse proxy
|
- "8002:8080" # Replace with "8080:8080" if you don't use a reverse proxy
|
||||||
volumes:
|
volumes:
|
||||||
- ./nitter.conf:/src/nitter.conf:Z,ro
|
- ./nitter.conf:/src/nitter.conf:Z,ro
|
||||||
|
- ./guest_accounts.json:/src/guest_accounts.json:Z,ro
|
||||||
|
- ./public/.twitterminator.txt:/src/public/.twitterminator.txt:Z,ro
|
||||||
depends_on:
|
depends_on:
|
||||||
- nitter-redis
|
- nitter-redis
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
@ -23,6 +28,8 @@ services:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
cap_drop:
|
cap_drop:
|
||||||
- ALL
|
- ALL
|
||||||
|
networks:
|
||||||
|
- nitter
|
||||||
|
|
||||||
nitter-redis:
|
nitter-redis:
|
||||||
image: redis:6-alpine
|
image: redis:6-alpine
|
||||||
|
@ -42,6 +49,8 @@ services:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
cap_drop:
|
cap_drop:
|
||||||
- ALL
|
- ALL
|
||||||
|
networks:
|
||||||
|
- nitter
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
nitter-redis:
|
nitter-redis:
|
||||||
|
|
|
@ -202,7 +202,7 @@ proc initAccountPool*(cfg: Config; path: string) =
|
||||||
quit 1
|
quit 1
|
||||||
|
|
||||||
let accountsPrePurge = accountPool.len
|
let accountsPrePurge = accountPool.len
|
||||||
accountPool.keepItIf(not it.hasExpired)
|
#accountPool.keepItIf(not it.hasExpired)
|
||||||
|
|
||||||
log "Successfully added ", accountPool.len, " valid accounts."
|
log "Successfully added ", accountPool.len, " valid accounts."
|
||||||
if accountsPrePurge > accountPool.len:
|
if accountsPrePurge > accountPool.len:
|
||||||
|
|
|
@ -10,7 +10,7 @@ import types, config, prefs, formatters, redis_cache, http_pool, auth
|
||||||
import views/[general, about]
|
import views/[general, about]
|
||||||
import routes/[
|
import routes/[
|
||||||
preferences, timeline, status, media, search, rss, list, debug,
|
preferences, timeline, status, media, search, rss, list, debug,
|
||||||
unsupported, embed, resolver, router_utils]
|
twitter_api, unsupported, embed, resolver, router_utils]
|
||||||
|
|
||||||
const instancesUrl = "https://github.com/zedeus/nitter/wiki/Instances"
|
const instancesUrl = "https://github.com/zedeus/nitter/wiki/Instances"
|
||||||
const issuesUrl = "https://github.com/zedeus/nitter/issues"
|
const issuesUrl = "https://github.com/zedeus/nitter/issues"
|
||||||
|
@ -53,6 +53,7 @@ createSearchRouter(cfg)
|
||||||
createMediaRouter(cfg)
|
createMediaRouter(cfg)
|
||||||
createEmbedRouter(cfg)
|
createEmbedRouter(cfg)
|
||||||
createRssRouter(cfg)
|
createRssRouter(cfg)
|
||||||
|
createTwitterApiRouter(cfg)
|
||||||
createDebugRouter(cfg)
|
createDebugRouter(cfg)
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
|
|
171
src/routes/twitter_api.nim
Normal file
171
src/routes/twitter_api.nim
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import json, asyncdispatch, options, uri
|
||||||
|
import times
|
||||||
|
import jester
|
||||||
|
import router_utils
|
||||||
|
import ".."/[types, api, apiutils, query, consts]
|
||||||
|
import httpclient, strutils
|
||||||
|
import sequtils
|
||||||
|
|
||||||
|
export api
|
||||||
|
|
||||||
|
proc videoToJson*(t: Video): JsonNode =
|
||||||
|
result = newJObject()
|
||||||
|
result["durationMs"] = %t.durationMs
|
||||||
|
result["url"] = %t.url
|
||||||
|
result["thumb"] = %t.thumb
|
||||||
|
result["views"] = %t.views
|
||||||
|
result["available"] = %t.available
|
||||||
|
result["reason"] = %t.reason
|
||||||
|
result["title"] = %t.title
|
||||||
|
result["description"] = %t.description
|
||||||
|
# result["playbackType"] = %t.playbackType
|
||||||
|
# result["variants"] = %t.variants
|
||||||
|
# playbackType*: VideoType
|
||||||
|
# variants*: seq[VideoVariant]
|
||||||
|
|
||||||
|
proc tweetToJson*(t: Tweet): JsonNode =
|
||||||
|
result = newJObject()
|
||||||
|
result["id"] = %t.id
|
||||||
|
result["threadId"] = %t.threadId
|
||||||
|
result["replyId"] = %t.replyId
|
||||||
|
result["user"] = %*{ "username": t.user.username }
|
||||||
|
result["text"] = %t.text
|
||||||
|
result["time"] = newJString(times.format(t.time, "yyyy-MM-dd'T'HH:mm:ss"))
|
||||||
|
result["reply"] = %t.reply
|
||||||
|
result["pinned"] = %t.pinned
|
||||||
|
result["hasThread"] = %t.hasThread
|
||||||
|
result["available"] = %t.available
|
||||||
|
result["tombstone"] = %t.tombstone
|
||||||
|
result["location"] = %t.location
|
||||||
|
result["source"] = %t.source
|
||||||
|
# result["stats"] = toJson(t.stats) # Define conversion for TweetStats type
|
||||||
|
# result["retweet"] = t.retweet.map(toJson) # Define conversion for Tweet type
|
||||||
|
# result["attribution"] = t.attribution.map(toJson) # Define conversion for User type
|
||||||
|
# result["mediaTags"] = toJson(t.mediaTags) # Define conversion for seq[User]
|
||||||
|
# result["quote"] = t.quote.map(toJson) # Define conversion for Tweet type
|
||||||
|
# result["card"] = t.card.map(toJson) # Define conversion for Card type
|
||||||
|
# result["poll"] = t.poll.map(toJson) # Define conversion for Poll type
|
||||||
|
# result["gif"] = t.gif.map(toJson) # Define conversion for Gif type
|
||||||
|
# result["video"] = videoToJson(t.video.get())
|
||||||
|
result["photos"] = %t.photos
|
||||||
|
|
||||||
|
proc getUserProfileJson*(username: string): Future[JsonNode] {.async.} =
|
||||||
|
let user: User = await getGraphUser(username)
|
||||||
|
let response: JsonNode = %*{
|
||||||
|
"id": user.id,
|
||||||
|
"username": user.username
|
||||||
|
}
|
||||||
|
result = response
|
||||||
|
|
||||||
|
proc getUserTweetsJson*(id: string): Future[JsonNode] {.async.} =
|
||||||
|
let tweetsGraph = await getGraphUserTweets(id, TimelineKind.tweets)
|
||||||
|
let repliesGraph = await getGraphUserTweets(id, TimelineKind.replies)
|
||||||
|
let mediaGraph = await getGraphUserTweets(id, TimelineKind.media)
|
||||||
|
|
||||||
|
let tweetsContent = tweetsGraph.tweets.content[0]
|
||||||
|
let tweetsJson = tweetsContent.map(tweetToJson)
|
||||||
|
|
||||||
|
let repliesContent = repliesGraph.tweets.content[0]
|
||||||
|
let repliesJson = repliesContent.map(tweetToJson)
|
||||||
|
|
||||||
|
let mediaContent = mediaGraph.tweets.content[0]
|
||||||
|
let mediaJson = mediaContent.map(tweetToJson)
|
||||||
|
|
||||||
|
let response: JsonNode = %*{
|
||||||
|
"tweets": %tweetsJson,
|
||||||
|
"replies": %repliesJson,
|
||||||
|
"media": %mediaJson
|
||||||
|
}
|
||||||
|
|
||||||
|
result = response
|
||||||
|
|
||||||
|
proc searchTimeline*(query: Query; after=""): Future[string] {.async.} =
|
||||||
|
let q = genQueryParam(query)
|
||||||
|
var
|
||||||
|
variables = %*{
|
||||||
|
"rawQuery": q,
|
||||||
|
"count": 20,
|
||||||
|
"product": "Latest",
|
||||||
|
"withDownvotePerspective": false,
|
||||||
|
"withReactionsMetadata": false,
|
||||||
|
"withReactionsPerspective": false
|
||||||
|
}
|
||||||
|
if after.len > 0:
|
||||||
|
variables["cursor"] = % after
|
||||||
|
let url = graphSearchTimeline ? {"variables": $variables, "features": gqlFeatures}
|
||||||
|
result = await fetchRaw(url, Api.search)
|
||||||
|
|
||||||
|
proc getUserTweets*(id: string; after=""): Future[string] {.async.} =
|
||||||
|
if id.len == 0: return
|
||||||
|
let
|
||||||
|
cursor = if after.len > 0: "\"cursor\":\"$1\"," % after else: ""
|
||||||
|
variables = userTweetsVariables % [id, cursor]
|
||||||
|
params = {"variables": variables, "features": gqlFeatures}
|
||||||
|
result = await fetchRaw(graphUserTweets ? params, Api.userTweets)
|
||||||
|
|
||||||
|
proc getUserReplies*(id: string; after=""): Future[string] {.async.} =
|
||||||
|
if id.len == 0: return
|
||||||
|
let
|
||||||
|
cursor = if after.len > 0: "\"cursor\":\"$1\"," % after else: ""
|
||||||
|
variables = userTweetsVariables % [id, cursor]
|
||||||
|
params = {"variables": variables, "features": gqlFeatures}
|
||||||
|
result = await fetchRaw(graphUserTweets ? params, Api.userTweetsAndReplies)
|
||||||
|
|
||||||
|
proc getUserMedia*(id: string; after=""): Future[string] {.async.} =
|
||||||
|
if id.len == 0: return
|
||||||
|
let
|
||||||
|
cursor = if after.len > 0: "\"cursor\":\"$1\"," % after else: ""
|
||||||
|
variables = userTweetsVariables % [id, cursor]
|
||||||
|
params = {"variables": variables, "features": gqlFeatures}
|
||||||
|
result = await fetchRaw(graphUserTweets ? params, Api.userMedia)
|
||||||
|
|
||||||
|
proc getTweetById*(id: string; after=""): Future[string] {.async.} =
|
||||||
|
let
|
||||||
|
cursor = if after.len > 0: "\"cursor\":\"$1\"," % after else: ""
|
||||||
|
variables = tweetVariables % [id, cursor]
|
||||||
|
params = {"variables": variables, "features": gqlFeatures}
|
||||||
|
result = await fetchRaw(graphTweet ? params, Api.tweetDetail)
|
||||||
|
|
||||||
|
proc createTwitterApiRouter*(cfg: Config) =
|
||||||
|
router api:
|
||||||
|
get "/api/echo":
|
||||||
|
resp Http200, {"Content-Type": "text/html"}, "hello, world!"
|
||||||
|
|
||||||
|
get "/api/user/@username":
|
||||||
|
let username = @"username"
|
||||||
|
let response = await getUserProfileJson(username)
|
||||||
|
respJson response
|
||||||
|
|
||||||
|
# get "/api/user/@id/tweets":
|
||||||
|
# let id = @"id"
|
||||||
|
# let response = await getUserTweetsJson(id)
|
||||||
|
# respJson response
|
||||||
|
|
||||||
|
get "/api/user/@username/timeline":
|
||||||
|
let username = @"username"
|
||||||
|
let query = Query(fromUser: @[username])
|
||||||
|
let response = await searchTimeline(query)
|
||||||
|
resp Http200, { "Content-Type": "application/json" }, response
|
||||||
|
|
||||||
|
get "/api/user/@id/tweets":
|
||||||
|
let id = @"id"
|
||||||
|
let after = getCursor()
|
||||||
|
let response = await getUserTweets(id, after)
|
||||||
|
resp Http200, { "Content-Type": "application/json" }, response
|
||||||
|
|
||||||
|
get "/api/user/@id/replies":
|
||||||
|
let id = @"id"
|
||||||
|
let response = await getUserReplies(id)
|
||||||
|
resp Http200, { "Content-Type": "application/json" }, response
|
||||||
|
|
||||||
|
get "/api/user/@id/media":
|
||||||
|
let id = @"id"
|
||||||
|
let response = await getUserMedia(id)
|
||||||
|
resp Http200, { "Content-Type": "application/json" }, response
|
||||||
|
|
||||||
|
get "/api/tweet/@id":
|
||||||
|
let id = @"id"
|
||||||
|
let response = await getTweetById(id)
|
||||||
|
resp Http200, { "Content-Type": "application/json" }, response
|
|
@ -32,8 +32,6 @@ proc renderNavbar(cfg: Config; req: Request; rss, canonical: string): VNode =
|
||||||
if cfg.enableRss and rss.len > 0:
|
if cfg.enableRss and rss.len > 0:
|
||||||
icon "rss-feed", title="RSS Feed", href=rss
|
icon "rss-feed", title="RSS Feed", href=rss
|
||||||
icon "bird", title="Open in Twitter", href=canonical
|
icon "bird", title="Open in Twitter", href=canonical
|
||||||
a(href="https://liberapay.com/zedeus"): verbatim lp
|
|
||||||
icon "info", title="About", href="/about"
|
|
||||||
icon "cog", title="Preferences", href=("/settings?referer=" & encodeUrl(path))
|
icon "cog", title="Preferences", href=("/settings?referer=" & encodeUrl(path))
|
||||||
|
|
||||||
proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
|
proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
|
||||||
|
|
|
@ -13,7 +13,7 @@ proc renderStat(num: int; class: string; text=""): VNode =
|
||||||
text insertSep($num, ',')
|
text insertSep($num, ',')
|
||||||
|
|
||||||
proc renderUserCard*(user: User; prefs: Prefs): VNode =
|
proc renderUserCard*(user: User; prefs: Prefs): VNode =
|
||||||
buildHtml(tdiv(class="profile-card")):
|
buildHtml(tdiv(class="profile-card", "data-profile-id" = $user.id)):
|
||||||
tdiv(class="profile-card-info"):
|
tdiv(class="profile-card-info"):
|
||||||
let
|
let
|
||||||
url = getPicUrl(user.getUserPic())
|
url = getPicUrl(user.getUserPic())
|
||||||
|
|
Loading…
Reference in a new issue