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
25 changes: 23 additions & 2 deletions docs/docs/cmd/teams/chat/chat-message-list.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,17 @@ m365 teams chat message list [options]
`-i, --chatId <chatId>`
: The ID of the chat conversation.

`--endDateTime [endDateTime]`
`--createdEndDateTime [createdEndDateTime]`
: Time indicating the exclusive end of a time range when the message was created.

`--endDateTime [endDateTime]`
: (deprecated. Use `createdEndDateTime` instead) Time indicating the exclusive end of a time range when the message was created.

`--modifiedStartDateTime [modifiedStartDateTime]`
: Time indicating the inclusive start of a time range when the message was last modified. Cannot be combined with `createdEndDateTime`.

`--modifiedEndDateTime [modifiedEndDateTime]`
: Time indicating the exclusive end of a time range when the message was last modified. Cannot be combined with `createdEndDateTime`.
```

<Global />
Expand Down Expand Up @@ -54,7 +63,19 @@ m365 teams chat message list --chatId 19:2da4c29f6d7041eca70b638b43d45437@thread
List messages from a Microsoft Teams chat conversation created before November 1, 2022

```sh
m365 teams chat message list --chatId 19:2da4c29f6d7041eca70b638b43d45437@thread.v2 --endDateTime 2022-11-01T00:00:00Z
m365 teams chat message list --chatId 19:2da4c29f6d7041eca70b638b43d45437@thread.v2 --createdEndDateTime 2022-11-01T00:00:00Z
```

List messages from a Microsoft Teams chat conversation modified after October 1, 2025

```sh
m365 teams chat message list --chatId 19:2da4c29f6d7041eca70b638b43d45437@thread.v2 --modifiedStartDateTime 2025-10-01T00:00:00Z
```

List messages from a Microsoft Teams chat conversation modified between October 1, 2025 and November 1, 2025

```sh
m365 teams chat message list --chatId 19:2da4c29f6d7041eca70b638b43d45437@thread.v2 --modifiedStartDateTime 2025-10-01T00:00:00Z --modifiedEndDateTime 2025-11-01T00:00:00Z
```

## Response
Expand Down
174 changes: 171 additions & 3 deletions src/m365/teams/commands/chat/chat-message-list.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js';
import commands from '../../commands.js';
import command, { options } from './chat-message-list.js';
import { settingsNames } from '../../../../settingsNames.js';
import { z } from 'zod';

describe(commands.CHAT_MESSAGE_LIST, () => {

Expand Down Expand Up @@ -134,8 +135,10 @@ describe(commands.CHAT_MESSAGE_LIST, () => {
let log: string[];
let logger: Logger;
let loggerLogSpy: sinon.SinonSpy;
let loggerLogToStderrSpy: sinon.SinonSpy;
let commandInfo: CommandInfo;
let commandOptionsSchema: typeof options;
let refinedSchema: z.ZodTypeAny;

before(() => {
sinon.stub(auth, 'restoreAuth').resolves();
Expand All @@ -146,6 +149,7 @@ describe(commands.CHAT_MESSAGE_LIST, () => {

commandInfo = cli.getCommandInfo(command);
commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options;
refinedSchema = commandInfo.command.getRefinedSchema!(commandOptionsSchema as any)!;
sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => settingName === settingsNames.prompt ? false : defaultValue);
});

Expand All @@ -163,6 +167,7 @@ describe(commands.CHAT_MESSAGE_LIST, () => {
}
};
loggerLogSpy = sinon.spy(logger, 'log');
loggerLogToStderrSpy = sinon.spy(logger, 'logToStderr');
});

afterEach(() => {
Expand Down Expand Up @@ -213,20 +218,97 @@ describe(commands.CHAT_MESSAGE_LIST, () => {

it('fails validation if the endDateTime is not valid', async () => {
const actual = commandOptionsSchema.safeParse({
chatId: '19:2da4c29f6d7041eca70b638b43d45437',
chatId: '19:2da4c29f6d7041eca70b638b43d45437@thread.v2',
endDateTime: 'invalid date time'
});
assert.notStrictEqual(actual.success, true);
});

it('validates for a correct input', async () => {
it('fails validation if the createdEndDateTime is not valid', async () => {
const actual = commandOptionsSchema.safeParse({
chatId: '19:2da4c29f6d7041eca70b638b43d45437@thread.v2',
createdEndDateTime: 'invalid date time'
});
assert.notStrictEqual(actual.success, true);
});

it('fails validation if the modifiedStartDateTime is not valid', async () => {
const actual = commandOptionsSchema.safeParse({
chatId: '19:2da4c29f6d7041eca70b638b43d45437@thread.v2',
modifiedStartDateTime: 'not a date'
});
assert.notStrictEqual(actual.success, true);
});

it('fails validation if the modifiedEndDateTime is not valid', async () => {
const actual = commandOptionsSchema.safeParse({
chatId: '19:2da4c29f6d7041eca70b638b43d45437@thread.v2',
modifiedEndDateTime: 'not a date'
});
assert.notStrictEqual(actual.success, true);
});

it('fails validation if both endDateTime and createdEndDateTime are specified', async () => {
const actual = refinedSchema.safeParse({
chatId: '19:2da4c29f6d7041eca70b638b43d45437@thread.v2',
endDateTime: '2025-11-01T00:00:00Z',
createdEndDateTime: '2025-11-01T00:00:00Z'
});
assert.notStrictEqual(actual.success, true);
});

it('fails validation if createdEndDateTime is combined with modifiedStartDateTime', async () => {
const actual = refinedSchema.safeParse({
chatId: '19:2da4c29f6d7041eca70b638b43d45437@thread.v2',
createdEndDateTime: '2025-11-01T00:00:00Z',
modifiedStartDateTime: '2025-10-01T00:00:00Z'
});
assert.notStrictEqual(actual.success, true);
});

it('fails validation if createdEndDateTime is combined with modifiedEndDateTime', async () => {
const actual = refinedSchema.safeParse({
chatId: '19:2da4c29f6d7041eca70b638b43d45437@thread.v2',
createdEndDateTime: '2025-11-01T00:00:00Z',
modifiedEndDateTime: '2025-12-01T00:00:00Z'
});
assert.notStrictEqual(actual.success, true);
});

it('fails validation if endDateTime is combined with modifiedStartDateTime', async () => {
const actual = refinedSchema.safeParse({
chatId: '19:2da4c29f6d7041eca70b638b43d45437@thread.v2',
endDateTime: '2025-11-01T00:00:00Z',
modifiedStartDateTime: '2025-10-01T00:00:00Z'
});
assert.notStrictEqual(actual.success, true);
});

it('validates for a correct input with endDateTime', async () => {
const actual = commandOptionsSchema.safeParse({
chatId: "19:2da4c29f6d7041eca70b638b43d45437@thread.v2",
endDateTime: "2022-11-01T00:00:00Z"
});
assert.strictEqual(actual.success, true);
});

it('validates for a correct input with createdEndDateTime', async () => {
const actual = commandOptionsSchema.safeParse({
chatId: "19:2da4c29f6d7041eca70b638b43d45437@thread.v2",
createdEndDateTime: "2022-11-01T00:00:00Z"
});
assert.strictEqual(actual.success, true);
});

it('validates for a correct input with modifiedStartDateTime and modifiedEndDateTime', async () => {
const actual = commandOptionsSchema.safeParse({
chatId: "19:2da4c29f6d7041eca70b638b43d45437@thread.v2",
modifiedStartDateTime: "2025-10-01T00:00:00Z",
modifiedEndDateTime: "2025-11-01T00:00:00Z"
});
assert.strictEqual(actual.success, true);
});

it('lists chat messages (verbose)', async () => {
sinon.stub(request, 'get').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/chats/19:2da4c29f6d7041eca70b638b43d45437@thread.v2/messages`) {
Expand All @@ -248,7 +330,7 @@ describe(commands.CHAT_MESSAGE_LIST, () => {
assert(loggerLogSpy.calledWith(apiResponse));
});

it('lists chat messages using endDateTime', async () => {
it('lists chat messages using deprecated endDateTime and shows deprecation warning', async () => {
sinon.stub(request, 'get').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/chats/19:2da4c29f6d7041eca70b638b43d45437@thread.v2/messages?$filter=createdDateTime lt 2025-11-01T00:00:00Z&$orderby=createdDateTime desc`) {
return {
Expand All @@ -266,6 +348,92 @@ describe(commands.CHAT_MESSAGE_LIST, () => {
}
});

assert(loggerLogSpy.calledWith(apiResponse));
assert(loggerLogToStderrSpy.calledWith(sinon.match(`Option 'endDateTime' is deprecated. Please use 'createdEndDateTime' instead.`)));
});

it('lists chat messages using createdEndDateTime', async () => {
sinon.stub(request, 'get').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/chats/19:2da4c29f6d7041eca70b638b43d45437@thread.v2/messages?$filter=createdDateTime lt 2025-11-01T00:00:00Z&$orderby=createdDateTime desc`) {
return {
value: [...apiResponse]
};
}

throw 'Invalid request';
});

await command.action(logger, {
options: {
chatId: "19:2da4c29f6d7041eca70b638b43d45437@thread.v2",
createdEndDateTime: "2025-11-01T00:00:00Z"
}
});

assert(loggerLogSpy.calledWith(apiResponse));
});

