Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(next): mention users in slack on ticket created #958

Merged
merged 2 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 41 additions & 16 deletions next/api/src/integration/slack/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,24 @@ class SlackIntegration {
});
}

async getUserId(email: string): Promise<string> {
async getUserId(email: string) {
const id = this.userIdMap.get(email);
if (id) {
return id;
}

const { user } = await this.client.users.lookupByEmail({ email });
if (!user || !user.id) {
throw new Error('Slack API returns an invalid user');
try {
const { user } = await this.client.users.lookupByEmail({ email });
if (!user || !user.id) {
throw new Error('Slack API returns an invalid user');
}
this.userIdMap.set(email, user.id);
return user.id;
} catch (error: any) {
if (error.data.error !== 'users_not_found') {
throw error;
}
}
this.userIdMap.set(email, user.id);
return user.id;
}

async getChannelId(userId: string): Promise<string> {
Expand Down Expand Up @@ -117,16 +123,10 @@ class SlackIntegration {
}

async sendToUser(email: string, message: Message) {
let userId: string;
try {
userId = await this.getUserId(email);
} catch (error: any) {
if (error.data.error === 'users_not_found') {
return;
}
throw error;
const userId = await this.getUserId(email);
if (!userId) {
return;
}

const channelId = await this.getChannelId(userId);
return this.send(channelId, message);
}
Expand All @@ -146,11 +146,36 @@ class SlackIntegration {
});
}

sendNewTicket = ({ ticket, from, to }: NewTicketContext) => {
async getCategoryMentionUserIds(categoryId: string) {
const category = await categoryService.findOne(categoryId);
if (!category || !category.meta) {
return;
}

const emails = category.meta.slackNewTicketMentionUserEmails as string[];
if (!emails) {
return;
}

const userIds: string[] = [];
for (const email of emails) {
const id = await this.getUserId(email);
if (id) {
userIds.push(id);
}
}
return userIds;
}

sendNewTicket = async ({ ticket, from, to }: NewTicketContext) => {
const message = new NewTicketMessage(ticket, from, to);
if (to?.email) {
this.sendToUser(to.email, message);
}
const userIds = await this.getCategoryMentionUserIds(ticket.categoryId);
if (userIds?.length) {
message.setMentions(userIds);
}
this.broadcast(message, ticket.categoryId);
};

Expand Down
12 changes: 11 additions & 1 deletion next/api/src/integration/slack/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,24 @@ export class Message {

protected color?: string;

protected mentions?: string[];

constructor(readonly summary: string, content: string) {
if (content.length > 1000) {
content = content.slice(0, 1000) + '...';
}
this.content = content;
}

setMentions(mentions: string[]) {
this.mentions = mentions;
}

toJSON() {
let text = this.summary;
if (this.mentions?.length) {
text += '\n' + this.mentions.map((id) => `<@${id}>`).join(' ');
}
const blocks = [
{
type: 'section',
Expand All @@ -25,7 +35,7 @@ export class Message {
},
];
return {
text: this.summary,
text,
attachments: [{ color: this.color, blocks }],
};
}
Expand Down
32 changes: 27 additions & 5 deletions next/web/src/App/Admin/Settings/Categories/CategoryForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,31 @@ const FORM_ITEM_STYLE = { marginBottom: 16 };

const CategoryMetaOptions: MetaOptionsGroup<CategorySchema>[] = [
{
label: 'AI 分类',
key: 'slack',
label: 'Slack 新工单通知用户邮箱',
children: [
{
key: 'slackNewTicketMentionUserEmails',
type: 'custom',
render: ({ value, onChange }) => (
<Form.Item
extra="当前分类的工单被创建时,将在指定频道 @ 上述用户"
style={{ marginBottom: 0 }}
>
<TextArea
placeholder="每行一个"
autoSize={{ minRows: 2 }}
value={value?.join('\n')}
onChange={(e) => onChange(e.target.value.split('\n'))}
/>
</Form.Item>
),
},
],
},
{
key: 'classify',
label: 'AI 分类',
children: [
{
key: 'enableAIClassify',
Expand All @@ -64,8 +87,8 @@ const CategoryMetaOptions: MetaOptionsGroup<CategorySchema>[] = [
},
{
key: 'previewAIClassify',
type: 'component',
component: <AiClassifyTest />,
type: 'custom',
render: () => <AiClassifyTest />,
predicate: (v) => !!v.alias,
},
],
Expand Down Expand Up @@ -563,8 +586,7 @@ export function CategoryForm({
control={control}
name="meta"
render={({ field: { ref, ...rest } }) => (
console.log(getValues()),
(<MetaField {...rest} options={CategoryMetaOptions} record={getValues()} />)
<MetaField {...rest} options={CategoryMetaOptions} record={getValues()} />
)}
/>

Expand Down
11 changes: 7 additions & 4 deletions next/web/src/App/Admin/components/MetaField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ export type MetaOption<T = any> = (
label: string;
}
| {
type: 'component';
component: ReactNode;
type: 'custom';
render: (options: { value: any; onChange: (value: any) => void }) => ReactNode;
}
) & { key: string; predicate?: (data: T) => boolean; description?: string };

Expand Down Expand Up @@ -53,8 +53,11 @@ const MetaOptionsForm = <T extends SchemaWithMeta>({
.filter(({ predicate }) => !record || !predicate || predicate(record))
.map((option) => (
<Form.Item key={option.key} help={option.description} className="!mb-0">
{option.type === 'component' ? (
option.component
{option.type === 'custom' ? (
option.render({
value: value?.[option.key],
onChange: handleFieldChangeFactory(option.key, (v) => v),
})
) : (
<div>
{option.type === 'boolean' ? (
Expand Down