Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -196,6 +199,10 @@ export class StreamClient<StreamFeedGenerics extends DefaultGenerics = DefaultGe
addUsers?: (this: StreamClient, users: StreamUser[], overrideExisting: boolean) => Promise<AddUsersResponse>; // eslint-disable-line no-use-before-define
getUsers?: (this: StreamClient, ids: string[]) => Promise<GetUsersResponse>; // eslint-disable-line no-use-before-define
deleteUsers?: (this: StreamClient, ids: string[]) => Promise<string[]>; // eslint-disable-line no-use-before-define
deleteActivities?: (this: StreamClient, activities: ActivityToDelete[]) => Promise<APIResponse>; // eslint-disable-line no-use-before-define
deleteReactions?: (this: StreamClient, ids: string[]) => Promise<APIResponse>; // eslint-disable-line no-use-before-define
exportUserActivitiesAndReactionIDs?: (this: StreamClient, userId: string) => Promise<ExportIDsResponse>; // eslint-disable-line no-use-before-define

/**
* Initialize a client
* @link https://getstream.io/activity-feeds/docs/node/#setup
Expand Down Expand Up @@ -288,14 +295,17 @@ export class StreamClient<StreamFeedGenerics extends DefaultGenerics = DefaultGe
this.reactions = new StreamReaction<StreamFeedGenerics>(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;
this.createRedirectUrl = createRedirectUrl;
this.addUsers = BatchOperations.addUsers;
this.getUsers = BatchOperations.getUsers;
this.deleteUsers = BatchOperations.deleteUsers;
this.deleteActivities = DataPrivacy.deleteActivities;
this.deleteReactions = DataPrivacy.deleteReactions;
this.exportUserActivitiesAndReactionIDs = DataPrivacy.exportUserActivitiesAndReactionIDs;
}
}

Expand Down
55 changes: 55 additions & 0 deletions src/data_privacy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { StreamClient, APIResponse } from './client';

export type ActivityToDelete = {
id: string;
remove_from_feeds: string[];
};

export type ExportIDsResult = {
activity_count: number;
reaction_count: number;
activity_ids?: string[];
reaction_ids?: string[];
user_id?: string;
};

export type ExportIDsResponse = APIResponse & {
export?: ExportIDsResult;
};

export function deleteActivities(this: StreamClient, activities: ActivityToDelete[]): Promise<APIResponse> {
this._throwMissingApiSecret();

return this.post<APIResponse>({
url: 'data_privacy/delete_activities/',
body: { activities },
token: this.getOrCreateToken(),
});
}

export function deleteReactions(this: StreamClient, ids: string[]): Promise<APIResponse> {
this._throwMissingApiSecret();

return this.post<APIResponse>({
url: 'data_privacy/delete_reactions/',
body: { ids },
token: this.getOrCreateToken(),
});
}

export function exportUserActivitiesAndReactionIDs(this: StreamClient, userId: string): Promise<ExportIDsResponse> {
if (!userId) {
throw new Error("User ID can't be null or empty");
}

return this.get<ExportIDsResponse>({
url: `data_privacy/export_ids/${userId}`,
token: this.getOrCreateToken(),
});
}

export default {
deleteActivities,
deleteReactions,
exportUserActivitiesAndReactionIDs,
};
66 changes: 66 additions & 0 deletions test/integration/node/client_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
});
});