feed.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import { Injectable } from "@angular/core";
  2. import { TwitterApiProvider } from "../twitter-api/twitter-api";
  3. import { P2pDatabaseGunProvider } from "../p2p-database-gun/p2p-database-gun";
  4. import { P2pStorageIpfsProvider } from "../p2p-storage-ipfs/p2p-storage-ipfs";
  5. import { CryptoProvider } from "../crypto/crypto";
  6. import { PgpKeyServerProvider } from "../../providers/pgp-key-server/pgp-key-server";
  7. import { Storage } from "@ionic/storage";
  8. @Injectable()
  9. export class FeedProvider {
  10. friends;
  11. userId: string;
  12. constructor(
  13. private twitter: TwitterApiProvider,
  14. private gun: P2pDatabaseGunProvider,
  15. private ipfs: P2pStorageIpfsProvider,
  16. private cryptoUtils: CryptoProvider,
  17. private opnpgp: PgpKeyServerProvider,
  18. private storage: Storage
  19. ) {
  20. this.storage.get("userId").then(userId => (this.userId = userId));
  21. }
  22. /**
  23. * Retrives the public and private tweets for a user
  24. * Since it is loaded in batches of 20 public tweets, public and private tweet are used as reference to load next 20 tweets
  25. * @param userId user id
  26. * @param oldestPublicTweet oldest public tweet
  27. * @param oldestPrivateTweet oldest private tweet
  28. */
  29. public async loadUserTimeline(
  30. userId,
  31. oldestPublicTweet ? ,
  32. oldestPrivateTweet ?
  33. ) {
  34. const maxId = oldestPublicTweet ? oldestPublicTweet["id_str"] : undefined;
  35. // Fetch tweets from Twitter
  36. let tweets = await this.twitter.fetchUserTimeline(userId, maxId);
  37. if (tweets.length === 0) return tweets;
  38. tweets = tweets.filter(tweet => tweet.id_str != maxId);
  39. // Determine start and end of time interval to look for private tweets
  40. const intervalStart: Date = oldestPrivateTweet ?
  41. new Date(oldestPrivateTweet["created_at"]) :
  42. new Date();
  43. const intervalEnd: Date = this.getOldestTweetTimestamp(tweets);
  44. // Fetch private tweet hashs from P2P DB for corresponding interval
  45. const privateTweetHashs: object[] = await this.gun.fetchPrivateTweetHashsForUserInInterval(
  46. userId,
  47. intervalStart,
  48. intervalEnd
  49. );
  50. if (privateTweetHashs.length > 0) {
  51. const privateTweets = await this.fetchPrivateTweets(privateTweetHashs);
  52. // Combine and sort tweets
  53. return tweets
  54. .concat(privateTweets)
  55. .sort((a, b) => this.sortByDateAsc(a, b));
  56. } else {
  57. return tweets;
  58. }
  59. }
  60. /**
  61. * Retrieves the home feed for the logged in user
  62. * Since it is loaded in batches of 20 public tweets, public and private tweet are used as reference to load next 20 tweets
  63. * @param oldestPublicTweet oldest public tweet
  64. * @param oldestPrivateTweet oldest private tweet
  65. */
  66. public async loadHomeTimeline(oldestPublicTweet ? , oldestPrivateTweet ? ) {
  67. // Fetch tweets from Twitter
  68. console.log("loading hometimeline");
  69. const maxId = oldestPublicTweet ? oldestPublicTweet["id_str"] : undefined;
  70. let tweets = await this.twitter.fetchHomeFeed(maxId);
  71. tweets = tweets.filter(tweet => tweet.id_str != maxId);
  72. // Determine start and end of time interval to look for private tweets
  73. const intervalStart: Date = oldestPrivateTweet ?
  74. new Date(oldestPrivateTweet["created_at"]) :
  75. new Date();
  76. const intervalEnd: Date = this.getOldestTweetTimestamp(tweets);
  77. // Fetch user's friends
  78. const friends = await this.getCachedFriends(this.userId);
  79. // Extract friends user ids and add own user id
  80. const friendsAndUserIds = friends
  81. .map(friend => friend.id_str)
  82. .concat([this.userId]);
  83. // Fetch ipfs hashs for period
  84. const promises: Promise < object[] > [] = friendsAndUserIds.map(accountId => {
  85. return this.gun.fetchPrivateTweetHashsForUserInInterval(
  86. accountId,
  87. intervalStart,
  88. intervalEnd
  89. );
  90. });
  91. //save pubkey of all fetchFollowersPublicKeys
  92. await this.fetchFollowersPublicKeys(friendsAndUserIds);
  93. const resolvedPromises = await Promise.all(promises);
  94. const privateTweetHashs = resolvedPromises.reduce(
  95. (privateTweets, el) => privateTweets.concat(el),
  96. []
  97. );
  98. if (privateTweetHashs.length > 0) {
  99. const privateTweets = await this.fetchPrivateTweets(privateTweetHashs);
  100. // Combine and sort tweets
  101. return tweets
  102. .concat(privateTweets)
  103. .sort((a, b) => this.sortByDateAsc(a, b));
  104. } else {
  105. return tweets;
  106. }
  107. }
  108. private async fetchFollowersPublicKeys(followers) {
  109. //Fetch email address of all friends from gunDB
  110. const emailPromises: Promise < string > [] = followers.map(accountId => {
  111. return this.gun.getEmail(
  112. accountId
  113. );
  114. });
  115. const resolvedPromises = await Promise.all(emailPromises);
  116. for (let i = 0; i < resolvedPromises.length; i++) {
  117. if (resolvedPromises[i]) {
  118. let email = resolvedPromises[i]["email"];
  119. if(email){
  120. this.opnpgp.lookupKeys(email);
  121. }
  122. }
  123. }
  124. }
  125. private async fetchPrivateTweets(privateTweetsData: object[]) {
  126. // console.log('error in console fetch private tweets');
  127. const privateTweets = [];
  128. // Load private tweets from P2P storage
  129. // console.log("privateTweetsData.length",privateTweetsData.length);
  130. for (let i = 0; i < privateTweetsData.length; i++) {
  131. let pvtKeyTimestamped;
  132. const hash = privateTweetsData[i]["hash"];
  133. const userId = privateTweetsData[i]["userId"];
  134. const timestamp = privateTweetsData[i]["created_at"];
  135. // fetch from IPFS
  136. const encryptedTweet = await this.ipfs.fetchTweet(hash);
  137. if (!encryptedTweet) {
  138. return;
  139. }
  140. // Fetch private key history for user
  141. try {
  142. const privateKeyHistory: object[] = await this.cryptoUtils.fetchPrivateKeyHistoryForUser(
  143. userId
  144. );
  145. pvtKeyTimestamped = await this.getPrivateKeyAt(timestamp,privateKeyHistory);
  146. // console.log("private key valid for ", timestamp, "is:",pvtKeyTimestamped);
  147. if(pvtKeyTimestamped){
  148. let armrdpvtkey = await this.opnpgp.getArmoredPrivateKey(pvtKeyTimestamped);
  149. // console.log("decrypted private key is:",armrdpvtkey);
  150. const decryptedTweet = await this.opnpgp.decrypt(encryptedTweet, armrdpvtkey);
  151. // console.log("decryptedTweet:",decryptedTweet)
  152. if (decryptedTweet)
  153. privateTweets.push(JSON.parse(decryptedTweet));
  154. }
  155. } catch (err) {
  156. console.log("Error caught in feed",err);
  157. }
  158. // let pvtKey = await this.storage.get("privateKey");
  159. }
  160. if (privateTweets.length > 0) {
  161. // Add retweeted/quoted status
  162. privateTweets.map(async tweet => await this.addQuotedStatusToTweet(tweet));
  163. // Add original status (reply to)
  164. privateTweets.map(
  165. async tweet => await this.addOriginalStatusToTweet(tweet)
  166. );
  167. // Add user object to private tweets
  168. return await Promise.all(
  169. privateTweets.map(async tweet => await this.addUserToTweet(tweet))
  170. );
  171. }
  172. return privateTweets;
  173. }
  174. private async addUserToTweet(tweet: object): Promise < object > {
  175. tweet["user"] = await this.twitter.fetchUser(tweet["user_id"]);
  176. return tweet;
  177. }
  178. private async addQuotedStatusToTweet(tweet: object): Promise < object > {
  179. if (!tweet["quoted_status_id"]) return tweet;
  180. const quoted_status = await this.twitter.fetchTweet(
  181. tweet["quoted_status_id"]
  182. );
  183. tweet["quoted_status"] = quoted_status["data"];
  184. return tweet;
  185. }
  186. private async addOriginalStatusToTweet(tweet: object): Promise < object > {
  187. if (!tweet["in_reply_to_status_id"]) return tweet;
  188. const originalTweet = await this.twitter.fetchTweet(
  189. tweet["in_reply_to_status_id"]
  190. );
  191. tweet["in_reply_to_screen_name"] =
  192. originalTweet["data"]["user"]["screen_name"];
  193. return tweet;
  194. }
  195. private getOldestTweetTimestamp(tweets): Date {
  196. if (tweets.length < 15) {
  197. // End of timeline is reached - load all private tweets
  198. return new Date("2018-04-01T00:00:00");
  199. } else {
  200. const lastTweetTimestamp = tweets[tweets.length - 1].created_at;
  201. return new Date(lastTweetTimestamp);
  202. }
  203. }
  204. private sortByDateAsc(a, b) {
  205. const dateA = new Date(a.created_at);
  206. const dateB = new Date(b.created_at);
  207. if (dateA > dateB) {
  208. return -1;
  209. } else if (dateA < dateB) {
  210. return 1;
  211. } else {
  212. return 0;
  213. }
  214. }
  215. private async getCachedFriends(userId) {
  216. // Cache friends for 15 minutes to avoid unnecessary API calls
  217. if (!this.friends || (Date.now() - this.friends.lastUpdate) / 900000 > 15) {
  218. this.friends = {
  219. friendList: await this.twitter.fetchFriends(userId),
  220. lastUpdate: Date.now()
  221. };
  222. }
  223. return this.friends.friendList;
  224. }
  225. private async getPrivateKeyAt(
  226. timestamp: string,
  227. privateKeyHistory: object[]
  228. ) {
  229. if(!privateKeyHistory) return await this.storage.get("privateKey") ;
  230. const timestampTweet = new Date(timestamp).getTime();
  231. for (let key of privateKeyHistory) {
  232. const timestampKey = new Date(key["validFrom"]).getTime();
  233. if (timestampTweet > timestampKey) {
  234. return key["key"];
  235. }
  236. }
  237. // todo: throw error
  238. return null;
  239. }
  240. }