facade.py 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192
  1. from typing import List
  2. from dataclasses import asdict, replace
  3. from dacite import from_dict
  4. from importlib.util import find_spec
  5. from configparser import ConfigParser
  6. import base64
  7. import sqlite3
  8. import os
  9. import json
  10. import json_stream
  11. from zipfile import ZipFile
  12. import itertools
  13. import time
  14. from io import BufferedReader
  15. import re
  16. import datetime
  17. import dateutil
  18. import dateutil.parser
  19. import dateutil.tz
  20. import requests
  21. from werkzeug.utils import secure_filename
  22. from flask import json, Response, render_template, request, send_from_directory, Blueprint, session, redirect, g, current_app, jsonify
  23. from flask_cors import CORS
  24. from twitter_v2.api import ApiV2TweetSource, TwitterApiV2SocialGraph, ApiV2ConversationSource
  25. from twitter_v2.types import Tweet, TweetExpansions
  26. from hogumathi_app.view_model import FeedItem, FeedServiceUser, ThreadItem, FeedItemAction, MediaItem, Card, PublicMetrics, NonPublicMetrics, UnrepliedSection, CollectionPage, cleandict
  27. from .view_model import user_model_dc, tweet_model_dc_vm
  28. from . import content_source
  29. from . import oauth2_login
  30. theme_variant = ''
  31. if find_spec('theme_bootstrap5'): # FIXME use g.
  32. theme_variant = '-bs'
  33. DATA_DIR='.data'
  34. twitter_app = Blueprint('twitter_v2_facade', 'twitter_v2_facade',
  35. template_folder='templates',
  36. static_folder='static',
  37. static_url_path='',
  38. url_prefix='/')
  39. twitter_app.register_blueprint(oauth2_login.oauth2_login, url_prefix="/")
  40. twitter_app.context_processor(oauth2_login.inject_me)
  41. twitter_app.before_request(oauth2_login.add_me)
  42. url_for = oauth2_login.url_for_with_me
  43. def run_script(script_name, script_vars):
  44. script_path = './{}.py'.format(script_name)
  45. if (os.path.exists(script_path)):
  46. script_file = open(script_path, 'r')
  47. script = script_file.read()
  48. script_file.close()
  49. try:
  50. return exec(script, script_vars)
  51. except:
  52. print('error running script: {}'.format(script_name))
  53. return False
  54. False
  55. class ActivityData:
  56. def __init__ (self, user_id, db_path):
  57. self.db_path = db_path
  58. self.user_id = user_id
  59. db_exists = os.path.exists(db_path)
  60. self.db = sqlite3.connect(db_path)
  61. if not db_exists:
  62. self.init_db()
  63. return
  64. def init_db (self):
  65. self.db.execute('create table seen_user (ts, user_id)')
  66. self.db.execute('create table seen_tweet (ts, tweet_id)')
  67. return
  68. def seen_tweet (self, tweet_id):
  69. return
  70. def seen_user (self, user_id):
  71. return
  72. def add_tweet_counts (self, user_id, start, end, tweet_count):
  73. return [current_ts, user_id, start, end, tweet_count]
  74. def add_tweet_public_metrics (self, tweet_id, like_count, reply_count, retweet_count, quote_count):
  75. return
  76. def add_tweet_non_public_metrics (self, tweet_id, impression_count, click_count, link_click_count, profile_click_count):
  77. return
  78. def add_user_public_metrics (self, user_id, followers_count, following_count, tweet_count, listed_count):
  79. return
  80. class DataSet:
  81. def __init__ (self):
  82. self.items = {}
  83. return
  84. def update_items (self, items):
  85. """
  86. merges objects by ID. Asssigns an ID if none exists. Mutates OG object.
  87. """
  88. ids = []
  89. for item in items:
  90. if not 'id' in item:
  91. #item = dict(item)
  92. item['id'] = uuid.uuid4().hex
  93. else:
  94. existing_item = self.items.get( item['id'] )
  95. if existing_item:
  96. existing_item.update(item)
  97. item = existing_item
  98. self.items[ item['id'] ] = item
  99. ids.append( item['id'] )
  100. return ids
  101. def get_items (self):
  102. return self.items.values()
  103. class TwitterMetadata:
  104. def __init__ (self, data_dir):
  105. self.data_dir = data_dir
  106. os.mkdir(data_dir, exist_ok=True)
  107. def get_tweet (self, tweet_id):
  108. path = f'{self.data_dir}/tweet_{tweet_id}.json'
  109. if not os.path.exists(path):
  110. return None
  111. with open(path, 'rt') as f:
  112. return json.loads(f.read())
  113. def update_tweet (self, tweet_id, fields):
  114. tweet = self.get_tweet(tweet_id)
  115. if not tweet:
  116. tweet = {'id': tweet_id}
  117. tweet.update(fields)
  118. with open(f'{self.data_dir}/tweet_{tweet_id}.json', 'wt') as f:
  119. f.write(json.dumps(tweet))
  120. return tweet
  121. #twitter_meta = TwitterMetadata('./data/meta')
  122. @twitter_app.route('/tweets', methods=['POST'])
  123. def post_tweets_create ():
  124. user_id = g.twitter_user['id']
  125. token = g.twitter_user['access_token']
  126. text = request.form.get('text')
  127. reply_to_tweet_id = request.form.get('reply_to_tweet_id')
  128. quote_tweet_id = request.form.get('quote_tweet_id')
  129. tweet_source = ApiV2TweetSource(token)
  130. result = tweet_source.create_tweet(text, reply_to_tweet_id=reply_to_tweet_id, quote_tweet_id=quote_tweet_id)
  131. print(result)
  132. run_script('on_tweeted', {'twitter_user': g.twitter_user, 'tweet': result})
  133. if 'HX-Request' in request.headers:
  134. return render_template('partial/compose-form.html', new_tweet_id=result['data']['id'])
  135. else:
  136. response_body = json.dumps({
  137. 'result': result
  138. })
  139. return jsonify(response_body)
  140. @twitter_app.route('/tweet/<tweet_id>/retweet', methods=['POST'])
  141. def post_tweet_retweet (tweet_id):
  142. user_id = g.twitter_user['id']
  143. token = g.twitter_user['access_token']
  144. tweet_source = ApiV2TweetSource(token)
  145. result = tweet_source.retweet(tweet_id, user_id=user_id)
  146. print(result)
  147. run_script('on_tweeted', {'twitter_user': g.twitter_user, 'retweet': result})
  148. if 'HX-Request' in request.headers:
  149. return """retweeted <script>Toast.fire({
  150. icon: 'success',
  151. title: 'Retweet was sent; <a style="text-align: right" href="{}">View</a>.'
  152. });</script>""".replace('{}', url_for('.get_tweet_html', tweet_id=tweet_id))
  153. else:
  154. response_body = json.dumps({
  155. 'result': result
  156. })
  157. return jsonify(response_body)
  158. @twitter_app.route('/tweet/<tweet_id>/bookmark', methods=['POST'])
  159. def post_tweet_bookmark (tweet_id):
  160. user_id = g.twitter_user['id']
  161. token = g.twitter_user['access_token']
  162. tweet_source = ApiV2TweetSource(token)
  163. result = tweet_source.bookmark(tweet_id, user_id=user_id)
  164. print(result)
  165. if 'HX-Request' in request.headers:
  166. return """bookmarked <script>Toast.fire({
  167. icon: 'success',
  168. title: 'Tweet was bookmarked; <a style="text-align: right" href="{}">View</a>.'
  169. });</script>""".replace('{}', url_for('.get_tweet_html', tweet_id=tweet_id))
  170. else:
  171. response_body = json.dumps({
  172. 'result': result
  173. })
  174. return jsonify(response_body)
  175. @twitter_app.route('/tweet/<tweet_id>/bookmark', methods=['DELETE'])
  176. def delete_tweet_bookmark (tweet_id):
  177. user_id = g.twitter_user['id']
  178. token = g.twitter_user['access_token']
  179. tweet_source = ApiV2TweetSource(token)
  180. result = tweet_source.delete_bookmark(tweet_id, user_id=user_id)
  181. response_body = json.dumps({
  182. 'result': result
  183. })
  184. return jsonify(response_body)
  185. @twitter_app.route('/tweet/<tweet_id>.html', methods=['GET'])
  186. def get_tweet_html (tweet_id):
  187. pagination_token = request.args.get('pagination_token')
  188. view = request.args.get('view', 'replies')
  189. if g.twitter_user:
  190. token = g.twitter_user['access_token']
  191. else:
  192. token = os.environ.get('BEARER_TOKEN')
  193. tweet_source = ApiV2TweetSource(token)
  194. only_replies = view == 'replies'
  195. tweets = []
  196. if not pagination_token:
  197. tweets_response = tweet_source.get_tweet(tweet_id, return_dataclass=True)
  198. tweet = tweets_response.data[0]
  199. tweets.append(tweet_model_dc_vm(tweets_response.includes, tweet, g.me))
  200. skip_embed_replies = False
  201. if view == 'replies':
  202. replies_response = tweet_source.get_thread(tweet_id,
  203. only_replies=True,
  204. pagination_token = pagination_token,
  205. return_dataclass=True)
  206. elif view == 'thread':
  207. skip_embed_replies = True
  208. replies_response = tweet_source.get_thread(tweet_id,
  209. only_replies=False,
  210. author_id=tweets[0].author_id,
  211. pagination_token = pagination_token,
  212. return_dataclass=True)
  213. elif view == 'conversation':
  214. replies_response = tweet_source.get_thread(tweet_id,
  215. only_replies=False,
  216. pagination_token = pagination_token,
  217. return_dataclass=True)
  218. elif view == 'tweet':
  219. replies_response = None
  220. next_token = None
  221. #print("conversation meta:")
  222. #print(json.dumps(tweets_response.get('meta'), indent=2))
  223. if replies_response and replies_response.meta and replies_response.meta.result_count:
  224. includes = replies_response.includes
  225. tweets = list(map(lambda t: tweet_model_dc_vm(includes, t, g.me, expand_path=request.args.get('expand'), reply_depth=1), replies_response.data)) + tweets
  226. next_token = replies_response.meta.next_token
  227. # this method is OK except it doesn't work if there are no replies.
  228. #tweets.append(tweet_model(includes, list(filter(lambda t: t['id'] == tweet_id, includes.get('tweets')))[0], me))
  229. #related_tweets = [] # derived from includes
  230. tweets.reverse()
  231. query = {}
  232. if next_token:
  233. query = {
  234. **query,
  235. # FIXME only_replies
  236. 'next_data_url': url_for('.get_tweet_html', tweet_id=tweet_id, pagination_token=next_token, only_replies = '1' if only_replies else '0', author_id = tweets[0].author_id),
  237. 'next_page_url': url_for('.get_tweet_html', tweet_id=tweet_id, view=view, pagination_token=next_token)
  238. }
  239. user = {
  240. }
  241. if 'HX-Request' in request.headers:
  242. # console.log(res.tweets.map(t => t.text).join("\n\n-\n\n"))
  243. return render_template('partial/tweets-timeline.html', user = user, tweets = tweets, query = query)
  244. else:
  245. page_nav = [
  246. dict(
  247. href=url_for('.get_tweet_html', tweet_id=tweets[0].conversation_id, view='thread'),
  248. label = 'author thread',
  249. order = 10
  250. ),
  251. dict(
  252. href = url_for('.get_tweet_html', tweet_id=tweets[0].conversation_id, view='conversation'),
  253. label = 'full convo',
  254. order = 20
  255. )
  256. ]
  257. tweet = tweets_response.data[0]
  258. user = list(filter(lambda u: u.id == tweet.author_id, tweets_response.includes.users))[0]
  259. source_url = f'https://twitter.com/{user.username}/status/{tweet_id}'
  260. title = f'Tweet by {user.name} at {tweet.created_at}'
  261. opengraph_info = dict(
  262. type = 'webpage', # threads might be article
  263. url = source_url,
  264. title = title,
  265. description = tweet.text,
  266. image = user.profile_image_url
  267. )
  268. if view == 'replies':
  269. tweet = tweets[0]
  270. if tweet.id == '1608510741941989378':
  271. unreplied = [
  272. UnrepliedSection(
  273. description = "Not clear what GS is still.",
  274. span = (40, 80)
  275. )
  276. ]
  277. tweet = replace(tweet,
  278. unreplied = unreplied
  279. )
  280. expand_parts = request.args.get('expand')
  281. if expand_parts:
  282. expand_parts = expand_parts.split(',')
  283. def reply_to_thread_item (fi):
  284. nonlocal expand_parts
  285. if fi.id == '1609714342211244038':
  286. print(f'reply_to_thread_item id={fi.id}')
  287. unreplied = [
  288. UnrepliedSection(
  289. description = "Is there proof of this claim?",
  290. span = (40, 80)
  291. )
  292. ]
  293. fi = replace(fi,
  294. unreplied = unreplied
  295. )
  296. children = None
  297. if expand_parts and len(expand_parts) and fi.id == expand_parts[0]:
  298. expand_parts = expand_parts[1:]
  299. print(f'getting expanded replied for tweet={fi.id}')
  300. expanded_replies_response = tweet_source.get_thread(fi.id,
  301. only_replies=True,
  302. return_dataclass=True)
  303. if expanded_replies_response.data:
  304. print('we got expanded responses data')
  305. children = list(map(lambda t: tweet_model_dc_vm(expanded_replies_response.includes, t, g.me, expand_path=request.args.get('expand'), reply_depth=1), expanded_replies_response.data))
  306. children = list(map(reply_to_thread_item, children))
  307. return ThreadItem(feed_item=fi, children=children)
  308. children = list(map(reply_to_thread_item, tweets[1:]))
  309. root = ThreadItem(
  310. feed_item = tweet,
  311. children = children
  312. )
  313. return render_template('tweet-thread.html', user = user, root = root, query = query, page_nav=page_nav, skip_embed_replies=skip_embed_replies, opengraph_info=opengraph_info)
  314. else:
  315. return render_template(f'tweet-collection{theme_variant}.html', user = user, tweets = tweets, query = query, page_nav=page_nav, skip_embed_replies=skip_embed_replies, opengraph_info=opengraph_info)
  316. @twitter_app.route('/tweet2/<tweet_id>.html', methods=['GET'])
  317. def get_tweet2_html (tweet_id):
  318. me = g.me
  319. use_embed = int(request.args.get('embed', 0))
  320. pagination_token = request.args.get('pagination_token')
  321. view = request.args.get('view', 'replies')
  322. if g.twitter_user:
  323. token = g.twitter_user['access_token']
  324. else:
  325. token = os.environ.get('BEARER_TOKEN')
  326. tweets = []
  327. if not pagination_token:
  328. if use_embed:
  329. tweet = get_content(f'twitter:tweet:{tweet_id}', content_source_id='twitter_v2_facade.content_source:get_tweet_embed')
  330. tweets.append(tweet)
  331. else:
  332. tweet_page = get_content(f'twitter:tweet:{tweet_id}', me=me)
  333. tweets.append(tweet_page.items[0])
  334. return render_template(f'tweet-collection{theme_variant}.html', user = {}, tweets = tweets, query = {})
  335. @twitter_app.route('/followers/<user_id>.html', methods=['GET'])
  336. def get_followers_html (user_id):
  337. me = g.me
  338. content_params = cleandict({
  339. 'max_results': int(request.args.get('max_results', 1000)),
  340. 'pagination_token': request.args.get('pagination_token')
  341. })
  342. followers_page = get_content(f'twitter:followers:user:{user_id}', me=me, **content_params)
  343. followers = followers_page.items
  344. content_params['pagination_token'] = followers_page.next_token
  345. query = {
  346. 'next_data_url': url_for('.get_followers_html', me=me, user_id=user_id, **content_params)
  347. }
  348. if 'HX-Request' in request.headers:
  349. return render_template('partial/users-list.html', users=followers, query=query)
  350. else:
  351. return render_template('followers.html', users=followers, query=query)
  352. @twitter_app.route('/following/<user_id>.html', methods=['GET'])
  353. def get_following_html (user_id):
  354. me = g.me
  355. content_params = cleandict({
  356. 'max_results': int(request.args.get('max_results', 1000)),
  357. 'pagination_token': request.args.get('pagination_token')
  358. })
  359. following_page = get_content(f'twitter:following:users:{user_id}', me=me, **content_params)
  360. following = following_page.items
  361. content_params['pagination_token'] = following_page.next_token
  362. query = {
  363. 'next_data_url': url_for('.get_following_html', me=me, user_id=user_id, **content_params)
  364. }
  365. if 'HX-Request' in request.headers:
  366. return render_template('partial/users-list.html', users=following, query=query)
  367. else:
  368. return render_template('following.html', users=following, query=query)
  369. # ---------------------------------------------------------------------------------------------------------
  370. # ---------------------------------------------------------------------------------------------------------
  371. # HTMx partials
  372. # ---------------------------------------------------------------------------------------------------------
  373. # ---------------------------------------------------------------------------------------------------------
  374. def tweet_paginated_timeline ():
  375. return
  376. @twitter_app.route('/data/tweets/user/<user_id>/media', methods=['GET'])
  377. def get_data_tweets_media (user_id):
  378. """
  379. Not used anywhere... trying an idea. tweet_model needs to be updated.
  380. """
  381. token = g.twitter_user['access_token']
  382. pagination_token = request.args.get('pagination_token')
  383. tweet_source = ApiV2TweetSource(token)
  384. response_json = tweet_source.get_media_tweets(author_id=user_id,
  385. has_images=True,
  386. is_reply=False,
  387. is_retweet=False,
  388. pagination_token = pagination_token)
  389. includes = response_json.get('includes')
  390. tweets = list(map(lambda t: tweet_model(includes, t, g.me), response_json['data']))
  391. next_token = response_json.get('meta').get('next_token')
  392. query = {}
  393. if next_token:
  394. query = {
  395. **query,
  396. 'next_data_url': url_for('.get_data_tweets_media', user_id=user_id, pagination_token=next_token)
  397. }
  398. if 'HX-Request' in request.headers:
  399. user = {
  400. 'id': user_id
  401. }
  402. return render_template('partial/tweets-timeline.html', user = user, tweets = tweets, query = query)
  403. else:
  404. response_body = json.dumps({
  405. 'data': tweets,
  406. 'query': query
  407. })
  408. return jsonify(response_body)
  409. # ---------------------------------------------------------------------------------------------------------
  410. # ---------------------------------------------------------------------------------------------------------
  411. # HTMx views
  412. # ---------------------------------------------------------------------------------------------------------
  413. # ---------------------------------------------------------------------------------------------------------
  414. @twitter_app.route('/gaps.html', methods=['GET'])
  415. def get_gaps ():
  416. if not g.twitter_user:
  417. return 'need to login. go to /login.html', 403
  418. gaps = content_source.get_query_gaps(auth_user_id=g.twitter_user['id'], query_type='home_feed')
  419. for gap in gaps:
  420. gap['url'] = url_for('.get_timeline_home_html',
  421. since_id = gap['since_id'],
  422. until_id = gap['until_id'],
  423. me = g.me
  424. )
  425. view_model = dict(
  426. gaps = gaps
  427. )
  428. return render_template('gaps.html', view_model=view_model)
  429. @twitter_app.route('/cached-bookmarks.html', methods=['GET'])
  430. def get_cached_bookmarks ():
  431. if not g.twitter_user:
  432. return 'need to login. go to /login.html', 403
  433. feed_items = content_source.get_cached_collection_all_latest(auth_user_id=g.twitter_user['id'], query_type='bookmarks')
  434. view_model = dict(
  435. user = {},
  436. tweets = feed_items[:10],
  437. query = {},
  438. show_thread_controls=True
  439. )
  440. return render_template('tweet-collection-bs.html', view_model=view_model, **view_model)
  441. @twitter_app.route('/latest.html', methods=['GET'])
  442. def get_timeline_home_html (variant = "reverse_chronological", pagination_token=None):
  443. if not g.twitter_user:
  444. return 'need to login. go to /login.html', 403
  445. user_id = g.twitter_user['id']
  446. token = g.twitter_user['access_token']
  447. # exclude_newer is a proof of concept to consume a timeline backwards.
  448. # if we make a request with it as -1 then it clears from the session.
  449. exclude_newer = int(request.args.get('exclude_newer', 0))
  450. # exclude_viewed will not serve any viewed tweet, based on cache DB
  451. exclude_viewed = int(request.args.get('exclude_viewed', 0))
  452. if exclude_newer < 0:
  453. print('resetting oldest_viewed_tweet_id if set')
  454. if 'oldest_viewed_tweet_id' in session:
  455. del session['oldest_viewed_tweet_id']
  456. exclude_newer = 0
  457. if not pagination_token:
  458. pagination_token = request.args.get('pagination_token')
  459. output_format = request.args.get('format', 'html')
  460. tq = cleandict({
  461. 'pagination_token': pagination_token,
  462. 'since_id': request.args.get('since_id'),
  463. 'until_id': request.args.get('until_id'),
  464. 'end_time': request.args.get('end_time'),
  465. 'start_time': request.args.get('start_time')
  466. })
  467. if exclude_newer and 'oldest_viewed_tweet_id' in session:
  468. until_id = str(session.get('oldest_viewed_tweet_id'))
  469. print(f'get_timeline_home_html: exclude_newer: {until_id}')
  470. tq['until_id'] = until_id
  471. timeline_page = get_content(f'twitter:feed:reverse_chronological:user:{user_id}', me=g.me, **tq)
  472. next_token = timeline_page.next_token
  473. tweets = timeline_page.items
  474. if exclude_viewed:
  475. tweets = list(filter(lambda t: not t.is_viewed, tweets))
  476. tq['exclude_viewed'] = exclude_viewed
  477. if exclude_newer:
  478. tq['exclude_newer'] = exclude_newer
  479. # oldest in collection should be last...
  480. # might have an issue if it's an old RT.
  481. if tweets:
  482. oldest_tweet_id = int(tweets[-1].id)
  483. if not 'oldest_viewed_tweet_id' in session:
  484. session['oldest_viewed_tweet_id'] = oldest_tweet_id
  485. else:
  486. if oldest_tweet_id < session['oldest_viewed_tweet_id']:
  487. session['oldest_viewed_tweet_id'] = oldest_tweet_id
  488. tq['pagination_token'] = next_token
  489. query = {
  490. **tq,
  491. 'format': output_format,
  492. 'me': g.me
  493. }
  494. if next_token:
  495. query = {
  496. **query,
  497. #'next_data_url': url_for('.get_data_timeline_home', variant=variant, pagination_token=next_token),
  498. 'next_data_url': url_for('.get_timeline_home_html', **tq),
  499. 'next_page_url': url_for('.get_timeline_home_html', **tq)
  500. }
  501. user = {
  502. 'id': user_id
  503. }
  504. if output_format == 'feed.json':
  505. return jsonify(cleandict({
  506. 'data': tweets,
  507. 'query': query
  508. }))
  509. elif 'HX-Request' in request.headers:
  510. return render_template('partial/tweets-timeline.html', user = user, tweets = tweets, query = query, show_thread_controls=True)
  511. else:
  512. return render_template('tweet-collection.html', user = user, tweets = tweets, query = query, show_thread_controls=True)
  513. @twitter_app.route('/bookmarks.html', methods=['GET'])
  514. def get_bookmarks2_html ():
  515. user_id = g.twitter_user['id']
  516. token = g.twitter_user['access_token']
  517. pagination_token = request.args.get('pagination_token')
  518. max_results = int(request.args.get('limit', 10))
  519. collection_page = get_content(f'twitter:bookmarks:{user_id}', pagination_token=pagination_token, max_results=max_results)
  520. tweets = collection_page.items
  521. next_token = collection_page.next_token
  522. query = {}
  523. if next_token:
  524. query = {
  525. **query,
  526. 'next_data_url': url_for('.get_bookmarks2_html', user_id=user_id, pagination_token=next_token, limit=max_results),
  527. 'next_page_url': url_for('.get_bookmarks2_html', user_id=user_id, pagination_token=next_token, limit=max_results)
  528. }
  529. user = {
  530. 'id': user_id
  531. }
  532. if 'HX-Request' in request.headers:
  533. return render_template(f'partial/tweets-timeline{theme_variant}.html', user = user, tweets = tweets, query = query)
  534. else:
  535. return render_template(f'tweet-collection{theme_variant}.html', user = user, tweets = tweets, query = query)
  536. #@twitter_app.route('/bookmarks.html', methods=['GET'])
  537. def get_bookmarks_old_html ():
  538. user_id = g.twitter_user['id']
  539. token = g.twitter_user['access_token']
  540. pagination_token = request.args.get('pagination_token')
  541. max_results = int(request.args.get('limit', 10))
  542. tweet_source = ApiV2TweetSource(token)
  543. response_json = tweet_source.get_bookmarks(user_id,
  544. pagination_token = pagination_token, return_dataclass=True,
  545. max_results=max_results
  546. )
  547. #print(response_json)
  548. includes = response_json.includes
  549. tweets = list(map(lambda t: tweet_model_dc_vm(includes, t, g.me), response_json.data))
  550. next_token = response_json.meta.next_token
  551. query = {}
  552. if next_token:
  553. query = {
  554. **query,
  555. 'next_data_url': url_for('.get_bookmarks_html', user_id=user_id, pagination_token=next_token, limit=max_results),
  556. 'next_page_url': url_for('.get_bookmarks_html', user_id=user_id, pagination_token=next_token, limit=max_results)
  557. }
  558. user = {
  559. 'id': user_id
  560. }
  561. ts = int(time.time() * 1000)
  562. with open(f'{DATA_DIR}/cache/bookmarks_{user_id}_{ts}_{pagination_token}.json', 'wt') as f:
  563. f.write(json.dumps(response_json))
  564. if 'HX-Request' in request.headers:
  565. return render_template(f'partial/tweets-timeline{theme_variant}.html', user = user, tweets = tweets, query = query)
  566. else:
  567. return render_template(f'tweet-collection{theme_variant}.html', user = user, tweets = tweets, query = query)
  568. from hogumathi_app.content_system import get_content
  569. @twitter_app.route('/profile/<user_id>/threads.html', methods=['GET'])
  570. def get_threads_html (user_id):
  571. category = request.args.get('category')
  572. collection = get_content(f'twitter:threads:user:{user_id}')
  573. print(collection)
  574. return 'ok'
  575. @twitter_app.route('/profile/<user_id>.html', methods=['GET'])
  576. def get_profile_html (user_id):
  577. me = g.get('me')
  578. if g.twitter_user:
  579. token = g.twitter_user['access_token']
  580. # issue: retweets don't come back if we request non_public_metrics
  581. is_me = False and user_id == g.twitter_user['id']
  582. else:
  583. token = os.environ.get('BEARER_TOKEN')
  584. is_me = False
  585. output_format = request.args.get('format', 'html')
  586. pagination_token = request.args.get('pagination_token')
  587. exclude_replies = int(request.args.get('exclude_replies', 0))
  588. exclude_retweets = int(request.args.get('exclude_retweets', 0))
  589. max_results = int(request.args.get('limit', 10))
  590. since_id = request.args.get('since_id')
  591. until_id = request.args.get('until_id')
  592. start_time = request.args.get('start_time')
  593. end_time = request.args.get('end_time')
  594. query = cleandict({
  595. 'pagination_token': pagination_token,
  596. 'exclude_replies': exclude_replies,
  597. 'exclude_retweets': exclude_retweets,
  598. 'max_results': max_results,
  599. 'since_id': since_id,
  600. 'until_id': until_id,
  601. 'start_time': start_time,
  602. 'end_time': end_time
  603. })
  604. collection_page = get_content(f'twitter:feed:user:{user_id}', me=me, **query)
  605. tweets = collection_page.items
  606. next_token = collection_page.next_token
  607. # FIXME janky
  608. query['pagination_token'] = next_token
  609. if next_token:
  610. query = {
  611. **query,
  612. 'format': output_format,
  613. 'next_data_url': url_for('.get_profile_html', user_id=user_id, **query),
  614. 'next_page_url': url_for('.get_profile_html', user_id=user_id , **query)
  615. }
  616. if output_format == 'feed.json':
  617. return jsonify(cleandict({
  618. 'data': tweets,
  619. 'query': query
  620. }))
  621. elif 'HX-Request' in request.headers:
  622. profile_user = {
  623. 'id': user_id
  624. }
  625. return render_template(f'partial/tweets-timeline{theme_variant}.html', user = profile_user, tweets = tweets, query = query)
  626. else:
  627. # FIXME the user is probably present in the tweet expansions info.
  628. #social_graph = TwitterApiV2SocialGraph(token)
  629. #users_response = social_graph.get_user(user_id)
  630. #print(users_response)
  631. #user = users_response['data'][0]
  632. user = get_content(f'twitter:user:{user_id}', me=me)
  633. title = f'{user.name} ({user.username})'
  634. # FIXME official Twitter or owner's instance?
  635. source_url = f'https://www.twitter.com/{user.username}'
  636. opengraph_info = dict(
  637. type = 'webpage', # threads might be article
  638. url = source_url,
  639. title = title,
  640. description = user.description,
  641. image = user.avatar_image_url
  642. )
  643. page_nav = [
  644. dict(
  645. href = url_for('twitter_v2_facade.get_profile_html', user_id=user.id),
  646. label = 'Timeline',
  647. order = 10,
  648. ),
  649. dict (
  650. href = url_for('twitter_v2_facade.get_following_html', user_id=user.id),
  651. label = 'Following',
  652. order = 40,
  653. ),
  654. dict (
  655. href = url_for('twitter_v2_facade.get_followers_html', user_id=user.id),
  656. label = 'Followers',
  657. order = 50,
  658. ),
  659. dict (
  660. href = url_for('twitter_v2_facade.get_threads_html', user_id=user.id),
  661. label = 'Threads',
  662. order = 55,
  663. )
  664. ]
  665. if not g.twitter_user:
  666. for uid, acct in session.items():
  667. if uid.startswith('twitter:'):
  668. page_nav += [
  669. dict(
  670. href = url_for('twitter_v2_facade.get_profile_html', user_id=user_id, me=uid, **query),
  671. label = f'View as {acct["id"]}',
  672. order = 1000,
  673. )
  674. ]
  675. if g.twitter_live_enabled:
  676. page_nav += [
  677. dict(
  678. href = url_for('twitter_v2_live_facade.get_likes_html', user_id=user.id),
  679. label = 'Likes',
  680. order = 20,
  681. ),
  682. dict (
  683. href = url_for('twitter_v2_live_facade.get_mentions_html', user_id=user.id),
  684. label = 'Mentions',
  685. order = 30,
  686. ),
  687. dict (
  688. href = url_for('twitter_v2_live_facade.get_user_activity_html', user_id=user.id),
  689. label = 'Activity',
  690. order = 60,
  691. )
  692. ]
  693. top8 = get_top8(user_id)
  694. brand = None
  695. brand_info = {}
  696. if g.twitter_live_enabled:
  697. brand = get_content(f'brand:search:account:twitter:{user_id}', expand=False)
  698. if brand:
  699. page_nav += [
  700. dict(
  701. href = url_for('brands.get_brand_html', brand_id=brand['id']),
  702. label = 'Brand Page',
  703. order = 5000,
  704. )
  705. ]
  706. brand_info = brand.get('expanded', {})
  707. return render_template(f'user-profile{theme_variant}.html',
  708. user = user.raw_user,
  709. user_dc = user,
  710. tweets = tweets,
  711. query = query,
  712. opengraph_info=opengraph_info,
  713. page_nav = page_nav,
  714. top8=top8,
  715. brand=brand,
  716. **brand_info
  717. )
  718. @twitter_app.route('/users.html', methods=['GET'])
  719. def get_users ():
  720. ids = request.args.get('ids')
  721. if not ids:
  722. return 'supply ids=', 400
  723. token = g.twitter_user['access_token']
  724. tweet_source = TwitterApiV2SocialGraph(token)
  725. response_json = tweet_source.get_users(ids)
  726. ts = int(time.time() * 1000)
  727. with open(f'{DATA_DIR}/cache/users_{ts}.json', 'wt') as f:
  728. f.write(json.dumps(response_json))
  729. #print(response_json)
  730. #run_script('on_user_seen', {'twitter_user': g.twitter_user, 'users': response_json})
  731. #following = list(map(lambda f: f['id'], response_json.get('data')))
  732. users = response_json.get('data')
  733. return render_template('following.html', users=users)
  734. @twitter_app.route('/media/upload', methods=['POST'])
  735. def post_media_upload ():
  736. token = g.twitter_user['access_token']
  737. form = {
  738. 'media_category': 'tweet_image'
  739. }
  740. headers = {
  741. 'Authorization': 'Bearer {}'.format(token)
  742. }
  743. url = 'http://localhost:5004/twitter/fake-twitter/media/upload'
  744. #url = 'https://upload.twitter.com/1.1/media/upload.json' # .json
  745. upload_media = {}
  746. for e in request.files.items():
  747. media_name = e[0]
  748. f = e[1]
  749. print('.')
  750. files = {'media': [secure_filename(f.filename), BufferedReader(f), f.content_type]}
  751. response = requests.post(url, files=files, data=form, headers=headers)
  752. print(response.status_code)
  753. print(response.text)
  754. response_json = json.loads(response.text)
  755. upload_media[media_name] = response_json
  756. return jsonify({'upload_media': upload_media})
  757. @twitter_app.route('/fake-twitter/media/upload', methods=['POST'])
  758. def post_media_upload2 ():
  759. print(request.content_type)
  760. f = request.files.get('media')
  761. f.seek(0,2)
  762. media_size = f.tell()
  763. media = {
  764. #'_auth': request.headers.get('Authorization'),
  765. 'media_key': '3_{}'.format(secure_filename(f.filename)),
  766. 'media_id': secure_filename(f.filename),
  767. 'size': media_size,
  768. 'expires_after_secs': 86400,
  769. 'image': {
  770. 'image_type': f.content_type,
  771. 'w': 1,
  772. 'h': 1
  773. }
  774. }
  775. return jsonify(media)
  776. def get_nav_items ():
  777. nav_items = [
  778. ]
  779. twitter_user = g.get('twitter_user')
  780. me = g.get('me')
  781. if twitter_user:
  782. nav_items += [
  783. dict(
  784. href = url_for('twitter_v2_facade.get_timeline_home_html'),
  785. label = 'Latest Tweets',
  786. order = 0
  787. ),
  788. dict (
  789. href = url_for('twitter_v2_facade.get_bookmarks2_html'),
  790. label = 'Bookmarks',
  791. order = 100
  792. ),
  793. dict (
  794. href = url_for('twitter_v2_facade.get_profile_html', user_id=twitter_user['id']),
  795. label = 'My Profile',
  796. order = 200
  797. ),
  798. dict(
  799. href = url_for('twitter_v2_facade.get_gaps'),
  800. label = 'Gaps',
  801. order = 300
  802. ),
  803. dict (
  804. href = url_for('twitter_v2_facade.oauth2_login.get_logout_html'),
  805. label = f'Logout ({me})',
  806. order = 1000
  807. )
  808. ]
  809. if g.get('twitter_live_enabled'):
  810. nav_items += [
  811. dict (
  812. href = url_for('twitter_v2_live_facade.get_conversations_html'),
  813. label = 'DMs',
  814. order = 10
  815. ),
  816. dict (
  817. href = url_for('twitter_v2_live_facade.get_mentions_html', user_id=twitter_user['id']),
  818. label = 'Mentions',
  819. order = 20
  820. )
  821. ]
  822. return nav_items
  823. @twitter_app.before_request
  824. def add_module_nav_items_to_template_context ():
  825. g.module_nav = get_nav_items()
  826. def get_top8 (user_id):
  827. if user_id != '14520320':
  828. return
  829. return [
  830. dict(
  831. id='14520320'
  832. ),
  833. dict(
  834. id='14520320'
  835. ),
  836. dict(
  837. id='14520320'
  838. ),
  839. dict(
  840. id='14520320'
  841. ),
  842. dict(
  843. id='14520320'
  844. ),
  845. dict(
  846. id='14520320'
  847. ),
  848. dict(
  849. id='14520320'
  850. ),
  851. dict(
  852. id='14520320'
  853. ),
  854. ]