Найти тему
Инфо-Эксперт

Разработка приложения для Битрикс24 от А до Я. Часть 3 - реализуем настройки приложения

А вот и очередная часть нашего рассказа о разработке приложения для Битрикс24. Если вы не читали первые 2 части, рекомендуем ознакомиться для понимания картины. Вот первая часть, рассказывающая о постановке задачи и определении функций приложения, а вот вторая часть, где рассказано о том, как реализовать установку приложения.

Как и в прошлый раз приводим рассказ с сохранением стиля нашего руководителя отдела web-разработки - Вадима Солуянова.

Реализуем настройки приложения

В настройках, куда перенаправляет портал пользователя сразу после нашего подтверждения установки (помните js BX24.installFinish()), и куда пользователь может зайти также через левое меню портала, кликнув по имени приложения, мы выводим текущие значения ключей и даем возможность их установить/обновить, а также отправить тестовое СМС-сообщение. Таким образом наша точка входа (settings) должна отделять друг от друга три вида запросов:

  • запрос с портала на открытие страницы настроек,
  • запрос с самой страницы при submit-е формы сохранения ключей,
  • запрос с самой страницы для отправки тестового СМС.

Бывает, что разработчики разделяют запросы по наличию пришедших данных, которые следует обработать. Однако, я настоятельно рекомендую для каждого конкретного действия либо слать предопределенный параметр с кодом действия, например, action=send_sms, либо отдельный параметр, наличие в запросе которого указывает на необходимость совершения действия, например, send_sms=Y. Почему? Прежде всего потому, что мы должны проверить валидность переданных данных, а что проверять, если мы не знаем какое действие от нас требуют? Сейчас поясню.

Скажем, в нашем приложении можно сохранение ключей делать только тогда, когда они пришли к нам непустыми. В этом случае, если пользователь ничего не заполнит и кликнет кнопку "Сохранить", мы ничего и делать не станем, даже не скажем ему, что он забыл указать ключи. Это, конечно, неверно. Потому и нужно отделять команду от ее данных. Мы получаем в параметре action команду сохранения ключей (action=save_settings), проверяем заполнены ли они, если нет - показываем ошибку, если да, то проверяем их валидность. Еще одно замечание по поводу использования имени кнопки submit-а для хранения команды. Ее значение, кажется, отправляется браузером не во всех случаях (или мои сведения устарели). Предпочтительнее все-таки добавлять в форму скрытое поле.

Теперь об авторизации. Токены, присланные порталом, нужны нам, если требуется делать какой-то запрос на портал. В данном случае этого не требуется, и можно данный вопрос опустить, но не хочется, поскольку в других приложениях они понадобятся. Когда приходит запрос с портала, с ним все понятно, токены - прямо в запросе. Когда мы сами стучимся к себе из фронта, например, отсабмитив форму, то тут уже все зависит от того, что мы добавили в скрытые поля. Именно в скрытые, и форма должна уходить методом POST, каким приходит и запрос с портала. Почему? Прежде всего для сокрытия конфиденциальных данных. Все, что передается через параметры УРЛ, т.е. методом GET попадает в логи серверов, через которые проходит наш запрос, а данные, переданные в POST-е, не логируются. Поэтому для сохранения авторизации присланные данные, а именно DOMAIN, LANG, AUTH_ID, REFRESH_ID, ну и member_id, конечно, следует сохранить в скрытых полях.

Но что делать, если в приложении нужно обратиться к самому себе через ссылку? Мы данный вариант использовать не будем, но скажу, что в этом случае придется сохранять авторизацию текущего пользователя в базу или сессию (лучше в базу в связи с последними нововведениями работы с cookies), генерить для данной записи уникальный непредсказуемый ключ (например md5 от сохраняемых параметров) и в ссылке указывать его. Состав полей при этом будет подобен нашему хранилищу 5 (Авторизация пользователя портала) с добавлением поля HASH. Другой вариант - это js. Сейчас в native js есть метод fetch(), который может отправлять данные на бэк методом post, даже никаких js-фреймворков не нужно. Отказ же от использования js не имеет смысла, поскольку в браузере с отключенными скриптами и приложение на портале не появится, поскольку открывается Битрикс24 путем submit-а формы через javascript. И третий вариант, через css можно сделать что угодно: ссылка будет отображаться как кнопка, кнопка - как ссылка, так что любую можно оформить как форму с method="post" и кнопкой type="submit".

