- PVSM.RU - https://www.pvsm.ru -

Как применить Twilio, Python и инструменты Google для автоматизации своей свадьбы

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

Для большинства людей 3 сентября 2016 было самой обычной субботой, но в моей памяти эта дата останется навсегда, ведь именно в этот день мы с моей супругой сыграли свадьбу.

image

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

Вы, возможно, удивитесь, узнав, что приглашение людей на свадьбу — дело дорогое (более 380 фунтов на человека). Сначала вам нужно отправить предварительные пригласительные с датой и коротким уведомлением, и только потом — полноценные, с более подробным описанием события. Все это, к тому же отправляется по почте, а значит, происходит медленно. Немало времени отнимают и попытки «поймать» приглашенных и получить от них ответ о том, хотят ли они прийти на праздник с бесплатной едой и выпивкой (хотя, казалось бы, кто не хочет?!). Ну и, наконец, рассылка приглашений не экологична, ведь бумажные открытки — вещь одноразовая, быстро забывается и становится никому не нужной.

Но вернемся к спискам. Гостей мы делим на несколько групп:

  1. Те, кого вы хотели бы видеть на празднике
  2. Те, кто ответил на второе приглашение с просьбой ответа
  3. Те, кто принял приглашение
  4. Те, кто принял приглашение и выбрал еду

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

Послание в бутылке

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

SMS [2] как коммуникационный канал прекрасно подходил под мои нужды. Я мог настроить массовую рассылку сообщений, быстро и эффективно обрабатывая ответы. Делая первые рабочие наброски продукта и рассматривая варианты БД, я пытался сделать нечто простое, чем можно было бы легко поделиться и не хотел уделять много времени внешнему виду. В итоге я наткнулся на python-библиотеку gspread, позволявшую читать из таблиц google [3] и писать в них. Это был не самый быстрый, но довольно гибкий вариант, предоставивший возможность легко получать доступ к таблицам и считывать результаты.

Для первого приглашения я создал таблицу [4] с тремя колонками:

  • Name (Имя)
  • Telephone_number (Телефонный номер)
  • Confirmation_status (Статус подтверждения)
  • Contact detail status (Статус контактных данных)
  • Message_count (Количество сообщений, отправленных гостю, пригодилось в будущем)

Завершив ввод основных данных, я прогнал список через gspread [5], который отправил SMS каждому гостю-обладателю мобильного номера:

import json
import time
import gspread
from oauth2client.client import SignedJwtAssertionCredentials
from twilio.rest import TwilioRestClient
 
# отправка сообщений гостям из таблицы
 
# добавляем имя файла для json, созданного для таблицы
json_key = json.load(open('.json'))
scope = ['https://spreadsheets.google.com/feeds']
 
credentials = SignedJwtAssertionCredentials(json_key['client_email'],
                                            json_key['private_key'].encode(),
                                            scope)
gc = gspread.authorize(credentials)
wks = gc.open("wedding_guests")  # здесь добавляем имя своего workbook-файла
wks_attendees = wks.get_worksheet(0)  # обращаемся к списку посетителей
 
ACCOUNT_SID = 'TWILIO_ACCOUNT_SID'
AUTH_TOKEN = 'TWILIO_AUTH_TOKEN'
 
client = TwilioRestClient(ACCOUNT_SID, AUTH_TOKEN)
 
