Conversation
4e0db45 to
70b2cb5
Compare
There was a problem hiding this comment.
Pull request overview
This PR updates the SDK’s reply/quote behavior to use Teams “quoted replies” (entity + <quoted .../> placeholder) instead of replyToId/blockquote HTML, adds first-class quoted-reply types/APIs, fixes an ActivityContext constructor enrichment bug, and introduces an example app to exercise the new functionality.
Changes:
- Add
QuotedReplyEntity/QuotedReplyData, plumb intoEntityunion, and addMessageActivity.getQuotedMessages()+addQuotedReply(). - Update
ActivityContext.reply()to stamp quoted-reply metadata + placeholder, and addActivityContext.quoteReply(messageId, ...). - Fix
ActivityContextconstructor ordering bug (enrichment beforeObject.assign) and add regression tests + a new quoted-replies example.
Reviewed changes
Copilot reviewed 16 out of 19 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/apps/src/contexts/activity.ts | Adds quoteReply() API, changes reply() quoting behavior, and fixes constructor activity enrichment ordering. |
| packages/apps/src/contexts/activity.test.ts | Updates tests to assert quoted-reply entity + placeholder behavior for reply() and quoteReply(). |
| packages/apps/src/app.process.spec.ts | Regression test ensuring message activities are enriched with interface methods (e.g., getQuotedMessages). |
| packages/api/src/models/entity/quoted-reply-entity.ts | Introduces QuotedReplyEntity and nested quoted reply data model. |
| packages/api/src/models/entity/index.ts | Exposes QuotedReplyEntity via the Entity union and exports. |
| packages/api/src/activities/message/message.ts | Adds getQuotedMessages() and addQuotedReply() builder API. |
| packages/api/src/activities/message/message.spec.ts | Adds unit tests for quoted-reply parsing and builder behavior. |
| packages/api/src/activities/activity.ts | Removes withReplyToId() builder method. |
| packages/api/src/activities/activity.spec.ts | Updates tests to reflect removal of withReplyToId(). |
| package-lock.json | Adds the new @examples/quoted-replies workspace entry. |
| examples/quoted-replies/** | New runnable example demonstrating reading/sending quoted replies. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (this.activity.id) { | ||
| const placeholder = `<quoted messageId="${this.activity.id}"/>`; | ||
| if (!activity.entities) { | ||
| activity.entities = []; | ||
| } | ||
| activity.entities.push({ | ||
| type: 'quotedReply', | ||
| quotedReply: { messageId: this.activity.id }, | ||
| }); |
There was a problem hiding this comment.
reply() currently stamps a quotedReply entity whenever this.activity.id exists, even when the outbound activity is not a message (e.g., reply({ type: 'typing' })). That can lead to sending a typing activity with a quotedReply entity that Teams/Bot Framework may not understand. Consider only adding the quoted-reply entity (and placeholder) when the outbound activity is a message (or otherwise explicitly supported).
There was a problem hiding this comment.
By design — the entity is metadata that is harmless on non-message types, and the text placeholder is only added when activity.type === 'message' (line 271). This is consistent across all three SDKs.
| const placeholder = `<quoted messageId="${this.activity.id}"/>`; | ||
| if (!activity.entities) { | ||
| activity.entities = []; | ||
| } | ||
| activity.entities.push({ | ||
| type: 'quotedReply', | ||
| quotedReply: { messageId: this.activity.id }, | ||
| }); | ||
|
|
||
| if (blockQuote) { | ||
| activity.text = `${blockQuote}\r\n${activity.text}`; | ||
| if (activity.type === 'message') { | ||
| const text = activity.text?.trim() ?? ''; | ||
| activity.text = text ? `${placeholder} ${text}` : placeholder; |
There was a problem hiding this comment.
The <quoted messageId="..."/> placeholder is built via string interpolation without escaping/validating this.activity.id. If an ID ever contains ", <, or >, this can break the tag and allow markup injection into the outbound message text. Please ensure messageId is validated to an expected safe format (or XML-escaped) before embedding it in the placeholder string.
There was a problem hiding this comment.
this.activity.id is a service-generated ID from the Teams/Bot Framework backend — always a safe alphanumeric string. Escaping would add complexity for a scenario that cannot occur with real Teams activity IDs.
| async quoteReply(messageId: string, activity: ActivityLike) { | ||
| activity = toActivityParams(activity); | ||
| const placeholder = `<quoted messageId="${messageId}"/>`; | ||
|
|
||
| if (!activity.entities) { | ||
| activity.entities = []; | ||
| } | ||
| activity.entities.push({ | ||
| type: 'quotedReply', | ||
| quotedReply: { messageId }, | ||
| }); | ||
|
|
||
| if (activity.type === 'message') { | ||
| const text = activity.text?.trim() ?? ''; | ||
| activity.text = text ? `${placeholder} ${text}` : placeholder; | ||
| } |
There was a problem hiding this comment.
quoteReply() always appends a quotedReply entity, even when the outbound activity is not a message. If callers pass { type: 'typing' } (or other non-message activities), this will attach a quotedReply entity without adding the required <quoted .../> text placeholder, which is likely invalid/ineffective. Consider restricting quoteReply() to message activities (throw or no-op for other types) so the entity + placeholder stay consistent.
There was a problem hiding this comment.
Same as the reply() comment — entity is metadata, text placeholder only added for messages (line 300 check). Consistent behavior.
| activity = toActivityParams(activity); | ||
| const placeholder = `<quoted messageId="${messageId}"/>`; | ||
|
|
||
| if (!activity.entities) { | ||
| activity.entities = []; | ||
| } | ||
| activity.entities.push({ | ||
| type: 'quotedReply', | ||
| quotedReply: { messageId }, | ||
| }); | ||
|
|
||
| if (activity.type === 'message') { | ||
| const text = activity.text?.trim() ?? ''; | ||
| activity.text = text ? `${placeholder} ${text}` : placeholder; | ||
| } |
There was a problem hiding this comment.
The messageId parameter is interpolated directly into <quoted messageId="${messageId}"/> without escaping/validation. Because this method is public and messageId can be arbitrary input, this creates a markup-injection risk and can also generate invalid placeholders. Validate the ID (e.g., non-empty and matches expected character set) or XML-escape it before constructing the placeholder.
There was a problem hiding this comment.
The messageId parameter comes from a prior send() result or from the service. Same reasoning — these are service-generated IDs.
| type: 'quotedReply', | ||
| quotedReply: { messageId }, | ||
| }); | ||
| this.addText(`<quoted messageId="${messageId}"/>`); |
There was a problem hiding this comment.
addQuotedReply() interpolates messageId directly into the <quoted messageId="..."/> placeholder without escaping/validation. Since messageId is a public input, this can break the placeholder markup (and potentially inject additional tags) if it contains special characters like " or <. Please validate/escape messageId before embedding it in the text.
| this.addText(`<quoted messageId="${messageId}"/>`); | |
| const escapedMessageId = messageId | |
| .replace(/&/g, '&') | |
| .replace(/"/g, '"') | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>'); | |
| this.addText(`<quoted messageId="${escapedMessageId}"/>`); |
There was a problem hiding this comment.
Same reasoning — message IDs are service-generated. Also fixed the text stripping issue: now using a trimmed copy only for the empty-check decision, preserving the original text.
| @@ -257,11 +257,6 @@ export class Activity<T extends string = string> implements IActivity<T> { | |||
| return this; | |||
| } | |||
|
|
|||
There was a problem hiding this comment.
Removing withReplyToId() from the public Activity builder is a breaking API change for any consumers that used it. If this is intended, it likely needs a deprecation period (keep the method but mark it @deprecated, or provide an alternative) and a documented versioning note; otherwise existing user code will stop compiling on upgrade.
| /** | |
| * @deprecated Use {@link Activity.clone} with `replyToId` in options | |
| * or set the `replyToId` property directly. | |
| */ | |
| withReplyToId(value: string) { | |
| this.replyToId = value; | |
| return this; | |
| } |
There was a problem hiding this comment.
Intentional — withReplyToId() is being replaced by the quoted-reply entity approach across all three SDKs (Python, .NET, TypeScript) as part of the design spec. The replyToId property itself still exists on the activity interface for raw access; the builder was the only thing removed.
withReplyToId():replyToIdbody property has no effect on threading (APX only uses it for encryption)QuotedReplyEntitytype with nestedquotedReplydata object (messageIdrequired,senderId/senderName/preview/time/isReplyDeleted/validatedMessageReferenceoptional)getQuotedMessages()onMessageActivityto read inbound quoted reply entitiesaddQuotedReply(messageId, response?)builder onMessageActivityfor constructing outbound quotes (build order = reading order)eply()to stampquotedReplyentity+ <quoted messageId="..."/>placeholder instead of blockquote HTML; removereplyToIdassignmentquoteReply(messageId, activity)onActivityContextfor quoting arbitrary messages@experimentalActivityContextconstructor whereObject.assign(this, rest)ran beforeMessageActivity.from().toInterface(), causing interface methods (getQuotedMessages,stripMentionsText,isRecipientMentioned,getAccountMention) to be missing oncontext.activity. Added regression test.