При обработке запроса помним о безопасности. Нужно сказать, что member_id портала узнать можно, но проблематично, поэтому, если в запросе он присутствует и есть ему соответствующая запись в хранилище 1 (Сведения о портале), то будем считать запрос валидным. Если же приложение не столь простое, как наше, и требования безопасности к нему жесткие, то сохранение авторизации пользователя потребуется однозначно, как и проверка ее валидности. В этом случае придется делать запрос на сервер авторизации, что размещается по адресу https://oauth.bitrix.info/ а полный запрос будет выглядеть https://oauth.bitrix.info/rest/app.info. В случае, если на нем нет данных о приложении или портале, получите соответствующую ошибку. Да, для того, чтобы получить данные вам потребуется послать один параметр - auth=<здесь access_token пользователя> (опять-таки методом POST). Ну, можно изобрести и другие способы проверки, но для запроса с портала подойдет именно описанный выше.

Итак, все сказано, больные места обговорены, можно бы и составить уже схему работы настроек приложения.

Схема 4. Страница настроек приложения в псевдокоде:

errors = []

showForm = true

action = getRequest('action')

memberId = getRequest('member_id')

if not isMemberIdValid(memberId)

errors[] = 'Not valid request'

showForm = false

else

logCurrentRequest()

switch action

case 'save_settings':

saveSettings()

loadCurrentSettings()

break

case 'send_sms':

res = sendSms()

answerJson(res)

exit

break

default:

loadCurrentSettings()

break

if errors

errorMessage = createUserMessage()

logErrors()

logCurrentSettings()

includeTemplate()

if errorMessage

showErrorMessage()

if showForm

showSettingsForm()

if currentSettings

showTestForm()

Помним схему 1, где мы подробно расписывали действия при установке приложения, и понимаем, что многие из действий логировались в базу. Данному коду требуются пояснения. Во-первых, переменная showForm - в ней мы сохраняем флаг, стоит ли показывать во фронте форму или же запрос был совсем левый. Затем answerJson и exit - в данном случае к нам приходит запрос с фронта посредством вызова из javascript fetch() и нам не нужно продолжать выполнение, подключая шаблон. Требуется лишь вернуть результат отправки СМС в виде json объекта и выйти. Далее loadCurrentSettings - они по всей видимости должны передать в шаблон не только текущие настройки, но также URL для атрибута action формы настроек и формы отправки тестового SMS, а также скрытые поля, которые по меньшей мере содержат member_id, а в случае озабоченного безопасностью приложения еще и hash сохраненных в базе авторизационных данных, ну.., или сами данные в скрытых полях.

Что касается createUserMessage(). Здесь мы отказались от запроса на портал, иначе бы добавились те же варианты, что и при установке. В данном случае ошибки все возникают либо при невалидном запросе, либо при сохранении настроек в базу, либо в ответе SMS-провайдера. Все их необходимо проанализировать и выдать пользователю понятное сообщение. Пожалуй, стоит подробнее расписать вызов saveSettings().

Итак, сохранение настроек. В первую очередь мы проверяем присланные данные. Если чего-то не заполнено, сразу добавляем ошибку, но не обрываем выполнение. Очень неприятно как пользователю, когда ошибки всплывают одна за другой, а не все сразу, поэтому лучше проверить все данные и выдать пользователю информацию обо всех полях, где он ввел что-то неподходящее. Если все корректно, делаем запрос на получение информации об аккаунте (сейчас сами данные нас мало волнуют, важно, чтобы пришли). Если что-то не так с ключами, к нам возвратится ошибка. Ее мы показываем пользователю. Это, если API провайдера возвращает что-то человекочитаемое, а в противном случае формируем сообщение сами по коду ошибки. Если все Ок, и данные получены, сохраняем их в хранилище 1 (Сведения о портале). Вспоминаем, что нам достаточно одних авторизационных данных.

Схема 5. Итак, распишем в виде превдокода saveSettings():

if not apiLogin

errors[] = 'API Login is empty'

if not apiKey

errors[] = 'API Key is empty'

if errors

return

res = getAccountDetail(apiLogin, apiKey)

if not res

code = getHttpCode()

switch code

case 401:

errors[] = 'Wrong credetials'

break

//... other vars

default:

if !res

errors[] = 'SMS Service currently unavailable'

else

errors[] = getResponseError(res)

break

else

saveApiKeys()

Надо признаться, что в псевдокоде я сохраняю ошибки без разбора на то, какие они. Нужно ли их выводить непосредственно пользователю или же формировать на их основе более человечное сообщение. В реалии можно использовать разные классы ошибок и в массив добавлять их экземпляры. Таким образом (ох, я все-таки влез в архитектуру), мы задействуем полиморфизм ООП. Например, мы создаем такие классы:

  • OurServerTemporaryError - выводит, что временная ошибка;
  • OuterServerTemporaryError - выводит, что временная ошибка;
  • OuterServerWrongCredetialsError - выводит, что указанные ключи невалидны;
  • OuterServerError - выводит то, что вернул СМС-провайдер.

