Недавно мы с девушкой серьезно поговорили и выяснилось, что я даже не пишу ей “С добрым утром” и вообще редко пишу по утрам. В целом, причина кроется в том, что я не просыпаюсь с восходом первых лучей солнца (как она), а переписываться не очень люблю. Ну а ей, конечно же, приятно получать нежности по утрам и все такое.
Так как общаемся мы исключительно в Instagram, я подумал, что неплохо бы совместить все это и автоматизировать процесс. Тем более что у соцсети вроде как есть открытый API.
Как оказалось, полноценного официального API там нет, а тот что есть – поддерживает только бизнес-аккаунты. Но так или иначе - попробовать хотелось.
Я уверен, что существуют сервисы для этого, но сделать собственного рабочего бота вот прямо очень хотелось. Я нашел на Хабре статью про отправку сообщений на PHP из которой взял адрес для отправки запроса (ссылка на статью в конце). А здесь я постараюсь описать, по сути, тот же процесс, но на Питоне с маленькой доработкой. Тот же бот, с минимальным набором функций. Может, кому-то пригодится.
Полный код и README на Github, а ниже - ключевые моменты.
Схема скрипта
Для организации кода и какой-никакой возможности расширения функционала в будущем, скрипт разбился на 3 класса:
Login – отвечает за авторизацию и создает сессию;
MessageMaker – формирование сообщения;
SendMsg – непосредственная отправка сообщения.
И дополнительно, 2 конфигурационных файла auth.txt и conf.txt: данные авторизации и словарь с сообщениями соответственно и менеджер запуска – insta_bot_manager.py.
Класс Login - авторизация
Посмотрим как работает авторизация Instagram. Для этого смотрим исходящие запросы прям в инструментах браузера:
Как видно – запрос отправляется на адрес https://www.instagram .com/accounts/login/ajax/, да и выглядит довольно просто. Нет ни токенов, ни каких-то левых параметров. Вот только пароль в зашифрованном виде. Как я выяснил, это кодировка AES-GCM256, очевидно, с каким-то префиксом. Строка из запроса выглядит так:
#PWD_INSTAGRAM_BROWSER:10:16940921:enc_password
Параметр "10" - обозначает пароль в зашифрованном виде, далее - время и сам пароль. Делать свой шифровальщик я, конечно же, не буду, но есть и другой способ залогиниться с паролем в чистом виде. Для передачи обычной строки достаточно заменить "10" на "0":
#PWD_INSTAGRAM_BROWSER:0:1690149:password
Для хранения данных авторизации используется файл – auth.txt. Знаю, что лучше хранить это все в зашифрованном виде, но так как данные находится только на сервере – это относительно безопасно.
Конструкция auth.txt выглядит так:
Login
Password
Ig_user_id (id пользователя которому отправляем сообщение)
Просто текст. Каждый параметр должен быть записан с новой строки. Читаем из файла:
with open("auth.txt", "r") as f:
l = f.read().split("\n")
username = l[0]
passwd = l[1]
self.user_id = l[2]
Теперь авторизуемся, используя requests
:
# Для начала, получим csrf-token:
r = requests()
# Можно использовать кусок полной ссылки для авторизации или сделать запрос прямо к https://www.instagram.com/.
login_url = "https://www.instagram.com/accounts/login/"
# В заголовках можно указать свой user-agent. В другом случае, приходит оповещение безопасности в приложении, которое лучше подтвердить.
# При отсутствии или каком-то мусоре в заголовке User-Agent, IG присылает в ответе ошибку “message”: “user-agent missmatch”.
# Так как имитируется сессия – сразу изменим user-agent прямо в объекте сессии.
s = request.Session()
s.headers["User-Agent"] = ("Mozilla/5.0 (iPhone; CPU iPhone OS 14_1 like Mac OS X) " +
"AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Instagram 142.0.0.22.109" +
"(iPhone12,5; iOS 14_1; en_US; en-US; scale=3.00; 1242x2688; 214888322) NW/1")
# Ну и, собственно, сам запрос
get_token = r.get(login_url, headers=headers)
if get_token.cookies.get("csrftoken") is not None:
headers["x-csrftoken"] = get_token.cookies["csrftoken"]
#Если не получилось вытянуть токен из cookie – пробуем другой путь
else:
if csrf_token.get("success") != None:
headers["x-csrftoken"] = csrf_token.get("value")
if csrf_token.get("error"):
return csrf_token
Отправляем POST-запрос с полученными данными:
login = r.post(login_url + “ajax/”, data=auth_data, headers=headers)
# статус операции можно проверить, распарсив ответ от сервера.
#Если значение status: ‘ok’ в словаре response, и присутствует UserId – все хорошо.
# Но, для простоты, можно посмотреть установились ликуки "sessionid"
if log_in.cookies.get("sessionid"):
return {'success': 'ok', 'response': log_in.content}
Полный ответ должен выглядеть как-то так:
Ищем csrf-токен вручную
Если меняется IP, агент, пароль и, наверняка, какие-то другие параметры клиента – Инстаграм начинает требовать подтверждение политики использования cookie. Соответственно, куки нет, токена нет и нормальный POST-запрос невозможен.
Если посмотреть текстовое представление ответа на первый GET-запрос по адресу /accounts/login/– можно найти токен в форме авторизации.
Для поиска сделаем простую регулярку, которая не будут работать только с этим ответом:
import re
def response_parse(self, response):
if response is not None:
token = re.search('(csrf_token":")+(?P<value>[A-Za-z0-9]*)', response)
token_value = token.groupdict()
if token_value is not None:
return {"success": "ok", "value": token_value.get("value")}
return {"error": "Unable to extrack token from response!"}
Как правило, это срабатывает только для первой попытки, после изменения параметров. Почему-то после авторизации куки начинают выдаваться как обычно. НО, важно следить за значением токена – после успешного запроса он сменится в cookie и, хорошо бы его перезаписать.
“Умный” выбор сообщения
Так как бот создан с единственной целью – отправлять девушке сообщения по утрам, то стоило бы добавить ему немного «мозгов». Вообще, схема подбора сообщения выглядит довольно-таки тривиально – читается список доступных фраз из файла и рандомно выбирается заветная.
Поскольку скрипт будет срабатывать каждый день, а сообщения попадаются вида «Доброе утро, уже на работе?» - лучше следить за тем, чтобы такое не отправлялось в выходные дни, и наоборот.
Для этого создадим словарь conf.txt с предложенными фразами в папке рядом со скриптом и придумаем для него простейший синтаксис. Мне пришло в голову выделять сообщения для выходного дня символом “@”, будних дней - “!”, а блоки без выделения отправлять каждый день. Ну и комментарии (куда без них) - “/”. Пример словаря:
С добрым утром <3
!
Привет, как добралась на работу?
!
@
Доброе утро, какие планы на день?
@
// Комментарий здесь. Это вообще не обрабатывается
Обработка словаря и псевдорандомный выбор
Заниматься подбором сообщения будет класс MessageMaker. В конструктор добавим только with as для чтения словаря:
def __init__(self):
with open("conf.txt", "r" as get_f:
self.get_f = get_f.read().split('\n')
Для определения дня недели можно использовать datetime.today().weekday()
:
day = datetime.today().weekday()
# Если это выходной
if day > 4:
get_phrase = self.array_sort(array=self.get_f, symbol="!")
# Если это будни
if day < 5:
get_phrase = self.array_sort(array=self.get_f, symbol="@")
get_random_str = random.choice(get_phrase)
Функция array_sort()
принимает 2 параметра: массив строк из conf.txt и разделитель, сообщения которого нужно игнорировать. Результатом выполнения будет новый, отсортированный список, из которого можно будет рандомно выбирать любую фразу (функция random.choice()
)
Сама сортировка выглядит ужасно примерно так:
def array_sort(self, array, symbol):
new_array = []
counter = 0
for string in array:
if counter > 0:
# Обнуляем счетчик если встречаем такой же символ снова
if string == symbol:
counter = 0
continue
# Пропускаем все строки между указанными разделителями
if string == symbol:
counter += 1
# Если условия удовлетворяются, то добавляем строку в новый
# массив, содержащий только доступные фразы
if string != symbol and len(string) > 2 and string[0] != "/":
new_array.append(string)
return new_array
Теперь добавим главную функцию, которая возвращает непосредственно выбранную строку:
def select_str(self):
get_random_str = self.almost_random_choice()
if get_random_str != '' and get_random_str != None:
return get_random_str
return self.select_str()
Поскольку Instagram сам экранирует символы и конвертирует любой тип в str – нет нужды принудительно приводить их вручную.
Тестовый запуск select_str()
:
SendMsg – непосредственная отправка сообщения
В классе SendMsg – Login и MessageMaker. А также добавим в конструктор инициализацию родительских классов:
class SendMsg(Login, MessageMaker):
def __init__(self, enc_password=False):
super().__init__(enc_password=enc_password)
MessageMaker().__init__()
И, непосредственно, отправляем сообщение, используя весь функционал. Создаем функцию send_message() с необязательными параметрами:
def send_message(self, random_msg=None):
pass
Если сообщение указано прямо в вызове функции – то данные из файла не читаются, и наоборот. Параметр random_msg принимает любую строку, которую хочется отправить в качестве сообщения.
Логинимся и создаем сессию:
log_in = self.login()
Если функция вернула success в словаре, значит, можно продолжать:
if log_in.get("success") is not None:
# Begin
Определяем набор параметров для отправки POST запроса:
# ссылка для отправки сообщения. Нашел ее на Хабре и еще на каком-то сайте.
send_mess_to_url = "https://i.instagram.com/api/v1/direct_v2/threads/broadcast/text/"
# Генерируем новый uuid v4. Это тоже стандартный функционал Python
uuid_v4 = uuid.uuid4()
# Проверяем, как формировать сообщение. Если параметр был задан – используется он
if random_msg is None:
message = self.select_str()
else:
message = str(random_msg)
enc_message = message.encode("utf-8")
# Собственно, тело запроса. Подставляем все параметры.
body = ('text={}&' +
'_uuid=&' +
'_csrftoken={}&' +
'recipient_users="[["{}"]]"&' +
'action=send_item&' +
'thread_ids=["0"]&' + '
'client_context={}').format(enc_message.decode("latin-1"), self.session.cookies["csrftoken"], user_id, uuid_v4)
# И все заголовки
headers = self.session.headers
headers["Content-Type"] = "application/x-www-form-urlencoded"
headers["x-csrftoken"] = self.session.cookies["csrftoken"]
# Ig-App-ID можно найти в заголовках запросов к Инстаграм. Он меняется время от времени, но не часто.
headers["X-IG-App-ID"] = "936619743392459"
Позже добавлю функция проверки X-IG-App-ID. Так как он возвращается в заголовках после успешной авторизации. Не сложно сверить значения и обновить, если требуется.
И отправляем запрос:
send_m = self.session.post(send_mess_to_url, data=body, headers=headers)
Еще желательно определить простейшую функцию логирования, чисто для упрощения отладки. Так как скрипт срабатывает на сервере – хорошо записывать все что происходит:
def _log(*args, **kwargs):
if args:
for i in args:
string = string + ' ' + str(i)
if kwargs:
for key, value in kwargs:
string = string + ' ' + str(value)
with open("log.log", "a") as f:
print(string, file=f)
Запускаем и смотрим лог:
Сообщение отправилось. Видна дата отправки, статус-код и текст, который был отправлен. Этого с головой достаточно, чтобы идентифицировать проблемы или сделать выбор сообщения не только рандомным, но еще и зависимым от предыдущего дня.
Менеджер запуска
Для удобства запуска, создадим менеджер скрипта – insta_bot_manager.py
и поместим его в папку рядом с insta_bot.py.
Разместим функцию-обработчик и импортируем написанный модуль:
from insta_bot import SendMsg
import os
def __send__(enc_password=None, random_msg=None):
#Создаем экземпляр класса
s = SendMsg(enc_password=enc_password)
# Отправляем сообщение
return s.send_message(random_msg=random_msg)
if __name__ == “__main__”:
# Можно добавить необязательные параметры enc_password и random_msg
__send__()
А также проверку существования файла auth.txt. Потому что запускать это все без данных авторизации не имеет смысла:
def _conf_check():
file_check = os.path.exists(
os.path.dirname(
os.path.abspath(__file__)) + "/auth.txt")
if not file_check:
raise Exception("You must create/or fill the auth.txt file!")
return 1
Теперь если auth.txt по каким-то причинам отсутствует - будет поднято исключение.
Автоматизация процесса в Cron
Поскольку я не хотел добавлять insta_bot_manager.py шебанги bash, то решил просто сделать еще один launcher специально для Cron.
В папке со скриптом создаем файл launcher:
$ touch launcher && nano launcher
Добавим что-то такое:
#!/bin/bash
sleep $[RANDOM%70]m
/usr/bin/python3.6 /home/path/to/insta_bot.manager.py
Получается, перед непосредственным запуском скрипт засыпает на рандомное время до 70 минут.
Вообще, при добавлении в cron стоит проследить за переменными окружения. В частности - PWD. Я получал ошибки из-за различия домашней директории и папки со скриптом. Для ее устранения можно приколхозить, в качестве первой, команду cd с полным путем к папке.
Выводы
Стоит быть осторожным, поскольку такая рассылка не совсем легальна и, вроде как, можно хватануть банхаммером Инстаграмма по лицу. Однако, как мне кажется, отправка 1-2 сообщений в сутки на один и тот же ID не вызовет подозрений. Лично я за несколько недель использования бота не получал никаких предупреждений, но мало ли.
Не известна реакция девушки на такое. С одной стороны – бот сделан с любовью и шлет приятности, с другой – это может пойти по статье «Наплевательское отношение». Но пока что полет нормальный, посмотрим, что будет дальше. Возможно в будущем мне придется слать голосовые сообщения :)
Комментарии (44)
sourbarberry
24.09.2021 10:22+12Идея прикольная, но автору лучше бы с девушкой поговорить, и донести, что вы просыпаетесь в разное время) Ну или отправлять самому сообщения по мере возможности. Тут же дело не в сообщениях как-таковых. Пофиг на эти "добрые утра", тут речь об отношении. И тут действительно можно нарваться на еще одну ссору.
А поводом для нее может быть то, что скрипт ломается любой нестандартной ситуацией: девушка написала первой, есть неоконченный со вчера диалог, отпуск, больничный и т.п.Palachintosh Автор
24.09.2021 13:47Это правда. Для полной проработки всех возможных ситуаций нужно гораздо больше времени. Да и хотелось просто сделать что-то такое. Не знал что дойдет до практического использования.
А вообще, я не такой психопат, чтобы натаскать нейронную сеть на основе всей нашей переписки..
0xd34df00d
24.09.2021 16:36+10Читаешь это все и понимаешь в очередной раз, что отношения — это сложно, и лучше пойти дальше ковырять всякий матан. Спасибо за такие комментарии и этакую мотивацию.
harios
24.09.2021 10:29А она знает что переписывается со скриптом?
Palachintosh Автор
24.09.2021 13:52Еще пока нет. Но, по-хорошему, нужно рассказать, да и сворачивать это все. Я думал она узнает сразу же, а получилось слишком правдоподобно.
serdjo2011
26.09.2021 06:48Есть сцена с сериала "Кремневая долина". Там Гилфой "отвечал" надоедлевому Динешу
volchenkodmitriy
24.09.2021 10:45-2Здравствуйте! Идея очень неплохая если бы речь шла например о том чтобы поздороваться удаленно с коллегами на работе. Но с девушкой - это вызывает улыбку, ей нужно не "здравствуй", а то что в нем заложено и расшифровывается так "вот видишь теперь я делаю то что ты скажешь, я тебе подчиняюсь". Вопрос только в том - хотите ли Вы подчиняться своей девушке или нет.)
SpiderEkb
24.09.2021 10:51Не думали, что девушке важно не получить сообщение "с добрым утром", а знать, что вы, проснувшись, сразу про нее вспомнили и что-то ей написали?
Что дальше будет? Для чего еще бота напишете? Там вариантов много, можно в пределе вообще все сферы "отношений" автоматизировать.
Palachintosh Автор
24.09.2021 13:59Я думаю, честно. Просто слишком поздно - часа на 3 позже чем нужно. А мне хотелось чтобы она на работу/учебу собиралась с улыбкой на лице.
Да и не буду скрывать - она прям счастливее сделалась за последние пару недель. А это же хорошо?!
Что дальше будет? Для чего еще бота напишете?
Дальше этого я точно не зайду) Но так или иначе было интересно попробовать взаимодействие с Инстаграм.
Panzer_Ex
24.09.2021 10:58+2ИМХО уметь написать бота конечно хорошо, но умение общаться с живым человеком - навык куда ценнее...
Gorthauer87
24.09.2021 11:05+3А в инстаграмме нет отложенных сообщений? И да, все же надо по таким вопросам с партнёрами разговаривать, а то ведь это читерство и на таких вещах отношения строить это стрельба по ногам.
pewpew
24.09.2021 12:25+3Куча советчиков налетела. А может и девушки нет. Но инсту реверснуть и бота написать хотелось из любопытства. Жаль, что при очередном обновлении это всё может превратиться в тыкву. Вот бы такое было, но с официальным API, например. Жаль, там вроде бы нет личных сообщений.
tioffs
24.09.2021 12:50В этом году instagram предоставил Chat-API https://developers.facebook.com/products/messenger/messenger-api-instagram/
pewpew
24.09.2021 13:11А вот это уже хорошо. Кроме маленького «но» про бизнес аккаунт. Впрочем, если не ошибаюсь, перевести свой аккаунт в бизнес можно за пару кликов без каких либо негативных последствий.
protobuf
24.09.2021 13:41Следующая статья от автора будет называться "Как я создал робота, который встречается с моей девушкой вместо меня".
Flux
24.09.2021 15:11+5Легче заменить девушку на пользующуюся телеграмом, там API поприятнее и готовые пакеты есть. Да и вообще есть отложенная отправка сообщений, можно всё руками сделать.
Ilirium
24.09.2021 16:32Мне кажется с т.з. психологии и чтобы было не обидно, можно написать на чем угодно и где угодно систему напоминаний, которая будет рандомно напоминать, что нужно девушке написать/позвонить.
Как вариант, дополнительно генерировать мелкие картинки/стишки, чтобы их пересылать. Или для вдохновения выдергивать куски из любовных писем/стихов и показывать их. Как самый простой вариант, написать бота для телеги. Который же будет время от времени сам чистить свою историю.
Palachintosh Автор
24.09.2021 16:57Это да. Только смысл в том, чтобы это все отправлялось автоматически. Ну и сделать это именно под Инсту тоже хотелось, потому что там надо извращаться. Да и такой бот явно лишним не будет. Чисто, так, для общего развития сделал.
С Телегой ситуация хуже, потому что в Польше это как-то не прижилось. Все в Инсте да Фейсбуке.
GeorgKDeft
24.09.2021 19:09Эх задачу надо ставить шире... И.И. подключать и чтоб он еще отчеты с результатами слал "довел дело разговорами до согласия что хоть ты и мудак, но на рыбалку ехать можно"... ну и дальше в этом стиле.
sokolov_fv
25.09.2021 00:30Автор, никого не слушай. В конце концов, твоя девушка может оказаться той единственной, которой понравятся именно такие пожелания доброго утра, которая поймёт, что бот - это проявление самых нежных чувств.
h0rn3t
25.09.2021 01:21Качество кода оставляет желать лучшего, где-то != None, где-то is not None, где-то по нормальному проверка на None... похоже на сборник копипаст из стекоферфлоу
dikkini
25.09.2021 10:17Меня волнует 1 вопрос. Бот отправляет сообщение в 8 утра, но сам человек встаёт только в 11 утра, девушка ожидает 1 сообщение от него, а дальше? Ей не интересно чего это вдруг он стал вставать на 3 часа раньше?
Palachintosh Автор
25.09.2021 11:31Выходит, она думала, что я встаю так рано чтобы просто отправить сообщение и дальше иду спать..
JavaFox
26.09.2021 00:56+1Статья напомнила мне момент из сериала "Кремниевая долина", когда Гилфоил написал бота для общения с Динешем. Хоть автор и сказал, что он не псих, но нейронную сеть можно попробовать подключить :)
alexeyshulzhenko
27.09.2021 00:04Мно очень понравилась идея, правда так и не смог побороть Instagram API у себя. Насколько понял, в они используют MQTT для чатов и даже есть NodeJS репорзиторий который умеет отправлять сообщения нативно. Но я его не осилил;
А используя "https://i.instagram.com/api/v1/direct_v2/threads/broadcast/text/" или ваш пример мне просто приходит 200 статус и никакого сообщения в результате не появляется(
Palachintosh Автор
27.09.2021 00:21200 статус приходит почти всегда. Во всех случаях, когда запрос был правильно передан, !но! не его параметры. Я подозреваю, что в некоторых случаях, строка параметров может быть не правильно десериализована на стороне Инсты и тогда сервер просто возвращает статус-код 200, но в ответе совсем другое. Такое случается, если не возвращаются cookie или сам запрос отправляется не так как нужно (странный user-agent, по каким-то причинам истекает срок действия сессии). Но, скорее всего, неправильно указан id конечного пользователя.
Кстати, нельзя отправить сообщение самому себе.
Желательно запустить отладку и смотреть, какие ответы возвращаются в каждом случае. Если хочешь, напиши в директ и попробуем разобраться.
alan008
Отношения, которые мы заслужили! (я мужЫг)
Earthsea
Старые фантастические фильмы и книги все больше и больше сбываются. Не совсем в том виде, но связь есть. По сути, мы уже живем в том самом киберпанке который описывали в 70-80-90-х годах.
Мне сразу вспомнилась сцена из Разрушителя: