Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4d6fdda
initial notes support
HookedBehemoth Jun 28, 2022
dd8efcb
notes: use runeSubStr for ranges
HookedBehemoth Jun 28, 2022
f5ea8ec
notes: implement lists, headers and twemoji
HookedBehemoth Jun 28, 2022
075e905
notes: replace hashtags and mentions
HookedBehemoth Jun 28, 2022
feeeb60
notes: don't replace text in links
HookedBehemoth Jun 28, 2022
12788ac
prevent karax from inserting whitespaces to fix wrapping
HookedBehemoth Jun 28, 2022
d8ddb29
notes: proxy small images
HookedBehemoth Jun 28, 2022
c798931
add twemoji domain to whitelist
HookedBehemoth Jun 28, 2022
0e9d7ae
notes: address review
HookedBehemoth Jun 29, 2022
4de795b
notes: properly flush list
HookedBehemoth Jun 29, 2022
5bcce76
notes: use userRestId api token pool
HookedBehemoth Jun 29, 2022
fd1ad96
notes: match twemoji size with font size
HookedBehemoth Jun 29, 2022
e52f8aa
notes: ignore empty media
HookedBehemoth Jun 29, 2022
ac9e6f2
notes: render tweets inline
HookedBehemoth Jun 30, 2022
fd8e267
notes: fix width to 600px
HookedBehemoth Jun 30, 2022
8ac79ef
notes: support "gifs"
HookedBehemoth Jun 30, 2022
2af64de
notes: treat tweets as mainTweet
HookedBehemoth Jun 30, 2022
b304d22
notes: fetch tweets in a nim idiomatic way
HookedBehemoth Jul 1, 2022
953089a
notes: return 404 on errors
HookedBehemoth Jul 1, 2022
90f8b07
notes: use absolute paths in style sheet to not apply to embeds
HookedBehemoth Jul 1, 2022
eebaa7b
notes: don't emit paragraphs for atomic blocks
HookedBehemoth Jul 1, 2022
0ed9414
notes: loop "gifs"
HookedBehemoth Jul 2, 2022
b6aff96
notes: bump stylesheet version
HookedBehemoth Jul 2, 2022
c9552a8
notes: support inlineStyles in the worst way possible
HookedBehemoth Jul 22, 2022
22113d6
notes: fix inlineStyle length
HookedBehemoth Jul 23, 2022
4b2c8bb
notes: properly fix inlineStyle length this time
HookedBehemoth Jul 23, 2022
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
6 changes: 6 additions & 0 deletions src/api.nim
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ proc getGraphListMembers*(list: List; after=""): Future[Result[User]] {.async.}
let url = graphListMembers ? {"variables": $variables}
result = parseGraphListMembers(await fetchRaw(url, Api.listMembers), after)

proc getGraphArticle*(id: string): Future[Article] {.async.} =
let
variables = %*{"twitterArticleId": id}
url = graphArticle ? {"variables": $variables}
result = parseGraphArticle(await fetch(url, Api.userRestId))

proc getListTimeline*(id: string; after=""): Future[Timeline] {.async.} =
if id.len == 0: return
let
Expand Down
1 change: 1 addition & 0 deletions src/consts.nim
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const
graphList* = graphql / "JADTh6cjebfgetzvF3tQvQ/List"
graphListBySlug* = graphql / "ErWsz9cObLel1BF-HjuBlA/ListBySlug"
graphListMembers* = graphql / "Ke6urWMeCV2UlKXGRy4sow/ListMembers"
graphArticle* = graphql / "rJMGbcr9LTsjVycjUmcnEg/TwitterArticleByRestId"

timelineParams* = {
"include_profile_interstitial_type": "0",
Expand Down
19 changes: 14 additions & 5 deletions src/formatters.nim
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ let
userPicRegex = re"_(normal|bigger|mini|200x200|400x400)(\.[A-z]+)$"
extRegex = re"(\.[A-z]+)$"
illegalXmlRegex = re"(*UTF8)[^\x09\x0A\x0D\x20-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]"
hashtagRegex = re"\B#(\w*[A-Za-z]\w*)\b"
mentionRegex = re"\B@(\w{1,15})\b"

proc getUrlPrefix*(cfg: Config): string =
if cfg.useHttps: https & cfg.hostname
Expand Down Expand Up @@ -72,6 +74,13 @@ proc replaceUrls*(body: string; prefs: Prefs; absolute=""): string =
if absolute.len > 0 and "href" in result:
result = result.replace("href=\"/", &"href=\"{absolute}/")

proc replaceHashtagsAndMentions*(body: string): string =
result = body
result = result.replacef(hashtagRegex, a(
"#$1", href = "/search?q=%23$1"))
result = result.replacef(mentionRegex, a(
"@$1", href = "/$1"))

proc getM3u8Url*(content: string): string =
var matches: array[1, string]
if re.find(content, m3u8Regex, matches) != -1:
Expand Down Expand Up @@ -121,14 +130,14 @@ proc getTime*(tweet: Tweet): string =
proc getRfc822Time*(tweet: Tweet): string =
tweet.time.format("ddd', 'dd MMM yyyy HH:mm:ss 'GMT'")

proc getShortTime*(tweet: Tweet): string =
proc getShortTime*(time: DateTime): string =
let now = now()
let since = now - tweet.time
let since = now - time

if now.year != tweet.time.year:
result = tweet.time.format("d MMM yyyy")
if now.year != time.year:
result = time.format("d MMM yyyy")
elif since.inDays >= 1:
result = tweet.time.format("MMM d")
result = time.format("MMM d")
elif since.inHours >= 1:
result = $since.inHours & "h"
elif since.inMinutes >= 1:
Expand Down
4 changes: 3 additions & 1 deletion src/nitter.nim
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import types, config, prefs, formatters, redis_cache, http_pool, tokens
import views/[general, about]
import routes/[
preferences, timeline, status, media, search, rss, list, debug,
unsupported, embed, resolver, router_utils]
unsupported, embed, notes, resolver, router_utils]

