This commit is contained in:
ScuroNeko 2024-03-24 04:05:54 +03:00
parent 26f511535a
commit c5e0c91828
Signed by: ScuroNeko
GPG Key ID: A75AA155FE93EBC0
24 changed files with 132 additions and 322 deletions

View File

@ -1,4 +1,9 @@
**1.1**
-
- Replaced `Message` with `MessageContext`. Now API and database should will accessed by context.
-
**1.0** **1.0**
- -
---
Initial release Initial release

View File

@ -1,6 +1,6 @@
**Kuro Core** **Kuro Core**
- -
Требуется Python 3.6-3.11 Требуется Python 3.6-3.12
Ниже возможно работает. Ниже возможно работает.

View File

@ -1,8 +1,5 @@
from .config import Config from .config import Config
from .bot import Bot from .bot import Bot
from .logger import BotLogger, RequestLogger, ClientLogger from .logger import BotLogger, RequestLogger, ClientLogger
from .database import Database
from main.plugins import Plugin, MethodWithPriority from .main import Plugin, MethodWithPriority, ChatEvent, Event, MessageContext, Message, Attachment
from main.event import ChatEvent, Event
from main.message import Message, MessageArgs

View File

@ -1,24 +1,27 @@
import sys import sys
from asyncio import WindowsSelectorEventLoopPolicy, set_event_loop_policy from asyncio import set_event_loop_policy
from platform import system from platform import system
from .logger import BotLogger, RequestLogger, ClientLogger from .logger import BotLogger, RequestLogger, ClientLogger
from .main.handler import Handler from .main.handler import Handler
from kurocore.database import Database
class Bot: class Bot:
__slots__ = ('config', 'is_running', 'middlewares', 'tasks') __slots__ = ('config', 'is_running', 'middlewares', 'tasks')
def __init__(self, config): def __init__(self, config, load_db=False):
BotLogger(config) BotLogger(config)
RequestLogger(config) RequestLogger(config)
ClientLogger(config) ClientLogger(config)
self.config = config self.config = config
# Database(self.config) if load_db:
Database(self.config)
self.is_running = False self.is_running = False
self.middlewares = [] self.middlewares = []
self.tasks = [] self.tasks = []
if sys.version_info >= (3, 8) and sys.platform.lower().startswith("win"): if sys.version_info >= (3, 8) and sys.platform.lower().startswith("win"):
from asyncio import WindowsSelectorEventLoopPolicy
set_event_loop_policy(WindowsSelectorEventLoopPolicy()) set_event_loop_policy(WindowsSelectorEventLoopPolicy())
def add_plugin(self, plugin): def add_plugin(self, plugin):
@ -55,5 +58,5 @@ class Bot:
handler.run() handler.run()
except KeyboardInterrupt: except KeyboardInterrupt:
handler.shutdown() handler.shutdown()
# if Database.db and not Database.db.is_closed(): if Database.db and not Database.db.is_closed():
# Database.db.close() Database.db.close()

View File

@ -0,0 +1,3 @@
from .plugins import Plugin, MethodWithPriority
from .message import Message, MessageContext, Attachment
from .event import Event, ChatEvent

View File

@ -1,11 +1,10 @@
from kurocore.utils.vk_utils import generate_random_id from kurocore.vk.utils import generate_random_id
class ChatEvent: class ChatEvent:
__slots__ = ('session', 'api', 'raw', 'type', 'member_id', 'text', 'photo') __slots__ = ('session', 'api', 'raw', 'type', 'member_id', 'text', 'photo')
def __init__(self, session, api, raw: dict): def __init__(self, api, raw: dict):
self.session = session
self.api = api self.api = api
self.raw: dict = raw self.raw: dict = raw
@ -18,8 +17,7 @@ class ChatEvent:
class Event: class Event:
__slots__ = ('session', 'api', 'raw', 'type') __slots__ = ('session', 'api', 'raw', 'type')
def __init__(self, session, api, raw: dict): def __init__(self, api, raw: dict):
self.session = session
self.api = api self.api = api
self.raw: dict = raw['object'] self.raw: dict = raw['object']
@ -35,7 +33,7 @@ class Event:
if text: if text:
data.update({'message': text}) data.update({'message': text})
if attachments: if attachments:
if type(attachments) == str: if type(attachments) is str:
data.update({'attachment': attachments}) data.update({'attachment': attachments})
else: else:
data.update({'attachment': ','.join(attachments)}) data.update({'attachment': ','.join(attachments)})

View File

@ -4,11 +4,11 @@ import traceback
from aiohttp.web import run_app from aiohttp.web import run_app
from sentry_sdk import init as sentry_init, capture_exception, set_user from sentry_sdk import init as sentry_init, capture_exception, set_user
from kurocore.logger import BotLogger
from kurocore.vk import VK, VKLongPoll, VkBotEventType
from .event import ChatEvent, Event from .event import ChatEvent, Event
from kurocore.utils.database.database import Database from .message import MessageContext, Message
from kurocore.utils.vk.longpoll import VKLongPoll, VkBotEventType from kurocore.database import Database
from kurocore.utils.vk.vk import VK
from ..logger import BotLogger
class Handler: class Handler:
@ -145,7 +145,10 @@ class Handler:
for before_process in p.before_process_methods: for before_process in p.before_process_methods:
before_process.call() before_process.call()
await p._process_command(command, msg, args) ctx = MessageContext(self.api, msg, None)
if Database.db:
ctx.db = Database
await p._process_command(command, ctx, args)
return return
except Exception as e: except Exception as e:
if self.config.debug: if self.config.debug:
@ -175,9 +178,9 @@ class Handler:
if not await self.check_payload(msg): if not await self.check_payload(msg):
await self.check_command(msg) await self.check_command(msg)
# if db: if db:
# db.close() db.close()
# BotLogger.log.debug('Connection closed!') BotLogger.log.debug('Connection closed!')
async def check_event(self, event: (ChatEvent, Event), msg): async def check_event(self, event: (ChatEvent, Event), msg):
event_type = event.type event_type = event.type
@ -220,23 +223,22 @@ class Handler:
# self.shutdown() # self.shutdown()
async def handle_event(self, event): async def handle_event(self, event):
from kurocore import Message
if ((event.type == VkBotEventType.MESSAGE_NEW and 'action' not in event.obj) if ((event.type == VkBotEventType.MESSAGE_NEW and 'action' not in event.obj)
or event.type == VkBotEventType.MESSAGE_EVENT): or event.type == VkBotEventType.MESSAGE_EVENT):
msg = Message(self.session, self.api, event.obj) msg = Message(self.api, event.obj)
if msg.user_id > 0: if msg.user_id > 0:
set_user({'id': msg.user_id}) set_user({'id': msg.user_id})
await self.check(msg) await self.check(msg)
elif event.type == VkBotEventType.MESSAGE_NEW and 'action' in event.obj: elif event.type == VkBotEventType.MESSAGE_NEW and 'action' in event.obj:
e = ChatEvent(self.session, self.api, event.obj['action']) e = ChatEvent(self.api, event.obj['action'])
msg = Message(self.session, self.api, event.obj) msg = Message(self.api, event.obj)
if msg.user_id > 0: if msg.user_id > 0:
set_user({'id': msg.user_id}) set_user({'id': msg.user_id})
await self.check_event(e, msg) await self.check_event(e, msg)
else: else:
e = Event(self.session, self.api, event.raw) e = Event(self.api, event.raw)
await self.check_event(e, None) await self.check_event(e, None)
async def _run(self): async def _run(self):

View File

@ -2,9 +2,9 @@ import json
from enum import Enum from enum import Enum
from typing import Union, Type, Tuple from typing import Union, Type, Tuple
from kurocore.utils.vk.keyboard import VkKeyboard from kurocore.database import Database
from kurocore.utils.vk.vk import VKApiException from kurocore.vk import VkKeyboard, VKApiException, VK
from kurocore.utils.vk_utils import generate_random_id from kurocore.vk.utils import generate_random_id
class MessageArgs(dict): class MessageArgs(dict):
@ -137,41 +137,6 @@ class Document(VkObject):
Attachment = Type[Photo], Type[Document], Type[Sticker] 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): class Message(VkObject):
__slots__ = ( __slots__ = (
'vk', 'api', 'raw', 'id', 'conversation_message_id', 'cmid', 'vk', 'api', 'raw', 'id', 'conversation_message_id', 'cmid',
@ -181,10 +146,9 @@ class Message(VkObject):
'meta' 'meta'
) )
def __init__(self, vk, api, raw): def __init__(self, api, raw):
super().__init__(raw) super().__init__(raw)
self.vk = vk
self.api = api self.api = api
if type(raw) is Message: if type(raw) is Message:
@ -218,8 +182,7 @@ class Message(VkObject):
self.event_id: str = self.raw.get('event_id', '') self.event_id: str = self.raw.get('event_id', '')
self.forwarded_messages: list = self.raw.get('fwd_messages', []) self.forwarded_messages: list = self.raw.get('fwd_messages', [])
self.reply_message = Message(self.vk, self.api, self.raw['reply_message']) \ self.reply_message = Message(self.api, self.raw['reply_message']) if 'reply_message' in self.raw else None
if 'reply_message' in self.raw else None
self.meta: dict = {} self.meta: dict = {}
@ -325,7 +288,56 @@ class Message(VkObject):
'conversation_message_ids': [cmid] 'conversation_message_ids': [cmid]
} }
res = await self.api.messages.getByConversationMessageId(**data) res = await self.api.messages.getByConversationMessageId(**data)
return Message(self.vk, self.api, res['items'][0]) return Message(self.api, res['items'][0])
def __repr__(self): def __repr__(self):
return str(self.raw) return str(self.raw)
class MessageContext:
__slots__ = ('api', 'msg', 'db', 'args', 'meta')
def __init__(self, api, msg, db):
self.api: VK = api
self.msg: Message = msg
self.db: Database = db
self.args = None
self.meta = {}
def __repr__(self):
return f'<MessageContext{self.msg}({self.args})>'
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

View File

@ -1,6 +1,7 @@
import inspect
import re import re
from kurocore.main.message import MessageArgs
class MethodWithPriority: class MethodWithPriority:
__slots__ = ('priority', 'method') __slots__ = ('priority', 'method')
@ -160,12 +161,10 @@ class Plugin:
self.tasks.append(f) self.tasks.append(f)
return f return f
async def _process_command(self, command: str, msg, args): async def _process_command(self, command: str, ctx, args):
sig = inspect.signature(self.commands[command]) # sig = inspect.signature(self.commands[command])
if len(sig.parameters) == 1: ctx.args = args
await self.commands[command](msg) await self.commands[command](ctx)
elif len(sig.parameters) == 2:
await self.commands[command](msg, args)
async def _process_payload(self, payload: str, msg): async def _process_payload(self, payload: str, msg):
await self.payloads[payload](msg) await self.payloads[payload](msg)
@ -180,7 +179,6 @@ class Plugin:
return command in self.admin_commands return command in self.admin_commands
async def _validate_command_args(self, command: str, cmd_args: tuple): async def _validate_command_args(self, command: str, cmd_args: tuple):
from kurocore import MessageArgs
commands_args = self.commands_args commands_args = self.commands_args
if command not in commands_args: if command not in commands_args:
return True, MessageArgs({}) return True, MessageArgs({})
@ -215,7 +213,6 @@ class Plugin:
return True, MessageArgs(args) return True, MessageArgs(args)
async def _validate_payload_args(self, payload: str, msg_args: dict): async def _validate_payload_args(self, payload: str, msg_args: dict):
from kurocore import MessageArgs
payloads_args = self.payloads_args payloads_args = self.payloads_args
if payload not in payloads_args: if payload not in payloads_args:
return True, None return True, None

View File

@ -1,173 +0,0 @@
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'

View File

@ -1,42 +0,0 @@
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

@ -1,6 +0,0 @@
from json import JSONEncoder
class EnumEncoder(JSONEncoder):
def default(self, obj):
return obj.value

3
kurocore/vk/__init__.py Normal file
View File

@ -0,0 +1,3 @@
from .vk import VK, VKApiException
from .longpoll import VKLongPoll, VkBotEventType
from .keyboard import VkKeyboard

View File

@ -1,9 +1,8 @@
from enum import Enum from enum import Enum
from logging import Logger
from kurocore import Config from kurocore import Config
from kurocore.logger import RequestLogger from kurocore.logger import RequestLogger, BotLogger
from kurocore.utils.vk.vk import VK from kurocore.vk.vk import VK
class DotDict(dict): class DotDict(dict):
@ -54,7 +53,7 @@ class VKLongPoll:
async def listen(self): async def listen(self):
while True: while True:
async for event in self.check(): async for event in self.check():
Logger.log.debug(f'new event: {event.raw}') BotLogger.log.debug(f'new event: {event.raw}')
yield event yield event

View File

@ -2,9 +2,9 @@ import json
from aiohttp import FormData from aiohttp import FormData
from handler.message import load_attachments from kurocore.logger import LoggingClientSession
from kurocore import LoggingClientSession from kurocore.main.message import load_attachments
from utils.vk.vk import VK, VkApiMethod from kurocore.vk.vk import VK, VkApiMethod
class JsonParser: class JsonParser:

View File

@ -1,9 +1,15 @@
from json import JSONEncoder
import io import io
from random import randint from random import randint
from aiohttp import ClientSession from aiohttp import ClientSession
class EnumEncoder(JSONEncoder):
def default(self, obj):
return obj.value
def get_self_id(api): def get_self_id(api):
return api.groups.getById()[0]['id'] return api.groups.getById()[0]['id']
@ -12,11 +18,22 @@ def generate_random_id():
return randint(-9 ** 99, 9 ** 99) return randint(-9 ** 99, 9 ** 99)
async def get_user_name(id, api, name_case='nom'): async def get_user_name(user_id, api, name_case='nom'):
user = (await api.users.get(user_ids=id, name_case=name_case))[0] user = (await api.users.get(user_ids=user_id, name_case=name_case))[0]
return f'{user["first_name"]} {user["last_name"]}' return f'{user["first_name"]} {user["last_name"]}'
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)
async def reupload_attachments(attachments, upload): async def reupload_attachments(attachments, upload):
new_attachments = [] new_attachments = []
for a in attachments: for a in attachments:

View File

@ -1,10 +1,10 @@
[project] [project]
name = "kurocore" name = "kurocore"
version = "1.0" version = "1.1"
authors = [ authors = [
{ name="ScuroNeko", email="author@example.com" }, { name="ScuroNeko" },
] ]
description = "A small example package" description = "A basic library for VK bot"
readme = "README.md" readme = "README.md"
requires-python = ">=3.6" requires-python = ">=3.6"
classifiers = [ classifiers = [

View File

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