Вторник , 5 мая 2020
Бизнес-Новости
Разное / Создать бота в телеграм – Manybot

Создать бота в телеграм – Manybot

Содержание

Создание и хостинг телеграм бота. От А до Я / Habr

Привет, хабрчане! Какой бы заезженной не была тема создания телеграм бота на python3, я не нашёл инструкций, где показан путь от первой строчки кода до деплоинга бота (по крайней мере все методы, что я видел, немного устарели). В этой статье я хочу показать процесс создания бота от написания BotFather-у до деплоинга бота на Heroku.

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

P.S. Пишите если нужна статья по созданию более сложного бота, т.е. с вебхуками, БД с настройками юзеров и т.д.


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

BotFather


Для начала нам надо зарегистрировать нашего бота в Telegram. Для этого:

В поиске вбиваем @BotFather и переходим в диалог с Отцом Ботов.

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

P.S. Оно должно заканчиваться на Bot/bot

Вот. Нам дали API ключ и ссылку на бота. Желательно сохранить API ключ и перейти в диалог с ботом, чтобы потом не копаться в переписке с BotFather

Дальше добавим ему пару команд: пропишем /setcommands и одним сообщением, т.к. /setcommands не добавляет команды, а задаёт их с нуля, пошлём ему команды.

all - спарсить заголовки с вкладки "ВСЁ ПОДРЯД"
top - спарсить заголовки с вкладки "ЛУЧШЕЕ"

На этом работа с BotFather закончилась, перейдём к следующей части.

Установка и настройка pipenv. Первый запуск.


Для начала создадим файл, в котором будет основной код бота bot.py. Если бот большой, то сразу создавайте файлы, куда вы вынесете функции, классы и т.д, иначе читаемость кода стремится к нулю. Я добавлю parser.py

Установим pipenv, если его конечно ещё нет.

Для Windows:

pip install pipenv

Для Linux:
sudo pip3 install pipenv

Установим pipenv в папку проекта.
pipenv install

Установим интересующие нас библиотеки. Я буду работать с PyTelegramBotAPI. Также для парсинга добавим BeautifulSoup4.
pipenv install PyTelegramBotAPI
pipenv install beautifulsoup4

Начинаем писать код!

Открываем bot.py, импортируем библиотеки и создаём главные переменные.

import telebot
import parser

#main variables
TOKEN = "555555555:AAAAaaaAaaA1a1aA1AAAAAAaaAAaa4AA"
bot = telebot.TeleBot(TOKEN)

Запустим бота. Посмотри наличие ошибок.Как запустить?Для Windows:
python bot.py

Для Linux:
python3 bot.py


Если ошибок не появилось, то продолжим.

Хэндлеры. Отвечаем на команды и сообщения


Пришло время научить бота отвечать нам. Возможно даже сделать его ответы полезными.
Основы взаимодействия. Ответ на команды

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

Начнём с самого простого: ответим на команды /start и /go

@bot.message_handler(commands=['start', 'go'])
def start_handler(message):
    bot.send_message(message.chat.id, 'Привет, когда я вырасту, я буду парсить заголовки с Хабра')
bot.polling()

Сейчас разберёмся что это и как это работает. Передаём в message_handler параметр commands равный массиву со строками — командами, на которые он будет отвечать описанным ниже образом. (На все эти команды он ответит одинаково). Далее используем send_message, в него записываем id чата (его можно достать из message.chat.id), в который отправить сообщение и, собственно, само сообщение. Нельзя забыть написать bot.polling() в конце кода, иначе бот сразу же выключиться. Почему так мы узнаем позже.

Теперь можно запустить бота и написать ему /start или /go и он ответит.

P.S. Сообщение может быть не только строкой, а, в принципе, чем угодно.

P.S.S. Что за message?Это json объект, хранящий информацию об отправителе, чате, и самом сообщении.
{
  'content_type': 'text',
  'message_id': 5,
  'from_user':
    {
      'id': 333960329,
      'first_name': 'Nybkox',
      'username': 'nybkox',
      'last_name': None
    },
  'date': 1520186598,
  'chat':
    {
      'type': 'private',
      'last_name': None,
      'first_name': 'Nybkox',
      'username': 'nybkox',
      'id': 333960329,
      'title': None,
      'all_members_are_administrators': None
    },
    'forward_from_chat': None,
    'forward_from': None,
    'forward_date': None,
    'reply_to_message': None,
    'edit_date': None,
    'text': '/start',
    'entities': [<telebot.types.MessageEntity object at 0x7f3061f42710>],
    'audio': None,
    'document': None,
    'photo': None,
    'sticker': None,
    'video': None,
    'voice': None,
    'caption': None,
    'contact': None,
    'location': None,
    'venue': None,
    'new_chat_member': None,
    'left_chat_member': None,
    'new_chat_title': None,
    'new_chat_photo': None,
    'delete_chat_photo': None,
    'group_chat_created': None,
    'supergroup_chat_created': None,
    'channel_chat_created': None,
    'migrate_to_chat_id': None,
    'migrate_from_chat_id': None,
    'pinned_message': None
}


Основы взаимодействия. Ответ на текстовые сообщения.

Теперь обработаем текстовые сообщения бота. Самое важное что нам нужно знать это то, что текст сообщения храниться в message.text и то, что, чтобы обрабатывать текст в message_handler нужно передавать content_types=[‘text’].

Добавим вот такой код.

@bot.message_handler(content_types=['text'])
def text_handler(message):
    text = message.text.lower()
    chat_id = message.chat.id
    if text == "привет":
        bot.send_message(chat_id, 'Привет, я бот - парсер хабра.')
    elif text == "как дела?":
        bot.send_message(chat_id, 'Хорошо, а у тебя?')
    else:
        bot.send_message(chat_id, 'Простите, я вам не понял :(')

Тут мы довабили пару переменных: вынесли текст сообщения (в нижнем регистре, чтобы не было лишних проблем с теми кто пишет капсом, заборчиком и т.д.) в переменную text, вынесли message.chat.id в отдельную переменную, чтобы каждый раз не обращаться к message. Также мы построили небольшое ветвление, для ответа на определённые сообщения, а также ответ на случай непонятного боту сообщения.Итоговый код
import bs4
import parser

#main variables
TOKEN = "555555555:AAAAaaaAaaA1a1aA1AAAAAAaaAAaa4AA"
bot = telebot.TeleBot(TOKEN)

#handlers
@bot.message_handler(commands=['start', 'go'])
def start_handler(message):
    bot.send_message(message.chat.id, 'Привет, когда я вырасту, я буду парсить заголовки с хабра')

@bot.message_handler(content_types=['text'])
def text_handler(message):
    text = message.text.lower()
    chat_id = message.chat.id
    if text == "привет":
        bot.send_message(chat_id, 'Привет, я бот - парсер хабра.')
    elif text == "как дела?":
        bot.send_message(chat_id, 'Хорошо, а у тебя?')
    else:
        bot.send_message(chat_id, 'Простите, я вас не понял :(')

bot.polling()


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

Для ответа на картинки, стикеры, документы, аудио и т.д. нужно всего лишь поменять content_types=[‘text’].

Рассмотрим пример с картинкой, добавив этот код.

@bot.message_handler(content_types=['photo'])
def text_handler(message):
    chat_id = message.chat.id
    bot.send_message(chat_id, 'Красиво.')

Все типы контента:

text, audio, document, photo, sticker, video, video_note, voice, location, contact, new_chat_members, left_chat_member, new_chat_title, new_chat_photo, delete_chat_photo, group_chat_created, supergroup_chat_created, channel_chat_created, migrate_to_chat_id, migrate_from_chat_id, pinned_message

Строим цепочку ответов.

Пришло время закончить с элементарными действиями и начать что-то серьёзное. Попробуем построить цепочку ответов. Для этого нам понадобиться register_next_step_handler(). Создадим простой пример, на котором и разберёмся как работает register_next_step_handler().
@bot.message_handler(commands=['start', 'go'])
def start_handler(message):
    chat_id = message.chat.id
    text = message.text
    msg = bot.send_message(chat_id, 'Сколько вам лет?')
    bot.register_next_step_handler(msg, askAge)

def askAge(message):
    chat_id = message.chat.id
    text = message.text
    if not text.isdigit():
        msg = bot.send_message(chat_id, 'Возраст должен быть числом, введите ещё раз.')
        bot.register_next_step_handler(msg, askAge) #askSource
        return
    msg = bot.send_message(chat_id, 'Спасибо, я запомнил что вам ' + text + ' лет.')

И так, в первой функции добавился bot.register_next_step_handler(msg, askAge), в него мы передаём сообщение, которые хотим послать, и следующий щаг, к которому перейти после ответа пользователя.

Во второй функции всё поинтересней, здесь идёт проверка ввёл ли пользователь число, и, если нет, то функция рекурсивно вызывает сама себя, с сообщением «Возраст должен быть числом, введите ещё раз.». Если пользователь ввёл всё верно, то он получает ответ.

Но, есть тут проблема. Можно повторно вызвать команду /go или /start, и начнётся бардак.


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

@bot.message_handler(commands=['start', 'go'])
def start_handler(message):
    global isRunning
    if not isRunning:
        chat_id = message.chat.id
        text = message.text
        msg = bot.send_message(chat_id, 'Сколько вам лет?')
        bot.register_next_step_handler(msg, askAge) #askSource
        isRunning = True

def askAge(message):
    chat_id = message.chat.id
    text = message.text
    if not text.isdigit():
        msg = bot.send_message(chat_id, 'Возраст должен быть числом, введите ещё раз.')
        bot.register_next_step_handler(msg, askAge) #askSource
        return
    msg = bot.send_message(chat_id, 'Спасибо, я запомнил что вам ' + text + ' лет.')
    isRunning = False

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

Для начала нужен сам парсер. Обратим внимание на то, что во вкладках «Лучшее» и «Всё подряд» есть дополнительные фильтры: сутки, неделя, месяц и ≥10, ≥25, ≥50, ≥100 соответственно.
Парсер конечно можно написать и в 1 функцию, но я разобью на 2, так будет проще читать код.Парсер.
import urllib.request
from bs4 import BeautifulSoup

def getTitlesFromAll(amount, rating='all'):
    output = ''
    for i in range(1, amount+1):
        try:
            if rating == 'all':
                html = urllib.request.urlopen('https://habrahabr.ru/all/page'+ str(i) +'/').read()
            else:
                html = urllib.request.urlopen('https://habrahabr.ru/all/'+ rating +'/page'+ str(i) +'/').read()
        except urllib.error.HTTPError:
            print('Error 404 Not Found')
            break
        soup = BeautifulSoup(html, 'html.parser')
        title = soup.find_all('a', class_ = 'post__title_link')
        for i in title:
            i = i.get_text()
            output += ('- "'+i+'",\n')
    return output

