Урок 5

Автопостинг в каналы

Примечание к уроку

Сам текст написан в 2015-м. В начале 2017 года ВК начал требовать токен пользователя для вызова метода wall.get и многих других. Процесс получения токена для ВК не относится к теме учебника, следовательно, описан не будет. По ссылке https://vk.com/dev/manuals любой желающий сможет найти документацию и описание процесса получения токена.

Где-то в 2018-2019 годах метод wall.get начал возвращать другую структуру ответа. Текст и код урока написаны для версии API v5.68

Описание

Когда в Telegram появились каналы, поначалу я к ним относился как к неудачной попытке клонировать Twitter. Сразу появились тупые канальчики с тупыми картиночками, что только усугубляло положение дел. Однако сейчас, спустя больше месяца [этот текст был написан в 2015 – прим. авт.], моё мнение изменилось на диаметрально противоположное. Призванные заменить списки рассылки (ими вообще кто-то пользовался?), каналы дали возможность получать огромное количество контента, которым можно делиться, который можно сохранять.

В Bot FAQ есть интересная фраза: “Мы будем внимательно смотреть на то, как люди пользуются ботами и развивать их в том направлении”. С учетом того, что многие боты занимались именно рассылкой информации, каналы - это очень логичный и правильный шаг в развитии автоматизированных средств.
С точки зрения программиста, каналы решают сразу несколько ключевых проблем:

  1. Не надо зависеть от временных сбоев серверов Telegram при поллинге, т.к. нет входящих сообщений.
  2. Анонимная рассылка сообщений (некоторые люди просили сделать возможность отправлять свои сообщения через “безликого” бота) максимально упростилась.
  3. Не надо вести списки пользователей, которым нужно отправить информацию и внедрять паузы между отправкой сообщения по очереди всем, эту заботу берёт на себя сам Telegram.

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

Получаем записи

В качестве “подопытного кролика” выберем официальный блог ВКонтакте. Получать новые записи будем при помощи VK API. Итак, сформируем ссылку, которая будет нам возвращать последние 10 записей от имени сообщества из группы VK Team: https://api.vk.com/method/wall.get?domain=team&count=10&filter=owner&access_token=token&v=5.68 Что здесь что? domain - короткое имя сообщества. Если его нет, то меняем domain=xxx на owner_id=-yyy (обратите внимание на минус перед числом, это важно). count - число выводимых записей. Чем дольше пауза между проверками и чем чаще в сообществе появляются записи, тем большее число нужно выставить, но не более 100. filter=owner просит сервер выводить записи только от имени группы, полезно, если стена открыта, access_token - это токен пользователя, который «дёргает» API от имени одного из приложений, а v=5.68 – номер используемой версии API. При использовании более свежей версии (5.100 и выше), структура ответа будет другой, оставляю это для самостоятельного изучения. Давайте теперь создадим файл bot.py, в котором зададим основные константы и импорты:

import time
import eventlet
import requests
import logging
import telebot
from time import sleep

 # Каждый раз получаем по 10 последних записей со стены
URL_VK = 'https://api.vk.com/method/wall.get?domain=team&count=10&filter=owner&access_token=Ваш_токен_VK&v=5.68'
FILENAME_VK = 'last_known_id.txt'
BASE_POST_URL = 'https://vk.com/wall-22822305_'

BOT_TOKEN = 'токен бота, постящего в канал'
CHANNEL_NAME = '@канал'

bot = telebot.TeleBot(BOT_TOKEN)

Во-первых, не забудьте сделать нужного бота администратором канала, иначе фокус не удастся. Во-вторых, обратите внимание, что в импортах появилась библиотека eventlet, она поможет нам избежать проблем при получении записей из ВК. В-третьих, в указанный txt-файл будем записывать номер верхнего поста на момент проверки, я решил не заморачиваться с созданием key-value хранилищ, ради одного числа-то. В-четвёртых, в качестве параметра BASE_POST_URL указываем ссылку на любой пост из нашей группы без последнего числа.

Иногда ВК начинает дурить и не возвращает список постов за приемлемое время. В этом случае, нам нужно отвалиться по таймауту и пропустить проверку. Можно, конечно, попробовать ещё раз, но мы люди не настойчивые :)

def get_data():
    timeout = eventlet.Timeout(10)
    try:
        feed = requests.get(URL_VK)
        return feed.json()
    except eventlet.timeout.Timeout:
        logging.warning('Got Timeout while retrieving VK JSON data. Cancelling...')
        return None
    finally:
        timeout.cancel()