# проходимся по списку гостей, значения внутри range заменяем на свои
for num in range(2, 60):
    print "sleeping for 2 seconds"
    time.sleep(2)  # добавляем задержку чтобы мобильный оператор не отфильтровал сообщения
 
    guest_number = wks_attendees.acell('B'+str(num)).value
    guest_name = wks_attendees.acell('A'+str(num)).value
    Message_body = <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2B50</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2764</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2B50</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2764</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2B50</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2764</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2B50</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2764</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-pds">"</span><span class="pl-cce">nn</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2709</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span><span class="pl-s"><span class="pl-pds">"</span> Save the date! <span class="pl-pds">"</span></span><span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2709</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span><span class="pl-s"><span class="pl-pds">"</span><span class="pl-cce">nn</span>Lauren Pang and Thomas Curtis are delighted to invite you to our wedding.<span class="pl-cce">nn</span>Saturday 3rd September 2016. <span class="pl-cce">nn</span>Colville Hall,<span class="pl-cce">n</span>Chelmsford Road,<span class="pl-cce">n</span>White Roding,<span class="pl-cce">n</span>CM6 1RQ.<span class="pl-cce">nn</span>The Ceremony begins at 2pm.<span class="pl-cce">nn</span>More details will follow shortly!<span class="pl-cce">nn</span>Please text YES if you are saving the date and can join us or text NO if sadly, you won't be able to be with us.<span class="pl-cce">nn</span><span class="pl-pds">"</span></span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2B50</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2764</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2B50</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2764</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2B50</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2764</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2B50</span><span class="pl-pds">"</span></span> <span class="pl-k">+</span> <span class="pl-s"><span class="pl-k">u</span><span class="pl-pds">"</span><span class="pl-cce">u2764</span><span class="pl-pds">"</span></span>,
    if not guest_number:  # гостей без мобильного номера пропускаем
        print guest_name + ' telephone number empty not messaging'
        wks_attendees.update_acell('E'+str(num), '0')  
 
    else:
        print 'Sending message to ' + guest_name
        client.messages.create(
            to="+" + guest_number,  # Добавляем + для соответствия e.164
            from_="",  # сюда вставляем свой номер Twillio
            body=message_body,
        )
        wks_attendees.update_acell('E'+str(num), int(wks_attendees.acell('E'+str(num)).value) + 1)  # increment the message count row
else:                  # else-часть цикла
    print 'finished'

И поскольку текстовые SMS обычно выглядят скучновато, я добавил немного юникода [6], чтобы их оживить. Вот как выглядело сообщение для приглашенных счастливчиков:

image

Далее я воспользовался Flask в качестве веб-сервера, сделал так, чтобы мой реквест URL для Twilio Messaging указывал на /messages и добавил простые if-проверки для парсинга ответов:

@app.route("/messages", methods=['GET', 'POST'])
def hello_guest():

    if "yes" in body_strip:
        # Ищем гостя и обновляем его confirmation_status
        wks_attendees.update_acell("F"+str(guest_confirmation_cell.row), 'Accepted')  # обновляем статус на «приглашение принято» для этого гостя
        resp.message(u"u2665" + "Thanks for confirming, we'll be in touch!" + u"u2665")  # rотвечаем гостю, подтвердившему участие

    elif "no" in from_body.lower():
        # обновляем статус на «приглашение отклонено» для этого гостя
        wks_attendees.update_acell("F"+str(guest_confirmation_cell.row), 'Declined')
        # отвечаем отказавшему гостю
        resp.message("Sorry to hear that, we still love you though!")

    else:  # отвечаем тем, кто дал некорректный ответ
        resp.message("You sent a different keyword, we need a yes or a no, you sent: "+  
                     from_body)
    return str(resp)

imageimage

Первое сообщение было отправлено в 8:37 утра 19 февраля, а первое подтверждение получено чуть позже в 8:40. К 9:38 я получил уже 23 подтверждения, то есть 32% ответов были у меня в кармане! Через 2 дня после начала массовой рассылки свое участие подтвердили уже 58% гостей. Несмотря на очевидный успех моя будущая жена пока что не была на все 100% впечатлена моим SMS-сервисом свадебных приглашений, и я решил добавить приложению еще немного функциональности.

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

# переменные статуса гостей
guest_confirmed = wks_attendees.acell('C70').value
guest_unconfirmed = wks_attendees.acell('C71').value
guest_no_response = wks_attendees.acell('C72').value
guest_acceptance = wks_attendees.acell('C73').value


elif "numbers" in from_body.lower():
    # возвращаем статистику (общее кол-во гостей, список выбранных блюд)
    resp.message("R.S.V.P update:nnTotal Accepted: " + guest_confirmed  
                 "nnTotal declined: "   guest_unconfirmed   "nnTotal no response: "+  
                 guest_no_response + "nnTotal acceptance rate: " + guest_acceptance)

Пример отправляемой этим кодом SMS:

