Давайте представим, что есть кандидат, и у него есть несколько этапов найма (интервью с 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.

Надеюсь данная статья будет кому-либо полезна

Комментарии (0)