initial commit

This commit is contained in:
2024-01-02 00:25:23 +03:00
commit 26f511535a
27 changed files with 2023 additions and 0 deletions

View File

42
kurocore/main/event.py Normal file
View File

@@ -0,0 +1,42 @@
from kurocore.utils.vk_utils import generate_random_id
class ChatEvent:
__slots__ = ('session', 'api', 'raw', 'type', 'member_id', 'text', 'photo')
def __init__(self, session, api, raw: dict):
self.session = session
self.api = api
self.raw: dict = raw
self.type: str = self.raw.get('type', '')
self.member_id: int = self.raw.get('member_id', 0)
self.text: str = self.raw.get('text', '')
self.photo: dict = self.raw.get('photo', {})
class Event:
__slots__ = ('session', 'api', 'raw', 'type')
def __init__(self, session, api, raw: dict):
self.session = session
self.api = api
self.raw: dict = raw['object']
self.type: str = raw['type']
async def send_message(self, target_id, text, attachments: (str, list, tuple, set, frozenset) = '', **kwargs):
data = kwargs.copy()
data.update({
'peer_id': target_id,
'random_id': generate_random_id()
})
if text:
data.update({'message': text})
if attachments:
if type(attachments) == str:
data.update({'attachment': attachments})
else:
data.update({'attachment': ','.join(attachments)})
await self.api.messages.send(**data)

256
kurocore/main/handler.py Normal file
View File

