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