nitter.nim 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. import asyncdispatch, asyncfile, httpclient, uri, os
  2. import sequtils, strformat, strutils
  3. from net import Port
  4. import jester, regex
  5. import api, utils, types, cache, formatters, search, config, prefs, agents
  6. import views/[general, profile, status, preferences]
  7. const configPath {.strdefine.} = "./nitter.conf"
  8. let cfg = getConfig(configPath)
  9. proc showSingleTimeline(name, after, agent: string; query: Option[Query];
  10. prefs: Prefs): Future[string] {.async.} =
  11. let railFut = getPhotoRail(name, agent)
  12. var timeline: Timeline
  13. var profile: Profile
  14. var cachedProfile = hasCachedProfile(name)
  15. if cachedProfile.isSome:
  16. profile = get(cachedProfile)
  17. if query.isNone:
  18. if cachedProfile.isSome:
  19. timeline = await getTimeline(name, after, agent)
  20. else:
  21. (profile, timeline) = await getProfileAndTimeline(name, agent, after)
  22. cache(profile)
  23. else:
  24. var timelineFut = getTimelineSearch(get(query), after, agent)
  25. if cachedProfile.isNone:
  26. profile = await getCachedProfile(name, agent)
  27. timeline = await timelineFut
  28. if profile.username.len == 0:
  29. return ""
  30. let profileHtml = renderProfile(profile, timeline, await railFut, prefs)
  31. return renderMain(profileHtml, prefs, cfg.title, pageTitle(profile), pageDesc(profile))
  32. proc showMultiTimeline(names: seq[string]; after, agent: string; query: Option[Query];
  33. prefs: Prefs): Future[string] {.async.} =
  34. var q = query
  35. if q.isSome:
  36. get(q).fromUser = names
  37. else:
  38. q = some(Query(kind: multi, fromUser: names, excludes: @["replies"]))
  39. var timeline = renderMulti(await getTimelineSearch(get(q), after, agent),
  40. names.join(","), prefs)
  41. return renderMain(timeline, prefs, cfg.title, "Multi")
  42. proc showTimeline(name, after: string; query: Option[Query];
  43. prefs: Prefs): Future[string] {.async.} =
  44. let agent = getAgent()
  45. let names = name.strip(chars={'/'}).split(",").filterIt(it.len > 0)
  46. if names.len == 1:
  47. return await showSingleTimeline(names[0], after, agent, query, prefs)
  48. else:
  49. return await showMultiTimeline(names, after, agent, query, prefs)
  50. template respTimeline(timeline: typed) =
  51. if timeline.len == 0:
  52. resp Http404, showError("User \"" & @"name" & "\" not found", cfg.title)
  53. resp timeline
  54. template cookiePrefs(): untyped {.dirty.} =
  55. getPrefs(request.cookies.getOrDefault("preferences"))
  56. setProfileCacheTime(cfg.profileCacheTime)
  57. settings:
  58. port = Port(cfg.port)
  59. staticDir = cfg.staticDir
  60. bindAddr = cfg.address
  61. routes:
  62. get "/":
  63. resp renderMain(renderSearch(), Prefs(), cfg.title)
  64. post "/search":
  65. if @"query".len == 0:
  66. resp Http404, showError("Please enter a username.", cfg.title)
  67. redirect("/" & @"query")
  68. post "/saveprefs":
  69. var prefs = cookiePrefs()
  70. genUpdatePrefs()
  71. setCookie("preferences", $prefs.id, daysForward(360), httpOnly=true, secure=cfg.useHttps)
  72. redirect(decodeUrl(@"referer"))
  73. post "/resetprefs":
  74. var prefs = cookiePrefs()
  75. resetPrefs(prefs)
  76. setCookie("preferences", $prefs.id, daysForward(360), httpOnly=true, secure=cfg.useHttps)
  77. redirect("/settings")
  78. get "/settings":
  79. let refUri = request.headers.getOrDefault("Referer").parseUri()
  80. var path =
  81. if refUri.path.len > 0 and "/settings" notin refUri.path: refUri.path
  82. else: "/"
  83. if refUri.query.len > 0: path &= &"?{refUri.query}"
  84. let prefs = cookiePrefs()
  85. resp renderMain(renderPreferences(prefs, path), prefs, cfg.title, "Preferences")
  86. get "/@name/?":
  87. cond '.' notin @"name"
  88. respTimeline(await showTimeline(@"name", @"after", none(Query), cookiePrefs()))
  89. get "/@name/search":
  90. cond '.' notin @"name"
  91. let prefs = cookiePrefs()
  92. let query = initQuery(@"filter", @"include", @"not", @"sep", @"name")
  93. respTimeline(await showTimeline(@"name", @"after", some(query), cookiePrefs()))
  94. get "/@name/replies":
  95. cond '.' notin @"name"
  96. let prefs = cookiePrefs()
  97. respTimeline(await showTimeline(@"name", @"after", some(getReplyQuery(@"name")), cookiePrefs()))
  98. get "/@name/media":
  99. cond '.' notin @"name"
  100. let prefs = cookiePrefs()
  101. respTimeline(await showTimeline(@"name", @"after", some(getMediaQuery(@"name")), cookiePrefs()))
  102. get "/@name/status/@id":
  103. cond '.' notin @"name"
  104. let prefs = cookiePrefs()
  105. let conversation = await getTweet(@"name", @"id", getAgent())
  106. if conversation == nil or conversation.tweet.id.len == 0:
  107. resp Http404, showError("Tweet not found", cfg.title)
  108. let title = pageTitle(conversation.tweet.profile)
  109. let desc = conversation.tweet.text
  110. let html = renderConversation(conversation, prefs)
  111. if conversation.tweet.video.isSome():
  112. let thumb = get(conversation.tweet.video).thumb
  113. let vidUrl = getVideoEmbed(conversation.tweet.id)
  114. resp renderMain(html, prefs, cfg.title, title, desc, images = @[thumb],
  115. `type`="video", video=vidUrl)
  116. elif conversation.tweet.gif.isSome():
  117. let thumb = get(conversation.tweet.gif).thumb
  118. let vidUrl = getVideoEmbed(conversation.tweet.id)
  119. resp renderMain(html, prefs, cfg.title, title, desc, images = @[thumb],
  120. `type`="video", video=vidUrl)
  121. else:
  122. resp renderMain(html, prefs, cfg.title, title, desc, images=conversation.tweet.photos)
  123. get "/i/web/status/@id":
  124. redirect("/i/status/" & @"id")
  125. get "/pic/@sig/@url":
  126. cond "http" in @"url"
  127. cond "twimg" in @"url"
  128. let
  129. uri = parseUri(decodeUrl(@"url"))
  130. path = uri.path.split("/")[2 .. ^1].join("/")
  131. filename = cfg.cacheDir / cleanFilename(path & uri.query)
  132. if getHmac($uri) != @"sig":
  133. resp showError("Failed to verify signature", cfg.title)
  134. if not existsDir(cfg.cacheDir):
  135. createDir(cfg.cacheDir)
  136. if not existsFile(filename):
  137. let client = newAsyncHttpClient()
  138. await client.downloadFile($uri, filename)
  139. client.close()
  140. if not existsFile(filename):
  141. resp Http404
  142. let file = openAsync(filename)
  143. let buf = await readAll(file)
  144. file.close()
  145. resp buf, mimetype(filename)
  146. get "/video/@sig/@url":
  147. cond "http" in @"url"
  148. var url = decodeUrl(@"url")
  149. let prefs = cookiePrefs()
  150. if getHmac(url) != @"sig":
  151. resp showError("Failed to verify signature", cfg.title)
  152. let client = newAsyncHttpClient()
  153. var content = await client.getContent(url)
  154. if ".vmap" in url:
  155. var m: RegexMatch
  156. discard content.find(re"""url="(.+.m3u8)"""", m)
  157. url = decodeUrl(content[m.group(0)[0]])
  158. content = await client.getContent(url)
  159. if ".m3u8" in url:
  160. content = proxifyVideo(content, prefs.proxyVideos)
  161. client.close()
  162. resp content, mimetype(url)
  163. runForever()