From 5a534abeac26530bcbbe1522454ab090b75dc5f7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 13 Mar 2025 18:13:45 +0100 Subject: [PATCH 1/3] chore: add data privacy endpoints --- src/client.ts | 12 ++++- src/data_privacy.ts | 55 ++++++++++++++++++++ test/integration/node/client_test.js | 75 ++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 src/data_privacy.ts diff --git a/src/client.ts b/src/client.ts index 39d8d944..1482835d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -16,12 +16,15 @@ import { StreamUser } from './user'; import { JWTScopeToken, JWTUserSessionToken } from './signing'; import { FeedError, StreamApiError, SiteError } from './errors'; import utils from './utils'; +import DataPrivacy, { ExportIDsResponse, ActivityToDelete } from './data_privacy'; + import BatchOperations, { AddUsersResponse, FollowRelation, GetUsersResponse, UnfollowRelation, } from './batch_operations'; + import createRedirectUrl from './redirect_url'; import { StreamFeed, @@ -196,6 +199,10 @@ export class StreamClient Promise; // eslint-disable-line no-use-before-define getUsers?: (this: StreamClient, ids: string[]) => Promise; // eslint-disable-line no-use-before-define deleteUsers?: (this: StreamClient, ids: string[]) => Promise; // eslint-disable-line no-use-before-define + deleteActivities?: (this: StreamClient, activities: ActivityToDelete[]) => Promise; // eslint-disable-line no-use-before-define + deleteReactions?: (this: StreamClient, ids: string[]) => Promise; // eslint-disable-line no-use-before-define + exportUserActivitiesAndReactionIDs?: (this: StreamClient, userId: string) => Promise; // eslint-disable-line no-use-before-define + /** * Initialize a client * @link https://getstream.io/activity-feeds/docs/node/#setup @@ -288,7 +295,7 @@ export class StreamClient(this, this.getOrCreateToken()); // If we are in a node environment and batchOperations/createRedirectUrl is available add the methods to the prototype of StreamClient - if (BatchOperations && !!createRedirectUrl) { + if (BatchOperations && !!createRedirectUrl && DataPrivacy) { this.addToMany = BatchOperations.addToMany; this.followMany = BatchOperations.followMany; this.unfollowMany = BatchOperations.unfollowMany; @@ -296,6 +303,9 @@ export class StreamClient { + this._throwMissingApiSecret(); + + return this.post({ + url: 'data_privacy/delete_activities/', + body: { activities }, + token: this.getOrCreateToken(), + }); +} + +export function deleteReactions(this: StreamClient, ids: string[]): Promise { + this._throwMissingApiSecret(); + + return this.post({ + url: 'data_privacy/delete_reactions/', + body: { ids }, + token: this.getOrCreateToken(), + }); +} + +export function exportUserActivitiesAndReactionIDs(this: StreamClient, userId: string): Promise { + if (!userId) { + throw new Error("User ID can't be null or empty"); + } + + return this.get({ + url: `data_privacy/export_ids/${userId}`, + token: this.getOrCreateToken(), + }); +} + +export default { + deleteActivities, + deleteReactions, + exportUserActivitiesAndReactionIDs, +}; diff --git a/test/integration/node/client_test.js b/test/integration/node/client_test.js index e2cf4308..535c937f 100644 --- a/test/integration/node/client_test.js +++ b/test/integration/node/client_test.js @@ -680,4 +680,79 @@ describe('[INTEGRATION] Stream client (Node)', function () { }); }); }); + + it('delete activities', function () { + const activities = [ + { + actor: 'user:1', + verb: 'tweet', + object: '1', + foreign_id: 'delete_gdpr_1', + time: new Date(), + }, + { + actor: 'user:2', + verb: 'tweet', + object: '2', + foreign_id: 'delete_gdpr_2', + time: new Date(), + }, + ]; + + return this.user1 + .addActivities(activities) + .then((body) => { + const activitiesToDelete = body.activities.map((activity) => ({ + id: activity.id, + remove_from_feeds: [this.user1.id], + })); + return this.client.deleteActivities(activitiesToDelete); + // return this.client.deleteUsers(['sa']); + }) + .then(() => this.user1.get({ limit: 2 })) + .then((body) => { + expect(body.results.length).to.be(0); + }); + }); + + it('delete reactions', async function () { + const activity = { + actor: 'user:1', + verb: 'tweet', + object: '1', + }; + + const activityRes = await this.user1.addActivity(activity); + + const reaction1 = await this.client.reactions.add('like1', activityRes.id, { text: 'text' }, { userId: 'user1' }); + const reaction2 = await this.client.reactions.add('like2', activityRes.id, { text: 'text' }, { userId: 'user1' }); + await this.client.reactions.add('like3', activityRes.id, { text: 'text' }, { userId: 'user1' }); + + // delete 2 reaction + await this.client.deleteReactions([reaction1.id, reaction2.id]); + const resp = await this.client.reactions.filter({ activity_id: activityRes.id }); + expect(resp.results.length).to.be(1); + }); + + it('export user data', async function () { + const userId = randUserId('export'); + const activity = { + actor: userId, + verb: 'tweet', + object: '1', + }; + + const activityRes = await this.user1.addActivity(activity); + + const reaction1 = await this.client.reactions.add('like1', activityRes.id, { text: 'text' }, { userId }); + await this.client.reactions.add('like2', activityRes.id, { text: 'text' }, { userId: 'user1' }); + const reaction3 = await this.client.reactions.add('like3', activityRes.id, { text: 'text' }, { userId }); + + const resp = await this.client.exportUserActivitiesAndReactionIDs(userId); + expect(resp.export.activity_count).to.be(1); + expect(resp.export.reaction_count).to.be(2); + expect(resp.export.user_id).to.be(userId); + expect(resp.export.activity_ids).to.eql([activityRes.id]); + expect(resp.export.reaction_ids).to.eql([reaction1.id, reaction3.id]); + }); }); From 1607562884e5b35da3f8d2b865e4734d1637898f Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 13 Mar 2025 18:26:33 +0100 Subject: [PATCH 2/3] chore: remove unused code --- test/integration/node/client_test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/integration/node/client_test.js b/test/integration/node/client_test.js index 535c937f..5e1efb6b 100644 --- a/test/integration/node/client_test.js +++ b/test/integration/node/client_test.js @@ -707,7 +707,6 @@ describe('[INTEGRATION] Stream client (Node)', function () { remove_from_feeds: [this.user1.id], })); return this.client.deleteActivities(activitiesToDelete); - // return this.client.deleteUsers(['sa']); }) .then(() => this.user1.get({ limit: 2 })) .then((body) => { From ffeca2f6964cd7cf9f8349a807f5f0e077e61434 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 13 Mar 2025 18:59:29 +0100 Subject: [PATCH 3/3] chore: fix tests --- test/integration/node/client_test.js | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/test/integration/node/client_test.js b/test/integration/node/client_test.js index 5e1efb6b..4cc2b0f5 100644 --- a/test/integration/node/client_test.js +++ b/test/integration/node/client_test.js @@ -681,7 +681,7 @@ describe('[INTEGRATION] Stream client (Node)', function () { }); }); - it('delete activities', function () { + it('delete activities', async function () { const activities = [ { actor: 'user:1', @@ -699,19 +699,11 @@ describe('[INTEGRATION] Stream client (Node)', function () { }, ]; - return this.user1 - .addActivities(activities) - .then((body) => { - const activitiesToDelete = body.activities.map((activity) => ({ - id: activity.id, - remove_from_feeds: [this.user1.id], - })); - return this.client.deleteActivities(activitiesToDelete); - }) - .then(() => this.user1.get({ limit: 2 })) - .then((body) => { - expect(body.results.length).to.be(0); - }); + const res = await this.user1.addActivities(activities); + + await this.client.deleteActivities(res.activities.map((a) => ({ id: a.id }))); + const resp = await this.client.getActivities({ ids: res.activities.map((a) => a.id) }); + expect(resp.results.length).to.be(0); }); it('delete reactions', async function () {