Затем, в методе createUserMessage нам достаточно будет вызывать метод getErrorMessage() и экземпляр соответствующего класса сам вернет то, что требуется пользователю.

И вижу, что в код закралась одна неприятная ошибка: в ветке default в наличии глупая проверка. Если посмотреть по коду выше, станет понятно, что вариант else здесь никогда не отработает. С другой стороны, сама строка в else выполняет вполне нужное действие. Вероятно, она просто находится не там. Поправим это:

Схема 6. saveSettings() в виде превдокода:

if not apiLogin

errors[] = 'API Login is empty'

if not apiKey

errors[] = 'API Key is empty'

if errors

return

res = getAccountDetail(apiLogin, apiKey)

if not res

code = getHttpCode()

switch code

case 401:

errors[] = 'Wrong credetials'

break

//... other vars

default:

errors[] = 'SMS Service currently unavailable'

break

else

if isResponseError(res)

errors[] = getResponseError(res)

else

saveApiKeys()

По отправке тестовых СМС, думаю, не стоит давать столь же подробные описания, поскольку они схожи с сохранением настроек. С одной оговоркой, ошибки там могут быть и другие. Например, недостаточно средств на счете или номер, на который отправлено сообщение, невалидный, или пользователю не позволяется отправлять международные СМС-сообщения. Эти все случаи, конечно, должны быть обработаны. Хорошо, когда в ошибке, возвращенной провайдером, имеется человекопонятное описание, но, если нет, придется его формировать самому.

Ну, с настройками и отправкой тестового СМС покончено. Думаю всем очевидно, что происходит во фронте на данной странице? Есть две формы. Одна с настройками, другая - с полями phone и message для тестового СМС. Первая отображается на странице всегда, если только сам запрос не был совсем левым. Вторая всплывает popup-ом при клике на кнопку Test (если сохраненная авторизация валидна, конечно). В обеих формах хранится в скрытом поле member_id, а при паранойе и hash или все авторизационные данные, присланные порталом. Первая форма отправляется обычным submit-ом, вторая - через js и функцию fetch(). Обе - методом POST. Да, и, конечно, на странице есть область, в которой выводятся ошибки. Даже две: одна где-то возле формы настроек, другая в popup-е возле формы отправки тестового SMS.

Итак, готово. Думаю, следующие точки входа принесут новые проблемы и изменения в уже сложившейся схеме. Но, как говорится, проект становится понятным лишь только после его завершения.

Обработка команд на отправку СМС (handler)

-2

Наш handler может быть вызван из разных мест Битрикс24. Это может быть срабатывание автоматизации при изменении статуса сделки, или менеджер, находясь в карточке контакта, захочет отправить ему SMS, в любом случае, если в качестве провайдера он выберет наше приложение, то в итоге портал постучится к нам. Запрос будет содержать среди прочего такие данные:

  • type => SMS
  • code => код нашего приложения, точнее зарегистрированного провайдера
  • message_id => буквы, цифры, идентифицирующие данное сообщение на портале
  • message_to => +11112243111
  • message_body => Hello my friend. I'm going to tell you that something goes wrong...

Ой, ой, ой... Кажется, при описании логики сохранения авторизации в настройках я совсем забыл о регистрации провайдера на портале. Так что пока менеджер не сможет выбрать наше приложение. Исправим это. Не будем повторять весь псевдокод saveSettings(), опишем лишь то место, где валидная авторизация сохраняется в базу. Вот как это выглядело:

if isResponseError(res)

errors[] = getResponseError(res)

else

saveApiKeys()

Добавим регистрацию SMS-провайдера в случае успешного сохранения ключей и отсутствия предыдущей регистрации.

if isResponseError(res)

errors[] = getResponseError(res)

else

oldKeys = getOldKeys()

res = saveApiKeys()

if res.success()

if empty oldKeys

res = registerProvider()

if not res.success()

errors[] = res.getError()

else

errors[] = res.getError()

Ну, вот теперь можно возвращаться к handler-у и, прежде всего, вспомнить про безопасность. Запрос на отправку SMS во многом подобен запросу на обработку события Битрикс24, а события в нем имеют один параметр, специально предназначенный для проверки валидности запроса. Параметр этот содержится в массиве под ключом auth, в котором лежит следующее:

  • access_token = ...
  • expires_in = 3600
  • scope = crm,im,user,department,bizproc
  • domain = ...
  • status = F
  • client_endpoint = .../rest/
  • member_id = ...
  • user_id = 8
  • application_token = ...