def getTitlesFromTop(amount, age='daily'):
    output = ''
    for i in range(1, amount+1):
        try:
            html = urllib.request.urlopen('https://habrahabr.ru/top/'+ age +'/page'+ str(i) +'/').read()
        except urllib.error.HTTPError:
            print('Error 404 Not Found')
            break
        soup = BeautifulSoup(html, 'html.parser')
        title = soup.find_all('a', class_ = 'post__title_link')
        for i in title:
            i = i.get_text()
            output += ('- "'+i+'",\n')
    return output


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

Итоговый код:

bot.py
import telebot
import bs4
from Task import Task
import parser

#main variables
TOKEN = '509706011:AAF7ghlYpqS5n7uF8kN0VGDCaaHnxfZxofg'
bot = telebot.TeleBot(TOKEN)
task = Task()

#handlers
@bot.message_handler(commands=['start', 'go'])
def start_handler(message):
    if not task.isRunning:
        chat_id = message.chat.id
        msg = bot.send_message(chat_id, 'Откуда парсить?')
        bot.register_next_step_handler(msg, askSource)
        task.isRunning = True

def askSource(message):
    chat_id = message.chat.id
    text = message.text.lower()
    if text in task.names[0]:
        task.mySource = 'top'
        msg = bot.send_message(chat_id, 'За какой временной промежуток?')
        bot.register_next_step_handler(msg, askAge)
    elif text in task.names[1]:
        task.mySource = 'all'
        msg = bot.send_message(chat_id, 'Какой минимальный порог рейтинга?')
        bot.register_next_step_handler(msg, askRating)
    else:
        msg = bot.send_message(chat_id, 'Такого раздела нет. Введите раздел корректно.')
        bot.register_next_step_handler(msg, askSource)
        return

def askAge(message):
    chat_id = message.chat.id
    text = message.text.lower()
    filters = task.filters[0]
    if text not in filters:
        msg = bot.send_message(chat_id, 'Такого временного промежутка нет. Введите порог корректно.')
        bot.register_next_step_handler(msg, askAge)
        return
    task.myFilter = task.filters_code_names[0][filters.index(text)]
    msg = bot.send_message(chat_id, 'Сколько страниц парсить?')
    bot.register_next_step_handler(msg, askAmount)

def askRating(message):
    chat_id = message.chat.id
    text = message.text.lower()
    filters = task.filters[1]
    if text not in filters:
        msg = bot.send_message(chat_id, 'Такого порога нет. Введите порог корректно.')
        bot.register_next_step_handler(msg, askRating)
        return
    task.myFilter = task.filters_code_names[1][filters.index(text)]
    msg = bot.send_message(chat_id, 'Сколько страниц парсить?')
    bot.register_next_step_handler(msg, askAmount)

def askAmount(message):
    chat_id = message.chat.id
    text = message.text.lower()
    if not text.isdigit():
        msg = bot.send_message(chat_id, 'Количество страниц должно быть числом. Введите корректно.')
        bot.register_next_step_handler(msg, askAmount)
        return
    if int(text) < 1 or int(text) > 11:
        msg = bot.send_message(chat_id, 'Количество страниц должно быть >0 и <11. Введите корректно.')
        bot.register_next_step_handler(msg, askAmount)
        return
    task.isRunning = False
    output = ''
    if task.mySource == 'top':
        output = parser.getTitlesFromTop(int(text), task.myFilter)
    else:
        output = parser.getTitlesFromAll(int(text), task.myFilter)
    msg = bot.send_message(chat_id, output)

bot.polling(none_stop=True)

Тут добавился none_stop=True) к bot.polling, из-за этого бот не будет падать при каждой ошибке.
Task.py
class Task():
    isRunning = False
    names = [
        ['лучшие', 'лучшее', 'топ'],
        ['всё', 'всё подряд', 'all']
    ]
    filters = [
        ['сутки', 'неделя', 'месяц'],
        ['без порога', '10', '25', '50', '100']
    ]
    filters_code_names = [
        ['daily', 'weekly', 'monthly'],
        ['all', 'top10', 'top25', 'top50', 'top100']
    ]
    mySource = ''
    myFilter = ''
    def __init__(self):
        return

parser.py
import urllib.request
from bs4 import BeautifulSoup

def getTitlesFromAll(amount, rating='all'):
    output = ''
    for i in range(1, amount+1):
        try:
            if rating == 'all':
                html = urllib.request.urlopen('https://habrahabr.ru/all/page'+ str(i) +'/').read()
            else:
                html = urllib.request.urlopen('https://habrahabr.ru/all/'+ rating +'/page'+ str(i) +'/').read()
        except urllib.error.HTTPError:
            print('Error 404 Not Found')
            break
        soup = BeautifulSoup(html, 'html.parser')
        title = soup.find_all('a', class_ = 'post__title_link')
        for i in title:
            i = i.get_text()
            output += ('- "'+i+'",\n')
    return output

def getTitlesFromTop(amount, age='daily'):
    output = ''
    for i in range(1, amount+1):
        try:
            html = urllib.request.urlopen('https://habrahabr.ru/top/'+ age +'/page'+ str(i) +'/').read()
        except urllib.error.HTTPError:
            print('Error 404 Not Found')
            break
        soup = BeautifulSoup(html, 'html.parser')
        title = soup.find_all('a', class_ = 'post__title_link')
        for i in title:
            i = i.get_text()
            output += ('- "'+i+'",\n')
    return output


Теория. Методы взаимодействия с ботом.

Мы используем long polling для получения данных о сообщениях от бота.

bot.polling(none_stop=True)

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

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

Маркапы. Добавляем клавиатуры для быстрого ответа.


Наконец основной код дописан. Теперь можно передохнуть и написать маркапы. Я думаю вы неоднократно видели их, но всё же, приложу скриншот. [SCREENSHOT]

Я выведу маркапы в отдельный файл — markups.py.

В написании маркапов нет ничего сложного. Нужно лишь создать маркап, указать пару параметров, создать пару кнопок и добавить их в маркап, далее просто указываем reply_markup=markup в send_message.

Примерmarkups.py
from telebot import types
source_markup = types.ReplyKeyboardMarkup(row_width=2, resize_keyboard=True)
source_markup_btn1 = types.KeyboardButton('Лучшие')
source_markup_btn2 = types.KeyboardButton('Всё подряд')
source_markup.add(source_markup_btn1, source_markup_btn2)

В параметры маркапа указываем ширину строки и изменение размеров кнопок, иначе они огромны.Можно конечно заполнять отдельно каждую строк.
markup = types.ReplyKeyboardMarkup()
itembtna = types.KeyboardButton('a')
itembtnv = types.KeyboardButton('v')
itembtnc = types.KeyboardButton('c')
itembtnd = types.KeyboardButton('d')
itembtne = types.KeyboardButton('e')
markup.row(itembtna, itembtnv)
markup.row(itembtnc, itembtnd, itembtne)


bot.py
def start_handler(message):
    if not task.isRunning:
        chat_id = message.chat.id
        msg = bot.send_message(chat_id, 'Откуда парсить?', reply_markup=m.source_markup)
        bot.register_next_step_handler(msg, askSource)
        task.isRunning = True


Применим полученные знания к нашему боту.Итоговый кодmarkups.py
from telebot import types

start_markup = types.ReplyKeyboardMarkup(row_width=1, resize_keyboard=True)
start_markup_btn1 = types.KeyboardButton('/start')
start_markup.add(start_markup_btn1)

source_markup = types.ReplyKeyboardMarkup(row_width=2, resize_keyboard=True)
source_markup_btn1 = types.KeyboardButton('Лучшие')
source_markup_btn2 = types.KeyboardButton('Всё подряд')
source_markup.add(source_markup_btn1, source_markup_btn2)

age_markup = types.ReplyKeyboardMarkup(row_width=3, resize_keyboard=True)
age_markup_btn1 =  types.KeyboardButton('Сутки')
age_markup_btn2 =  types.KeyboardButton('неделя')
age_markup_btn3 =  types.KeyboardButton('Месяц')
age_markup.add(age_markup_btn1, age_markup_btn2, age_markup_btn3)

rating_markup = types.ReplyKeyboardMarkup(row_width=3, resize_keyboard=True)
rating_markup_btn1 =  types.KeyboardButton('Без порога')
rating_markup_btn2 =  types.KeyboardButton('10')
rating_markup_btn3 =  types.KeyboardButton('25')
rating_markup_btn4 =  types.KeyboardButton('50')
rating_markup_btn5 =  types.KeyboardButton('100')
rating_markup.row(rating_markup_btn1, rating_markup_btn2)
rating_markup.row(rating_markup_btn3, rating_markup_btn4, rating_markup_btn5)

amount_markup = types.ReplyKeyboardMarkup(row_width=3, resize_keyboard=True)
amount_markup_btn1 =  types.KeyboardButton('1')
amount_markup_btn2 =  types.KeyboardButton('3')
amount_markup_btn3 =  types.KeyboardButton('5')
amount_markup.add(amount_markup_btn1, amount_markup_btn2, amount_markup_btn3)

bot.py
import telebot
import bs4
from Task import Task
import parser
import markups as m

#main variables
TOKEN = '509706011:AAF7aaaaaaaaaaaaaaaaaaaAAAaaAAaAaAAAaa'
bot = telebot.TeleBot(TOKEN)
task = Task()

#handlers
@bot.message_handler(commands=['start', 'go'])
def start_handler(message):
    if not task.isRunning:
        chat_id = message.chat.id
        msg = bot.send_message(chat_id, 'Откуда парсить?', reply_markup=m.source_markup)
        bot.register_next_step_handler(msg, askSource)
        task.isRunning = True

def askSource(message):
    chat_id = message.chat.id
    text = message.text.lower()
    if text in task.names[0]:
        task.mySource = 'top'
        msg = bot.send_message(chat_id, 'За какой временной промежуток?', reply_markup=m.age_markup)
        bot.register_next_step_handler(msg, askAge)
    elif text in task.names[1]:
        task.mySource = 'all'
        msg = bot.send_message(chat_id, 'Какой минимальный порог рейтинга?', reply_markup=m.rating_markup)
        bot.register_next_step_handler(msg, askRating)
    else:
        msg = bot.send_message(chat_id, 'Такого раздела нет. Введите раздел корректно.')
        bot.register_next_step_handler(msg, askSource)
        return

def askAge(message):
    chat_id = message.chat.id
    text = message.text.lower()
    filters = task.filters[0]
    if text not in filters:
        msg = bot.send_message(chat_id, 'Такого временного промежутка нет. Введите порог корректно.')
        bot.register_next_step_handler(msg, askAge)
        return
    task.myFilter = task.filters_code_names[0][filters.index(text)]
    msg = bot.send_message(chat_id, 'Сколько страниц парсить?', reply_markup=m.amount_markup)
    bot.register_next_step_handler(msg, askAmount)

def askRating(message):
    chat_id = message.chat.id
    text = message.text.lower()
    filters = task.filters[1]
    if text not in filters:
        msg = bot.send_message(chat_id, 'Такого порога нет. Введите порог корректно.')
        bot.register_next_step_handler(msg, askRating)
        return
    task.myFilter = task.filters_code_names[1][filters.index(text)]
    msg = bot.send_message(chat_id, 'Сколько страниц парсить?', reply_markup=m.amount_markup)
    bot.register_next_step_handler(msg, askAmount)

def askAmount(message):
    chat_id = message.chat.id
    text = message.text.lower()
    if not text.isdigit():
        msg = bot.send_message(chat_id, 'Количество страниц должно быть числом. Введите корректно.')
        bot.register_next_step_handler(msg, askAmount)
        return
    if int(text) < 1 or int(text) > 5:
        msg = bot.send_message(chat_id, 'Количество страниц должно быть >0 и <6. Введите корректно.')
        bot.register_next_step_handler(msg, askAmount)
        return
    task.isRunning = False
    print(task.mySource + " | " + task.myFilter + ' | ' + text) #
    output = ''
    if task.mySource == 'top':
        output = parser.getTitlesFromTop(int(text), task.myFilter)
    else:
        output = parser.getTitlesFromAll(int(text), task.myFilter)
    msg = bot.send_message(chat_id, output, reply_markup=m.start_markup)

bot.polling(none_stop=True)

Ура! С кодом впринципе разобрались. Теперь самое важное — деплоинг бота не хероку.

Деплоим бота на Heroku.


Для начала надо зарегистрироваться на Хероку и на Гитхабе.

Теперь создаём репозиторий на гитхабе. (нажмите плюсик слева от вашего аватара)
Сейчас нам нужен Procfile (Procfile.windows для windows). Создаём его и записываем в него bot: python3 bot.py

Теперь удаляем TOKEN из bot.py, здесь он не нужен, ведь мы будем загружать этот файл на гитхаб. Через тот же терминале, что использовали для запуска бота, заливаем файлы на гитхаб. (Предворительно удалите папку __pycache__).

echo "# HabrParser_Bot" >> README.md
git init
git add .
git add *
git commit -m "Initial Commit" -a
git remote add origin origin https://github.com/name/botname.git #Указываем свою ссылку
git push -u origin master

Гит просит логин и пароль, спокойно вводим и преступаем к деплоингу бота на хероку. Пишем всё в том же терминале.

Теперь возвращаем TOKEN в bot.py, здесь он нужен, ведь мы будем загружать этот файл на хероку.

heroku login #Вводим email и пароль
heroku create --region eu habrparserbot #Не забываемпоменять имя приложения
#P.S. в имени могут быть только буквы в нижнем регитсре, цифры  и тире.
heroku addons:create heroku-redis:hobby-dev -a habrparserbot #И снова меняем имя!
heroku buildpacks:set heroku/python
git push heroku master
heroku ps:scale bot=1 # запускаем бота
heroku logs --tail #включаем логи

Чтобы выключить бота
heroku ps:stop bot

И, не забываем перед залитием на гитхаб и удалить TOKEN из нашего bot.py. Ведь нам не нужно, чтобы кто-то им пользовался. Можно конечно воспользоваться .gitignore и вынести токены в отдельный фай.
Поздравляю!

Работа окончена, бот работает удалённо.

Ссылки


Конечный код бота на гитхабе
API для управления ботом
Про деплоинг
Про pipenv
Большой гайд, возможно кому-то пригодится

Заключение


Если кому-то было интересно, то цель написания статьи выполнена. Если кому-то хочется увидеть статью про более сложного бота (с вебхуками, подключенной БД с настройками пользователей и т.д.) — пишите.UPDATES
UPD1
  • Добавлены якори в содержание.
  • Изменён алгоритм залития кода на гитхаб и хероку.
  • Убрана версия PyTelegramBotAPI, т.к. теперь хероку работает нормально с новыми версиями.

habr.com

Как создать и подключить бота к каналу Telegram для оформления постов!

В мессенджере Telegram, если размещать на своём канале посты стандартным способом, не заморачиваясь с подключением каких-либо ботов, вы не сможете эти посты никак оформить. Максимум — текст и ссылку приложить. И то ссылку эту даже спрятать не получится в какое-нибудь слово или фразу, можно только в обычном виде отправить 🙂 А это, как говорится, не камельфо, некрасиво… Ну и другие возможности оформления также будут закрыты: возможность оформить пост разными шрифтами, прикрепить картинку, добавить смайликов и прочего.

Сейчас речь идёт именно о посте (т.е. о сообщении) на канал, а не о статье в Telegra.ph!

Но вы же хотите канал вести нормально, а не как попало? 🙂 А нормально — это значит не только годный контент выдавать подписчикам, но и оформлять на уровне!

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

Система этих ботов в Телеграме на самом деле маленько запутанная 🙂 Я не сразу въехал что к чему и почему, например, одного бота нужно создавать через другого бота 🙂 Но я помогу в этом разобраться и после того как начнете потихоньку работать с ботами, пазл в голове полностью сложится!

Боты ControllerBot, BotFather и собственный наш бот. Что к чему!

