Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API: add removeMentionsText util and tests #30

Merged
merged 4 commits into from
Mar 5, 2025
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
3 changes: 2 additions & 1 deletion packages/api/src/activities/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './activity-utils';
export * from './remove-mentions-text';
export * from './to-activity-params';
129 changes: 129 additions & 0 deletions packages/api/src/activities/utils/remove-mentions-text.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { MessageSendActivity } from '../message';
import { removeMentionsText } from './remove-mentions-text';

describe('Activity Utils', () => {
describe('removeMentionsText', () => {
const activity: MessageSendActivity = {
id: '1',
type: 'message',
channelId: 'msteams',
text: 'Hello <at>test-bot</at>! How are you?',
conversation: {
id: '2',
conversationType: 'personal',
},
from: {
id: '3',
name: 'test-user',
role: 'user',
},
recipient: {
id: '4',
name: 'test-bot',
role: 'bot',
},
entities: [
{
type: 'mention',
mentioned: {
id: '4',
name: 'test-bot',
role: 'bot',
},
},
],
};

it('should do nothing when no text', () => {
const text = removeMentionsText({
...activity,
type: 'typing',
text: undefined,
});

expect(text).toBeUndefined();
});

it('should do nothing when no mentions', () => {
const text = removeMentionsText({
...activity,
entities: undefined,
});

expect(text).toEqual(activity.text);
});

it('should remove mention', () => {
const text = removeMentionsText(activity);
expect(text).toEqual('Hello ! How are you?');
});

it('should remove multiple mentions', () => {
const text = removeMentionsText({
...activity,
text: `${activity.text} <at>some other text</at>`,
entities: [
...(activity.entities || []),
{
type: 'mention',
text: '<at>some other text</at>',
mentioned: {
id: '4',
name: 'test-bot',
role: 'bot',
},
},
],
});

expect(text).toEqual('Hello ! How are you?');
});

it('should remove only mention tags', () => {
const text = removeMentionsText(
{
...activity,
text: `${activity.text} <at>some other text</at>`,
entities: [
...(activity.entities || []),
{
type: 'mention',
text: '<at>some other text</at>',
mentioned: {
id: '4',
name: 'test-bot',
role: 'bot',
},
},
],
},
{ tagOnly: true }
);

expect(text).toEqual('Hello test-bot! How are you? some other text');
});

it('should remove only specific account mentions', () => {
const text = removeMentionsText(
{
...activity,
text: `${activity.text} <at>test-bot-2</at>`,
entities: [
...(activity.entities || []),
{
type: 'mention',
mentioned: {
id: '5',
name: 'test-bot-2',
role: 'bot',
},
},
],
},
{ accountId: '4' }
);

expect(text).toEqual('Hello ! How are you? <at>test-bot-2</at>');
});
});
});
54 changes: 54 additions & 0 deletions packages/api/src/activities/utils/remove-mentions-text.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { MessageSendActivity, MessageUpdateActivity } from '../message';
import { TypingActivity } from '../typing';

/**
* any activity type that has a `text` property
*/
type TextActivity = MessageSendActivity | MessageUpdateActivity | TypingActivity;

export interface RemoveMentionsTextOptions {
/**
* the account to remove mentions for
* by default, all at-mentions listed in `entities` are removed.
*/
accountId?: string;

/**
* when `true`, the inner text of the tag
* will not be removed
* Eg. input: Hello <at>my-bot</at>! How are you?
* output: Hello my-bot! How are you?
*/
tagOnly?: boolean;
}

/**
* remove "\<at>...\</at>" text from an activity
* @param activity the activity
*/
export function removeMentionsText<TActivity extends TextActivity>(
activity: TActivity,
{ accountId, tagOnly }: RemoveMentionsTextOptions = {}
): TActivity['text'] {
if (!activity.text) return;

let text = activity.text;

for (const mention of activity.entities?.filter((e) => e.type === 'mention') || []) {
if (accountId && mention.mentioned.id !== accountId) {
continue;
}

if (mention.text) {
const textWithoutTags = mention.text.replace('<at>', '').replace('</at>', '');
text = text.replace(mention.text, !tagOnly ? '' : textWithoutTags);
} else {
text = text.replace(
`<at>${mention.mentioned.name}</at>`,
!tagOnly ? '' : mention.mentioned.name
);
}
}

return text.trim();
}
37 changes: 37 additions & 0 deletions packages/api/src/activities/utils/to-activity-params.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Card, CodeBlock } from '@microsoft/spark.cards';

import { MessageSendActivity } from '../message';
import { toActivityParams } from './to-activity-params';

describe('Activity Utils', () => {
describe('toActivityParams', () => {
it('should convert `string` to message activity', () => {
const activity = toActivityParams('testing123');
expect(activity).toEqual({
type: 'message',
text: 'testing123',
});
});

it('should convert builder to message activity', () => {
const activity = toActivityParams(MessageSendActivity('testing123'));
expect(activity).toEqual({
type: 'message',
text: 'testing123',
});
});

it('should convert card to message activity with card attachment', () => {
const card = Card([
CodeBlock({
language: 'TypeScript',
codeSnippet: 'let test = 1',
}),
]);

const activity = toActivityParams(card);

expect(activity).toEqual(MessageSendActivity('').card('adaptive', card).build());
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ export function toActivityParams(activity: ActivityLike): ActivityParams {
} else if (isCard(activity)) {
activity = new MessageSendActivityBuilder('').card('adaptive', activity).build();
}

return activity;
}
21 changes: 21 additions & 0 deletions packages/apps/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
TokenExchangeState,
cardAttachment,
ActivityLike,
RemoveMentionsTextOptions,
} from '@microsoft/spark.api';

import pkg from '../package.json';
Expand Down Expand Up @@ -80,8 +81,23 @@ export type AppOptions = Partial<Credentials> & {
* The apps manifest
*/
readonly manifest?: Partial<manifest.Manifest>;

/**
* Activity Options
*/
readonly activity?: AppActivityOptions;
};

export interface AppActivityOptions {
readonly mentions?: {
/**
* Automatically remove `<at>...</at>` mention
* from inbound activity `text`
*/
readonly removeText?: boolean | RemoveMentionsTextOptions;
};
}

export interface AppTokens {
/**
* bot token used to send activities
Expand Down Expand Up @@ -257,6 +273,11 @@ export class App {
this.plugin(plugin);
}

if (this.options.activity?.mentions?.removeText) {
const options = this.options.activity?.mentions?.removeText;
this.use(middleware.removeMentionsText(typeof options === 'boolean' ? {} : options));
}

// default event handlers
this.on('signin.token-exchange', this.onTokenExchange.bind(this));
this.on('signin.verify-state', this.onVerifyState.bind(this));
Expand Down
1 change: 1 addition & 0 deletions packages/apps/src/middleware/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './with-client-auth';
export * from './remove-mentions-text';
15 changes: 15 additions & 0 deletions packages/apps/src/middleware/remove-mentions-text.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as api from '@microsoft/spark.api';

import { MiddlewareContext } from '../middleware-context';

export function removeMentionsText(options?: api.RemoveMentionsTextOptions) {
return ({ activity }: MiddlewareContext) => {
if (
activity.type === 'message' ||
activity.type === 'messageUpdate' ||
activity.type === 'typing'
) {
activity.text = api.removeMentionsText(activity, options);
}
};
}