Последний параметр (application_token) - это и есть тот ключ, по которому мы сможем защитить приложение от левых запросов. И тут выявляется необходимость подправить наш список событий, на которые мы подписывались в момент установки, а также подправить наше хранилище 1 (Сведения о портале).

Для проверки нам потребуется сравнить присланное значение application_token с тем, что мы сохранили ранее у себя. А как и в какой момент мы получаем это значение для его сохранения? Сделать это можно, подписавшись на событие Битрикс24 OnAppInstall. Именно при этом событии к нам впервые приходит application_token. Конечно, он приходит и при любом другом событии, и нет уж такой необходимости подписываться на еще одно. Можно делать дополнительную проверку на валидность запроса в тот момент, когда у нас сохраненное значение отсутствует, запоминать при успешной проверке, а затем уже сравнивать присланный токен с тем, что у нас есть. Но, мне кажется, что проще подписаться, делать проверку именно при событии OnAppInstall, а во всех других событиях проверять по одной схеме, а именно - сравнивая присланное с сохраненным. Стало быть, дополним наш список действий:

18. Обработать OnAppInstall. Проверить валидность запроса через сервер авторизации, при успехе сохранить присланный application_token в хранилище 1 (Сведения о портале).

И добавим поле в само хранилище:

APP_TOKEN - токен для проверки валидности событий и запросов на отправку SMS

Теперь еще немного о самом запросе на отправку. Запрос, как вы видели приходит сразу с авторизацией, среди которой указан и ID пользователя. Ну, когда менеджер жмакает по кнопке SMS в карточке клиента, тогда понятно, что за пользователь к нам придет. Но если запрос приходит при срабатывании автоматизации, когда, например, сделка меняет свой статус? В этом случае к нам придет ID ответственного за сделку. Почему я начал говорить об этом? Дело в том, что ошибки при отправке очень даже возможны, но handler не содержит пользовательского интерфейса, в котором мы могли бы их вывести. Что же делать? Для этого в Битрикс24 существует возможность отправки пользователю нотификаций, и есть соответствующий REST-метод. Метод этот доступен тем приложениям, которые запросили доступ (scope) im. И выходит, что я допустил еще одну промашку, забыв упомянуть о доступах приложения. Чтобы не отвлекаться сейчас от handler-а, давайте обсудим этот вопрос чуть позже, когда закончим с отправкой SMS.

Схема 7. Отправка SMS в псевдокоде:

errors = []

auth = getRequest('auth)

if not isMemberIdValid(auth.member_id)

exit

if not isAppTokenValid(auth.application_token)

exit

logCurrentRequest()

messageId = getRequest('message_id')

messageTo = getRequest('message_to')

messageBody = getRequest('message_body')

if empty messageTo or empty messageBody

errors[] = 'Empty required data'

if not errors

res = saveUserAuth(auth)

if res.success()

errors[] = res.getErrorMessage()

if not errors

res = sendSMS(messageTo, messageBody)

if res.success()

saveRes = saveMessage(auth.user_id, messageId, res)

if not saveRes.success()

errors[] = saveRes.getErrorMessage()

if errors

logErrors()

message = getErrorMessage(errors)

sendNotification(auth.user_id, message)

Поскольку в данном месте отсутствует пользовательский интерфейс вообще, нет смысла выводить какое-либо сообщение (или, тем более, отсылать его) при невалидном запросе, поэтому мы лишь вызываем exit (прекратить дальнейшее выполнение).

saveUserAuth() - здесь мы впервые запоминаем авторизацию пользователя, поскольку она нам понадобится в дальнейшем при смене статуса сообщения. Если записи у нас нет, добавляем, если есть, обновляем токены.

saveMessage() - сохраняем у себя идентификатор сообщения на портале, оный же у SMS-провайдера, запоминаем текущий статус сообщения (например, queued - поставлено в очередь), пользователя и прочие служебные поля типа домена, идентификатора портала и т.п.

sendNotification() - отправляем нотификацию об ошибке тому пользователю, к которому привязано сообщение. Это либо менеджер, отправляющий СМС из карточки контакта, либо ответственный, напр., за сделку, чей статус поменялся, в результате чего сработала автоматизация.

Пожалуй, на сегодня достаточно. Последнюю часть этой занимательной инструкции опубликуем на следующей неделе, а вы пока что подпишитесь на нас в социальных сетях, чтобы не пропускать интересные статьи и обзоры.