diff --git a/cdk/lib/__snapshots__/stack.test.ts.snap b/cdk/lib/__snapshots__/stack.test.ts.snap index 2cfa16bc..9e8ef9e0 100644 --- a/cdk/lib/__snapshots__/stack.test.ts.snap +++ b/cdk/lib/__snapshots__/stack.test.ts.snap @@ -2250,7 +2250,7 @@ type Query { listItems(pinboardId: String!): [Item] listLastItemSeenByUsers(pinboardId: String!): [LastItemSeenByUser] getMyUser: MyUser - searchMentionableUsers(prefix: String!): UsersAndGroups + searchMentionableUsers(prefix: String!): UsersGroupsAndChatBots getUsers(emails: [String!]!): [User] getGroupPinboardIds: [PinboardIdWithClaimCounts!]! getItemCounts(pinboardIds: [String!]!): [PinboardIdWithItemCounts!]! @@ -2308,6 +2308,7 @@ type Item { pinboardId: String! mentions: [MentionHandle!] groupMentions: [MentionHandle!] + chatBotMentions: [String!] claimedByEmail: String claimable: Boolean! relatedItemId: String @@ -2345,9 +2346,16 @@ type Group { memberEmails: [String!]! } -type UsersAndGroups { +type ChatBot { + shorthand: String! + description: String! + avatarUrl: String +} + +type UsersGroupsAndChatBots { users: [User!]! groups: [Group!]! + chatBots: [ChatBot!]! } type WorkflowStub { @@ -2385,6 +2393,7 @@ input CreateItemInput { pinboardId: String! mentions: [String!] groupMentions: [String!] + chatBotMentions: [String!] claimable: Boolean relatedItemId: String } @@ -2467,7 +2476,7 @@ type PinboardIdWithItemCounts { "DataSourceName": "database_bridge_lambda_ds", "FieldName": "addManuallyOpenedPinboardIds", "Kind": "UNIT", - "ResponseMappingTemplate": "## schema checksum : aa02bae16942e48366fa36a1cd643d16 + "ResponseMappingTemplate": "## schema checksum : a54e7dc179390650f513c62f63e1cfe7 $util.toJson($ctx.result)", "TypeName": "Mutation", }, @@ -2488,7 +2497,7 @@ $util.toJson($ctx.result)", "DataSourceName": "database_bridge_lambda_ds", "FieldName": "claimItem", "Kind": "UNIT", - "ResponseMappingTemplate": "## schema checksum : aa02bae16942e48366fa36a1cd643d16 + "ResponseMappingTemplate": "## schema checksum : a54e7dc179390650f513c62f63e1cfe7 $util.toJson($ctx.result)", "TypeName": "Mutation", }, @@ -2509,7 +2518,7 @@ $util.toJson($ctx.result)", "DataSourceName": "database_bridge_lambda_ds", "FieldName": "createItem", "Kind": "UNIT", - "ResponseMappingTemplate": "## schema checksum : aa02bae16942e48366fa36a1cd643d16 + "ResponseMappingTemplate": "## schema checksum : a54e7dc179390650f513c62f63e1cfe7 $util.toJson($ctx.result)", "TypeName": "Mutation", }, @@ -2530,7 +2539,7 @@ $util.toJson($ctx.result)", "DataSourceName": "database_bridge_lambda_ds", "FieldName": "deleteItem", "Kind": "UNIT", - "ResponseMappingTemplate": "## schema checksum : aa02bae16942e48366fa36a1cd643d16 + "ResponseMappingTemplate": "## schema checksum : a54e7dc179390650f513c62f63e1cfe7 $util.toJson($ctx.result)", "TypeName": "Mutation", }, @@ -2551,7 +2560,7 @@ $util.toJson($ctx.result)", "DataSourceName": "database_bridge_lambda_ds", "FieldName": "editItem", "Kind": "UNIT", - "ResponseMappingTemplate": "## schema checksum : aa02bae16942e48366fa36a1cd643d16 + "ResponseMappingTemplate": "## schema checksum : a54e7dc179390650f513c62f63e1cfe7 $util.toJson($ctx.result)", "TypeName": "Mutation", }, @@ -2572,7 +2581,7 @@ $util.toJson($ctx.result)", "DataSourceName": "database_bridge_lambda_ds", "FieldName": "removeManuallyOpenedPinboardIds", "Kind": "UNIT", - "ResponseMappingTemplate": "## schema checksum : aa02bae16942e48366fa36a1cd643d16 + "ResponseMappingTemplate": "## schema checksum : a54e7dc179390650f513c62f63e1cfe7 $util.toJson($ctx.result)", "TypeName": "Mutation", }, @@ -2593,7 +2602,7 @@ $util.toJson($ctx.result)", "DataSourceName": "database_bridge_lambda_ds", "FieldName": "seenItem", "Kind": "UNIT", - "ResponseMappingTemplate": "## schema checksum : aa02bae16942e48366fa36a1cd643d16 + "ResponseMappingTemplate": "## schema checksum : a54e7dc179390650f513c62f63e1cfe7 $util.toJson($ctx.result)", "TypeName": "Mutation", }, @@ -2614,7 +2623,7 @@ $util.toJson($ctx.result)", "DataSourceName": "database_bridge_lambda_ds", "FieldName": "setWebPushSubscriptionForUser", "Kind": "UNIT", - "ResponseMappingTemplate": "## schema checksum : aa02bae16942e48366fa36a1cd643d16 + "ResponseMappingTemplate": "## schema checksum : a54e7dc179390650f513c62f63e1cfe7 $util.toJson($ctx.result)", "TypeName": "Mutation", }, @@ -2635,7 +2644,7 @@ $util.toJson($ctx.result)", "DataSourceName": "database_bridge_lambda_ds", "FieldName": "visitTourStep", "Kind": "UNIT", - "ResponseMappingTemplate": "## schema checksum : aa02bae16942e48366fa36a1cd643d16 + "ResponseMappingTemplate": "## schema checksum : a54e7dc179390650f513c62f63e1cfe7 $util.toJson($ctx.result)", "TypeName": "Mutation", }, @@ -2656,7 +2665,7 @@ $util.toJson($ctx.result)", "DataSourceName": "database_bridge_lambda_ds", "FieldName": "getGroupPinboardIds", "Kind": "UNIT", - "ResponseMappingTemplate": "## schema checksum : aa02bae16942e48366fa36a1cd643d16 + "ResponseMappingTemplate": "## schema checksum : a54e7dc179390650f513c62f63e1cfe7 $util.toJson($ctx.result)", "TypeName": "Query", }, @@ -2677,7 +2686,7 @@ $util.toJson($ctx.result)", "DataSourceName": "database_bridge_lambda_ds", "FieldName": "getItemCounts", "Kind": "UNIT", - "ResponseMappingTemplate": "## schema checksum : aa02bae16942e48366fa36a1cd643d16 + "ResponseMappingTemplate": "## schema checksum : a54e7dc179390650f513c62f63e1cfe7 $util.toJson($ctx.result)", "TypeName": "Query", }, @@ -2698,7 +2707,7 @@ $util.toJson($ctx.result)", "DataSourceName": "database_bridge_lambda_ds", "FieldName": "getMyUser", "Kind": "UNIT", - "ResponseMappingTemplate": "## schema checksum : aa02bae16942e48366fa36a1cd643d16 + "ResponseMappingTemplate": "## schema checksum : a54e7dc179390650f513c62f63e1cfe7 $util.toJson($ctx.result)", "TypeName": "Query", }, @@ -2719,7 +2728,7 @@ $util.toJson($ctx.result)", "DataSourceName": "database_bridge_lambda_ds", "FieldName": "getUsers", "Kind": "UNIT", - "ResponseMappingTemplate": "## schema checksum : aa02bae16942e48366fa36a1cd643d16 + "ResponseMappingTemplate": "## schema checksum : a54e7dc179390650f513c62f63e1cfe7 $util.toJson($ctx.result)", "TypeName": "Query", }, @@ -2740,7 +2749,7 @@ $util.toJson($ctx.result)", "DataSourceName": "database_bridge_lambda_ds", "FieldName": "listItems", "Kind": "UNIT", - "ResponseMappingTemplate": "## schema checksum : aa02bae16942e48366fa36a1cd643d16 + "ResponseMappingTemplate": "## schema checksum : a54e7dc179390650f513c62f63e1cfe7 $util.toJson($ctx.result)", "TypeName": "Query", }, @@ -2761,7 +2770,7 @@ $util.toJson($ctx.result)", "DataSourceName": "database_bridge_lambda_ds", "FieldName": "listLastItemSeenByUsers", "Kind": "UNIT", - "ResponseMappingTemplate": "## schema checksum : aa02bae16942e48366fa36a1cd643d16 + "ResponseMappingTemplate": "## schema checksum : a54e7dc179390650f513c62f63e1cfe7 $util.toJson($ctx.result)", "TypeName": "Query", }, @@ -2782,7 +2791,7 @@ $util.toJson($ctx.result)", "DataSourceName": "database_bridge_lambda_ds", "FieldName": "searchMentionableUsers", "Kind": "UNIT", - "ResponseMappingTemplate": "## schema checksum : aa02bae16942e48366fa36a1cd643d16 + "ResponseMappingTemplate": "## schema checksum : a54e7dc179390650f513c62f63e1cfe7 $util.toJson($ctx.result)", "TypeName": "Query", }, @@ -2907,7 +2916,7 @@ $util.toJson($ctx.result)", "DataSourceName": "grid_bridge_lambda_ds", "FieldName": "asGridPayload", "Kind": "UNIT", - "ResponseMappingTemplate": "## schema checksum : aa02bae16942e48366fa36a1cd643d16 + "ResponseMappingTemplate": "## schema checksum : a54e7dc179390650f513c62f63e1cfe7 $util.toJson($ctx.result)", "TypeName": "Query", }, @@ -2928,7 +2937,7 @@ $util.toJson($ctx.result)", "DataSourceName": "grid_bridge_lambda_ds", "FieldName": "getGridSearchSummary", "Kind": "UNIT", - "ResponseMappingTemplate": "## schema checksum : aa02bae16942e48366fa36a1cd643d16 + "ResponseMappingTemplate": "## schema checksum : a54e7dc179390650f513c62f63e1cfe7 $util.toJson($ctx.result)", "TypeName": "Query", }, @@ -3053,7 +3062,7 @@ $util.toJson($ctx.result)", "DataSourceName": "workflow_bridge_lambda_ds", "FieldName": "getPinboardByComposerId", "Kind": "UNIT", - "ResponseMappingTemplate": "## schema checksum : aa02bae16942e48366fa36a1cd643d16 + "ResponseMappingTemplate": "## schema checksum : a54e7dc179390650f513c62f63e1cfe7 $util.toJson($ctx.result)", "TypeName": "Query", }, @@ -3074,7 +3083,7 @@ $util.toJson($ctx.result)", "DataSourceName": "workflow_bridge_lambda_ds", "FieldName": "getPinboardsByIds", "Kind": "UNIT", - "ResponseMappingTemplate": "## schema checksum : aa02bae16942e48366fa36a1cd643d16 + "ResponseMappingTemplate": "## schema checksum : a54e7dc179390650f513c62f63e1cfe7 $util.toJson($ctx.result)", "TypeName": "Query", }, @@ -3095,7 +3104,7 @@ $util.toJson($ctx.result)", "DataSourceName": "workflow_bridge_lambda_ds", "FieldName": "listPinboards", "Kind": "UNIT", - "ResponseMappingTemplate": "## schema checksum : aa02bae16942e48366fa36a1cd643d16 + "ResponseMappingTemplate": "## schema checksum : a54e7dc179390650f513c62f63e1cfe7 $util.toJson($ctx.result)", "TypeName": "Query", }, diff --git a/client/gql.ts b/client/gql.ts index 550a643c..3206e156 100644 --- a/client/gql.ts +++ b/client/gql.ts @@ -133,6 +133,11 @@ export const gqlSearchMentionableUsers = (prefix: string) => gql` name memberEmails } + chatBots { + shorthand + description + avatarUrl + } } } `; diff --git a/client/src/avatarRoundel.tsx b/client/src/avatarRoundel.tsx index 604daf14..a1dcaae5 100644 --- a/client/src/avatarRoundel.tsx +++ b/client/src/avatarRoundel.tsx @@ -1,23 +1,29 @@ import { css } from "@emotion/react"; import { neutral } from "@guardian/source-foundations"; import React from "react"; -import { Group, User } from "../../shared/graphql/graphql"; +import { ChatBot, Group, User } from "../../shared/graphql/graphql"; import { composer } from "../colours"; import { agateSans } from "../fontNormaliser"; -import { isUser } from "../../shared/graphql/extraTypes"; +import { + hasAvatarUrl, + isChatBot, + isGroup, + isUser, +} from "../../shared/graphql/extraTypes"; interface AvatarRoundelProps { - maybeUserOrGroup: User | Group | undefined; + maybeUserOrGroupOrChatBot: User | Group | ChatBot | undefined; size: number; fallback: string; } export const AvatarRoundel = ({ - maybeUserOrGroup, + maybeUserOrGroupOrChatBot, size, fallback, }: AvatarRoundelProps) => - maybeUserOrGroup && isUser(maybeUserOrGroup) && maybeUserOrGroup.avatarUrl ? ( + hasAvatarUrl(maybeUserOrGroupOrChatBot) && + maybeUserOrGroupOrChatBot.avatarUrl ? ( ) : ( @@ -47,17 +53,17 @@ export const AvatarRoundel = ({ line-height: ${size}px; `} > - {maybeUserOrGroup ? ( - isUser(maybeUserOrGroup) ? ( - - {maybeUserOrGroup.firstName.charAt(0).toUpperCase()} - {maybeUserOrGroup.lastName?.charAt(0).toUpperCase()} - - ) : ( - maybeUserOrGroup.memberEmails?.length - ) - ) : ( - fallback.charAt(0).toUpperCase() + {isUser(maybeUserOrGroupOrChatBot) && ( + + {maybeUserOrGroupOrChatBot.firstName.charAt(0).toUpperCase()} + {maybeUserOrGroupOrChatBot.lastName?.charAt(0).toUpperCase()} + )} + {isGroup(maybeUserOrGroupOrChatBot) && + maybeUserOrGroupOrChatBot.memberEmails?.length} + {isChatBot(maybeUserOrGroupOrChatBot) && ( + 🤖 /* TODO replace with actual bot SVG */ + )} + {!maybeUserOrGroupOrChatBot && fallback.charAt(0).toUpperCase()} ); diff --git a/client/src/itemDisplay.tsx b/client/src/itemDisplay.tsx index e9603f6f..2dc5f25a 100644 --- a/client/src/itemDisplay.tsx +++ b/client/src/itemDisplay.tsx @@ -123,7 +123,7 @@ export const ItemDisplay = ({ {isDifferentUserFromPreviousItem && ( diff --git a/client/src/itemInputBox.tsx b/client/src/itemInputBox.tsx index 2b908492..6e0eb40f 100644 --- a/client/src/itemInputBox.tsx +++ b/client/src/itemInputBox.tsx @@ -4,7 +4,7 @@ import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete"; import { PayloadAndType } from "./types/PayloadAndType"; import { palette, space } from "@guardian/source-foundations"; import { PayloadDisplay } from "./payloadDisplay"; -import { Group, User } from "shared/graphql/graphql"; +import { ChatBot, Group, User } from "shared/graphql/graphql"; import { AvatarRoundel } from "./avatarRoundel"; import { agateSans } from "../fontNormaliser"; import { scrollbarsCss } from "./styling"; @@ -12,14 +12,17 @@ import { composer } from "../colours"; import { LazyQueryHookOptions, useApolloClient } from "@apollo/client"; import { gqlSearchMentionableUsers } from "../gql"; import { SvgSpinner } from "@guardian/source-react-components"; -import { isGroup, isUser } from "shared/graphql/extraTypes"; -import { groupToMentionHandle, userToMentionHandle } from "./mentionsUtil"; +import { isChatBot, isGroup, isUser } from "shared/graphql/extraTypes"; +import { + groupOrChatBotToMentionHandle, + userToMentionHandle, +} from "./mentionsUtil"; import { useTourProgress } from "./tour/tourState"; import { PINBOARD_TELEMETRY_TYPE, TelemetryContext } from "./types/Telemetry"; interface WithEntity { entity: E & { - heading?: string; + maybeHeading?: string; }; } @@ -40,10 +43,10 @@ const LoadingSuggestions = () => ( ); const Suggestion = ({ - entity: { heading, ...userOrGroup }, -}: WithEntity) => ( + entity: { maybeHeading, ...userOrGroupOrChatBot }, +}: WithEntity) => (
- {heading && ( + {maybeHeading && (
e.stopPropagation()} > - {heading} + {maybeHeading}
)}
@@ -84,16 +97,20 @@ const Suggestion = ({ }), }} > - {isUser(userOrGroup) - ? `${userOrGroup.firstName} ${userOrGroup.lastName}` - : userOrGroup.shorthand} + {isUser(userOrGroupOrChatBot) + ? `${userOrGroupOrChatBot.firstName} ${userOrGroupOrChatBot.lastName}` + : userOrGroupOrChatBot.shorthand}
- {isUser(userOrGroup) ? userOrGroup.email : userOrGroup.name} + {isUser(userOrGroupOrChatBot) + ? userOrGroupOrChatBot.email + : isGroup(userOrGroupOrChatBot) + ? userOrGroupOrChatBot.name + : userOrGroupOrChatBot.description}
@@ -125,7 +142,7 @@ interface ItemInputBoxProps { message: string; setMessage: (newMessage: string) => void; sendItem?: () => void; - addUnverifiedMention?: (userOrGroup: User | Group) => void; + addUnverifiedMention?: (userOrGroupOrChatBot: User | Group | ChatBot) => void; panelElement: HTMLElement | null; isSending: boolean; asGridPayload: ( @@ -175,16 +192,20 @@ export const ItemInputBox = ({ .then( ({ data: { - searchMentionableUsers: { users, groups }, + searchMentionableUsers: { users, groups, chatBots }, }, }) => [ ...users.map((user: User, index: number) => ({ ...user, - heading: index === 0 ? "INDIVIDUALS" : undefined, + maybeHeading: index === 0 ? "INDIVIDUALS" : undefined, })), ...groups.map((group: Group, index: number) => ({ ...group, - heading: index === 0 ? "GROUPS" : undefined, + maybeHeading: index === 0 ? "GROUPS" : undefined, + })), + ...chatBots.map((chatBot: ChatBot, index: number) => ({ + ...chatBot, + maybeHeading: index === 0 ? "CHAT BOTS" : undefined, })), ] ); @@ -260,7 +281,7 @@ export const ItemInputBox = ({ `} > {maybeReplyingToElement} - + innerRef={(element) => (textAreaRef.current = element)} disabled={isSending} trigger={{ @@ -269,13 +290,13 @@ export const ItemInputBox = ({ ? mentionsDataProvider : () => [], component: Suggestion, - output: (userOrGroup) => ({ - key: isGroup(userOrGroup) - ? userOrGroup.shorthand - : userOrGroup.email, - text: isGroup(userOrGroup) - ? groupToMentionHandle(userOrGroup) - : userToMentionHandle(userOrGroup), + output: (userOrGroupOrChatBot) => ({ + key: isUser(userOrGroupOrChatBot) + ? userOrGroupOrChatBot.email + : userOrGroupOrChatBot.shorthand, + text: isUser(userOrGroupOrChatBot) + ? userToMentionHandle(userOrGroupOrChatBot) + : groupOrChatBotToMentionHandle(userOrGroupOrChatBot), caretPosition: "next", }), allowWhitespace: true, diff --git a/client/src/mentionsUtil.tsx b/client/src/mentionsUtil.tsx index 7ba9e8c8..6ccb6692 100644 --- a/client/src/mentionsUtil.tsx +++ b/client/src/mentionsUtil.tsx @@ -1,12 +1,19 @@ import { css } from "@emotion/react"; import { composer } from "../colours"; import React, { Fragment } from "react"; -import { Group, MentionHandle, User } from "../../shared/graphql/graphql"; +import { + ChatBot, + Group, + MentionHandle, + User, +} from "../../shared/graphql/graphql"; export const userToMentionHandle = (user: User) => `@${user.firstName} ${user.lastName}`; -export const groupToMentionHandle = (group: Group) => `@${group.shorthand}`; +export const groupOrChatBotToMentionHandle = ( + groupOrChatBot: Group | ChatBot +) => `@${groupOrChatBot.shorthand}`; const meMentionedCSS = (unread: boolean | undefined) => css` color: white; diff --git a/client/src/nestedItemDisplay.tsx b/client/src/nestedItemDisplay.tsx index 13362a23..729cd0dc 100644 --- a/client/src/nestedItemDisplay.tsx +++ b/client/src/nestedItemDisplay.tsx @@ -90,7 +90,7 @@ export const NestedItemDisplay = ({ `} > diff --git a/client/src/seenBy.tsx b/client/src/seenBy.tsx index 531801d4..b059635c 100644 --- a/client/src/seenBy.tsx +++ b/client/src/seenBy.tsx @@ -60,7 +60,7 @@ export const SeenBy = ({ seenBy, userLookup }: SeenByProps) => { `} > diff --git a/client/src/sendMessageArea.tsx b/client/src/sendMessageArea.tsx index 51ed7e17..1aeafe75 100644 --- a/client/src/sendMessageArea.tsx +++ b/client/src/sendMessageArea.tsx @@ -2,7 +2,13 @@ import { ApolloError, useLazyQuery, useMutation } from "@apollo/client"; import { css } from "@emotion/react"; import { palette, space } from "@guardian/source-foundations"; import React, { useContext, useState } from "react"; -import { CreateItemInput, Group, Item, User } from "shared/graphql/graphql"; +import { + ChatBot, + CreateItemInput, + Group, + Item, + User, +} from "shared/graphql/graphql"; import { gqlAsGridPayload, gqlCreateItem } from "../gql"; import { ItemInputBox } from "./itemInputBox"; import { PayloadAndType } from "./types/PayloadAndType"; @@ -12,9 +18,12 @@ import SendArrow from "../icons/send.svg"; import { buttonBackground } from "./styling"; import { PINBOARD_TELEMETRY_TYPE, TelemetryContext } from "./types/Telemetry"; import { SvgSpinner } from "@guardian/source-react-components"; -import { isGroup, isUser } from "shared/graphql/extraTypes"; +import { isChatBot, isGroup, isUser } from "shared/graphql/extraTypes"; import { useConfirmModal } from "./modal"; -import { groupToMentionHandle, userToMentionHandle } from "./mentionsUtil"; +import { + groupOrChatBotToMentionHandle, + userToMentionHandle, +} from "./mentionsUtil"; import { useTourProgress } from "./tour/tourState"; import { demoPinboardData } from "./tour/tourConstants"; @@ -48,10 +57,10 @@ export const SendMessageArea = ({ }: SendMessageAreaProps) => { const [message, setMessage] = useState(""); const [unverifiedMentions, setUnverifiedMentions] = useState< - Array + Array >([]); - const addUnverifiedMention = (userOrGroup: User | Group) => - setUnverifiedMentions((prevState) => [...prevState, userOrGroup]); // TODO: also make user unique in list + const addUnverifiedMention = (userOrGroupOrChatBot: User | Group | ChatBot) => + setUnverifiedMentions((prevState) => [...prevState, userOrGroupOrChatBot]); // TODO: also make user unique in list const verifiedIndividualMentionEmails = Array.from( new Set( @@ -66,11 +75,24 @@ export const SendMessageArea = ({ new Set( unverifiedMentions .filter(isGroup) - .filter((group) => message.includes(groupToMentionHandle(group))) + .filter((group) => + message.includes(groupOrChatBotToMentionHandle(group)) + ) .map((group) => group.shorthand) ) ); + const verifiedChatBotMentionShorthands = Array.from( + new Set( + unverifiedMentions + .filter(isChatBot) + .filter((chatBot) => + message.includes(groupOrChatBotToMentionHandle(chatBot)) + ) + .map((chatBot) => chatBot.shorthand) + ) + ); + const sendTelemetryEvent = useContext(TelemetryContext); const [_sendItem, { loading: isItemSending }] = useMutation<{ @@ -137,6 +159,7 @@ export const SendMessageArea = ({ pinboardId, mentions: verifiedIndividualMentionEmails, groupMentions: verifiedGroupMentionShorthands, + chatBotMentions: verifiedChatBotMentionShorthands, claimable, relatedItemId: maybeReplyingToItemId, } satisfies CreateItemInput, diff --git a/client/src/tour/tourMessageReplies.ts b/client/src/tour/tourMessageReplies.ts index 1e67dcd6..c049f638 100644 --- a/client/src/tour/tourMessageReplies.ts +++ b/client/src/tour/tourMessageReplies.ts @@ -22,6 +22,7 @@ const buildMessageItem = ( userEmail: user.email, groupMentions: null, mentions: null, + chatBotMentions: null, payload: null, relatedItemId: null, deletedAt: null, diff --git a/client/src/tour/tourState.tsx b/client/src/tour/tourState.tsx index c4049870..49f235db 100644 --- a/client/src/tour/tourState.tsx +++ b/client/src/tour/tourState.tsx @@ -303,6 +303,7 @@ export const TourStateProvider: React.FC = ({ children }) => { isMe: false, })), groupMentions: [], //TODO - map variables.input.groupMentions to mention handle, + chatBotMentions: [], //TODO - map variables.input.chatBotMentions to mention handle, claimable: variables.input.claimable || false, }; setSuccessfulSends((prevSuccessfulSends) => [ diff --git a/database-bridge-lambda/src/sql/Item.ts b/database-bridge-lambda/src/sql/Item.ts index efe977a7..3e86153f 100644 --- a/database-bridge-lambda/src/sql/Item.ts +++ b/database-bridge-lambda/src/sql/Item.ts @@ -6,6 +6,7 @@ import { } from "../../../shared/graphql/graphql"; import { Sql } from "../../../shared/database/types"; import { Range } from "../../../shared/types/grafanaType"; +import fetch from "node-fetch"; const fragmentIndividualMentionsToMentionHandles = ( sql: Sql, @@ -58,12 +59,52 @@ export const createItem = async ( sql: Sql, args: { input: CreateItemInput }, userEmail: string -) => - sql` +) => { + const newItem = await sql` INSERT INTO "Item" ${sql({ userEmail, ...args.input })} RETURNING ${fragmentItemFields(sql, userEmail)} `.then((rows) => rows[0]); + // FIXME move out to dedicated lambda (invoked from DB trigger similar to email-lambda & notifications-lambda) + if (args.input.chatBotMentions && args.input.chatBotMentions.length > 0) { + const allItemsInThisPinboard = await listItems( + sql, + { pinboardId: args.input.pinboardId }, + userEmail + ); + + await Promise.allSettled( + args.input.chatBotMentions.map(async (chatBotShorthand) => { + const { url, headerKey, headerValue } = { + url: "", + headerKey: "", + headerValue: "", + }; //TODO lookup using chatBotShorthand + return fetch(url, { + method: "POST", + headers: { + [headerKey]: headerValue, + "Content-Type": "application/json", + }, + body: JSON.stringify( + { + callbackToken: "", //TODO implement single-use callback token mechanism + callbackUrl: `https://pinboard.${"domain"}/api/replyToItem/${ + newItem.pinboardId + }/${newItem.id}`, //FIXME replace domain + item: newItem, + allItemsInThisPinboard: allItemsInThisPinboard, + }, + null, + 2 + ), + }); + }) + ); + } + + return newItem; +}; export const editItem = async ( sql: Sql, args: { itemId: string; input: EditItemInput }, diff --git a/database-bridge-lambda/src/sql/User.ts b/database-bridge-lambda/src/sql/User.ts index a9699abd..4eaa3bbe 100644 --- a/database-bridge-lambda/src/sql/User.ts +++ b/database-bridge-lambda/src/sql/User.ts @@ -6,19 +6,19 @@ const fragmentUserWithoutPushSubscriptionSecrets = (sql: Sql) => export const searchMentionableUsers = async ( sql: Sql, args: { prefix: string } -) => ({ - users: await sql` - SELECT ${fragmentUserWithoutPushSubscriptionSecrets(sql)} - FROM "User" - WHERE "isMentionable" = true AND ( - "firstName" ILIKE ${args.prefix + "%"} - OR "lastName" ILIKE ${args.prefix + "%"} - OR CONCAT("firstName", ' ', "lastName") ILIKE ${args.prefix + "%"} - ) - ORDER BY "webPushSubscription" IS NOT NULL DESC, "manuallyOpenedPinboardIds" IS NOT NULL DESC, "firstName" - LIMIT 5 - `, - groups: await sql` +) => { + const usersPromise = sql` + SELECT ${fragmentUserWithoutPushSubscriptionSecrets(sql)} + FROM "User" + WHERE "isMentionable" = true AND ( + "firstName" ILIKE ${args.prefix + "%"} + OR "lastName" ILIKE ${args.prefix + "%"} + OR CONCAT("firstName", ' ', "lastName") ILIKE ${args.prefix + "%"} + ) + ORDER BY "webPushSubscription" IS NOT NULL DESC, "manuallyOpenedPinboardIds" IS NOT NULL DESC, "firstName" + LIMIT 5 + `; + const groupsPromise = sql` SELECT "shorthand", "name", COALESCE(( SELECT json_agg("email") FROM "User", "GroupMember" @@ -36,8 +36,26 @@ export const searchMentionableUsers = async ( ) ORDER BY "name" LIMIT 3 - `, -}); + `; + const chatBotsPromise = Promise.resolve( + [ + //TODO list all the chatBots from paramstore in playground account??? + { + shorthand: "chatGuPT", + description: "sample chat bot", + }, + { + shorthand: "HelloWorld", + description: "replies to hello with world", + }, + ].filter((chatBot) => chatBot.shorthand.includes(args.prefix)) + ); + return { + users: await usersPromise, + groups: await groupsPromise, + chatBots: await chatBotsPromise, + }; +}; export const getUsers = (sql: Sql, args: { emails: string[] }) => sql` diff --git a/notifications-lambda/run.ts b/notifications-lambda/run.ts index 98ad2dc2..1ebad0b7 100644 --- a/notifications-lambda/run.ts +++ b/notifications-lambda/run.ts @@ -30,6 +30,7 @@ import { Item } from "shared/graphql/graphql"; payload: null, mentions: [], groupMentions: [], + chatBotMentions: [], userEmail: "tom.richards@guardian.co.uk", id: "535b86e2-4f01-4f60-a2d0-a5e4f5a7d312", message: "testing one two three", diff --git a/shared/database/local/runDatabaseSetup.ts b/shared/database/local/runDatabaseSetup.ts index d4ffe63b..cad1c743 100644 --- a/shared/database/local/runDatabaseSetup.ts +++ b/shared/database/local/runDatabaseSetup.ts @@ -85,6 +85,8 @@ const runSetupTriggerSqlFile = ( getEmailLambdaFunctionName(stage), EMAIL_DATABASE_TRIGGER_NAME ), + "add chatBotMentions column to Item table": () => + runSetupSqlFile(sql, "020-AddChatBotMentionsColumnToItemTable.sql"), }; const allSteps = async () => { diff --git a/shared/database/local/setup/020-AddChatBotMentionsColumnToItemTable.sql b/shared/database/local/setup/020-AddChatBotMentionsColumnToItemTable.sql new file mode 100644 index 00000000..0531b43c --- /dev/null +++ b/shared/database/local/setup/020-AddChatBotMentionsColumnToItemTable.sql @@ -0,0 +1,2 @@ +ALTER TABLE "Item" + ADD COLUMN "chatBotMentions" VARCHAR(128)[]; diff --git a/shared/graphql/extraTypes.ts b/shared/graphql/extraTypes.ts index 56bc6b9b..b5827408 100644 --- a/shared/graphql/extraTypes.ts +++ b/shared/graphql/extraTypes.ts @@ -1,4 +1,5 @@ import type { + ChatBot, Group, PinboardIdWithClaimCounts, User, @@ -28,8 +29,22 @@ export const isPinboardDataWithClaimCounts = ( ): pinboardData is PinboardDataWithClaimCounts => "unclaimedCount" in pinboardData; -export const isGroup = (userOrGroup: User | Group): userOrGroup is Group => - "shorthand" in userOrGroup; +export const isGroup = ( + userOrGroupOrChatBot: User | Group | ChatBot | undefined +): userOrGroupOrChatBot is Group => + !!userOrGroupOrChatBot && "memberEmails" in userOrGroupOrChatBot; -export const isUser = (userOrGroup: User | Group): userOrGroup is User => - !isGroup(userOrGroup); +export const isUser = ( + userOrGroupOrChatBot: User | Group | ChatBot | undefined +): userOrGroupOrChatBot is User => + !!userOrGroupOrChatBot && "email" in userOrGroupOrChatBot; + +export const isChatBot = ( + userOrGroupOrChatBot: User | Group | ChatBot | undefined +): userOrGroupOrChatBot is ChatBot => + !!userOrGroupOrChatBot && "description" in userOrGroupOrChatBot; + +export const hasAvatarUrl = ( + userOrGroupOrChatBot: User | Group | ChatBot | undefined +): userOrGroupOrChatBot is User | ChatBot => + !!userOrGroupOrChatBot && "avatarUrl" in userOrGroupOrChatBot; diff --git a/shared/graphql/schema.graphql b/shared/graphql/schema.graphql index cb1d8029..bf3b3bb2 100644 --- a/shared/graphql/schema.graphql +++ b/shared/graphql/schema.graphql @@ -9,7 +9,7 @@ type Query { listItems(pinboardId: String!): [Item] listLastItemSeenByUsers(pinboardId: String!): [LastItemSeenByUser] getMyUser: MyUser - searchMentionableUsers(prefix: String!): UsersAndGroups + searchMentionableUsers(prefix: String!): UsersGroupsAndChatBots getUsers(emails: [String!]!): [User] getGroupPinboardIds: [PinboardIdWithClaimCounts!]! getItemCounts(pinboardIds: [String!]!): [PinboardIdWithItemCounts!]! @@ -67,6 +67,7 @@ type Item { pinboardId: String! mentions: [MentionHandle!] groupMentions: [MentionHandle!] + chatBotMentions: [String!] claimedByEmail: String claimable: Boolean! relatedItemId: String @@ -104,9 +105,16 @@ type Group { memberEmails: [String!]! } -type UsersAndGroups { +type ChatBot { + shorthand: String! + description: String! + avatarUrl: String +} + +type UsersGroupsAndChatBots { users: [User!]! groups: [Group!]! + chatBots: [ChatBot!]! } type WorkflowStub { @@ -144,6 +152,7 @@ input CreateItemInput { pinboardId: String! mentions: [String!] groupMentions: [String!] + chatBotMentions: [String!] claimable: Boolean relatedItemId: String }