Skip to main content

asyncVK is asynchronous library for creating a bot in VK

Project description

asyncVK – асинхронный фреймворк для создания ботов ВК. Преимущества: удобство, скорость выигрываемая за счёт асинхронности.

Бот создаётся за счёт пяти основных структурных единиц:

  1. Bot – это самая главная структурная единица. Это собственно сам бот, который подаёт ивенты обработчикам.
  2. Handler – эта структурная единица отвечает за обработку ивентов.
  3. Dispatcher – эта структурная единица отвечает за взаимодействие с ВК (ответы на сообщения, добавление комментариев). Она автоматически настраивается хандлерами.
  4. Condition (Condition, And, Or) – эта структурная единица отвечает за условия. С помощью неё можно строить сложные условия для хандлеров.
  5. Chain - эта структурная единица позволяет создавать цепочки команд.

Также есть такие второстепенные структурные единицы как:

  1. Keyboard – это второстепенная структурная единица. Она отвечает за создание кнопок в ВК.
  2. Message - структура сообщения для облегчения работы с сообщениями.

Как работать с библиотекой? Легко и интуитивно понятно! Для начала нужно импортировать саму библиотеку и создать бота:

from asyncVK import Handler, Bot, run_polling
from asyncVK.dispatcher import Dispatcher
from asyncVK.condition import Condition, And, Or


TOKEN = "access_token"
GROUP_ID = 182801600

bot = Bot(TOKEN, GROUP_ID)

Теперь мы можем запустить бота на LongPoll API:

if __name__ == "__main__":
    run_polling(bot)

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

@bot.handle
@Handler.on.message_new(Condition(command="привет!"), is_lower=True)
async def handler(dispatcher: Dispatcher): 
    await dispatcher.send_message("Hi!")

В примере выше мы создали обработчик новых сообщений с помощью декоратора @Handler.on.message_new и добавили его в бота с помощью декоратора @bot.handle. Вместо декоратора @bot.handle можно конечно прописать bot.handle(handler)

Как работают хандлеры (обработчики)? Когда мы засовываем экземпляр класса Bot в функцию run_polling, мы как бы активируем метод bot.run_polling. bot.run_polling это бесконечный цикл. В экземпляре класса Bot есть список всех хандлеров, которые мы создали и добавили в него. И когда приходит какой-то ивент (событие), то этот цикл пересылает это событие всем хандлерам. И потом если условие истинно, то активируется функция, из которой мы сделали хандлер.

Что делает эта асинхронная функция? Она на сообщение "привет!" (в любом регистре) будет отвечать в тот же чат сообщением "Hi!". Как сделать чтобы она ответила не в тот же чат, а в ЛС? Легко! Заменить

await dispatcher.send_message("Hi!")

на

await dispatcher.answer("Hi!")

Как строить условия? Какие условия можно построить? Строить условия легко, и можно построить абсолютно любые условия! Например мы хотим, чтобы бот отвечал на привет. В таком случае мы пишем:

Condition(command="привет")

Или мы хотим, чтобы бот отвечал на привет или если peer_id равен 2000000001. В таком случае мы пишем:

Condition(command="привет", peer_id=2000000001)

То есть прописывая дополнительные условия в Condition, мы как бы делаем if command == "привет" or peer_id == 2000000001. Также можно аналогично прописать вот так:

Or(Condition(command="привет"), Condition(peer_id=2000000001))

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

А что если мы хотим, чтобы бот отвечал либо если ему написали "привет", либо если в сообщении есть строка "а" и написал это пользователь с id 386746383. Тоже легко! Вот так:

Or(
    Condition(command="привет"),
    And(Condition(contains_command="a"), Condition(user_id=386746383))
)

И так, давайте разбирать как же строить так любые запросы. Если мы пропишем несколько аргументов в Condition, то это будет ИЛИ (or). Если же мы засунем несколько условий в And, то тут условие будет истинным если все условия в And истинны, то есть это И (and). Если же мы засунем несколько условий в Or, то условие будет истинным если истинно хотя бы одно условие в нём, то есть это ИЛИ (or). В And и Or можно засовывать как и Condition, так и другие And и Or. Стоит также заметить, что хандлер без условия будет срабатывать всегда, когда активируется нужное событие (то есть вне зависимости от условий, наверное потому что их нет) - @Handler.on.message_new()

Вот все аргументы Condition:

