view_model.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. from dataclasses import replace
  2. from flask import g, request, session
  3. import sqlite3
  4. from twitter_v2.types import Tweet, TweetExpansions
  5. from hogumathi_app.view_model import FeedServiceUser, FeedItem, FeedItemAction, CollectionPage, PublicMetrics, Card, MediaItem
  6. from . import oauth2_login
  7. # FIXME we want this to mark tweets as viewed.
  8. # from .content_source import get_object_over_time
  9. url_for = oauth2_login.url_for_with_me
  10. def user_model_dc (user, my_url_for=url_for):
  11. fsu = FeedServiceUser(
  12. id = user.id,
  13. name = user.name,
  14. username = user.username,
  15. created_at = user.created_at,
  16. description = user.description,
  17. preview_image_url = user.profile_image_url,
  18. website = user.url,
  19. is_verified = user.verified,
  20. is_protected = user.protected,
  21. location = user.location,
  22. url = my_url_for('twitter_v2_facade.get_profile_html', user_id=user.id),
  23. source_url = f'https://twitter.com/{user.username}',
  24. raw_user = user
  25. )
  26. return fsu
  27. def tweet_model_dc_vm (includes: TweetExpansions, tweet: Tweet, me, my_url_for=url_for, my_g=g, reply_depth=0, expand_path=None, tweets_viewed={}) -> FeedItem:
  28. # retweeted_by, avi_icon_url, display_name, handle, created_at, text
  29. # HACK we should not refer to the request directly...
  30. is_marked = False
  31. if request and request.args.get('marked_reply') == str(tweet.id):
  32. is_marked = True
  33. # HACK we shouldn't go to session directly
  34. #is_viewed = False
  35. #if session and 'oldest_viewed_tweet_id' in session:
  36. # #print('--- tweet_model_dc_vm: checking oldest_viewed_tweet_id')
  37. # oldest_viewed_tweet_id = session.get('oldest_viewed_tweet_id')
  38. # if oldest_viewed_tweet_id < int(tweet.id):
  39. # is_viewed = True
  40. is_viewed = tweets_viewed.get(tweet.id)
  41. user = list(filter(lambda u: u.id == tweet.author_id, includes.users))[0]
  42. published_by = user_model_dc(user, my_url_for=my_url_for)
  43. url = my_url_for('twitter_v2_facade.get_tweet_html', tweet_id=tweet.id, view='tweet')
  44. source_url = 'https://twitter.com/{}/status/{}'.format(user.username, tweet.id)
  45. avi_icon_url = my_url_for('get_image', url=user.profile_image_url)
  46. retweet_of = None
  47. quoted = None
  48. replied_to = None
  49. if tweet.referenced_tweets:
  50. retweet_of = list(filter(lambda r: r.type == 'retweeted', tweet.referenced_tweets))
  51. quoted = list(filter(lambda r: r.type == 'quoted', tweet.referenced_tweets))
  52. replied_to = list(filter(lambda r: r.type == 'replied_to', tweet.referenced_tweets))
  53. if reply_depth:
  54. if expand_path:
  55. expand_path += f',{tweet.id}'
  56. else:
  57. expand_path = tweet.id
  58. actions = {
  59. 'view_replies': FeedItemAction('twitter_v2_facade.get_tweet_html', {'tweet_id': tweet.conversation_id, 'view': 'replies', 'expand': expand_path}),
  60. 'view_thread': FeedItemAction('twitter_v2_facade.get_tweet_html', {'tweet_id': tweet.conversation_id, 'view': 'thread'}),
  61. 'view_conversation': FeedItemAction('twitter_v2_facade.get_tweet_html', {'tweet_id': tweet.conversation_id, 'view': 'conversation'}),
  62. }
  63. setattr(actions['view_replies'], 'url', url_for(actions['view_replies'].route, **actions['view_replies'].route_params))
  64. setattr(actions['view_thread'], 'url', url_for(actions['view_thread'].route, **actions['view_thread'].route_params))
  65. setattr(actions['view_conversation'], 'url', url_for(actions['view_conversation'].route, **actions['view_conversation'].route_params))
  66. if reply_depth:
  67. vr = actions['view_replies']
  68. url = my_url_for(vr.route, **vr.route_params)
  69. is_bookmarked = None
  70. if me:
  71. cache_db = sqlite3.connect('.data/twitter_v2_cache.db')
  72. auth_user_id = me[len('twitter:'):]
  73. # this will cache deleted bookmarks. we need a next level abstraction over events / aggregate.
  74. is_bookmarked = cache_db.execute('select count(*) from tweet t, query q where q.rowid = t.query_id and q.query_type=? and t.id=? and q.auth_user_id=?', ['bookmarks', tweet.id, auth_user_id]).fetchone()[0] and True
  75. cache_db.close()
  76. if my_g.get('twitter_user'):
  77. actions.update(
  78. retweet = FeedItemAction('twitter_v2_facade.post_tweet_retweet', {'tweet_id': tweet.id})
  79. )
  80. setattr(actions['retweet'], 'url', my_url_for(actions['retweet'].route, actions['retweet'].route_params))
  81. if is_bookmarked:
  82. actions.update(
  83. delete_bookmark = FeedItemAction('twitter_v2_facade.delete_tweet_bookmark', {'tweet_id': tweet.id})
  84. )
  85. setattr(actions['delete_bookmark'], 'url', my_url_for(actions['delete_bookmark'].route, **actions['delete_bookmark'].route_params))
  86. else:
  87. actions.update(
  88. bookmark = FeedItemAction('twitter_v2_facade.post_tweet_bookmark', {'tweet_id': tweet.id})
  89. )
  90. setattr(actions['bookmark'], 'url', my_url_for(actions['bookmark'].route, **actions['bookmark'].route_params))
  91. if my_g.get('twitter_live_enabled'):
  92. actions.update(
  93. view_activity = FeedItemAction('twitter_v2_live_facade.get_tweet_activity_html', {'tweet_id': tweet.id})
  94. )
  95. setattr(actions['view_activity'], 'url', my_url_for(actions['view_activity'].route, **actions['view_activity'].route_params))
  96. t = FeedItem(
  97. id = tweet.id,
  98. text = tweet.text,
  99. created_at = tweet.created_at,
  100. published_by = published_by,
  101. author_is_verified = user.verified,
  102. url = url,
  103. conversation_id = tweet.conversation_id,
  104. avi_icon_url = avi_icon_url,
  105. display_name = user.name,
  106. handle = user.username,
  107. author_url = my_url_for('twitter_v2_facade.get_profile_html', user_id=user.id),
  108. author_id = user.id,
  109. source_url = source_url,
  110. source_author_url = 'https://twitter.com/{}'.format(user.username),
  111. #'is_edited': len(tweet['edit_history_tweet_ids']) > 1
  112. actions = actions,
  113. is_bookmarked = is_bookmarked,
  114. is_marked = is_marked,
  115. is_viewed = is_viewed
  116. )
  117. if reply_depth:
  118. t = replace(t, reply_depth = reply_depth)
  119. # This is where we should put "is_bookmark", "is_liked", "is_in_collection", etc...
  120. if tweet.entities:
  121. if tweet.entities.urls:
  122. urls = list(filter(lambda u: u.title and u.description, tweet.entities.urls))
  123. if len(urls):
  124. url = urls[0]
  125. card = Card(
  126. display_url = url.display_url.split('/')[0],
  127. source_url = url.unwound_url,
  128. content = url.description,
  129. title = url.title
  130. )
  131. if url.images:
  132. print(url.images)
  133. card = replace(card,
  134. preview_image_url = my_url_for('get_image', url=url.images[1].url),
  135. image_url = my_url_for('get_image', url=url.images[0].url)
  136. )
  137. t = replace(t, card = card)
  138. if tweet.public_metrics:
  139. public_metrics = PublicMetrics(
  140. reply_count = tweet.public_metrics.reply_count,
  141. quote_count = tweet.public_metrics.quote_count,
  142. retweet_count = tweet.public_metrics.retweet_count,
  143. like_count = tweet.public_metrics.like_count,
  144. impression_count = tweet.public_metrics.impression_count,
  145. bookmark_count = tweet.public_metrics.bookmark_count,
  146. )
  147. t = replace(t, public_metrics = public_metrics)
  148. if tweet.non_public_metrics:
  149. non_public_metrics = NonPublicMetrics(
  150. impression_count = tweet.non_public_metrics.impression_count,
  151. user_profile_clicks = tweet.non_public_metrics.user_profile_clicks,
  152. url_link_clicks = tweet.non_public_metrics.url_link_clicks
  153. )
  154. t = replace(t, non_public_metrics = non_public_metrics)
  155. if retweet_of and len(retweet_of):
  156. print('found retweet_of')
  157. t = replace(t, retweeted_tweet_id = retweet_of[0].id)
  158. retweeted_tweet:Tweet = list(filter(lambda t: t.id == retweet_of[0].id, includes.tweets))[0]
  159. rt = tweet_model_dc_vm(includes, retweeted_tweet, me)
  160. t = replace(rt,
  161. retweeted_tweet_id = retweet_of[0].id,
  162. source_retweeted_by_url = 'https://twitter.com/{}'.format(user.username),
  163. retweeted_by = user.name,
  164. retweeted_by_url = my_url_for('twitter_v2_facade.get_profile_html', user_id=user.id)
  165. )
  166. try:
  167. if tweet.attachments and tweet.attachments.media_keys and includes.media:
  168. media_keys = tweet.attachments.media_keys
  169. def first_media (mk):
  170. medias = list(filter(lambda m: m.media_key == mk, includes.media))
  171. if len(medias):
  172. return medias[0]
  173. return None
  174. media = list(filter(lambda m: m != None, map(first_media, media_keys)))
  175. photos = filter(lambda m: m.type == 'photo', media)
  176. videos = filter(lambda m: m.type == 'video', media)
  177. photo_media = map(lambda p: MediaItem(
  178. media_key = p.media_key,
  179. type = 'photo',
  180. preview_image_url = my_url_for('get_image', url=p.url + '?name=tiny&format=webp'),
  181. url = my_url_for('get_image', url=p.url),
  182. width = p.width,
  183. height = p.height
  184. ), photos)
  185. def video_to_mi (v):
  186. use_hls = False # mainly iOS
  187. max_bitrate = 100000000
  188. if use_hls:
  189. variants = list(filter(lambda var: var.content_type == 'application/x-mpegURL'))
  190. else:
  191. variants = list(filter(lambda var: var.content_type != 'application/x-mpegURL' and var.bit_rate <= max_bitrate, v.variants))
  192. variants.sort(key=lambda v: v.bit_rate, reverse=True)
  193. url = None
  194. content_type = None
  195. size = None
  196. if len(variants):
  197. if len(variants) > 1:
  198. print('multiple qualifying variants (using first):')
  199. print(variants)
  200. variant = variants[0]
  201. url = my_url_for('get_image', url=variant.url)
  202. content_type = variant.content_type
  203. size = int(v.duration_ms / 1000 * variant.bit_rate)
  204. public_metrics = None
  205. if v.public_metrics and v.public_metrics.view_count:
  206. public_metrics = PublicMetrics(
  207. view_count = v.public_metrics.view_count
  208. )
  209. mi = MediaItem(
  210. media_key = v.media_key,
  211. type = 'video',
  212. preview_image_url = my_url_for('get_image', url=v.preview_image_url + '?name=tiny&format=webp'),
  213. image_url = my_url_for('get_image', url=v.preview_image_url),
  214. width = v.width,
  215. height = v.height,
  216. url=url,
  217. content_type = content_type,
  218. duration_ms = v.duration_ms,
  219. size = size,
  220. public_metrics = public_metrics
  221. )
  222. return mi
  223. video_media = map(video_to_mi, videos)
  224. t = replace(t,
  225. photos = list(photo_media),
  226. videos = list(video_media)
  227. )
  228. elif tweet.attachments and tweet.attachments.media_keys and not includes.media:
  229. print('tweet had attachments and media keys, but no expansion media content was given')
  230. print(tweet.attachments.media_keys)
  231. except:
  232. # it seems like this comes when we have a retweeted tweet with media on it.
  233. print('exception adding attachments to tweet:')
  234. print(tweet)
  235. print('view tweet:')
  236. print(t)
  237. print('included media:')
  238. print(includes.media)
  239. raise 'exception adding attachments to tweet'
  240. try:
  241. if quoted and len(quoted):
  242. t = replace(t, quoted_tweet_id = quoted[0].id)
  243. quoted_tweets = list(filter(lambda t: t.id == quoted[0].id, includes.tweets))
  244. if len(quoted_tweets):
  245. t = replace(t, quoted_tweet = tweet_model_dc_vm(includes, quoted_tweets[0], me))
  246. except:
  247. raise 'error adding quoted tweet'
  248. try:
  249. if replied_to and len(replied_to) and includes.tweets:
  250. t = replace(t, replied_tweet_id = replied_to[0].id)
  251. if reply_depth < 1:
  252. replied_tweets = list(filter(lambda t: t.id == replied_to[0].id, includes.tweets))
  253. if len(replied_tweets):
  254. t = replace(t, replied_tweet = tweet_model_dc_vm(includes, replied_tweets[0], me, reply_depth=reply_depth + 1))
  255. else:
  256. print("No replied tweet found (t={}, rep={})".format(t.id, t.replied_tweet_id))
  257. except:
  258. raise 'error adding replied_to tweet'
  259. return t