Русский
Русский
English
Статистика
Реклама

Voximplant

Как бросить нелюбимую работу и прочно войти в АЙТИ

01.05.2021 12:14:42 | Автор: admin
На этой неделе в наших соцсетях выступил Николай Сладкий, один из создателей SamaraITCommunity.

Николай, Senior Serverless JavaScript Developer в Voximplant обладает восьмилетним опытом работы с бек- и фронтендом, влюблен *nix- подобные системы, vim, чистый код, архитектурные подходы и open source, как лучшее, что случилось с человечеством. Он один из создателей SamaraITCommunity (сильно причастен к появлению GrindConf, VolgaHack, подкастов SITCast и SITalk), а также он входит в команду авторов проекта Джуность.

Делимся с вами расшифровкой эфира и записью.



Меня зовут Коля, фамилия моя Сладкий. Мне 29 лет, я разработчик, работаю сейчас на позиции Senior Serverless JavaScript Developer в компании Voximplant, занимаюсь движком Voxengine это serverless-платформа. Я занимаюсь разработкой уже около 7.5 лет, почти 8; мои основные инструменты на данный момент языки JavaScript и Go. То есть, на JavaScript мы делаем serverless, а на Go, когда нужно, докидываем перфоманса.

Несколько слов о себе. Я занимался и backend, и frontend, то есть, я full stack web developer, хотя больше backend-based. Как я уже говорил, это почти 8 лет опыта. Я являюсь devops culture addict-ом, то есть, я пропагандирую, что нужно везде докидывать operations, куда только можно, чтобы максимально автоматизировать процессы. Являюсь технологическим евангелистом, в частности, я очень сильно топлю за NodeJS ну, тоже в каких-то разумных пределах. NodeJS это очень классная технология. Ещё я занимался архитектурой, построением web-приложений, работал тимлидом. Работал техническим директором в двух компаниях в стартапе и в более крупной компании, которая называется Mechanica (раньше она называлась ANMEDIO).

Много занимаюсь разной общественно-полезной деятельностью я думаю, это можно так назвать. Есть независимое сообщество разработчиков Самарской области Samara IT Community, мы его когда-то сделали вместе с другими ребятами, теперь оно активно развивается без особого участия с нашей стороны (мы больше модерируем). В рамках нашего комьюнити мы делали интересные мероприятия, у нас была конференция GrindConf, несколько раз мы делали хакатон VolgaHack. Еще мы записываем подкасты SITalk и SITCast. Также я стейкхолдер и один из создателей проекта Джуность.

Это проект для обучения, и он так называется потому, что его главная идея в том, чтобы всегда как бы быть джуном и жить в погоне за новыми знаниями, за развитием. Еще я влюблен в *nix-подобные системы (так исторически сложилось), очень люблю vim, топлю за чистый код, чистую архитектуру, чтобы все было понятно, чтобы все легко читалось. Я топлю за open source, у меня есть много всего интересного по open source, я сам пишу open source меня можно найти в GitHub. Вообще, я считаю, что open source это одна из лучших вещей, что случились за все время существования IT-технологий.

Почему я рассказываю про войти в IT? Наверно, должна быть какая-то подоплека. На самом деле, она есть: дело в том, что я два года своей жизни проработал в полиции, а именно в уголовном розыске. И в 2014 году я начал изучать программирование это было сложно, странно, но я это сделал и поэтому могу вам что-то рассказать об этом. Через 7 месяцев после того, как я начал изучать программирование с нуля, я нашел свою первую IT-работу. Это не была работа супер-программистом: было что-то вроде вебмастера (поддержка сайта, SEO, контекст). С тех пор прошло уже почти 8 лет, я занимаюсь любимым делом, и всем советую. Я прошел тот самый путь, который предстоит пройти каждому человеку, который хочет войти в IT. Конечно, этот путь не будет 1 в 1, и конкретно мой опыт может быть нерелевантен для другого человека. Однако, скорее всего, я знаю многие из подводных камней, с которыми предстоит столкнуться, и на эту тему мы поговорим, порассуждаем и сделаем выводы. Возможно, я сегодня кому-то смогу помочь. Если хотя бы одному человеку это будет полезно уже день не зря пройдет.

image

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

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

Итак, поговорим немного про IT. Есть такая интересная фраза даже не помню, где я ее слышал: У вас должна быть очень серьезная причина, чтобы в 2021 году работать не в IT. Это шутка, конечно, но в ней есть большая доля правды. Уже в 2020 году а сейчас уже 2021 год IT было везде. То есть, IT проникает во все сферы. Уже нельзя сказать, что IT это какие-то отдельные технологии, сейчас это образ ведения бизнеса. Бизнес меняется до неузнаваемости. И для его правильно функционирования, роста и развития ему приходится уходить в сферу IT.

Мы прошли с вами через серьезное испытание, которое для нас еще даже не закончилось, а для многих стран оно вообще только начинается. Но, тем не менее, мы сейчас уже более комфортно чувствуем себя в этих условиях. Возьмите это же время год назад март, апрель; помните, что происходило, когда объявляли локдауны и все уходили на удалёнку? Было сложно, но, в основном, все сферы побеждают это. Даже сейчас мы с вами сидим я сижу в одном месте, вы смотрите меня, возможно, из другого города или страны и делаем это с помощью интернета, с помощью той самой IT-сферы. И нам это удобно. Получается, что так же мы можем работать. Люди созваниваются через Vox, через другие сервисы так можно коммуницировать друг с другом, общаться, работать, зарабатывать деньги. Сфера здоровья точно так же сейчас уходит в IT, и вообще весь госсектор, все госуслуги это делают. Есть тот же самый сайт госуслуг, где сейчас можно, зарегистрировавшись и подтвердив аккаунт, записываться к врачу, например, а с мая можно будет даже перерегистрировать автомобиль при продаже; то есть, кучу всего можно делать. Доставка опять же, на локдауне мы все это очень здорово почувствовали, и сейчас продолжаем пользоваться. Это очень здорово, очень удобно, когда не нужно выходить из дома, чтобы купить еду, купить другие вещи, сделать что-то еще мы можем полностью удаленно это делать. Эквайринг, удаленный эквайринг или обычный безналичный расчет как часто мы с вами сейчас пользуемся обычными физическими деньгами? Они уже просто не имеют такого смысла, какой в них вкладывался раньше. Транспорт, вся e-commerce, все гаджеты, которые мы носим в кармане это все тоже IT. То есть, все сферы сейчас завязаны на IT, а IT на всех сферах. Мы с вами уже непосредственно работаем в сферах, которые не могут без IT либо в сферах, без которых само IT не может обойтись. Это замыкается рекурсивно на том, что все сферы не могут без IT.

И все это произошло, в первую очередь, благодаря развитию интернета. Многие люди не задумываются о том, насколько интернет мощное оружие, насколько это классная и крутая вещь в современном мире. Это далеко не только Facebook, Instagram и TikTok; люди обычно просто не думают об этом, но, на самом деле, интернет сейчас это в первую очередь работа. Все вышеперечисленные сферы, про которые мы с вами говорили, сводятся в итоге к рынку. А рынок существует для того, чтобы зарабатывать деньги. То есть, интернет это наше главное оружие для того, чтобы зарабатывать деньги. И для того, чтобы делать вещи, нужен только ноутбук, выход в сеть и какие-то скиллы. Получается, что запросы бизнеса уходят в сеть: тот бизнес, которого раньше не было в интернете ему там, допустим, нечего было делать в 2021 году должен там быть. Если его нет в интернете, то, скорее всего, его самого уже нет, или не будет он постепенно деградирует и умрет.

Из этого можно сделать простой вывод: если у нас все больше появляется сфер, все больше появляется работы в интернете и вообще в IT, то ее кто-то должен делать. И мы постоянно слышим о том особенно в последние несколько лет что миру нужны программисты. Иногда кто-то утверждает об обратном, но мы-то с вами знаем правду. Миру нужны программисты, миру нужны IT-шники. И если на фоне спроса рождается предложение, то на фоне потребности бизнеса рождаются рабочие места. И эту работу кто-то должен делать. И, если эту работу кто-то должен делать, то о ней постоянно говорят, мы слышим об этом из каждого утюга, и люди начинают интересоваться. То есть, сфера разогревается, молекулы этой сферы двигаются быстрее, и люди уже начинают интересоваться извне им становится интересно. Многие люди хотят что-то изменить в своей жизни в основном, в погоне за чем-то интересным, или за деньгами но все равно этому способствует то, что мы постоянно об этом слышим, потому что сфера сильно прогрета. Люди хотят что-то изменить в своей жизни и начинают хотеть в IT. Или, как минимум, узнать об этом побольше.

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

Давайте представим абстрактного персонажа Петю, который хочет войти в IT. Пусть он будет примерно нашего с вами возраста. Важная оговорка: возраст не слишком важен, и, если вам 35-40-50 лет, я не думаю, что вас это остановит. Конечно, будет чуть сложнее по ряду причин, но это не будет невозможно. Итак, Петя нашего с нами возраста, он так же пришел сюда, чтобы узнать для себя что-то новое, и, конечно, он хочет войти в IT. Он хочет стать программистом, потому что это популярно сейчас и он будет обязательно программистом, это его путь, он его выбрал для себя. Что он знает о программировании? Он знает, что это популярно. Потому что работы действительно много, отрицать это глупо; у нас есть вакансии, есть Headhunter, где мы можем их посмотреть, мы постоянно слышим новости, мы приходим в бар и слышим, как программисты общаются друг с другом о странных вещах. То есть, это действительно популярно. И мы знаем, что это хорошо оплачивается. По крайней мере, про космические зарплаты сейчас, опять же, из каждого утюга вещают, мы не можем пройти мимо этого об этом громко говорят громкими словами.

Еще Петя постоянно натыкается на рекламу курсов. Я думаю, так у всех сейчас мы постоянно натыкаемся на рекламу курсов, где из нас обещают сделать программиста за 3 месяца (недели, дня, часа). И Петя задумывается о том, чтобы попробовать. А почему нет он молод (ну, или не очень) и полон сил, он может совмещать текущую работу с обучением, если постараться, и он не совсем тупой. Тут без обид, но мы должны прекрасно понимать: тут нужно будет думать головой. Думать надо везде, конечно, но в IT чуть-чуть побольше, наверно. У Пети есть какое-то образование; это необязательно, но, скорее всего, если у тебя есть образование, то оно научило тебя учиться, и ты знаешь, что это такое. То есть, ты готов дальше самостоятельно обучаться. И еще Петя умеет в компьютеры ну, потому что это все-таки IT, а в 2021 году надо уметь в компьютеры хоть как-то. Хоть серфить Интернет. Есть еще нюансы; ну, Петя понимает, что он не самый умный, хотя и не самый тупой (опять же, без обид), и у него нет профильного образования, хотя бы технического; с техническим образованием будет проще, конечно, а с профильным вообще другая история. Но у Пети этого нет. И, самое главное, он боится, переживает; это довольно тяжело взять и сменить профессию. Но он решает попробовать.

image

И сейчас Петя будет задавать вопросы, а мы будем стараться ответить (это те самые вопросы, которые были в анонсе):
Q: Когда стоит менять профессию?
Очевидно когда ты ненавидишь свою работу. Если ты каждый день ходишь на работу, которую ненавидишь не будешь же ты туда ходить всю жизнь. Нужно что-то менять. Еще если ты не готов всю жизнь заниматься тем, чем занимаешься сейчас. Потому что, опять же, сложно просто прожить всю жизнь; это что получается родился, потерпел и умер? Это странно.

Если он готов к тому, что это будет очень трудно, и нужно будет потратить много времени. Будем честными программистам платят деньги, но платят не за просто так. Это сложно. Это не невозможно; это не rocket science мы не запускаем ракеты в космос (ну, большая часть из нас не запускает), но это потребует очень много усидчивости, времени и сил.
И если ты готов рискнуть своим временем. Потому что, опять же, почему бы и нет. Ты просто принимаешь это решение для себя, это твоя ставка.

Но не стоит этого делать, если ты просто устал, если тебе нужно отдохнуть, чтобы дальше заниматься своим делом. Если ты психуешь, что-то у тебя идет не так, но при этом ты любишь (или хотя бы не ненавидишь) свою работу и готов всю жизнь ею заниматься, то, наверно, лучше не рыпаться. Это будет более правильным решением. Не нужно менять профессию, просто поверив в сказку. Про 300 тысяч в месяц конечно, это есть, это все правда, мы видим эти зарплатные вилки. Но для того, чтобы зарабатывать деньги, нужно делать дела. Легко не будет. Если ты не любишь трудности лучше пройди мимо. Программисты всегда превозмогают что-то, это не так просто, как хотелось бы. Если тебе нравится решать сложные задачи это профессия для тебя, а если нет нужно еще подумать, стоит ли оно того.

Q: Стоит ли идти в IT?
Стоит, если ты понимаешь, что это будет очень сложно. Поначалу будет сложно, потом еще сложнее, потом еще сложнее, потом начнет немного отпускать, когда ты уже начнешь понимать, как что-то делать, и разбираться, как что работает.

Тебе нужно идти в IT, если ты технически подкован и ты не боишься этой вашей математики. Про математику мы еще поговорим позже, но, в целом, нужно понимать, что математика здесь присутствует. Не очень сложная, но, тем не менее, без нее никак не получится.
Если ты готов тратить все свободное время на обучение. Если ты учился в университете, ты знаешь, как проходят эти 5 лет. А если ты учился на заочном, ты ещё и прекрасно понимаешь, что такое учиться самостоятельно, когда тебе дают только материалы и показывают роадмап для продвижения. Здесь будет то же самое: ты будешь тратить очень много времени на обучение. Если тебе хочется тусить, то, наверно, это история не для тебя. Но если бы готов вкладывать свое время в обучение, то действуй.

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

Это не займет две недели. Сделаем из вас программиста за две недели это все сказки. Если ты не хочешь разбираться в математике, если не любишь думать, если родился для того, чтобы отдыхать это точно не для тебя. Если хочешь тусить тоже; по крайней мере, первое время придется постоянно вкалывать, чтобы когда-нибудь потом тусить. И если ты не любишь или не можешь долго сидеть за одной задачей, то это тоже не для тебя. Это такая история, где нужно долго сидеть и много думать.

Q: Хорошо. Ты решаешь стать программистом, тебя пока ничто не пугает. Какое направление выбрать?
Вот здесь начинается интересное. Что мы знаем про направления в программировании? Есть web frontend это HTML, CSS, JavaScript и иже с ними; построены популярные фреймворки React, Vue, Angular. Это более простой путь; не сказать, что все совсем просто бывают разные задачи по степени сложности и в рамках одной профессии.

Мы сейчас будем идти сверху вниз, и каждое следующее будет сложнее по моему собственному мнению. После frontend у нас идет backend именно web-based backend. Это NodeJS, PHP, Go, это всякие Express, Laravel и прочие фреймворки, построенные на этих технологиях. Также у нас есть enterprise backend это Java, C#, то есть, то, про что все слышали. Здесь есть Spring, .NET технологии, на которых строят большие корпоративные приложения, большие распределенные сложные системы. Это банковская сфера и так далее.
Дальше у нас есть mobile development разработка под мобильные устройства. Она сейчас на пике популярности, потому что есть Kotlin, Swift, Dart это три языка, которые больше всего под мобильную разработку заточены. Здесь прямо волна хайпа, однако это довольно сложно.
Также есть направление data science, machine learning работа с данными, обучение нейросетей. Это Python как язык программирования, платформы на нем всякие GPT3, TensorFlow и прочие интересные решения от серьезных игроков, мировых корпораций, которые делают машинное обучение. Это сложно, но очень интересно.