@@ -0,0 +1,256 @@
import asyncio
import traceback
from aiohttp.web import run_app
from sentry_sdk import init as sentry_init, capture_exception, set_user
from .event import ChatEvent, Event
from kurocore.utils.database.database import Database
from kurocore.utils.vk.longpoll import VKLongPoll, VkBotEventType
from kurocore.utils.vk.vk import VK
from ..logger import BotLogger
class Handler:
__slots__ = (
'config', 'session', 'api', 'plugins', 'middlewares', 'loop', 'app'
)
def __init__(self, config, middlewares: list):
self.config = config
self.session = None
self.api = None
self.app = None
self.plugins = []
self.middlewares = middlewares
self.loop = asyncio.get_event_loop()
def init(self):
if not self.config.tokens:
BotLogger.log.error('No access token!')
exit()
self.session = VK(self.config.tokens[0])
self.api = self.session.get_api()
if not self.config.debug and self.config.sentry_dsn:
sentry_init(
self.config.sentry_dsn,
traces_sample_rate=1.0
)
BotLogger.log.info('Sentry initialized!')
for p in self.config.plugins:
for init in p.init_methods:
BotLogger.log.debug(f'Init: {p.__class__.__name__}')
init.call()
self.plugins.append(p)
def shutdown(self):
for p in self.plugins:
for method in p.shutdown_methods:
method.call()
BotLogger.log.info('Bot has been shutdown!')
async def check_payload(self, msg):
payload = msg.payload['command'] if 'command' in msg.payload else ''
args = msg.payload['args'] if 'args' in msg.payload else []
for p in self.plugins:
if p.custom_checker:
try:
for before_process in p.before_process_methods:
before_process()
await p.custom_checker(msg, p)
return True
except Exception as e:
if self.config.debug:
BotLogger.log.error(traceback.format_exc())
else:
capture_exception(e)
if payload in p.payloads.keys():
try:
msg.meta.update({'args': args})
for before_process in p.before_process_methods:
before_process.call()
valid, args = await p._validate_payload_args(payload, args)
if not valid:
await msg.answer('Неверное количество или тип аргументов!')
return False
for before_process in p.before_process_methods:
before_process.call()
if valid and args is not None:
await p._process_payload_with_args(payload, msg, args)
else:
await p._process_payload(payload, msg)
return True
except Exception as e:
if self.config.debug:
BotLogger.log.error(traceback.format_exc())
else:
capture_exception(e)
else:
return False
async def check_command(self, msg):
text = msg.text
for prefix in self.config.prefixes:
if text.startswith(prefix):
text = text[len(prefix):]
msg.meta['prefix'] = prefix
break
else:
if msg.is_chat:
return
for p in self.plugins:
if p.custom_checker:
try:
for before_process in p.before_process_methods:
before_process()
await p.custom_checker(msg, p)
return
except Exception as e:
if self.config.debug:
BotLogger.log.error(traceback.format_exc())
else:
capture_exception(e)
for command in p.commands:
if text.startswith(command):
# if p._is_vip_command(command):
# user = await get_user_or_none(msg.from_id)
# if not user or not user.group.is_vip:
# return msg.answer('Для доступа к этой команде требует VIP доступ!')
#
# if p._is_admin_command(command):
# user = await get_user_or_none(msg.from_id)
# if not user or not user.group.is_admin:
# return msg.answer('Данная комманда доступна только для администраторов!')
try:
msg.meta['cmd'] = command
args = text[len(command) + 1:].split()
msg.meta['args'] = args
args_valid, args = await p._validate_command_args(command, args)
if not args_valid:
return await msg.answer('Неверное количество или тип аргументов!')
for before_process in p.before_process_methods:
before_process.call()
await p._process_command(command, msg, args)
return
except Exception as e:
if self.config.debug:
BotLogger.log.error(traceback.format_exc())
else:
capture_exception(e)
async def check(self, msg):
db = Database.db
if db:
if not db.is_closed():
BotLogger.log.debug('Connection reused!')
else:
db.connect()
BotLogger.log.debug('Connection reopened!')
else:
BotLogger.log.debug('No database')
for m in self.middlewares:
await m(msg)
for plugin in self.plugins:
for method in plugin.before_check_methods:
await method(msg)
if not await self.check_payload(msg):
await self.check_command(msg)
# if db:
# db.close()
# BotLogger.log.debug('Connection closed!')
async def check_event(self, event: (ChatEvent, Event), msg):
event_type = event.type
for plugin in self.plugins:
if event_type in plugin.chat_events.keys():
try:
for before_process in plugin.before_process_methods:
before_process.call()
return await plugin.chat_events[event_type](event, msg)
except Exception as e:
if self.config.debug:
BotLogger.log.error(traceback.format_exc())
else:
capture_exception(e)
elif event_type in plugin.events.keys():
try:
for before_process in plugin.before_process_methods:
before_process.call()
return await plugin.events[event_type](event)
except Exception as e:
if self.config.debug:
BotLogger.log.error(traceback.format_exc())
else:
capture_exception(e)
def run(self):
if self.config.callback.enabled:
run_app(self.app, port=self.config.port)
try:
# Register all plugins tasks
for p in self.config.plugins:
for task in p.tasks:
BotLogger.log.debug(f'registered task: {task.__name__}')
self.loop.create_task(task())
self.loop.run_until_complete(self._run())
except KeyboardInterrupt:
self.session.shutdown()
# self.shutdown()
async def handle_event(self, event):
from kurocore import Message
if ((event.type == VkBotEventType.MESSAGE_NEW and 'action' not in event.obj)
or event.type == VkBotEventType.MESSAGE_EVENT):
msg = Message(self.session, self.api, event.obj)
if msg.user_id > 0:
set_user({'id': msg.user_id})
await self.check(msg)
elif event.type == VkBotEventType.MESSAGE_NEW and 'action' in event.obj:
e = ChatEvent(self.session, self.api, event.obj['action'])
msg = Message(self.session, self.api, event.obj)
if msg.user_id > 0:
set_user({'id': msg.user_id})
await self.check_event(e, msg)
else:
e = Event(self.session, self.api, event.raw)
await self.check_event(e, None)
async def _run(self):
group = (await self.api.groups.getById())['groups'][0]
BotLogger.log.info(f'Login as {group["name"]} (https://vk.com/public{group["id"]})')
lp = VKLongPoll(self.config, self.session)
await lp.init_lp()
async for event in lp.listen():
try:
await self.loop.create_task(self.handle_event(event))
except Exception as e:
if self.config.debug:
BotLogger.log.error(traceback.format_exc())
else:
capture_exception(e)

