Skip to content

Commit 635b3df

Browse files
committed
Enhances 'teams chat message send' with contentType. Closes #6483
1 parent a576f35 commit 635b3df

File tree

4 files changed

+104
-71
lines changed

4 files changed

+104
-71
lines changed

docs/docs/cmd/teams/chat/chat-message-send.mdx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import Tabs from '@theme/Tabs';
2+
import TabItem from '@theme/TabItem';
13
import Global from '/docs/cmd/_global.mdx';
24

35
# teams chat message send
@@ -24,6 +26,9 @@ m365 teams chat message send [options]
2426

2527
`-m, --message <message>`
2628
: The message to send
29+
30+
`--contentType [contentType]`
31+
: The content type of the message. Allowed values are `text` and `html`. Default is `text`.
2732
```
2833

2934
<Global />
@@ -32,12 +37,29 @@ m365 teams chat message send [options]
3237

3338
A new chat conversation will be created if no existing conversation with the participants specified with emails is found.
3439

40+
## Permissions
41+
42+
<Tabs>
43+
<TabItem value="Delegated">
44+
45+
| Resource | Permissions |
46+
|-----------------|-----------------------------|
47+
| Microsoft Graph | Chat.Read, ChatMessage.Send |
48+
49+
</TabItem>
50+
<TabItem value="Application">
51+
52+
This command does not support application permissions.
53+
54+
</TabItem>
55+
</Tabs>
56+
3557
## Examples
3658

3759
Send a message to a Microsoft Teams chat conversation by id
3860

3961
```sh
40-
m365 teams chat message send --chatId 19:[email protected] --message "Welcome to Teams"
62+
m365 teams chat message send --chatId 19:[email protected] --message "<b>Welcome</b> to Teams" --contentType html
4163
```
4264

4365
Send a message to a single person

src/m365/teams/commands/chat/chat-message-send.spec.ts

Lines changed: 63 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ describe(commands.CHAT_MESSAGE_SEND, () => {
4848
auth.connection.active = true;
4949
sinon.stub(accessToken, 'assertAccessTokenType').returns();
5050
commandInfo = cli.getCommandInfo(command);
51+
52+
sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => settingName === settingsNames.prompt ? false : defaultValue);
5153
});
5254

5355
beforeEach(() => {
@@ -101,7 +103,6 @@ describe(commands.CHAT_MESSAGE_SEND, () => {
101103
request.get,
102104
request.post,
103105
accessToken.getUserNameFromAccessToken,
104-
cli.getSettingWithDefaultValue,
105106
cli.handleMultipleResultsFound
106107
]);
107108
});
@@ -120,14 +121,6 @@ describe(commands.CHAT_MESSAGE_SEND, () => {
120121
});
121122

122123
it('fails validation if chatId and chatName and userEmails are not specified', async () => {
123-
sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => {
124-
if (settingName === settingsNames.prompt) {
125-
return false;
126-
}
127-
128-
return defaultValue;
129-
});
130-
131124
const actual = await command.validate({
132125
options: {
133126
message: "Hello World"
@@ -187,14 +180,6 @@ describe(commands.CHAT_MESSAGE_SEND, () => {
187180
});
188181

189182
it('fails validation if chatId and chatName properties are both defined', async () => {
190-
sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => {
191-
if (settingName === settingsNames.prompt) {
192-
return false;
193-
}
194-
195-
return defaultValue;
196-
});
197-
198183
const actual = await command.validate({
199184
options: {
200185
chatId: '19:8b081ef6-4792-4def-b2c9-c363a1bf41d5_5031bb31-22c0-4f6f-9f73-91d34ab2b32d',
@@ -206,14 +191,6 @@ describe(commands.CHAT_MESSAGE_SEND, () => {
206191
});
207192

208193
it('fails validation if chatId and userEmails properties are both defined', async () => {
209-
sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => {
210-
if (settingName === settingsNames.prompt) {
211-
return false;
212-
}
213-
214-
return defaultValue;
215-
});
216-
217194
const actual = await command.validate({
218195
options: {
219196
chatId: '19:8b081ef6-4792-4def-b2c9-c363a1bf41d5_5031bb31-22c0-4f6f-9f73-91d34ab2b32d',
@@ -225,14 +202,6 @@ describe(commands.CHAT_MESSAGE_SEND, () => {
225202
});
226203

227204
it('fails validation if chatName and userEmails properties are both defined', async () => {
228-
sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => {
229-
if (settingName === settingsNames.prompt) {
230-
return false;
231-
}
232-
233-
return defaultValue;
234-
});
235-
236205
const actual = await command.validate({
237206
options: {
238207
chatName: 'test',
@@ -244,14 +213,6 @@ describe(commands.CHAT_MESSAGE_SEND, () => {
244213
});
245214

246215
it('fails validation if all three mutually exclusive properties are defined', async () => {
247-
sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => {
248-
if (settingName === settingsNames.prompt) {
249-
return false;
250-
}
251-
252-
return defaultValue;
253-
});
254-
255216
const actual = await command.validate({
256217
options: {
257218
chatId: '19:8b081ef6-4792-4def-b2c9-c363a1bf41d5_5031bb31-22c0-4f6f-9f73-91d34ab2b32d',
@@ -264,17 +225,20 @@ describe(commands.CHAT_MESSAGE_SEND, () => {
264225
});
265226

266227
it('fails validation if message is not specified', async () => {
267-
sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => {
268-
if (settingName === settingsNames.prompt) {
269-
return false;
228+
const actual = await command.validate({
229+
options: {
230+
chatId: "19:8b081ef6-4792-4def-b2c9-c363a1bf41d5_5031bb31-22c0-4f6f-9f73-91d34ab2b32d@unq.gbl.spaces"
270231
}
232+
}, commandInfo);
233+
assert.notStrictEqual(actual, true);
234+
});
271235

272-
return defaultValue;
273-
});
274-
236+
it('fails validation if contentType is not valid', async () => {
275237
const actual = await command.validate({
276238
options: {
277-
chatId: "19:8b081ef6-4792-4def-b2c9-c363a1bf41d5_5031bb31-22c0-4f6f-9f73-91d34ab2b32d@unq.gbl.spaces"
239+
chatId: '19:8b081ef6-4792-4def-b2c9-c363a1bf41d5_5031bb31-22c0-4f6f-9f73-91d34ab2b32d@unq.gbl.spaces',
240+
contentType: 'Invalid',
241+
message: 'Hello World'
278242
}
279243
}, commandInfo);
280244
assert.notStrictEqual(actual, true);
@@ -404,14 +368,6 @@ describe(commands.CHAT_MESSAGE_SEND, () => {
404368
});
405369

406370
it('fails sending message with multiple found chat conversations by chatName', async () => {
407-
sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => {
408-
if (settingName === settingsNames.prompt) {
409-
return false;
410-
}
411-
412-
return defaultValue;
413-
});
414-
415371
await assert.rejects(command.action(logger, {
416372
options: {
417373
chatName: "Just a conversation with same name",
@@ -433,14 +389,6 @@ describe(commands.CHAT_MESSAGE_SEND, () => {
433389
});
434390

435391
it('fails sending message with multiple found chat conversations by userEmails', async () => {
436-
sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => {
437-
if (settingName === settingsNames.prompt) {
438-
return false;
439-
}
440-
441-
return defaultValue;
442-
});
443-
444392
await assert.rejects(command.action(logger, {
445393
options: {
446394
@@ -503,6 +451,57 @@ describe(commands.CHAT_MESSAGE_SEND, () => {
503451
assert(loggerLogSpy.notCalled);
504452
});
505453

454+
it('sends chat messages using HTML content type', async () => {
455+
sinonUtil.restore(request.post);
456+
const postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
457+
if (opts.url === `https://graph.microsoft.com/v1.0/chats/19:82fe7758-5bb3-4f0d-a43f-e555fd399c6f_8c0a1a67-50ce-4114-bb6c-da9c5dbcf6ca@unq.gbl.spaces/messages`) {
458+
return messageSentResponse;
459+
}
460+
461+
throw 'Invalid request';
462+
});
463+
464+
await command.action(logger, {
465+
options: {
466+
chatId: '19:82fe7758-5bb3-4f0d-a43f-e555fd399c6f_8c0a1a67-50ce-4114-bb6c-da9c5dbcf6ca@unq.gbl.spaces',
467+
message: '<b>Hello World</b>',
468+
contentType: 'html'
469+
}
470+
});
471+
472+
assert.deepStrictEqual(postStub.firstCall.args[0].data, {
473+
body: {
474+
contentType: 'html',
475+
content: '<b>Hello World</b>'
476+
}
477+
});
478+
});
479+
480+
it('sends chat messages using text content type when not specified', async () => {
481+
sinonUtil.restore(request.post);
482+
const postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
483+
if (opts.url === `https://graph.microsoft.com/v1.0/chats/19:82fe7758-5bb3-4f0d-a43f-e555fd399c6f_8c0a1a67-50ce-4114-bb6c-da9c5dbcf6ca@unq.gbl.spaces/messages`) {
484+
return messageSentResponse;
485+
}
486+
487+
throw 'Invalid request';
488+
});
489+
490+
await command.action(logger, {
491+
options: {
492+
chatId: '19:82fe7758-5bb3-4f0d-a43f-e555fd399c6f_8c0a1a67-50ce-4114-bb6c-da9c5dbcf6ca@unq.gbl.spaces',
493+
message: 'Hello World'
494+
}
495+
});
496+
497+
assert.deepStrictEqual(postStub.firstCall.args[0].data, {
498+
body: {
499+
contentType: 'text',
500+
content: 'Hello World'
501+
}
502+
});
503+
});
504+
506505
// The following test is used to test the retry mechanism in use because of an intermittent Graph issue.
507506
it('fails sending chat message when maximum of 3 retries with 404 intermittent failure have occurred', async () => {
508507
sinonUtil.restore(request.post);

src/m365/teams/commands/chat/chat-message-send.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@ interface Options extends GlobalOptions {
2020
userEmails?: string;
2121
chatName?: string;
2222
message: string;
23+
contentType?: string;
2324
}
2425

2526
class TeamsChatMessageSendCommand extends GraphDelegatedCommand {
27+
private readonly contentTypes = ['text', 'html'];
28+
2629
public get name(): string {
2730
return commands.CHAT_MESSAGE_SEND;
2831
}
@@ -45,7 +48,8 @@ class TeamsChatMessageSendCommand extends GraphDelegatedCommand {
4548
Object.assign(this.telemetryProperties, {
4649
chatId: typeof args.options.chatId !== 'undefined',
4750
userEmails: typeof args.options.userEmails !== 'undefined',
48-
chatName: typeof args.options.chatName !== 'undefined'
51+
chatName: typeof args.options.chatName !== 'undefined',
52+
contentType: args.options.contentType ?? 'text'
4953
});
5054
});
5155
}
@@ -63,6 +67,10 @@ class TeamsChatMessageSendCommand extends GraphDelegatedCommand {
6367
},
6468
{
6569
option: '-m, --message <message>'
70+
},
71+
{
72+
option: '--contentType [contentType]',
73+
autocomplete: this.contentTypes
6674
}
6775
);
6876
}
@@ -81,6 +89,10 @@ class TeamsChatMessageSendCommand extends GraphDelegatedCommand {
8189
}
8290
}
8391

