Давайте представим, что есть кандидат, и у него есть несколько этапов найма (интервью с hr, техническое интервью, согласование с руководством и тд.). По некоторым этапам HR сотруднику приходилось руками передавать информацию по кандидату в разные чаты, что неудобно и требовало время и внимание HR. Поэтому появилась идея это автоматизировать.
Найм сотрудников у этой компании ведется через систему HuntFlow. Сотрудники компании общаются друг с другом через Slack. Поэтому два этих сервиса нужно как-то подружить. У HuntFlow есть апи хуков, в частности нас интересует вебхук на изменения по кандиату. Когда у кандидата меняется статус найма мы должны отправить сообщение в нужный нам канал slack с инфой по кандидату. Например если у сотрудника статус изменился на “Тех интервью” и он, например, IOS разработчик, то сообщение должно упасть в чат IOS, с текстом “Кандидату {имя} {фамилия} нужно провести тех. интервью {ссылка на него в huntflow}”. Если кандидат Android разработчик, то такое сообщение должно упасть в чат Andriod и тд. думаю идея понятна.
Веб сервер
Для обработки сообщений от хука HuntFlow нам нужен веб сервер. Так как я являюсь фронтенд разработчиком я взял Nest фреймворк. Теперь нам нужно написать эндпоинт, который будет принимать сообщение от HuntFlow.
@Controller('slack-hooks') export class SlackHooksController { constructor( private slackHooksService: SlackHooksService, private http: HttpService, ) {} @Post('applicant-changed') async applicantChangedHook(@Body() body: unknown, @Res() response: Response) { /** * когда мы добавляем хук в huntflow для проверки его работоспособности отправляется post запрос * а для токго что бы хук прицепился, на этот запрос нужно ответить статусом 200 */ if (checkIsHookWork(body)) { return response.status(HttpStatus.OK).send('ok'); } if (!checkIsApplicantChangedData(body)) { return response.status(HttpStatus.BAD_REQUEST).send('fail'); } await this.slackHooksService.onApplicantChanged(body); return response.status(HttpStatus.OK).send('ok'); } }
Отлично! теперь у нас есть эндпоинт по обработки всех сообщений от HuntFlow по изменению кандидата. Теперь нам нужно понять, что изменился именно статус и что он входит в список нужных нам статусов, по которым нужно отправить сообщение в чат.
Отправка сообщений в Slack
Начну с того что отправить сообщение можно двумя способами, через slack вебхук или через slack api. У каждого из способов есть свои плюсы и свои минусы. Для себя, я выбрал способ через slack api, так как мне нужно отправлять сообщение в несколько чатов, если бы я выбрал вебхук, на каждый из каналов пришлось бы создавать новый хук. Минус у отправки через апи, нужно добавить приложение в чат, что для нас было совсем не критично.
Давайте снова попишем код:
// тут мы юзаем бибилотеку slack '@slack/web-api' private webClient = new WebClient(this.configService.get('slackBotToken')); private sendMessageToChannel( channel: string, data: HuntflowApplicantChangedData, messageType: MessagesType, ) { return lastValueFrom( this.huntflowService // в HuntFlow можно настроить кастомные поля по кандидату, тут мы их и получаем .loadApplicantQuestionary(data.event.applicant.id) .pipe( map((response) => { // Генерируем сообщение, так как сообщение много, решил вынести их в фабрику и генерить их по типу сообщения return MessageFactory.from(data.event, response.data) .generate(messageType) .toMessage(); }), ), ).then((message) => { // тут через библиотеку slack отправляем в канал сообщение return this.webClient.chat.postMessage({ channel, text: message, }); }); }
Теперь давайте рассмотрим как генерятся сообщения, как я уже писал выше, я генерю разные сообщения под конкретный статус. Мы же рассмотрим генерацию сообщения на примере статуса “Согласование с руководством”, так как в нем больше всего инфы про кандидата.
// Тут я наследуюсь от MessageHelper // Некоторые методы представленные представленные ниже находятся в нем export class CoordManagementCandidateMessageHelper extends MessageHelper { static from( event: HuntflowApplicantChangedData['event'], questionary: BSLApplicantQuestionary, ) { return new CoordManagementCandidateMessageHelper(event, questionary); } constructor( event: HuntflowApplicantChangedData['event'], private questionary: BSLApplicantQuestionary, ) { super(event); } public toMessage() { const { applicant, vacancy } = this.event; const { location, grade, technique, employee_date, work_format } = this.questionary || {}; // здесь я генерю ссылку на кандидата используя разметку slack const nameStr = this.getFullNameWithUrl(applicant); const money = applicant?.money; const vacancyName = vacancy?.position || ''; // у меня были проблемы с разметкой, поэтому решил сообщение разбить на массив строк и джоинить их через \n. const rows = [ // <!chanel> тегает всех в чате `<!channel> Привет!`, `${nameStr} прошел все этапы.`, `Вакансия: ${vacancyName}`, `З/п: ${this.generateFieldMessage(money)}`, `Будем выставлять оффер?`, '', `Локация: ${this.generateFieldMessage(location)}`, `Грейд: ${this.generateFieldMessage(grade)}`, `Техника: ${this.generateFieldMessage(technique)}`, `Дата выхода: ${this.generateFieldMessage(employee_date)}`, `Формат работы: ${this.generateFieldMessage(work_format)}`, ]; return rows.join('\n'); } protected getFullName(applicant?: Applicant) { const firstName = applicant?.first_name || ''; const lastName = applicant?.last_name || ''; const middleName = applicant?.middle_name || ''; return `${firstName} ${lastName} ${middleName}`; } protected getFullNameWithUrl(applicant?: Applicant) { const url = this.generateCandidateUrl(); const nameStr = this.getFullName(applicant); // url = ссылке, nameStr текст ссылки return `<${url}|${nameStr}>`; } protected generateCandidateUrl() { const { applicant, vacancy } = this.event || {}; return `https://…`; } }
Мы собрали сообщение и теперь осталось его отправлять когда происходит нужное нам событие в HuntFlow.
export class SlackHooksService { private get channels() { return this.configService.get<ChannelConfig>('channels'); } onApplicantChanged(data: HuntflowApplicantChangedData) { const statusId = data?.event?.status?.id; // нам нужны только события по смене статуса if (data.event.type !== 'STATUS' || !statusId) { return; } // на каждый статус отправляется свое сообщение if (statusId === VacancyStatusesIds.CustomerCoordination) { return this.sendMessageByCustomerCoordination(data); } if (statusId === VacancyStatusesIds.ManagementCoordination) { return this.sendMessageToChannel( this.channels.CoordManagement, data, MessagesTypes.CoordManagementCandidate, ); } if (statusId === VacancyStatusesIds.OfferAccepted) { return this.sendMessageByOfferAccepted(data); } if (statusId === VacancyStatusesIds.WentToWork) { return this.sendMessageToChannel( this.channels.HRDevops, data, MessagesTypes.DevopsCandidateWentToWork, ); } } }
Вот и все, у нас есть работающий slack бот, который реагирует на смену статуса в HuntFlow и отправляет сообщения в нужные каналы.
Заключение
В заключение хотелось бы сказать, что таким простым ботом вы облегчите, и так не легкую, работу своим HR.
Надеюсь данная статья будет кому-либо полезна
