nitter.nim 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  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; path: string): 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, path)
  31. return renderMain(profileHtml, prefs, cfg.title, pageTitle(profile),
  32. pageDesc(profile), path)
  33. proc showMultiTimeline(names: seq[string]; after, agent: string; query: Option[Query];
  34. prefs: Prefs; path: string): Future[string] {.async.} =
  35. var q = query
  36. if q.isSome:
  37. get(q).fromUser = names
  38. else:
  39. q = some(Query(kind: multi, fromUser: names, excludes: @["replies"]))
  40. var timeline = renderMulti(await getTimelineSearch(get(q), after, agent),
  41. names.join(","), prefs, path)
  42. return renderMain(timeline, prefs, cfg.title, "Multi")
  43. proc showTimeline(name, after: string; query: Option[Query];
  44. prefs: Prefs; path: string): Future[string] {.async.} =
  45. let agent = getAgent()
  46. let names = name.strip(chars={'/'}).split(",").filterIt(it.len > 0)
  47. if names.len == 1:
  48. return await showSingleTimeline(names[0], after, agent, query, prefs, path)
  49. else:
  50. return await showMultiTimeline(names, after, agent, query, prefs, path)
  51. template respTimeline(timeline: typed) =
  52. if timeline.len == 0:
  53. resp Http404, showError("User \"" & @"name" & "\" not found", cfg.title)
  54. resp timeline
  55. template cookiePrefs(): untyped {.dirty.} =
  56. getPrefs(request.cookies.getOrDefault("preferences"))
  57. template getPath(): untyped {.dirty.} =
  58. $(parseUri(request.path) ? filterParams(request.params))
  59. template refPath(): untyped {.dirty.} =
  60. if @"referer".len > 0: @"referer" else: "/"
  61. setProfileCacheTime(cfg.profileCacheTime)
  62. settings:
  63. port = Port(cfg.port)
  64. staticDir = cfg.staticDir
  65. bindAddr = cfg.address
  66. routes:
  67. get "/":
  68. resp renderMain(renderSearch(), Prefs(), cfg.title)
  69. post "/search":
  70. if @"query".len == 0:
  71. resp Http404, showError("Please enter a username.", cfg.title)
  72. redirect("/" & @"query")
  73. get "/settings":
  74. let prefs = cookiePrefs()
  75. let path = refPath()
  76. resp renderMain(renderPreferences(prefs, path), prefs, cfg.title,
  77. "Preferences", path)
  78. post "/saveprefs":
  79. var prefs = cookiePrefs()
  80. genUpdatePrefs()
  81. setCookie("preferences", $prefs.id, daysForward(360), httpOnly=true, secure=cfg.useHttps)
  82. redirect(refPath())
  83. post "/resetprefs":
  84. var prefs = cookiePrefs()
  85. resetPrefs(prefs)
  86. setCookie("preferences", $prefs.id, daysForward(360), httpOnly=true, secure=cfg.useHttps)
  87. redirect($(parseUri("/settings") ? filterParams(request.params)))
  88. post "/enablehls":
  89. var prefs = cookiePrefs()
  90. prefs.hlsPlayback = true
  91. cache(prefs)
  92. setCookie("preferences", $prefs.id, daysForward(360), httpOnly=true, secure=cfg.useHttps)
  93. redirect(refPath())
  94. get "/@name/?":
  95. cond '.' notin @"name"
  96. respTimeline(await showTimeline(@"name", @"after", none(Query),
  97. cookiePrefs(), getPath()))
  98. get "/@name/search":
  99. cond '.' notin @"name"
  100. let prefs = cookiePrefs()
  101. let query = initQuery(@"filter", @"include", @"not", @"sep", @"name")
  102. respTimeline(await showTimeline(@"name", @"after", some(query),
  103. cookiePrefs(), getPath()))
  104. get "/@name/replies":
  105. cond '.' notin @"name"
  106. let prefs = cookiePrefs()
  107. respTimeline(await showTimeline(@"name", @"after", some(getReplyQuery(@"name")),
  108. cookiePrefs(), getPath()))
  109. get "/@name/media":
  110. cond '.' notin @"name"
  111. let prefs = cookiePrefs()
  112. respTimeline(await showTimeline(@"name", @"after", some(getMediaQuery(@"name")),
  113. cookiePrefs(), getPath()))
  114. get "/@name/status/@id":
  115. cond '.' notin @"name"
  116. let prefs = cookiePrefs()
  117. let conversation = await getTweet(@"name", @"id", getAgent())
  118. if conversation == nil or conversation.tweet.id.len == 0:
  119. resp Http404, showError("Tweet not found", cfg.title)
  120. let path = getPath()
  121. let title = pageTitle(conversation.tweet.profile)
  122. let desc = conversation.tweet.text
  123. let html = renderConversation(conversation, prefs, path)
  124. if conversation.tweet.video.isSome():
  125. let thumb = get(conversation.tweet.video).thumb
  126. let vidUrl = getVideoEmbed(conversation.tweet.id)
  127. resp renderMain(html, prefs, cfg.title, title, desc, path, images = @[thumb],
  128. `type`="video", video=vidUrl)
  129. elif conversation.tweet.gif.isSome():
  130. let thumb = get(conversation.tweet.gif).thumb
  131. let vidUrl = getVideoEmbed(conversation.tweet.id)
  132. resp renderMain(html, prefs, cfg.title, title, desc, path, images = @[thumb],
  133. `type`="video", video=vidUrl)
  134. else:
  135. resp renderMain(html, prefs, cfg.title, title, desc, path, images=conversation.tweet.photos)
  136. get "/i/web/status/@id":
  137. redirect("/i/status/" & @"id")
  138. get "/pic/@sig/@url":
  139. cond "http" in @"url"
  140. cond "twimg" in @"url"
  141. let
  142. uri = parseUri(decodeUrl(@"url"))
  143. path = uri.path.split("/")[2 .. ^1].join("/")
  144. filename = cfg.cacheDir / cleanFilename(path & uri.query)
  145. if getHmac($uri) != @"sig":
  146. resp showError("Failed to verify signature", cfg.title)
  147. if not existsDir(cfg.cacheDir):
  148. createDir(cfg.cacheDir)
  149. if not existsFile(filename):
  150. let client = newAsyncHttpClient()
  151. await client.downloadFile($uri, filename)
  152. client.close()
  153. if not existsFile(filename):
  154. resp Http404
  155. let file = openAsync(filename)
  156. let buf = await readAll(file)
  157. file.close()
  158. resp buf, mimetype(filename)
  159. get "/video/@sig/@url":
  160. cond "http" in @"url"
  161. var url = decodeUrl(@"url")
  162. let prefs = cookiePrefs()
  163. if getHmac(url) != @"sig":
  164. resp showError("Failed to verify signature", cfg.title)
  165. let client = newAsyncHttpClient()
  166. var content = await client.getContent(url)
  167. if ".vmap" in url:
  168. var m: RegexMatch
  169. discard content.find(re"""url="(.+.m3u8)"""", m)
  170. url = decodeUrl(content[m.group(0)[0]])
  171. content = await client.getContent(url)
  172. if ".m3u8" in url:
  173. content = proxifyVideo(content, prefs.proxyVideos)
  174. client.close()
  175. resp content, mimetype(url)
  176. runForever()