facade.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738
  1. from dataclasses import asdict
  2. from typing import List
  3. from dacite import from_dict
  4. from configparser import ConfigParser
  5. import base64
  6. from flask import Flask, json, Response, render_template, request, send_from_directory, Blueprint, url_for, g, jsonify
  7. from flask_cors import CORS
  8. import sqlite3
  9. import os
  10. import json
  11. import json_stream
  12. from zipfile import ZipFile
  13. import itertools
  14. import datetime
  15. import dateutil
  16. import dateutil.parser
  17. import dateutil.tz
  18. import requests
  19. from twitter_v2 import types as tv2_types
  20. from twitter_v2.archive import ArchiveTweetSource
  21. from hogumathi_app import content_system, view_model as h_vm
  22. ARCHIVE_TWEETS_PATH=os.environ.get('ARCHIVE_TWEETS_PATH', '.data/tweets.json')
  23. TWEET_DB_PATH=os.environ.get('TWEET_DB_PATH', '.data/tweet.db')
  24. twitter_app = Blueprint('twitter_archive_facade', 'twitter_archive_facade',
  25. static_folder='static',
  26. static_url_path='',
  27. url_prefix='/')
  28. @twitter_app.before_request
  29. def add_me ():
  30. #if me.startswith('twitter') and me in session:
  31. #g.twitter_user = {'id': '0'}
  32. return
  33. @twitter_app.context_processor
  34. def inject_me():
  35. #return {'twitter_user': g.twitter_user}
  36. return {}
  37. # ---------------------------------------------------------------------------------------------------------
  38. # ---------------------------------------------------------------------------------------------------------
  39. # Tweet Archive and old tests
  40. # ---------------------------------------------------------------------------------------------------------
  41. # ---------------------------------------------------------------------------------------------------------
  42. # https://stackoverflow.com/questions/48218065/programmingerror-sqlite-objects-created-in-a-thread-can-only-be-used-in-that-sa
  43. db = sqlite3.connect(":memory:", check_same_thread=False)
  44. db_need_init = True
  45. if db_need_init:
  46. print("Creating tweet db...")
  47. db.execute("create table tweet (id, created_at, content)")
  48. def tweets_js_to_json (path, to_path):
  49. # open JS file provided in archive and convert it to JSON
  50. # string manipulation should be enough
  51. return True
  52. def populate_tweetsdb_from_compressed_json (db, tweets_json_path):
  53. # perf: we should find a batch size for executemany if this is too slow.
  54. # https://stackoverflow.com/questions/43785569/for-loop-or-executemany-python-and-sqlite3
  55. ti = open(tweets_json_path)
  56. data = json_stream.load(ti)
  57. for tweet in data.persistent():
  58. reply = None
  59. if "reply" in tweet:
  60. reply = tweet["reply"]
  61. values = [tweet["id"], tweet["full_text_length"], tweet["date"], reply]
  62. db.execute("insert into tweet (id, full_text_length, date, reply) values (?, ?, ?, ?)", values)
  63. ti.close()
  64. return True
  65. def print_retweets (tweets_path):
  66. tweets_file = open(tweets_path, 'rt', encoding='utf-8')
  67. tweets_data = json_stream.load(tweets_file)
  68. print('[')
  69. for t in tweets_data:
  70. tweet = t.persistent()['tweet']
  71. if int(tweet['retweet_count']) > 1:
  72. print(json.dumps({'id': tweet['id'], 'x': tweet['created_at'], 'y': tweet['retweet_count']}) + ',')
  73. print(']')
  74. tweets_file.close()
  75. return True
  76. def tweet_to_actpub (t):
  77. return t
  78. @twitter_app.route('/tweets/isd', methods=['GET'])
  79. def get_tweets_isd ():
  80. # simulate GraphQL conventions with REST:
  81. # created_at[gte]=
  82. # created_at[lte]=
  83. # author=
  84. # content[re]=
  85. # expansions=media,...
  86. #results = langs_con.execute("select rowid, id, created_at, content from tweet").fetchall()
  87. #return Response(json.dumps(results), mimetype='application/json')
  88. return send_from_directory('data', 'tweets-ispoogedaily.json')
  89. @twitter_app.route('/tweets/storms', methods=['GET'])
  90. def get_tweet_storms ():
  91. #content = open('data/storm-summaries-2021.json').read()
  92. #return Response(content, mimetype='application/json')
  93. return send_from_directory('data', 'storm-summaries-2021.json')
  94. @twitter_app.route('/bookmarks', methods=['GET'])
  95. def get_bookmarks ():
  96. #content = open('data/storm-summaries-2021.json').read()
  97. #return Response(content, mimetype='application/json')
  98. return send_from_directory('data', 'bookmarks-ispoogedaily.json')
  99. @twitter_app.route('/timeline', methods=['GET'])
  100. def get_timeline ():
  101. #content = open('data/storm-summaries-2021.json').read()
  102. #return Response(content, mimetype='application/json')
  103. return send_from_directory('data', 'timeline-minimal.json')
  104. @twitter_app.route('/tweets/compressed', methods=['POST'])
  105. def post_tweets_compressed ():
  106. db_exists = os.path.exists(TWEET_DB_PATH)
  107. if not db_exists:
  108. db = sqlite3.connect(TWEET_DB_PATH)
  109. db.execute("create table tweet (id, full_text_length, date, reply)")
  110. populate_tweetsdb_from_compressed_json(db, ".data/tweet-items.json")
  111. db.commit()
  112. db.close()
  113. #content = open('data/storm-summaries-2021.json').read()
  114. #return Response(content, mimetype='application/json')
  115. return Response("ok")
  116. tweets_form_meta_data = {
  117. 'fields': [
  118. {'name': 'id'},
  119. {'name': 'created_at', 'type': 'date'},
  120. {'name': 'retweeted', 'type': 'boolean'},
  121. {'name': 'favorited', 'type': 'boolean'},
  122. {'name': 'retweet_count', 'type': 'int'},
  123. {'name': 'favorite_count', 'type': 'int'},
  124. {'name': 'full_text', 'type': 'string', 'searchable': True},
  125. {'name': 'in_reply_to_status_id_str', 'type': 'string'},
  126. {'name': 'in_reply_to_user_id', 'type': 'string'},
  127. {'name': 'in_reply_to_screen_name', 'type': 'string'}
  128. ],
  129. 'id': 'id',
  130. 'root': 'tweets',
  131. 'url': '/tweets/search',
  132. 'access': ['read']
  133. }
  134. @twitter_app.route('/tweets/form', methods=['GET'])
  135. def get_tweets_form ():
  136. response_body = {
  137. 'metaData': tweets_form_meta_data
  138. }
  139. return Response(json.dumps(response_body), mimetype="application/json")
  140. def db_tweet_to_card (tweet):
  141. user = {'username': 'ispoogedaily', 'id': '14520320'}
  142. tweet_url = 'https://twitter.com/{}/status/{}'.format(user['username'], tweet['id'])
  143. content = tweet['full_text'] + "\n\n[view tweet]({})".format(tweet_url)
  144. card = {
  145. 'id': 'tweet-' + tweet['id'],
  146. 'content': content,
  147. 'content_type': 'text/plain',
  148. 'created_at': tweet['created_at'],
  149. 'modified_at': None,
  150. 'title': '@' + user['username'] + ' at ' + tweet['created_at'],
  151. 'content_source': tweet_url,
  152. #'tweet': tweet,
  153. #'user': user
  154. }
  155. return card
  156. # tweetStore = new Ext.data.JsonStore({'url': 'http://localhost:5004/tweets/search.rows.json', 'autoLoad': true})
  157. def tweet_model (tweet_data):
  158. # retweeted_by, avi_icon_url, display_name, handle, created_at, text
  159. """
  160. {"id": "797839193", "created_at": "2008-04-27T04:00:27", "retweeted": 0, "favorited": 0, "retweet_count": "0", "favorite_count": "0", "full_text": "Putting pizza on. Come over any time!", "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_screen_name": null, "author_id": "14520320"}, {"id": "797849979", "created_at": "2008-04-27T04:27:46", "retweeted": 0, "favorited": 0, "retweet_count": "0", "favorite_count": "0", "full_text": "hijacked!@!!!", "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_screen_name": null, "author_id": "14520320"}
  161. """
  162. t = {
  163. 'id': tweet_data['id'],
  164. 'text': tweet_data['full_text'],
  165. 'created_at': tweet_data['created_at'],
  166. 'author_is_verified': False,
  167. 'conversation_id': tweet_data['id'],
  168. 'avi_icon_url': '',
  169. 'display_name': 'Archive User',
  170. 'handle': '!archive',
  171. 'author_url': url_for('.get_profile_html', user_id='0'),
  172. 'author_id': '0',
  173. 'source_url': '!source_url',
  174. 'source_author_url': '!source_author_url',
  175. #'is_edited': len(tweet_data['edit_history_tweet_ids']) > 1
  176. }
  177. t['public_metrics'] = {
  178. 'like_count': int(tweet_data['favorite_count']),
  179. 'retweet_count': int(tweet_data['retweet_count']),
  180. 'reply_count': 0,
  181. 'quote_count': 0
  182. }
  183. return t
  184. def tweet_model_vm (tweet_data) -> List[h_vm.FeedItem]:
  185. # retweeted_by, avi_icon_url, display_name, handle, created_at, text
  186. """
  187. {"id": "797839193", "created_at": "2008-04-27T04:00:27", "retweeted": 0, "favorited": 0, "retweet_count": "0", "favorite_count": "0", "full_text": "Putting pizza on. Come over any time!", "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_screen_name": null, "author_id": "14520320"}, {"id": "797849979", "created_at": "2008-04-27T04:27:46", "retweeted": 0, "favorited": 0, "retweet_count": "0", "favorite_count": "0", "full_text": "hijacked!@!!!", "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_screen_name": null, "author_id": "14520320"}
  188. """
  189. t = h_vm.FeedItem(
  190. id = tweet_data['id'],
  191. text = tweet_data['full_text'],
  192. created_at = tweet_data['created_at'],
  193. author_is_verified = False,
  194. conversation_id = tweet_data['id'],
  195. avi_icon_url = '',
  196. display_name = 'Archive User',
  197. handle = '!archive',
  198. url = url_for('twitter_archive_facade.get_tweet_html', tweet_id = tweet_data['id']),
  199. author_url = url_for('twitter_archive_facade.get_profile_html', user_id='0'),
  200. author_id = '0',
  201. source_url = '!source_url',
  202. source_author_url = '!source_author_url',
  203. #'is_edited': len(tweet_data['edit_history_tweet_ids']) > 1
  204. public_metrics = h_vm.PublicMetrics(
  205. like_count = int(tweet_data['favorite_count']),
  206. retweet_count = int(tweet_data['retweet_count']),
  207. reply_count = 0,
  208. quote_count = 0
  209. ),
  210. debug_source_data = tweet_data
  211. )
  212. return t
  213. def get_user_feed (user_id, pagination_token=None, me=None):
  214. tweet_source = ArchiveTweetSource(ARCHIVE_TWEETS_PATH)
  215. db_tweets = tweet_source.get_user_timeline(author_id = user_id,
  216. since_id = pagination_token,
  217. #exclude_replies = exclude_replies == '1'
  218. )
  219. tweets = list(map(tweet_model_vm, db_tweets))
  220. next_token = db_tweets[-1]['id']
  221. collection_page = h_vm.CollectionPage(
  222. id = user_id,
  223. items = tweets,
  224. next_token = next_token
  225. )
  226. return collection_page
  227. def get_tweets (ids, me = None):
  228. tweet_source = ArchiveTweetSource(ARCHIVE_TWEETS_PATH)
  229. db_tweets = tweet_source.get_tweets(ids)
  230. tweets = list(map(tweet_model_vm, db_tweets))
  231. collection_page = h_vm.CollectionPage(
  232. id = ','.join(ids),
  233. items = tweets
  234. )
  235. return collection_page
  236. def get_tweets_search_sql (sql = None, sql_params = [], to_feed_item_fn=tweet_model_vm):
  237. print('get_tweets_search_sql')
  238. tweet_source = ArchiveTweetSource(ARCHIVE_TWEETS_PATH)
  239. db_tweets = tweet_source.search_tweets_sql(sql, sql_params)
  240. tweets = list(map(to_feed_item_fn, db_tweets))
  241. collection_page = h_vm.CollectionPage(
  242. id = sql,
  243. items = tweets,
  244. total_count = len(tweets) # FIXME it's up to the client to do pagination
  245. )
  246. return collection_page
  247. def tweets_search_content (q, pagination_token = None, max_results = 100, to_feed_item_fn=tweet_model_vm):
  248. print('tweets_search_content')
  249. tweet_source = ArchiveTweetSource(ARCHIVE_TWEETS_PATH)
  250. if pagination_token == None:
  251. pagination_token = 0
  252. next_token = pagination_token + max_results
  253. sql = 'select * from tweet where full_text like(?) order by created_at asc limit ?,?'
  254. sql_params = [f'%{q}%', pagination_token, next_token]
  255. db_tweets = tweet_source.search_tweets_sql(sql, sql_params)
  256. tweets = list(map(to_feed_item_fn, db_tweets))
  257. if len(tweets) < (next_token - pagination_token):
  258. next_token = None
  259. collection_page = h_vm.CollectionPage(
  260. id = sql,
  261. items = tweets,
  262. total_count = len(tweets), # FIXME it's up to the client to do pagination
  263. next_token = next_token
  264. )
  265. return collection_page
  266. def get_tweet (tweet_id, me = None):
  267. ids = [tweet_id]
  268. collection_page = get_tweets(ids, me=me)
  269. if collection_page.items:
  270. return collection_page.items[0]
  271. def register_content_sources ():
  272. content_system.register_content_source('twitter:feed:user:', get_user_feed, id_pattern='([\d]+)')
  273. content_system.register_content_source('twitter:tweets', get_tweets, id_pattern='')
  274. content_system.register_content_source('twitter:tweet:', get_tweet, id_pattern='([\d]+)')
  275. content_system.register_content_source('twitter:tweets:search', tweets_search_content, id_pattern='')
  276. content_system.register_content_source('twitter:tweets:search:sql', get_tweets_search_sql, id_pattern='')
  277. @twitter_app.route('/profile/<user_id>.html', methods=['GET'])
  278. def get_profile_html (user_id):
  279. pagination_token = request.args.get('pagination_token')
  280. #exclude_replies = request.args.get('exclude_replies', '1')
  281. # FIXME we want to use a specific source here...
  282. collection_page = content_system.get_content(f'twitter:feed:user:{user_id}', content_source_id = 'twitter_archive_facade.facade:get_user_feed', pagination_token=pagination_token)
  283. tweets = collection_page.items
  284. next_token = collection_page.next_token
  285. query = {}
  286. if next_token:
  287. query = {
  288. **query,
  289. 'next_data_url': url_for('twitter_archive_facade.get_profile_html', user_id=user_id , pagination_token=next_token),
  290. 'next_page_url': url_for('twitter_archive_facade.get_profile_html', user_id=user_id , pagination_token=next_token)
  291. }
  292. profile_user = {
  293. 'id': user_id
  294. }
  295. if 'HX-Request' in request.headers:
  296. user = {
  297. 'id': user_id
  298. }
  299. return render_template('partial/tweets-timeline.html', user = profile_user, tweets = tweets, query = query)
  300. else:
  301. return render_template('user-profile.html', user = profile_user, tweets = tweets, query = query)
  302. @twitter_app.get('/tweet/<tweet_id>.html')
  303. @twitter_app.get('/tweets.html')
  304. def get_tweet_html (tweet_id = None):
  305. output_format = request.args.get('format')
  306. if not tweet_id:
  307. ids = request.args.get('ids').split(',')
  308. collection_page = content_system.get_content('twitter:tweets', ids=tuple(ids), content_source_id='twitter_archive_facade.facade:get_tweets')
  309. tweets = collection_page.items
  310. else:
  311. tweet = content_system.get_content(f'twitter:tweet:{tweet_id}', content_source_id='twitter_archive_facade.facade:get_tweet')
  312. tweets = [tweet]
  313. query = {}
  314. profile_user = {}
  315. if output_format == 'feed.json':
  316. return jsonify(dict(
  317. data = tweets
  318. ))
  319. else:
  320. return render_template('search.html', user = profile_user, tweets = tweets, query = query)
  321. @twitter_app.route('/latest.html', methods=['GET'])
  322. def get_timeline_home_html (variant = "reverse_chronological", pagination_token=None):
  323. return 'ok'
  324. @twitter_app.route('/conversations.html', methods=['GET'])
  325. def get_conversations_html ():
  326. return 'ok'
  327. @twitter_app.route('/bookmarks.html', methods=['GET'])
  328. def get_bookmarks_html (user_id):
  329. return 'ok'
  330. @twitter_app.route('/logout.html', methods=['GET'])
  331. def get_logout_html ():
  332. return 'ok'
  333. @twitter_app.route('/media/upload', methods=['POST'])
  334. def post_media_upload ():
  335. return 'ok'
  336. @twitter_app.route('/tweets/search', methods=['GET'])
  337. @twitter_app.route('/tweets/search.<string:response_format>', methods=['GET'])
  338. def get_tweets_search (response_format='json'):
  339. search = request.args.get('q')
  340. limit = int(request.args.get('limit', 100))
  341. offset = int(request.args.get('offset', 0))
  342. in_reply_to_user_id = int(request.args.get('in_reply_to_user_id', 0))
  343. sql = """
  344. select
  345. id, created_at, retweeted, favorited, retweet_count, favorite_count, full_text, in_reply_to_status_id_str, in_reply_to_user_id, in_reply_to_screen_name
  346. from tweet
  347. """
  348. sql_params = []
  349. if search:
  350. sql += " where full_text like ?"
  351. sql_params.append("%{}%".format(search))
  352. if in_reply_to_user_id:
  353. sql += " where in_reply_to_user_id = ?"
  354. sql_params.append(str(in_reply_to_user_id))
  355. sql += ' order by cast(id as integer)'
  356. if limit:
  357. sql += ' limit ?'
  358. sql_params.append(limit)
  359. if offset:
  360. sql += ' offset ?'
  361. sql_params.append(offset)
  362. #collection_page = content_system.get_content('twitter:tweets:search:sql', sql=sql, sql_params=sql_params)
  363. collection_page = content_system.get_content('twitter:tweets:search', q=search, pagination_token=offset, max_results=limit)
  364. tweets = collection_page.items
  365. result = None
  366. print(f'tweet archive. search results length={len(tweets)}')
  367. if response_format == 'cards.json':
  368. cards = list(map(db_tweet_to_card, tweets))
  369. result = {
  370. "q": search,
  371. "cards": cards
  372. }
  373. elif response_format == 'rows.json':
  374. meta = tweets_form_meta_data
  375. fields = meta['fields']
  376. fields = list(map(lambda f: {**f[1], 'mapping': f[0]}, enumerate(fields)))
  377. meta = {**meta, 'fields': fields, 'id': '0'}
  378. def tweet_to_row (t):
  379. row = list(map(lambda f: t.get(f['name']), fields))
  380. return row
  381. rows = list(map(tweet_to_row, tweets))
  382. result = {
  383. "q": search,
  384. "metaData": meta,
  385. "tweets": rows
  386. }
  387. elif response_format == 'html':
  388. #tweets = list(map(tweet_model_vm, tweets))
  389. query = {
  390. 'next_data_url': url_for('.get_tweets_search', response_format=response_format, limit=limit, offset=collection_page.next_token)
  391. }
  392. profile_user = {}
  393. return render_template('search.html', user = profile_user, tweets = tweets, query = query)
  394. else:
  395. result = {
  396. "q": search,
  397. "tweets": tweets
  398. }
  399. return Response(json.dumps(result), mimetype="application/json")
  400. @twitter_app.route('/tweets/on-this-day.<response_format>', methods=['GET'])
  401. def get_tweets_on_this_day (response_format):
  402. otd_method = request.args.get("otd_method", "traditional")
  403. if otd_method == "calendar":
  404. otd_where_sql = """
  405. -- "on this day" calendar-wise
  406. and week = now_week
  407. and dow = now_dow
  408. """
  409. else:
  410. otd_where_sql = """
  411. -- "on this day" traditional
  412. and `month` = now_month
  413. and dom = now_dom
  414. """
  415. sql = f"""
  416. select
  417. *,
  418. cast(strftime('%Y', created_at) as integer) as `year`,
  419. cast(strftime('%m', created_at) as integer) as `month`,
  420. cast(strftime('%d', created_at) as integer) as dom,
  421. cast(strftime('%W', created_at) as integer) as week,
  422. cast(strftime('%w', created_at) as integer) as dow,
  423. cast(strftime('%j', created_at) as integer) as doy,
  424. datetime(current_timestamp, 'localtime') as now_ts,
  425. cast(strftime('%Y', datetime(current_timestamp, 'localtime')) as integer) as now_year,
  426. cast(strftime('%m', datetime(current_timestamp, 'localtime')) as integer) as now_month,
  427. cast(strftime('%d', datetime(current_timestamp, 'localtime')) as integer) as now_dom,
  428. cast(strftime('%W', datetime(current_timestamp, 'localtime')) as integer) as now_week,
  429. cast(strftime('%w', datetime(current_timestamp, 'localtime')) as integer) as now_dow,
  430. cast(strftime('%j', datetime(current_timestamp, 'localtime')) as integer) as now_doy
  431. from tweet
  432. where
  433. true
  434. {otd_where_sql}
  435. """
  436. sql_params = []
  437. collection_page = content_system.get_content('twitter:tweets:search:sql', sql=sql, sql_params=tuple(sql_params))
  438. tweets = collection_page.items
  439. query = {}
  440. profile_user = {}
  441. if response_format == 'html':
  442. return render_template('search.html', user = profile_user, tweets = tweets, query = query)
  443. elif response_format == 'json':
  444. response = dict(
  445. data = tweets
  446. )
  447. return jsonify(response)
  448. @twitter_app.route('/tweets', methods=['POST'])
  449. def post_tweets ():
  450. tweets_path = ARCHIVE_TWEETS_PATH
  451. tweets_file = open(tweets_path, 'rt', encoding='utf-8')
  452. tweets_data = json_stream.load(tweets_file)
  453. db = sqlite3.connect(TWEET_DB_PATH)
  454. db.execute('create table tweet (id integer, created_at, retweeted, favorited, retweet_count integer, favorite_count integer, full_text, in_reply_to_status_id_str integer, in_reply_to_user_id, in_reply_to_screen_name)')
  455. db.commit()
  456. i = 0
  457. cur = db.cursor()
  458. for tweet in tweets_data.persistent():
  459. t = dict(tweet['tweet'])
  460. dt = dateutil.parser.parse(t['created_at'])
  461. dt_utc = dt.astimezone(dateutil.tz.tz.gettz('UTC'))
  462. created_at = dt_utc.strftime('%Y-%m-%dT%H:%M:%SZ')
  463. sql = 'insert into tweet (id, created_at, retweeted, favorited, retweet_count, favorite_count, full_text, in_reply_to_status_id_str, in_reply_to_user_id, in_reply_to_screen_name) values (?,?,?,?,?,?,?,?,?,?)'
  464. tweet_values = [
  465. t['id'],
  466. created_at,
  467. t['retweeted'],
  468. t['favorited'],
  469. t['retweet_count'],
  470. t['favorite_count'],
  471. t['full_text'],
  472. t.get('in_reply_to_status_id_str'),
  473. t.get('in_reply_to_user_id'),
  474. t.get('in_reply_to_screen_name')
  475. ]
  476. cur.execute(sql, tweet_values)
  477. i += 1
  478. if i % 100 == 0:
  479. cur.connection.commit()
  480. cur = db.cursor()
  481. cur.connection.commit()
  482. cur.close()
  483. db.close()
  484. tweets_file.close()
  485. # ---------------------------------------------------------------------------------------------------------
  486. # ---------------------------------------------------------------------------------------------------------
  487. def tweet_to_card (tweet, includes):
  488. if type(tweet) == t_vm.FeedItem:
  489. tweet = asdict(tweet)
  490. if type(includes) == tv2_types.TweetExpansions:
  491. includes = t_vm.cleandict(asdict(includes))
  492. user = list(filter(lambda u: u.get('id') == tweet['author_id'], includes.get('users')))[0]
  493. tweet_url = 'https://twitter.com/{}/status/{}'.format(user['username'], tweet['id'])
  494. content = tweet['text'] + "\n\n[view tweet]({})".format(tweet_url)
  495. card = {
  496. 'id': 'tweet-' + tweet['id'],
  497. 'content': content,
  498. 'content_type': 'text/markdown',
  499. 'created_at': tweet['created_at'], # can be derived from oldest in edit_history_tweet_ids
  500. 'modified_at': None, # can be derived from newest in edit_history_tweet_ids
  501. 'title': '@' + user['username'] + ' at ' + tweet['created_at'],
  502. 'content_source': tweet_url,
  503. #'tweet': tweet,
  504. #'user': user
  505. }
  506. return card
  507. def response_to_cards (response_json, add_included = True):
  508. """
  509. Seems to be unused
  510. """
  511. tweets = response_json.get('data')
  512. includes = response_json.get('includes')
  513. cards = list(map(lambda t: tweet_to_card(t, includes), tweets))
  514. if add_included:
  515. included_cards = list(map(lambda t: tweet_to_card(t, includes), includes.get('tweets')))
  516. cards += included_cards
  517. return cards