ekonda / kutana Goto Github PK
View Code? Open in Web Editor NEWThe library for developing systems for messengers and social networks
License: MIT License
The library for developing systems for messengers and social networks
License: MIT License
on_commands ignores prefix when bot mentioned in group chats, but not in private messages
on_commands ignores prefix when bot mentioned in private messages
only in group chats on_commands ignores prefix when bot mentioned
Bot users try to use mention as command prefix in private messages as they use it in group chats
Довольно часто требуется запускать постоянные coroutines эное-кол-во раз в несколько секунд. Текущий подход всегда выглядел так, несомненно работает нет проблем. Но как бы это удобно выглядело с декоратором?
from kutana import Plugin
import asyncio
plugin = Plugin("TestPlugin")
@plugin.on_start()
async def _():
asyncio.create_task(egg_printer())
# Prints egg every 5 seconds
async def egg_printer():
while True:
print("Its Egg!")
await asyncio.sleep(5)
Вот как бы это выглядело с помощью декоратора, красиво а самое главное удобно. И мы избавляемся от лишних импортов и лишних блоков кода.
from kutana import Plugin
plugin = Plugin("TestPlugin")
# Prints egg every 5 seconds
@plugin.schedule(5)
async def egg_printer():
print("Its Egg!")
Сейчас он обходит все *.py
файлы, но хочется, чтобы он умел адекватно импортировать модули. При этом просто лежащие рядом файлы-модули всё-равно надо оставить импортируемыми. Т.е. поиграть с определением папок как модулей и т.д.
Special decorator for handling callback buttons events
(VK Docs)
Something like:
@vk.on_callback(payload=...)
—
Make similar to @vk.on_payloads(...) but with callback buttons
It's difficult to use callback buttons in current kutana api's
Commands like /command@BotUsername don't work as expected.
Example code:
@plugin.on_commands(commands=["command"])
async def _(msg, ctx):
await ctx.reply("Works!!!")
@plugin.on_any_unprocessed_message()
async def _(msg, ctx):
await ctx.reply("Doesn't work :-(")
/command => Works!!!
/command@BotUsername => Works!!!
/command => Works!!!
/command@BotUsername => Doesn't work :-(
Strip anything after @ in commands.
This feature is quite useful if there're multiple bots in one chat.
Я беру во внимание Red (Discord bot). В нем используется более гибкая система команд, которая позволяет очень легко и просто создавать подкоманды (subcommands) в плагинах с помощью декораторов. Также было бы удобно передавать аргументы, которые пользователь передает вместе с командой в *args
.
Просто посмотрите, как легко и просто в Red создаются команды:
@commands.group(alias=["nf"])
async def nickforcer(self, ctx:commands.Context):
"""Nickname Forcer related commands"""
pass
@nickforcer.command()
@commands.admin()
async def set(self, ctx: commands.Context, user: discord.Member, nick: commands.UserInputOptional[str]):
"""Sets the user to Nickname Forcer"""
await self.config.user(user).nickname.set(nick)
current_users = await self.config.guild(ctx.guild).users_to_force()
if user.id not in current_users:
current_users.append(user.id)
await self.config.guild(ctx.guild).users_to_force.set(current_users)
await ctx.send(_("This user now will be forced with this nickname: **{nickname}**").format(nickname=nick))
try:
await user.edit(nick=nick)
except discord.errors.Forbidden:
await ctx.send(_("So I've tried to do this, but i have no permission to change nicknames of other users, "
"add it to me, please."))
@nickforcer.command()
@commands.admin()
async def unset(self, ctx: commands.context, user: discord.Member):
"""Unsets the user to Nickname Forcer"""
current_users = await self.config.guild(ctx.guild).users_to_force()
if user.id not in current_users:
await ctx.send(_("Nothing to unset..."))
else:
current_users.remove(user.id)
await ctx.send(_("User {mention} has been removed from forcing!").format(mention=user.mention))
Как вы можете заметить, самая первая функция - назовём её "мастер-командой", с которой начинается вся магия. Использовав ее как декоратор, вторая функция становится после нее и получается так, что чтобы вызвать эту команду нужно набрать /nickforcer set [аргументы]
. Ведь просто же? И что самое главное логично - вторая команда как-бы наследует мастер-команду.
Также обратите внимание, что у функций после ctx
объявлены еще несколько параметров. Red передает в них, аргументы, который спарсил из полученной команды. Разработчику плагинов при этом больше не нужно заниматься парсингом body
, достаточно доверить все фреймворку, просто объявив аргументы разного типа. Также, если им задать значение по умолчанию None
, можно сделать их необязательными. Было бы здорово позаимствовать такой мощный парсер команд у Red и добавить в Kutana
, не так ли?
И ещё один ключевой фактор, который здесь присутствует - это docstring
. У каждой команды есть свой комментарий, который находится там не просто так. Это справка по команде, которую использует встроенный плагин help
в Red. Также, эти комментарии, как и все строки внутри функции перевода _("string")
замечательно извлекается с помощью форка gettext от Red, который называется redgettext
(есть в PyPI). Хотелось бы еще, чтобы Kutana
отошла от использования yml
в i18n
и использовала специальные форматы, такие как po
и mo
. Все системы перевода заточены именно под эти форматы файлов.
На данный момент Kutana очень скудно обрабатывает команды. Да, конечно сейчас можно сделать подкоманды вот так:
@plugin.on_commands(['settings set'])
async def __(message, context):
...
Но делать их с помощью декораторов гораздо приятнее и красивее. Ну и, кстати говоря, Kutana
не умеет парсить аргументы, приходится самостоятельно парсить их из body
, что не очень удобно и выглядит несколько кустарно.
Также сейчас Kutana
использует yml
для i18n
, что неудобно для перевода плагинов, так как все системы перевода используют форматы файлов po
и mo
.
Red - открытый фреймворк для построения ботов для Discord. Вооружившись его открытым исходным кодом, из него можно позаимствовать различные идеи и наработки, с помощью которых можно сделать Kutana
лучшим фреймворком для создания мультиплатформенных ботов.
Контекст не требуется. Всё было сказано выше. Если у вас есть какие-то вопросы, прошу связаться со мной:
ВКонтакте, Telegram
app.run(webhook=True)
or
Vkontakte(webhook=True)
Webhooks save a lot of resources, and it would also be possible to deploy bots to services like Heroku.
We could make an aiohttp server to serve webhooks.
I can implement it, but I need your approval and any suggestions on how it could be implemented better.
Both Telegram and Vkontakte, as well as most popular messengers, support WebHooks.
I guess it could be easily done with aiohttp.
I'm okay with both English and Russian.
Now when bot only has access to messages with mentions (example: "@bot_name") it's also necessary to use prefix. In this case to execute command user should write something like:
"@bot_name echo" should work
"@bot_name, echo" should work too (mobile clients add a comma by default)
"@bot_name echo" and "@bot_name, echo" don't works (only "@bot_name / echo" or "@bot_name /echo")
—
Users try to execute the bot command with a mention, but they do not expect that you also need to specify the prefix
Добавить базу данных, не файловую, желательно peewee
Надо сделать так, чтобы Environment могли использовать для передачи информации между плагинами. Как в первых версиях.
Когда разработчики разрабатывают плагины, они хотят видеть больше возможностей для разграничения доступа для различных пользователей. К примеру, разработчик пишет плагин для модерации мультичата (беседы). Ему необходимо ограничить доступ к командам, например к команде, которая банит участника. Было бы здорово не писать условие внутри функции, а просто импортировать готовый декоратор для нее. И декоратор, который проверяет, является ли отправитель (sender) администратором мультичата, этакий @plugin.is_chat_admin()
.
Хотелось бы видеть следующие декораторы:
@is_solo()
- является ли чат диалогом@is_multi()
- является ли чат мультичатом (беседой)@is_owner()
- является ли отправитель создателем бота@is_chat_owner()
- является ли отправитель создателем чата (да, если диалог)@is_chat_admin()
- является ли отправитель администратором чата (да, если диалог)@is_backend(backend)
- проверка бэкенда, через который пришло сообщениеВозможно удастся придумать их больше, но я столкнулся с необходимость иметь хотя-бы эти.
Приведенных выше декораторов не существует в Kutana
.
Нужно - всего-то - ничего, просто добавить эти декораторы в kutana.Plugin
.
Эти декораторы мне очень сильно понадобились, когда я писал своего бота. Пришлось писать их с нуля, потому что в моем боте есть очень много плагинов с разграничением доступа.
Возник такой вопрос, каким образом можно реализовать проверку - когда хотя бы один из декораторов on_commands
во всех плагинах стригерился. Но, при этом проверять это в декораторе у которого приоритет выше всех и срабатывает он в первую очередь, в нашем случае on_messages(priority=10)
. Подскажите правильный ход реализации данной конструкции. Спасибо :3
Происходит замена метода в окружении 1, из которого получается окружение 2, которое используется другими плагинами. Замена метода не переносится.
I'd not found any solutions to change bot prefix. Is it possible?
2024-01-04 19:50:54,281 [ ERROR ] Task exception was never retrieved
future: <Task finished name='Task-4' coro=<Telegram.acquire_updates() done, defined at /root/kutana/kutana/backends/telegram.py:215> exception=KeyError('file_name')>
Traceback (most recent call last):
File "/root/kutana/kutana/backends/telegram.py", line 229, in acquire_updates
await queue.put((self._make_update(raw_update), self))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/kutana/kutana/backends/telegram.py", line 198, in _make_update
attachments.append(self._make_attachment(raw_update["message"][kind], kind))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/kutana/kutana/backends/telegram.py", line 147, in _make_attachment
title=raw_attachment["file_name"],
~~~~~~~~~~~~~~^^^^^^^^^^^^^
KeyError: 'file_name'
Hi, I tried to intercept all messages that start with the bot prefix but the function stopped responding if the command is used somewhere, how can I get around this?
a
App need to start.
App shooting down with NotImplementedError in add_signal_handler (file > kutana.py > line(203) > (add_signal_handler)
Make the correct exception handler in the event loop.
На сколько я понимаю, сейчас принцип работы ядра при выборе плагина примерно следующий - проходится по всем плагинам, пока не найдёт плагин с нужной командой, если нашёл - выполнил, если не нашёл - не выполнил ничего.
Как насчёт, такого варианта работы:
LIKE/ILIKE
всяких), ну а дальше действовать, как и сейчас - вначале вызывать те плагины, которые помечены для предварительного вызова, а потом уже тот, для которого пришла команда.Я так понимаю, для этого потребуется немного переработать ядро.
Столкнулся с проблемой, когда хотел для плагинов добавить просто свои декораторы, оказалось что перед тем как добраться до нужного плагина, вызываются все мои декораторы, которые находятся у предыдущих плагинов.
И, на сколько я знаю, подобный принцип регистрации плагинов в ядре присутствует у многих систем(в которых реализована модульность), к примеру Odoo.
P.S. Если в реализации потребуется помощь - готов заняться этим.
Спасибо.
У телеграма существует лимит в 30 сообщений в секунду, но это действует на личные сообщения пользователям, так же есть и другой лимит, 20 сообщений в минуту в групповых чатах, и на каждый чат свой лимит, и бот довольно часто может упираться в него и в итоге сообщения попросту теряются. Было бы не плохо добавить и для него обход лимитов, в виде какой-нибудь очереди, и в случае превышения кол-ва в 20 штук за минуту пихать их туда а потом уже высчитать и отправить по новой когда таймер спадёт.
Как сделать несколько команд в одном плагине?
К примеру, чтоб в одном плагине были команды - help/помощь/? и т.п.
Упразднить и удалить части библиотеки, связанные с i18n. Реализация получается слишком специфичной, и поддержка этой части будет требовать много ресурсов. Простои использование gettext
пользователями (и нами) должно закрыть проблему более адекватной и поддерживаемой не нами реализацией.
Предупреждение: это не Feature Request! Можете считать это предложением по созданию платформы/фреймворка. Если это произойдет, я обязательно впишусь.
Обдумав ответы на 3 моих предыдущих issue, я пришел к очень интересному умозаключению, которое может положить начало новому проекту. Давайте по порядку.
Проект Kutana действительно может остаться собой, не потеряв при этом ничего. Однако, его всё еще мало для того, что хочет видеть народ. Я предлагаю начать разработку мощного фреймворка для построения self-hosted ботов. Давайте пока что, как эскиз, назовём его Katuna, в противовес Kutana.
Упростить разработку ботов, конечно же! И дать разработчикам обширный перечень инструментов для этого. В моей голове я это вижу, как аналог Red Discord Bot, только гораздо мощнее.
В плане архитектуры я вижу это, как реализацию Kutana. То есть, полу-готовый бот, который нужно лишь настроить и из коробки он будет обладать некоторым базовым функционалом, который я опишу в отдельном пункте. Но это будет не просто бот, как я уже говорил, он даст набор инструментов, не доступный в Kutana из коробки и более конкретную архитектуру плагинов для их стандартизации.
В качестве базового функционала я рассматриваю пока два краеугольных базовых плагина:
help
- как понятно из названия, встроенный плагин справки. Этот плагин будет автоматически собирать из других загруженных плагинов справочную информацию, чтобы отдать её пользователю бота. Разработчикам сторонних плагинов нужно будет лишь рассказать, как оставить эту справочную информацию в командах их плагинов.
plugin
- этот плагин и его команды будут отвечать за получение/обновление плагинов из репозитория. В качестве репозитория, Red (если грубо говоря) использует github-репозиторий, в котором хранится файл со ссылками на сторонние github-репозитории с плагинами. Сторонние разработчики смогут добавлять свои репозитории в этот файл с помощью пулл реквестов. Удобно, не правда ли? Для установки или обновления, этому плагину нужно будет просто использовать утилиту git и просто клонировать или обновлять локальные репозитории. Вкратце, так устроена дистрибуция плагинов в Red.
Прежде всего мы получим платформу для создания ботов. Прекрасный инструмент, который позволит создавать ботов и вовсе без навыков программирования. Буквально, ввел пару-тройку команд, настроил, запустил и мгновение спустя отдаешь боту команды на установку плагинов на свое усмотрение. Грубо говоря, собираешь своего бота, как конструктор. Могу привести аналогию: словно ты установил игру и не зная ни одного языка программирования, устанавливаешь в него различные моды.
Если вам интересна эта идея, я обязательно в нее впишусь. Маякните мне, я дам данные для связи со мной.
Почему это нужно? Опираясь на опыт Red (Discord bot), разделение плагинов над поддиректории поможет избежать путаницы, удобно дистрибутировать и распространять плагины, как это сделано в вышеприведенном фреймворке. Также в последствии можно будет создать репозиторий с плагинами.
Я предлагаю сделать структуру плагинов такой: внутри папки plugins/
будут находиться подпапки, названные как плагины. Внутри каждой папки будет находится __init__.py
, который будет создавать плагин, подпапка i18n
с переводами для каждого конкретного плагина. Структура должна получится примерно такой:
Плагины расположены в виде файлов внутри папки plugins/
, инкапсулированы на уровне файлов. Переводы слиты в один файл, что неудобно переводчикам и разработчикам.
Нужно просто немного изменить несколько функций внутри класса "Kutana", изменить то, как загружает переводы модуль i18n
- и начало будет положено.
В моей реализации бота я решил инкапсулировать плагины в поддиректории самостоятельно. Для это написал небольшую функцию:
def load_plugins_from_dir(bot: Kutana, directory="plugins") -> None:
"""Loads all plugins from subdirectories in current working directory's subdirectory using reflection"""
path = Path.cwd() / directory
if not path.exists():
raise FileNotFoundError("Directory doesn't exist!")
if not path.is_dir():
raise NotADirectoryError("Path is not a directory!")
for subdir in path.glob("*"):
if subdir.is_dir():
# from plugins.ping import plugins
module = __import__(f"{directory}.{subdir.name}")
plugin = getattr(module, subdir.name)
plugin = getattr(plugin, "plugin")
if isinstance(plugin, kutana.Plugin):
bot.add_plugin(plugin)
Библиотека httpx
обладает более дружелюбным интерфейсом. Одно из критичных мест – загрузка документов в телеграмм. httpx
лучше работает с мультипартами.
Добавьте Discord backend, хотя бы в экспериментальном режиме.
When whe try upload image to group conversation we always get this error:
{'method': 'photos.saveMessagesPhoto', 'error_code': 1, 'error_msg': 'Unknown error occurred'})
Standart conversations loading success, just in group have a error always
Для воспроивзедения потребуется создать 2 плагина, отдельным файлом друг от друга.
plugin_first.py:
import json
from kutana import Plugin
plugin = Plugin("kb2")
ac_name = "accept"
dc_name = "decline"
@plugin.vk.on_payloads([ac_name, dc_name])
@plugin.on_commands(["kb2"])
async def __(msg, ctx):
if hasattr(ctx, "payload"):
return await ctx.reply("TRIGGERED")
await ctx.reply("hey", keyboard=json.dumps({
"inline": True,
"buttons": [
[
{
"action": {"type": "text", "payload": f"\"{ac_name}\"", "label": "✅ Принять"},
"color": "positive",
},
{
"action": {"type": "text", "payload": f"\"{dc_name}\"", "label": "❌ Отказать"},
"color": "negative",
},
],
],
}))
plugin_second.py:
import json
from kutana import Plugin
plugin = Plugin("kb")
@plugin.vk.on_payloads([{"kind": "1"}])
async def __(msg, ctx):
await ctx.reply(f"Handler for kind (1), your meta is '{ctx.payload['meta']}'")
@plugin.vk.on_payloads([{"kind": "2"}])
async def __(msg, ctx):
await ctx.reply(f"Handler for kind (2), your meta is '{ctx.payload['meta']}'")
@plugin.on_commands(["kb"])
async def __(msg, ctx):
await ctx.reply("hey", keyboard=json.dumps({
"one_time": True,
"buttons": [
[
{"action": {
"type":"text",
"label": "kind=1, meta=2", "payload": "{\"kind\": \"1\", \"meta\": \"2\"}"
}}
],
[
{"action": {
"type":"text",
"label": "kind=1, meta=4", "payload": "{\"kind\": \"1\", \"meta\": \"4\"}"
}}
],
[
{"action": {
"type":"text",
"label": "kind=2, meta=4", "payload": "{\"kind\": \"1\", \"meta\": \"4\"}"
}}
],
]
}))
При использовании конструкции on_payloads
в plugin_first, в следующем нашем plugin_second перестает срабатывать тригер @plugin.vk.on_payloads([{"kind": "1"}])
и @plugin.vk.on_payloads([{"kind": "2"}])
но если не использовать кострукцию из первого плагина, все срабатывает нормально.
Приветствую, такой вопрос возник, к примеру у нас есть куча кнопок по типу u_1_2, u_0_6, u_9_1 и тд, такой вопрос, как можно вызвать vk.on_payload
что бы он откликался на все кнопки которые начинаются допустим в нашем примере на u_?
При выводе изображения иногда нужно и написать кое-что, но телеграм отправляет это отдельным сообщением, было бы гораздо удобнее если бы при загрузке аттачей была бы возможность указывать их описание - caption
К примеру:
from kutana import Attachment
with open("assets/pizza.png", "rb") as f:
image = Attachment.new(f.read(), caption="А вот и твоя пицца!")
Это было бы очень удобно :3
Решил на проде всё же замерить скорость обработки запросов как аио так и httpx, делаем 500 запросов. Как итог, httpx справляется в 2 раза дольше нежели aiohttp. Когда бот разрастется это будет уже критично, но пока что не заметно на маленьких кол-вах запросов.
import asyncio
import time
import httpx
import aiohttp
httpx_client = httpx.AsyncClient()
aiohttp_client = aiohttp.ClientSession()
try:
text = ""
start_time = time.perf_counter()
tasks = [httpx_client.get("https://daeeros.ru") for _ in range(500)]
await asyncio.gather(*tasks)
end_time = time.perf_counter()
text += f"HTTPX: {end_time - start_time:.2f} seconds"
start_time = time.perf_counter()
tasks = [aiohttp_client.get("https://daeeros.ru") for _ in range(500)]
await asyncio.gather(*tasks)
end_time = time.perf_counter()
text += f"AIOHTTP: {end_time - start_time:.2f} seconds"
finally:
await aiohttp_client.close()
await httpx_client.aclose()
print(text)
вылетает при вызове reply для vk, если сообщение для ответа больше, чем 4096
File "C:\Python38\lib\site-packages\kutana\context.py", line 137, in reply
return await self.send_message(
File "C:\Python38\lib\site-packages\kutana\context.py", line 158, in send_message
responses.append(await self.backend.perform_send(
File "C:\Python38\lib\site-packages\kutana\backends\vkontakte\backend.py", line 427, in perform_send
true_kwargs.update(kwargs)
TypeError: 'NoneType' object is not iterable
for part in parts[:-1]:
responses.append(await self.backend.perform_send(
target_id,
part,
(),
None,
))
может быть, заменить None
It would be convenient to add the is_prefix = False parameter to the usual on_commands decorator, after which it would be triggered on commands with or without a prefix, instead of crutches with on_messages
use:
#prefixes (".",)
@plugin.on_commands(["test"])
@plugin.on_commands(["test"], is_prefix=False)
Добавить .get
метод для Document в хранилищах, чтобы можно было получать опциональные значения из корня хранимого документа.
У нас есть такой плагин рассылки, но при его работе все остальные пользователи так же ждут пока он завершится, тк как бот не будет обрабатывать их учитывая что он асинхронный, подскажите как можно не делать ожидание завершения в некоторых плагинах и сразу же давать возможность боту обрабатывать следующие сообщения/плагины?
from kutana import Plugin, RequestException
plugin = Plugin()
@plugin.on_commands(["test"])
async def __(msg, ctx):
await ctx.reply("Начинаю рассылку.")
members = []
offset = 1000
data = await ctx.request("groups.getMembers", sort="time_asc", group_id="1")
members += data['items']
steps = int(data['count'] / 1000)
for _ in range(steps):
data = await ctx.request("groups.getMembers", sort="time_asc", group_id="1", offset=offset)
members += data['items']
offset += 1000
sended = 0
for u in members:
try:
await ctx.send_message(u, text="У нас тут новая запись!", attachment="wall-1_1")
sended += 1
except RequestException:
continue
return await ctx.reply("Рассылка завершена! Всего отправлено: {}".format(sended))
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.