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

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

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

Мне стало интересно, я зарегистрировался на одном из популярных сервисов, посмотрел функционал, он мне показался интересным, пользовался я им некоторое время.
Стоит сказать, что в тот момент Telegram представил платформу Bot Platform, она резко набрало популярность, много разработчиков пытались что-то сделать на основе него, в том числе и я, в частности использовал для отправки уведомлений с сайтов.

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

Решение было простым, почему бы не написать Telegram бота, с помощью которого можно добавлять аккаунты, создавать задачи для подписок и отписок, и получать оповещение непосредственно в мессенджер о завершение задачи?!

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

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

Начал разработку на Node.js, для хранения данных использовал MongoDB.

Основные возможности, которые я хотел написать были:

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

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

Схема для хранения id пользователей telegram, для их идентификации:

const UserSchema = new db.mongoose.Schema({
  id: { type: Number, required: [true, 'idRequired'] },
  name: { type: String, required: [true, 'nameRequired'] },
  date: { type: Date, default: Date.now }
})

Схема для хранения аккаунтов Instagram:

const AccountSchema = new db.mongoose.Schema({
  user: { type: Number, required: [true, 'userRequired'] },
  login: { type: String, required: [true, 'loginRequired'] },
  password: { type: String, required: [true, 'passwordRequired'] },
  verified: { type: Boolean, default: false },
  date: { type: Date, default: Date.now }
})

А так же схема для поставленых задач:

const TaskSchema = new db.mongoose.Schema({
  user: { type: Number, required: [true, 'userRequired'] },
  login: { type: String, required: [true, 'loginRequired'] },
  type: { type: String, required: [true, 'typeRequired'] },
  params: { type: Object, required: [true, 'paramsRequired'] },
  status: { type: String, default: 'active' },
  date: { type: Date, default: Date.now },
  start: { type: Number, required: [true, 'startReqiured'] }
})

