feed.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  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. @Injectable()
  7. export class FeedProvider {
  8. friends;
  9. constructor(
  10. private twitter: TwitterApiProvider,
  11. private gun: P2pDatabaseGunProvider,
  12. private ipfs: P2pStorageIpfsProvider,
  13. private cryptoUtils: CryptoProvider
  14. ) {}
  15. public async loadUserTimeline(
  16. userId,
  17. oldestPublicTweet?,
  18. oldestPrivateTweet?
  19. ) {
  20. const maxId = oldestPublicTweet ? oldestPublicTweet["id_str"] : undefined;
  21. // Fetch tweets from Twitter
  22. let tweets = await this.twitter.fetchUserTimeline(userId, maxId);
  23. tweets = tweets.filter(tweet => tweet.id_str != maxId);
  24. // Determine start and end of time interval to look for private tweets
  25. const intervalStart: Date = oldestPrivateTweet
  26. ? new Date(oldestPrivateTweet["created_at"])
  27. : new Date();
  28. const intervalEnd: Date = this.getOldestTweetTimestamp(tweets);
  29. // Fetch private tweet hashs from P2P DB for corresponding interval
  30. const privateTweetHashs: object[] = await this.gun.fetchPrivateTweetHashsForUserInInterval(
  31. userId,
  32. intervalStart,
  33. intervalEnd
  34. );
  35. if (privateTweetHashs.length) {
  36. const privateTweets = await this.fetchPrivateTweets(
  37. privateTweetHashs,
  38. userId
  39. );
  40. // Combine and sort tweets
  41. return tweets
  42. .concat(privateTweets)
  43. .sort((a, b) => this.sortByDateAsc(a, b));
  44. } else {
  45. return tweets;
  46. }
  47. }
  48. public async loadHomeTimeline(
  49. userId,
  50. oldestPublicTweet?,
  51. oldestPrivateTweet?
  52. ) {
  53. // Fetch tweets from Twitter
  54. const maxId = oldestPublicTweet ? oldestPublicTweet["id_str"] : undefined;
  55. let tweets = await this.twitter.fetchHomeFeed(maxId);
  56. tweets = tweets.filter(tweet => tweet.id_str != maxId);
  57. // Determine start and end of time interval to look for private tweets
  58. const intervalStart: Date = oldestPrivateTweet
  59. ? new Date(oldestPrivateTweet["created_at"])
  60. : new Date();
  61. const intervalEnd: Date = this.getOldestTweetTimestamp(tweets);
  62. // Fetch user's friends
  63. const friends = await this.getCachedFriends(userId);
  64. let privateTweetHashs = [];
  65. friends.forEach(async friend => {
  66. privateTweetHashs = privateTweetHashs.concat(
  67. await this.gun.fetchPrivateTweetHashsForUserInInterval(
  68. friend.id_str,
  69. intervalStart,
  70. intervalEnd
  71. )
  72. );
  73. });
  74. // Add users private tweets
  75. privateTweetHashs = privateTweetHashs.concat(
  76. await this.gun.fetchPrivateTweetHashsForUserInInterval(
  77. userId,
  78. intervalStart,
  79. intervalEnd
  80. )
  81. );
  82. if (privateTweetHashs.length) {
  83. const privateTweets = await this.fetchPrivateTweets(
  84. privateTweetHashs,
  85. userId
  86. );
  87. // Combine and sort tweets
  88. return tweets
  89. .concat(privateTweets)
  90. .sort((a, b) => this.sortByDateAsc(a, b));
  91. } else {
  92. return tweets;
  93. }
  94. }
  95. private async fetchPrivateTweets(
  96. privateTweetsData: object[],
  97. userId: string
  98. ) {
  99. const privateTweets = [];
  100. // Load private tweets from P2P storage
  101. for (let i = 0; i < privateTweetsData.length; i++) {
  102. const hash = privateTweetsData[i]["hash"];
  103. privateTweetsData[i]["encryptedTweet"] = await this.ipfs.fetchTweet(hash);
  104. }
  105. // Fetch public key history for user
  106. const publicKeyHistory: object[] = await this.cryptoUtils.fetchPublicKeyHistoryForUser(
  107. userId
  108. );
  109. // Decrypt tweets
  110. for (let i = 0; i < privateTweetsData.length; i++) {
  111. const encryptedTweet = privateTweetsData[i]["encryptedTweet"];
  112. const timestamp = privateTweetsData[i]["created_at"];
  113. const decryptedTweet = this.cryptoUtils.decrypt(
  114. encryptedTweet,
  115. this.getPublicKeyAt(timestamp, publicKeyHistory)
  116. );
  117. privateTweets.push(JSON.parse(decryptedTweet));
  118. }
  119. // Add retweeted/quoted status
  120. privateTweets.map(async tweet => await this.addQuotedStatusToTweet(tweet));
  121. // Add original status (reply to)
  122. privateTweets.map(
  123. async tweet => await this.addOriginalStatusToTweet(tweet)
  124. );
  125. // Add user object to private tweets
  126. return await Promise.all(
  127. privateTweets.map(async tweet => await this.addUserToTweet(tweet))
  128. );
  129. }
  130. private getPublicKeyAt(
  131. timestamp: string,
  132. publicKeyHistory: object[]
  133. ): string {
  134. const timestampTweet = new Date(timestamp).getTime();
  135. for (let key of publicKeyHistory) {
  136. const timestampKey = new Date(key["validFrom"]).getTime();
  137. if (timestampTweet > timestampKey) {
  138. return key["key"];
  139. }
  140. }
  141. // todo: throw error
  142. return "";
  143. }
  144. private async addUserToTweet(tweet: object): Promise<object> {
  145. tweet["user"] = await this.twitter.fetchUser(tweet["user_id"]);
  146. return tweet;
  147. }
  148. private async addQuotedStatusToTweet(tweet: object): Promise<object> {
  149. if (!tweet["quoted_status_id"]) return tweet;
  150. const quoted_status = await this.twitter.fetchTweet(
  151. tweet["quoted_status_id"]
  152. );
  153. tweet["quoted_status"] = quoted_status["data"];
  154. return tweet;
  155. }
  156. private async addOriginalStatusToTweet(tweet: object): Promise<object> {
  157. if (!tweet["in_reply_to_status_id"]) return tweet;
  158. const originalTweet = await this.twitter.fetchTweet(
  159. tweet["in_reply_to_status_id"]
  160. );
  161. tweet["in_reply_to_screen_name"] =
  162. originalTweet["data"]["user"]["screen_name"];
  163. return tweet;
  164. }
  165. private getOldestTweetTimestamp(tweets): Date {
  166. if (tweets.length < 15) {
  167. // End of timeline is reached - load all private tweets
  168. return new Date("2018-04-01T00:00:00");
  169. } else {
  170. const lastTweetTimestamp = tweets[tweets.length - 1].created_at;
  171. return new Date(lastTweetTimestamp);
  172. }
  173. }
  174. private sortByDateAsc(a, b) {
  175. const dateA = new Date(a.created_at);
  176. const dateB = new Date(b.created_at);
  177. if (dateA > dateB) {
  178. return -1;
  179. } else if (dateA < dateB) {
  180. return 1;
  181. } else {
  182. return 0;
  183. }
  184. }
  185. private async getCachedFriends(userId) {
  186. // Cache friends for 15 minutes to avoid unnecessary API calls
  187. if (!this.friends || (Date.now() - this.friends.lastUpdate) / 900000 > 15) {
  188. this.friends = {
  189. friendList: await this.twitter.fetchFriends(userId),
  190. lastUpdate: Date.now()
  191. };
  192. }
  193. return this.friends.friendList;
  194. }
  195. }