331
kurocore/main/message.py Normal file
View File

@@ -0,0 +1,331 @@
import json
from enum import Enum
from typing import Union, Type, Tuple
from kurocore.utils.vk.keyboard import VkKeyboard
from kurocore.utils.vk.vk import VKApiException
from kurocore.utils.vk_utils import generate_random_id
class MessageArgs(dict):
def __init__(self, args: dict):
self.__args: dict = args
super().__init__(args)
def __getattr__(self, item):
try:
return self.__args[item]
except KeyError:
return None
class VkObject:
__slots__ = (
'raw',
)
def __init__(self, raw):
self.raw = raw
class PhotoSize:
__slots__ = (
'type', 'url', 'width', 'height'
)
def __init__(self, raw: dict):
self.type: str = raw['type']
self.url: str = raw['url']
self.width: int = raw['width']
self.height: int = raw['height']
class StickerSize:
__slots__ = (
'url', 'width', 'height'
)
def __init__(self, raw: dict):
self.url: str = raw['url']
self.width: int = raw['width']
self.height: int = raw['height']
class Photo(VkObject):
__slots__ = (
'id', 'album_id', 'owner_id', 'access_key',
'user_id', 'text', 'date', 'sizes',
'type'
)
def __init__(self, raw: dict):
super().__init__(raw)
self.type = 'photo'
self.raw: dict = raw['photo']
self.id: int = self.raw['id']
self.album_id: int = self.raw['album_id']
self.owner_id: int = self.raw['owner_id']
self.access_key: str = self.raw.get('access_key', '')
self.user_id: int = self.raw.get('user_id', 0)
self.text: str = self.raw['text']
self.date: int = self.raw['date'] # unix time
self.sizes = [PhotoSize(r) for r in self.raw['sizes']]
def __repr__(self):
return f'photo{self.owner_id}_{self.id}'
class Sticker(VkObject):
__slots__ = (
'type', 'images', 'images_with_background',
'animation_url', 'is_allowed'
)
def __init__(self, raw: dict):
super().__init__(raw)
self.type = 'sticker'
self.raw: dict = raw['sticker']
self.images = [StickerSize(img) for img in self.raw['images']]
self.images_with_background = [StickerSize(img) for img in self.raw['images_with_background']]
self.animation_url = self.raw.get('animation_url', '')
self.is_allowed = self.raw.get('is_allowed')
def __repr__(self):
return f'sticker{self}'
class DocumentType(Enum):
TEXT = 1
ARCHIVE = 2
GIF = 3
PHOTO = 4
AUDIO = 5
VIDEO = 6
BOOKS = 7
UNKNOWN = 8
class Document(VkObject):
__slots__ = (
'id', 'album_id', 'owner_id',
'title', 'size', 'ext', 'url',
'date', 'type'
)
def __init__(self, raw: dict):
super().__init__(raw)
self.raw: dict = raw['doc']
self.id: int = self.raw['id']
self.owner_id: int = self.raw['owner_id']
self.title: str = self.raw['title']
self.size: int = self.raw['size'] # размер в байтах
self.ext: str = self.raw['ext'] # расширение
self.url: str = self.raw['url']
self.date: int = self.raw['date'] # unix time
self.type: DocumentType = self.raw['type']
def __repr__(self):
return f'doc{self.owner_id}_{self.id}'
Attachment = Type[Photo], Type[Document], Type[Sticker]
def load_attachments(raw: list[dict]) -> list[Attachment]:
attachments = []
for attachment in raw:
match attachment['type']:
case 'photo':
attachments.append(Photo(attachment))
case 'doc':
attachments.append(Document(attachment))
case 'sticker':
attachments.append(Sticker(attachment))
case _:
attachments.append(VkObject(attachment))
return attachments
def dump_attachments(raw_attachments: list[Attachment]) -> str:
return ','.join(map(repr, raw_attachments))
class EventActions:
SHOW_SNACKBAR = 'show_snackbar'
OPEN_LINK = 'open_link'
OPEN_APP = 'open_app'
class MessageID:
def __init__(self, body):
if type(body) is dict:
self.message_id = body.get('message_id', 0)
self.cmid = body.get('cmid', 0)
else:
self.message_id = body
self.cmid = body
class Message(VkObject):
__slots__ = (
'vk', 'api', 'raw', 'id', 'conversation_message_id', 'cmid',
'date', 'peer_id', 'from_id', 'user_id', 'chat_id', 'is_chat',
'original_text', 'text', 'attachments', 'payload',
'event_id', 'forwarded_messages', 'reply_message',
'meta'
)
def __init__(self, vk, api, raw):
super().__init__(raw)
self.vk = vk
self.api = api
if type(raw) is Message:
self.raw = raw.raw
else:
self.raw = raw.get('message', raw)
self.id: int = self.raw.get('id', 0)
self.conversation_message_id: int = self.raw.get('conversation_message_id', 0)
self.cmid: int = self.conversation_message_id
self.date: int = self.raw.get('date', 0)
self.peer_id: int = self.raw.get('peer_id', 0)
self.from_id: int = self.raw.get('from_id', 0)
self.user_id: int = self.raw.get('user_id', self.from_id)
self.chat_id: int = self.peer_id - 2000000000
self.is_chat: bool = self.chat_id > 0
self.original_text: str = self.raw.get('text', '')
self.text: str = self.original_text.lower()
self.attachments: list[Attachment] = load_attachments(self.raw.get('attachments', []))
raw_payload = self.raw.get('payload', '{}')
if type(raw_payload) is dict:
self.payload: dict = raw_payload
else:
self.payload: dict = json.loads(raw_payload)
self.event_id: str = self.raw.get('event_id', '')
self.forwarded_messages: list = self.raw.get('fwd_messages', [])
self.reply_message = Message(self.vk, self.api, self.raw['reply_message']) \
if 'reply_message' in self.raw else None
self.meta: dict = {}
async def send(self, peer_id, text: str = '', attachments: (Tuple[Attachment], Attachment, str) = '',
keyboard: Union[VkKeyboard, dict] = None, **kwargs):
data = kwargs.copy()
data.update({
'peer_id': peer_id,
'random_id': generate_random_id()
})
if text:
data.update({'message': text})
if keyboard:
if type(keyboard) is VkKeyboard:
data.update({'keyboard': keyboard.get_keyboard()})
else:
data.update({'keyboard': keyboard})
if attachments:
if type(attachments) is str:
data.update({'attachment': attachments})
elif type(attachments) in (Photo, Document):
data.update({'attachment': str(attachments)})
else:
data.update({'attachment': dump_attachments(attachments)})
return MessageID(await self.api.messages.send(**data))
async def answer_event(self, event_data):
data = {
'peer_id': self.peer_id,
'event_id': self.event_id,
'user_id': self.user_id,
'event_data': json.dumps(event_data),
}
try:
return await self.api.messages.sendMessageEventAnswer(**data)
except VKApiException:
...
async def answer_event_hide_keyboard(self, event_data):
await self.answer_event(event_data)
await self.api.messages.edit(
peer_id=self.peer_id,
conversation_message_id=self.conversation_message_id,
keyboard=VkKeyboard.get_empty_keyboard()
)
async def answer(self, text: str = '', attachments: (list[Attachment], Attachment, str) = '', **kwargs):
return await self.send(self.peer_id, text, attachments, **kwargs)
async def edit(self, text='', attachments: list[Attachment] = '', keyboard: Union[VkKeyboard, dict] = None,
**kwargs):
data: dict = kwargs.copy()
data.update({
'peer_id': self.peer_id
})
if text:
data.update({'message': text})
if keyboard:
if type(keyboard) is VkKeyboard:
data.update({'keyboard': keyboard.get_keyboard()})
else:
data.update({'keyboard': keyboard})
if attachments:
if type(attachments) is str:
data.update({'attachment': attachments})
elif type(attachments) in (Photo, Document):
data.update({'attachment': str(attachments)})
elif type(attachments) is list:
if type(attachments[0]) is dict:
data.update({'attachment': dump_attachments(load_attachments(attachments))})
else:
data.update({'attachment': dump_attachments(attachments)})
if 'cmid' not in kwargs and 'conversation_message_id' not in kwargs:
data.update({
'conversation_message_id': self.conversation_message_id
})
if 'cmid' in kwargs:
data.update({
'conversation_message_id': kwargs['cmid']
})
kwargs.pop('cmid')
try:
message_id = await self.api.messages.edit(**data)
except VKApiException:
message_id = await self.api.messages.send(**data, random_id=generate_random_id())
return MessageID(message_id)
async def get_by_cmid(self, cmid: int):
data = {
'peer_id': self.peer_id,
'conversation_message_ids': [cmid]
}
res = await self.api.messages.getByConversationMessageId(**data)
return Message(self.vk, self.api, res['items'][0])
def __repr__(self):
return str(self.raw)