В свойстве params мы храним специфичные конкретной задаче данные, например для подписки:
sourceType Типа источника (пользователь, хештег, локация)
source наименование источника (username, #хештег, [lat, long])
actionFollow общее количество подписок/отписок
actionFollowDay количество действий в день
actionLikeDay количество лайков каждому пользователю

Остальные схемы доступны в репозитории проекта instalator-telegram.

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

Карта проекта
module.exports = {
  event: 'home',
  children: {
    'Создать задание': {
      event: 'task:create',
      children: {
        '*': {
          event: 'task:select',
          children: {
            'Лайк + Подписка': {
              event: 'task:select:follow+like',
              children: {
                Пользователь: {
                  event: 'task:select:follow+like:user',
                  children: {
                    '*': {
                      event: 'task:select:follow+like:user:select',
                      children: {
                        '*': {
                          event: 'task:select:follow+like:source:action',
                          children: {
                            '*': {
                              event: 'task:select:follow+like:source:actionPerDay',
                              children: {
                                '*': {
                                  event: 'task:select:follow+like:source:like'
                                }
                              }
                            },
                            Назад: {
                              event: 'location:back'
                            }
                          }
                        },
                        Назад: {
                          event: 'location:back'
                        }
                      }
                    },
                    Назад: {
                      event: 'location:back'
                    }
                  }
                },
                Хештег: {
                  event: 'task:select:follow+like:hashtag',
                  children: {
                    '*': {
                      event: 'task:select:follow+like:hashtag:find',
                      children: {
                        '*': {
                          event: 'task:select:follow+like:source:action',
                          children: {
                            '*': {
                              event: 'task:select:follow+like:source:actionPerDay',
                              children: {
                                '*': {
                                  event: 'task:select:follow+like:source:like'
                                }
                              }
                            },
                            Назад: {
                              event: 'location:back'
                            }
                          }
                        },
                        Назад: {
                          event: 'location:back'
                        }
                      }
                    }
                  }
                },
                Источники: {
                  event: 'task:select:follow+like:source',
                  children: {
                    '*': {
                      event: 'task:select:follow+like:source:select',
                      children: {
                        '*': {
                          event: 'task:select:follow+like:source:action',
                          children: {
                            '*': {
                              event: 'task:select:follow+like:source:actionPerDay',
                              children: {
                                '*': {
                                  event: 'task:select:follow+like:source:like'
                                },
                                Назад: {
                                  event: 'location:back'
                                }
                              }
                            },
                            Назад: {
                              event: 'location:back'
                            }
                          }
                        },
                        Назад: {
                          event: 'location:back'
                        }
                      }
                    },
                    Назад: {
                      event: 'location:back'
                    }
                  }
                },
                Назад: {
                  event: 'location:back'
                }
              }
            },
            Отписка: {
              event: 'task:select:type',
              children: {
                '*': {
                  event: 'task:select:type:unfollow'
                  // await: true
                },
                Назад: {
                  event: 'location:back'
                }
              }
            },
            Назад: {
              event: 'location:back'
            }
          }
        },
        'Добавить аккаунт': {
          event: 'account:add',
          children: {
            '*': {
              event: 'account:await',
              await: true
            },
            Назад: {
              event: 'location:back'
            }
          }
        },
        Назад: {
          event: 'location:back'
        }
      }
    },
    Активность: {
      event: 'actions',
      children: {
        '*': {
          event: 'actions:account',
          children: {
            Редактировать: {
              event: 'actions:account:update',
              children: {
                '*': {
                  event: 'actions:account:update:one',
                  children: {
                    '*': {
                      event: 'actions:account:update:two',
                      children: {
                        '*': {
                          event: 'actions:account:update:three'
                        }
                      }
                    }
                  }
                },
                Назад: {
                  event: 'location:back'
                }
              }
            },
            Отменить: {
              event: 'actions:account:cancel'
            },
            Назад: {
              event: 'location:back'
            }
          }
        },
        Назад: {
          event: 'location:back'
        }
      }
    },
    Аккаунты: {
      event: 'account:list',
      children: {
        '*': {
          event: 'account:select',
          children: {
            Редактировать: {
              event: 'account:edit',
              children: {
                '*': {
                  event: 'account:edit:await',
                  await: true
                },
                Назад: {
                  event: 'location:back'
                }
              }
            },
            Удалить: {
              event: 'account:delete',
              await: true
            },
            Назад: {
              event: 'location:back'
            }
          }
        },
        'Добавить аккаунт': {
          event: 'account:add',
          children: {
            '*': {
              event: 'account:await',
              await: true
            },
            Назад: {
              event: 'location:back'
            }
          }
        },
        Назад: {
          event: 'location:back'
        }
      }
    },
    Лимиты: {
      event: 'limit:message',
      children: {
        Назад: {
          event: 'location:back'
        }
      }
    }
  }
}


Карта содержит в себе объекты «события», в нем есть свойство event, которая содержит имя события EventEmitter, а так же свойство children, которая содержит в себе дочерние «роуты», по структуре соответствующие.

Как это работает, пользователь отправляет команду, например Создать задание, получив сообщение, проходимся по дереву, если есть свойство, вызываем соответствующее событие EventEmitter, после чего записываем id пользователя в объект state, где будем хранить текущее расположение пользователей, например:

state = {
  23445432: ['Создать задание', 'Лайк + Подписка'],
  1345532: ['Активность']
}

Теперь если пользователь под id 23445432 отправит сообщение, в ответ будет вызвано событие task:select:follow+like, а что если соответствующего сообщения нет в карте? или нам нужно получить какие-то данные от пользователя, которые не прописаны в карте, например количество подписок в день? Для этого одно из свойств children необходимо пометить звездочкой, вот так: *, далее внутри него описываем event, который необходимо вызвать.

Непосредственно сам «роутинг»:

const router = msg => {
  // Декодируем эмодзи
  if (msg.text) msg.text = emoji.decode(msg.text)
  // No user status, we give the main menu
  if (!state[msg.from.id]) {
    commandEvents.emit('/home', msg)
    // Adding the user to the state
    state[msg.from.id] = []
  } else {
    // Go to the desired branch
    const findBranch = state[msg.from.id].reduce((path, item) => {
      // If there are no child partitions
      if (!path.children) {
        return path
      } else {
        if (path.children[item]) {
          return path.children[item]
        } else {
          // If there is no suitable branch, then we try to use a common branch
          if (path.children['*']) {
            return path.children['*']
          } else {
            return path
          }
        }
      }
    }, map)
    // Call branch method
    const callBranch = branch => {
      const action = findBranch.children[branch]
      // Call action
      event.emit(action.event, msg, action, (value = msg.text) => {
        event.emit('location:next', msg, action, value)
      })
    }
    // We check the existence of the method
    if (findBranch.children.hasOwnProperty(msg.text)) {
      callBranch(msg.text)
    } else if (findBranch.children['*']) {
      // If there is no suitable branch, then we try to use a common branch
      callBranch('*')
    } else {
      // back
      event.emit('location:back', msg)
    }
  }
}

Если свойства в карте нет, то мы вызываем ивент back, возвращаем пользователя на один шаг выше.

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

event.on('account:list', async (msg, action, next) => {
    try {
      const list = await Account.list(msg.from.id)
      if (list === null) {
        throw new Error(`There are no accounts for ${msg.from.id}`)
      }

      // Sending the list of accounts
      const elements = list.map(item => item.login)
      send.keyboard(msg.from.id, 'Выберите аккаунт', [
        ...elements,
        'Добавить аккаунт',
        'Назад'
      ])

      next && next()
    } catch (e) {
      event.emit('account:empty', msg)
      next && next()
    }
  })

Метод next() переводит пользователя на следующий уровень в карте, для этого он добавляет активный раздел в state, а метод keyboard отправляет кнопки пользователю.

Далее если вы заметили, в схеме Task у нас есть свойство start, в которое записывается минута, в которую создается задача, так вот пора запускать задачи, для этого мы перебираем все задачи созданные в текущую минуту, полный код работы «cron'a»:

cron.js
cron.schedule(conf.cron, async () => {
  try {
    const list = await task.currentList()
    if (list === null) throw new Error('No active assignments')

    for (let item of list) {
      const id = item._id.toString()

      // Missing running tasks
      if (activeTask.includes(id)) continue

      switch (item.type) {
        case 'Лайк + Подписка':
          activeTask.push(id)
          actions
            .followLike(item)
            .then(res => {
              // Remove from list
              const keyActiveTask = activeTask.indexOf(id)
              delete activeTask[keyActiveTask]

              if (res.name === 'AuthenticationError') {
                send.message(
                  item.user,
                  `? При входе в ${item.login} возникла ошибка, отредактируйте акккаунт!`
                )
                throw new Error('Ошибка авторизации')
              }

              // notify the user when the task is completed
              if (res) {
                send.message(
                  item.user,
                  `Задание ${item.type} завершено для аккаунта ${item.login}`
                )
              }
            })
            .catch(e => console.log(e))
          break

        case 'Отписка':
          activeTask.push(id)
          actions
            .unFollow(item)
            .then(res => {
              // Remove from list
              const keyActiveTask = activeTask.indexOf(id)
              delete activeTask[keyActiveTask]

              if (res.name === 'AuthenticationError') {
                send.message(
                  item.user,
                  `? При входе в ${item.login} возникла ошибка, отредактируйте акккаунт!`
                )
                throw new Error('Ошибка авторизации')
              }

              // notify the user when the task is completed
              if (res) {
                send.message(
                  item.user,
                  `Задание ${item.type} завершено для аккаунта ${item.login}`
                )
              }
            })
            .catch(e => console.log(e))
          break

        default:
          // Job type is not defined
          break
      }
    }
  } catch (e) {
    // No active assignments
    return e
  }
})


Для работы с Telegram я настроил webhook, если вам пока он не нужен, можете запустить в режиме dev (npm run dev).

Рекомендую настроить webook, так ваш сервер не будет все время посылать polling запросы, конфигурация находится в файле conf.json.

И наверное последнее, это работа с приватным API Instagram, ее нет смысла рассматривать, документация доступна с примерами ве репозитории проекта.

Ну и наконец пример рабочего функционала:


Заключение


Хочется сказать, что все таки есть проблемы, они связаны с тем, что Instagram блокирует и не дает разрешения для авторизации большего количества аккаунтов с одного IP адреса какого-нибудь VPS. Так в моем случае авторизоваться смог с нескольких аккаунтов, далее уже доступ был блокирован для добавление новых, но добавленные ранее не имели никаких проблем, если конечно не нарушать дневные лимиты.

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

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

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

Будьте осторожны используя бота! Специально для вас добавил раздел «Лимиты», где написана некоторая информация, которая предостережёт вас от блокировки со стороны Instagram.

Важно! пароли хранятся в не зашифрованном виде, не вводите свой пароль!
Репозиторий проекта

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


  1. hamnsk
    07.05.2018 14:34

    Да не работает давно уже массфоловинг как вариант раскрутки, вы опоздали с этим лет так на 5


    1. hazratgs Автор
      07.05.2018 14:38

      Может быть, но у меня есть страница, которая за месяц набрала 5000 тыс. подписчиков, тут скорее важен контент страницы, развлекательные быстро набирают.


      1. zzzmmtt
        07.05.2018 14:42

        5 миллионов подписчиков это сильно. Подобных массфоловеров в инсте обычно отправляю в бан + жалоба на спам. И периодически такие аккаунты таки выпиливают, что не может не радовать.


        1. hazratgs Автор
          07.05.2018 14:49

          ссори, имел ввиду 5 тыс.


        1. hazratgs Автор
          07.05.2018 14:50

          на счет бана вы правы


    1. catanfa
      07.05.2018 17:17

      а что работает?


  1. igorsmi
    07.05.2018 16:11

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


    1. hazratgs Автор
      07.05.2018 16:14

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


      1. igorsmi
        07.05.2018 16:21

        В статье вы написали, что добавили раздел «Лимиты». Но я так и не нашёл где он находится. Можете подсказать?


        1. hazratgs Автор
          07.05.2018 17:22

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


          1. rootbmb
            08.05.2018 12:21

            приятно удивило удивив на видео Дербент)))


            1. hazratgs Автор
              08.05.2018 12:21

              ну как бы я сам с Дербента)


  1. rootbmb
    08.05.2018 12:58

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