initial commit

This commit is contained in:
ScuroNeko 2024-01-02 00:25:23 +03:00
commit 26f511535a
Signed by: ScuroNeko
GPG Key ID: C9081BDABF0837CA
27 changed files with 2023 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.idea/
**/__pycache__
/.venv
test/
*.egg-info
dist/
._*

4
CHANGELOG.md Normal file
View File

@ -0,0 +1,4 @@
**1.0**
-
---
Initial release

21
LICENSE Normal file
View 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
View 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
View 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
View 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
View 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
View 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)

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)

View File

View File

View 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')

View 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
View 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

View File

View 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}

View 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'

View 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)])

View 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
View 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

View 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
View 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
View File

@ -0,0 +1,9 @@
asyncio
aiohttp
aiopg
psycopg2-binary
peewee
peewee-async
sentry-sdk
py-aiovk