249
kurocore/main/plugins.py Normal file
View File

@@ -0,0 +1,249 @@
import inspect
import re
class MethodWithPriority:
__slots__ = ('priority', 'method')
def __init__(self, method, priority):
self.priority = priority
self.method = method
def call(self):
self.method()
class Plugin:
__slots__ = ('custom_checker', 'custom_processor',
'commands', 'commands_args', 'commands_help',
'args_help', 'before_check_methods',
'vip_commands', 'admin_commands',
'payloads', 'payloads_args',
'events', 'chat_events', 'init_methods',
'before_process_methods', 'shutdown_methods',
'tasks')
def __init__(self, custom_checker=None, custom_processor=None):
self.custom_checker = custom_checker
self.custom_processor = custom_processor
self.before_check_methods: list = []
self.commands: dict = {}
self.commands_args: dict = {}
self.commands_help: dict = {}
self.args_help: dict = {}
self.vip_commands: list = []
self.admin_commands: list = []
self.payloads: dict = {}
self.payloads_args: dict = {}
self.events: dict = {}
self.chat_events: dict = {}
self.init_methods: list = []
self.before_process_methods: list = []
self.shutdown_methods: list = []
self.tasks: list = []
def __repr__(self):
return str({
'custom_checker': self.custom_checker,
'custom_processor': self.custom_processor,
'before_check_commands': self.before_check_methods,
'commands': list(self.commands.keys()),
'commands_args': list(self.commands_args.keys()),
'commands_help': list(self.commands_help.keys()),
'args_help': list(self.args_help.keys()),
'vip_commands': self.vip_commands,
'admin_commands': self.admin_commands,
'payloads': list(self.payloads.keys()),
'payloads_args': list(self.payloads_args.keys()),
'events': list(self.events.keys()),
'chat_events': list(self.chat_events.keys()),
'init_methods': self.init_methods,
'before_process_methods': self.before_process_methods,
'shutdown_methods': self.shutdown_methods,
'tasks': self.tasks
})
def init(self, priority: int = 0):
def wrapper(f):
self.init_methods.append(MethodWithPriority(f, priority))
self.init_methods.sort(key=lambda method: method.priority, reverse=True)
return f
return wrapper
def before_process(self, priority: int = 0):
def wrapper(f):
self.before_process_methods.append(MethodWithPriority(f, priority))
self.before_process_methods.sort(key=lambda method: method.priority, reverse=True)
return f
return wrapper
def on_command(self, *commands, args='', h=tuple(), is_admin: bool = False):
def wrapper(f):
self.commands.update(map(lambda cmd: (cmd, f), commands))
if is_admin:
self.admin_commands.append(*commands)
if args:
self.commands_args.update(map(lambda cmd: (cmd, args), commands))
if h:
self.commands_help.update({commands[0]: h[0]})
if len(h) > 1:
self.args_help.update({commands[0]: h[1:]})
return f
return wrapper
def before_check(self, f):
self.before_check_methods.append(f)
# print(self.before_check_methods)
return f
def vip_command(self, f):
for k in self.commands.keys():
self.vip_commands.append(k)
return f
def admin_command(self, f):
for k in self.commands.keys():
self.admin_commands.append(k)
return f
def on_payload(self, *payloads: str, args=''):
def wrapper(f):
if args:
self.payloads_args.update(map(lambda cmd: (cmd, args), payloads))
self.payloads.update(dict(map(lambda payload: (payload, f), payloads)))
return f
return wrapper
def on_event(self, *events):
def wrapper(f):
self.chat_events.update(
map(lambda event: (event, f), filter(lambda event: event.startswith('chat'), events)))
self.events.update(map(lambda event: (event, f),
filter(lambda event: not event.startswith('chat'), events)))
return f
return wrapper
def on_shutdown(self, priority: int = 0):
def wrapper(f):
self.shutdown_methods.append(MethodWithPriority(f, priority))
self.shutdown_methods.sort(key=lambda method: method.priority, reverse=True)
return f
return wrapper
"""Decorator for task
"""
def task(self, f):
self.tasks.append(f)
return f
async def _process_command(self, command: str, msg, args):
sig = inspect.signature(self.commands[command])
if len(sig.parameters) == 1:
await self.commands[command](msg)
elif len(sig.parameters) == 2:
await self.commands[command](msg, args)
async def _process_payload(self, payload: str, msg):
await self.payloads[payload](msg)
async def _process_payload_with_args(self, payload: str, msg, args):
await self.payloads[payload](msg, args)
def _is_vip_command(self, command: str) -> bool:
return command in self.vip_commands
def _is_admin_command(self, command: str) -> bool:
return command in self.admin_commands
async def _validate_command_args(self, command: str, cmd_args: tuple):
from kurocore import MessageArgs
commands_args = self.commands_args
if command not in commands_args:
return True, MessageArgs({})
args = commands_args[command].split()
if not cmd_args and not tuple(filter(lambda x: '?' not in x, args)):
return True, MessageArgs({})
if len(cmd_args) < len(tuple(filter(lambda x: '?' not in x, args))):
return False, None
args_map = []
for arg in args:
name, arg_type = arg.split(':')
if name.endswith('?'):
name = name[:-1]
arg_type = arg_type.replace('str', r'.').replace('int', r'\d')
args_map.append((name, re.compile(arg_type)))
args = dict()
for index in range(len(cmd_args)):
if len(args_map) == index:
break
name, expression = args_map[index]
if not expression.match(cmd_args[index]):
return False, None
args.update({name: cmd_args[index]})
return True, MessageArgs(args)
async def _validate_payload_args(self, payload: str, msg_args: dict):
from kurocore import MessageArgs
payloads_args = self.payloads_args
if payload not in payloads_args:
return True, None
args = payloads_args[payload].split()
if len(msg_args) < len(tuple(filter(lambda x: '?' not in x, args))):
return False, None
args_map = []
for arg in args:
name, arg_type = arg.split(':')
if name.endswith('?'):
name = name[:-1]
arg_type = arg_type.replace('str', r'.').replace('int', r'\d')
args_map.append((name, re.compile(arg_type)))
args = dict()
for index in range(len(msg_args)):
name, expression = args_map[index]
if not expression.match(str(tuple(msg_args.values())[index])):
return False, None
args.update({name: tuple(msg_args.values())[index]})
return True, MessageArgs(args)
async def _process_event(self, event_type: str, event):
await self.events[event_type](event)
async def _process_chat_event(self, event_type: str, event, msg):
await self.chat_events[event_type](event, msg)