Суть простая: получилось - возвращаем объект с постами. Не получилось - возвращаем None. Теперь перейдем непосредственно к парсингу. Алгоритм будет такой:

  1. Открываем файл, хранящий последний известный номер верхнего поста.
  2. Если метод get_data() вернул объект с записями, начинаем проходить по нему со второго элемента, т.к. первый - какое-то неизвестное мне рандомное число.
  3. Если номер поста меньше или равен последнему известному - завершаем обход.
  4. Проверяем наличие закрепленного поста. Если таковой есть, то передаем функции отправки сообщений все записи, кроме закрепленной. Иначе - просто передаем все.
  5. У каждой проверяемой записи забираем ID, подставляем рядом с BASE_POST_URL и получаем полный ID записи.
  6. Отправляем его в канал.
  7. Как только обход завершился, берем номер первой (второй, если первая - закрепленная) записи и записываем в файл поверх старого значения.
  8. Засыпаем или завершаемся.

По поводу п.8: дополнительно предусмотрим в нашей программе два режима: в первом режиме скрипт постоянно работает, засыпая после каждой итерации на 4 минуты; во втором режиме скрипт просто завершает работу, что позволяет ставить его в планировщик cron. В определении режима нам поможет константная переменная SINGLE_RUN, которую надо не забыть указать где-нибудь вверху.

def send_new_posts(items, last_id):
    for item in items:
        if item['id'] <= last_id:
            break
        link = '{!s}{!s}'.format(BASE_POST_URL, item['id'])
        bot.send_message(CHANNEL_NAME, link)
        # Спим секунду, чтобы избежать разного рода ошибок и ограничений (на всякий случай!)
        time.sleep(1)
    return


def check_new_posts_vk():
    # Пишем текущее время начала
    logging.info('[VK] Started scanning for new posts')
    with open(FILENAME_VK, 'rt') as file:
        last_id = int(file.read())
        if last_id is None:
            logging.error('Could not read from storage. Skipped iteration.')
            return
        logging.info('Last ID (VK) = {!s}'.format(last_id))
    try:
        feed = get_data()
        # Если ранее случился таймаут, пропускаем итерацию. Если всё нормально - парсим посты.
        if feed is not None:
            entries = feed['response'][1:]
            try:
                # Если пост был закреплен, пропускаем его
                tmp = entries[0]['is_pinned']
                # И запускаем отправку сообщений
                send_new_posts(entries[1:], last_id)
            except KeyError:
                send_new_posts(entries, last_id)
            # Записываем новый last_id в файл.
            with open(FILENAME_VK, 'wt') as file:
                try:
                    tmp = entries[0]['is_pinned']
                    # Если первый пост - закрепленный, то сохраняем ID второго
                    file.write(str(entries[1]['id']))
                    logging.info('New last_id (VK) is {!s}'.format((entries[1]['id'])))
                except KeyError:
                    file.write(str(entries[0]['id']))
                    logging.info('New last_id (VK) is {!s}'.format((entries[0]['id'])))
    except Exception as ex:
        logging.error('Exception of type {!s} in check_new_post(): {!s}'.format(type(ex).__name__, str(ex)))
        pass
    logging.info('[VK] Finished scanning')
    return

Осталось дело за малым - написать логику запуска всего процесса и инициализировать логгер, который будет писать в текстовый файлик обо всех событиях в жизни бота:

if __name__ == '__main__':
    # Избавляемся от спама в логах от библиотеки requests
    logging.getLogger('requests').setLevel(logging.CRITICAL)
    # Настраиваем наш логгер
    logging.basicConfig(format='[%(asctime)s] %(filename)s:%(lineno)d %(levelname)s - %(message)s', level=logging.INFO,
                        filename='bot_log.log', datefmt='%d.%m.%Y %H:%M:%S')
    if not SINGLE_RUN:
        while True:
            check_new_posts_vk()
            # Пауза в 4 минуты перед повторной проверкой
            logging.info('[App] Script went to sleep.')
            time.sleep(60 * 4)
    else:
        check_new_posts_vk()
    logging.info('[App] Script exited.\n')

Перед запуском бота, создадим вручную файл last_known_id.txt и впишем в него один из последних числовых ID, в моём случае это было чудесное число 1893. После включения бота, в зависимости от значения SINGLE_RUN, он будет либо постоянно работать, проверяя каждые 4 минуты на наличие новых постов, либо завершится после окончания первой проверки. Для себя я выбрал второй вариант, добавив скрипт в cron.

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

← Урок №4 Урок №6 →