image

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

Тот факт, что Лорен теперь могла следить за автоматическими обновлениями списка гостей избавил нас от многих головных болей. В итоге я получил от нее добро на повсеместную интеграцию SMS и вскоре этот инструмент использовался едва ли не во всех процессах, где это только было возможно. Некоторые из применений были очевидны, например, отправка SMS-уведомлений о запуске свадебного сайта (сделанного, к слову, на Heroku [7]), или работа со списками свадебных подарков и многие другие решения, которыми я горжусь и по сей день.

Славный пир

После составления списка приглашенных и сбора ответов, дело дошло до занятия, которое обычно откладывают на потом — выяснение вкусовых предпочтений гостей. Первым шагом была отправка еще одной SMS, сообщающей гостям о необходимости посетить веб-сайт и выбрать категории подаваемой еды с помощью google-формы. Выглядела форма вполне обыкновенно, но заполняла тот же файл, в котором находились данные о посетителях. Таким образом, теперь у нас была как таблица принявших приглашения посетителей, так и таблица заполнивших форму выбора еды. Если бы речь шла о каком-нибудь обычном средстве автоматизации, мне пришлось бы ждать, пока гости будут медленно выбирать свои блюда, однако моя свадьба проходила при поддержке Twillio, и это значит, что я мог добиваться ответа от гостей с минимальными усилиями.

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

import json
import time
import gspread
from oauth2client.client import SignedJwtAssertionCredentials
from twilio.rest import TwilioRestClient

# добавляем имя файла для json, созданного для таблицы
json_key = json.load(open(''))
scope = ['https://spreadsheets.google.com/feeds']

credentials = SignedJwtAssertionCredentials(json_key['client_email'],
                                            json_key['private_key'].encode(),
                                            scope)
gc = gspread.authorize(credentials)
wks = gc.open("")  # здесь добавляем имя своей таблицы
wks_attendees = wks.get_worksheet(0)  # список гостей
wks_food = wks.get_worksheet(1)  # список выбора блюд

ACCOUNT_SID = 'TWILIO_ACCOUNT_SID'
AUTH_TOKEN = 'TWILIO_AUTH_TOKEN'

client = TwilioRestClient(ACCOUNT_SID, AUTH_TOKEN)

# Обрабатываем список гостей. Кол-во указано вручную чтобы никого не забыть
for num in range(2, 60):
    food_guest_name = wks_food.acell('B'+str(num)).value  # ячейка из колонка с названиями блюд

    if food_guest_name:
        attendees_name = wks_attendees.find(val_food_guest_name).value
        attendees_name_row = wks_attendees.find(val_food_guest_name).row
        menu_status = wks_attendees.acell("G"+str(attendees_name_row)).value

        if food_guest_name == attendees_name:
            print
            if menu_status == 'Y':  # данные уже сравнили, идем дальше
                print('Skipping')

            else:  # пользователь сделал свой выбор, обновляем основную таблицу
                print ('Food sheet name ' + food_guest_name + 'Attendees sheet name ' + attendees_name)
                # обновляем строку выбора меню
                wks_attendees.update_acell("G"+str(attendees_name_row), 'Y')
        else:
            print('nothing found, moving on')
            wks_attendees.update_acell('E'+str(num), int(wks.acell('E'+str(num)).value) + 1)  # инкремент строки подсчета сообщений

    else:
        # отправляем админу сообщение о завершении обновления статистики
        client.messages.create(from_="",  # номер Twillio
                               to="",  # номер админа
                               body="Finished processing current meal listnnGuest meals confirmed" + guest_meals_confirmed + "nnGuest meals unconfirmed: " + guest_meals_unconfirmed)

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

# отправляем актуальные данные об общем количестве еды и выборе блюд
elif "food" in body_strip.strip():

    resp.message("Guest meals decided:" + guest_meals_confirmed + 
                 "nGuest meals undecided: " + guest_meals_unconfirmed +
                 "nnMenu breakdown:nn" + starter_option_1 +": " +
                 starter_option_1_amount + "n" + starter_option_2 +": " +
                 starter_option_2_amount + "n" + starter_option_3 +": " +
                 starter_option_3_amount + "n" + main_option_1 +": " +
                 main_option_1_amount + "n" + main_option_2 +": " + main_option_2_amount +
                 "n" + main_option_3 +": " + main_option_3_amount + "n" +
                 dessert_option_1 + ": " + dessert_option_1_amount + "n" + dessert_option_2
                 + ": " + dessert_option_2_amount)