command – проверяет на равенство текста (если сообщение, то текста сообщения и т.д.) с этим аргументом.
contains_command – проверяет на то, есть ли строка contains_command в тексте.
user_id – проверяет на равенство id пользователя, инициировавшего событие, и этим аргументом.
peer_id – проверяет на равенство id чата с этим аргументом.
post_id – проверяет на равенство id записи на стене/id записи в обсуждениях с этим аргументом.
owner_id – проверяет на равенство id сообщества, где произошло событие (если событие было в сообществе).

Вот весь список хандлеров:

Handler.on.message_new – новое сообщение.
Handler.on.message_edit – редактирование сообщения.
Handler.on.wall_reply_new – новые комментарий на стене.
Handler.on.wall_reply_edit – редактирование комментария на стене.
Handler.on.wall_post_new – новый пост на стене.
Handler.on.board_post_new – новый комментарий в обсуждениях.
Handler.on.board_post_edit – редактирование комментария в обсуждениях.

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

Возможности диспетчера:

dispatcher.answer – ответить в ЛС. Можно активировать при любом событии, отправит сообщение инициатору события. Список аргументов: 
	text – текст сообщения.
	attachment – вложение сообщения (в виде части ссылки такого рода: 
	    от ссылки https://vk.com/id386746383?z=photo386746383_457256628%2Falbum386746383_0 
	    берём только photo386746383_457256628 и передаём это в качестве аргумента). 
	keyboard – кнопки ВК.
	Возвращает структуру Message вашего сообщения.

dispatcher.reply - ответить на сообщение пользователя. Список аргументов и возвращаемое значение идентичны с answer.
dispatcher.send_message – ответить в том же чате. Список аргументов и возвращаемое значение идентичны с answer.
dispatcher.send_comment – ответить в комментариях. Список аргументов идентичен с answer, но аргумент keyboard отсутствует.
dispatcher.mark_as_read – пометить сообщение как "прочитанное". Никаких аргументов не принимает.
dispatcher.set_typing_status – установить статус на набор текста / запись голосового сообщения. Принимает один аргумент: 
    	typing_status. Его значение по умолчанию "typing" (набор текста). Можно изменить на "audiomessage" – запись голосового сообщения.
dispatcher.kick_user - удаляет участника из беседы
    	member_id - id участника беседы (id сообщества пишется со знаком -)
dispatcher.edit_chat_name - изменяет название беседы
    	title - новое название беседы

Пример использования структуры Message:

@bot.handle
@Handler.on.message_new(Condition(command="прив"))
async def handler(dispatcher: Dispatcher):
    message = await dispatcher.reply("Это сообщение исчезнет через 3 секунды, а твоё сообщение будет закреплено")
    await asyncio.sleep(1)
    await message.edit("Это сообщение исчезнет через 2 секунды, а твоё сообщение будет закреплено")
    await asyncio.sleep(1)
    await message.edit("Это сообщение исчезнет через 1 секунду, а твоё сообщение будет закреплено")
    await asyncio.sleep(1)
    await message.delete()
    await dispatcher.message.pin()

Можно строить любые запросы, даже если этого не предполагает отсутствие метода в диспетчере:

@bot.handle
@Handler.on.message_new(Condition(contains_command="прив"), is_lower=True)
async def handler(dispatcher: Dispatcher):
    result = await bot.execute("messages.send", peer_id=dispatcher.peer_id, 
                               message="okay", random_id=0)
    print(result)

В этом примере мы на новое сообщение, содержащее "прив" отвечаем "okay" нашим построенным запросом. peer_id же берём из диспетчера.

Параметры диспетчера:

dispatcher.token - ваш токен
dispatcher.user_id - id инициировавшего событие пользователя
dispatcher.peer_id - id чата (если это ЛС, то peer_id равен user_id)
dispatcher.post_id - id записи на стене или обсуждения (если событие это новая запись на стене, новый комментарий на стене или в обсуждении)
dispatcher.owner_id - если событие было внутри группы, то owner_id это id группы
dispatcher.object_id - id объекта события
dispatcher.event - объект события
dispatcher.text - если к примеру событие это новое сообщение, то text это текст сообщения, если это к примеру новый комментарий, то text это текст комментария и т.д.
dispatcher.reply_text - текст отвеченного сообщения (если таковое имеется)
dispatcher.reply_user_id - id пользователя написавшего отмеченное сообщение (если таковое имеется)
dispatcher.reply_peer_id - id чата отмеченного сообщения (если таковое имеется)
dispatcher.reply_object_id - id объекта ответа
dispatcher.action_type - тип действия
dispatcher.action_text - текст действия
dispatcher.action_object_id - id объекта действия
dispatcher.action_member_id - id пользователя, инициировавшего действие
dispatcher.payload - payload
dispatcher.message - выдаёт структуру Message для сообщения из события
dispatcher.reply_message - выдаёт структуру Message для отвеченного сообщения из события

