nitter.nim 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import asyncdispatch, asyncfile, httpclient, sequtils, strutils, strformat, uri, os
  2. from net import Port
  3. import jester, regex
  4. import api, utils, types, cache, formatters, search, config, agents
  5. import views/[general, profile, status]
  6. const configPath {.strdefine.} = "./nitter.conf"
  7. let cfg = getConfig(configPath)
  8. proc showSingleTimeline(name, after, agent: string; query: Option[Query]): Future[string] {.async.} =
  9. let profileFut = getCachedProfile(name, agent)
  10. let railFut = getPhotoRail(name, agent)
  11. var timelineFut: Future[Timeline]
  12. if query.isNone:
  13. timelineFut = getTimeline(name, after, agent)
  14. else:
  15. timelineFut = getTimelineSearch(get(query), after, agent)
  16. let profile = await profileFut
  17. if profile.username.len == 0:
  18. return ""
  19. let profileHtml = renderProfile(profile, await timelineFut, await railFut)
  20. return renderMain(profileHtml, title=cfg.title, titleText=pageTitle(profile))
  21. proc showMultiTimeline(names: seq[string]; after, agent: string; query: Option[Query]): Future[string] {.async.} =
  22. var q = query
  23. if q.isSome:
  24. get(q).fromUser = names
  25. else:
  26. q = some(Query(kind: multi, fromUser: names, excludes: @["replies"]))
  27. var timeline = renderMulti(await getTimelineSearch(get(q), after, agent), names.join(","))
  28. return renderMain(timeline, title=cfg.title, titleText="Multi")
  29. proc showTimeline(name, after: string; query: Option[Query]): Future[string] {.async.} =
  30. let agent = getAgent()
  31. let names = name.strip(chars={'/'}).split(",").filterIt(it.len > 0)
  32. if names.len == 1:
  33. return await showSingleTimeline(names[0], after, agent, query)
  34. else:
  35. return await showMultiTimeline(names, after, agent, query)
  36. template respTimeline(timeline: typed) =
  37. if timeline.len == 0:
  38. resp Http404, showError("User \"" & @"name" & "\" not found", cfg.title)
  39. resp timeline
  40. setProfileCacheTime(cfg.profileCacheTime)
  41. settings:
  42. port = Port(cfg.port)
  43. staticDir = cfg.staticDir
  44. bindAddr = cfg.address
  45. routes:
  46. get "/":
  47. resp renderMain(renderSearch(), title=cfg.title)
  48. post "/search":
  49. if @"query".len == 0:
  50. resp Http404, showError("Please enter a username.", cfg.title)
  51. redirect("/" & @"query")
  52. get "/@name/?":
  53. cond '.' notin @"name"
  54. respTimeline(await showTimeline(@"name", @"after", none(Query)))
  55. get "/@name/search":
  56. cond '.' notin @"name"
  57. let query = initQuery(@"filter", @"include", @"not", @"sep", @"name")
  58. respTimeline(await showTimeline(@"name", @"after", some(query)))
  59. get "/@name/replies":
  60. cond '.' notin @"name"
  61. respTimeline(await showTimeline(@"name", @"after", some(getReplyQuery(@"name"))))
  62. get "/@name/media":
  63. cond '.' notin @"name"
  64. respTimeline(await showTimeline(@"name", @"after", some(getMediaQuery(@"name"))))
  65. get "/@name/status/@id":
  66. cond '.' notin @"name"
  67. let conversation = await getTweet(@"name", @"id", getAgent())
  68. if conversation == nil or conversation.tweet.id.len == 0:
  69. resp Http404, showError("Tweet not found", cfg.title)
  70. let title = pageTitle(conversation.tweet.profile)
  71. resp renderMain(renderConversation(conversation), title=cfg.title, titleText=title)
  72. get "/pic/@sig/@url":
  73. cond "http" in @"url"
  74. cond "twimg" in @"url"
  75. let
  76. uri = parseUri(decodeUrl(@"url"))
  77. path = uri.path.split("/")[2 .. ^1].join("/")
  78. filename = cfg.cacheDir / cleanFilename(path & uri.query)
  79. if getHmac($uri) != @"sig":
  80. resp showError("Failed to verify signature", cfg.title)
  81. if not existsDir(cfg.cacheDir):
  82. createDir(cfg.cacheDir)
  83. if not existsFile(filename):
  84. let client = newAsyncHttpClient()
  85. await client.downloadFile($uri, filename)
  86. client.close()
  87. if not existsFile(filename):
  88. resp Http404
  89. let file = openAsync(filename)
  90. let buf = await readAll(file)
  91. file.close()
  92. resp buf, mimetype(filename)
  93. get "/video/@sig/@url":
  94. cond "http" in @"url"
  95. cond "video.twimg" in @"url"
  96. let url = decodeUrl(@"url")
  97. if getHmac(url) != @"sig":
  98. resp showError("Failed to verify signature", cfg.title)
  99. let
  100. client = newAsyncHttpClient()
  101. video = await client.getContent(url)
  102. defer: client.close()
  103. resp video, mimetype(url)
  104. runForever()