image

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

for num in range(2, 72):  # и снова вручную чтобы никого не забыть
    print "sleeping for 3 seconds"

    time.sleep(3)  # опять небольшая задержка чтобы оператор не отсеял сообщение как спам
    wedding_guest_number = wks_attendees.acell('B'+str(num)).value  # берем номер гостя
    wedding_guest_name = wks_attendees.acell('A'+str(num)).value  # берем имя гостя
    menu_guest = wks_attendees.acell('G'+str(num)).value

    if not wedding_guest_number:
        print wedding_guest_name+' telephone number empty not messaging'  # выведем на консоль, что у гостя нет тел. Номера и мы не можем связаться с ним
        wks_attendees.update_acell('H'+str(num), '1')  # инкремент строки подсчета сообщений для отдельного пользователя
    else:
        if menu_guest == "N":  # гость не выбрал еду! НЕ ОТСТАВАТЬ ОТ НЕГО ДО ПОСЛЕДНЕГО!
            print 'Sending message to '+wedding_guest_name
            client.messages.create(
                to="+" + wedding_guest_number,
                from_="",  # ваш номер Twillio
                body="If you have received this message, you have not chosen your food options for Tom & Lauren's Wedding!nnYou can pick your choices via the website, no paper or postage required!nnhttp://www.yourwebsitehere.com/food"
            )
            wks_attendees.update_acell('H'+str(num), int(wks_attendees.acell('H'+str(num)).value) + 1)  # инкремент строки подсчета сообщений для отдельного пользователя
else:                  # else-часть цикла
    print 'finished'

image

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

image

В заключение

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

Создание масштабируемых решений для комплексных задач никогда не бывает простым, и даже один из конечных вариантов моего приложения местами еле справлялся с поставленными задачами. Изначально я планировал разработать более комплексное решение, с визуализацией прогресса, интеграцией голоса, менее зависимое от CLI-скриптов, но время одержало в этой гонке верх. В целом я доволен тем, как все получилось. Идеальных коммуникационных систем не бывает. Всегда нужно использовать наиболее подходящий для вашей аудитории канал, будь то SMS [2], Voice [8], Chat [9], Video [10] или семафор [11].

Если захотите поговорить об автоматизации свадеб, пишите мне в Twitter [12].

Автор: Wirex

Источник [13]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/python/253772

Ссылки в тексте:

[1] репозитории GitHub: https://github.com/SeekTom/Twilio/tree/master/Wedication

[2] SMS: https://www.twilio.com/docs/api/rest/sending-messages

[3] таблиц google: https://www.twilio.com/blog/2017/02/an-easy-way-to-read-and-write-to-a-google-spreadsheet-in-python.html

[4] создал таблицу: https://docs.google.com/spreadsheets/d/1Zud0nYlAQw7RywwiDmADf9Cd3bBTsHaUSQYXh_Cl9_w/edit?usp=sharing

[5] gspread: http://gspread.readthedocs.io/en/latest/

[6] юникода: https://www.twilio.com/blog/2015/08/common-sms-problems-unicode-twilio.html

[7] Heroku: https://www.heroku.com/

[8] Voice: https://www.twilio.com/docs/api/rest/making-calls

[9] Chat: https://www.twilio.com/docs/api/chat

[10] Video: https://www.twilio.com/docs/api/video

[11] семафор: https://ru.wikipedia.org/wiki/%D0%A1%D0%B5%D0%BC%D0%B0%D1%84%D0%BE%D1%80_(%D0%B8%D0%BD%D1%84%D0%BE%D1%80%D0%BC%D0%B0%D1%82%D0%B8%D0%BA%D0%B0)

[12] Twitter: https://twitter.com/SeekTom

[13] Источник: https://geektimes.ru/post/287616/