Bläddra i källkod

Add timeline RSS support

Zed 7 år sedan
förälder
incheckning
a87e278a68
3 ändrade filer med 127 tillägg och 16 borttagningar
  1. 27 16
      src/formatters.nim
  2. 31 0
      src/routes/timeline.nim
  3. 69 0
      src/views/rss.nimf

+ 27 - 16
src/formatters.nim

@@ -15,6 +15,8 @@ const
   twRegex = re"(www.|mobile.)?twitter.com"
   nbsp = $Rune(0x000A0)
 
+const hostname {.strdefine.} = "nitter.net"
+
 proc stripText*(text: string): string =
   text.replace(nbsp, " ").strip()
 
@@ -23,13 +25,17 @@ proc shortLink*(text: string; length=28): string =
   if result.len > length:
     result = result[0 ..< length] & "…"
 
-proc toLink*(url, text: string; class="timeline-link"): string =
-  a(text, class=class, href=url)
+proc toLink*(url, text: string): string =
+  a(text, href=url)
 
-proc reUrlToLink*(m: RegexMatch; s: string): string =
+proc reUrlToShortLink*(m: RegexMatch; s: string): string =
   let url = s[m.group(0)[0]]
   toLink(url, shortLink(url))
 
+proc reUrlToLink*(m: RegexMatch; s: string): string =
+  let url = s[m.group(0)[0]]
+  toLink(url, url.replace(re"https?://(www.)?", ""))
+
 proc reEmailToLink*(m: RegexMatch; s: string): string =
   let url = s[m.group(0)[0]]
   toLink("mailto://" & url, url)
@@ -48,19 +54,9 @@ proc reUsernameToLink*(m: RegexMatch; s: string): string =
 
   pretext & toLink("/" & username, "@" & username)
 
-proc linkifyText*(text: string; prefs: Prefs): string =
-  result = xmltree.escape(stripText(text))
-  result = result.replace(ellipsisRegex, "")
-  result = result.replace(emailRegex, reEmailToLink)
-  result = result.replace(urlRegex, reUrlToLink)
-  result = result.replace(usernameRegex, reUsernameToLink)
-  result = result.replace(re"([^\s\(\n%])<a", "$1 <a")
-  result = result.replace(re"</a>\s+([;.,!\)'%]|&apos;)", "</a>$1")
-  result = result.replace(re"^\. <a", ".<a")
-  if prefs.replaceYouTube.len > 0:
-    result = result.replace(ytRegex, prefs.replaceYouTube)
-  if prefs.replaceTwitter.len > 0:
-    result = result.replace(twRegex, prefs.replaceTwitter)
+proc reUsernameToFullLink*(m: RegexMatch; s: string): string =
+  result = reUsernameToLink(m, s)
+  result = result.replace("href=\"/", &"href=\"https://{hostname}/")
 
 proc replaceUrl*(url: string; prefs: Prefs): string =
   result = url
@@ -69,6 +65,21 @@ proc replaceUrl*(url: string; prefs: Prefs): string =
   if prefs.replaceTwitter.len > 0:
     result = result.replace(twRegex, prefs.replaceTwitter)
 
+proc linkifyText*(text: string; prefs: Prefs; rss=false): string =
+  result = xmltree.escape(stripText(text))
+  result = result.replace(ellipsisRegex, "")
+  result = result.replace(emailRegex, reEmailToLink)
+  if rss:
+    result = result.replace(urlRegex, reUrlToLink)
+    result = result.replace(usernameRegex, reUsernameToFullLink)
+  else:
+    result = result.replace(urlRegex, reUrlToShortLink)
+    result = result.replace(usernameRegex, reUsernameToLink)
+  result = result.replace(re"([^\s\(\n%])<a", "$1 <a")
+  result = result.replace(re"</a>\s+([;.,!\)'%]|&apos;)", "</a>$1")
+  result = result.replace(re"^\. <a", ".<a")
+  result = result.replaceUrl(prefs)
+
 proc stripTwitterUrls*(text: string): string =
   result = text
   result = result.replace(picRegex, "")

+ 31 - 0
src/routes/timeline.nim

@@ -6,6 +6,8 @@ import router_utils
 import ".."/[api, prefs, types, utils, cache, formatters, agents, search]
 import ../views/[general, profile, timeline, status]
 
+include "../views/rss.nimf"
+
 export uri, sequtils
 export router_utils
 export api, cache, formatters, search, agents
@@ -69,6 +71,26 @@ template respTimeline*(timeline: typed) =
     resp Http404, showError("User \"" & @"name" & "\" not found", cfg.title)
   resp timeline
 