Возможность структуры Message:

message.edit - изменить сообщение
		text - новый текст
	attachment - вложение
	keyboard - клавиатура
message.pin - закрепить сообщение
message.delete - удалить сообщение

Если вы хотите выполнить сразу несколько запросов асинхронно, то можно просто воспользовать библиотекой asyncio. К примеру:

@bot.handle
@Handler.on.message_new(Condition(contains_command="прив"), is_lower=True)
async def handler(dispatcher: Dispatcher):
    tasks = [asyncio.create_task(dispatcher.mark_as_read()),
             asyncio.create_task(dispatcher.set_typing_status()),
             asyncio.create_task(asyncio.sleep(9))]

    await asyncio.gather(*tasks)
    await dispatcher.send_message("okay")


@bot.handle
@Handler.on.message_new(Condition(contains_command="а"), is_lower=True)
async def handler(dispatcher: Dispatcher):
    await dispatcher.send_message("Б!")

Хандлер, обрабатывающий сообщение, где есть строка "прив" сперва пометит сообщение как прочитанное, потом установит статус "печатает…" и через 9 секунд отправит сообщение "okay" и всё это асинхронно.

P.S. хандлеры друг друга не блокируют, так что во время работы первого хандлера вы можете написать "а" и бот ответит "Б!", несмотря на работу первого хандлера.

По-мимо этого можно делать хандлеры не для условий, а для всего события целиком. Например:

@bot.handle
@Handler.on("message_new")
async def handler(dispatcher: Dispatcher):
    if dispatcher.text.lower() == "abs":
        await dispatcher.send_message("peer")
    elif dispatcher.text.lower() == "help me":
        await dispatcher.send_message("no")

Этот хандлер будет обрабатывать все события типа message_new. В данном случае он на "abs" будет отвечать "peer", а на "help me" будет отвечать "no". И также регистр сообщения не важен, ибо мы применили метод lower.

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

@bot.handle
@Handler.on("wall_reply_new")
async def handler(dispatcher: Dispatcher):
    if dispatcher.text.lower() == "nice":
        await dispatcher.send_comment("ok")
    elif dispatcher.text.lower() == "not bad":
        await dispatcher.send_comment("no, very bad!")

Какой обработчик использовать? Для условий или для всего события целиком? Если вам нужно сделать обработчик для простых команд (ответить на то этим и что-то в этом роде), то лучше все эти команды прописать в обработчике события, в данном случае это будет @Handler.on("message_new") ведь нам нужно отвечать на сообщения. А если же команды сложные, а не простые ответы с какими-то дополнительными действиями, то лучше их прописать в обработчике условия. К примеру нам нужно, чтобы при сообщении "статистика" бот получил статистику откуда-то, рассортировал и отфильтровал её и потом отправил. Такое лучше прописывать в обработчике условия, в данном случае @Handler.on.message_new(Condition(command="статистика")) Но нужно смотреть на код в целом, ибо иногда может пригодится сделать исключение и написать сложную команду в обработчик события, а простую в обработчик условия. То есть выбор должен зависеть от ситуации и структуры вашего кода.

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

from asyncVK.asyncDB import SQLite
db = SQLite("data.db")
bot = Bot(TOKEN, GROUP_ID)



async def create_db():
    async with db:
        await db.execute("""
            CREATE TABLE IF NOT EXISTS profile (
                user_id INTEGER,
                money INTEGER
            )
        """)


@bot.handle
@Handler.on("message_new")
async def handler(dispatcher: Dispatcher):
    if dispatcher.text.lower() == "create db" and dispatcher.user_id == OWNER_ID:
        await create_db()
        await dispatcher.send_message("db was created!")

    elif dispatcher.text.lower() == "register":
        async with db:
            await db.execute("""
                INSERT INTO profile 
                VALUES (?, 0)
            """, (dispatcher.user_id,))

        await dispatcher.send_message("you are was registered!")


@bot.handle
@Handler.on.message_new(Condition(command="click"), is_lower=True)
async def handler(dispatcher: Dispatcher):
    async with db:
        await db.execute("""
            UPDATE profile
            SET money=money+1
            WHERE user_id=(?)
        """, (dispatcher.user_id,))

        state = await db.execute("""
            SELECT money
            FROM profile
            WHERE user_id=(?)
        """, (dispatcher.user_id,))

    money = state[0][0]
    await dispatcher.send_message(f"Money: {money}")

OWNER_ID это константа, которая должна ваш ID в ВК, это условие запрещает создавать база-данных кому-либо кроме вас командой. Что делает async with db?. async with db ждёт пока база-данных откроется для запросов, потом закрывает базу-данных для запросов и как все ваши запросы прошли к базе, она опять открывает базу-данных для запросов. Метод db.execute отправляет ваш запрос к базе-данных.

Также можно использовать глобальные переменные с синхронизацией. Вот пример:

from asyncVK.asyncDB import Variable


total_money = Variable(0)


@bot.handle
@Handler.on.message_new(Condition(command="/click"), is_lower=True)
async def handler(dispatcher: Dispatcher):
    async with total_money:
        total_money.object += 1
        await dispatcher.send_message("Все деньги мира: " + str(total_money.object))

В фреймворке также присутствует встроенный функционал создания цепочек. Что такое цепочки? Это когда команда состоит из нескольких частей. То есть, к примеру, регистрация. Вы пишите /регистрация и бот далее просит вас ввести имя. И вы вводите своё имя и регистрация пройдена в 2 сообщения, то есть 2 части.

from asyncVK.chain import Chain

Таким образом мы импортируем класс цепочек. Как создать цепочку? Всё просто. Вместо bot.handle используем chain.add_handler, а потом bot.add_chain(chain).

К примеру:

chain = Chain()


@chain.add_handler
@Handler.on.message_new(Condition(contains_command="прив"), is_lower=True)
async def handler_1(dispatcher: Dispatcher):
    await dispatcher.send_message("Напиши что-то")


@chain.add_handler
@Handler.on.message_new(Condition(contains_command="что-то"), is_lower=True)
async def handler_2(dispatcher: Dispatcher):
    await dispatcher.send_message("Пон")
    
    
bot.add_chain(chain)

Или:

chain = Chain()


@Handler.on.message_new(Condition(contains_command="прив"), is_lower=True)
async def handler_1(dispatcher: Dispatcher):
    await dispatcher.send_message("Напиши что-то")


@Handler.on.message_new(Condition(contains_command="что-то"), is_lower=True)
async def handler_2(dispatcher: Dispatcher):
    await dispatcher.send_message("Пон")
    
    
if __name__ == "__main__":
    chain = Chain()
    chain.add_handler(handler_1)
    chain.add_handler(handler_2)
    bot.add_chain(chain)

    run_polling(bot)

Также можно пробрасывать какие-то данные по цепочке:

chain = Chain()


@Handler.on.message_new(Condition(contains_command="прив"), is_lower=True)
async def handler_1(dispatcher: Dispatcher):
    await dispatcher.send_message("Напиши что-то")
    return 12


@Handler.on.message_new(Condition(contains_command="что-то"), is_lower=True)
async def handler_2(dispatcher: Dispatcher):
    await dispatcher.send_message(f"Пон {dispatcher.chain_data}")
    
    
if __name__ == "__main__":
    chain = Chain()
    chain.add_handler(handler_1)
    chain.add_handler(handler_2)
    bot.add_chain(chain)

    run_polling(bot)

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

Также вместо произвольных данных в цепочке можно возвращать команды. В данном случае Reject - это полностью сбросить цепочку и Reset - текущий хандлер сработает ещё раз. При Reset ваш chain_data не сбрасывается.

К примеру

from asyncVK.chain import Chain, Reset


chain = Chain()


@Handler.on.message_new(Condition(contains_command="прив"), is_lower=True)
async def handler_1(dispatcher: Dispatcher):
    await dispatcher.send_message("Напиши что-то")


@Handler.on.message_new(is_lower=True)
async def handler_2(dispatcher: Dispatcher):
    if dispatcher.text != "что-то":
        await dispatcher.send_message("Я жду от тебя что-то")
	return Reset()
    
    await dispatcher.send_message("Пон")
    
    
if __name__ == "__main__":
    chain = Chain()
    chain.add_handler(handler_1)
    chain.add_handler(handler_2)
    bot.add_chain(chain)

    run_polling(bot)

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

Пример использования структуры Message и ActionCondition, бот примет закреплённое сообщение и будет его удерживать в закрепе. ActionCondition это условие на действие.

Возможные значения ActionCondition:

chat_pin_message - закрепление сообщения
chat_unpin_message - открепление сообщения
chat_title_update - обновление названия беседы
chat_photo_update - обновление аватарки беседы
	chat_photo_remove - удаление аватарки беседы
 	chat_kick_user - удаление пользователя из чата, пользователь вышел из чата
  	chat_invite_user - добавление пользователя в чат
pinned_message = {"object": None}