const instancesUrl = "https://github.com/zedeus/nitter/wiki/Instances"
const issuesUrl = "https://github.com/zedeus/nitter/issues"
Expand Down Expand Up @@ -48,6 +48,7 @@ createListRouter(cfg)
createStatusRouter(cfg)
createSearchRouter(cfg)
createMediaRouter(cfg)
createNotesRouter(cfg)
createEmbedRouter(cfg)
createRssRouter(cfg)
createDebugRouter(cfg)
Expand Down Expand Up @@ -100,4 +101,5 @@ routes:
extend status, ""
extend media, ""
extend embed, ""
extend notes, ""
extend debug, ""
68 changes: 68 additions & 0 deletions src/parser.nim
Original file line number Diff line number Diff line change
Expand Up @@ -435,3 +435,71 @@ proc parseGraphConversation*(js: JsonNode; tweetId: string): Conversation =
result.replies.content.add thread
elif entryId.startsWith("cursor-bottom"):
result.replies.bottom = e{"content", "itemContent", "value"}.getStr

proc parseGraphArticle*(js: JsonNode): Article =
if not js{"errors"}.isNull:
return

let article = js{"data", "twitterArticle"}
let meta = article{"metadata"}

result = Article(
title: article{"title"}.getStr,
coverImage: article{"cover_image", "media_info", "original_img_url"}.getStr,
user: meta{"authorResults", "result", "legacy"}.parseUser,
time: meta{"publishedAtMs"}.getStr.parseInt.div(1000).fromUnix.utc,
)

let
content = article{"data", "contentStateJson"}.getStr.parseJson

for p in content{"blocks"}:
var paragraph = ArticleParagraph(
text: p{"text"}.getStr,
baseType: parseEnum[ArticleType](p{"type"}.getStr)
)
for sr in p{"inlineStyleRanges"}:
paragraph.inlineStyleRanges.add ArticleStyleRange(
offset: sr{"offset"}.getInt,
length: sr{"length"}.getInt,
style: parseEnum[ArticleStyle](sr{"style"}.getStr)
)
for er in p{"entityRanges"}:
paragraph.entityRanges.add ArticleEntityRange(
offset: er{"offset"}.getInt,
length: er{"length"}.getInt,
key: er{"key"}.getInt
)
result.paragraphs.add paragraph

# Note: This is a map but the indices are integers so it's fine.
for _, jEntity in content{"entityMap"}:
var entity = ArticleEntity(
entityType: parseEnum[ArticleEntityType] jEntity{"type"}.getStr,
)
case entity.entityType
of ArticleEntityType.link:
entity.url = jEntity{"data", "url"}.getStr
of ArticleEntityType.media:
for jMedia in jEntity{"data", "mediaItems"}:
entity.mediaIds.add jMedia{"mediaId"}.getStr
of ArticleEntityType.tweet:
entity.tweetId = jEntity{"data", "tweetId"}.getStr
of ArticleEntityType.twemoji:
entity.twemoji = jEntity{"data", "url"}.getStr
else: discard

result.entities.add entity

for m in article{"media"}:
let mediaInfo = m{"media_info"}
var media = ArticleMedia(
mediaType: parseEnum[ArticleMediaType](mediaInfo{"__typename"}.getStr)
)
case media.mediaType
of ArticleMediaType.image:
media.url = mediaInfo{"original_img_url"}.getStr
of ArticleMediaType.gif:
media.url = mediaInfo{"variants"}[0]{"url"}.getStr
else: discard
result.media[m{"media_id"}.getStr] = media
34 changes: 34 additions & 0 deletions src/routes/notes.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# SPDX-License-Identifier: AGPL-3.0-only
import asyncdispatch, tables, asyncfutures, options
import jester, karax/vdom
import ".."/[types, api]
import ../views/[notes, tweet, general]
import router_utils