+proc showRssTimeline*(name: string): Future[string] {.async.} =
+  var timeline: Timeline
+  var profile: Profile
+  var cachedProfile = hasCachedProfile(name)
+  let agent = getAgent()
+
+  if cachedProfile.isSome:
+    profile = get(cachedProfile)
+
+  if cachedProfile.isSome:
+    timeline = await getTimeline(name, "", agent)
+  else:
+    (profile, timeline) = await getProfileAndTimeline(name, agent, "")
+  cache(profile)
+
+  if profile.username.len == 0:
+    return ""
+
+  return renderTimelineRss(timeline.content, profile)
+
 proc createTimelineRouter*(cfg: Config) =
   setProfileCacheTime(cfg.profileCacheTime)
 
@@ -94,6 +116,15 @@ proc createTimelineRouter*(cfg: Config) =
       respTimeline(await showTimeline(@"name", @"after", some(getMediaQuery(@"name")),
                                       cookiePrefs(), getPath(), cfg.title))
 
+    get "/@name/rss":
+      cond '.' notin @"name"
+      let rss = await showRssTimeline(@"name")
+      if rss.len == 0:
+        resp Http404, showError("User \"" & @"name" & "\" not found", cfg.title)
+      else:
+        resp rss, "application/rss+xml;charset=utf-8"
+
+
     get "/@name/status/@id":
       cond '.' notin @"name"
       let prefs = cookiePrefs()

+ 69 - 0
src/views/rss.nimf

@@ -0,0 +1,69 @@
+#? stdtmpl(subsChar = '$', metaChad = '#')
+#import strutils, xmltree, strformat
+#import ../types, ../utils, ../formatters
+#const hostname {.strdefine.} = "nitter.net"
+#
+#proc renderRssTweet(tweet: Tweet; prefs: Prefs): string =
+#let text = linkifyText(tweet.text, prefs, rss=true)
+#if tweet.quote.isSome and get(tweet.quote).available:
+#let quoteLink = hostname & getLink(get(tweet.quote))
+<p>${text}<br><a href="https://${quoteLink}">${quoteLink}</a></p>
+#else:
+<p>${text}</p>
+#end if
+#if tweet.photos.len > 0:
+<img src="https://${hostname}${getPicUrl(tweet.photos[0])}" width="250" />
+#elif tweet.video.isSome:
+<img src="https://${hostname}${getPicUrl(get(tweet.video).thumb)}" width="250" />
+#elif tweet.gif.isSome:
+#let thumb = &"https://{hostname}{getPicUrl(get(tweet.gif).thumb)}"
+#let url = &"https://{hostname}{getGifUrl(get(tweet.gif).url)}"
+<video poster="${thumb}" autoplay muted loop width="250">
+  <source src="${url}" type="video/mp4"</source></video>
+#end if
+#end proc
+#
+#proc getTitle(tweet: Tweet; prefs: Prefs): string =
+#if tweet.pinned: result = "Pinned: "
+#elif tweet.retweet.isSome: result = "RT: "
+#end if
+#result &= xmltree.escape(replaceUrl(tweet.text, prefs))
+#if result.len > 0: return
+#end if
+#if tweet.photos.len > 0:
+#  result &= "Image"
+#elif tweet.video.isSome:
+#  result &= "Video"
+#elif tweet.gif.isSome:
+#  result &= "Gif"
+#end if
+#end proc
+#
+#proc renderTimelineRss*(tweets: seq[Tweet]; profile: Profile): string =
+#let prefs = Prefs(replaceTwitter: hostname)
+#result = ""
+<?xml version="1.0" encoding="UTF-8"?>
+<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:nitter="http://${hostname}" version="2.0">
+  <channel>
+    <atom:link href="http://${hostname}${profile.username}/rss" rel="self" type="application/rss+xml" />
+    <title>${profile.fullname} / @${profile.username}</title>
+    <link>https://${hostname}${profile.username}</link>
+    <description>Twitter feed for: ${profile.username}. Generated by ${hostname}</description>
+    <language>en-us</language>
+    <ttl>40</ttl>
+    <image>
+        <url>https://${hostname}${getPicUrl(profile.getUserPic(style="_400x400"))}</url>
+    </image>
+    #for tweet in tweets:
+    <item>
+      <title>${getTitle(tweet, prefs)}</title>
+      <creator> (@${tweet.profile.username})</creator>
+      <description><![CDATA[${renderRssTweet(tweet, prefs).strip(chars={'\n'})}]]></description>
+      <pubDate>${getTime(tweet)}</pubDate>
+      <guid>https://${hostname}${getLink(tweet)}</guid>
+      <link>https://${hostname}${getLink(tweet)}</link>
+    </item>
+    #end for
+  </channel>
+</rss>
+#end proc