Compare commits

...

2 commits

6 changed files with 87 additions and 31 deletions

View file

@ -4,6 +4,9 @@ Nitter is a free and open source alternative Twitter front-end focused on
privacy and performance. The source is available on GitHub at privacy and performance. The source is available on GitHub at
<https://github.com/zedeus/nitter> <https://github.com/zedeus/nitter>
**This instance is running a fork, whose source can be found at**
<https://gitdab.com/Cynosphere/nitter>
* No JavaScript or ads * No JavaScript or ads
* All requests go through the backend, client never talks to Twitter * All requests go through the backend, client never talks to Twitter
* Prevents Twitter from tracking your IP or JavaScript fingerprint * Prevents Twitter from tracking your IP or JavaScript fingerprint
@ -19,6 +22,13 @@ Nitter's GitHub wiki contains
[browser extensions](https://github.com/zedeus/nitter/wiki/Extensions) [browser extensions](https://github.com/zedeus/nitter/wiki/Extensions)
maintained by the community. maintained by the community.
### Fork features
* Localized following via cookies (list exportable and editable in preferences)
* Image zooming/carousel (requires JavaScript)
* Up to date Twitter features, e.g. Community Notes
* Embeds for chat services on-par with services like [FxTwitter](https://github.com/FixTweet/FxTwitter) and [vxTwitter](https://github.com/dylanpdx/BetterTwitFix)
## Why use Nitter? ## Why use Nitter?
It's impossible to use Twitter without JavaScript enabled. For privacy-minded It's impossible to use Twitter without JavaScript enabled. For privacy-minded
@ -36,12 +46,12 @@ Twitter without JavaScript while retaining your privacy. In addition to
respecting your privacy, Nitter is on average around 15 times lighter than respecting your privacy, Nitter is on average around 15 times lighter than
Twitter, and in most cases serves pages faster (eg. timelines load 2-4x faster). Twitter, and in most cases serves pages faster (eg. timelines load 2-4x faster).
In the future a simple account system will be added that lets you follow Twitter
users, allowing you to have a clean chronological timeline without needing a
Twitter account.
## Donating ## Donating
Even though I could be selfish and point people to donate to me instead of
Zedeus, it would be disrespectful.
GitHub Sponsors: <https://github.com/sponsors/zedeus> \
Liberapay: <https://liberapay.com/zedeus> \ Liberapay: <https://liberapay.com/zedeus> \
Patreon: <https://patreon.com/nitter> \ Patreon: <https://patreon.com/nitter> \
BTC: bc1qp7q4qz0fgfvftm5hwz3vy284nue6jedt44kxya \ BTC: bc1qp7q4qz0fgfvftm5hwz3vy284nue6jedt44kxya \
@ -49,6 +59,23 @@ ETH: 0x66d84bc3fd031b62857ad18c62f1ba072b011925 \
LTC: ltc1qhsz5nxw6jw9rdtw9qssjeq2h8hqk2f85rdgpkr \ LTC: ltc1qhsz5nxw6jw9rdtw9qssjeq2h8hqk2f85rdgpkr \
XMR: 42hKayRoEAw4D6G6t8mQHPJHQcXqofjFuVfavqKeNMNUZfeJLJAcNU19i1bGdDvcdN6romiSscWGWJCczFLe9RFhM3d1zpL XMR: 42hKayRoEAw4D6G6t8mQHPJHQcXqofjFuVfavqKeNMNUZfeJLJAcNU19i1bGdDvcdN6romiSscWGWJCczFLe9RFhM3d1zpL
## Contact ## Credits
Feel free to join our [Matrix channel](https://matrix.to/#/#nitter:matrix.org). * Zedeus for this project
* PrivacyDevel, cmj, and taskylizard for keeping this project alive with forks after the main repo went inactive
* Every other contributors who've committed to the main repo in the past
## To any law enforcement agencies and copyright holders
**All illegal content should be reported to Twitter directly.** This service is
merely a proxy of Twitter and no content is hosted on this server. Do not waste
your time contacting internet service providers, hosting providers and/or domain
registrars.
If you would like more context, you can read about this exact issue happening to
[PussTheCat.org's instance](https://pussthecat.org/nitter/).
I emplore all Nitter instance hosts to not enable media proxying, even if it
"phones home" to Twitter's CDN (which doesn't really pose a tracking risk and
breaks videos anyways), as it [has been used as an attack vector to take down
nitter.net](https://github.com/zedeus/nitter/issues/1150#issuecomment-1890855255).

View file

@ -208,6 +208,7 @@ proc parseTweet(js: JsonNode; jsCard: JsonNode = newJNull()): Tweet =
id: js{"id_str"}.getId, id: js{"id_str"}.getId,
threadId: js{"conversation_id_str"}.getId, threadId: js{"conversation_id_str"}.getId,
replyId: js{"in_reply_to_status_id_str"}.getId, replyId: js{"in_reply_to_status_id_str"}.getId,
replyHandle: js{"in_reply_to_screen_name"}.getStr,
text: js{"full_text"}.getStr, text: js{"full_text"}.getStr,
time: js{"created_at"}.getTime, time: js{"created_at"}.getTime,
hasThread: js{"self_thread"}.notNull, hasThread: js{"self_thread"}.notNull,

View file

@ -1,5 +1,5 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
import asyncdispatch, strutils, sequtils, uri, options, sugar import asyncdispatch, strutils, sequtils, uri, options, sugar, strformat
import jester, karax/vdom import jester, karax/vdom
@ -64,30 +64,43 @@ proc createStatusRouter*(cfg: Config) =
resp Http404, showError(error, cfg) resp Http404, showError(error, cfg)
let let
title = pageTitle(conv.tweet) tweet = conv.tweet
ogTitle = pageTitle(conv.tweet.user) title = pageTitle(tweet)
desc = conv.tweet.text ogTitle = pageTitle(tweet.user)
avatar = conv.tweet.user.userPic desc = tweet.text
time = some(conv.tweet.time) avatar = tweet.user.userPic
time = some(tweet.time)
var var
images = conv.tweet.photos images = tweet.photos
video = "" video = ""
context = ""
contextUrl = ""
if conv.tweet.video.isSome(): if tweet.quote.isSome():
let videoObj = get(conv.tweet.video) let
quote = tweet.quote.get()
quoteUser = quote.user
context = &"↘ Quoting: {quoteUser.fullname} (@{quoteUser.username})"
contextUrl = &"{getUrlPrefix(cfg)}/i/status/{quote.id}"
elif tweet.replyId != 0:
context = &"↩ Replying to: @{tweet.replyHandle}"
contextUrl = &"{getUrlPrefix(cfg)}/i/status/{tweet.replyId}"
if tweet.video.isSome():
let videoObj = get(tweet.video)
images = @[videoObj.thumb] images = @[videoObj.thumb]
let vars = videoObj.variants.filterIt(it.contentType == mp4) let vars = videoObj.variants.filterIt(it.contentType == mp4)
# idk why this wont sort when it sorts everywhere else # idk why this wont sort when it sorts everywhere else
#video = vars.sortedByIt(it.bitrate)[^1].url #video = vars.sortedByIt(it.bitrate)[^1].url
video = vars[^1].url video = vars[^1].url
elif conv.tweet.gif.isSome(): elif tweet.gif.isSome():
let gif = get(conv.tweet.gif) let gif = get(tweet.gif)
images = @[gif.thumb] images = @[gif.thumb]
video = getPicUrl(gif.url) video = getPicUrl(gif.url)
#elif conv.tweet.card.isSome(): #elif tweet.card.isSome():
# let card = conv.tweet.card.get() # let card = tweet.card.get()
# if card.image.len > 0: # if card.image.len > 0:
# images = @[card.image] # images = @[card.image]
# elif card.video.isSome(): # elif card.video.isSome():
@ -95,7 +108,8 @@ proc createStatusRouter*(cfg: Config) =
let html = renderConversation(conv, prefs, getPath() & "#m") let html = renderConversation(conv, prefs, getPath() & "#m")
resp renderMain(html, request, cfg, prefs, title, desc, ogTitle, resp renderMain(html, request, cfg, prefs, title, desc, ogTitle,
images=images, video=video, avatar=avatar, time=time) images=images, video=video, avatar=avatar, time=time,
context=context, contextUrl=contextUrl)
get "/@name/@s/@id/@m/?@i?": get "/@name/@s/@id/@m/?@i?":
cond @"s" in ["status", "statuses"] cond @"s" in ["status", "statuses"]

View file

@ -211,6 +211,7 @@ type
text*: string text*: string
time*: DateTime time*: DateTime
reply*: seq[string] reply*: seq[string]
replyHandle*: string
pinned*: bool pinned*: bool
hasThread*: bool hasThread*: bool
available*: bool available*: bool

View file

@ -5,7 +5,7 @@ import karax/[karaxdsl, vdom]
const const
date = staticExec("git show -s --format=\"%cd\" --date=format:\"%Y.%m.%d\"") date = staticExec("git show -s --format=\"%cd\" --date=format:\"%Y.%m.%d\"")
hash = staticExec("git show -s --format=\"%h\"") hash = staticExec("git show -s --format=\"%h\"")
link = "https://github.com/zedeus/nitter/commit/" & hash link = "https://gitdab.com/Cynosphere/nitter/commit/" & hash
version = &"{date}-{hash}" version = &"{date}-{hash}"
var aboutHtml: string var aboutHtml: string

View file

@ -38,7 +38,7 @@ proc renderNavbar(cfg: Config; req: Request; rss, canonical: string): VNode =
proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc=""; proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
video=""; images: seq[string] = @[]; banner=""; ogTitle=""; video=""; images: seq[string] = @[]; banner=""; ogTitle="";
rss=""; canonical=""; avatar=""; rss=""; canonical=""; avatar=""; context=""; contextUrl="";
time: Option[DateTime] = none(DateTime)): VNode = time: Option[DateTime] = none(DateTime)): VNode =
var theme = prefs.theme.toTheme var theme = prefs.theme.toTheme
if "theme" in req.params: if "theme" in req.params:
@ -140,16 +140,28 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
meta(property="og:video:url", content=video) meta(property="og:video:url", content=video)
meta(property="og:video:secure_url", content=video) meta(property="og:video:secure_url", content=video)
meta(property="og:video:type", content="video/mp4") meta(property="og:video:type", content="video/mp4")
var title = encodeUrl(finalizedDesc)
var author = encodeUrl(finalizedTitleText) var
title = encodeUrl(finalizedDesc)
author = encodeUrl(finalizedTitleText)
url = req.path
if len(finalizedDesc) > 67: if len(finalizedDesc) > 67:
title = author title = author
author = encodeUrl(finalizedDesc) author = encodeUrl(finalizedDesc)
verbatim &"<link rel=\"alternate\" href=\"{getUrlPrefix(cfg)}/oembed.json?type=video&provider={encodeUrl(siteName)}&title={title}&user={author}&url={encodeUrl(req.path)}\" type=\"application/json+oembed\" />" if context != "":
#link(rel="alternate", author = encodeUrl(context & "\n") & author
# href=&"{getUrlPrefix(cfg)}/oembed.json?type=video&title={encodeUrl(stripHtml(desc))}&user={encodeUrl(finalizedTitleText)}&url={encodeUrl(req.path)}",
# `type`="application/json+oembed") if contextUrl != "":
url = contextUrl
verbatim &"<link rel=\"alternate\" href=\"{getUrlPrefix(cfg)}/oembed.json?type=video&provider={encodeUrl(siteName)}&title={title}&user={author}&url={encodeUrl(url)}\" type=\"application/json+oembed\" />"
elif context != "" and contextUrl != "":
var
title = encodeUrl(finalizedTitleText)
author = encodeUrl(context)
verbatim &"<link rel=\"alternate\" href=\"{getUrlPrefix(cfg)}/oembed.json?type=video&provider={encodeUrl(siteName)}&title={title}&user={author}&url={encodeUrl(contextUrl)}\" type=\"application/json+oembed\" />"
# this is last so images are also preloaded # this is last so images are also preloaded
# if this is done earlier, Chrome only preloads one image for some reason # if this is done earlier, Chrome only preloads one image for some reason
@ -158,14 +170,15 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
proc renderMain*(body: VNode; req: Request; cfg: Config; prefs=defaultPrefs; proc renderMain*(body: VNode; req: Request; cfg: Config; prefs=defaultPrefs;
titleText=""; desc=""; ogTitle=""; rss=""; video=""; titleText=""; desc=""; ogTitle=""; rss=""; video="";
images: seq[string] = @[]; banner=""; avatar=""; images: seq[string] = @[]; banner=""; avatar=""; context="";
time: Option[DateTime] = none(DateTime)): string = contextUrl = ""; time: Option[DateTime] = none(DateTime)
): string =
let canonical = getTwitterLink(req.path, req.params) let canonical = getTwitterLink(req.path, req.params)
let node = buildHtml(html(lang="en")): let node = buildHtml(html(lang="en")):
renderHead(prefs, cfg, req, titleText, desc, video, images, banner, ogTitle, renderHead(prefs, cfg, req, titleText, desc, video, images, banner, ogTitle,
rss, canonical, avatar, time) rss, canonical, avatar, context, contextUrl, time)
body: body:
renderNavbar(cfg, req, rss, canonical) renderNavbar(cfg, req, rss, canonical)