Для начала внесу немного ясности, чтобы сразу хоть немного прояснить ситуацию с этими ботами 🙂

  • ControllerBot — это, проще говоря, основной управляющий бот в Телеграме. Через него вы можете управлять своими каналами (подключать к каналам собственных ботов и отключать), перейти к публикации поста в канале.

    С этого бота мы начнём работу по созданию своего бота и подключению его к каналу.

  • BotFather — служит непосредственно для управления вашими ботами. Через него можно создать и редактировать своих ботов.

  • Наш собственный бот, которого мы создадим и назовём как душе угодно, уже будет нам нужен именно для создания и оформления постов на наш канал.

Создаём собственного бота через ControllerBot и BotFather

Для добавления бота, в строке поиска Telegram наберите «ControllerBot» [1], затем откройте найденного бота с таким же названием [2] и в окне бота нажмите «Запустить» [3] внизу.



Бот предложит выбрать язык, на котором вам будут выдаваться его инструкции. Выбирайте родной язык:



Бот предлагает нам список возможных действий. Сейчас нам необходимо подключить свой канал. Кликаем по предложенной ссылке «/addchannel» (добавить канал).



Получаем инструкцию по подключению бота. Кликаем по имени бота «@BotFather», указанного в пункте 1.



Вы перейдёте сразу в окно бота BotFather, через которого уже будем создавать своего бота. Нажмите «Запустить» внизу.



Бот ответит нам, после чего кликните по команде «/newbot»:



Теперь нужно указать имя создаваемого бота и отправить сообщение. Можно указывать любое, например, часть имени фамилии или часть названия канала, как удобно. Пример:



Теперь необходимо указать ник для создаваемого бота. Здесь можно указать тоже самое, но обязательно на конце должно стоять «bot» либо через знак нижнего подчёркивания «_bot». Пример:



Если вы вдруг получили сообщение «Sorry, this username is already taken. Please try something different.», то значит такой ник уже занят и нужно указать другой.



Если всё верно сделали, получите сообщение об успешном создании бота и специальный длинный ключ (токен):



Его нужно скопировать в ControllerBot, с которым мы сразу начинали работу.

Вставив ключ в окно чата ControllerBot, он в ответ выдаст инструкцию по добавлению канала к нему. Выполняем её…

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



Затем открываем созданный нами канал для работы, переходим в его меню и выбираем «Управление каналом».



Открываем раздел «Администраторы».



Внизу нажимаем «Добавить администратора».



В строке поиска вверху [1] вставьте имя скопированного бота и после, когда он появится в результатах поиска, кликните по нему [2].



Нажимаем «ОК» в окне с вопросом о назначении бота администратором.



Ничего не изменяя в настройках, нажмите «Сохранить» в следующем окне.



Всё, бот в качестве администратора канала добавлен и теперь окно можно закрыть:



Теперь, выполняя пункт 2, нам нужно переслать в чат «Controller Bot» любое сообщение с вашего канала или его адрес. Проще взять адрес, поскольку канал новый и в нём может ещё не быть никаких сообщений.

Открываем снова меню канала и выбираем «Описание канала».



Копируем ссылку, которая является адресом вашего канала (кликните по ней правой кнопкой мыши и выберите «Копировать ссылку»).



Отправляем эту ссылку боту «Controller Bot».



Следующим сообщением он предложит выбрать часовой пояс. Нужно отправить в ответ название вашего города.



Далее бот уточняет, верно ли он выбрал город. Нажмите «Верно», если всё так или введите другой ближайший к вам город.



На этом всё. Канал к боту «Controller Bot» был подключен, основные настройки выполнены, о чём свидетельствует очередной ответ бота:




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

Заключение

Без ботов, работая со своим каналом, пожалуй, не обойтись. Ну или обойтись, но тогда канал будете вести не совсем качественно и упустите разные возможности! Поэтому, скорее всего, придётся с этими всеми нюансами столкнуться, если решите всерьёз заниматься работой в Telegram, т.е. развитием своего канала.

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

Буду рад адекватным комментариям и готов подискутировать по теме статьи 🙂

serfery.ru

15 лучших ботов Telegram для бизнеса / Habr

Боты для Telegram все больше входят в нашу повседневную жизнь, их способности становятся разнообразными, а функции, которые они выполняют — заменяют привычные «тяжелые» сервисы. Мы подготовили список ботов, которыми пользуемся сами — бот-юрист, бот-HR, бот для звонков и еще буквально несколько маленьких помощников в Телеграм. Полный список ниже:

1) telegram.me/SkillangeBot – незаменимый помощник для отдела HR или владельца бизнеса. Помогает найти человека, обладающего определенными навыками: программисты, дизайнеры, учителя, администраторы и т.д

2) telegram.me/tviggo_bot – вы все еще платите за роуминг? Тогда этот бот для вас. Он научит ваш телеграмм звонить да еще и даст пару интересных фишек: звоните в любую точку мира без платы за роуминг, фиксированная плата в месяц и даже есть возможность звонить между мессенджерами.

3) telegram.me/pravorubot — юридическая помощь в считанные минуты. Бот поможет получить как ответы на вопросы, так и найти специалиста, разбирающегося в юриспруденции.

4) telegram.me/cbr_bot — уследить за курсом валют очень сложно. Этот бот поможет вам быть всегда в курсе цены на нефть, золота и даже биткоина.

5) telegram.me/egrul_bot — выписка из Единого государственного реестра юридических лиц. Просто отправьте боту ИНН или ОГРН. Обратите внимание, что физлиц (и ИП) бот пока проверять не умеет.