Направление геймдева разработки компьютерных игр считается самым классным и кайфовым, конечно. Я прямо по себе помню когда-то я думал, что буду разрабатывать игры, и это будет классно. Была такая идея. Но, когда я начал этим заниматься, я понял, что это совсем не весело. Если ты сам любишь игры скорее всего, тебе совсем не понравится, что под капотом в разработке компьютерных игр. Языки программирования скорее всего, самые хайповые сейчас C# и C++, потому что на C# построен игровой движок Unity, а на C++ Unreal Engine и множество разных физических кишков, хотя используются не только эти языки и платформы.

Сложное направление, на которое нужно учиться и учиться, потому что здесь нужна хорошая математика это алгоритмы и структуры данных. Именно разработка и оптимизация: всякие карты, геолоки и прочие штуки, где надо по графам считать кратчайшие пути, например.
Есть еще более сложные вещи например, сферы, связанные со сложным перфомансом, со сложными расчетами. Какие-нибудь кишки крипты или просто блокчейнов, которые будут написаны на специализированных языках программирования, например, Solidity (высокий уровень) или Rusty (низкий уровень), но это уже сложно.

Я бы советовал думать больше про первые 3-4 направления. Может быть, ещё data science на Python. То есть, web-контент, web backend, enterprise backend, mobile development плюс data science / machine learning.

Q: Что там с зарплатами на рынке, деньги платят?
Деньги платят, да. Зарплаты есть. Но я не буду приводить цифры. Во-первых, они просто гуглятся: реально, достаточно зайти на headhunter. Иногда работодатель скрывает зарплатную вилку, но среднюю температуру по больнице определить просто из 150-200 вакансий будет 15-20 с открытой зарплатной вилкой, можно посчитать среднее и определить, сколько денег платят тому или иному специалисту в той или иной области. Во-вторых, они сильно разнятся по регионам если я буду рассказывать про московские зарплаты, а вы сами из солнечного города-курорта Самары, то получится несоответствие. Но, опять же пандемия, удаленка, все это вот. Можно и московскую зарплату найти удаленно, и работать удаленно же на заграничную фирму это тоже не проблема. Все зависит от скиллов, от своих умений, от умения себя продавать, что тоже немаловажно. Умение проходить собеседования, подать себя и повысить где-то свою зарплатную вилку это тоже очень здорово, нужно понимать это.

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

И нужно понимать, что, как сказал дядя Бен, большая сила это большая ответственность. Чем больше тебе платят, тем больше ты должен делать, тем более сложные у тебя задачи. Или не чем больше платят, а чем более высокая позиция не обязательно все в деньгах представлять. Для кого-то деньги не первостепенны: может быть, кто-то просто хочет самоутвердиться, добиться чего-то для себя в жизни. Стать лидом, например, или руководить вообще большой компанией быть CTO или solution architect. Многие люди работают именно ради этого, а не ради денег. Я сам знаю много людей, которые работают именно по такому принципу: для них деньги это, конечно, хорошо (можно вкусно покушать, если они есть), но они не самое главное, что есть на рынке.

Q: Где учиться?
Наверно, в 2021 году это самый сложный вопрос. Казалось бы, все должно быть наоборот; когда я начинал учиться в 2014 году, не было такого большого количества информации, какое есть сейчас, ее приходилось где-то выискивать. Я даже не представляю, как в каком-нибудь 2010 году учились самоучки. Сейчас информация в избытке, но это и является главной проблемой. Ее слишком много. Как в этой информационной буре найти то, что нужно тебе, и что не просто потратит твоё время и высосет деньги на платные курсы, а будет иметь хорошее соотношение цена/качество? При этом цена здесь учитывается не только в деньгах (может быть и бесплатно), но и в затратах времени.
Я думаю, самый лучший совет, который здесь можно дать это найти знакомых, которые уже есть в этой сфере. Если у вас есть такие знакомые можно с ними пообщаться, попросить у них материалы и советы, хотя бы по тому, по чему мы уже сегодня прошлись, по тем же самым сферам и выбору будущей профессии в IT. То есть, чем вы хотите заниматься, каким программистом хотите быть frontend, backend, data science. Лучше посоветоваться с кем-то, кто в этом реально шарит, кто в сфере уже хотя бы несколько лет 3, 5, лучше больше. Вам дадут советы, дадут материалы.

В дальнейшем с этими людьми можно договориться о менторстве, или просто о code review; чтобы вы, что-то написав, показывали им, а они отвечали: чувак, здесь надо все переделать, а вот здесь хорошо, делай так, а тут чуть-чуть поправь. Тогда вы будете лучше, быстрее и качественнее обучаться. Но внимание, это очень важно не приставайте к этим людям слишком сильно. Не просите научить вас всему, не требуйте от них чего-то. Старайтесь как можно меньше тупить и не доставать их вопросами, особенно теми, которые сами чувствуете себя в состоянии нагуглить. Я знаю по себе я был тем человеком, который доставал своими вопросами. Я был и тем, которого доставали, но я всегда говорил об этом: успокойся, я не твоя мама, я не буду сидеть с тобой за тетрадками и решать твои домашние задания; я могу тебе помочь, подсказать, но ты должен делать это все сам. Поэтому мой вам совет не доставайте этих людей. Если вы не будете их доставать, если это ваш хороший товарищ и отзывчивый человек, то ему будет даже приятно с вами это обсуждать, разговаривать. Еще важный момент: когда программист, инженер кому-то что-то объясняет, он сам как бы заново это проходит и начинает в этом еще лучше разбираться. Поэтому, если вы не будете сильно доставать собеседника, он, скорее всего, сам будет хотеть общаться.

К сожалению, легко нарваться на ерунду в сети, если вам никто не помогает. Сейчас масса плохих платных предложений менторов, курсов, некачественных книг, на которые можно наткнуться и потратить время впустую. Плохих материалов, наверно, даже больше, чем хороших, поэтому и хорошо спрашивать у того, кто в теме. Хотя я бы не сказал, что тут 90 процентов грязи и только 10 годноты; наверно, соотношение чуть похуже, чем 50 на 50. Старайтесь показывать тот материал, который вы находите, своему другу, который вас менторит или общается с вами он вам даст свой фидбек на этот материал. Это будет здорово.

По поводу материалов в целом книг, вебинаров, курсов, подкастов, скринкастов и так далее нет правильного решения. Кому-то больше заходят книги, кому-то видео. Я люблю смотреть видео, я кайфую от него, я не так хорошо воспринимаю текст, как видео и звуковую информацию. Я слушаю много подкастов, и до сих пор очень много полезной информации для себя получаю из них, каждую неделю. Но кому-то подкасты совсем не заходят, кто-то не может что-то делать и одновременно слушать подкаст, например. Все индивидуально. Главное учиться фильтровать источники, а на каких они носителях дело десятое.

Q: По поводу платного контента стоит ли его юзать?
Очень важный момент: откуда берется платный контент? Его делают люди, для того, чтобы зарабатывать деньги. Они тратят на это свое время. Если люди тратят на это свое время это первый фильтр. Если человек квалифицированный, то, скорее всего, там есть какая-то компоновка, фильтрация информации. Это всегда хорошо. Допустим, как мы учимся в университете на заочке: мы получаем источники, те же самые книги, статьи, нам говорят, откуда мы можем получить информацию, и мы идем и получаем ее самостоятельно. Нам никто не читает лекций, но нам дают источники.

Здесь такая же история. На курсах нам могут дать источники правильные, хорошие и нам могут структурировать информацию, то есть, подача будет структурированная. У нас будет роадмап: идем отсюда сюда, сюда, сюда, и при этом знаем, что действительно идем по пути куда-то, а не топчемся на месте, что часто бывает.

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

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

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

Q:По поводу математики, английского языка и профильного образования. Нужна ли математика программисту?
Я как-то раз в начале своего пути общался со своим товарищем, очень хорошим программистом, и задал ему этот вопрос. Он сказал: а нужна ли продавцу математика? И, вы знаете, это очень глубокая мысль такое маленькое семечко, из которого может вырасти огромное дерево. Математика нужна вообще всем математика везде вокруг нас. Я это понял потом, когда начал заниматься, поднял заново и переосмыслил школьные знания. Получается, что, конечно, программисту нужна математика. Но она нужны и продавцу иначе он не сможет работать, деньги считать. Она нужна всем без исключения, и надо понимать, какой именно уровень математики нужен. Я скажу так: если вы выберете какое-то из доступных направлений (web frontend/backend), то там не будет чего-то особенного. Вы не будете запускать ракету в космос, никто от вас этого не потребует, особенно в самом начале. Никто не будет сразу давать суперсложные задачи, они все подъемные. В общем, конечно, математика нужна в определенной мере.

По поводу английского языка: английский язык это круто не только для программиста или IT-шника, а для человека в целом. Я думаю, через несколько лет это будет вообще must have. Будем честны: английский это язык мира, на нем весь мир разговаривает. Вся официальная документация, все спецификации языков, протоколов, решений, RFC, чего-то еще написаны на английском языке. То есть, если вы хотите читать и получать четкие знания, то вам нужен именно английский язык. Без него будет сложно.

По поводу образования в некоторых случаях, конечно, корочка нужна, но это тоже скорее исключение. Придете в какую-то компанию работать, вас там спросят а есть ли у вас образование, вы скажете нет, вам скажут ну и уходи, вы скажете ок, и уйдете. В другое место, где нужны люди, которые задачи решают, а не светят корочками. Но, если вы вдруг думаете о том, чтобы пойти на второе высшее, я бы советовал сначала постараться понять, ваше это или не ваше, попробовать позаниматься, может быть, даже найти уже первую работу. Это не так сложно сейчас, на самом деле; может быть, вам потребуется полгода поучиться или, как максимум, год своего времени потратить на это. Но это будет гораздо лучше, чем потратить еще 3-4 года в университете или вообще бросить его через 1-2 года из-за того, что будем честны в университете вы не будете получать тех знаний, которые вам будут нужны для непосредственного решения проблем прямо сейчас. Все-таки университетская база знаний устаревает. Я не говорю, что это происходит во всех университетах, но, наверно, в большинстве своем они учат именно базовым вещам, а не решению задач. А если вы сами начнете учиться на курсах, то вы сможете начать решать задачи и почувствовать профит от решения своих задач, и это будет очень полезно.

image

По поводу кодинга: сколько уделять времени именно на код. Теория это здорово, конечно, но нужно кодить. Кодить нужно столько, сколько сможете. Очень много. Если вы хотите научиться писать код надо писать код. Если вы не будете этого делать, а будете прокачиваться в теории, вы будете отличным теоретиком, может быть, но теоретикам, скорее всего, не платят деньги. Или это бывает редко. Нужна практика. Это как в школе: чтобы научиться делить столбиком, надо делить столбиком.

Допустим, вы всему научились, и начинаете искать работу. Что нужно для того, чтобы начать это делать? Нужна база. Базовые знания математика та же самая, базовая: то есть, базовая логика, чтобы уметь писать решения, алгоритмы, алгоритмы структур данных. Это самое главное, что нужно программисту. Алгоритмы и структуры данных самые простые, чтобы научиться решать задачи, без этой базы вы никогда не будете разработчиком. Также надо знать конструкции и синтаксис целевого языка программирования, библиотеки, фреймворка: то есть, не зная JavaScript, вы не устроитесь во frontend, не зная HTML и CSS, вы не устроитесь верстальщиком, не зная PHP, вы не сможете делать сайты. Это естественно. Синтаксис и конструкции надо знать довольно хорошо. Кроме того, нужно умение решать поставленную задачу. Пусть неоптимально, но верно. Когда вы начинающий разработчик, молодой специалист, джун, от вас требуют одного: чтобы вы учились быстро, решая задачи. То есть, вы берете и делаете задачу, ее дальше передают в работу, а вы, научившись, следующую сделаете лучше.

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

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

Вам действительно будет сложно. Вы должны быть к этому готовы. Но когда вы пройдете этот путь, даже тогда, когда вы просто получите первую работу, вы осознаете, что это невероятный кайф. Когда вы развиваетесь, это очень здорово. Нужно ставить достижимые цели и идти к ним. Вряд ли вы станете премьер-министром планеты такую цель ставить не надо. Ставьте себе короткие майлстоуны, идите к ним, получайте внутренние ачивменты так вы будете развиваться. Нужно научиться декомпозировать сложные задачи на более простые, декомпозиция это вообще лучший друг программиста. Как только вы разбиваете большую задачу на маленькие, оказывается, что с ними гораздо проще справляться. И однажды вы заметите, что уже прошло несколько лет, и вы все сидите, кодите, обучаетесь, тратите на это все свободное время, и вам это нравится.
Вот он, в принципе, весь путь. Наверно, я рассказал про все, что нужно знать человеку, который хочет войти в IT.

Расскажу кратко о том, как было у меня. Это был примерно тот же самый путь, как я описал выше, только все было не так структурировано, и все было очень размазано, потому что я не особенно понимал, что я делаю, и мне было довольно сложно. Вообще, с компами я дружил с самого детства, но это значит только, что я ковырялся в винде, в реестре, ставил линукс, любил железки, в BIOS залезал мне это все нравилось. Но я никогда не был программистом. В детстве я учился в лицее информационных технологий, но что-то пошло не так, как говорится: у родителей закончились деньги на лицей, а там такая история ты или очень умный, или учишься за деньги. Я ушел из лицея после 7 класса, перевелся в обычную школу с направлением в юриспруденцию, а после 9 класса пошел получать среднее специальное юридическое образование. О чем я, на самом деле, не жалею. После среднего специального получил высшее юридическое, а потом пошел работать по специальности. Около 2 лет я работал в уголовном розыске. Это было весело, это было интересно, это был замечательный жизненный опыт. Я совсем не жалею, что моя жизнь так сложилась.

Но в 2014 году я пришел к осознанию того, что занимаюсь чем-то не тем, и мне нужно что-то менять, делать другие вещи. Я собрался увольняться. Последние несколько лет я много думал о программировании, о том, что когда-то я этим занимался, и меня всегда к этому тянуло. Мне так нравилось, что можно весь день проводить за компом я очень любил компьютеры, любил играть, тратил много времени на MMORPG какие-то. И почему бы точно так же не тратить время на программирование сидеть по многу часов, пытаться решить задачу, получать от этого удовольствие? Я тогда проводил у себя эту параллель с MMORPG. И я понял, что настало время для этого, уволился, начал учиться. Я понял, что у меня отсутствует школьная база знаний, что я в плане технической подкованности и грамотности математики, геометрии, физики сильно отупел. Понял, что мне нужно все это подтянуть, начал общаться с ребятами. Походил по знакомым, собрал у них учебники по математике за 7-8-9-10-11 классы. Перечитал это все для себя, порешал задачи. Нашел несколько знакомых программистов, пообщался с ними и понял, что нужно садиться учиться и писать код.

Начал обучения с Java я до сих пор считаю, что это лучший первый язык программирования. По ряду причин, основная из которых то, что язык включает в себя все самое лучшее и самое худшее (или хотя бы многое). Становится понятно, как вообще вещи работают, как устроены истории с Java под капотом. Думаю, JavaScript слишком запутанный язык для роли первого, а C++ слишком многословный.