92+
if (args.options.contentType && !this.contentTypes.includes(args.options.contentType)) {
93+
return `'${args.options.contentType}' is not a valid value for option contentType. Allowed values are ${this.contentTypes.join(', ')}.`;
94+
}
95+
8496
return true;
8597
}
8698
);
@@ -93,7 +105,7 @@ class TeamsChatMessageSendCommand extends GraphDelegatedCommand {
93105
public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
94106
try {
95107
const chatId = await this.getChatId(logger, args);
96-
await this.sendChatMessage(chatId as string, args);
108+
await this.sendChatMessage(chatId, args);
97109
}
98110
catch (err: any) {
99111
this.handleRejectedODataJsonPromise(err);
@@ -112,7 +124,7 @@ class TeamsChatMessageSendCommand extends GraphDelegatedCommand {
112124

113125
private async ensureChatIdByUserEmails(userEmailsOption: string): Promise<string> {
114126
const userEmails = userEmailsOption.trim().toLowerCase().split(',').filter(e => e && e !== '');
115-
const currentUserEmail = accessToken.getUserNameFromAccessToken(auth.connection.accessTokens[this.resource].accessToken).toLowerCase();
127+
const currentUserEmail = accessToken.getUserNameFromAccessToken(auth.connection.accessTokens[auth.defaultResource].accessToken).toLowerCase();
116128
const existingChats = await chatUtil.findExistingChatsByParticipants([currentUserEmail, ...userEmails]);
117129

118130
if (!existingChats || existingChats.length === 0) {
@@ -189,11 +201,12 @@ class TeamsChatMessageSendCommand extends GraphDelegatedCommand {
189201
url: `${this.resource}/v1.0/chats/${chatId}/messages`,
190202
headers: {
191203
accept: 'application/json;odata.metadata=none',
192-
'content-type': 'application/json;odata=nometadata'
204+
'content-type': 'application/json'
193205
},
194206
responseType: 'json',
195207
data: {
196208
body: {
209+
contentType: args.options.contentType || 'text',
197210
content: args.options.message
198211
}
199212
}

src/m365/teams/commands/chat/chatUtil.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ import { formatting } from '../../../../utils/formatting.js';
33
import { odata } from '../../../../utils/odata.js';
44

55
export const chatUtil = {
6-
76
/**
87
* Finds existing Microsoft Teams chats by participants, using the Microsoft Graph
9-
* @param expectedMemberEmails a string array of participant emailaddresses
8+
* @param expectedMemberEmails a string array of participant email addresses
109
* @param logger a logger to pipe into the graph request odata helper.
1110
*/
1211
async findExistingChatsByParticipants(expectedMemberEmails: string[]): Promise<Chat[]> {

0 commit comments

Comments
 (0)