6) telegram.me/mnp_bot — позволяет определить мобильного оператора и регион по номеру телефона. Вводить номер нужно без лишних символов и пробелов.

7) telegram.me/transferrobot — в наше время почти никто не пользуется флешками, все используют облачное хранение данных. Этот бот тому подтверждение. Разместите ваш файл размером <20мб или просто добавить бота в группу для загрузки всех файлов в облако.

8) telegram.me/evernoterobot — компания Evernote презентовала своего бота. Просто соедините свой аккаунт в Evernote с Telegram и отправляйте боту сообщения. Все сообщения, которые вы отправите, будут добавлены в заметки.

9) telegram.me/topdf_bot — pdf — универсальный формат для хранения информации. Этот бот поможет перевести ваши текстовые документы и фотографии в формат pdf.

10) telegram.me/multitran_bot — даже в Африке, с этим ботом-переводчиком, вы сможете чувствовать себя спокойно, так как бот содержит в себе несколько словарей.

11) telegram.me/GetStatsBot — не менее важный бот для тех, кто ведет бизнес в интернете. Этот бот всегда в курсе данных из Google Analytics по вашему сайту. Бот покажет количество пользователей, сессий за сегодняшний день, предыдущий или за неделю. Доступно как цифровое отображение, так и в виде графика.

12) telegram.me/expensebot — управлять можно тем, что умеешь измерять. Именно поэтому этот бот, поможет вам управлять и отслеживать ваши ежедневные расходы.

13) telegram.me/orfobot — деловая переписка с ошибками невозможна. Поэтому если сомневаетесь в написанном, лучше дать этому боту текст на проверку. И да, этот бот не только не поставит вам двойку, а еще и исправит все ошибки за вас.

14) telegram.me/do_itBot — все удержать в памяти нереально. На помощь приходит бот Do it, в котором вы сможете создать напоминание, а бот уведомит вас через клиента телеграмм.

15) telegram.me/RubleRobot — не можете принять важное решение? А о методе «подбросить монетку» слышали? Добавляйте этого бота и больше вы не столкнетесь с проблемой принятия решений.

А какими ботами пользуетесь вы? Поделитесь полезными Телеграм-ботами и я добавлю их в список.

habr.com

моя история. Часть первая / Habr


Доброго времени суток, Хабрахабр! Целью статьи является рассказать начинающим программистам о возможности не только разработать что-то интересное на основе несложных инструментов, но и разместить проект в общий доступ, а при усердных стараниях увидеть, что не только автор может оценить потраченные усилия.

Ближе к делу — мой первый пет-проект, решающий проблему доступа к расписанию университета на мобильных устройствах через Telegram, с аудиторией более, чем сто пятьдесят уникальных посетителей в сутки. Несмотря на то, что цифра не является заоблачной, это совсем неплохой результат, выступающий последующей долговременной средой настоящей боевой разработки, учитывая все возможные особенности, и сопровождения продукта длительный период, что бесспорно полезно и ценно для каждого разработчика.

UPD: проект успешно прошел рефакторинг с помощью авторского фреймворка для расписаний занятий университетов — «Rutetider» (статья на Хабрахабре, GitHub). Поэтому некоторые моменты в этой статье могут не совпадать, часть когда может отсутвовать или выглядеть по-другому, но концепция сохранена.

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

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

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

  • pyTelegramBotAPI — основная библиотека для взаимодействия с API Telegram.
  • Heroku Cloud Application Platform — поддержка внушительного ряда языков, большая и легкая (местами заставляющая монитор лить воду) документация с обучающими инструкциями для новичков, наличие удобного интерфейса для полной работы с приложением, включая доступ через консоль, наличие баз данных. Важным фактором является возможность «крутиться» на сервере круглосуточно.
  • Github — работа с развертыванием приложения на сервере проходит с системой контроля версий Git. Приятный бонус — это без времязатрат совмещать со своим репозиторием.
  • PostgreSQL — базы данных SQL, сказать и нечего.

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

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


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

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

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

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

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

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

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

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

7) Вся работа должна быть автоматизирована — встроенный CRON в виде подключаемой библиотеке на сервере — это обычный планировщик, выполняющий задачи в заданный период времени.

  • Ежедневное обновление дат — сразу после полуночи доступны обновленные даты расписания на текущий и завтрашний день.
  • Еженедельное обновления расписания — по определенным ресурсным причинам на сервисе отсутствует возможность обновлять его каждый день, поэтому пользователи могут рассчитывать на обновление пар раз в неделю (с 04.12 расписание обновляется ежедневно).


Здесь я опишу неочевидные инструкции по развертыванию на Heroku Cloud Platform, включая технический код в виде подключения к серверу, внедрения баз данных и оформления приложения. Перед прочтением желательно ознакомиться с обучающими инструкциями для новичков, «пощупать» уже готовый предложенный в нем проект и интерфейс сервиса.

Для общего удобства все переменные и данные содержаться в одном участке кода, но вы так не делайте, делайте конфиги.

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

import telebot
import os
from flask import Flask, request

bot = telebot.TeleBot(токен вашего бота)
server = Flask(__name__)


@bot.message_handler(commands=['start'])
def start(message):
    bot.reply_to(message, 'Hello, ' + message.from_user.first_name)

@bot.message_handler(func=lambda message: True, content_types=['text'])
def echo_message(message):
    bot.reply_to(message, message.text)