Прошло несколько месяцев, я начал писать на Java за еду, что называется (на самом деле, за опыт). Знакомые мне начали подкидывать задачи, по которым надо было писать код, они мне даже какие-то символические деньги платили. То есть, я уже начал работать почувствовал, что я могу себе этим хотя бы на что-то зарабатывать. Где-то через 7 месяцев после того, как я сел учиться, у друга на работе появилась вакансия: они искали не то чтобы программиста, а человека, который бы сайты поддерживал, что-то там дописывал и настраивал рекламу. Там были PHP и JavaScript я с ними был вообще никак не знаком, но все равно решил попробовать. Сходил на собеседование, ответил там на все вопросы, которые они хотели услышать. Они поняли, что я могу решать какие-то проблемы, и наняли меня.

Так я устроился на первую официальную работу в IT, через 7 месяцев. Там я выучил PHP и JavaScript; параллельно пописывал на Java для знакомых, когда было свободное время, потом начал больше времени изучать JavaScript, уделять время NodeJS тогда был 2014 год, эта платформа начала как раз активно развиваться, вокруг нее хайп пошел. Я и по сей день на ней пишу, классная штука. Дальше у меня началась череда работ, подработок; я учился, учился, получил много опыта. Тут, наверно, я как раз и стал миддлом разработчиком, который уже начинает более рационально подходить к решению проблем. Миддл начинает решать задачи более правильно, он может решить задачу, даже если она сложная, и он изначально не знает, как ее решать он все равно найдет решение. Так я для себя оцениваю этот уровень.
Дальше у меня было все, как у нашего гипотетического Пети. Я работал, учился, развивался в принципе, по сей день этим и занимаюсь. Наверно, это вся моя история.

Маленькая поправка.. Сегодня перед выступлением я смотрел комментарии на хабре, и там один человек написал такое: история успеха ничему не научит, гораздо полезнее истории неуспеха, на их основе можно понять, чего и как не надо делать. Такие истории у меня, на самом деле, тоже есть. К сожалению, время ограничено иначе я мог бы часов 5 рассказывать интересные истории из своей жизни но у меня есть знакомые (и знакомые знакомых), которые так и не стали разработчиками по разным причинам. В основном что происходит люди не верят в себя, люди боятся, люди слушают своих родных, которые их мотивируют (да куда ты пошел, зачем, чего). Ребята, нужно верить только в себя, самому себе доверять. Если вы понимаете, что вы способны на это вы точно на это способны. Сядьте и делайте, не ищите отговорки, не ищите отмазки. Но самое страшное это не то, что нет веры в себя, а то, что есть лень. Люди просто не хотят напрягаться и думать, они хотят приходить с работы домой и играть, или садиться на диван с пивом. Это лень.

Есть несколько примеров. Один человек работает инженером, он кодит уже 10 лет, у него абсолютно нормальные мозги. Я с ним часто общаюсь. Но он инженер-то не особо успешный по решениям он нормальный, технически грамотный, но по успеху ну такое. Он кодит в свободное от работы время, кодит интересные вещи, но он так и не стал программистом. Ему лень, он не хочет тратить силы на то, чтобы уволиться, несколько месяцев потратить на обучение вложить это время в себя, и найти себе первую работу. Еще один интересный случай я познакомился с чуваком, который чинит велосипеды (я к нему свой велосипед на ТО отгонял). Он попробовал и устал. То есть, он мне рассказывал, что он занимается frontend, что начал изучать. А через три месяца мы встретились снова, и он сказал нет, я бросил, там что-то сложно, думать надо. Ну, дам. Там надо думать.

Есть еще отличный кейс: человек называет себя программистом, свято верит в это. При этом работает черт знает где, занимается черт знает чем и пять лет учит Java. Пять лет. Не знает ничего, зато пять лет учит Java. По-моему, самый классный антипример. Натуральнейшая прокрастинация. Так жить не надо. Если уж вы что-то взялись делать делайте, это так везде работает.

Еще несколько минут хотелось бы уделить тому, что делать Пете, если он не хочет быть программистом. Ну, IT большое, и не программированием единым. Мы можем много чем заниматься. Я вот работаю в компании Voximplant, и у нас здесь не только одни программисты. Voximplant это платформа, это бизнес, это серьезное решение. Я, готовясь к этому выступлению, как раз решил посмотреть, кто нам нужен на работу. Почему бы и нет, это вполне релевантно. Я смотрю и оказывается, что направлений очень много. И это все равно IT: то есть, ты так или иначе будешь крутиться в этой сфере, даже не будучи программистом.

Например, user interface/experience (UI/UX) design здесь надо делать интерфейсы, рисовать это все, делать качественным то, с чем потом конечный пользователь работает, продумывать это все. Технический писатель занимается написанием технической документации к решениям, которые пишут программисты. Есть Sales-менеджеры, которые занимаются продажами. Или Presales-менеджеры и инженеры это те, кто еще до продажников общается с заказчиками и предлагает им какие-то решения; менеджеры здесь ближе к бизнесу, а инженеры ближе к технической стороне, и они могут на этапе предпродажи сказать клиенту, как сделать получится, а как не получится. Есть business development-менеджеры, которые выясняют потребность клиента и то, чем мы непосредственно можем им помочь. Также есть, например, event and communications-менеджеры, которые организуют мероприятия любого уровня все конференции, наша конференция Intercom, например, или другие конференции, которые проводит компания; тоже отличная профессия. Есть project-менеджеры, account-менеджеры, customer success-менеджеры, которые занимаются непосредственно ведением проектов, аккаунтов, продуктов внутри компании. Есть всевозможные юристы, HR или просто рекрутеры, которые занимаются поиском и наймом сотрудников. Контент-менеджеры, таргетологи, маркетологи.

Просто откройте Headhunter и посмотрите нашу компанию, или какую-то другую. Профессий очень много, открытых вакансий очень много. Вам даже не обязательно менять свою профессию, если вы хороший специалист в своем деле, но при этом работаете в компании, которая вам не нравится по каким-то причинам (стагнация внутри компании, странное руководство, все вокруг старые и больные), и вам тяжело в ней. Вы всегда можете найти себе работу по душе, просто сменив направление на IT-шное. И вы будете находиться внутри IT, хотя и не будете писать код. Так что заходите, смотрите на вакансии. Я думаю, вы всегда найдете для себя то, что вам интересно.

image

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

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

По поводу пузыря лопнет он или нет. Вопрос хороший, никто не может это знать, но можно судить по пандемии. Она очень сильно на нас подействовала, но бизнес все равно живет, и пузырь не лопнул. Мы продолжаем развиваться, продолжаем работать. Должно произойти что-то плохое на реально глобальном уровне, чтобы пузырь лопнул пока такого не предвидится, и мы можем спокойно работать, учиться, искать работу, зарабатывать деньги и приносить пользу. Это мое личное мнение, но, опять же, я просто много кручусь в этих сферах и понимаю, как это все устроено. Я понимаю, что IT будет продолжать все новые и новые горизонты, становясь все более привлекательной сферой. То есть, в ближайшее время отката не предвидится, и бояться этого не нужно.

Сделаем выводы из всего, о чем мы сегодня говорили. В 2021 году нужно быть в IT. Серьезно. Если вы работаете в какой-то компании, где нет IT, и ваше руководство или ваш бизнес всячески сопротивляется выходу на IT-рынок я бы советовал вам сменить работу. Не сменить профессию, но работу однозначно. IT-сфера будет расти и развиваться, и бизнес будет продолжать уходить в IT мы сегодня об этом с вами много говорили, и мы все понимаем, что это правда. Интерес к сфере будет расти, рынок будет греться все сильнее, и у нас будет работа. Наш Петя сможет стать программистом, я в него верю но только если действительно захочет. То есть, не только захочет, но и приложит усилия поэтому, если он все это сделает, соберется с силами и сумеет, то у него все обязательно получится, и он найдет работу. Если Петя не захочет быть программистом, то он попадет в IT другим путем благо, их сейчас много. Нужно верить в себя и не лениться, тогда у вас все получится.

image
Q:У меня только один вопрос насколько реально человеку с уже сформировавшимся мозгом, после 25 лет, не испытывавшим до этого интереса к программированию, взять и начать этим заниматься? А если был какой-то интерес почему не углубился в тему самостоятельно, ведь IT-хайпу уже лет 10 точно? Можно было давно обратить внимание.
Могу сказать по себе. Интерес у меня был всегда, но меня всегда что-то останавливало. Лень, скорее всего (сидишь на диване, смотришь сериал все нормально) и страх (ничего не получится, работу не найду, время потрачу). Я не говорю, что это плохо но, если ты постоянно этим занимаешься, то с твоей жизнью никаких изменений не произойдет. Мне тогда было 24 года, если я не ошибаюсь, и я считаю, что у меня мозг тоже был уже вполне сформировавшийся. Осознание, конечно, может приходить в разном возрасте.

То есть, мне кажется это просто лень, вот и все. Вряд ли человек никогда не слышал о программировании: 2021 год на дворе, куда ни зайди рекламируются курсы программирования, открываешь любой ролик на YouTube видишь рекламу. Если он этим никогда не интересовался ну, взял и заинтересовался. Посмотрел какой-нибудь бесплатный ролик наш сегодняшний вебинар, хотя бы и уже многое для себя узнал. Понял, что это классно, интересно. Или, наоборот, подумал: нет, фигня все. Поэтому мне кажется, что это вполне реально.
Q: какую литературу можешь посоветовать для новичков, где лучше ее искать, если спрашивать негде?
Насчет спрашивать негде я сейчас сделаю аттракцион невиданной щедрости и просто возьму и оставлю свой telegram. Собственно, у меня во всех соцсетях одинаковый никнейм nikolasmelui, я попрошу модераторов оставить ссылку. Можете мне написать напрямую, я точно смогу уделить 5 минут на то, чтобы с вами поболтать, ответить на вопросы и посоветовать, дать какие-то материалы для обучения, исходя из ваших потребностей.

По поводу литературы у меня есть 10-15 книг, которые я могу рекомендовать, но для новичков среди них нет ничего. Мне кажется, что лучше смотреть не книги, а курсы ну, в моем случае это видео, например. Однако есть проект Джуность, который я упоминал вскользь сегодня это тот проект, который мы с ребятами сделали, когда я еще в Mechanica/ANMEDIO работал; мы его сделали для того, чтобы распределять по грейдам (junior-middle-senior, и еще по три градации внутри каждой) наших сотрудников. Там есть материалы для обучения, в том числе. То есть, есть тег что нужно знать, чтобы стать миддлом заходишь в этот тег, там есть материалы по обучению.

Единственное я сейчас не являюсь прямым мейнтейнером проекта по ряду причин (в том числе, из-за нехватки времени), и некоторые материалы там уже подпротухли. Но там есть классные материалы, и, наверно, мы в ближайшем будущем обновим проект. Я как раз насобирал материалы; мы и внутри компании Voximplant занимаемся тем, что собираем материалы для какой-то подобной внутренней вещи, и на внешку (в Джуность) тоже стоит этими материалами поделиться. Так что, можно будет там посмотреть. А так напишите мне, и я вам отвечу, покажу, расскажу.

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

Подробнее..

Оптимизация графики в Voximplant Kit

24.03.2021 14:22:50 | Автор: admin

Что делать, если сценарий крутой и сложный, но из-за этого начинает тормозить? Данным вопросом задались наши разработчики Voximplant Kit и придумали функцию оптимизации. Продолжая серию обновлений Кита, расскажем, как оптимизация заставила большие сценарии летать и с какими проблемами мы столкнулись в процессе её создания.

Почему надо оптимизировать

Многим знакома проблема производительности, вызванная наличием слишком большого количества элементов на странице. Что это значит? В нашем случае чем больше элементов в сценарии Voximplant Kit, тем больше это влияет на скорость визуализации перемещения блоков по холсту (всех вместе и по отдельности), а также на скорость визуализации перемещения и масштабирования самого холста.

Мы рассмотрели разные варианты и пришли к выводу, что в качестве решения отлично подойдёт CSS свойство will-change, позволяющее заранее проинформировать браузер об изменениях, которые могут быть применены к элементу. Благодаря этому свойству можно настроить оптимизацию до начала изменений во избежание затрат на запуск операций, негативно влияющих на отзывчивость страницы.

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

<svg ... > <---- Холст <g transform="matrix(1,0,0,1,224,444)"> <---- Группа элементов внутри svg  <rect>  <rect>

Реализация

У наших разработчиков появилась идея обернуть SVG в div-элемент, чтобы применять все трансформации сначала к нему, а затем при необходимости к самому SVG-элементу с холстом. После того, как трансформации стали применяться к <div>, мы смогли использовать will-change: transform для их отслеживания:

<div> <---- div-обёртка, к которой применяется оптимизация  <svg ... > <---- Холст   <g> <---- Группа элементов внутри svg    <rect>    <rect>

Но появилась ещё одна проблема использование will-change инициирует создание нового слоя, и чем больше ширина и высота элемента, к которому это св-во применяется, тем больше расходуется оперативной памяти для хранения слоя. Справиться с этим помогло уменьшение масштаба SVG в 10 раз. Так, например, при масштабе холста =200% для слоя сwill-change требовалось300 мегабайтоперативки , а после уменьшения масштаба стало нужно всего около3 мегабайт.

Чтобы это осуществить, выставляем параметр zoom = 0.1 и подключаем к работе методtransformToCenterViewport, после чего применяем те же трансформации к div-элементу:

