Skip to content

Commit

Permalink
feat(slack): adjust message format after testing (#2112)
Browse files Browse the repository at this point in the history
  • Loading branch information
capJavert authored Aug 7, 2024
1 parent c9e27a7 commit 6e163a2
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 27 deletions.
16 changes: 16 additions & 0 deletions __tests__/integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
13 changes: 12 additions & 1 deletion __tests__/workers/postAddedSlackChannelSend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. <http://localhost:5002/posts/p1?utm_source=notification&utm_medium=slack&utm_campaign=new_post|http://localhost:5002/posts/p1>',
unfurl_links: false,
});
});

Expand Down
104 changes: 104 additions & 0 deletions src/common/userIntegration.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -33,3 +47,93 @@ export const getIntegrationToken = async <
throw new Error('unsupported integration type');
}
};

export const getSlackClient = async ({
integration,
}: {
integration: UserIntegration;
}): Promise<WebClient> => {
return new WebClient(await getIntegrationToken({ integration }), {
logLevel: isProd ? LogLevel.ERROR : LogLevel.WARN,
});
};

export const getAttachmentForPostType = async <TPostType extends PostType>({
con,
post,
postType,
postLink,
}: {
con: DataSource;
post: Post;
postType: TPostType;
postLink: string;
}): Promise<MessageAttachment> => {
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;
};
50 changes: 31 additions & 19 deletions src/schema/integrations.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -20,6 +19,7 @@ import { ConflictError } from '../errors';
import graphorm from '../graphorm';
import {
UserIntegration,
UserIntegrationSlack,
UserIntegrationType,
} from '../entity/UserIntegration';
import {
Expand All @@ -33,6 +33,7 @@ import {
GQLDatePageGeneratorConfig,
queryPaginatedByDate,
} from '../common/datePageGenerator';
import { ConversationsInfoResponse } from '@slack/web-api';

export type GQLSlackChannels = {
id?: string;
Expand Down Expand Up @@ -204,9 +205,9 @@ export const resolvers: IResolvers<unknown, BaseContext> = 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({
Expand Down Expand Up @@ -315,12 +316,17 @@ export const resolvers: IResolvers<unknown, BaseContext> = 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)
Expand All @@ -332,15 +338,21 @@ export const resolvers: IResolvers<unknown, BaseContext> = 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');
}

Expand All @@ -354,7 +366,7 @@ export const resolvers: IResolvers<unknown, BaseContext> = traceResolvers({
['userIntegrationId', 'sourceId'],
);

if (!channelResult.channel.is_member) {
if (!channelResult.channel?.is_member) {
await client.conversations.join({
channel: args.channelId,
});
Expand All @@ -364,11 +376,11 @@ export const resolvers: IResolvers<unknown, BaseContext> = 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 🙌`,
});
}

Expand Down
41 changes: 34 additions & 7 deletions src/workers/postAddedSlackChannelSend.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -21,6 +24,7 @@ export const postAddedSlackChannelSendWorker: TypedWorker<'api.v1.post-visible'>
},
relations: {
source: true,
author: true,
},
}),
con.getRepository(UserSourceIntegrationSlack).find({
Expand All @@ -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 ({
Expand All @@ -48,19 +52,42 @@ 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
await slackClient.conversations.join({
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;
Expand Down

0 comments on commit 6e163a2

Please sign in to comment.