it('lists chat messages using modifiedStartDateTime', async () => {
sinon.stub(request, 'get').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/chats/19:2da4c29f6d7041eca70b638b43d45437@thread.v2/messages?$filter=lastModifiedDateTime gt 2025-10-01T00:00:00Z&$orderby=lastModifiedDateTime desc`) {
return {
value: [...apiResponse]
};
}

throw 'Invalid request';
});

await command.action(logger, {
options: {
chatId: "19:2da4c29f6d7041eca70b638b43d45437@thread.v2",
modifiedStartDateTime: "2025-10-01T00:00:00Z"
}
});

assert(loggerLogSpy.calledWith(apiResponse));
});

it('lists chat messages using modifiedEndDateTime', async () => {
sinon.stub(request, 'get').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/chats/19:2da4c29f6d7041eca70b638b43d45437@thread.v2/messages?$filter=lastModifiedDateTime lt 2025-12-01T00:00:00Z&$orderby=lastModifiedDateTime desc`) {
return {
value: [...apiResponse]
};
}

throw 'Invalid request';
});

await command.action(logger, {
options: {
chatId: "19:2da4c29f6d7041eca70b638b43d45437@thread.v2",
modifiedEndDateTime: "2025-12-01T00:00:00Z"
}
});

assert(loggerLogSpy.calledWith(apiResponse));
});

