initial commit
This commit is contained in:
commit
26f511535a
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.idea/
|
||||||
|
**/__pycache__
|
||||||
|
/.venv
|
||||||
|
test/
|
||||||
|
*.egg-info
|
||||||
|
dist/
|
||||||
|
|
||||||
|
._*
|
4
CHANGELOG.md
Normal file
4
CHANGELOG.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
**1.0**
|
||||||
|
-
|
||||||
|
---
|
||||||
|
Initial release
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
15
README.md
Normal file
15
README.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
**Kuro Core**
|
||||||
|
-
|
||||||
|
Требуется Python 3.6-3.11
|
||||||
|
|
||||||
|
|
||||||
|
Ниже возможно работает.
|
||||||
|
|
||||||
|
|
||||||
|
***LINKS***
|
||||||
|
-
|
||||||
|
**[Changelog](CHANGELOG.md)**
|
||||||
|
|
|
||||||
|
**[MIT License](LICENSE)**
|
||||||
|
|
|
||||||
|
**[Author VK](https://vk.com/scur0nek0)**
|
8
kurocore/__init__.py
Normal file
8
kurocore/__init__.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from .config import Config
|
||||||
|
from .bot import Bot
|
||||||
|
from .logger import BotLogger, RequestLogger, ClientLogger
|
||||||
|
|
||||||
|
from main.plugins import Plugin, MethodWithPriority
|
||||||
|
|
||||||
|
from main.event import ChatEvent, Event
|
||||||
|
from main.message import Message, MessageArgs
|
59
kurocore/bot.py
Normal file
59
kurocore/bot.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import sys
|
||||||
|
from asyncio import WindowsSelectorEventLoopPolicy, set_event_loop_policy
|
||||||
|
from platform import system
|
||||||
|
|
||||||
|
from .logger import BotLogger, RequestLogger, ClientLogger
|
||||||
|
from .main.handler import Handler
|
||||||
|
|
||||||
|
|
||||||
|
class Bot:
|
||||||
|
__slots__ = ('config', 'is_running', 'middlewares', 'tasks')
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
BotLogger(config)
|
||||||
|
RequestLogger(config)
|
||||||
|
ClientLogger(config)
|
||||||
|
self.config = config
|
||||||
|
# Database(self.config)
|
||||||
|
self.is_running = False
|
||||||
|
self.middlewares = []
|
||||||
|
self.tasks = []
|
||||||
|
if sys.version_info >= (3, 8) and sys.platform.lower().startswith("win"):
|
||||||
|
set_event_loop_policy(WindowsSelectorEventLoopPolicy())
|
||||||
|
|
||||||
|
def add_plugin(self, plugin):
|
||||||
|
if self.is_running:
|
||||||
|
BotLogger.log.error('Bot already running!')
|
||||||
|
self.config.plugins.append(plugin)
|
||||||
|
|
||||||
|
def add_prefix(self, prefix):
|
||||||
|
if self.is_running:
|
||||||
|
BotLogger.log.error('Bot already running!')
|
||||||
|
self.config.prefixes.append(prefix)
|
||||||
|
|
||||||
|
def add_middleware(self, middleware):
|
||||||
|
if self.is_running:
|
||||||
|
BotLogger.log.error('Bot already running!')
|
||||||
|
self.middlewares.append(middleware)
|
||||||
|
|
||||||
|
def add_task(self, task):
|
||||||
|
if self.is_running:
|
||||||
|
BotLogger.log.error('Bot already running!')
|
||||||
|
if system == 'Windows':
|
||||||
|
BotLogger.log.warning('Windows don\'t support tasks')
|
||||||
|
return
|
||||||
|
self.tasks.append(task)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
handler = Handler(self.config, self.middlewares)
|
||||||
|
handler.init()
|
||||||
|
try:
|
||||||
|
for task in self.tasks:
|
||||||
|
BotLogger.log.debug(f'registered task: {task}')
|
||||||
|
handler.loop.create_task(task())
|
||||||
|
self.is_running = True
|
||||||
|
handler.run()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
handler.shutdown()
|
||||||
|
# if Database.db and not Database.db.is_closed():
|
||||||
|
# Database.db.close()
|
73
kurocore/config.py
Normal file
73
kurocore/config.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
from dataclasses import dataclass, field, asdict
|
||||||
|
from json import loads, dump, dumps
|
||||||
|
from typing import TextIO
|
||||||
|
|
||||||
|
config_version = 1
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Config:
|
||||||
|
@dataclass
|
||||||
|
class Logs:
|
||||||
|
path: str = 'logs/'
|
||||||
|
name: str = 'KuroCore'
|
||||||
|
level: str = 'DEBUG'
|
||||||
|
requests: bool = False
|
||||||
|
console: bool = False
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Database:
|
||||||
|
driver: str = 'psql'
|
||||||
|
host: str = ''
|
||||||
|
name: str = ''
|
||||||
|
user: str = 'root'
|
||||||
|
password: str = ''
|
||||||
|
port: int = 5432
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Callback:
|
||||||
|
enabled: bool = False
|
||||||
|
code: str = ''
|
||||||
|
port: int = 9980
|
||||||
|
secret: str = ''
|
||||||
|
|
||||||
|
version: int = config_version
|
||||||
|
tokens: list[str] = field(default_factory=list[str])
|
||||||
|
prefixes: list[str] = field(default_factory=list[str])
|
||||||
|
sentry_dsn: str = ''
|
||||||
|
debug: bool = False
|
||||||
|
logs: Logs = field(default_factory=Logs)
|
||||||
|
database: Database = field(default_factory=Database)
|
||||||
|
callback: Callback = field(default_factory=Callback)
|
||||||
|
|
||||||
|
plugins = []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def loads(config_str: str):
|
||||||
|
json: dict = loads(config_str)
|
||||||
|
if json.get('version', 0) < config_version:
|
||||||
|
raise Exception('This config was generated for old version! Move config file and try again.')
|
||||||
|
if json.get('version', 0) > config_version:
|
||||||
|
raise Exception('This config was generated for newer version! Move config file and try again.')
|
||||||
|
|
||||||
|
json.update({'callback': Config.Callback(**json['callback'])})
|
||||||
|
json.update({'database': Config.Database(**json['database'])})
|
||||||
|
json.update({'logs': Config.Logs(**json['logs'])})
|
||||||
|
|
||||||
|
return Config(**json)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def loadp(path: str):
|
||||||
|
try:
|
||||||
|
with open(path, 'r') as f:
|
||||||
|
return Config.loads(f.read())
|
||||||
|
except FileNotFoundError:
|
||||||
|
gen = asdict(Config())
|
||||||
|
with open(path, 'w') as f:
|
||||||
|
dump(gen, f)
|
||||||
|
return Config.loads(dumps(gen))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load(file: TextIO):
|
||||||
|
return Config.loads(file.read())
|
||||||
|
|
113
kurocore/logger.py
Normal file
113
kurocore/logger.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
from logging import Formatter, StreamHandler, Logger, DEBUG, INFO, WARNING, ERROR
|
||||||
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
|
from os import makedirs
|
||||||
|
from os.path import dirname
|
||||||
|
|
||||||
|
from aiohttp import ClientSession, TraceRequestStartParams, FormData
|
||||||
|
from multidict import CIMultiDict
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
|
||||||
|
|
||||||
|
def mkdir_p(path):
|
||||||
|
try:
|
||||||
|
makedirs(dirname(path), exist_ok=True)
|
||||||
|
except TypeError:
|
||||||
|
makedirs(dirname(path))
|
||||||
|
|
||||||
|
|
||||||
|
class BotLogger:
|
||||||
|
log: Logger
|
||||||
|
|
||||||
|
def __init__(self, config: Config):
|
||||||
|
formatter = Formatter(
|
||||||
|
"[%(name)s] [%(levelname)s] [%(asctime)s] [%(filename)s:%(lineno)d]: %(message)s",
|
||||||
|
"%H:%M:%S %d.%m.%Y",
|
||||||
|
)
|
||||||
|
mkdir_p(config.logs.path)
|
||||||
|
level = DEBUG if config.debug else INFO
|
||||||
|
|
||||||
|
BotLogger.log = Logger(config.logs.name or "KuroCore", level=level)
|
||||||
|
|
||||||
|
console_handler = StreamHandler()
|
||||||
|
console_handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
file_handler = TimedRotatingFileHandler(
|
||||||
|
"logs/log.log", when="midnight", encoding="utf-8"
|
||||||
|
)
|
||||||
|
file_handler.suffix = "%d.%m.%Y.log"
|
||||||
|
file_handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
BotLogger.log.addHandler(console_handler)
|
||||||
|
BotLogger.log.addHandler(file_handler)
|
||||||
|
|
||||||
|
BotLogger.log.info("Logger initialized")
|
||||||
|
|
||||||
|
|
||||||
|
class RequestLogger:
|
||||||
|
log: Logger
|
||||||
|
|
||||||
|
def __init__(self, config: Config) -> None:
|
||||||
|
formatter = Formatter(
|
||||||
|
'[%(name)s] [REQUEST] [%(asctime)s]: %(message)s', "%H:%M:%S %d.%m.%Y"
|
||||||
|
)
|
||||||
|
mkdir_p(config.logs.path)
|
||||||
|
RequestLogger.log = Logger(config.logs.name, level=DEBUG)
|
||||||
|
|
||||||
|
console_handler = StreamHandler()
|
||||||
|
console_handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
file_handler = TimedRotatingFileHandler(f'{config.logs.path}/requests.log', 'midnight')
|
||||||
|
file_handler.suffix = "%d.%m.%Y.log"
|
||||||
|
file_handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
if config.logs.console:
|
||||||
|
RequestLogger.log.addHandler(console_handler)
|
||||||
|
RequestLogger.log.addHandler(file_handler)
|
||||||
|
|
||||||
|
RequestLogger.log.info("Request logger initialized")
|
||||||
|
|
||||||
|
|
||||||
|
class ClientLogger:
|
||||||
|
log: Logger
|
||||||
|
|
||||||
|
def __init__(self, config: Config):
|
||||||
|
formatter = Formatter(
|
||||||
|
"[%(name)s] [CLIENT] [%(asctime)s]: %(message)s",
|
||||||
|
"%H:%M:%S %d.%m.%Y",
|
||||||
|
)
|
||||||
|
mkdir_p(config.logs.path)
|
||||||
|
|
||||||
|
ClientLogger.log = Logger(config.logs.name, level=DEBUG)
|
||||||
|
|
||||||
|
console_handler = StreamHandler()
|
||||||
|
console_handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
file_handler = TimedRotatingFileHandler("logs/client.log", when="midnight", encoding="utf-8")
|
||||||
|
file_handler.suffix = "%d.%m.%Y.log"
|
||||||
|
file_handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
if config.logs.console:
|
||||||
|
ClientLogger.log.addHandler(console_handler)
|
||||||
|
ClientLogger.log.addHandler(file_handler)
|
||||||
|
|
||||||
|
ClientLogger.log.info("Client logger initialized")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_header(headers: CIMultiDict):
|
||||||
|
out = []
|
||||||
|
for k, v in headers.items():
|
||||||
|
out.append(f'{k}:{v}')
|
||||||
|
return f'<{", ".join(out)}>'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def on_request_start(session: ClientSession, context, params: TraceRequestStartParams):
|
||||||
|
formatted_headers = ClientLogger.format_header(params.headers)
|
||||||
|
ClientLogger.log.debug(f'request {params.method} {params.url} headers={formatted_headers}')
|
||||||
|
|
||||||
|
|
||||||
|
class LoggingClientSession(ClientSession):
|
||||||
|
async def _request(self, method, url, **kwargs):
|
||||||
|
ClientLogger.log.debug(f'request <{method}> "{url}"')
|
||||||
|
ClientLogger.log.debug(f' body: {kwargs}')
|
||||||
|
return await super()._request(method, url, **kwargs)
|
0
kurocore/main/__init__.py
Normal file
0
kurocore/main/__init__.py
Normal file
42
kurocore/main/event.py
Normal file
42
kurocore/main/event.py
Normal 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
256
kurocore/main/handler.py
Normal 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
331
kurocore/main/message.py
Normal 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
249
kurocore/main/plugins.py
Normal 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)
|
0
kurocore/utils/__init__.py
Normal file
0
kurocore/utils/__init__.py
Normal file
0
kurocore/utils/database/__init__.py
Normal file
0
kurocore/utils/database/__init__.py
Normal file
29
kurocore/utils/database/database.py
Normal file
29
kurocore/utils/database/database.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import logging
|
||||||
|
from asyncio import get_event_loop
|
||||||
|
from peewee_async import PostgresqlDatabase, Manager
|
||||||
|
|
||||||
|
from kurocore.logger import BotLogger
|
||||||
|
|
||||||
|
|
||||||
|
class Database:
|
||||||
|
db: PostgresqlDatabase = PostgresqlDatabase(None)
|
||||||
|
manager: Manager
|
||||||
|
|
||||||
|
def __init__(self, settings):
|
||||||
|
db_settings = settings.database
|
||||||
|
|
||||||
|
driver_name = db_settings.driver
|
||||||
|
host = db_settings.host
|
||||||
|
name = db_settings.name
|
||||||
|
user = db_settings.user
|
||||||
|
password = db_settings.password
|
||||||
|
port = db_settings.port
|
||||||
|
|
||||||
|
if driver_name.lower() not in ('postgres', 'postgresql', 'psql'):
|
||||||
|
raise
|
||||||
|
|
||||||
|
Database.db.init(name, user=user, password=password, host=host, port=port)
|
||||||
|
loop = get_event_loop()
|
||||||
|
Database.manager = Manager(Database.db, loop=loop)
|
||||||
|
Database.manager.allow_sync = logging.ERROR
|
||||||
|
BotLogger.log.info('Connected to database')
|
173
kurocore/utils/database/models.py
Normal file
173
kurocore/utils/database/models.py
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
from peewee import Model, IntegerField, BigIntegerField, DecimalField, TextField, ForeignKeyField, DateTimeField, \
|
||||||
|
BooleanField, CompositeKey, CharField, AutoField
|
||||||
|
|
||||||
|
# from plugins import models
|
||||||
|
from utils.database.database import Database
|
||||||
|
|
||||||
|
|
||||||
|
class BaseModel(Model):
|
||||||
|
class Meta:
|
||||||
|
database = Database.db
|
||||||
|
|
||||||
|
|
||||||
|
class Groups(BaseModel):
|
||||||
|
id = IntegerField(null=False)
|
||||||
|
name = TextField(default='')
|
||||||
|
is_vip = BooleanField(default=False)
|
||||||
|
is_admin = BooleanField(default=False)
|
||||||
|
is_tester = BooleanField(default=False)
|
||||||
|
multiplier = DecimalField(default=1)
|
||||||
|
sale = DecimalField(default=1)
|
||||||
|
max_multigen = IntegerField(default=3)
|
||||||
|
|
||||||
|
|
||||||
|
class Works(BaseModel):
|
||||||
|
id = IntegerField(null=False)
|
||||||
|
name = TextField(default='')
|
||||||
|
required_lvl = IntegerField(default=1)
|
||||||
|
money_income = DecimalField(default=0)
|
||||||
|
min_exp = IntegerField(default=0)
|
||||||
|
max_exp = IntegerField(default=0)
|
||||||
|
|
||||||
|
|
||||||
|
class Fractions(BaseModel):
|
||||||
|
id = IntegerField()
|
||||||
|
name = TextField(default='Без названия', null=False)
|
||||||
|
owner_id = IntegerField(null=False)
|
||||||
|
money = DecimalField()
|
||||||
|
exp = IntegerField()
|
||||||
|
level = IntegerField()
|
||||||
|
|
||||||
|
|
||||||
|
class Auto(BaseModel):
|
||||||
|
id = IntegerField()
|
||||||
|
name = TextField()
|
||||||
|
price = DecimalField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = 'shop_auto'
|
||||||
|
|
||||||
|
|
||||||
|
class Business(BaseModel):
|
||||||
|
id = IntegerField()
|
||||||
|
name = TextField()
|
||||||
|
price = DecimalField()
|
||||||
|
income = DecimalField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = 'shop_business'
|
||||||
|
|
||||||
|
|
||||||
|
class Maid(BaseModel):
|
||||||
|
id = IntegerField()
|
||||||
|
name = TextField()
|
||||||
|
price = DecimalField()
|
||||||
|
income = DecimalField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = 'shop_maid'
|
||||||
|
|
||||||
|
|
||||||
|
class Miner(BaseModel):
|
||||||
|
id = IntegerField()
|
||||||
|
name = TextField()
|
||||||
|
price = DecimalField()
|
||||||
|
income = DecimalField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = 'shop_miner'
|
||||||
|
|
||||||
|
|
||||||
|
class Users(BaseModel):
|
||||||
|
id = IntegerField()
|
||||||
|
user_id = BigIntegerField()
|
||||||
|
|
||||||
|
name = TextField()
|
||||||
|
greeting = TextField()
|
||||||
|
pair = IntegerField()
|
||||||
|
donat = IntegerField()
|
||||||
|
|
||||||
|
balance = DecimalField(default=10000, max_digits=20)
|
||||||
|
invested = DecimalField(default=0, max_digits=20)
|
||||||
|
btc = DecimalField(default=0, max_digits=20, decimal_places=6)
|
||||||
|
level = IntegerField()
|
||||||
|
exp = DecimalField(default=0)
|
||||||
|
|
||||||
|
group_id = IntegerField()
|
||||||
|
group = ForeignKeyField(Groups)
|
||||||
|
fraction = ForeignKeyField(Fractions, null=True, lazy_load=True)
|
||||||
|
|
||||||
|
work = ForeignKeyField(Works)
|
||||||
|
work_time = DateTimeField()
|
||||||
|
|
||||||
|
income_time = DateTimeField()
|
||||||
|
|
||||||
|
auto = ForeignKeyField(Auto)
|
||||||
|
business = ForeignKeyField(Business)
|
||||||
|
maid = ForeignKeyField(Maid)
|
||||||
|
miner = ForeignKeyField(Miner)
|
||||||
|
|
||||||
|
subscribed = BooleanField()
|
||||||
|
|
||||||
|
|
||||||
|
class Reports(BaseModel):
|
||||||
|
id = IntegerField(unique=True, null=False, primary_key=True)
|
||||||
|
text = TextField(null=False)
|
||||||
|
admin_answer = TextField(null=True)
|
||||||
|
from_id = BigIntegerField(null=False)
|
||||||
|
date = DateTimeField()
|
||||||
|
status = CharField(null=False)
|
||||||
|
attachments = TextField(null=True)
|
||||||
|
|
||||||
|
|
||||||
|
class FractionMember(BaseModel):
|
||||||
|
fraction = ForeignKeyField(Fractions)
|
||||||
|
user = ForeignKeyField(Users)
|
||||||
|
is_moderator = BooleanField()
|
||||||
|
is_admin = BooleanField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = 'fraction_member'
|
||||||
|
primary_key = CompositeKey('fraction', 'user')
|
||||||
|
|
||||||
|
|
||||||
|
# WD
|
||||||
|
class WDModels(BaseModel):
|
||||||
|
model_id = IntegerField(null=False)
|
||||||
|
name = TextField(null=False)
|
||||||
|
image = TextField(null=False)
|
||||||
|
description = TextField(null=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = 'wd_models'
|
||||||
|
|
||||||
|
|
||||||
|
class WDUsers(BaseModel):
|
||||||
|
user_id = IntegerField(null=False)
|
||||||
|
model = ForeignKeyField(WDModels)
|
||||||
|
model_id = IntegerField(null=False, default=1)
|
||||||
|
orientation = TextField(null=False, default='PORTRAIT')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = 'wd_users'
|
||||||
|
primary_key = CompositeKey('user_id', 'model_id')
|
||||||
|
|
||||||
|
|
||||||
|
class WDLoras(BaseModel):
|
||||||
|
id = IntegerField(null=False)
|
||||||
|
name = TextField(null=False, default='NOT SET')
|
||||||
|
trigger_prompt = TextField(null=False, default='')
|
||||||
|
prompt_mixin = TextField(null=False, default='')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = 'wd_loras'
|
||||||
|
primary_key = CompositeKey('id', 'trigger_prompt')
|
||||||
|
|
||||||
|
|
||||||
|
class WDPrompts(BaseModel):
|
||||||
|
id = AutoField(null=False)
|
||||||
|
user_id = IntegerField(null=False)
|
||||||
|
prompt = TextField(null=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = 'wd_prompts'
|
42
kurocore/utils/utils.py
Normal file
42
kurocore/utils/utils.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def chunk_array(array, chunk_number) -> list:
|
||||||
|
if len(array) <= chunk_number:
|
||||||
|
return [array]
|
||||||
|
out, tmp, index = [], [], 0
|
||||||
|
for el in array:
|
||||||
|
tmp.append(el)
|
||||||
|
index += 1
|
||||||
|
if index == chunk_number:
|
||||||
|
out.append(tmp)
|
||||||
|
tmp = []
|
||||||
|
index = 0
|
||||||
|
out.append(tmp)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def parse_attachments(attachments) -> tuple:
|
||||||
|
out = []
|
||||||
|
for attach in attachments:
|
||||||
|
t = attach['type']
|
||||||
|
if t == 'wall':
|
||||||
|
out.append(f'{t}{attach[t]["from_id"]}_{attach[t]["id"]}')
|
||||||
|
else:
|
||||||
|
out.append(f'{t}{attach[t]["owner_id"]}_{attach[t]["id"]}')
|
||||||
|
return tuple(out)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_mention(mention: (str, None)) -> int:
|
||||||
|
if not mention:
|
||||||
|
return 0
|
||||||
|
reg = r'\[id(\d+)\|.+\]'
|
||||||
|
match = re.match(reg, mention)
|
||||||
|
if not match:
|
||||||
|
return 0
|
||||||
|
return int(match.group(1))
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_sex(user_id, api):
|
||||||
|
query = await api.users.get(user_ids=user_id, fields='sex')
|
||||||
|
return query[0]['sex'] if len(query) > 0 else 2
|
0
kurocore/utils/vk/__init__.py
Normal file
0
kurocore/utils/vk/__init__.py
Normal file
184
kurocore/utils/vk/keyboard.py
Normal file
184
kurocore/utils/vk/keyboard.py
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
import json
|
||||||
|
from enum import Enum
|
||||||
|
from json import JSONEncoder
|
||||||
|
|
||||||
|
|
||||||
|
class EnumEncoder(JSONEncoder):
|
||||||
|
def default(self, obj):
|
||||||
|
return obj.value
|
||||||
|
|
||||||
|
|
||||||
|
class VkKeyboardColor(Enum):
|
||||||
|
PRIMARY = 'primary'
|
||||||
|
SECONDARY = 'secondary'
|
||||||
|
NEGATIVE = 'negative'
|
||||||
|
POSITIVE = 'positive'
|
||||||
|
|
||||||
|
|
||||||
|
class VkKeyboard:
|
||||||
|
def __init__(self, one_time=False, inline=False):
|
||||||
|
self.inline = inline
|
||||||
|
self.lines = [[]]
|
||||||
|
|
||||||
|
self.keyboard = {
|
||||||
|
'inline': self.inline,
|
||||||
|
'buttons': self.lines
|
||||||
|
}
|
||||||
|
if not inline:
|
||||||
|
self.keyboard['one_time'] = one_time
|
||||||
|
|
||||||
|
def __load_payload(self, payload) -> str:
|
||||||
|
if isinstance(payload, str):
|
||||||
|
return payload
|
||||||
|
elif isinstance(payload, dict):
|
||||||
|
return json.dumps(payload)
|
||||||
|
elif isinstance(payload, Payload):
|
||||||
|
return json.dumps(payload.get())
|
||||||
|
|
||||||
|
def add_text_button(self, text, payload=None, color=VkKeyboardColor.PRIMARY):
|
||||||
|
current_line = self.lines[-1]
|
||||||
|
if len(current_line) == 5:
|
||||||
|
raise TypeError('max elements in line: 5')
|
||||||
|
|
||||||
|
action = {
|
||||||
|
'type': 'text',
|
||||||
|
'label': text
|
||||||
|
}
|
||||||
|
if payload:
|
||||||
|
action.update({'payload': self.__load_payload(payload)})
|
||||||
|
button = {
|
||||||
|
'color': color,
|
||||||
|
'action': action
|
||||||
|
}
|
||||||
|
current_line.append(button)
|
||||||
|
|
||||||
|
def add_link_button(self, text, link, payload=None):
|
||||||
|
current_line = self.lines[-1]
|
||||||
|
if len(current_line) == 5:
|
||||||
|
raise TypeError('max elements in line: 5')
|
||||||
|
|
||||||
|
action = {
|
||||||
|
'type': 'open_link',
|
||||||
|
'link': link,
|
||||||
|
'label': text
|
||||||
|
}
|
||||||
|
if payload:
|
||||||
|
action.update({'payload': self.__load_payload(payload)})
|
||||||
|
button = {
|
||||||
|
'action': action
|
||||||
|
}
|
||||||
|
current_line.append(button)
|
||||||
|
|
||||||
|
def add_location_button(self, payload=None):
|
||||||
|
current_line = self.lines[-1]
|
||||||
|
if len(current_line) == 5:
|
||||||
|
raise TypeError('max elements in line: 5')
|
||||||
|
|
||||||
|
action = {
|
||||||
|
'type': 'location'
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload:
|
||||||
|
action.update({'payload': self.__load_payload(payload)})
|
||||||
|
|
||||||
|
button = {
|
||||||
|
'action': action
|
||||||
|
}
|
||||||
|
current_line.append(button)
|
||||||
|
|
||||||
|
def add_vk_pay_button(self, hash):
|
||||||
|
current_line = self.lines[-1]
|
||||||
|
if len(current_line) == 5:
|
||||||
|
raise TypeError('max elements in line: 5')
|
||||||
|
|
||||||
|
action = {
|
||||||
|
'type': 'vkpay',
|
||||||
|
'hash': hash
|
||||||
|
}
|
||||||
|
button = {
|
||||||
|
'action': action
|
||||||
|
}
|
||||||
|
current_line.append(button)
|
||||||
|
|
||||||
|
def add_vk_apps_button(self, label, app_id, owner_id=None, hash=None, payload=None):
|
||||||
|
current_line = self.lines[-1]
|
||||||
|
if len(current_line) == 5:
|
||||||
|
raise TypeError('max elements in line: 5')
|
||||||
|
|
||||||
|
action = {
|
||||||
|
'type': 'open_app',
|
||||||
|
'label': label,
|
||||||
|
'app_id': app_id
|
||||||
|
}
|
||||||
|
if owner_id:
|
||||||
|
action.update({'owner_id': owner_id})
|
||||||
|
if hash:
|
||||||
|
action.update({'hash': hash})
|
||||||
|
if payload:
|
||||||
|
action.update({'payload': self.__load_payload(payload)})
|
||||||
|
|
||||||
|
button = {
|
||||||
|
'action': action
|
||||||
|
}
|
||||||
|
current_line.append(button)
|
||||||
|
|
||||||
|
def add_callback_button(self, label, payload=None, color=VkKeyboardColor.PRIMARY):
|
||||||
|
current_line = self.lines[-1]
|
||||||
|
if len(current_line) == 5:
|
||||||
|
raise TypeError('max elements in line: 5')
|
||||||
|
|
||||||
|
action = {
|
||||||
|
'type': 'callback',
|
||||||
|
'label': label
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload:
|
||||||
|
action.update({'payload': self.__load_payload(payload)})
|
||||||
|
|
||||||
|
button = {
|
||||||
|
'action': action,
|
||||||
|
'color': color
|
||||||
|
}
|
||||||
|
current_line.append(button)
|
||||||
|
|
||||||
|
def add_line(self):
|
||||||
|
if len(self.lines) == 10:
|
||||||
|
if self.inline:
|
||||||
|
raise TypeError('max lines: 6')
|
||||||
|
else:
|
||||||
|
raise TypeError('max lines: 10')
|
||||||
|
self.lines.append([])
|
||||||
|
|
||||||
|
def get_current_line(self):
|
||||||
|
return self.lines[-1]
|
||||||
|
|
||||||
|
def get_keyboard(self):
|
||||||
|
keyboard = self.keyboard.copy()
|
||||||
|
if 'buttons' not in keyboard:
|
||||||
|
keyboard.update({'buttons': self.lines})
|
||||||
|
return json.dumps(keyboard, cls=EnumEncoder)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_empty_keyboard(cls):
|
||||||
|
keyboard = cls(True)
|
||||||
|
keyboard.keyboard['buttons'] = []
|
||||||
|
return keyboard.get_keyboard()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_empty_inline(cls):
|
||||||
|
keyboard = cls(False, True)
|
||||||
|
keyboard.keyboard['buttons'] = []
|
||||||
|
return keyboard.get_keyboard()
|
||||||
|
|
||||||
|
|
||||||
|
class Payload:
|
||||||
|
def __init__(self, cmd, **kwargs):
|
||||||
|
self.cmd = cmd
|
||||||
|
self.args: dict = kwargs
|
||||||
|
self.value = {'command': cmd}
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
if self.args:
|
||||||
|
return {'command': self.cmd, 'args': self.args}
|
||||||
|
else:
|
||||||
|
return {'command': self.cmd}
|
170
kurocore/utils/vk/longpoll.py
Normal file
170
kurocore/utils/vk/longpoll.py
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from logging import Logger
|
||||||
|
|
||||||
|
from kurocore import Config
|
||||||
|
from kurocore.logger import RequestLogger
|
||||||
|
from kurocore.utils.vk.vk import VK
|
||||||
|
|
||||||
|
|
||||||
|
class DotDict(dict):
|
||||||
|
__getattr__ = dict.get
|
||||||
|
__setattr__ = dict.__setitem__
|
||||||
|
__delattr__ = dict.__delitem__
|
||||||
|
|
||||||
|
|
||||||
|
class VKLongPoll:
|
||||||
|
__slots__ = ('vk', 'api', 'config', 'server', 'key', 'ts')
|
||||||
|
|
||||||
|
def __init__(self, config: Config, vk: VK):
|
||||||
|
self.vk = vk
|
||||||
|
self.api = vk.get_api()
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
self.server = ''
|
||||||
|
self.key = ''
|
||||||
|
self.ts = 0
|
||||||
|
|
||||||
|
async def init_lp(self):
|
||||||
|
group = (await self.api.groups.getById())['groups'][0]
|
||||||
|
lp = await self.api.groups.getLongPollServer(group_id=group['id'])
|
||||||
|
|
||||||
|
self.server = lp['server']
|
||||||
|
self.key = lp['key']
|
||||||
|
self.ts = lp['ts']
|
||||||
|
|
||||||
|
async def check(self):
|
||||||
|
async with self.vk.session.get(f'{self.server}?act=a_check&key={self.key}&ts={self.ts}&wait=25') as res:
|
||||||
|
body = await res.json()
|
||||||
|
|
||||||
|
if 'failed' in body:
|
||||||
|
code = body['failed']
|
||||||
|
if code == 1:
|
||||||
|
self.ts = body['ts']
|
||||||
|
if code == 2 or code == 3:
|
||||||
|
await self.init_lp()
|
||||||
|
else:
|
||||||
|
self.ts = body['ts']
|
||||||
|
|
||||||
|
if self.config.logs.requests:
|
||||||
|
RequestLogger.log.info(body)
|
||||||
|
|
||||||
|
for event in body['updates']:
|
||||||
|
yield VkBotEvent(event)
|
||||||
|
|
||||||
|
async def listen(self):
|
||||||
|
while True:
|
||||||
|
async for event in self.check():
|
||||||
|
Logger.log.debug(f'new event: {event.raw}')
|
||||||
|
yield event
|
||||||
|
|
||||||
|
|
||||||
|
class VkBotEvent(object):
|
||||||
|
__slots__ = (
|
||||||
|
'raw',
|
||||||
|
't', 'type',
|
||||||
|
'obj', 'object',
|
||||||
|
'client_info', 'message',
|
||||||
|
'group_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, raw):
|
||||||
|
self.raw = raw
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.type = VkBotEventType(raw['type'])
|
||||||
|
except ValueError:
|
||||||
|
self.type = raw['type']
|
||||||
|
|
||||||
|
self.t = self.type # shortcut
|
||||||
|
|
||||||
|
self.object = DotDict(raw['object'])
|
||||||
|
try:
|
||||||
|
self.message = DotDict(raw['object']['message'])
|
||||||
|
except KeyError:
|
||||||
|
self.message = None
|
||||||
|
self.obj = self.object
|
||||||
|
try:
|
||||||
|
self.client_info = DotDict(raw['object']['client_info'])
|
||||||
|
except KeyError:
|
||||||
|
self.client_info = None
|
||||||
|
|
||||||
|
self.group_id = raw['group_id']
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<{}({})>'.format(type(self), self.raw)
|
||||||
|
|
||||||
|
|
||||||
|
class VkBotEventType(Enum):
|
||||||
|
MESSAGE_NEW = 'message_new'
|
||||||
|
MESSAGE_REPLY = 'message_reply'
|
||||||
|
MESSAGE_EDIT = 'message_edit'
|
||||||
|
MESSAGE_EVENT = 'message_event'
|
||||||
|
|
||||||
|
MESSAGE_TYPING_STATE = 'message_typing_state'
|
||||||
|
|
||||||
|
MESSAGE_ALLOW = 'message_allow'
|
||||||
|
|
||||||
|
MESSAGE_DENY = 'message_deny'
|
||||||
|
|
||||||
|
PHOTO_NEW = 'photo_new'
|
||||||
|
|
||||||
|
PHOTO_COMMENT_NEW = 'photo_comment_new'
|
||||||
|
PHOTO_COMMENT_EDIT = 'photo_comment_edit'
|
||||||
|
PHOTO_COMMENT_RESTORE = 'photo_comment_restore'
|
||||||
|
|
||||||
|
PHOTO_COMMENT_DELETE = 'photo_comment_delete'
|
||||||
|
|
||||||
|
AUDIO_NEW = 'audio_new'
|
||||||
|
|
||||||
|
VIDEO_NEW = 'video_new'
|
||||||
|
|
||||||
|
VIDEO_COMMENT_NEW = 'video_comment_new'
|
||||||
|
VIDEO_COMMENT_EDIT = 'video_comment_edit'
|
||||||
|
VIDEO_COMMENT_RESTORE = 'video_comment_restore'
|
||||||
|
|
||||||
|
VIDEO_COMMENT_DELETE = 'video_comment_delete'
|
||||||
|
|
||||||
|
WALL_POST_NEW = 'wall_post_new'
|
||||||
|
WALL_REPOST = 'wall_repost'
|
||||||
|
|
||||||
|
WALL_REPLY_NEW = 'wall_reply_new'
|
||||||
|
WALL_REPLY_EDIT = 'wall_reply_edit'
|
||||||
|
WALL_REPLY_RESTORE = 'wall_reply_restore'
|
||||||
|
|
||||||
|
WALL_REPLY_DELETE = 'wall_reply_delete'
|
||||||
|
|
||||||
|
BOARD_POST_NEW = 'board_post_new'
|
||||||
|
BOARD_POST_EDIT = 'board_post_edit'
|
||||||
|
BOARD_POST_RESTORE = 'board_post_restore'
|
||||||
|
|
||||||
|
BOARD_POST_DELETE = 'board_post_delete'
|
||||||
|
|
||||||
|
MARKET_COMMENT_NEW = 'market_comment_new'
|
||||||
|
MARKET_COMMENT_EDIT = 'market_comment_edit'
|
||||||
|
MARKET_COMMENT_RESTORE = 'market_comment_restore'
|
||||||
|
|
||||||
|
MARKET_COMMENT_DELETE = 'market_comment_delete'
|
||||||
|
|
||||||
|
GROUP_LEAVE = 'group_leave'
|
||||||
|
GROUP_JOIN = 'group_join'
|
||||||
|
|
||||||
|
USER_BLOCK = 'user_block'
|
||||||
|
USER_UNBLOCK = 'user_unblock'
|
||||||
|
|
||||||
|
POLL_VOTE_NEW = 'poll_vote_new'
|
||||||
|
|
||||||
|
GROUP_OFFICERS_EDIT = 'group_officers_edit'
|
||||||
|
GROUP_CHANGE_SETTINGS = 'group_change_settings'
|
||||||
|
GROUP_CHANGE_PHOTO = 'group_change_photo'
|
||||||
|
|
||||||
|
VKPAY_TRANSACTION = 'vkpay_transaction'
|
||||||
|
|
||||||
|
APP_PAYLOAD = 'app_payload'
|
||||||
|
|
||||||
|
DONUT_SUBSCRIPTION_CREATE = 'donut_subscription_create'
|
||||||
|
DONUT_SUBSCRIPTION_PROLONGED = 'donut_subscription_prolonged'
|
||||||
|
DONUT_SUBSCRIPTION_EXPIRED = 'donut_subscription_expired'
|
||||||
|
DONUT_SUBSCRIPTION_CANCELLED = 'donut_subscription_cancelled'
|
||||||
|
DONUT_SUBSCRIPTION_PRICE_CHANGED = 'donut_subscription_price_changed'
|
||||||
|
DONUT_SUBSCRIPTION_WITHDRAW = 'donut_money_withdraw'
|
||||||
|
DONUT_SUBSCRIPTION_WITHDRAW_ERROR = 'donut_money_withdraw_error'
|
64
kurocore/utils/vk/upload.py
Normal file
64
kurocore/utils/vk/upload.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from aiohttp import FormData
|
||||||
|
|
||||||
|
from handler.message import load_attachments
|
||||||
|
from kurocore import LoggingClientSession
|
||||||
|
from utils.vk.vk import VK, VkApiMethod
|
||||||
|
|
||||||
|
|
||||||
|
class JsonParser:
|
||||||
|
@staticmethod
|
||||||
|
def dumps(data):
|
||||||
|
return json.dumps(data, ensure_ascii=False, separators=(",", ":"))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def loads(string):
|
||||||
|
return json.loads(string)
|
||||||
|
|
||||||
|
|
||||||
|
class VkUpload(object):
|
||||||
|
__slots__ = ('vk',)
|
||||||
|
|
||||||
|
def __init__(self, vk):
|
||||||
|
if not isinstance(vk, (VK, VkApiMethod)):
|
||||||
|
raise TypeError('The arg should be VK or VkApiMethod instance')
|
||||||
|
|
||||||
|
if isinstance(vk, VkApiMethod):
|
||||||
|
self.vk = vk
|
||||||
|
else:
|
||||||
|
self.vk = vk.get_api()
|
||||||
|
|
||||||
|
async def photo_messages(self, photo, pid=0):
|
||||||
|
upload_info = await self.vk.photos.getMessagesUploadServer(peer_id=pid)
|
||||||
|
|
||||||
|
data = FormData()
|
||||||
|
data.add_field(
|
||||||
|
'photo', photo,
|
||||||
|
content_type='multipart/form-data',
|
||||||
|
filename='a.png',
|
||||||
|
)
|
||||||
|
|
||||||
|
async with LoggingClientSession() as session, session.post(upload_info['upload_url'], data=data) as response:
|
||||||
|
response = await response.text()
|
||||||
|
response = json.loads(response)
|
||||||
|
|
||||||
|
photos = await self.vk.photos.saveMessagesPhoto(**response)
|
||||||
|
photos = [{'type': 'photo', 'photo': photo} for photo in photos]
|
||||||
|
return load_attachments(photos)
|
||||||
|
|
||||||
|
async def doc_message(self, doc, pid):
|
||||||
|
upload_info = await self.vk.docs.getMessagesUploadServer(peer_id=pid)
|
||||||
|
|
||||||
|
data = FormData()
|
||||||
|
data.add_field(
|
||||||
|
'file', doc,
|
||||||
|
content_type='multipart/form-data',
|
||||||
|
filename=f'a.png',
|
||||||
|
)
|
||||||
|
|
||||||
|
async with LoggingClientSession() as session, session.post(upload_info['upload_url'], data=data) as response:
|
||||||
|
response = await response.text()
|
||||||
|
response = json.loads(response)
|
||||||
|
|
||||||
|
return load_attachments([await self.vk.docs.save(**response)])
|
6
kurocore/utils/vk/utils.py
Normal file
6
kurocore/utils/vk/utils.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from json import JSONEncoder
|
||||||
|
|
||||||
|
|
||||||
|
class EnumEncoder(JSONEncoder):
|
||||||
|
def default(self, obj):
|
||||||
|
return obj.value
|
117
kurocore/utils/vk/vk.py
Normal file
117
kurocore/utils/vk/vk.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import asyncio
|
||||||
|
from time import time
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from aiohttp import ClientSession, FormData
|
||||||
|
|
||||||
|
from kurocore.logger import RequestLogger
|
||||||
|
|
||||||
|
|
||||||
|
class VkToken:
|
||||||
|
__slots__ = (
|
||||||
|
'cur_requests', 'max_rps',
|
||||||
|
'__token', '__last_req'
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, token):
|
||||||
|
self.cur_requests = 0
|
||||||
|
self.max_rps = 20
|
||||||
|
self.__token = token
|
||||||
|
self.__last_req = 0
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
# if self.cur_requests >= self.max_rps:
|
||||||
|
# raise TypeError('too many requests')
|
||||||
|
# self.cur_requests += 1
|
||||||
|
# self.__last_req = int(time())
|
||||||
|
# if self.__last_req < int(time()):
|
||||||
|
# self.cur_requests = 0
|
||||||
|
return self.__token
|
||||||
|
|
||||||
|
|
||||||
|
class VkTokenProvider:
|
||||||
|
__slots__ = (
|
||||||
|
'tokens',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, tokens: (Union[VkToken], VkToken)):
|
||||||
|
if type(tokens) is str:
|
||||||
|
self.tokens = [VkToken(tokens)]
|
||||||
|
else:
|
||||||
|
self.tokens = [VkToken(t) for t in tokens]
|
||||||
|
|
||||||
|
def obtain_token(self):
|
||||||
|
return self.tokens[0]()
|
||||||
|
# for t in self.tokens:
|
||||||
|
# if t.cur_requests < t.max_rps:
|
||||||
|
# return t()
|
||||||
|
# else:
|
||||||
|
# raise ValueError('no free tokens!')
|
||||||
|
|
||||||
|
|
||||||
|
class VK:
|
||||||
|
__slots__ = (
|
||||||
|
'token_provider',
|
||||||
|
'v', 'session'
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, tokens: (str, list, tuple, set, frozenset), v='5.220'):
|
||||||
|
self.token_provider = VkTokenProvider(tokens)
|
||||||
|
self.v = v
|
||||||
|
self.session = ClientSession()
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
asyncio.get_event_loop().run_until_complete(self.session.close())
|
||||||
|
|
||||||
|
async def call_method(self, method, **params):
|
||||||
|
params.update({'v': self.v})
|
||||||
|
|
||||||
|
params.update({'access_token': self.token_provider.obtain_token()})
|
||||||
|
|
||||||
|
async with self.session.post(
|
||||||
|
f'https://api.vk.com/method/{method}',
|
||||||
|
data=FormData(params)
|
||||||
|
) as res:
|
||||||
|
j = await res.json()
|
||||||
|
|
||||||
|
if 'error' in j:
|
||||||
|
error = j['error']
|
||||||
|
raise VKApiException(error['error_msg'], error['error_code'])
|
||||||
|
|
||||||
|
params.update({"access_token": "[MASKED]"})
|
||||||
|
RequestLogger.log.debug(f'method: {method} {params}')
|
||||||
|
|
||||||
|
if 'response' in j:
|
||||||
|
return j['response']
|
||||||
|
|
||||||
|
return j
|
||||||
|
|
||||||
|
def get_api(self):
|
||||||
|
return VkApiMethod(self)
|
||||||
|
|
||||||
|
|
||||||
|
class VkApiMethod(object):
|
||||||
|
__slots__ = ('_vk', '_method')
|
||||||
|
|
||||||
|
def __init__(self, vk, method=None):
|
||||||
|
self._vk: VK = vk
|
||||||
|
self._method = method
|
||||||
|
|
||||||
|
def __getattr__(self, method):
|
||||||
|
if '_' in method:
|
||||||
|
m = method.split('_')
|
||||||
|
method = m[0] + ''.join(i.title() for i in m[1:])
|
||||||
|
|
||||||
|
return VkApiMethod(
|
||||||
|
self._vk,
|
||||||
|
(self._method + '.' if self._method else '') + method
|
||||||
|
)
|
||||||
|
|
||||||
|
async def __call__(self, **kwargs):
|
||||||
|
return await self._vk.call_method(self._method, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class VKApiException(Exception):
|
||||||
|
def __init__(self, msg, code):
|
||||||
|
self.msg = msg
|
||||||
|
self.code = code
|
32
kurocore/utils/vk_utils.py
Normal file
32
kurocore/utils/vk_utils.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import io
|
||||||
|
from random import randint
|
||||||
|
|
||||||
|
from aiohttp import ClientSession
|
||||||
|
|
||||||
|
|
||||||
|
def get_self_id(api):
|
||||||
|
return api.groups.getById()[0]['id']
|
||||||
|
|
||||||
|
|
||||||
|
def generate_random_id():
|
||||||
|
return randint(-9 ** 99, 9 ** 99)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_name(id, api, name_case='nom'):
|
||||||
|
user = (await api.users.get(user_ids=id, name_case=name_case))[0]
|
||||||
|
return f'{user["first_name"]} {user["last_name"]}'
|
||||||
|
|
||||||
|
|
||||||
|
async def reupload_attachments(attachments, upload):
|
||||||
|
new_attachments = []
|
||||||
|
for a in attachments:
|
||||||
|
t = a['type']
|
||||||
|
if t != 'photo':
|
||||||
|
continue
|
||||||
|
url = a[t]['sizes'][-1]['url']
|
||||||
|
|
||||||
|
async with ClientSession() as session, session.get(url) as response:
|
||||||
|
file = io.BytesIO(await response.content.read())
|
||||||
|
attachment = upload.photo_messages(file)[0]
|
||||||
|
new_attachments.append(f'photo{attachment["owner_id"]}_{attachment["id"]}')
|
||||||
|
return new_attachments
|
18
pyproject.toml
Normal file
18
pyproject.toml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[project]
|
||||||
|
name = "kurocore"
|
||||||
|
version = "1.0"
|
||||||
|
authors = [
|
||||||
|
{ name="ScuroNeko", email="author@example.com" },
|
||||||
|
]
|
||||||
|
description = "A small example package"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.6"
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://git.nix13.pw/ScuroNeko/KuroCore"
|
||||||
|
Issues = "https://git.nix13.pw/ScuroNeko/KuroCore/issues"
|
9
requirements.txt
Normal file
9
requirements.txt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
asyncio
|
||||||
|
aiohttp
|
||||||
|
aiopg
|
||||||
|
psycopg2-binary
|
||||||
|
peewee
|
||||||
|
peewee-async
|
||||||
|
sentry-sdk
|
||||||
|
|
||||||
|
py-aiovk
|
Loading…
Reference in New Issue
Block a user