if (isPerfMode) {  this.el.classList.add('perf-mode');  // Меняем масштаб перед включением performance mode  const prevScale = this._viewportMatrix.a;  const point = this.getViewPortCenter();  const zoom = 0.1;       // Уменьшаем исходный svg, чтобы will-change тратил меньше оперативной памяти  this.transformToCenterViewport(point, zoom, true, false, true);  this.initScale = this._viewportMatrix.a;  this.createMatrix();     this.isPerfMode = true;       // Применяем трансформации к элементу-обертке  this.startPerformance();  this.transformToCenterViewport(point, prevScale, false, false, true);}

Т.к. при переходе в режим оптимизации мы уменьшаем SVG, холст становится очень маленьким и неудобным для работы. Чтобы это исправить, применим обратное масштабирование непосредственно к div-элементу:

public startPerformance(force = false) {  ...  this.isPerformance = true;    // Получаем размер области с блоками и отступ от левого угла вьюпорта  const { x, y, width, height } = this.layers.getBBox();  const initScale = this.initScale;    // Ширина и высота для обёртки и смещение по оси x и y для области с блоками  const wrapW = Math.floor(width * initScale) + 2;  const wrapH = Math.floor(height * initScale) + 2;  const layerX = -x * initScale;  const layerY = -y * initScale;    // this.wrapMatrix - матрица div-элемента с холстом   this.wrapMatrix.e = +(this._viewportMatrix.e + x * this._viewportMatrix.a);   this.wrapMatrix.f = +(this._viewportMatrix.f + y * this._viewportMatrix.d);   this.svgWrapper.style.width = wrapW + 'px';   this.svgWrapper.style.height = wrapH + 'px';   this.svgWrapper.style.transform = this.wrapMatrix.toString();   this.svgWrapper.style.willChange = 'transform'; this.layers.style.transform = `matrix(${initScale},0,0,${initScale},${layerX} ,${layerY} )`;}

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

После завершения масштабирования (событие о скролле), св-во will-change удаляется на 0.1 секунды и затем устанавливается заново. Это заставляет браузер повторно растрировать слой, возвращая пропавшие детали изображения:

// Добавляем 3d трансформацию, чтобы слой не был удаленthis.svgWrapper.style.transform = this.wrapMatrix.toString() + ' translateZ(0)';this.transformFrameId = requestAnimationFrame(() => {  // Устанавливаем св-во will-change для применения в следующем кадре  this.svgWrapper.style.willChange = '';  this.transformFrameId = requestAnimationFrame(() => {    this.svgWrapper.style.willChange = 'transform';    this.svgWrapper.style.transform = this.wrapMatrix.toString();  });});

Осталось внести последний фикс всегда отображать перемещаемый блок поверх других. В JointJS для перемещения блоков и линков по оси Z существуют методы toFront и toBack (аналог z-index в HTML). Принцип их работы заключается в сортировке элементов и перерисовке блоков и линков, это вызывает задержки.

Наши разработчики придумали следующее: блок, с которым мы взаимодействуем, временно ставится в конец дерева элементов внутри SVG (элемент с самым высоким z-index находится в конце списка) на событие mousedown, а затем возвращается на прежнее место на событие mouseup.

Принцип работы

Режим оптимизации можно протестировать во всех браузерах на основе Chromium (Chrome, Opera, Edge, Yandex Browser и т.п.), а также в браузере Safari. Для сценариев, содержащих от 50 блоков, функция включается автоматически. Самостоятельно включить или отключить её можно, перейдя в меню настроек сценария в правом верхнем углу:

Как только оптимизация будет включена или выключена, в верхней части окна со сценарием появится уведомление:

Ниже для сравнения представлены 2 гифки, демонстрирующие работу в редакторе с выключенным и включенным режимом оптимизации. Но поскольку всегда интереснее потестить самому, смело переходим в свой сценарий Voximplant Kit или, если ещё нет аккаунта, на страницу регистрации.

Без оптимизации работа с холстом и его элементами выглядит примерно так (на разных компьютерах с разными мощностями результат может отличаться):

Подключаем оптимизацию и вуаля!

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

Надеемся, что статья была интересной, мы будем продолжать улучшать продукт и делиться успехами и лайфхаками с вами!

Подробнее..

Живые интерактивные логи визуализация логов в Voximplant Kit

30.06.2020 14:13:12 | Автор: admin

Мы продолжаем обновлять Voximplant Kit с помощью JointJS. И рады сообщить о появлении живых логов (live logs) звонков. Насколько они живые и опасны ли для простых юзеров, читайте под катом.

Ранее для анализа звонков в Voximplant Kit пользователям были доступны лишь записи разговоров. Нам же хотелось в дополнение к аудио сделать не просто текстовый лог, а более удобный инструмент для просмотра деталей звонка и анализа ошибок. И поскольку мы имеем дело с low-code/no-code продуктом, появилась идея визуализации логов.

В чем соль?/ Новый концепт


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


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


Управление


Контролы старт\стоп (1) останавливают/возобновляют воспроизведение, а назад\далее (2) точечно перемещают юзера к началу следующего/предыдущего блока. Можно также просто кликать по таймлайну, чтобы начать воспроизведение с определенного момента времени, как с проигрыванием песни.

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


Для удобства пользователя также доступен список пройденных блоков с таймстампами (Лог):


Спойлер:
Во вкладке Лог мы планируем показывать детали блоков. Они помогут нам понять, почему из блока вышли по определенному порту и были ли ошибки. Например, для блока распознавания мы увидим результаты и ошибки распознавания.
Наибольший интерес здесь будут представлять сложные блоки, такие как DialogFlowConnector, IVR, ASR и т.д.


Переменные


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


Лайфхак


Логи звонков сохраняются в первоначальном виде, даже после изменения или удаления сценария. А это значит, что не возникнет проблем в восстановлении хода звонка: если он понадобится, всегда можно обратиться к логу.

Самостоятельно пощупать логи можно на Voximplant Kit.

Так, а что внутри?


Разберемся, как именно динамические логи реализованы в коде. Скажем сразу, от Joint JS мы взяли лишь анимацию и выделение блоков, как в деморежиме. Остальное (что можно на основе этого сделать) наша фантазия.

Кстати, чтобы узнать подробнее о внутренностях деморежима, читайте нашу предыдущую статью.

Получаем timepointы


При переходе в просмотр лога, сервер присылает данные, где содержится список всех пройденных блоков, время входа в них и список переменных, которые менялись во время звонка. Другими словами, на фронте мы получаем два массива объектов: log_path и log_variables.

Также в ответе сервера есть ссылка на аудио и его продолжительность, если разговор записывался.

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

Обновляем временную шкалу


После нажатия кнопки play временная шкала начинает обновляться каждые 10 мс. Во время каждого обновления проверяем, совпадает ли текущее время с одним из timepointов:

const found = this.timePoints.find((item) => item === this.playTime);

Если совпадение есть, будем искать все блоки у которых timepoint = текущее время + 600 мс (время, за которое происходит анимация перемещения между блоками).

Код метода updatePlayTime():

updatePlayTime(): void {    const interval = 10;    let expected = Date.now() + interval;    const tick = () => {        const drift = Date.now() - expected;        const found = this.timePoints.find((item) => item === this.playTime);        this.$emit('update', {            time: this.playTime,            found: found !== undefined        });        if (this.playTime >= this.duration) {            this.isPlay = false;            this.playTime = this.duration;            clearTimeout(this.playInterval);            this.$emit('end', this.playTime);            return;        }        expected += interval;        this.playTime += 0.01;        this.playTime = +this.playTime.toFixed(2);        this.updateProgress();        this.playInterval = window.setTimeout(tick, Math.max(0, interval - drift));    };    this.playInterval = window.setTimeout(tick, 10);}

Так же каждые 90 мс мы проверяем совпадения для текущего времени и timepoint'ов у измененных переменных + 4000 мс (время, в течение которого висит уведомление об изменении переменной).

Выделяем блоки


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

Если блоков с timepoint = текущее время + 600 мс несколько, то переход анимируется только к последнему:

if (i === blocks.length - 1) {    await this.selectBlock(blocks[i], 600, true, true);}

Это необходимо, поскольку есть блоки, которые обрабатываются очень быстро. Например, Проверка данных, Изменение данных и т.п. за 1 секунду может быть пройдено сразу несколько блоков. Если их анимировать последовательно, то возникнет отставание от времени таймлайна.

Код метода onUpdateTimeline:

async onUpdateTimeline({    time,    found}) {    this.checkHistoryNotify();    if (!found) return;    // Выделяем группу блоков от первой найденной точки + 600мс    const blocks = this.callHistory.log_path.filter((item) => {        return item.timepoint >= this.logTimer && item.timepoint < this.logTimer + 600;    });    if (blocks.length) {        this.editor.unselectAll();        for (let i = 0; i < blocks.length; i++) {            if (i === blocks.length - 1) {                await this.selectBlock(blocks[i], 600, true, true);                const cell = this.editor.getCellById(blocks[i].idTarget);                this.editor.select(cell);            } else {                await this.selectBlock(blocks[i], 0, false, true);            }        }    }}


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

В этом нам помогает метод selectBlock():

async selectBlock(voxHistory, timeout = 700, animate = true, animateLink = true) {    const inQueue = this.selectQueue.find((item) => item[0].targetId === voxHistory.idTarget);    if (!inQueue) this.selectQueue.push(arguments);    return this.exeQueue();}


Перематываем


При перемотке тот же принцип: когда таймлайн переместили, мы получаем время, на которое нужно перемотать и отмечаем блоки с timepoint'ами меньше текущего времени как пройденные:

const forSelect = this.callHistory.log_path.filter((item) => {        const time = accurate ? item.accurateTime : item.timepoint;        return time <= this.logTimer;    });

Анимированный переход делаем к последнему из них.

Код метода onRewind():

async onRewind({    time,    accurate}, animation = true) {    this.editor.unselectAll();    this.stopLinksAnimation();    this.checkHistoryNotify(true);    const forSelect = this.callHistory.log_path.filter((item) => {        const time = accurate ? item.accurateTime : item.timepoint;        return time <= this.logTimer;    });    for (let i = 0; i < forSelect.length; i++) {        if (i === forSelect.length - 1) {            await this.selectBlock(forSelect[i], 600, animation, false);            const cell = this.editor.getCellById(forSelect[i].idTarget);            this.editor.select(cell);        } else {            await this.selectBlock(forSelect[i], 0, false, false);        }    }    if (this.isPlay) this.restartAnimateLink();    this.onEndTimeline();}

Проигрываем аудио


С включение/выключением аудиозаписи дела обстоят еще проще. Если время таймлайна совпадает со стартом записи, она начинает проигрываться и далее время синхронизируется. За это отвечает метод updatePlayer():

updatePlayer() {    if (this.playTime >= this.recordStart && this.player.paused && !this.isEndAudio) {        this.player.play();        this.player.currentTime = this.playTime - this.recordStart;    } else if (this.playTime < this.recordStart && !this.player.paused) {        this.player.pause();    }}

На этом всё! Вот так на основе методов Joint JS и креатива наших разработчиков появились живые логи. Обязательно протестируйте их самостоятельно, если вы этого еще не сделали :)

Здорово, если вам нравится наша серия статей про обновления Кита. Будем и дальше делиться с вами самым свежим и интересным!
Подробнее..

Скрываем номера курьеров и клиентов с помощью key-value хранилища

17.06.2021 18:06:38 | Автор: admin

В условиях пандемии курьерские сервисы стали востребованы как никогда прежде. Чтобы клиент и курьер могли созвониться для уточнения информации по заказу, им нужно знать номера телефонов друг друга. А что насчет соблюдения прайваси? Многие сервисы доставок уже озаботились этим вопросом после не очень приятных инцидентов, о которых вы могли читать в новостях.

Каждый сервис использует свои решения для маскировки номеров клиентов и курьеров. В данной статье я расскажу, как сделать это с помощью key-value хранилища в Voximplant.

Как это будет работать

Мы создадим сценарий, который позволит курьеру и клиенту созваниваться, не зная при этом личные номера телефонов друг друга.

У нас будет только один нейтральный номер, на который будут звонить и клиент, и курьер. Номер мы арендуем в панели Voximplant. Затем создадим некую структуру данных, где клиент и курьер будут связаны между собой номером заказа (то есть ключом в терминологии key-value storage).

Так при звонке на арендованный номер звонящий введёт номер заказа, и если такой заказ есть в базе, наш сценарий проверит номера телефонов, привязанные к нему. Далее если номер звонящего будет идентифицирован как номер клиента, произойдет соединение с курьером, ответственным за заказ, и наоборот.

Например, звонок курьера клиенту будет выглядеть следующим образом:

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

Перейдем непосредственно к реализации.

Вам понадобятся

1) Чтобы начать разработку, войдите в свой аккаунт: manage.voximplant.com/auth. В меню слева нажмите Приложения, затем Создать приложение в правом верхнем углу. Дайте ему имя, например, numberMasking и снова кликните Создать.

2) Зайдите в новое приложение, переключитесь на вкладку Сценарии и создайте сценарий, нажав на +. Назовём его kvs-scenario. Здесь мы будем писать код, но об этом чуть позже.

3) Сначала перейдем во вкладку Роутинг и создадим правило для нашего сценария. Маску (регулярное выражение) оставим .* по умолчанию, так правило будет срабатывать для всех номеров.

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

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

5) Осталось привязать его к нашему приложению. Заходим в приложение, открываем вкладку Номера Доступные и нажимаем Прикрепить. В открывшемся окне можно также прикрепить наше правило, тогда оно будет автоматически назначено для входящих вызовов, а все остальные правила будут проигнорированы.

6) Далее необходимо верифицировать аккаунт, чтобы использовать этот номер для звонков.

Отлично, структура готова, осталось заполнить key-value хранилище и добавить код в сценарий.

Key-value хранилище

Чтобы сценарий заработал, нужно положить что-то в хранилище. Это можно сделать, воспользовавшись Voximplant Management API. Я буду использовать Python API client, он работает с Python 2.x или 3.x с установленным pip и setuptools> = 18.5.

1) Зайдем в папку проекта и установим SDK, используя pip:

python -m pip install --user voximplant-apiclient

2) Создадим файл с расширением .py и добавим в него код, при выполнении которого данные о заказе попадут в key-value хранилище. Применим метод set_key_value_item:

from voximplant.apiclient import VoximplantAPI, VoximplantExceptionif __name__ == "__main__":    voxapi = VoximplantAPI("credentials.json")        # SetKeyValueItem example.    KEY = 12345    VALUE = '{"courier": "79991111111", "client": "79992222222"}'    APPLICATION_ID = 1    TTL = 864000        try:        res = voxapi.set_key_value_item(KEY,            VALUE,            APPLICATION_ID,            ttl=TTL)        print(res)    except VoximplantException as e:        print("Error: {}".format(e.message))

Файл с необходимыми credentials вы сможете сгенерировать при создании сервисного аккаунта в разделе Служебные аккаунты в настройках панели.

APPLICATION_ID появится в адресной строке при переходе в ваше приложение.

В качестве ключа (KEY) будет использоваться пятизначный номер заказа, а в качестве значений телефонные номера: courier номер курьера, client номер клиента. TTL нам здесь необходимо для указания срока хранения значений.

3) Осталось запустить файл, чтобы сохранить данные заказа:

python3 kvs.py

Если мы больше не захотим, чтобы клиент и курьер беспокоили друг друга, можно будет удалить их данные из хранилища. Информацию о всех доступных методах key-value storage вы найдёте в нашей документации: management API и VoxEngine.

Код сценария

Код, который необходимо вставить в сценарий kvs-scenario, представлен ниже, его можно смело копировать as is:

Полный код сценария
require(Modules.ApplicationStorage);/** * @param {boolean} repeatAskForInput - была ли просьба ввода произнесена повторно * @param longInputTimerId - таймер на отсутствие ввода * @param shortInputTimerId - таймер на срабатывание фразы для связи с оператором * @param {boolean} firstTimeout - индикатор срабатывания первого таймаута * @param {boolean} wrongPhone - индикатор совпадения номера звонящего с номером, полученным из хранилища * @param {boolean} inputRecieved - получен ли ввод от пользователя *  */let repeatAskForInput;let longInputTimerId;let shortInputTimerId;let firstTimeout = true;let wrongPhone;let inputRecieved;const store = {    call: null,    caller: '',    callee: '',    callid: '74990000000',    operator_call: null,    operatorNumber: '',    input: '',    data: {        call_operator: '',        order_number: '',        order_search: '',        phone_search: '',        sub_status: '',        sub_available: '',        need_operator: '',        call_record: ''    }}const phrases = {    start: 'Здр+авствуйтте. Пожалуйста, -- введите пятизначный номер заказa в тт+ооновом режиме.',    repeat: 'Пожалуйста , , - - введите пятизначный номер заказа в т+оновом режиме,, или нажмите решетку для соединения со специалистом',    noInputGoodbye: 'Вы - ничего не выбрали. Вы можете посмотреть номер заказа в смс-сообщении и позвонить нам снова. Всего д+обровоо до свидания.',    connectToOpearator: 'Для соединения со специалистом,, нажмите решетку',    connectingToOpearator: 'Ожидайте, соединяю со специалистом',    operatorUnavailable: 'К сожалению,, все операторы заняты. Пожалуйста,,, перезвоните позднее. Всего д+обровоо до свидания.',    wrongOrder: 'Номер заказа не найден. Посмотрите номер заказа в смс-сообщении и введите его в т+оновом режиме. Или свяжитесь со специалистом,, нажав клавишу решетка.',    wrongOrderGoodbye: 'Вы ничего не выбрали, всего д+обровоо до свидания.',    wrongPhone: 'Номер телефона не найден. Если вы кли+ент, перезвоните с номера, который использовали для оформления заказа. Если вы курьер, перезвоните с номера, который зарегистрирован в нашей системе. Или свяжитесь со специалистом,,- нажав клавишу решетка.',    wrongPhoneGoodbye: 'Вы ничего не выбрали. Всего доброго, до свидания!',    courierIsCalling: `Вам звонит курьер по поводу доставки вашего заказа, - - ${store.data.order_number}`,    clientIsCalling: `Вам звонит клиент по поводу доставки заказа, - - ${store.data.order_number} `,    courierUnavailable: 'Похоже,,, курь+ер недоступен. Пожалуйста,,, перезвоните через п+ару мин+ут. Всего д+обровоо до свидания.',    clientUnavailable: 'Похоже,,, абонент недоступен. Пожалуйста,,, перезвоните через пп+ару мин+ут. Всего д+обровоо до свидания.',    waitForCourier: 'Ожидайте на линии,, - соединяю с курьером.',    waitForClient: 'Ожидайте на линии,, соединяю с клиентом.'}VoxEngine.addEventListener(AppEvents.Started, async e => {    VoxEngine.addEventListener(AppEvents.CallAlerting, callAlertingHandler);})async function callAlertingHandler(e) {    store.call = e.call;    store.caller = e.callerid;    store.call.addEventListener(CallEvents.Connected, callConnectedHandler);    store.call.addEventListener(CallEvents.Disconnected, callDisconnectedHandler);    store.call.answer();}async function callDisconnectedHandler(e) {    await sendResultToDb();    VoxEngine.terminate();}async function callConnectedHandler() {    store.call.handleTones(true);    store.call.addEventListener(CallEvents.RecordStarted, (e) => {        store.data.call_record = e.url;    });    store.call.record();    store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);    await say(phrases.start);    addInputTimeouts();}function dtmfHandler(e) {    clearInputTimeouts();    store.input += e.tone;    Logger.write('Введена цифра ' + e.tone)    Logger.write('Полный код ' + store.input)    if (e.tone === '#') {        store.data.need_operator = "Да";        store.call.removeEventListener(CallEvents.ToneReceived);        store.call.handleTones(false);        callOperator();        return;    }    if (!wrongPhone) {        if (store.input.length >= 5) {            repeatAskForInput = true;            Logger.write(`Получен код ${store.input}. `);            store.call.handleTones(false);            store.call.removeEventListener(CallEvents.ToneReceived);            handleInput(store.input);            return;        }    }    addInputTimeouts();}function addInputTimeouts() {    clearInputTimeouts();    if (firstTimeout) {        Logger.write('Запущен таймер на срабатывание фразы для связи с оператором');        shortInputTimerId = setTimeout(async () => {            await say(phrases.connectToOpearator);        }, 1500);        firstTimeout = false;    }    longInputTimerId = setTimeout(async () => {        Logger.write('Сработал таймер на отсутствие ввода от пользователя ' + longInputTimerId);        store.call.removeEventListener(CallEvents.ToneReceived);        store.call.handleTones(false);        if (store.input) {            handleInput(store.input);            return;        }        if (!repeatAskForInput) {            Logger.write('Просим пользователя повторно ввести код');            store.call.handleTones(true);            store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);            await say(phrases.repeat);            addInputTimeouts();            repeatAskForInput = true;        } else {            Logger.write('Код не введен. Завершаем звонок.');            await say(inputRecieved ? phrases.wrongOrderGoodbye : phrases.noInputGoodbye);            store.call.hangup();        }    }, 8000);    Logger.write('Запущен таймер на отсутствие ввода от пользователя ' + longInputTimerId);}function clearInputTimeouts() {    Logger.write(`Очищаем таймер ${longInputTimerId}. `);    if (longInputTimerId) clearTimeout(longInputTimerId);    if (shortInputTimerId) clearTimeout(shortInputTimerId);}async function handleInput() {    store.data.order_number = store.input;    Logger.write('Ищем совпадение в kvs по введенному коду: ' + store.input)    inputRecieved = true;    let kvsAnswer = await ApplicationStorage.get(store.input);    if (kvsAnswer) {        store.data.order_search = 'Заказ найден';        Logger.write('Получили ответ от kvs: ' + kvsAnswer.value)        let { courier, client } = JSON.parse(kvsAnswer.value);        if (store.caller == courier) {            Logger.write('Звонит курьер')            store.callee = client;            store.data.sub_status = 'Курьер';            store.data.phone_search = 'Телефон найден';            callCourierOrClient();        } else if (store.caller == client) {            Logger.write('Звонит клиент')            store.callee = courier;            store.data.sub_status = 'Клиент';            store.data.phone_search = 'Телефон найден';            callCourierOrClient();        } else {            Logger.write('Номер звонящего не совпадает с номерами, полученными из kvs');            wrongPhone = true;            store.data.phone_search = 'Телефон не найден';            store.input = '';            store.call.handleTones(true);            store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);            await say(phrases.wrongPhone);            addInputTimeouts();        }    } else {        Logger.write('Совпадение в kvs по введенному коду не найдено');        store.data.order_search = 'Заказ не найден';        store.input = '';        store.call.handleTones(true);        store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);        await say(phrases.wrongOrder);        Logger.write(`Очищаем таймер ${longInputTimerId}. `);        addInputTimeouts();    }}async function callCourierOrClient() {    clearInputTimeouts();    Logger.write('Начинаем звонок курьеру/клиенту');    await say(store.data.sub_status === 'Курьер' ? phrases.waitForClient : phrases.waitForCourier, store.call);    const secondCall = VoxEngine.callPSTN(store.callee, store.callid);    store.call.startPlayback('http://cdn.voximplant.com/toto.mp3');    secondCall.addEventListener(CallEvents.Connected, async () => {        store.data.sub_available = 'Да';        await say(store.data.sub_status === 'Курьер' ? phrases.courierIsCalling : phrases.clientIsCalling, secondCall);        store.call.stopPlayback();        VoxEngine.sendMediaBetween(store.call, secondCall);    });    secondCall.addEventListener(CallEvents.Disconnected, () => {        store.call.hangup();    });    secondCall.addEventListener(CallEvents.Failed, async () => {        store.data.sub_available = 'Нет';        store.call.stopPlayback();        await say(store.data.sub_status === 'Курьер' ? phrases.clientUnavailable : phrases.courierUnavailable, store.call);        store.call.hangup();    });}async function callOperator() {    Logger.write('Начинаем звонок оператору');    await say(phrases.connectingToOpearator, store.call);    store.call.startPlayback('http://cdn.voximplant.com/toto.mp3');    store.operator_call = VoxEngine.callPSTN(store.operatorNumber, store.callid);    store.operator_call.addEventListener(CallEvents.Connected, async () => {        store.data.call_operator = 'Оператор свободен';        VoxEngine.sendMediaBetween(store.call, store.operator_call);    });    store.operator_call.addEventListener(CallEvents.Disconnected, () => {        store.call.hangup();    });    store.operator_call.addEventListener(CallEvents.Failed, async () => {        store.data.call_operator = 'Оператор занят';        await say(phrases.operatorUnavailable, store.call);        store.call.hangup();    });}async function sendResultToDb() {    Logger.write('Данные для отправки в БД');    Logger.write(JSON.stringify(store.data));    const options = new Net.HttpRequestOptions();    options.headers = ['Content-Type: application/json'];    options.method = 'POST';    options.postData = JSON.stringify(store.data);    await Net.httpRequestAsync('https://voximplant.com/', options);}function say(text, call = store.call, lang = VoiceList.Yandex.Neural.ru_RU_alena) {    return new Promise((resolve) => {        call.say(text, lang);        call.addEventListener(CallEvents.PlaybackFinished, function callback(e) {            resolve(call.removeEventListener(CallEvents.PlaybackFinished, callback));        });    });};

Код тщательно прокомментирован, но в некоторые моменты углубимся подробнее.

Вводим номер заказа

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

store.input += e.tone;

Если звонящий ввел #, сразу соединяем его с оператором:

if (e.tone === '#') {    store.data.need_operator = "Да";    store.call.removeEventListener(CallEvents.ToneReceived);    store.call.handleTones(false);    callOperator();    return;}

Если он ввел последовательность из 5 цифр, вызываем функцию handleInput:

if (store.input.length >= 5) {    repeatAskForInput = true;    Logger.write('Получен код ${store.input}. ');    store.call.handleTones(false);    store.call.removeEventListener(CallEvents.ToneReceived);    handleInput(store.input);    return;}

Ищем заказ в хранилище

Здесь мы будем сравнивать введенный номер заказа с номером в хранилище, используя метод ApplicationStorage.get(), в качестве ключа используем введенную последовательность:

store.data.order_number = store.input;Logger.write('Ищем совпадение в kvs по введенному коду: ' + store.input)inputRecieved = true;let kvsAnswer = await ApplicationStorage.get(store.input);

Если заказ найден, получаем для него номера клиента и курьера:

if (kvsAnswer) {    store.data.order_search = 'Заказ найден';    Logger.write('Получили ответ от kvs: ' + kvsAnswer.value)    let { courier, client } = JSON.parse(kvsAnswer.value);

Теперь осталось разобраться, кому звонить. Если номер звонящего принадлежит курьеру, будем выполнять переадресацию на клиента, если клиенту на курьера. В этом нам поможет функция callCourierOrClient:

if (store.caller == courier) {    Logger.write('Звонит курьер')    store.callee = client;    store.data.sub_status = 'Курьер';    store.data.phone_search = 'Телефон найден';    callCourierOrClient();} else if (store.caller == client) {    Logger.write('Звонит клиент')    store.callee = courier;    store.data.sub_status = 'Клиент';    store.data.phone_search = 'Телефон найден';    callCourierOrClient();}

Если номера нет в хранилище, просим перезвонить с другого номера, который указывался при оформлении заказа:

else {    Logger.write('Номер звонящего не совпадает с номерами, полученными из kvs');    wrongPhone = true;    store.data.phone_search = 'Телефон не найден';    store.input = '';    store.call.handleTones(true);    store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);    await say(phrases.wrongPhone);    addInputTimeouts();}

И наконец, обрабатываем вариант, когда номер заказа не был найден в базе. В этом случае просим попробовать ввести его снова, предварительно удостоверившись, что номер верный:

else {    Logger.write('Совпадение в kvs по введенному коду не найдено');    store.data.order_search = 'Заказ не найден';    store.input = '';    store.call.handleTones(true);    store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);    await say(phrases.wrongOrder);    Logger.write(`Очищаем таймер ${longInputTimerId}. `);    addInputTimeouts();}

Звоним клиенту/курьеру

Переходим непосредственно к звонку клиенту/курьеру, то есть к логике функции callCourierOrClient. Здесь мы сообщим звонящему, что переводим его звонок на курьера/клиента, и включим музыку на ожидание. С помощью метода callPSTN позвоним клиенту или курьеру (в зависимости от того, чей номер был ранее идентифицирован как номер звонящего):

await say(store.data.sub_status === 'Курьер' ? phrases.waitForClient : phrases.waitForCourier, store.call);const secondCall = VoxEngine.callPSTN(store.callee, store.callid);store.call.startPlayback('http://cdn.voximplant.com/toto.mp3');

В этот же момент сообщим второй стороне о том, что звонок касается уточнения информации по заказу:

secondCall.addEventListener(CallEvents.Connected, async () => {    store.data.sub_available = 'Да';    await say(store.data.sub_status === 'Курьер' ? phrases.courierIsCalling : phrases.clientIsCalling, secondCall);    store.call.stopPlayback();    VoxEngine.sendMediaBetween(store.call, secondCall);});

Обработаем событие дисконнекта:

secondCall.addEventListener(CallEvents.Disconnected, () => {    store.call.hangup();});

И оповестим звонящего, если вторая сторона недоступна:

secondCall.addEventListener(CallEvents.Failed, async () => {    store.data.sub_available = 'Нет';    store.call.stopPlayback();    await say(store.data.sub_status === 'Курьер' ? phrases.clientUnavailable : phrases.courierUnavailable, store.call);    store.call.hangup();});

За все фразы, который произносит робот, отвечает функция say, а сами фразы перечислены в ассоциативном массиве phrases. В качестве TTS провайдера мы используем Yandex, голос Alena:

function say(text, call = store.call, lang = VoiceList.Yandex.Neural.ru_RU_alena) {    return new Promise((resolve) => {        call.say(text, lang);        call.addEventListener(CallEvents.PlaybackFinished, function callback(e) {            resolve(call.removeEventListener(CallEvents.PlaybackFinished, callback));        });    });};

Кроме всего прочего, наш сценарий записывает звонки, используя метод record, и показывает, как можно сохранить статистику в базу данных (в нашем коде за это отвечает функция sendResultToDb). Это очень важно для бизнеса, поскольку позволяет анализировать статистику, обеспечивать контроль качества и оперативно решать спорные ситуации, которые могли возникнуть в процессе доставки заказа.

Тестируем

Когда полный код добавлен в сценарий, а данные по заказу в хранилище, смело начинайте тестировать.

Позвоним с телефона клиента или курьера на номер, арендованный в панели. Затем введем номер заказа (в нашем случае это 12345) и будем ждать соединения со второй стороной.

Если все сделано верно, клиент и курьер смогут созваниваться и обсуждать детали заказа, не зная настоящих номеров друг друга, а значит, не нарушая прайваси. Круто, не так ли?) Желаем вам успешной разработки и беспроблемной доставки!

P.S. Также мой коллега недавно рассказал, как обезопасить общение клиента и курьера с помощью Voximplant Kit (наш low-code/no-code продукт). Если эта тема вас заинтересовала, переходите по ссылке :)

Подробнее..

А я говорю, возьми Excel и позвони

01.04.2021 14:11:52 | Автор: admin

Voximplant, как и любая облачная платформа, предоставляет API, с помощью которого можно управлять пользователями, запускать звонки, просматривать их историю и логи и делать немало других полезных вещей. В принципе, практически всё, что можно настроить в панели управления, можно сделать и через API.

Но в современном мире иметь API недостаточно мало кто хочет формировать HTTP-запросы, передавать параметры, думать про правильную авторизацию. Поэтому мы предлагаем SDK для разных языков программирования: Python, PHP, C# и многих других. И кажется, что этого достаточно, чтобы сделать нашу платформу лёгкой в использовании для очень большой аудитории. Или всё-таки недостаточно?

Обратимся к статистике. По разным данным сейчас в мире насчитывается где-то 15-30 миллионов разработчиков цифра несомненно впечатляющая. Но, например, пользователей MS Excel в мире не менее 100 миллионов. Почему же они должны страдать? Ведь, будем честны, почти каждый из тех, кто хоть раз открывал Excel, явно ощущал недостаток возможностей по управлению коммуникационными платформами в этом без сомнения очень гибком программном продукте. Практически каждый день мы получаем на наш email сотни запросов, которые сводятся к очень простой просьбе: Я хочу звонить из Excel!. Однажды у окон нашего офиса даже выстроились люди с такими требованиями (видели фото выше?) Мы просто не могли оставаться в стороне.

Однако звонки это всё-таки слишком революционно, а главное, потребует установки дополнительных ActiveX-компонентов, что, безусловно, противоречит всем существующим и несуществующим политикам информационной безопасности, поэтому давайте начнём с более простой вещи SDK для работы с нашим API. Из средств разработки в Экселе доступен VBA, для него мы и создадим SDK.

Для того, чтобы выполнить API-запрос, необходимо:

  1. Сформировать URL и тело POST-запроса.

  2. Добавить аутентификационные параметры.

  3. Непосредственно выполнить запрос.

  4. Распарсить результат (в нашем случае это JSON).

Формируем URL и тело POST-запроса

Первая часть, казалось бы, самая простая: нужно просто закодировать параметры в URL-кодировку и склеить их. Но в стандартом VBA не предусмотрена URL-кодировка (позже мы поймём, почему). Ничего страшного, на просторах Интернета есть множество разных решений, выберем одно из них.