it('lists chat messages using both modifiedStartDateTime and modifiedEndDateTime', async () => {
sinon.stub(request, 'get').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/chats/19:2da4c29f6d7041eca70b638b43d45437@thread.v2/messages?$filter=lastModifiedDateTime gt 2025-10-01T00:00:00Z and lastModifiedDateTime lt 2025-12-01T00:00:00Z&$orderby=lastModifiedDateTime desc`) {
return {
value: [...apiResponse]
};
}

throw 'Invalid request';
});

await command.action(logger, {
options: {
chatId: "19:2da4c29f6d7041eca70b638b43d45437@thread.v2",
modifiedStartDateTime: "2025-10-01T00:00:00Z",
modifiedEndDateTime: "2025-12-01T00:00:00Z"
}
});

assert(loggerLogSpy.calledWith(apiResponse));
});

Expand Down
53 changes: 49 additions & 4 deletions src/m365/teams/commands/chat/chat-message-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,25 @@ export const options = z.strictObject({
error: e => `'${e.input}' is not a valid value for option chatId.`
})
.alias('i'),
createdEndDateTime: z.string()
.refine(time => validation.isValidISODateTime(time), {
error: e => `'${e.input}' is not a valid ISO date-time string for option createdEndDateTime.`
})
.optional(),
endDateTime: z.string()
.refine(time => validation.isValidISODateTime(time), {
error: e => `'${e.input}' is not a valid ISO date-time string for option endDateTime.`
})
.optional(),
modifiedStartDateTime: z.string()
.refine(time => validation.isValidISODateTime(time), {
error: e => `'${e.input}' is not a valid ISO date-time string for option modifiedStartDateTime.`
})
.optional(),
modifiedEndDateTime: z.string()
.refine(time => validation.isValidISODateTime(time), {
error: e => `'${e.input}' is not a valid ISO date-time string for option modifiedEndDateTime.`
})
.optional()
});

Expand All @@ -47,14 +62,44 @@ class TeamsChatMessageListCommand extends GraphCommand {
return options;
}

public getRefinedSchema(schema: typeof options): z.ZodObject<any> | undefined {
return schema
.refine(options => !(options.endDateTime && options.createdEndDateTime), {
error: 'Specify either endDateTime or createdEndDateTime, but not both.'
})
.refine(options => !(options.createdEndDateTime && (options.modifiedStartDateTime || options.modifiedEndDateTime)), {
error: 'You cannot combine createdEndDateTime with modifiedStartDateTime or modifiedEndDateTime. These filters operate on different properties.'
})
.refine(options => !(options.endDateTime && (options.modifiedStartDateTime || options.modifiedEndDateTime)), {
error: 'You cannot combine endDateTime with modifiedStartDateTime or modifiedEndDateTime. These filters operate on different properties.'
});
}

public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
if (args.options.endDateTime) {
await this.warn(logger, `Option 'endDateTime' is deprecated. Please use 'createdEndDateTime' instead.`);

args.options.createdEndDateTime = args.options.endDateTime;
}

try {
let apiUrl = `${this.resource}/v1.0/chats/${args.options.chatId}/messages`;

if (args.options.endDateTime) {
// You can only filter results if the request URL contains the $orderby and $filter query parameters configured for the same property;
// otherwise, the $filter query option is ignored.
apiUrl += `?$filter=createdDateTime lt ${args.options.endDateTime}&$orderby=createdDateTime desc`;
if (args.options.createdEndDateTime) {
apiUrl += `?$filter=createdDateTime lt ${args.options.createdEndDateTime}&$orderby=createdDateTime desc`;
}
else if (args.options.modifiedStartDateTime || args.options.modifiedEndDateTime) {
const filters: string[] = [];

if (args.options.modifiedStartDateTime) {
filters.push(`lastModifiedDateTime gt ${args.options.modifiedStartDateTime}`);
}

if (args.options.modifiedEndDateTime) {
filters.push(`lastModifiedDateTime lt ${args.options.modifiedEndDateTime}`);
}

apiUrl += `?$filter=${filters.join(' and ')}&$orderby=lastModifiedDateTime desc`;
}

const items = await odata.getAllItems<ExtendedMessage>(apiUrl);
Expand Down