feed.ts 6.7 KB

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