@metamask/social-controllers 3.5.0 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,10 +33,13 @@ const PositionStruct = structType({
33
33
  chain: string(),
34
34
  positionAmount: number(),
35
35
  isOpen: optional(boolean()),
36
- boughtUsd: number(),
37
- soldUsd: number(),
38
- realizedPnl: number(),
39
- costBasis: number(),
36
+ // Incomplete Clicker metrics serialize these as `null` (`undefined + n`
37
+ // becomes `NaN`, then JSON `null`). Rejecting the field used to fail the
38
+ // entire feed page, so they are nullable here.
39
+ boughtUsd: nullable(number()),
40
+ soldUsd: nullable(number()),
41
+ realizedPnl: nullable(number()),
42
+ costBasis: nullable(number()),
40
43
  trades: array(TradeStruct),
41
44
  lastTradeAt: number(),
42
45
  tokenImageUrl: optional(nullable(string())),
@@ -182,6 +185,34 @@ const FeedResponseStruct = structType({
182
185
  items: array(FeedItemStruct),
183
186
  pagination: FeedPaginationStruct,
184
187
  });
188
+ /**
189
+ * One malformed feed item must not blank the page. When the envelope is
190
+ * well-shaped, keep items that match {@link FeedItemStruct} and drop the rest.
191
+ * Still throw when `items` or `pagination` themselves are unusable.
192
+ *
193
+ * @param feedData - Parsed JSON body from a feed endpoint.
194
+ * @param invalidMessage - Error message to throw when the envelope is unusable.
195
+ * @returns The validated feed response, possibly with invalid items dropped.
196
+ */
197
+ const parseFeedResponse = (feedData, invalidMessage) => {
198
+ if (is(feedData, FeedResponseStruct)) {
199
+ return feedData;
200
+ }
201
+ if (typeof feedData !== 'object' ||
202
+ feedData === null ||
203
+ !('items' in feedData) ||
204
+ !('pagination' in feedData)) {
205
+ throw new Error(invalidMessage);
206
+ }
207
+ const { items, pagination } = feedData;
208
+ if (!Array.isArray(items) || !is(pagination, FeedPaginationStruct)) {
209
+ throw new Error(invalidMessage);
210
+ }
211
+ return {
212
+ items: items.filter((item) => is(item, FeedItemStruct)),
213
+ pagination,
214
+ };
215
+ };
185
216
  const FollowersResponseStruct = structType({
186
217
  followers: array(ProfileSummaryStruct),
187
218
  count: number(),
@@ -196,6 +227,28 @@ const FollowResponseStruct = structType({
196
227
  const UnfollowResponseStruct = structType({
197
228
  unfollowed: array(ProfileSummaryStruct),
198
229
  });
230
+ const BlockedProfileStruct = structType({
231
+ blockedAt: number(),
232
+ reason: nullable(string()),
233
+ profile: ProfileSummaryStruct,
234
+ });
235
+ const BlockedProfilesResponseStruct = structType({
236
+ items: array(BlockedProfileStruct),
237
+ cursor: nullable(string()),
238
+ });
239
+ const BlockedContentStruct = structType({
240
+ blockedAt: number(),
241
+ reason: nullable(string()),
242
+ type: enums(['comment', 'reply']),
243
+ id: string(),
244
+ text: string(),
245
+ createdAt: number(),
246
+ author: nullable(ProfileSummaryStruct),
247
+ });
248
+ const BlockedContentResponseStruct = structType({
249
+ items: array(BlockedContentStruct),
250
+ cursor: nullable(string()),
251
+ });
199
252
  // ---------------------------------------------------------------------------
200
253
  // Messenger types
201
254
  // ---------------------------------------------------------------------------
@@ -205,7 +258,10 @@ const MESSENGER_EXPOSED_METHODS = [
205
258
  'fetchOpenPositions',
206
259
  'fetchClosedPositions',
207
260
  'fetchFollowers',
261
+ 'fetchTraderFollowing',
208
262
  'fetchFollowing',
263
+ 'fetchMyFollowing',
264
+ 'fetchMyFollowers',
209
265
  'fetchPositionById',
210
266
  'fetchFeed',
211
267
  'fetchTraderFeed',
@@ -218,6 +274,9 @@ const MESSENGER_EXPOSED_METHODS = [
218
274
  'optOutOfLeaderboard',
219
275
  'optInToLeaderboard',
220
276
  'refreshNotificationPreferencesCache',
277
+ 'block',
278
+ 'fetchBlockedProfiles',
279
+ 'fetchBlockedContent',
221
280
  ];
222
281
  // ---------------------------------------------------------------------------
223
282
  // Service
@@ -289,7 +348,7 @@ export class SocialService extends BaseDataService {
289
348
  headers: authHeaders,
290
349
  });
291
350
  _a.#throwIfNotOk(response, SocialServiceErrorMessage.FETCH_LEADERBOARD_FAILED);
292
- const leaderboardData = await response.json();
351
+ const leaderboardData = (await response.json());
293
352
  if (!is(leaderboardData, LeaderboardResponseStruct)) {
294
353
  throw new Error(SocialServiceErrorMessage.FETCH_LEADERBOARD_INVALID_RESPONSE);
295
354
  }
@@ -316,7 +375,7 @@ export class SocialService extends BaseDataService {
316
375
  const authHeaders = await this.#getAuthHeaders();
317
376
  const response = await fetch(url, { headers: authHeaders });
318
377
  _a.#throwIfNotOk(response, SocialServiceErrorMessage.FETCH_TRADER_PROFILE_FAILED);
319
- const traderProfileData = await response.json();
378
+ const traderProfileData = (await response.json());
320
379
  if (!is(traderProfileData, TraderProfileResponseStruct)) {
321
380
  throw new Error(SocialServiceErrorMessage.FETCH_TRADER_PROFILE_INVALID_RESPONSE);
322
381
  }
@@ -360,22 +419,31 @@ export class SocialService extends BaseDataService {
360
419
  /**
361
420
  * Fetches a trader's MetaMask followers.
362
421
  *
363
- * Calls `GET ${baseUrl}/traders/${addressOrId}/followers`.
422
+ * Calls `GET ${baseUrl}/traders/${addressOrId}/followers`. Pass
423
+ * `mutuals: true` to append `?mutuals=true` (profiles that follow both this
424
+ * trader and the authenticated user; `count` is the mutual total). Requires a
425
+ * bearer token; the trader is identified by `addressOrId`, not the JWT
426
+ * subject (see {@link fetchMyFollowers} for the current user's inbound list).
364
427
  *
365
428
  * @param options - Options bag.
366
429
  * @param options.addressOrId - Wallet address or Clicker profile ID.
430
+ * @param options.mutuals - When true, request mutual followers only.
367
431
  * @returns The followers response.
368
432
  */
369
433
  async fetchFollowers(options) {
370
- const { addressOrId } = options;
434
+ const { addressOrId, mutuals } = options;
435
+ const mutualsOnly = mutuals === true;
371
436
  const followersResponse = await this.fetchQuery({
372
- queryKey: [`${this.name}:fetchFollowers`, addressOrId],
437
+ queryKey: [`${this.name}:fetchFollowers`, addressOrId, mutualsOnly],
373
438
  queryFn: async () => {
374
- const url = `${this.#v1Url}/traders/${encodeURIComponent(addressOrId)}/followers`;
439
+ const url = new URL(`${this.#v1Url}/traders/${encodeURIComponent(addressOrId)}/followers`);
440
+ if (mutualsOnly) {
441
+ url.searchParams.append('mutuals', 'true');
442
+ }
375
443
  const authHeaders = await this.#getAuthHeaders();
376
- const response = await fetch(url, { headers: authHeaders });
444
+ const response = await fetch(url.toString(), { headers: authHeaders });
377
445
  _a.#throwIfNotOk(response, SocialServiceErrorMessage.FETCH_FOLLOWERS_FAILED);
378
- const followersData = await response.json();
446
+ const followersData = (await response.json());
379
447
  if (!is(followersData, FollowersResponseStruct)) {
380
448
  throw new Error(SocialServiceErrorMessage.FETCH_FOLLOWERS_INVALID_RESPONSE);
381
449
  }
@@ -384,6 +452,35 @@ export class SocialService extends BaseDataService {
384
452
  });
385
453
  return followersResponse;
386
454
  }
455
+ /**
456
+ * Fetches the list of traders a profile follows.
457
+ *
458
+ * Calls `GET ${baseUrl}/traders/${addressOrId}/following`. Requires a bearer
459
+ * token; the trader is identified by `addressOrId`, not the JWT subject (see
460
+ * {@link fetchFollowing} for the current user's outbound list).
461
+ *
462
+ * @param options - Options bag.
463
+ * @param options.addressOrId - Wallet address or Clicker profile ID.
464
+ * @returns The following response.
465
+ */
466
+ async fetchTraderFollowing(options) {
467
+ const { addressOrId } = options;
468
+ const followingResponse = await this.fetchQuery({
469
+ queryKey: [`${this.name}:fetchTraderFollowing`, addressOrId],
470
+ queryFn: async () => {
471
+ const url = `${this.#v1Url}/traders/${encodeURIComponent(addressOrId)}/following`;
472
+ const authHeaders = await this.#getAuthHeaders();
473
+ const response = await fetch(url, { headers: authHeaders });
474
+ _a.#throwIfNotOk(response, SocialServiceErrorMessage.FETCH_TRADER_FOLLOWING_FAILED);
475
+ const followingData = (await response.json());
476
+ if (!is(followingData, FollowingResponseStruct)) {
477
+ throw new Error(SocialServiceErrorMessage.FETCH_TRADER_FOLLOWING_INVALID_RESPONSE);
478
+ }
479
+ return followingData;
480
+ },
481
+ });
482
+ return followingResponse;
483
+ }
387
484
  /**
388
485
  * Fetches a single position by its unique ID.
389
486
  *
@@ -402,7 +499,7 @@ export class SocialService extends BaseDataService {
402
499
  const authHeaders = await this.#getAuthHeaders();
403
500
  const response = await fetch(url, { headers: authHeaders });
404
501
  _a.#throwIfNotOk(response, SocialServiceErrorMessage.FETCH_POSITION_BY_ID_FAILED);
405
- const positionData = await response.json();
502
+ const positionData = (await response.json());
406
503
  if (!is(positionData, PositionStruct)) {
407
504
  throw new Error(SocialServiceErrorMessage.FETCH_POSITION_BY_ID_INVALID_RESPONSE);
408
505
  }
@@ -459,11 +556,8 @@ export class SocialService extends BaseDataService {
459
556
  headers: authHeaders,
460
557
  });
461
558
  _a.#throwIfNotOk(response, SocialServiceErrorMessage.FETCH_FEED_FAILED);
462
- const feedData = await response.json();
463
- if (!is(feedData, FeedResponseStruct)) {
464
- throw new Error(SocialServiceErrorMessage.FETCH_FEED_INVALID_RESPONSE);
465
- }
466
- return feedData;
559
+ const feedData = (await response.json());
560
+ return parseFeedResponse(feedData, SocialServiceErrorMessage.FETCH_FEED_INVALID_RESPONSE);
467
561
  },
468
562
  });
469
563
  return feedResponse;
@@ -516,11 +610,8 @@ export class SocialService extends BaseDataService {
516
610
  headers: authHeaders,
517
611
  });
518
612
  _a.#throwIfNotOk(response, SocialServiceErrorMessage.FETCH_TRADER_FEED_FAILED);
519
- const feedData = await response.json();
520
- if (!is(feedData, FeedResponseStruct)) {
521
- throw new Error(SocialServiceErrorMessage.FETCH_TRADER_FEED_INVALID_RESPONSE);
522
- }
523
- return feedData;
613
+ const feedData = (await response.json());
614
+ return parseFeedResponse(feedData, SocialServiceErrorMessage.FETCH_TRADER_FEED_INVALID_RESPONSE);
524
615
  },
525
616
  });
526
617
  return traderFeedResponse;
@@ -575,10 +666,7 @@ export class SocialService extends BaseDataService {
575
666
  });
576
667
  _a.#throwIfNotOk(response, SocialServiceErrorMessage.FETCH_TOKEN_FEED_FAILED);
577
668
  const feedData = (await response.json());
578
- if (!is(feedData, FeedResponseStruct)) {
579
- throw new Error(SocialServiceErrorMessage.FETCH_TOKEN_FEED_INVALID_RESPONSE);
580
- }
581
- return feedData;
669
+ return parseFeedResponse(feedData, SocialServiceErrorMessage.FETCH_TOKEN_FEED_INVALID_RESPONSE);
582
670
  },
583
671
  });
584
672
  return tokenFeedResponse;
@@ -608,7 +696,7 @@ export class SocialService extends BaseDataService {
608
696
  body: JSON.stringify({ emotion }),
609
697
  });
610
698
  _a.#throwIfNotOk(response, SocialServiceErrorMessage.REACT_TO_COMMENT_FAILED);
611
- const metrics = await response.json();
699
+ const metrics = (await response.json());
612
700
  if (!is(metrics, CommentEngagementStruct)) {
613
701
  throw new Error(SocialServiceErrorMessage.REACT_TO_COMMENT_INVALID_RESPONSE);
614
702
  }
@@ -639,7 +727,7 @@ export class SocialService extends BaseDataService {
639
727
  headers: authHeaders,
640
728
  });
641
729
  _a.#throwIfNotOk(response, SocialServiceErrorMessage.REMOVE_COMMENT_REACTION_FAILED);
642
- const metrics = await response.json();
730
+ const metrics = (await response.json());
643
731
  if (!is(metrics, CommentEngagementStruct)) {
644
732
  throw new Error(SocialServiceErrorMessage.REMOVE_COMMENT_REACTION_INVALID_RESPONSE);
645
733
  }
@@ -704,6 +792,10 @@ export class SocialService extends BaseDataService {
704
792
  * Calls `GET ${baseUrl}/users/me/following`. The caller is identified
705
793
  * server-side from the JWT sub claim carried in the Authorization header.
706
794
  *
795
+ * Prefer {@link fetchMyFollowing} in new code (pairs with
796
+ * {@link fetchMyFollowers}). This method is unchanged and remains the
797
+ * `/users/me/following` read.
798
+ *
707
799
  * @returns The following response.
708
800
  */
709
801
  async fetchFollowing() {
@@ -715,7 +807,7 @@ export class SocialService extends BaseDataService {
715
807
  const authHeaders = await this.#getAuthHeaders();
716
808
  const response = await fetch(url, { headers: authHeaders });
717
809
  _a.#throwIfNotOk(response, SocialServiceErrorMessage.FETCH_FOLLOWING_FAILED);
718
- const followingData = await response.json();
810
+ const followingData = (await response.json());
719
811
  if (!is(followingData, FollowingResponseStruct)) {
720
812
  throw new Error(SocialServiceErrorMessage.FETCH_FOLLOWING_INVALID_RESPONSE);
721
813
  }
@@ -724,6 +816,43 @@ export class SocialService extends BaseDataService {
724
816
  });
725
817
  return followingResponse;
726
818
  }
819
+ /**
820
+ * Fetches the list of traders the current user is following.
821
+ *
822
+ * Alias of {@link fetchFollowing}. Calls `GET ${baseUrl}/users/me/following`.
823
+ * Preferred name in new code so `/users/me` reads are both `fetchMy*`.
824
+ *
825
+ * @returns The following response.
826
+ */
827
+ async fetchMyFollowing() {
828
+ return this.fetchFollowing();
829
+ }
830
+ /**
831
+ * Fetches the list of traders following the current user.
832
+ *
833
+ * Calls `GET ${baseUrl}/users/me/followers`. The caller is identified
834
+ * server-side from the JWT sub claim carried in the Authorization header.
835
+ *
836
+ * @returns The followers response.
837
+ */
838
+ async fetchMyFollowers() {
839
+ const followersResponse = await this.fetchQuery({
840
+ queryKey: [`${this.name}:fetchMyFollowers`],
841
+ staleTime: 0,
842
+ queryFn: async () => {
843
+ const url = `${this.#v1Url}/users/me/followers`;
844
+ const authHeaders = await this.#getAuthHeaders();
845
+ const response = await fetch(url, { headers: authHeaders });
846
+ _a.#throwIfNotOk(response, SocialServiceErrorMessage.FETCH_MY_FOLLOWERS_FAILED);
847
+ const followersData = (await response.json());
848
+ if (!is(followersData, FollowersResponseStruct)) {
849
+ throw new Error(SocialServiceErrorMessage.FETCH_MY_FOLLOWERS_INVALID_RESPONSE);
850
+ }
851
+ return followersData;
852
+ },
853
+ });
854
+ return followersResponse;
855
+ }
727
856
  /**
728
857
  * Follows one or more traders on behalf of the current user.
729
858
  *
@@ -748,7 +877,7 @@ export class SocialService extends BaseDataService {
748
877
  body: JSON.stringify({ targets }),
749
878
  });
750
879
  _a.#throwIfNotOk(response, SocialServiceErrorMessage.FOLLOW_FAILED);
751
- const followData = await response.json();
880
+ const followData = (await response.json());
752
881
  if (!is(followData, FollowResponseStruct)) {
753
882
  throw new Error(SocialServiceErrorMessage.FOLLOW_INVALID_RESPONSE);
754
883
  }
@@ -785,7 +914,7 @@ export class SocialService extends BaseDataService {
785
914
  headers: authHeaders,
786
915
  });
787
916
  _a.#throwIfNotOk(response, SocialServiceErrorMessage.UNFOLLOW_FAILED);
788
- const unfollowData = await response.json();
917
+ const unfollowData = (await response.json());
789
918
  if (!is(unfollowData, UnfollowResponseStruct)) {
790
919
  throw new Error(SocialServiceErrorMessage.UNFOLLOW_INVALID_RESPONSE);
791
920
  }
@@ -864,6 +993,105 @@ export class SocialService extends BaseDataService {
864
993
  },
865
994
  });
866
995
  }
996
+ /**
997
+ * Blocks a trader, swap comment, or reply for the current user.
998
+ *
999
+ * Calls `PUT ${baseUrl}/moderation/block`. Provide exactly one of
1000
+ * `profileId`, `commentId`, or `replyId`. Blocking the same target again
1001
+ * replaces `reason` when one is given. The caller is identified server-side
1002
+ * from the JWT sub claim carried in the Authorization header.
1003
+ *
1004
+ * @param options - Options bag. Exactly one target, plus an optional reason.
1005
+ * @param options.profileId - Profile id (UUID) of the trader to block.
1006
+ * @param options.commentId - Swap comment to block.
1007
+ * @param options.replyId - Comment reply to block.
1008
+ * @param options.reason - Free-form reason stored on the block.
1009
+ */
1010
+ async block(options) {
1011
+ await this.fetchQuery({
1012
+ queryKey: [`${this.name}:block`, options],
1013
+ staleTime: 0,
1014
+ queryFn: async () => {
1015
+ const url = `${this.#v1Url}/moderation/block`;
1016
+ const authHeaders = await this.#getAuthHeaders();
1017
+ const response = await fetch(url, {
1018
+ method: 'PUT',
1019
+ headers: { ...authHeaders, 'Content-Type': 'application/json' },
1020
+ body: JSON.stringify(options),
1021
+ });
1022
+ _a.#throwIfNotOk(response, SocialServiceErrorMessage.BLOCK_FAILED);
1023
+ return null;
1024
+ },
1025
+ });
1026
+ }
1027
+ /**
1028
+ * Fetches traders the current user has blocked.
1029
+ *
1030
+ * Calls `GET ${baseUrl}/moderation/blocks/profiles`. Results are most
1031
+ * recently blocked first, 250 per page. The caller is identified
1032
+ * server-side from the JWT sub claim carried in the Authorization header.
1033
+ *
1034
+ * @param options - Options bag.
1035
+ * @param options.cursor - Cursor from the previous page. Omit for the first page.
1036
+ * @returns The blocked profiles page.
1037
+ */
1038
+ async fetchBlockedProfiles(options) {
1039
+ const cursor = options?.cursor;
1040
+ return await this.fetchQuery({
1041
+ queryKey: [`${this.name}:fetchBlockedProfiles`, cursor ?? null],
1042
+ staleTime: 0,
1043
+ queryFn: async () => {
1044
+ const url = new URL(`${this.#v1Url}/moderation/blocks/profiles`);
1045
+ if (cursor) {
1046
+ url.searchParams.append('cursor', cursor);
1047
+ }
1048
+ const authHeaders = await this.#getAuthHeaders();
1049
+ const response = await fetch(url.toString(), {
1050
+ headers: authHeaders,
1051
+ });
1052
+ _a.#throwIfNotOk(response, SocialServiceErrorMessage.FETCH_BLOCKED_PROFILES_FAILED);
1053
+ const blockedProfiles = (await response.json());
1054
+ if (!is(blockedProfiles, BlockedProfilesResponseStruct)) {
1055
+ throw new Error(SocialServiceErrorMessage.FETCH_BLOCKED_PROFILES_INVALID_RESPONSE);
1056
+ }
1057
+ return blockedProfiles;
1058
+ },
1059
+ });
1060
+ }
1061
+ /**
1062
+ * Fetches comments and replies the current user has blocked.
1063
+ *
1064
+ * Calls `GET ${baseUrl}/moderation/blocks/content`. Results are most
1065
+ * recently blocked first, 250 per page. The caller is identified
1066
+ * server-side from the JWT sub claim carried in the Authorization header.
1067
+ *
1068
+ * @param options - Options bag.
1069
+ * @param options.cursor - Cursor from the previous page. Omit for the first page.
1070
+ * @returns The blocked content page.
1071
+ */
1072
+ async fetchBlockedContent(options) {
1073
+ const cursor = options?.cursor;
1074
+ return await this.fetchQuery({
1075
+ queryKey: [`${this.name}:fetchBlockedContent`, cursor ?? null],
1076
+ staleTime: 0,
1077
+ queryFn: async () => {
1078
+ const url = new URL(`${this.#v1Url}/moderation/blocks/content`);
1079
+ if (cursor) {
1080
+ url.searchParams.append('cursor', cursor);
1081
+ }
1082
+ const authHeaders = await this.#getAuthHeaders();
1083
+ const response = await fetch(url.toString(), {
1084
+ headers: authHeaders,
1085
+ });
1086
+ _a.#throwIfNotOk(response, SocialServiceErrorMessage.FETCH_BLOCKED_CONTENT_FAILED);
1087
+ const blockedContent = (await response.json());
1088
+ if (!is(blockedContent, BlockedContentResponseStruct)) {
1089
+ throw new Error(SocialServiceErrorMessage.FETCH_BLOCKED_CONTENT_INVALID_RESPONSE);
1090
+ }
1091
+ return blockedContent;
1092
+ },
1093
+ });
1094
+ }
867
1095
  /**
868
1096
  * Shared helper for fetching open/closed positions.
869
1097
  *
@@ -911,7 +1139,7 @@ export class SocialService extends BaseDataService {
911
1139
  headers: authHeaders,
912
1140
  });
913
1141
  _a.#throwIfNotOk(response, failedMessage);
914
- const positionsData = await response.json();
1142
+ const positionsData = (await response.json());
915
1143
  if (!is(positionsData, PositionsResponseStruct)) {
916
1144
  throw new Error(invalidMessage);
917
1145
  }