html_renderer.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. import traceback
  2. import urllib.parse
  3. import re
  4. accent_color = 'darkgreen'
  5. def extend_text(text, urls_container):
  6. for url in urls_container['urls']:
  7. text = text.replace(url['url'], f"<a href='{url['expanded_url']}'>{url['expanded_url']}</a>")
  8. text = re.sub(r'@([a-zA-Z0-9_]+)', r'<a href="/\1">@\1</a>', text)
  9. return text
  10. def render_user(user):
  11. html = ''
  12. html += f"<a href='/{user['screen_name']}' class=icon-container>"
  13. html += f"<img src='{user['profile_image_url_https']}'>"
  14. html += '<div style="display:inline;margin-left:5px">'
  15. html += f"<h2 style='margin:0px'>{user['name']}</h2>"
  16. html += '@' + user['screen_name']
  17. html += '</div>'
  18. html += '</a>'
  19. return html
  20. def render_tweet(tweet, user, graph_tweet=None, is_pinned=False):
  21. html = '<div style="background:#111111;padding:20px;padding-bottom:45px;margin-bottom:10px;padding-top:5px">'
  22. if 'retweeted_status_result' in tweet:
  23. graph_retweeted = tweet['retweeted_status_result']['result']
  24. if 'tweet' in graph_retweeted:
  25. graph_retweeted = graph_retweeted['tweet']
  26. retweeted = graph_retweeted['legacy']
  27. retweeted_user = graph_retweeted['core']['user_results']['result']['legacy']
  28. tweet_link = f"/{retweeted_user['screen_name']}/status/{retweeted['id_str']}"
  29. else:
  30. retweeted = None
  31. tweet_link = f"/{user['screen_name']}/status/{tweet['id_str']}"
  32. html += f"<a href='{tweet_link}'>"
  33. if is_pinned:
  34. html += '<p class=icon-container><img src="/static/pin.svg" class=icon>Pinned Tweet</p>'
  35. html += f"<p>{tweet['created_at']}</p></a>"
  36. html += render_user(user)
  37. if retweeted is not None:
  38. html += render_tweet(retweeted, retweeted_user, graph_retweeted)
  39. html += '</div>'
  40. return html
  41. if graph_tweet is not None and 'note_tweet' in graph_tweet:
  42. text = graph_tweet['note_tweet']['note_tweet_results']['result']['text']
  43. else:
  44. text = tweet.get('full_text', tweet.get('text'))
  45. text = extend_text(text, tweet['entities'])
  46. html += '<p>' + text + '</p>'
  47. try:
  48. for media in tweet['extended_entities']['media']:
  49. html = html.replace(media['url'], '')
  50. url = media['media_url_https']
  51. if media['type'] in ('video', 'animated_gif'):
  52. html += f"<video poster='{url}' style='max-height:512px;max-width:100%;display:block' "
  53. if media['type'] == 'video':
  54. html += 'controls'
  55. else:
  56. html += 'autoplay loop'
  57. html += '>'
  58. for variant in media['video_info']['variants']:
  59. html += f"<source src='{variant['url']}' type='{variant['content_type']}'>"
  60. html += '</video>'
  61. else:
  62. html += f'<a href="{url}"><img src="{url}" style="max-height:512px;max-width:100%;display:block"></a>'
  63. except KeyError:
  64. pass
  65. if graph_tweet is not None and 'quoted_status_result' in graph_tweet:
  66. quoted_tweet_graph = graph_tweet['quoted_status_result']['result']
  67. quoted_tweet = quoted_tweet_graph['legacy']
  68. quoted_user = quoted_tweet_graph['core']['user_results']['result']['legacy']
  69. html += render_tweet(quoted_tweet, quoted_user, quoted_tweet_graph)
  70. html += f'<a href="{tweet_link}" class=icon-container style="float:left">'
  71. try:
  72. html += f"<img src='/static/message-reply.svg' class=icon>{tweet['reply_count']}"
  73. except KeyError:
  74. pass
  75. html += '</a>'
  76. html += f'<a href="/search?q=quoted_tweet_id:{tweet["id_str"]}" class=icon-container style="float:left">'
  77. html += f"<img src='/static/format-quote-close.svg' class=icon>{tweet['quote_count']}"
  78. html += '</a>'
  79. html += f'<a href="{tweet_link}/retweeters" class=icon-container style="float:left">'
  80. html += f"<img src='/static/repeat-variant.svg' class=icon>{tweet['retweet_count']}"
  81. html += '</a>'
  82. html += f'<a href="{tweet_link}/favoriters" class=icon-container style="float:left">'
  83. html += f"<img src='/static/thumb-up.svg' class=icon>{tweet['favorite_count']}"
  84. html += '</a>'
  85. html += f'<a href="{tweet_link}" class=icon-container style="float:left">'
  86. try:
  87. if graph_tweet is not None:
  88. html += f"<img src='/static/eye-outline.svg' class=icon>{graph_tweet['views']['count']}"
  89. except KeyError:
  90. pass
  91. html += '</a>'
  92. html += '</div>'
  93. return html
  94. def render_graph_tweet(content, is_pinned):
  95. graph_tweet = content['itemContent']['tweet_results']['result']
  96. if 'tweet' in graph_tweet:
  97. graph_tweet = graph_tweet['tweet']
  98. tweet = graph_tweet['legacy']
  99. user = graph_tweet['core']['user_results']['result']
  100. if 'tweet' in user:
  101. user = user['tweet']
  102. user = user['legacy']
  103. return render_tweet(tweet, user, graph_tweet, is_pinned)
  104. def render_load_more(content, params):
  105. params['cursor'] = content['value']
  106. href = urllib.parse.urlencode(params)
  107. return f"<a href='?{href}'>load more</a>"
  108. def render_instruction(entry, params, is_pinned=False):
  109. html = ''
  110. content = entry['content']
  111. if 'itemContent' in content:
  112. itemContent = content['itemContent']
  113. if 'tweet_results' in itemContent:
  114. html += render_graph_tweet(content, is_pinned)
  115. elif 'user_results' in itemContent:
  116. try:
  117. html += render_user_card(itemContent['user_results']['result']['legacy'])
  118. except KeyError:
  119. traceback.print_exc()
  120. else:
  121. try:
  122. html += render_load_more(itemContent, params)
  123. except KeyError:
  124. traceback.print_exc()
  125. if 'items' in content:
  126. html += '<div style="position:relative">'
  127. if len(content['items']) > 1:
  128. html += f"<div style='position:absolute;height:100%;width:5px;background:{accent_color}'></div>"
  129. for item in content['items']:
  130. try:
  131. html += render_graph_tweet(item['item'], is_pinned)
  132. except KeyError:
  133. traceback.print_exc()
  134. html += '</div>'
  135. if 'value' in content:
  136. html += render_load_more(content, params)
  137. return html
  138. def render_instructions(timeline, params=None):
  139. if params is None:
  140. params = {}
  141. html = ''
  142. for instruction in timeline['instructions']:
  143. if 'entry' in instruction and instruction['type'] == 'TimelinePinEntry':
  144. html += render_instruction(instruction['entry'], params, True)
  145. for instruction in timeline['instructions']:
  146. if 'entries' in instruction:
  147. for entry in instruction['entries']:
  148. html += render_instruction(entry, params)
  149. if 'entry' in instruction and instruction['type'] != 'TimelinePinEntry':
  150. html += render_instruction(instruction['entry'], params)
  151. return html
  152. def render_user_card(user):
  153. username = user['screen_name']
  154. html = ''
  155. html += '<div style="background:#111111;padding:20px;margin-bottom:20px">'
  156. html += f"<title>{user['name']} (@{username}) - yitter</title>"
  157. html += render_user(user)
  158. description = extend_text(user['description'], user['entities']['description'])
  159. html += '<p>' + description + '</p>'
  160. html += '<p>'
  161. html += f'<a href="/{username}/following">Following: {user["friends_count"]}</a> '
  162. html += f'<a href="/{username}/followers">Followers: {user["followers_count"]}</a>'
  163. html += '</p>'
  164. html += f'<a href="/{username}">Home</a> <a href="/{username}/favorites">Likes</a>'
  165. html += '</div>'
  166. return html
  167. def render_user_header(user):
  168. user = user['data']['user']['result']['legacy']
  169. username = user['screen_name']
  170. html = ''
  171. if 'profile_banner_url' in user:
  172. html += f"<a href='{user['profile_banner_url']}'><img src='{user['profile_banner_url']}' style='max-width:100%'></a>"
  173. html += render_user_card(user)
  174. return html
  175. def render_top():
  176. html = '<style>body{background:black;color:white}a{color:' + accent_color + ';text-decoration:none}.icon{height:24px;filter:invert(100%);margin-right:5px;margin-left:10px}.icon-container{display:inline-flex;margin-top:10px;align-items:center;transform:translateX(-10px)}</style>'
  177. html += '<link rel="icon" href="/static/head.webp">'
  178. html += '<div style="margin:auto;width:50%">'
  179. html += '<div style="display:flex">'
  180. html += f"<h1 style='margin:auto;display:block;color:{accent_color}'>yitter</h1>"
  181. html += '</div>'
  182. html += '<div style="height:64px;display:flex">'
  183. html += '<img src="/static/head.webp" style="height:64px;float:left">'
  184. html += '<form action="/search" style="margin:auto;display:block"><input name="q"><input type="submit" value="search"></form>'
  185. html += '<img src="/static/head.webp" style="height:64px;float:right;transform:scaleX(-1);">'
  186. html += '</div>'
  187. return html