initial notes support

This commit is contained in:
HookedBehemoth 2022-06-28 14:29:05 +02:00
parent 81ec41328d
commit f4481c7e9a
12 changed files with 248 additions and 9 deletions

View File

@ -40,6 +40,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.article))
proc getListTimeline*(id: string; after=""): Future[Timeline] {.async.} =
if id.len == 0: return
let

View File

@ -23,6 +23,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",

View File

@ -127,14 +127,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:

View File

@ -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"
@ -48,6 +48,7 @@ createListRouter(cfg)
createStatusRouter(cfg)
createSearchRouter(cfg)
createMediaRouter(cfg)
createNotesRouter(cfg)
createEmbedRouter(cfg)
createRssRouter(cfg)
createDebugRouter(cfg)
@ -99,4 +100,5 @@ routes:
extend status, ""
extend media, ""
extend embed, ""
extend notes, ""
extend debug, ""

View File

@ -415,3 +415,56 @@ proc parsePhotoRail*(js: JsonNode): PhotoRail =
if url.len == 0: continue
result.add GalleryPhoto(url: url, tweetId: $t.id)
proc parseGraphArticle*(js: JsonNode): Article =
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
)
for sr in p{"inlineStyleRanges"}:
paragraph.inlineStyleRanges.add ArticleStyleRange(
offset: sr{"offset"}.getInt,
length: sr{"length"}.getInt,
style: 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
else: discard
result.entities.add entity
for media in article{"media"}:
result.media[media{"media_id"}.getStr] =
media{"media_info", "original_img_url"}.getStr

17
src/routes/notes.nim Normal file
View File

@ -0,0 +1,17 @@
# SPDX-License-Identifier: AGPL-3.0-only
import asyncdispatch
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
prefs = cookiePrefs()
article = await getGraphArticle(@"id")
note = renderNote(article, prefs)
resp renderMain(note, request, cfg, prefs, titleText=article.title)

View File

@ -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()

View File

@ -7,6 +7,7 @@
@import 'inputs';
@import 'timeline';
@import 'search';
@import 'note';
body {
// colors

44
src/sass/note.scss Normal file
View File

@ -0,0 +1,44 @@
.note {
max-width: 600px;
margin: 0 auto;
background-color: var(--bg_panel);
article {
padding: 20px;
}
img.cover {
margin: 0;
max-width: 100%;
}
h1 {
display:inherit;
font-size: 2.5rem;
margin: 30px 0;
}
p {
font-size: 18px;
font-family: sans-serif;
line-height: 1.5em;
margin: 30px 0;
word-wrap: break-word;
white-space: break-spaces;
.image {
text-align: center;
width: 100%;
img {
max-width: 100%;
border-radius: 20px;
margin: 0 auto;
}
}
}
iframe {
width: 100%;
height: 400px;
}
}

View File

@ -18,6 +18,7 @@ type
listMembers
userRestId
status
article
RateLimit* = object
remaining*: int
@ -115,6 +116,42 @@ type
PhotoRail* = seq[GalleryPhoto]
Article* = object
title*: string
coverImage*: string
user*: User
time*: DateTime
paragraphs*: seq[ArticleParagraph]
entities*: seq[ArticleEntity]
media*: Table[string, string]
ArticleParagraph* = object
text*: string
inlineStyleRanges*: seq[ArticleStyleRange]
entityRanges*: seq[ArticleEntityRange]
ArticleStyleRange* = object
offset*: int
length*: int
style*: string
ArticleEntityRange* = object
offset*: int
length*: int
key*: int
ArticleEntity* = object
entityType*: ArticleEntityType
url*: string
mediaIds*: seq[string]
tweetId*: string
ArticleEntityType* {.pure.} = enum
link = "LINK"
media = "MEDIA"
tweet = "TWEET"
unknown
Poll* = object
options*: seq[string]
values*: seq[int]

78
src/views/notes.nim Normal file
View File

@ -0,0 +1,78 @@
# SPDX-License-Identifier: AGPL-3.0-only
import strutils, tables, strformat
import karax/[karaxdsl, vdom, vstyles]
from jester import Request
import renderutils
import ".."/[types, utils, formatters]
const doctype = "<!DOCTYPE html>\n"
proc getSmallPic(url: string): string =
result = url
if "?" notin url and not url.endsWith("placeholder.png"):
result &= "?name=small"
result = getPicUrl(result)
proc renderMiniAvatar(user: User; prefs: Prefs): VNode =
let url = getPicUrl(user.getUserPic("_mini"))
buildHtml():
img(class=(prefs.getAvatarClass & " mini"), src=url)
proc renderNoteParagraph(articleParagraph: ArticleParagraph; article: Article): VNode =
let text = articleParagraph.text
result = p.newVNode()
if articleParagraph.inlineStyleRanges.len > 0:
# Assume the style applies for the entire paragraph
result.setAttr("style", "font-style:" & articleParagraph.inlineStyleRanges[0].style.toLowerAscii)
var last = 0
for er in articleParagraph.entityRanges:
# flush remaining text
if er.offset > last:
result.add text text.substr(last, er.offset - 1)
let entity = article.entities[er.key]
case entity.entityType
of ArticleEntityType.link:
let link = buildHtml(a(href=entity.url)):
text text.substr(er.offset, er.offset + er.length - 1)
result.add link
of ArticleEntityType.media:
for id in entity.mediaIds:
let url: string = article.media[id]
let image = buildHtml(span(class="image")):
img(src=url, alt="")
result.add image
of ArticleEntityType.tweet:
let url = fmt"/i/status/{entity.tweetId}/embed"
let iframe = buildHtml(iframe(src=url, loading="lazy", frameborder="0", style={maxWidth: "100%"}))
result.add iframe
else: discard
last = er.offset + er.length
# flush remaining text
if last < text.len:
result.add text text.substr(last)
proc renderNote*(article: Article; prefs: Prefs): VNode =
let cover = getSmallPic(article.coverImage)
let author = article.user
buildHtml(tdiv(class="note")):
img(class="cover", src=(cover), alt="")
article:
h1: text article.title
tdiv(class="author"):
renderMiniAvatar(author, prefs)
linkUser(author, class="fullname")
linkUser(author, class="username")
text " · "
text article.time.getShortTime
for paragraph in article.paragraphs:
renderNoteParagraph(paragraph, article)

View File

@ -45,7 +45,7 @@ proc renderHeader(tweet: Tweet; retweet: string; prefs: Prefs): VNode =
span(class="tweet-date"):
a(href=getLink(tweet), title=tweet.getTime):
text tweet.getShortTime
text tweet.time.getShortTime
proc renderAlbum(tweet: Tweet): VNode =
let
@ -261,7 +261,7 @@ proc renderQuote(quote: Tweet; prefs: Prefs; path: string): VNode =
span(class="tweet-date"):
a(href=getLink(quote), title=quote.getTime):
text quote.getShortTime
text quote.time.getShortTime
if quote.reply.len > 0:
renderReply(quote)