@server.route("/токен бота", methods=['POST'])
def getMessage():
    bot.process_new_updates([telebot.types.Update.de_json(request.stream.read().decode("utf-8"))])
    return "!", 200

@server.route("/")
def webhook():
    bot.remove_webhook()
    bot.set_webhook(url="https://ссылка на приложение/токен вашего бота")
    return "!", 200

server.run(host="0.0.0.0", port=os.environ.get('PORT', 5000))

Все необходимое для подключения доступно в Database Credentials на странице баз данных (имя пользователя, пароль, хост, порт и собственно само название базы данных). Все помещается в маленький скрипт с использованием удобных модулей для разработки:
import dj_database_url
import psycopg2

DATABASELINK = "postgres://username:[email protected]:port/database"


db_info = dj_database_url.config(default=DATABASELINK)
connection = psycopg2.connect(database=db_info.get('NAME'),
		    		user=db_info.get('USER'),
		    		password=db_info.get('PASSWORD'),
		    		host=db_info.get('HOST'),
		    		port=db_info.get('PORT'))
cursor = connection.cursor()

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

habr.com

Боты в Телеграм — что это такое и как они работают?

Здравствуйте, дорогие читатели. У меня для вас отличная новость. Наконец-то дошли руки рассказать про то, что такое боты в Телеграм. Расскажу, зачем они нужны, как их искать и как правильно использовать. Как всегда, в описании будет минимум воды и максимум пользы.

Полное руководство по Telegram

Бот — что это такое и что он умеет?

Робот-помощник, готовый выполнить любое рутинное занятие, или специальный программный код, выполняющий определённые команды пользователя.

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

На сегодня роботы Телеграм могут:

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

Другими словами, они, как посредники между человеком и многочисленными веб-службами. Их большой плюс — это общая оболочка, теперь внутри Телеграм находится вся информация, которую люди привыкли искать через Яндекс и Гугл.

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

Далее разберём, как они работают.

Принцип очень прост. Вы находите бота, пишете ему текстовое сообщение (команду) и через доли секунды получаете ответ.

Достоинства:

  • Круглосуточная помощь — по сути, их работу остановит только авария на сервере, что случается крайне редко.
  • Удобство использования — большинство команд находится в меню бота.
  • Ответы приходят за несколько секунд.
  • Для работы используются ресурсы сторонних серверов, так что мощности вашего устройства не задействованы.
  • Безопасность. Многие переживают за сохранность своих данных, так вот можете не беспокоиться. Боты никак не украдут ваши данные. Они их просто не видят. Всё, что им доступно, — это текстовые команды из чата.
  • Установка дополнительных программ не требуется.

Теперь о том, как найти бота в Телеграмме.

Ищем своего бота

Поиск бота ничем не отличается от поиска каналов в Телеграмм. Помните, о них я писал немногим раньше. Основные варианты поиска:

  • Через поисковики найти подборки популярных ботов

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

  • По поиску внутри Телеграм

Для этого нужно знать точное имя и ввести его в строку поиска. Имя обязательно заканчивается на bot и начинается с собачки — @.

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

  • Воспользоваться официальным магазином — storebot.me.
  • Если бота с нужными функциями вы не нашли, то можете создать своего бота.

Как ими пользоваться

Давайте на живых примерах рассмотрим варианты использования.

Общая схема: найти бота — нажать start/начать — ввести команду — получить ответ.

Думаю всем уже понятно, что бот — это программа, которая понимает определённые фразы. Они запускают цепочку действий, итогом является решённая задача.

Скриншоты будут из версии для ПК. В мобильной и онлайн-версиях всё то же самое.

Боты с готовым меню

В большинстве случаев бот имеет собственное меню. Вот пример Яндекс-бота.

Вот ещё вариант меню у бота популярного сайта AdMe.

Скрытое меню

У некоторых роботов меню скрыто. Чтобы его вызвать, необходимо нажать «/» и появится список команд.

Значок слэш является обязательным для ввода команд.

Смотрите пример.

Нужно знать команды

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

Универсальные команды

Их понимает большинство онлайн-помощников:

  • /start — начало,
  • /help — помощь,
  • /settings — настройки.

Иногда боты понимают команды на русском, можно просто догадаться по смыслу. Вот образец — знаменитый робот Антон подрабатывает ещё и в Гидрометцентре. Если его спросить «Погода Воронеж», он тут же сообщит прогноз.

Если бот молчит

Иногда можно найти бота, который не отвечает. Что ни спроси, он будет молчать, как партизан. В чём причина?

  1. Так как они находятся на стороннем ресурсе, значит основная причина в проблемах сервера. Может быть временный сбой или полный отказ сервера.
  2. Также может быть, что неопытный программист ошибся при написании кода.
  3. Третья причина — вы вводите команду, которую бот не понимает. В таком случае, Гугл вам в помощь.

Сам столкнулся с проблемой — не захотел работать популярный @nationalgeographic_bot, он должен показывать лучшие фотки знаменитого журнала. Но, как я ни старался, команду подобрать не смог. Может у вас получится? Сообщите, пожалуйста.

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

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

подпишись и начни получать лучшие статьи первым

Подписаться

Подписалось уже 6999 человек

iklife.ru

Check Also

Как работать на тендерах – Тендеры для новичков: как все устроено?

Содержание с чего начинать в 2019 годуС чего начинать?Кто может быть участником торгов?Решение принято. Что …

Отправить ответ

avatar
  Подписаться  
Уведомление о