facade.py 34 KB

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