From 6e163a2140eeeab2f5299ff4e1a4c44aaa014a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ante=20Bari=C4=87?= Date: Wed, 7 Aug 2024 20:43:01 +0200 Subject: [PATCH] feat(slack): adjust message format after testing (#2112) --- __tests__/integrations.ts | 16 +++ .../workers/postAddedSlackChannelSend.ts | 13 ++- src/common/userIntegration.ts | 104 ++++++++++++++++++ src/schema/integrations.ts | 50 +++++---- src/workers/postAddedSlackChannelSend.ts | 41 +++++-- 5 files changed, 197 insertions(+), 27 deletions(-) diff --git a/__tests__/integrations.ts b/__tests__/integrations.ts index 01e463d01..b94059840 100644 --- a/__tests__/integrations.ts +++ b/__tests__/integrations.ts @@ -453,6 +453,22 @@ describe('slack integration', () => { expect(resUpdate.errors).toBeFalsy(); expect(slackPostMessage).toHaveBeenCalledTimes(1); }); + + it('should return error if integration is not found', async () => { + loggedUser = '1'; + + await testMutationErrorCode( + client, + { + mutation: MUTATION({ + integrationId: '4a51defd-a083-4967-82a8-edb009d57d05', + channelId: '1', + sourceId: 'squadslack', + }), + }, + 'NOT_FOUND', + ); + }); }); describe('query sourceIntegration', () => { diff --git a/__tests__/workers/postAddedSlackChannelSend.ts b/__tests__/workers/postAddedSlackChannelSend.ts index 39be122ee..eb65d8190 100644 --- a/__tests__/workers/postAddedSlackChannelSend.ts +++ b/__tests__/workers/postAddedSlackChannelSend.ts @@ -103,7 +103,18 @@ describe('postAddedSlackChannelSend worker', () => { }); expect(chatPostMessage).toHaveBeenCalledWith({ channel: '1', - text: expect.any(String), + attachments: [ + { + author_icon: 'https://app.daily.dev/apple-touch-icon.png', + author_name: 'daily.dev', + image_url: 'https://daily.dev/image.jpg', + title: 'P1', + title_link: + 'http://localhost:5002/posts/p1?utm_source=notification&utm_medium=slack&utm_campaign=new_post', + }, + ], + text: 'New post on "A" source. ', + unfurl_links: false, }); }); diff --git a/src/common/userIntegration.ts b/src/common/userIntegration.ts index 7a7f69fe0..12c1f3038 100644 --- a/src/common/userIntegration.ts +++ b/src/common/userIntegration.ts @@ -1,9 +1,23 @@ +import { LogLevel, MessageAttachment, WebClient } from '@slack/web-api'; +import { DataSource } from 'typeorm'; + import { UserIntegration, UserIntegrationSlack, UserIntegrationType, } from '../entity/UserIntegration'; import { decrypt } from './crypto'; +import { isProd } from './utils'; +import { + PostType, + Post, + ArticlePost, + CollectionPost, + YouTubePost, + FreeformPost, + WelcomePost, + SharePost, +} from '../entity/posts'; export type GQLUserIntegration = { id: string; @@ -33,3 +47,93 @@ export const getIntegrationToken = async < throw new Error('unsupported integration type'); } }; + +export const getSlackClient = async ({ + integration, +}: { + integration: UserIntegration; +}): Promise => { + return new WebClient(await getIntegrationToken({ integration }), { + logLevel: isProd ? LogLevel.ERROR : LogLevel.WARN, + }); +}; + +export const getAttachmentForPostType = async ({ + con, + post, + postType, + postLink, +}: { + con: DataSource; + post: Post; + postType: TPostType; + postLink: string; +}): Promise => { + const attachment: MessageAttachment = { + author_name: 'daily.dev', + author_icon: 'https://app.daily.dev/apple-touch-icon.png', + }; + + switch (postType) { + case PostType.Article: + case PostType.Collection: + case PostType.VideoYouTube: { + const articlePost = post as ArticlePost & CollectionPost & YouTubePost; + + if (articlePost.title) { + attachment.title = articlePost.title; + attachment.title_link = postLink; + } + + if (articlePost.summary) { + attachment.text = articlePost.summary; + } + + if (articlePost.image) { + attachment.image_url = articlePost.image; + } + + break; + } + case PostType.Freeform: + case PostType.Welcome: { + const freeformPost = post as FreeformPost & WelcomePost; + + attachment.title = freeformPost.title; + attachment.title_link = postLink; + + if (freeformPost.image) { + attachment.image_url = freeformPost.image; + } + + break; + } + case PostType.Share: { + const sharePost = post as SharePost; + let title = sharePost.title; + + const sharedPost = (await con.getRepository(Post).findOneBy({ + id: sharePost.sharedPostId, + })) as ArticlePost; + + if (!title) { + title = sharedPost?.title; + } + + if (sharedPost?.image) { + attachment.image_url = sharedPost.image; + } + + if (title) { + attachment.title = title; + attachment.title_link = postLink; + } + + break; + } + default: + throw new Error(`unsupported post type ${postType}`); + } + + return attachment; +}; diff --git a/src/schema/integrations.ts b/src/schema/integrations.ts index def2bb8f8..78a589e57 100644 --- a/src/schema/integrations.ts +++ b/src/schema/integrations.ts @@ -1,10 +1,9 @@ import { IResolvers } from '@graphql-tools/utils'; import { AuthContext, BaseContext } from '../Context'; import { traceResolvers } from './trace'; -import { WebClient } from '@slack/web-api'; import { logger } from '../logger'; import { - getIntegrationToken, + getSlackClient, getLimit, getSlackIntegrationOrFail, GQLUserIntegration, @@ -20,6 +19,7 @@ import { ConflictError } from '../errors'; import graphorm from '../graphorm'; import { UserIntegration, + UserIntegrationSlack, UserIntegrationType, } from '../entity/UserIntegration'; import { @@ -33,6 +33,7 @@ import { GQLDatePageGeneratorConfig, queryPaginatedByDate, } from '../common/datePageGenerator'; +import { ConversationsInfoResponse } from '@slack/web-api'; export type GQLSlackChannels = { id?: string; @@ -204,9 +205,9 @@ export const resolvers: IResolvers = traceResolvers({ con: ctx.con, }); - const client = new WebClient( - await getIntegrationToken({ integration: slackIntegration }), - ); + const client = await getSlackClient({ + integration: slackIntegration, + }); const result = await client.conversations.list({ limit: getLimit({ @@ -315,12 +316,17 @@ export const resolvers: IResolvers = traceResolvers({ ); const [slackIntegration] = await Promise.all([ - getSlackIntegrationOrFail({ - id: args.integrationId, - userId: ctx.userId, - con: ctx.con, + ctx.con.getRepository(UserIntegrationSlack).findOneOrFail({ + where: { + id: args.integrationId, + userId: ctx.userId, + }, + relations: { + user: true, + }, }), ]); + const user = await slackIntegration.user; const existing = await ctx.con .getRepository(UserSourceIntegrationSlack) @@ -332,15 +338,21 @@ export const resolvers: IResolvers = traceResolvers({ throw new ConflictError('source already connected to a channel'); } - const client = new WebClient( - await getIntegrationToken({ integration: slackIntegration }), - ); - - const channelResult = await client.conversations.info({ - channel: args.channelId, + const client = await getSlackClient({ + integration: slackIntegration, }); - if (!channelResult.ok && channelResult.channel.id !== args.channelId) { + let channelResult: ConversationsInfoResponse | undefined; + + try { + channelResult = await client.conversations.info({ + channel: args.channelId, + }); + + if (!channelResult.ok) { + throw new Error(channelResult.error); + } + } catch { throw new ValidationError('invalid channel'); } @@ -354,7 +366,7 @@ export const resolvers: IResolvers = traceResolvers({ ['userIntegrationId', 'sourceId'], ); - if (!channelResult.channel.is_member) { + if (!channelResult.channel?.is_member) { await client.conversations.join({ channel: args.channelId, }); @@ -364,11 +376,11 @@ export const resolvers: IResolvers = traceResolvers({ if (channelChanged) { const sourceTypeName = - source.type === SourceType.Squad ? 'squad' : 'source'; + source.type === SourceType.Squad ? 'Squad' : 'source'; await client.chat.postMessage({ channel: args.channelId, - text: `You've successfully connected the "${source.name}" ${sourceTypeName} from daily.dev to this channel. Important ${sourceTypeName} updates will be posted here 🙌`, + text: `${user.name || user.username} successfully connected "${source.name}" ${sourceTypeName} from daily.dev to this channel. Important updates from this ${sourceTypeName} will be posted here 🙌`, }); } diff --git a/src/workers/postAddedSlackChannelSend.ts b/src/workers/postAddedSlackChannelSend.ts index 3a501d53b..554c34f5c 100644 --- a/src/workers/postAddedSlackChannelSend.ts +++ b/src/workers/postAddedSlackChannelSend.ts @@ -1,9 +1,12 @@ -import { WebClient } from '@slack/web-api'; import { UserSourceIntegrationSlack } from '../entity/UserSourceIntegration'; import { TypedWorker } from './worker'; import fastq from 'fastq'; import { Post, SourceType } from '../entity'; -import { getIntegrationToken } from '../common'; +import { + getAttachmentForPostType, + getSlackClient, +} from '../common/userIntegration'; +import { addNotificationUtm } from '../common'; const sendQueueConcurrency = 10; @@ -21,6 +24,7 @@ export const postAddedSlackChannelSendWorker: TypedWorker<'api.v1.post-visible'> }, relations: { source: true, + author: true, }, }), con.getRepository(UserSourceIntegrationSlack).find({ @@ -35,7 +39,7 @@ export const postAddedSlackChannelSendWorker: TypedWorker<'api.v1.post-visible'> const source = await post.source; const sourceTypeName = - source.type === SourceType.Squad ? 'squad' : 'source'; + source.type === SourceType.Squad ? 'Squad' : 'source'; const sendQueue = fastq.promise( async ({ @@ -48,9 +52,9 @@ export const postAddedSlackChannelSendWorker: TypedWorker<'api.v1.post-visible'> const userIntegration = await integration.userIntegration; try { - const slackClient = new WebClient( - await getIntegrationToken({ integration: userIntegration }), - ); + const slackClient = await getSlackClient({ + integration: userIntegration, + }); // channel should already be joined when the integration is connected // but just in case @@ -58,9 +62,32 @@ export const postAddedSlackChannelSendWorker: TypedWorker<'api.v1.post-visible'> channel: channelId, }); + const postLinkPlain = `${process.env.COMMENTS_PREFIX}/posts/${post.id}`; + const postLink = addNotificationUtm( + postLinkPlain, + 'slack', + 'new_post', + ); + let message = `New post on "${source.name}" ${sourceTypeName}. <${postLink}|${postLinkPlain}>`; + const author = await post.author; + const authorName = author?.name || author?.username; + + if (sourceTypeName === 'Squad' && authorName) { + message = `${authorName} shared a new post on "${source.name}" ${sourceTypeName}. ${process.env.COMMENTS_PREFIX}/posts/${post.id}`; + } + + const attachment = await getAttachmentForPostType({ + con, + post, + postType: data.post.type, + postLink, + }); + await slackClient.chat.postMessage({ channel: channelId, - text: `New post added to ${sourceTypeName} "${source.name}" ${process.env.COMMENTS_PREFIX}/posts/${post.id}`, + text: message, + attachments: [attachment], + unfurl_links: false, }); } catch (originalError) { const error = originalError as Error;