Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Давайте представим, что есть кандидат, и у него есть несколько этапов найма (интервью с 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.
Надеюсь данная статья будет кому-либо полезна