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

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

Как оказалось, полноценного официального API там нет, а тот что есть – поддерживает только бизнес-аккаунты. Но так или иначе - попробовать хотелось.

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

Полный код и README на Github, а ниже - ключевые моменты.

Схема скрипта

Для организации кода и какой-никакой возможности расширения функционала в будущем, скрипт разбился на 3 класса:

  1. Login – отвечает за авторизацию и создает сессию;

  2. MessageMaker – формирование сообщения;

  3. 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 выглядит так:

  1. Login

  2. Password

  3. 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/– можно найти токен в форме авторизации.

csrf-токен в ответе. 19 г. до н. э.
csrf-токен в ответе. 19 г. до н. э.

Для поиска сделаем простую регулярку, которая не будут работать только с этим ответом:

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 не вызовет подозрений. Лично я за несколько недель использования бота не получал никаких предупреждений, но мало ли.

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

Пример работы
Пример работы

Код на GitHub

Ссылка на статью на Хабре

Комментарии (44)


  1. alan008
    24.09.2021 10:18
    +24

    Отношения, которые мы заслужили! (я мужЫг)


    1. Earthsea
      24.09.2021 10:37
      +3

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

      Мне сразу вспомнилась сцена из Разрушителя:


  1. sourbarberry
    24.09.2021 10:22
    +12

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

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


    1. ImLoaD
      24.09.2021 11:02
      +11

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


      1. AcidVenom
        24.09.2021 13:22
        +17

        И ты спрашиваешь: "Кто там тебе пишет?".

        Через месяц она уходит к твоему чат-боту, потому что "он внимательный, не то что ты!".


    1. Palachintosh Автор
      24.09.2021 13:47

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

      А вообще, я не такой психопат, чтобы натаскать нейронную сеть на основе всей нашей переписки..


    1. 0xd34df00d
      24.09.2021 16:36
      +10

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


  1. Dreaming
    24.09.2021 10:23

    Очень смело)


  1. harios
    24.09.2021 10:29

    А она знает что переписывается со скриптом?


    1. fox_12
      24.09.2021 10:36
      +14

      Думаю если б знала - она б уже не была его девушкой )


      1. emerald_isle
        24.09.2021 11:06
        +6

        А мы уверены, что она не отвечает скриптом, вдруг тоже программистка?


    1. EPIDEMIASH
      24.09.2021 13:39
      -1

      хахахахаха


    1. Palachintosh Автор
      24.09.2021 13:52

      Еще пока нет. Но, по-хорошему, нужно рассказать, да и сворачивать это все. Я думал она узнает сразу же, а получилось слишком правдоподобно.


      1. sinneren
        24.09.2021 15:08
        +2

        не говори, не надо, сверни и всё....


    1. serdjo2011
      26.09.2021 06:48

      Есть сцена с сериала "Кремневая долина". Там Гилфой "отвечал" надоедлевому Динешу


  1. volchenkodmitriy
    24.09.2021 10:45
    -2

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


  1. anonymous
    00.00.0000 00:00


  1. SpiderEkb
    24.09.2021 10:51

    Не думали, что девушке важно не получить сообщение "с добрым утром", а знать, что вы, проснувшись, сразу про нее вспомнили и что-то ей написали?

    Что дальше будет? Для чего еще бота напишете? Там вариантов много, можно в пределе вообще все сферы "отношений" автоматизировать.


    1. Palachintosh Автор
      24.09.2021 13:59

      Я думаю, честно. Просто слишком поздно - часа на 3 позже чем нужно. А мне хотелось чтобы она на работу/учебу собиралась с улыбкой на лице.

      Да и не буду скрывать - она прям счастливее сделалась за последние пару недель. А это же хорошо?!

      Что дальше будет? Для чего еще бота напишете?

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


  1. Panzer_Ex
    24.09.2021 10:58
    +2

    ИМХО уметь написать бота конечно хорошо, но умение общаться с живым человеком - навык куда ценнее...


  1. Gorthauer87
    24.09.2021 11:05
    +3

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


  1. Ar0x13
    24.09.2021 11:27
    -3

    Автор жжот???? - за идею 5


  1. AcidVenom
    24.09.2021 11:35
    +2

    Решаем психологические проблемы технически. Попробуйте ещё поговорить.


    1. zetroot
      24.09.2021 12:34
      +4

      Попробуйте ещё поговорить.

      С чат ботом, например.


  1. pewpew
    24.09.2021 12:25
    +3

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


    1. tioffs
      24.09.2021 12:50

      В этом году instagram предоставил Chat-API https://developers.facebook.com/products/messenger/messenger-api-instagram/


      1. pewpew
        24.09.2021 13:11

        А вот это уже хорошо. Кроме маленького «но» про бизнес аккаунт. Впрочем, если не ошибаюсь, перевести свой аккаунт в бизнес можно за пару кликов без каких либо негативных последствий.


  1. vladvul
    24.09.2021 13:38
    +3

    нет ли API у её вибратора?


    1. intelligentpotato
      24.09.2021 18:08

      Есть!

      https://ru.lovense.com/sextoys/developer/doc


  1. protobuf
    24.09.2021 13:41

    Следующая статья от автора будет называться "Как я создал робота, который встречается с моей девушкой вместо меня".


  1. Flux
    24.09.2021 15:11
    +5

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


    1. tommyangelo27
      24.09.2021 20:42
      +4

      можно всё руками сделать.

      При таком подходе и девушка не нужна =)


  1. Ilirium
    24.09.2021 16:32

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

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


    1. Palachintosh Автор
      24.09.2021 16:57

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

      С Телегой ситуация хуже, потому что в Польше это как-то не прижилось. Все в Инсте да Фейсбуке.


  1. GeorgKDeft
    24.09.2021 19:09

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


  1. sokolov_fv
    25.09.2021 00:30

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


  1. h0rn3t
    25.09.2021 01:21

    Качество кода оставляет желать лучшего, где-то != None, где-то is not None, где-то по нормальному проверка на None... похоже на сборник копипаст из стекоферфлоу


    1. Palachintosh Автор
      25.09.2021 11:35

      Вот и коммент по делу. Спасибо, буду внимательней!


  1. dikkini
    25.09.2021 10:17

    Меня волнует 1 вопрос. Бот отправляет сообщение в 8 утра, но сам человек встаёт только в 11 утра, девушка ожидает 1 сообщение от него, а дальше? Ей не интересно чего это вдруг он стал вставать на 3 часа раньше?


    1. Palachintosh Автор
      25.09.2021 11:31

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


  1. JavaFox
    26.09.2021 00:56
    +1

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


  1. mbamber
    27.09.2021 00:03

    Будем надеятся, что девушка читает только Инстаграм)


  1. alexeyshulzhenko
    27.09.2021 00:04

    Мно очень понравилась идея, правда так и не смог побороть Instagram API у себя. Насколько понял, в они используют MQTT для чатов и даже есть NodeJS репорзиторий который умеет отправлять сообщения нативно. Но я его не осилил;

    А используя "https://i.instagram.com/api/v1/direct_v2/threads/broadcast/text/" или ваш пример мне просто приходит 200 статус и никакого сообщения в результате не появляется(


    1. Palachintosh Автор
      27.09.2021 00:21

      200 статус приходит почти всегда. Во всех случаях, когда запрос был правильно передан, !но! не его параметры. Я подозреваю, что в некоторых случаях, строка параметров может быть не правильно десериализована на стороне Инсты и тогда сервер просто возвращает статус-код 200, но в ответе совсем другое. Такое случается, если не возвращаются cookie или сам запрос отправляется не так как нужно (странный user-agent, по каким-то причинам истекает срок действия сессии). Но, скорее всего, неправильно указан id конечного пользователя.

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

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