SlackBot на минималках для работы с кандидатами

от автора

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/590077/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *