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..4cc2b0f5 100644 --- a/test/integration/node/client_test.js +++ b/test/integration/node/client_test.js @@ -680,4 +680,70 @@ describe('[INTEGRATION] Stream client (Node)', function () { }); }); }); + + it('delete activities', async 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(), + }, + ]; + + 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 () { + 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]); + }); });