Public Function URL_Encode(ByRef txt As String) As String    Dim buffer As String, i As Long, c As Long, n As Long    buffer = String$(Len(txt) * 12, "%")     For i = 1 To Len(txt)        c = AscW(Mid$(txt, i, 1)) And 65535         Select Case c            Case 48 To 57, 65 To 90, 97 To 122, 45, 46, 95  ' Unescaped 0-9A-Za-z-._ '                n = n + 1                Mid$(buffer, n) = ChrW(c)            Case Is <= 127            ' Escaped UTF-8 1 bytes U+0000 to U+007F '                n = n + 3                Mid$(buffer, n - 1) = Right$(Hex$(256 + c), 2)            Case Is <= 2047           ' Escaped UTF-8 2 bytes U+0080 to U+07FF '                n = n + 6                Mid$(buffer, n - 4) = Hex$(192 + (c \ 64))                Mid$(buffer, n - 1) = Hex$(128 + (c Mod 64))            Case 55296 To 57343       ' Escaped UTF-8 4 bytes U+010000 to U+10FFFF '                i = i + 1                c = 65536 + (c Mod 1024) * 1024 + (AscW(Mid$(txt, i, 1)) And 1023)                n = n + 12                Mid$(buffer, n - 10) = Hex$(240 + (c \ 262144))                Mid$(buffer, n - 7) = Hex$(128 + ((c \ 4096) Mod 64))                Mid$(buffer, n - 4) = Hex$(128 + ((c \ 64) Mod 64))                Mid$(buffer, n - 1) = Hex$(128 + (c Mod 64))            Case Else                 ' Escaped UTF-8 3 bytes U+0800 to U+FFFF '                n = n + 9                Mid$(buffer, n - 7) = Hex$(224 + (c \ 4096))                Mid$(buffer, n - 4) = Hex$(128 + ((c \ 64) Mod 64))                Mid$(buffer, n - 1) = Hex$(128 + (c Mod 64))        End Select    Next    URL_Encode = Left$(buffer, n)End Function

Следующий нюанс передача даты и времени. В API Voximplant временные метки принимаются в UTC в формате YYYY-MM-DD hh:mm:ss. В Excel же дата и время хранятся без учёта часового пояса (на самом деле, в самой таблице они вообще хранятся как число с плавающей точкой). Поэтому нам придётся принимать дату/время из таблицы тоже UTC. Мы думаем, что все 100+ миллионов пользователей Excel знают, что такое UTC, и это не вызовет у них никаких вопросов.

Кстати, в VBA есть функция форматирования даты, и она даже работает, но весьма необычным образом. Интересующий нас формат даты описывается так: yyyy-mm-dd hh:mm:ss. То есть mm это либо месяц, либо минуты в зависимости от того, за чем оно следует: за hhили за yyyy (это не шутка, это даже в MSDN описано). В общем, если кто-то захочет вывести время без часов, придётся импровизировать.

Переходим к аутентификации

Здесь нас ожидает самое большое разочарование. Мы в Voximplant предлагаем нашим клиентам использовать JWT, что, конечно, весьма мудрёно, если выполнять запросы из консоли или браузера, но при использовании наших SDK это совершенно никак не усложняет жизнь разработчику. В то же время JWT обеспечивает крайне высокий уровень безопасности.

А что же VBA? К сожалению, разумно простого способа сформировать JWT-подпись просто не существует. Причина в том, что в VBA доступен фреймворк .NET версии 4.x, а функция RSA.ImportPkcs8PrivateKey, необходимая для загрузки приватного ключа из PKCS8, появилась только в .NET 5. Да и вообще, все .NET-разработчики используют для таких задач сторонние библиотеки.

Поэтому нам придётся ограничиться авторизацией с помощью статического API-ключа. Ведь, как известно, один из способов обработки риска информационной безопасности - принятие этого риска. Так и поступим. Ведь нам ну очень-очень нужен этот SDK.

Кадр из кинофильма Большой Лебовски (The Big Lebowski (1998), Polygram Filmed Entertainment, Working Title Films)Кадр из кинофильма Большой Лебовски (The Big Lebowski (1998), Polygram Filmed Entertainment, Working Title Films)

Выполняем запрос

Переходим к третьей части к выполнению самого запроса. Встроенных средств работы с HTTP в VBA нет (теперь понятно, почему нет и функции URL-кодирования, а зачем?).

Но, тем не менее, это достаточно тривиальная манипуляция подключаем необходимый фреймворк MSXML 6.0 и Microsoft Scripting Runtime и выполняем запрос, подключая через COM сам MSXML. Просто!

Function makeRequest(name As String, params As Dictionary, accountId As Integer, apiKey As String) As Object    Dim objHTTP As New MSXML2.XMLHTTP60    Dim jsonData As String    Dim parsedJson As Object    Dim postString As String    postString = ""        Dim iterKey As Variant        For Each iterKey In params.Keys        postString = postString & "&" & iterKey & "=" & URL_Encode(params(iterKey))    Next    Url = "https://api.voximplant.com/platform_api/" + name    objHTTP.Open "POST", Url, False    objHTTP.send "account_id=" & accountId & "&api_key=" & apiKey & postString    jsonData = objHTTP.responseText    Set parsedJson = JsonConverter.ParseJson(jsonData)    Set makeRequest = parsedJsonEnd Function

Парсим JSON

Ну и, наконец, JSON. Как и всё остальное, парсер JSON надо искать где-то вовне экосистемы VBA. К счастью, на дворе 2021 год, есть GitHub, и кто-то уже озадачился созданием JSON-парсера для VBA. Мы взяли вот такой.

Он подключается как отдельный модуль и превращает JSON-строку в Dictionary. То, что нужно!

Дальше берём генератор одного из наших SDK (мы взяли питоновский), заменяем шаблоны и заставляем его генерировать код на VBA. В итоге получаем готовый SDK, который можно скачать на нашем GitHub.

SDK представляет собой Class Module, который можно подключить к вашей любимой книге в Excel и делать с его помощью разные странные вещи. В принципе, можно даже звонки запускать, SDK поддерживает все необходимые для этого функции API в). Но это будет не лучшей идеей, учитывая, что не до конца известно, когда Excel решит пересчитать все формулы (именно в этот момент произойдёт вызов функции API).

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

Для этого пишем вот такую функцию:

Function getTotalCallCost(FromDate, ToDate, Username) As Double    Dim totalCost As Double    Dim lastCount As Integer    Dim offset As Integer    Dim res As Dictionary    Dim RecordsPerRequest As Integer    Dim api As New VoximplantAPI    totalCost = 0    lastCount = 1    offset = 0    RecordsPerRequest = 100        'Pass Voximplant account id and API key    api.SetCredentials 100, "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"        Do While lastCount > 0        Set res = api.GetCallHistory(FromDate, ToDate, remote_number:=Username, with_calls:=True, with_records:=True, with_other_resources:=True, offset:=offset, count:=RecordsPerRequest)                Dim session As Variant        Dim item As Variant                For Each session In res("result")            For Each item In session("calls")                totalCost = totalCost + item("cost")            Next            For Each item In session("records")                totalCost = totalCost + item("cost")            Next            For Each item In session("other_resource_usage")                totalCost = totalCost + item("cost")            Next        Next                lastCount = res("count")        offset = offset + RecordsPerRequest    Loop        getTotalCallCost = totalCostEnd Function

И вызываем её следующим образом:

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


Резюме:

При желании можно и для VBA сделать какое-то подобие SDK. При его создании не пострадал ни один разработчик. Ах да, с 1 апреля! :D

Подробнее..

Как мы подружили Flutter с CallKit Call Directory

21.04.2021 14:19:32 | Автор: admin

Flutter+CallKitCallDirectory=Love


Привет!


В этом лонгриде я расскажу о том, как мы в Voximplant пришли к реализации собственного Flutter плагина для использования CallKit во Flutter приложении, и в итоге оказались первыми, кто сделал поддержку блокировки/определения номеров через Call Directory для Flutter.


Что такое CallKit


Apple CallKit это фреймворк для интеграции звонков стороннего приложения в систему.


Если звонок из стороннего приложения отображается как нативный, то тут задействован CallKit. Если звонок из стороннего приложения отображается в списке звонков системного приложения Phone тоже CallKit. Сторонние приложения, выступающие в качестве определителя номера CallKit. Звонки из сторонних приложений, которые не могут пробиться через режим Не беспокоить ну вы поняли.



CallKit предоставляет сторонним разработчикам системный UI для отображения звонков



А что с CallKit на Flutter?


CallKit является частью iOS SDK, во Flutter он не представлен, однако доступ к нему из Flutter возможен путём взаимодействия с нативным кодом. Для использования функциональности этого фреймворка потребуется подключить сторонний плагин, инкапсулирующий взаимодействие Flutter с iOS, или реализовывать всё самостоятельно, например, так:



Пример реализации CallKit сервиса для Flutter, где код iOS приложения (platform code) связывает приложение Flutter с системой




Готовые решения с CallKit на Flutter


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


Существующие плагины частично или полностью оборачивали CallKit API в собственный высокоуровневый API. Таким образом терялась гибкость, а некоторые возможности становились недоступными. Из-за собственной реализации архитектуры и интерфейсов такие плагины содержали свои баги. Документация хромала или отсутствовала, а авторы некоторых из них прекратили поддержку почти сразу, что особенно опасно на быстроразвивающемся Flutter.



Как мы пришли к созданию своего решения


Для простых сценариев на первое время это было приемлемо, однако, как только появлялся специфичный кейс, тут же появлялись неудобства. Приходилось изучать исходный код, чтобы выяснить, как именно этот плагин взаимодействует с CallKit. В конце концов могло обнаружиться, что реализовать требуемое вообще не выйдет из-за ограничений, накладываемых высокоуровневым API.


Мы задумались о том, чтобы реализовать своё решение с учетом этих недостатков.


Хотелось пойти по пути сохранения архитектуры и интерфейсов CallKit. Таким образом оставить пользователям всю гибкость, возможность использовать оригинальную документацию и оградить от потенциальных багов в собственной реализации.



Наша Реализация


Нам удалось перенести всё CallKit API на Dart с сохранением иерархии классов и механизмов взаимодействия с ними.



Наш плагин закрывает собой всю работу с платформой, при этом предоставляет идентичный интерфейс


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


Например, нативное CallKit API CXProviderDelegate.provider(_:execute:) требует синхронно возвращать Bool значение:


optional func provider(_ provider: CXProvider,     execute transaction: CXTransaction) -> Bool

Этот метод вызывается каждый раз, когда нужно обработать новую транзакцию CXTransaction. Можно вернуть true, чтобы обработать транзакцию самостоятельно и уведомить об этом систему. Вернув false, получим дефолтное поведение, при котором для каждого CXAction, содержащегося в транзакции, будет вызван соответствующий метод обработчик в CXProviderDelegate.


Для переноса этого API в плагин требовалось объявить его в Dart коде так, чтобы пользователь мог управлять этим поведением, несмотря на асинхронный характер обмена данными между платформами. Возвращая в нативном коде значение true, мы смогли перенести управление транзакциями в Dart код, где выполняем ручную или автоматическую обработку CXTransaction в зависимости от значения, полученного от пользователя.


Проблемы с асинхронностью возникают и в нативной части. Например, есть iOS фреймворк PushKit, он не является частью CallKit, но часто они используются вместе, так что интеграция с ним была необходима. При получении VoIP пуша требуется немедленно уведомить CallKit о входящем звонке в нативном коде, в противном случае приложение упадет. Для обработки этого нюанса мы решили дать возможность репортить входящие звонки напрямую в CallKit из нативного кода без асинхронного крюка в виде Flutter. В итоге для этой интеграции реализовали несколько хелперов в нативной части плагина (доступны через FlutterCallkitPlugin iOS класс) и несколько на стороне Flutter (доступны через FCXPlugin Dart класс).


Дополнительные возможности плагина мы объявили в его собственном классе, чтобы отделить интерфейс плагина от интерфейса CallKit.

Как зарепортить входящий звонок напрямую в CallKit

При получении VoIP пуша вызывается один из методов PKPushRegistryDelegate.pushRegistry(_: didReceiveIncomingPushWith:). Здесь необходимо создать экземпляр CXProvider и вызвать reportNewIncomingCall для уведомления CallKit о звонке. Так как для дальнейшей работы со звонком необходим тот же экземпляр провайдера, мы добавили метод FlutterCallkitPlugin.reportNewIncomingCallWithUUID с нативной стороны плагина. При его вызове плагин сам зарепортит звонок в CXProvider, а так же вызовет FCXPlugin.didDisplayIncomingCall хендлер на стороне Dart для продолжения работы со звонком.