@bot.handle
@Handler.on.message_new(Condition(contains_command="удержи"))
async def handler(dispatcher: Dispatcher):
    pinned_message["object"] = dispatcher.reply_message
    await pinned_message["object"].pin()
    await dispatcher.send_message("ok")


@bot.handle
@Handler.on.message_new(Or(ActionCondition(action="chat_pin_message"),
                           ActionCondition(action="chat_unpin_message")))
async def handler(dispatcher: Dispatcher):
    await dispatcher.send_message("Не, не выйдет")
    await pinned_message["object"].pin()

Методы диспетчера send_message, answer, reply возвращают структуру Message

Пример создания кнопок, а также использования пайлоада через functional_condition

def check_payload(event_params: dict) -> bool:
    payload = event_params["payload"]
    if payload.get("answer") == "yes":
        return True
    return False


@bot.handle
@Handler.on.message_new(Condition(contains_command="дай"))
async def handler(dispatcher: Dispatcher):
    keyboard = Keyboard(
        Line(
            Button("да", "positive", {"answer": "yes"})
        ),
        Line(
            Button("нет", "negative", {"answer": "no"}),
            Button("возможно", "default", {"answer": "maybe"})
        ), one_time=True, inline=True
    )

    await dispatcher.send_message("Не дам", keyboard=keyboard)


@bot.handle
@Handler.on.message_new(Condition(functional_condition=check_payload))
async def handler(dispatcher: Dispatcher):
    await dispatcher.send_message("Ок")
    await dispatcher.send_message(f"Ваш пайлоад: {dispatcher.payload}")

Также существует PayloadCondition, который в отличие от простого Condition принимает все аргументы за И, а не ИЛИ по-умолчанию.

@bot.handle
@Handler.on.message_new(Condition(contains_command="дай"))
async def handler(dispatcher: Dispatcher):
    keyboard = Keyboard(
        Line(
            Button("да", "positive", {"answer": "yes", "module": "vk"})
        ),
        Line(
            Button("нет", "negative", {"answer": "no"}),
            Button("возможно", "default", {"answer": "yes"})
        ), one_time=True, inline=True
    )

    await dispatcher.send_message("Не дам", keyboard=keyboard)


@bot.handle
@Handler.on.message_new(PayloadCondition(answer="yes", module="vk"))
async def handler(dispatcher: Dispatcher):
    await dispatcher.send_message("Ок")
    await dispatcher.send_message(f"Ваш пайлоад: {dispatcher.payload}")

В библиотеке есть возможность создавать шаблоны сообщений для дальнейшей их рассылки

@bot.handle
@Handler.on.message_new(Condition(contains_command="разошли им"), is_lower=True)
async def handler(dispatcher: Dispatcher):
    await dispatcher.reply("Окей")

    message = dispatcher.create_message_template("Вам послание, месье", forward=dispatcher.forward)
    await message.send_to("476393332,584575899")

В этом коде бот перешлёт сообщение двум пользователям с соответствующими id. Можно отправлять сообщения по-одиночному, для этого замените на await message.send_to(476393332)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

async_vksher-2.2.2.tar.gz (42.6 kB view details)

Uploaded Source

Built Distribution

async_VKsher-2.2.2-py3-none-any.whl (29.7 kB view details)

Uploaded Python 3

File details

Details for the file async_vksher-2.2.2.tar.gz.

File metadata

  • Download URL: async_vksher-2.2.2.tar.gz
  • Upload date:
  • Size: 42.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.1.1 CPython/3.12.4

File hashes

Hashes for async_vksher-2.2.2.tar.gz
Algorithm Hash digest
SHA256 a165e293901442d859b33e83bc9ff73448cc74e5dafc401b65adf1d9c14e2734
MD5 271a1eff148a05f75585ce3854a250d5
BLAKE2b-256 a13f3d3eb794080cd65ae6bc644f4023f87ab33a4bcd294e5345f0cd1e5ea235

See more details on using hashes here.

File details

Details for the file async_VKsher-2.2.2-py3-none-any.whl.

File metadata

  • Download URL: async_VKsher-2.2.2-py3-none-any.whl
  • Upload date:
  • Size: 29.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.1.1 CPython/3.12.4

File hashes

Hashes for async_VKsher-2.2.2-py3-none-any.whl
Algorithm Hash digest
SHA256 669ad08785dcd19963135d733acf9622f7b715aa437945657fad01c0d7c8a36d
MD5 1469acd88a0731e8b02433f5812a46e4
BLAKE2b-256 6dfd87e808416ba651a94fe3a9e54d66cafe2ac4d736e11c3a563cc3a8923e59

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page