facade.py 33 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064
  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('/followers/<user_id>.html', methods=['GET'])
  316. def get_followers_html (user_id):
  317. if not g.twitter_user:
  318. return 'need to log in.', 403
  319. use_cache = request.args.get('use_cache')
  320. token = g.twitter_user['access_token']
  321. social_source = TwitterApiV2SocialGraph(token)
  322. if use_cache:
  323. print(f'using cache for user {user_id}: {use_cache}')
  324. with open(f'.data/cache/followers_{user_id}_{use_cache}.json', 'rt') as f:
  325. response_json = json.load(f)
  326. else:
  327. response_json = social_source.get_followers(user_id, max_results=1000, return_dataclass=True)
  328. ts = int(time.time() * 1000)
  329. print(f'followers cache for {user_id}: {ts}')
  330. with open(f'{DATA_DIR}/cache/followers_{user_id}_{ts}.json', 'wt') as f:
  331. json.dump(response_json, f, indent=2)
  332. #print(response_json)
  333. #run_script('on_user_seen', {'twitter_user': g.twitter_user, 'users': response_json})
  334. #followers = list(map(lambda f: f['id'], response_json.get('data')))
  335. followers = response_json.data
  336. followers = list(map(user_model_dc, followers))
  337. return render_template('following.html', users=followers)
  338. @twitter_app.route('/following/<user_id>.html', methods=['GET'])
  339. def get_following_html (user_id):
  340. if not g.twitter_user:
  341. return 'need to log in.', 403
  342. token = g.twitter_user['access_token']
  343. social_source = TwitterApiV2SocialGraph(token)
  344. response_json = social_source.get_following(user_id, max_results=1000, return_dataclass=True)
  345. ts = int(time.time() * 1000)
  346. with open(f'{DATA_DIR}/cache/following_{user_id}_{ts}.json', 'wt') as f:
  347. f.write(json.dumps(response_json))
  348. #print(response_json)
  349. #run_script('on_user_seen', {'twitter_user': g.twitter_user, 'users': response_json})
  350. #following = list(map(lambda f: f['id'], response_json.get('data')))
  351. following = list(map(user_model_dc, response_json.data))
  352. return render_template('following.html', users=following)
  353. # ---------------------------------------------------------------------------------------------------------
  354. # ---------------------------------------------------------------------------------------------------------
  355. # HTMx partials
  356. # ---------------------------------------------------------------------------------------------------------
  357. # ---------------------------------------------------------------------------------------------------------
  358. def tweet_paginated_timeline ():
  359. return
  360. @twitter_app.route('/data/tweets/user/<user_id>/media', methods=['GET'])
  361. def get_data_tweets_media (user_id):
  362. """
  363. Not used anywhere... trying an idea. tweet_model needs to be updated.
  364. """
  365. token = g.twitter_user['access_token']
  366. pagination_token = request.args.get('pagination_token')
  367. tweet_source = ApiV2TweetSource(token)
  368. response_json = tweet_source.get_media_tweets(author_id=user_id,
  369. has_images=True,
  370. is_reply=False,
  371. is_retweet=False,
  372. pagination_token = pagination_token)
  373. includes = response_json.get('includes')
  374. tweets = list(map(lambda t: tweet_model(includes, t, g.me), response_json['data']))
  375. next_token = response_json.get('meta').get('next_token')
  376. query = {}
  377. if next_token:
  378. query = {
  379. **query,
  380. 'next_data_url': url_for('.get_data_tweets_media', user_id=user_id, pagination_token=next_token)
  381. }
  382. if 'HX-Request' in request.headers:
  383. user = {
  384. 'id': user_id
  385. }
  386. return render_template('partial/tweets-timeline.html', user = user, tweets = tweets, query = query)
  387. else:
  388. response_body = json.dumps({
  389. 'data': tweets,
  390. 'query': query
  391. })
  392. return jsonify(response_body)
  393. # ---------------------------------------------------------------------------------------------------------
  394. # ---------------------------------------------------------------------------------------------------------
  395. # HTMx views
  396. # ---------------------------------------------------------------------------------------------------------
  397. # ---------------------------------------------------------------------------------------------------------
  398. @twitter_app.route('/latest.html', methods=['GET'])
  399. def get_timeline_home_html (variant = "reverse_chronological", pagination_token=None):
  400. if not g.twitter_user:
  401. return 'need to login. go to /login.html', 403
  402. user_id = g.twitter_user['id']
  403. token = g.twitter_user['access_token']
  404. if not pagination_token:
  405. pagination_token = request.args.get('pagination_token')
  406. output_format = request.args.get('format', 'html')
  407. tq = cleandict({
  408. 'pagination_token': pagination_token,
  409. 'since_id': request.args.get('since_id'),
  410. 'until_id': request.args.get('until_id'),
  411. 'end_time': request.args.get('end_time')
  412. })
  413. tweet_source = ApiV2TweetSource(token)
  414. response = tweet_source.get_home_timeline(user_id, **tq)
  415. #print(json.dumps(response_json, indent=2))
  416. includes = response.includes
  417. tweets = list(map(lambda t: tweet_model_dc_vm(includes, t, g.me), response.data))
  418. next_token = response.meta.next_token
  419. tq['pagination_token'] = next_token
  420. query = {
  421. **tq,
  422. 'format': output_format,
  423. 'me': g.me
  424. }
  425. if next_token:
  426. query = {
  427. **query,
  428. #'next_data_url': url_for('.get_data_timeline_home', variant=variant, pagination_token=next_token),
  429. 'next_data_url': url_for('.get_timeline_home_html', **tq),
  430. 'next_page_url': url_for('.get_timeline_home_html', **tq)
  431. }
  432. user = {
  433. 'id': user_id
  434. }
  435. if output_format == 'feed.json':
  436. return jsonify(cleandict({
  437. 'data': tweets,
  438. 'query': query
  439. }))
  440. elif 'HX-Request' in request.headers:
  441. return render_template('partial/tweets-timeline.html', user = user, tweets = tweets, query = query, show_thread_controls=True)
  442. else:
  443. return render_template('tweet-collection.html', user = user, tweets = tweets, query = query, show_thread_controls=True)
  444. @twitter_app.route('/bookmarks.html', methods=['GET'])
  445. def get_bookmarks2_html ():
  446. user_id = g.twitter_user['id']
  447. token = g.twitter_user['access_token']
  448. pagination_token = request.args.get('pagination_token')
  449. max_results = int(request.args.get('limit', 10))
  450. collection_page = get_content(f'twitter:bookmarks:{user_id}', pagination_token=pagination_token, max_results=max_results)
  451. tweets = collection_page.items
  452. next_token = collection_page.next_token
  453. query = {}
  454. if next_token:
  455. query = {
  456. **query,
  457. 'next_data_url': url_for('.get_bookmarks2_html', user_id=user_id, pagination_token=next_token, limit=max_results),
  458. 'next_page_url': url_for('.get_bookmarks2_html', user_id=user_id, pagination_token=next_token, limit=max_results)
  459. }
  460. user = {
  461. 'id': user_id
  462. }
  463. if 'HX-Request' in request.headers:
  464. return render_template(f'partial/tweets-timeline{theme_variant}.html', user = user, tweets = tweets, query = query)
  465. else:
  466. return render_template(f'tweet-collection{theme_variant}.html', user = user, tweets = tweets, query = query)
  467. #@twitter_app.route('/bookmarks.html', methods=['GET'])
  468. def get_bookmarks_old_html ():
  469. user_id = g.twitter_user['id']
  470. token = g.twitter_user['access_token']
  471. pagination_token = request.args.get('pagination_token')
  472. max_results = int(request.args.get('limit', 10))
  473. tweet_source = ApiV2TweetSource(token)
  474. response_json = tweet_source.get_bookmarks(user_id,
  475. pagination_token = pagination_token, return_dataclass=True,
  476. max_results=max_results
  477. )
  478. #print(response_json)
  479. includes = response_json.includes
  480. tweets = list(map(lambda t: tweet_model_dc_vm(includes, t, g.me), response_json.data))
  481. next_token = response_json.meta.next_token
  482. query = {}
  483. if next_token:
  484. query = {
  485. **query,
  486. 'next_data_url': url_for('.get_bookmarks_html', user_id=user_id, pagination_token=next_token, limit=max_results),
  487. 'next_page_url': url_for('.get_bookmarks_html', user_id=user_id, pagination_token=next_token, limit=max_results)
  488. }
  489. user = {
  490. 'id': user_id
  491. }
  492. ts = int(time.time() * 1000)
  493. with open(f'{DATA_DIR}/cache/bookmarks_{user_id}_{ts}_{pagination_token}.json', 'wt') as f:
  494. f.write(json.dumps(response_json))
  495. if 'HX-Request' in request.headers:
  496. return render_template(f'partial/tweets-timeline{theme_variant}.html', user = user, tweets = tweets, query = query)
  497. else:
  498. return render_template(f'tweet-collection{theme_variant}.html', user = user, tweets = tweets, query = query)
  499. from hogumathi_app.content_system import get_content
  500. @twitter_app.route('/profile/<user_id>.html', methods=['GET'])
  501. def get_profile_html (user_id):
  502. me = g.get('me')
  503. if g.twitter_user:
  504. token = g.twitter_user['access_token']
  505. # issue: retweets don't come back if we request non_public_metrics
  506. is_me = False and user_id == g.twitter_user['id']
  507. else:
  508. token = os.environ.get('BEARER_TOKEN')
  509. is_me = False
  510. output_format = request.args.get('format', 'html')
  511. pagination_token = request.args.get('pagination_token')
  512. exclude_replies = request.args.get('exclude_replies', '0')
  513. exclude_retweets = request.args.get('exclude_retweets', '0')
  514. query = cleandict({
  515. 'pagination_token': pagination_token,
  516. 'exclude_replies': exclude_replies,
  517. 'exclude_retweets': exclude_retweets,
  518. 'format': output_format
  519. })
  520. collection_page = get_content(f'twitter:feed:user:{user_id}', me=me, **query)
  521. tweets = collection_page.items
  522. next_token = collection_page.next_token
  523. if next_token:
  524. query = {
  525. **query,
  526. 'next_data_url': url_for('.get_profile_html', user_id=user_id, pagination_token=next_token, exclude_replies=exclude_replies, exclude_retweets=exclude_retweets),
  527. 'next_page_url': url_for('.get_profile_html', user_id=user_id , pagination_token=next_token, exclude_replies=exclude_replies, exclude_retweets=exclude_retweets)
  528. }
  529. if output_format == 'feed.json':
  530. return jsonify(cleandict({
  531. 'data': tweets,
  532. 'query': query
  533. }))
  534. elif 'HX-Request' in request.headers:
  535. profile_user = {
  536. 'id': user_id
  537. }
  538. return render_template(f'partial/tweets-timeline{theme_variant}.html', user = profile_user, tweets = tweets, query = query)
  539. else:
  540. # FIXME the user is probably present in the tweet expansions info.
  541. #social_graph = TwitterApiV2SocialGraph(token)
  542. #users_response = social_graph.get_user(user_id)
  543. #print(users_response)
  544. #user = users_response['data'][0]
  545. user = get_content(f'twitter:user:{user_id}', me=me)
  546. title = f'{user.name} ({user.username})'
  547. # FIXME official Twitter or owner's instance?
  548. source_url = f'https://www.twitter.com/{user.username}'
  549. opengraph_info = dict(
  550. type = 'webpage', # threads might be article
  551. url = source_url,
  552. title = title,
  553. description = user.description,
  554. image = user.avatar_image_url
  555. )
  556. page_nav = [
  557. dict(
  558. href = url_for('twitter_v2_facade.get_profile_html', user_id=user.id),
  559. label = 'Timeline',
  560. order = 10,
  561. ),
  562. dict (
  563. href = url_for('twitter_v2_facade.get_following_html', user_id=user.id),
  564. label = 'Following',
  565. order = 40,
  566. ),
  567. dict (
  568. href = url_for('twitter_v2_facade.get_followers_html', user_id=user.id),
  569. label = 'Followers',
  570. order = 50,
  571. )
  572. ]
  573. if not g.twitter_user:
  574. for uid, acct in session.items():
  575. if uid.startswith('twitter:'):
  576. page_nav += [
  577. dict(
  578. href = url_for('twitter_v2_facade.get_profile_html', user_id=user_id, me=uid),
  579. label = f'View as {acct["id"]}',
  580. order = 1000,
  581. )
  582. ]
  583. if g.twitter_live_enabled:
  584. page_nav += [
  585. dict(
  586. href = url_for('twitter_v2_live_facade.get_likes_html', user_id=user.id),
  587. label = 'Likes',
  588. order = 20,
  589. ),
  590. dict (
  591. href = url_for('twitter_v2_live_facade.get_mentions_html', user_id=user.id),
  592. label = 'Mentions',
  593. order = 30,
  594. ),
  595. dict (
  596. href = url_for('twitter_v2_live_facade.get_user_activity_html', user_id=user.id),
  597. label = 'Activity',
  598. order = 60,
  599. )
  600. ]
  601. top8 = get_top8(user_id)
  602. brand = None
  603. brand_info = {}
  604. if g.twitter_live_enabled:
  605. brand = get_content(f'brand:search:account:twitter:{user_id}', expand=False)
  606. if brand:
  607. page_nav += [
  608. dict(
  609. href = url_for('brands.get_brand_html', brand_id=brand['id']),
  610. label = 'Brand Page',
  611. order = 5000,
  612. )
  613. ]
  614. brand_info = brand.get('expanded', {})
  615. return render_template(f'user-profile{theme_variant}.html',
  616. user = user.raw_user,
  617. user_dc = user,
  618. tweets = tweets,
  619. query = query,
  620. opengraph_info=opengraph_info,
  621. page_nav = page_nav,
  622. top8=top8,
  623. brand=brand,
  624. **brand_info
  625. )
  626. @twitter_app.route('/users.html', methods=['GET'])
  627. def get_users ():
  628. ids = request.args.get('ids')
  629. if not ids:
  630. return 'supply ids=', 400
  631. token = g.twitter_user['access_token']
  632. tweet_source = TwitterApiV2SocialGraph(token)
  633. response_json = tweet_source.get_users(ids)
  634. ts = int(time.time() * 1000)
  635. with open(f'{DATA_DIR}/cache/users_{ts}.json', 'wt') as f:
  636. f.write(json.dumps(response_json))
  637. #print(response_json)
  638. #run_script('on_user_seen', {'twitter_user': g.twitter_user, 'users': response_json})
  639. #following = list(map(lambda f: f['id'], response_json.get('data')))
  640. users = response_json.get('data')
  641. return render_template('following.html', users=users)
  642. @twitter_app.route('/media/upload', methods=['POST'])
  643. def post_media_upload ():
  644. token = g.twitter_user['access_token']
  645. form = {
  646. 'media_category': 'tweet_image'
  647. }
  648. headers = {
  649. 'Authorization': 'Bearer {}'.format(token)
  650. }
  651. url = 'http://localhost:5004/twitter/fake-twitter/media/upload'
  652. #url = 'https://upload.twitter.com/1.1/media/upload.json' # .json
  653. upload_media = {}
  654. for e in request.files.items():
  655. media_name = e[0]
  656. f = e[1]
  657. print('.')
  658. files = {'media': [secure_filename(f.filename), BufferedReader(f), f.content_type]}
  659. response = requests.post(url, files=files, data=form, headers=headers)
  660. print(response.status_code)
  661. print(response.text)
  662. response_json = json.loads(response.text)
  663. upload_media[media_name] = response_json
  664. return jsonify({'upload_media': upload_media})
  665. @twitter_app.route('/fake-twitter/media/upload', methods=['POST'])
  666. def post_media_upload2 ():
  667. print(request.content_type)
  668. f = request.files.get('media')
  669. f.seek(0,2)
  670. media_size = f.tell()
  671. media = {
  672. #'_auth': request.headers.get('Authorization'),
  673. 'media_key': '3_{}'.format(secure_filename(f.filename)),
  674. 'media_id': secure_filename(f.filename),
  675. 'size': media_size,
  676. 'expires_after_secs': 86400,
  677. 'image': {
  678. 'image_type': f.content_type,
  679. 'w': 1,
  680. 'h': 1
  681. }
  682. }
  683. return jsonify(media)
  684. def get_nav_items ():
  685. nav_items = [
  686. ]
  687. twitter_user = g.get('twitter_user')
  688. me = g.get('me')
  689. if twitter_user:
  690. nav_items += [
  691. dict(
  692. href = url_for('twitter_v2_facade.get_timeline_home_html'),
  693. label = 'Latest Tweets',
  694. order = 0
  695. ),
  696. dict (
  697. href = url_for('twitter_v2_facade.get_bookmarks2_html'),
  698. label = 'Bookmarks',
  699. order = 100
  700. ),
  701. dict (
  702. href = url_for('twitter_v2_facade.get_profile_html', user_id=twitter_user['id']),
  703. label = 'My Profile',
  704. order = 200
  705. ),
  706. dict (
  707. href = url_for('twitter_v2_facade.oauth2_login.get_logout_html'),
  708. label = f'Logout ({me})',
  709. order = 1000
  710. )
  711. ]
  712. if g.get('twitter_live_enabled'):
  713. nav_items += [
  714. dict (
  715. href = url_for('twitter_v2_live_facade.get_conversations_html'),
  716. label = 'DMs',
  717. order = 10
  718. ),
  719. dict (
  720. href = url_for('twitter_v2_live_facade.get_mentions_html', user_id=twitter_user['id']),
  721. label = 'Mentions',
  722. order = 20
  723. )
  724. ]
  725. return nav_items
  726. @twitter_app.before_request
  727. def add_module_nav_items_to_template_context ():
  728. g.module_nav = get_nav_items()
  729. def get_top8 (user_id):
  730. if user_id != '14520320':
  731. return
  732. return [
  733. dict(
  734. id='14520320'
  735. ),
  736. dict(
  737. id='14520320'
  738. ),
  739. dict(
  740. id='14520320'
  741. ),
  742. dict(
  743. id='14520320'
  744. ),
  745. dict(
  746. id='14520320'
  747. ),
  748. dict(
  749. id='14520320'
  750. ),
  751. dict(
  752. id='14520320'
  753. ),
  754. dict(
  755. id='14520320'
  756. ),
  757. ]