Skip to content

Commit 7d52a47

Browse files
feat(bots): improve mention behavior dx (#4569)
1 parent 84d604c commit 7d52a47

File tree

2 files changed

+96
-3
lines changed

2 files changed

+96
-3
lines changed

packages/bot/src/bot.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
isDefined,
2525
genIdBlob,
2626
ParsedEvent,
27+
type ChannelMessageEvent,
2728
} from '@towns-protocol/sdk'
2829
import { describe, it, expect, beforeAll, vi } from 'vitest'
2930
import type { BasePayload, Bot, BotPayload, DecryptedInteractionResponse } from './bot'
@@ -744,6 +745,60 @@ describe('Bot', { sequential: true }, () => {
744745
)
745746
})
746747

748+
it('bot can mention bob', async () => {
749+
await setForwardSetting(ForwardSettingValue.FORWARD_SETTING_ALL_MESSAGES)
750+
const { eventId: messageId } = await bot.sendMessage(channelId, 'Hello @bob', {
751+
mentions: [
752+
{
753+
userId: bob.userId,
754+
displayName: 'bob',
755+
},
756+
],
757+
})
758+
await waitFor(() =>
759+
expect(
760+
bobDefaultChannel.timeline.events.value.find((x) => x.eventId === messageId)
761+
?.content?.kind,
762+
).toBe(RiverTimelineEvent.ChannelMessage),
763+
)
764+
const channelMessage = bobDefaultChannel.timeline.events.value.find(
765+
(x) => x.eventId === messageId,
766+
)?.content as ChannelMessageEvent
767+
768+
expect(channelMessage.mentions).toBeDefined()
769+
expect(channelMessage.mentions?.length).toBe(1)
770+
expect(channelMessage.mentions?.[0].userId).toBe(bob.userId)
771+
expect(channelMessage.mentions?.[0].displayName).toBe('bob')
772+
})
773+
774+
it('bot can mention channel', async () => {
775+
await setForwardSetting(ForwardSettingValue.FORWARD_SETTING_ALL_MESSAGES)
776+
const { eventId: messageId } = await bot.sendMessage(channelId, 'Hello @channel', {
777+
mentions: [{ atChannel: true }],
778+
})
779+
await waitFor(() =>
780+
expect(
781+
bobDefaultChannel.timeline.events.value.find((x) => x.eventId === messageId)
782+
?.content?.kind,
783+
).toBe(RiverTimelineEvent.ChannelMessage),
784+
)
785+
const channelMessage = bobDefaultChannel.timeline.events.value.find(
786+
(x) => x.eventId === messageId,
787+
)
788+
let channelMessageEvent: ChannelMessageEvent | undefined
789+
if (channelMessage?.content?.kind === RiverTimelineEvent.ChannelMessage) {
790+
channelMessageEvent = channelMessage.content
791+
} else {
792+
throw new Error('Message is not a channel message')
793+
}
794+
795+
expect(channelMessageEvent.mentions).toBeDefined()
796+
expect(channelMessageEvent.mentions?.length).toBe(1)
797+
// @ts-expect-error - types of timeline is wrong
798+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
799+
expect(channelMessageEvent.mentions?.[0].mentionBehavior?.case).toBe('atChannel')
800+
})
801+
747802
it('bot can fetch existing decryption keys when sending a message', async () => {
748803
// on a fresh boot the bot won't have any keys in cache, so it should fetch them from the app server if they exist
749804
await setForwardSetting(ForwardSettingValue.FORWARD_SETTING_ALL_MESSAGES)

packages/bot/src/bot.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import {
8181
ChannelMessage_Post_AttachmentSchema,
8282
type AppMetadata,
8383
StreamEvent,
84+
ChannelMessage_Post_MentionSchema,
8485
} from '@towns-protocol/proto'
8586
import {
8687
bin_equal,
@@ -118,6 +119,7 @@ import { getSmartAccountFromUserIdImpl } from './smart-account'
118119
import type { BotIdentityConfig, BotIdentityMetadata, ERC8004Endpoint } from './identity-types'
119120
import channelsFacetAbi from '@towns-protocol/generated/dev/abis/Channels.abi'
120121
import rolesFacetAbi from '@towns-protocol/generated/dev/abis/Roles.abi'
122+
import { EmptySchema } from '@bufbuild/protobuf/wkt'
121123

122124
type BotActions = ReturnType<typeof buildBotActions>
123125

@@ -192,7 +194,9 @@ export type MessageOpts = {
192194
}
193195

194196
export type PostMessageOpts = MessageOpts & {
195-
mentions?: PlainMessage<ChannelMessage_Post_Mention>[]
197+
mentions?: Array<
198+
{ userId: string; displayName: string } | { roleId: number } | { atChannel: true }
199+
>
196200
attachments?: Array<
197201
| ImageAttachment
198202
| ChunkedMediaAttachment
@@ -1831,7 +1835,7 @@ const buildBotActions = (
18311835
value: {
18321836
body: message,
18331837
attachments: processedAttachments.filter((x) => x !== null),
1834-
mentions: opts?.mentions || [],
1838+
mentions: processMentions(opts?.mentions),
18351839
},
18361840
},
18371841
},
@@ -1895,7 +1899,7 @@ const buildBotActions = (
18951899
case: 'text',
18961900
value: {
18971901
body: message,
1898-
mentions: opts?.mentions || [],
1902+
mentions: processMentions(opts?.mentions),
18991903
attachments: processedAttachments.filter((x) => x !== null),
19001904
},
19011905
},
@@ -2440,3 +2444,37 @@ const parseMentions = (
24402444
? [{ userId: m.userId, displayName: m.displayName }]
24412445
: [],
24422446
)
2447+
2448+
const processMentions = (
2449+
mentions: PostMessageOpts['mentions'],
2450+
): PlainMessage<ChannelMessage_Post_Mention>[] => {
2451+
if (!mentions) {
2452+
return []
2453+
}
2454+
return mentions.map((mention) => {
2455+
if ('userId' in mention) {
2456+
return create(ChannelMessage_Post_MentionSchema, {
2457+
userId: mention.userId,
2458+
displayName: mention.displayName,
2459+
})
2460+
} else if ('roleId' in mention) {
2461+
return create(ChannelMessage_Post_MentionSchema, {
2462+
mentionBehavior: {
2463+
case: 'atRole',
2464+
value: {
2465+
roleId: mention.roleId,
2466+
},
2467+
},
2468+
})
2469+
} else if ('atChannel' in mention) {
2470+
return create(ChannelMessage_Post_MentionSchema, {
2471+
mentionBehavior: {
2472+
case: 'atChannel',
2473+
value: create(EmptySchema, {}),
2474+
},
2475+
})
2476+
} else {
2477+
throw new Error(`Invalid mention type: ${JSON.stringify(mention)}`)
2478+
}
2479+
})
2480+
}

0 commit comments

Comments
 (0)