nitter.nim 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  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, title=cfg.title, titleText=pageTitle(profile),
  32. desc=pageDesc(profile))
  33. proc showMultiTimeline(names: seq[string]; after, agent: string; query: Option[Query];
  34. prefs: Prefs): 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)
  42. return renderMain(timeline, prefs, title=cfg.title, titleText="Multi")
  43. proc showTimeline(name, after: string; query: Option[Query];
  44. prefs: Prefs): 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)
  49. else:
  50. return await showMultiTimeline(names, after, agent, query, prefs)
  51. template respTimeline(timeline: typed) =
  52. if timeline.len == 0:
  53. resp Http404, showError("User \"" & @"name" & "\" not found", cfg.title, prefs)
  54. resp timeline
  55. proc getCookiePrefs(request: Request): Prefs =
  56. getPrefs(request.cookies.getOrDefault("preferences"))
  57. setProfileCacheTime(cfg.profileCacheTime)
  58. settings:
  59. port = Port(cfg.port)
  60. staticDir = cfg.staticDir
  61. bindAddr = cfg.address
  62. routes:
  63. get "/":
  64. let prefs = getCookiePrefs(request)
  65. resp renderMain(renderSearch(), prefs, title=cfg.title)
  66. post "/search":
  67. if @"query".len == 0:
  68. resp Http404, showError("Please enter a username.", cfg.title,
  69. getCookiePrefs(request))
  70. redirect("/" & @"query")
  71. post "/saveprefs":
  72. var prefs = getCookiePrefs(request)
  73. genUpdatePrefs()
  74. setCookie("preferences", $prefs.id, daysForward(360), httpOnly=true, secure=true)
  75. redirect("/settings")
  76. get "/settings":
  77. let prefs = getCookiePrefs(request)
  78. resp renderMain(renderPreferences(prefs), prefs, title=cfg.title, titleText="Preferences")
  79. get "/@name/?":
  80. cond '.' notin @"name"
  81. let prefs = getCookiePrefs(request)
  82. respTimeline(await showTimeline(@"name", @"after", none(Query), prefs))
  83. get "/@name/search":
  84. cond '.' notin @"name"
  85. let prefs = getCookiePrefs(request)
  86. let query = initQuery(@"filter", @"include", @"not", @"sep", @"name")
  87. respTimeline(await showTimeline(@"name", @"after", some(query), prefs))
  88. get "/@name/replies":
  89. cond '.' notin @"name"
  90. let prefs = getCookiePrefs(request)
  91. respTimeline(await showTimeline(@"name", @"after", some(getReplyQuery(@"name")), prefs))
  92. get "/@name/media":
  93. cond '.' notin @"name"
  94. let prefs = getCookiePrefs(request)
  95. respTimeline(await showTimeline(@"name", @"after", some(getMediaQuery(@"name")), prefs))
  96. get "/@name/status/@id":
  97. cond '.' notin @"name"
  98. let prefs = getCookiePrefs(request)
  99. let conversation = await getTweet(@"name", @"id", getAgent())
  100. if conversation == nil or conversation.tweet.id.len == 0:
  101. resp Http404, showError("Tweet not found", cfg.title, prefs)
  102. let title = pageTitle(conversation.tweet.profile)
  103. let desc = conversation.tweet.text
  104. let html = renderConversation(conversation, prefs)
  105. if conversation.tweet.video.isSome():
  106. let thumb = get(conversation.tweet.video).thumb
  107. let vidUrl = getVideoEmbed(conversation.tweet.id)
  108. resp renderMain(html, prefs, title=cfg.title, titleText=title, desc=desc,
  109. images = @[thumb], `type`="video", video=vidUrl)
  110. elif conversation.tweet.gif.isSome():
  111. let thumb = get(conversation.tweet.gif).thumb
  112. let vidUrl = getVideoEmbed(conversation.tweet.id)
  113. resp renderMain(html, prefs, title=cfg.title, titleText=title, desc=desc,
  114. images = @[thumb], `type`="video", video=vidUrl)
  115. else:
  116. resp renderMain(html, prefs, title=cfg.title, titleText=title,
  117. desc=desc, images=conversation.tweet.photos)
  118. get "/pic/@sig/@url":
  119. cond "http" in @"url"
  120. cond "twimg" in @"url"
  121. let prefs = getCookiePrefs(request)
  122. let
  123. uri = parseUri(decodeUrl(@"url"))
  124. path = uri.path.split("/")[2 .. ^1].join("/")
  125. filename = cfg.cacheDir / cleanFilename(path & uri.query)
  126. if getHmac($uri) != @"sig":
  127. resp showError("Failed to verify signature", cfg.title, prefs)
  128. if not existsDir(cfg.cacheDir):
  129. createDir(cfg.cacheDir)
  130. if not existsFile(filename):
  131. let client = newAsyncHttpClient()
  132. await client.downloadFile($uri, filename)
  133. client.close()
  134. if not existsFile(filename):
  135. resp Http404
  136. let file = openAsync(filename)
  137. let buf = await readAll(file)
  138. file.close()
  139. resp buf, mimetype(filename)
  140. get "/video/@sig/@url":
  141. cond "http" in @"url"
  142. cond "video.twimg" in @"url"
  143. let prefs = getCookiePrefs(request)
  144. let url = decodeUrl(@"url")
  145. if getHmac(url) != @"sig":
  146. resp showError("Failed to verify signature", cfg.title, prefs)
  147. let client = newAsyncHttpClient()
  148. let video = await client.getContent(url)
  149. client.close()
  150. resp video, mimetype(url)
  151. runForever()