export api, notes, vdom, tweet, general, router_utils

proc createNotesRouter*(cfg: Config) =
router notes:
get "/i/notes/@id":
let article = await getGraphArticle(@"id")

if article == nil:
resp Http404

var tweetFutures: seq[Future[Conversation]]
for e in article.entities:
if e.entityType == ArticleEntityType.tweet:
tweetFutures.add getTweet(e.tweetId)

let convs = await tweetFutures.all

var tweets = initTable[int64, Tweet]()
for c in convs:
if c != nil and c.tweet != nil:
tweets[c.tweet.id] = c.tweet

let
path = getPath()
prefs = cookiePrefs()
note = renderNote(article, tweets, path, prefs)
resp renderMain(note, request, cfg, prefs, titleText=article.title)
2 changes: 1 addition & 1 deletion src/routes/unsupported.nim
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ proc createUnsupportedRouter*(cfg: Config) =
feature()

get "/i/@i?/?@j?":
cond @"i" notin ["status", "lists" , "user"]
cond @"i" notin ["status", "lists" , "user", "notes"]
feature()
1 change: 1 addition & 0 deletions src/sass/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
@import 'inputs';
@import 'timeline';
@import 'search';
@import 'note';

body {
// colors
Expand Down
60 changes: 60 additions & 0 deletions src/sass/note.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
.note {
width: 600px;
margin: 0 auto;
background-color: var(--bg_panel);
font-family: sans-serif;

img.cover {
margin: 0;
width: 100%;
}

article {
padding: 20px;

&>h1 {
display: inherit;
font-size: 2.5rem;
margin: 30px 0;
}

&>p,
li {
font-size: 18px;
}

&>p {
line-height: 1.5em;
margin: 30px 0;
word-wrap: break-word;
white-space: break-spaces;
}

&>span.image {
text-align: center;
width: 100%;

img,
video {
max-width: 100%;
border-radius: 20px;
margin: 0 auto;
}
}

img.twemoji {
width: 18px;
height: 18px;
}

&>ul>li,
&>ol>li {
line-height: 2em;
}

&>iframe {
width: 100%;
height: 400px;
}
}
}
65 changes: 65 additions & 0 deletions src/types.nim
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type
protectedUser = 22
couldntAuth = 32
doesntExist = 34
invalidPermission = 37
userNotFound = 50
suspended = 63
rateLimited = 88
Expand Down Expand Up @@ -120,6 +121,70 @@ type

PhotoRail* = seq[GalleryPhoto]

Article* = ref object
title*: string
coverImage*: string
user*: User
time*: DateTime
paragraphs*: seq[ArticleParagraph]
entities*: seq[ArticleEntity]
media*: Table[string, ArticleMedia]

ArticleParagraph* = object
text*: string
baseType*: ArticleType
inlineStyleRanges*: seq[ArticleStyleRange]
entityRanges*: seq[ArticleEntityRange]

ArticleType* {.pure.} = enum
headerOne = "header-one"
headerTwo = "header-two"
headerThree = "header-three"
orderedListItem = "ordered-list-item"
unorderedListItem = "unordered-list-item"
unstyled = "unstyled"
atomic = "atomic"
unknown

ArticleStyleRange* = object
offset*: int
length*: int
style*: ArticleStyle

ArticleStyle* {.pure.} = enum
bold = "BOLD"
italic = "ITALIC"
strikethrough = "STRIKETHROUGH"
unknown

ArticleEntityRange* = object
offset*: int
length*: int
key*: int

ArticleEntity* = object
entityType*: ArticleEntityType
url*: string
mediaIds*: seq[string]
tweetId*: string
twemoji*: string

ArticleEntityType* {.pure.} = enum
link = "LINK"
media = "MEDIA"
tweet = "TWEET"
twemoji = "TWEMOJI"
unknown

ArticleMedia* = object
mediaType*: ArticleMediaType
url*: string

ArticleMediaType* {.pure.} = enum
image = "ApiImage"
gif = "ApiGif"
unknown

Poll* = object
options*: seq[string]
values*: seq[int]
Expand Down
1 change: 1 addition & 0 deletions src/utils.nim
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const
"pic.twitter.com",
"twimg.com",
"abs.twimg.com",
"abs-0.twimg.com",
"pbs.twimg.com",
"video.twimg.com"
]
Expand Down
2 changes: 1 addition & 1 deletion src/views/general.nim
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
let opensearchUrl = getUrlPrefix(cfg) & "/opensearch"

buildHtml(head):
link(rel="stylesheet", type="text/css", href="/css/style.css?v=18")
link(rel="stylesheet", type="text/css", href="/css/style.css?v=19")
link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=2")

if theme.len > 0:
Expand Down
Loading