func pushRegistry(_ registry: PKPushRegistry,                  didReceiveIncomingPushWith payload: PKPushPayload,                  for type: PKPushType,                  completion: @escaping () -> Void) {    // Достаем необходимые данные из пуша    guard let uuidString = payload["UUID"] as? String,        let uuid = UUID(uuidString: uuidString),        let localizedName = payload["identifier"] as? String    else {        return    }    let callUpdate = CXCallUpdate()    callUpdate.localizedCallerName = localizedName    let configuration = CXProviderConfiguration(        localizedName: "ExampleLocalizedName"    )        // Репортим звонок в плагин, а он зарепортит его в CallKit    FlutterCallkitPlugin.sharedInstance.reportNewIncomingCall(        with: uuid,        callUpdate: callUpdate,        providerConfiguration: configuration,        pushProcessingCompletion: completion    )}


Подводя итог: главной фишкой нашего плагина является то, что его использование на Flutter практически не отличается от использования нативного CallKit на iOS.


One more thing


Но оставалось ещё кое-что в Apple CallKit, что мы не реализовали у себя (и не реализовал никто в доступных сторонних решениях). Это поддержка Call Directory App Extension.



Что такое Call Directory


CallKit умеет блокировать и определять номера, доступ к этим возможностям для разработчиков открыт через специальное системное расширение Call Directory. Подробнее про iOS app extensions можно почитать в App Extension Programming Guide.



Call Directory app extension позволяет блокировать и/или идентифицировать номера


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


Например, при получении входящего звонка iOS пытается определить или найти звонящего в списке заблокированных стандартными средствами. Если номер не был найден, система может запросить данные у доступных Call Directory расширений, чтобы так или иначе обработать звонок. В этот момент расширение должно эти номера достать из некого хранилища номеров. Само приложение может заполнять это хранилище номерами из своих баз в любое время. Таким образом, взаимодействия между расширением и приложением нет, обмен данными происходит через общее хранилище.



Пример архитектуры для реализации Call Directory


Примеры с передачей номеров в Call Directory уже есть на хабре: раз и два.


Подробнее про iOS App Extensions: App Extension Programming Guide.



Call Directory Extension на Flutter


Не так давно нам написал пользователь с запросом на добавление поддержки Call Directory. Начав изучать возможность реализации этой фичи, мы выяснили, что сделать Flutter API без необходимости написания пользователями нативного кода не выйдет. Проблема заключается в том, что, как было сказано выше, Call Directory работает в расширении. Оно запускается системой, работает очень короткое время и не зависит от приложения (и в том числе от Flutter). Таким образом, для поддержки этого функционала пользователю плагина так или иначе потребуется реализовать app extension и хранилище данных самостоятельно.



Пример работы с Call Directory во Flutter приложении



Принятое решение


Несмотря на сложности с нативным кодом, мы твёрдо решили сделать использование Call Directory максимально удобным для пользователей нашего фреймворка.


Проверив возможность работы такого расширения в связке с Flutter приложением, мы принялись за проектирование. Решение должно было сохранить все Call Directory Manager API, а также требовать от пользователя минимум написания нативного кода и быть удобным для взаимодействия через Flutter.


Так мы сделали версию 1.2.0 с поддержкой Call Directory Extension.



Как мы реализовывали Call Directory для Flutter


Итак, для реализации этого функционала требовалось учесть несколько аспектов:


  • Перенести интерфейс класса CXCallDirectoryManager (CallKit объект позволяющий управлять Call Directory)
  • Решить, что делать с app extension и хранилищем номеров для него
  • Создать удобный способ передачи данных из Dart в натив и обратно для управления списками номеров из Flutter приложения


Перенос интерфейсов CXCallDirectoryManager во Flutter


Код, приведенный в статье, был специально упрощен для облегчения восприятия, полную версию кода можно найти по ссылкам в конце статьи. Для реализации плагина мы использовали Objective-C, так как он был выбран основным в проекте ранее. Интерфейсы CallKit представлены на Swift для простоты.


Интерфейс


Первым делом посмотрим, что конкретно требуется перенести:


extension CXCallDirectoryManager {    public enum EnabledStatus : Int {        case unknown = 0        case disabled = 1        case enabled = 2    }}open class CXCallDirectoryManager : NSObject {    open class var sharedInstance: CXCallDirectoryManager { get }    open func reloadExtension(        withIdentifier identifier: String,        completionHandler completion: ((Error?) -> Void)? = nil    )    open func getEnabledStatusForExtension(        withIdentifier identifier: String,        completionHandler completion: @escaping (CXCallDirectoryManager.EnabledStatus, Error?) -> Void    )    open func openSettings(        completionHandler completion: ((Error?) -> Void)? = nil    )}

Воссоздадим аналог CXCallDirectoryManager.EnabledStatus энама в Dart:


enum FCXCallDirectoryManagerEnabledStatus {  unknown,  disabled,  enabled}

Теперь можно объявить класс и методы. Необходимости в sharedInstance в нашем интерфейсе нет, так что сделаем обычный Dart класс со static методами:


class FCXCallDirectoryManager {  static Future<void> reloadExtension(String extensionIdentifier) async { }  static Future<FCXCallDirectoryManagerEnabledStatus> getEnabledStatus(    String extensionIdentifier,  ) async { }  static Future<void> openSettings() async { }}

Сохранение API важно, но так же важно учитывать платформенные и языковые code-style, чтобы использование интерфейса было понятно и удобно для пользователей плагина.


Для API в Dart мы использовали более короткое название без слов-связок (длинное название пришло из objective-C) и заменили completion блок на Future. Future является стандартным механизмом, используемым для получения результата выполнения асинхронных методов в Dart. Мы также возвращаем Future из большинства Dart методов плагина, потому что коммуникация с нативным кодом происходит асинхронно.


Было getEnabledStatusForExtension(withIdentifier:completionHandler:)


Стало Future getEnabledStatus(extensionIdentifier)




Реализация


Для коммуникации между Flutter и iOS будем использовать FlutterMethodChannel.


Подробнее про особенности работы этого канала связи можно почитать здесь.



On the Flutter side


Создадим объект MethodChannel:


const MethodChannel _methodChannel =  const MethodChannel('plugins.voximplant.com/flutter_callkit');


On the iOS side


Первым делом iOS класс плагина нужно подписать на протокол FlutterPlugin, чтобы иметь возможность взаимодействовать с Flutter:


@interface FlutterCallkitPlugin : NSObject<FlutterPlugin>@end

При инициализации плагина создадим FlutterMethodChannel с таким же идентификатором, что мы использовали выше:


+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {    FlutterMethodChannel *channel        = [FlutterMethodChannel           methodChannelWithName:@"plugins.voximplant.com/flutter_callkit"          binaryMessenger:[registrar messenger]];    FlutterCallkitPlugin *instance         = [FlutterCallkitPlugin sharedPluginWithRegistrar:registrar];    [registrar addMethodCallDelegate:instance channel:channel];}

Теперь можно использовать этот канал для вызова iOS методов из Flutter.



Рассмотрим подробно реализацию методов в Dart и нативной части плагина на примере getEnabledStatus.



On the Flutter side


Реализация на Dart будет максимально проста и будет заключаться в вызове MethodChannel.invokeMethod с необходимыми аргументами, а также в обработке результата этого вызова.


Про MethodChannel

MethodChannel API позволяет асинхронно получить результат вызова из нативного кода посредством Future, но накладывает ограничения на передаваемые типы данных.




Итак, нам потребуется передать имя метода (его будем использовать в нативном коде для того, чтобы идентифицировать вызов) и аргумент extensionIdentifier в MethodChannel.invokeMethod, а затем преобразовать результат из простейшего типа int в FCXCallDirectoryManagerEnabledStatus. На случай ошибки в нативном коде следует обработать PlatformException.


static Future<FCXCallDirectoryManagerEnabledStatus> getEnabledStatus(  String extensionIdentifier,) async {  try {    // Воспользуемся объектом MethodChannel для вызова    // соответствующего метода в платформенном коде    // с аргументом extensionIdentifier.    int index = await _methodChannel.invokeMethod(      'Plugin.getEnabledStatus',      extensionIdentifier,    );    // Преобразуем результат в энам     // FCXCallDirectoryManagerEnabledStatus    // и вернем его значение пользователю    return FCXCallDirectoryManagerEnabledStatus.values[index];  } on PlatformException catch (e) {    // Если что-то пошло не так, обернем ошибку в собственный тип     // и отдадим пользователю    throw FCXException(e.code, e.message);  }}

Обратите внимание на идентификатор метода который мы использовали:


Plugin.getEnabledStatus


Слово перед точкой используется, для определения модуля ответственного за тот или иной метод.


getEnabledStatus идентично названию метода во Flutter, а не в iOS (или Android).




On the iOS side


Теперь переместимся в платформенный код и реализуем бэкенд для этого метода.


Вызовы через FlutterMethodChannel попадают в метод handleMethodCall:result:.


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


- (void)handleMethodCall:(FlutterMethodCall*)call                  result:(FlutterResult)result {    // Вызовы из Flutter можно идентифицировать по названию,    // которое передается в `FlutterMethodCall.method` проперти    if ([@"Plugin.getEnabledStatus" isEqualToString:call.method]) {        // При передаче аргументов с помощью MethodChannel,         // они упаковываются в `FlutterMethodCall.arguments`        // Извлечем extensionIdentifier, который         // мы передали сюда ранее из Flutter кода        NSString *extensionIdentifier = call.arguments;        if (isNull(extensionIdentifier)) {            // Если аргументы не валидны, вернём ошибку через             // `result` обработчик            // Ошибка должна быть упакована в `FlutterError`            // Она вылетит в виде PlatformException в Dart коде            result([FlutterError errorInvalidArguments:@"extensionIdentifier must not be null"]);            return;}        // Теперь, когда метод обнаружен,        // а аргументы извлечены и провалидированы,         // можно реализовать саму логику        // Для взаимодействия с этой функциональностью CallKit // потребуется экземпляр CallDirectoryManager        CXCallDirectoryManager *manager             = CXCallDirectoryManager.sharedInstance;        // Вызываем метод CallDirectoryManager        // с требуемой функциональностью        // и ожидаем результата        [manager             getEnabledStatusForExtensionWithIdentifier:extensionIdentifier            completionHandler:^(CXCallDirectoryEnabledStatus status,                                            NSError * _Nullable error) {            // completion с результатом вызова запустился,             // можем пробросить результат в Dart            // предварительно сконвертировав его в подходящие типы,             // так как через MethodChannel можно передавать            // лишь некоторые определенные типы данных.            if (error) {                // Ошибки передаются упакованные в `FlutterError`                result([FlutterError errorFromCallKitError:error]);            } else {                // Номера передаются упакованные в `NSNumber`                // Так как этот энам представлен значениями `NSInteger`,                 // выполним требуемое преобразование                result([self convertEnableStatusToNumber:enabledStatus]);            }}];    }}


По аналогии реализуем оставшиеся два метода FCXCallDirectoryManager



On the Flutter side


static Future<void> reloadExtension(String extensionIdentifier) async {  try {    // Задаем идентификатор, передаем аргумент     // и вызываем платформенный метод    await _methodChannel.invokeMethod(      'Plugin.reloadExtension',      extensionIdentifier,    );  } on PlatformException catch (e) {    throw FCXException(e.code, e.message);  }}static Future<void> openSettings() async {  try {    // А этот метод не принимает аргументов     await _methodChannel.invokeMethod(      'Plugin.openSettings',    );  } on PlatformException catch (e) {    throw FCXException(e.code, e.message);  }}


On the iOS side


if ([@"Plugin.reloadExtension" isEqualToString:call.method]) {    NSString *extensionIdentifier = call.arguments;    if (isNull(extensionIdentifier)) {        result([FlutterError errorInvalidArguments:@"extensionIdentifier must not be null"]);        return;    }    CXCallDirectoryManager *manager         = CXCallDirectoryManager.sharedInstance;    [manager         reloadExtensionWithIdentifier:extensionIdentifier        completionHandler:^(NSError * _Nullable error) {        if (error) {            result([FlutterError errorFromCallKitError:error]);        } else {            result(nil);        }    }];}if ([@"Plugin.openSettings" isEqualToString:call.method]) {    if (@available(iOS 13.4, *)) {        CXCallDirectoryManager *manager             = CXCallDirectoryManager.sharedInstance;        [manager             openSettingsWithCompletionHandler:^(NSError * _Nullable error) {            if (error) {                result([FlutterError errorFromCallKitError:error]);            } else {                result(nil);            }        }];    } else {        result([FlutterError errorLowiOSVersionWithMinimal:@"13.4"]);    }}


Готово, CallDirectoryManager реализован и может быть использован.


Подробнее про Platform-Flutter взаимодействие



App Extension и хранилище номеров


Так как из-за нахождения Call Directory в iOS расширении мы не сможем предоставить его реализацию с плагином, а работа с платформенным кодом обычно непривычна для Flutter разработчиков, не знакомых с нативной разработкой, постараемся по максимуму помочь им с помощью Документации!


Реализуем полноценный пример app extension и хранилища и подключим их к example app нашего плагина.


В качестве простейшего варианта хранилища используем UserDefaults, которые обернем в propertyWrapper.


Примерно так выглядит интерфейс нашего хранилища:


// Доступ к хранилищу из iOS приложения@UIApplicationMainfinal class AppDelegate: FlutterAppDelegate {    @UserDefault("blockedNumbers", defaultValue: [])    private var blockedNumbers: [BlockableNumber]    @UserDefault("identifiedNumbers", defaultValue: [])    private var identifiedNumbers: [IdentifiableNumber]}// Доступ к хранилищу из app extensionfinal class CallDirectoryHandler: CXCallDirectoryProvider {    @UserDefault("blockedNumbers", defaultValue: [])    private var blockedNumbers: [BlockableNumber]    @UserDefault("identifiedNumbers", defaultValue: [])    private var identifiedNumbers: [IdentifiableNumber]    @NullableUserDefault("lastUpdate")    private var lastUpdate: Date?}


Код имплементации хранилища:


UserDefaults


Код iOS приложения:


iOS App Delegate


Код iOS расширения:


iOS App Extension


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


Передача номеров из Flutter в iOS и обратно


Итак, app extension настроен и связан с хранилищем, необходимые методы CallDirectoryManager реализованы, осталась последняя деталь научиться передавать номера из Flutter в платформенное хранилище или, наоборот, запрашивать номера оттуда.


Наиболее простым вариантом кажется взвалить передачу данных на пользователя плагина, тогда ему придется самостоятельно организовывать MethodChannel или использовать другие сторонние решения по управлению хранилищем. И, безусловно, кому-то это даже подойдет! :) А для остальных сделаем простое и удобное API, чтобы пробрасывать номера прямо через наш фреймворк. Этот функционал будем делать опциональным, чтобы не ограничивать тех, кому удобнее использовать свои способы передачи данных.



Интерфейс


Посмотрим, какие интерфейсы могут понадобиться:


  • Добавление блокируемых/идентифицируемых номеров в хранилище
  • Удаление блокируемых/идентифицируемых номеров из хранилища
  • Запрос блокируемых/идентифицируемых номеров из хранилища


On the Flutter side


Для методов-хелперов мы ранее решили использовать классы плагина FCXPlugin (Flutter) и FlutterCallkitPlugin (iOS). Однако Call Directory является узкоспециализированным функционалом, который используется далеко не в каждом проекте. Поэтому хотелось вынести это в отдельный файл, но оставить доступ через объект класса FCXPlugin, для этого подойдет extension:


extension FCXPlugin_CallDirectoryExtension on FCXPlugin {  Future<List<FCXCallDirectoryPhoneNumber>> getBlockedPhoneNumbers()    async { }  Future<void> addBlockedPhoneNumbers(    List<FCXCallDirectoryPhoneNumber> numbers,  ) async { }  Future<void> removeBlockedPhoneNumbers(List<FCXCallDirectoryPhoneNumber> numbers,  ) async { }  Future<void> removeAllBlockedPhoneNumbers() async { }  Future<List<FCXIdentifiablePhoneNumber>> getIdentifiablePhoneNumbers()    async { }  Future<void> addIdentifiablePhoneNumbers(List<FCXIdentifiablePhoneNumber> numbers,  ) async { }  Future<void> removeIdentifiablePhoneNumbers(List<FCXCallDirectoryPhoneNumber> numbers,  ) async { }  Future<void> removeAllIdentifiablePhoneNumbers() async { }}


On the iOS side


Чтобы со стороны Flutter получить доступ к номерам, которые находятся в неком хранилище на стороне iOS, пользователю плагина нужно будет как-то связать свою базу номеров с плагином. Для этого дадим ему такой интерфейс:



@interface FlutterCallkitPlugin : NSObject<FlutterPlugin>@property(strong, nonatomic, nullable)NSArray<FCXCallDirectoryPhoneNumber *> *(^getBlockedPhoneNumbers)(void);@property(strong, nonatomic, nullable)void(^didAddBlockedPhoneNumbers)(NSArray<FCXCallDirectoryPhoneNumber *> *numbers);@property(strong, nonatomic, nullable)void(^didRemoveBlockedPhoneNumbers)(NSArray<FCXCallDirectoryPhoneNumber *> *numbers);@property(strong, nonatomic, nullable)void(^didRemoveAllBlockedPhoneNumbers)(void);@property(strong, nonatomic, nullable)NSArray<FCXIdentifiablePhoneNumber *> *(^getIdentifiablePhoneNumbers)(void);@property(strong, nonatomic, nullable)void(^didAddIdentifiablePhoneNumbers)(NSArray<FCXIdentifiablePhoneNumber *> *numbers);@property(strong, nonatomic, nullable)void(^didRemoveIdentifiablePhoneNumbers)(NSArray<FCXCallDirectoryPhoneNumber *> *numbers);@property(strong, nonatomic, nullable)void(^didRemoveAllIdentifiablePhoneNumbers)(void);@end


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


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


Реализация


Теперь реализуем связь между объявленными методами-хелперами во Flutter и обработчиками в iOS.


Методов много, а работают они +- одинаково, поэтому будем рассматривать два из них с противоположным направлением движения данных.

Get identifiable numbers



On the Flutter side


Future<List<FCXIdentifiablePhoneNumber>> getIdentifiablePhoneNumbers() async {  try {    // Вызываем платформенный метод и сохраняем результат    List<dynamic> numbers = await _methodChannel.invokeMethod(      'Plugin.getIdentifiablePhoneNumbers',    );    // Типизируем результат и возвращаем пользователю    return numbers      .map(        (f) => FCXIdentifiablePhoneNumber(f['number'], label: f['label']))      .toList();  } on PlatformException catch (e) {    throw FCXException(e.code, e.message);  }}


On the iOS side


if ([@"Plugin.getIdentifiablePhoneNumbers" isEqualToString:call.method]) {    if (!self.getIdentifiablePhoneNumbers) {        // Проверяем существует-ли обработчик,        // если нет  возвращаем ошибку        result([FlutterError errorHandlerIsNotRegistered:@"getIdentifiablePhoneNumbers"]);        return;    }    // Используя обработчик, запрашиваем номера у пользователя    NSArray<FCXIdentifiablePhoneNumber *> *identifiableNumbers        = self.getIdentifiablePhoneNumbers();    NSMutableArray<NSDictionary *> *phoneNumbers        = [NSMutableArray arrayWithCapacity:identifiableNumbers.count];    // Оборачиваем каждый номер в словарь,     // чтобы иметь возможность передать их через MethodChannel     for (FCXIdentifiablePhoneNumber *identifiableNumber in identifiableNumbers) {        NSMutableDictionary *dictionary             = [NSMutableDictionary dictionary];        dictionary[@"number"]             = [NSNumber numberWithLongLong:identifiableNumber.number];        dictionary[@"label"]             = identifiableNumber.label;        [phoneNumbers addObject:dictionary];    }    // Отправляем номера во Flutter    result(phoneNumbers);}


Add identifiable numbers



On the Flutter side


Future<void> addIdentifiablePhoneNumbers(  List<FCXIdentifiablePhoneNumber> numbers,) async {  try {    // Готовим номера для передачи через MethodChannel    List<Map> arguments = numbers.map((f) => f._toMap()).toList();    // Отправляем номера в нативный код    await _methodChannel.invokeMethod(      'Plugin.addIdentifiablePhoneNumbers',      arguments    );  } on PlatformException catch (e) {    throw FCXException(e.code, e.message);  }}


On the iOS side


if ([@"Plugin.addIdentifiablePhoneNumbers" isEqualToString:call.method]) {    if (!self.didAddIdentifiablePhoneNumbers) {        // Проверяем существует-ли обработчик,        // если нет  возвращаем ошибку        result([FlutterError errorHandlerIsNotRegistered:@"didAddIdentifiablePhoneNumbers"]);        return;    }    // Достаем переданные в аргументах номера    NSArray<NSDictionary *> *numbers = call.arguments;    if (isNull(numbers)) {        // Проверяем их валидность        result([FlutterError errorInvalidArguments:@"numbers must not be null"]);        return;    }    NSMutableArray<FCXIdentifiablePhoneNumber *> *identifiableNumbers        = [NSMutableArray array];    // Типизируем номера    for (NSDictionary *obj in numbers) {        NSNumber *number = obj[@"number"];        __auto_type identifiableNumber            = [[FCXIdentifiablePhoneNumber alloc] initWithNumber:number.longLongValue                                                                                     label:obj[@"label"]];        [identifiableNumbers addObject:identifiableNumber];    }    // Отдаём типизированные номера в обработчик пользователю    self.didAddIdentifiablePhoneNumbers(identifiableNumbers);    // Сообщаем во Flutter о завершении операции    result(nil);}


Остальные методы реализуются по аналогии, полный код:




Примеры использования


Теперь переместимся на сторону пользователя получившегося плагина и посмотрим, как он может воспользоваться нашими интерфейсами.



Reload extension


Метод reloadExtension(withIdentifier:completionHandler:) используется для перезагрузки расширения Call Directory. Это может потребоваться, например, после добавления новых номеров в хранилище, чтобы они попали в CallKit.


Использование идентично нативному CallKit API: обращаемся к FCXCallDirectoryManager и запрашиваем перезагрузку по заданному extensionIdentifier:


final String _extensionID =  'com.voximplant.flutterCallkit.example.CallDirectoryExtension';Future<void> reloadExtension() async {  await FCXCallDirectoryManager.reloadExtension(_extensionID);}


Get identified numbers



On the Flutter side


Запрашиваем список идентифицируемых номеров через класс плагина из Flutter:


final FCXPlugin _plugin = FCXPlugin();Future<List<FCXIdentifiablePhoneNumber>> getIdentifiedNumbers() async {  return await _plugin.getIdentifiablePhoneNumbers();}


On the iOS side


Добавляем обработчик getIdentifiablePhoneNumbers, который плагин использует для передачи заданных номеров во Flutter. Будем передавать в него номера из нашего хранилища identifiedNumbers:


private let callKitPlugin = FlutterCallkitPlugin.sharedInstance@UserDefault("identifiedNumbers", defaultValue: [])private var identifiedNumbers: [IdentifiableNumber]// Добавляем обработчик событий запроса номеровcallKitPlugin.getIdentifiablePhoneNumbers = { [weak self] in    guard let self = self else { return [] }    // Возвращаем номера из хранилища в обработчик    return self.identifiedNumbers.map {        FCXIdentifiablePhoneNumber(number: $0.number, label: $0.label)    }}


Теперь номера из пользовательского хранилища будут попадать в обработчик, а из него через плагин во Flutter.



Add identified numbers



On the Flutter side


Передаем номера, которые хотим идентифицировать, в объект плагина:


final FCXPlugin _plugin = FCXPlugin();Future<void> addIdentifiedNumber(String number, String id) async {  int num = int.parse(number);  var phone = FCXIdentifiablePhoneNumber(num, label: id);  await _plugin.addIdentifiablePhoneNumbers([phone]);}


On the iOS side


Добавляем обработчик didAddIdentifiablePhoneNumbers, который плагин использует для уведомления платформенного кода о получении новых номеров из Flutter. В обработчике сохраняем полученные номера в хранилище номеров:


private let callKitPlugin = FlutterCallkitPlugin.sharedInstance@UserDefault("identifiedNumbers", defaultValue: [])private var identifiedNumbers: [IdentifiableNumber]// Добавляем обработчик событий добавления номеровcallKitPlugin.didAddIdentifiablePhoneNumbers = { [weak self] numbers in    guard let self = self else { return }    // Сохраняем в хранилище номера, переданные плагином в обработчик    self.identifiedNumbers.append(        contentsOf: numbers.map {            IdentifiableNumber(identifiableNumber: $0)        }    )    // Номера в Call Directory обязательно должны быть отсортированы    self.identifiedNumbers.sort()}


Теперь номера из Flutter будут попадать в плагин, из него в обработчик события, а оттуда в пользовательское хранилище номеров. При следующей перезагрузке Call Directory расширения они станут доступны CallKit для идентификации звонков.


Полные примеры:




Итог


У нас получилось дать возможность использовать CallKit Call Directory из Flutter!


Детали платформенных коммуникаций по прежнему скрыты в недрах плагина, нативное API сохранено, а необходимая к написанию пользовательская iOS реализация хорошо задокументирована.


Теперь во Flutter можно относительно просто блокировать и/или определять номера с помощью нативного Call Directory.



Пример работы с Call Directory в Flutter приложении с использованием flutter_callkit_voximplant



Результаты:


  • Интерфейс CallDirectoryManager полностью перенесен
  • Добавлен простой способ передачи номеров из Flutter кода в iOS, оставлена возможность использовать собственные решения передачи данных
  • Архитектура решения описана в README с визуальными схемами для лучшего понимания
  • Добавлен полноценный работоспособный example app, использующий всю функциональность Call Directory, реализующий пример платформенных модулей (таких как iOS расширение и хранилище данных)


Полезные ссылки


Source код flutter_callkit на GitHub


Example app код на GitHub


Полная документация по использованию Call Directory с flutter_callkit


CallKit Framework Documentation by Apple


App Extension Programming Guide by Apple


Writing custom platform-specific code by Flutter

Подробнее..

Наш опыт работы с DeepPavlov голосовой помощник за 20 дней и приём 5000 звонков на горячей линии

21.07.2020 16:21:44 | Автор: admin
Когда объявили режим самоизоляции, на горячую линию по коронавирусу в Татарстане поступало множество вопросов от жителей. Чтобы разгрузить операторов коллцентра, мы в Центре Цифровой Трансформации республики вместе с уполномоченным по ИИ в Татарстане разработали голосового помощника, который отвечал на несложные вопросы.



Для приема звонков мы использовали платформу Voximplant, а для распознавания вопросов и ответов DeepPavlov. Голосового помощника получилось запустить за две с половиной недели, и он помог обработать 5000 звонков. У нас получилось выкатить продукт, который помогал жителям Татарстана получать достоверную информацию от властей, да и просто выходить на улицу. Ниже расскажем, как мы это делали.

Какая задача стояла


Мы изначально задумывали сделать голосовой помощник, который бы помогал людям получать госуслуги и отвечать на часто задаваемые вопросы. Но когда началась вся канитель с коронавирусом, мы поняли, что Лилия поможет разгрузить коллцентр: например, консультировать на горячей линии, помогать получать цифровые пропуска и пособия по безработице. Все то, что человек может узнать на сайте госуслуг, можно узнать и у Лилии, а еще с ней можно просто поболтать.

Немного предыстории. 1 апреля в Татарстане ввели систему СМС-пропусков: чтобы выйти на улицу, нужно было отправить СМС на короткий номер с паспортными данными и указанием номера цели. Сначала целей было пять: доставка несовершеннолетних в образовательные организации, участие в похоронах, выезд на дачу, доставка лекарств и продуктов и изменение места проживания. Цели постепенно добавлялись и на последнем этапе их стало уже 12.

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


Кроме оформления цифровых пропусков, Лилия должна была отвечать на вопросы татарстанцев. Мы задумывали голосового помощника, который бы отвечал на распространенные вопросы, вроде Что нужно сделать чтобы получить пропуск и Помогает ли имбирь от коронавируса.

Для решения этой задачи мы могли бы сделать и обычный IVR. Но IVR долго, и если делать нормально, то нужно закладывать сложную архитектуру. В общем, времени на это не было. Мы подумали, что сделать голосового помощника (который умеет переводить речь в текст, обрабатывать его, классифицировать интенты и выдавать голосовой ответ) проще. Хе-хе.

Как это выглядит со стороны пользователя


Тут все просто.

  1. Человек звонит на горячую линию, ему предлагают выбрать между разговором с оператором и Лилией. На любом этапе можно переключиться на живого оператора.
  2. Лилия предлагает озвучить свой вопрос и человек его задает.
  3. Лилия отвечает и спрашивает, смогла ли она ответить на вопрос. Если ответ нерелевантный или Лилия не знает ответа на этот вопрос, то она просит его повторить или сформулировать по-другому.



Как обрабатываются звонки


Для приема звонков и реализации сценариев использовали платформу Voximplant: написали скрипт и подключали сигнал автоответчика. Лилия приветствовала человека и спрашивала, чего он хочет.

Звонящий задает вопрос по телефону. Затем Лилия начинала слушать. У VoxImplant мы использовали модуль ASR для перевода речи в текст, он под капотом вызывает модель Yandex.SpeechKit. Таким образом, аудиопоток переводится в текст, который потом проходит токенизацию и стемминг. Мы попробовали также извлекать признаки: NER, POS и Chunk для базовых методов ML, но все это занимало очень много времени.



Например, изначально, из вопроса Скажите, пожалуйста, как защититься от коронавируса мы стеммим защит и коронавирус и преобразуем их в векторное представление. Векторное представление сущностей попадает в классификатор по вопросам коронавируса (а позже и госуслуг). Теперь всем этим занимается DeepPavlov.

Дальше возможно несколько вариантов в зависимости от уровня уверенности (confidence):

  1. Если сеть классифицировала вопрос с достаточным confidence (исходя из исследования пороги подбирались индивидуально, поклассово), то Лилия даст ответ на вопрос.
  2. Если у сети малый confidence ответа, мы предполагаем, что это вопрос который мы не охватили в рамках нашего датасета (но вопрос при этом всё ещё относится к домену коронавируса) или человеку просто захотелось поговорить на другую тему. Например, спросил Кто такой Илон Маск.

Для таких вопросов мы использовали обученную на дампе Википедии модель BERT для задачи knowledge base question answering.

В конце Лилия обязательно верифицирует корректность распознавания интента и качество ответа, спросив: Ответила ли я на ваш вопрос?. Если пользователь ответит да, то Лилия будет ждать следующего вопроса. Если нет, то мы попросим переформулировать вопрос и пройдем весь цикл заново. Бывает так, что это не получается. Тогда в бой вступают живые операторы.

Сейчас Лилия говорит приятным голоском синтезатора речи от Яндекса мы немного поменяли тональность и увеличили скорость. Иногда Лилия путает ударения, но это можно исправить с помощью разметки. Конечно, хотелось бы добавить татарский, но пока это сложно.

Суммарно проект занял две с половиной недели с учетом датасетов: возникла идея, обсудили проект с министром и, как говорят у нас, алга. Неделя ушла на прикидку и исследование, еще дней 10 заняла разработка, затем мы дорабатывали и прикручивали дополнительные функции. Основными лошадками были Nvidia RTX2070. Для BERT'ов требовалось около 12-16 ГБ видеопамяти.

От LSVM и catboost до DeepPavlov


В процессе разработки мы использовали разные модели классификаторов. Сначала мы попробовали такие модели машинного обучения, как random forest, LSVM, catboost, logreg. Вообще точность моделей машинного обучения была не очень высокой. Почему так? Потому что многие вопросы пользователей очень похожи друг на друга: вопрос у меня ребенок заболел коронавирусом что делать достаточно похож на вопрос можно ли гулять с ребенком во время коронавируса, хотя это разные категории и требуются разные ответы.

Мы могли бы заняться извлечением сущностей, увеличением выборки, исследования. Но мы торопились. Поэтому мы решили использовать в работе библиотеку DeepPavlov от МФТИ, давшую точность 78% с логистической регрессией и BERT 84%.



Точность ответов зависит от разметки датасетов. У нас был список из 200 вопросов, которые люди задают на горячей линии, но они были неправильно разделены. Например, люди спрашивали, что делать, если у них заболел кто-то из родственников, и был вопрос, какие симптомы у коронавируса. Модели ML путались.

Результаты и планы на будущее


Лилия проработала 2 недели и обработала 5000 звонков. За это время Лилия существенно облегчила работу операторам горячей линии им не приходилось отвечать на банальные и повторяющиеся вопросы. Благодаря Лилии пользователи получали пропуска, ответы на вопросы и просто разговаривали. Конечно, были пользователи, которые ругались на нее и просили перевести на оператора.

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

Мы министерство, и у нас, по факту, две задачи: чтобы другие ведомства нормально пользовались технологиями и чтобы жители Татарстана могли легко и просто коммуницировать с государством. Со второй задачей прекрасно справляется портал госуслуг наш, местный, а не федеральный. Но этот портал сайт и приложение, до которых некоторым добраться все-таки тяжело. А если жители не идут на портал, то портал пойдет к жителю, то есть мы движемся в сторону того, чтобы упростить взаимодействие с порталом людей, которые не особо пользуются интернетом.

Сейчас минцифра Татарстана работает над тем, чтобы люди могли получить госуслуги голосом и с помощью чата. Мы хотим сделать универсального помощника, которому можно позвонить/написать и получить ответы на все важные вопросы.

Пока что Лилия может подсказать, до скольки работает ЗАГС, а в будущем мы планируем, что она сможет принимать показания счетчиков (но это только тогда, когда мы решим вопрос с безопасностью передачи этих данных). В общем, мы превращаем Лилию в отдельный продукт.

Если вам было бы интересно поучить Лилию, велком в нашу команду.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru