v1.1
This commit is contained in:
3
kurocore/vk/__init__.py
Normal file
3
kurocore/vk/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .vk import VK, VKApiException
|
||||
from .longpoll import VKLongPoll, VkBotEventType
|
||||
from .keyboard import VkKeyboard
|
184
kurocore/vk/keyboard.py
Normal file
184
kurocore/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}
|
169
kurocore/vk/longpoll.py
Normal file
169
kurocore/vk/longpoll.py
Normal file
@@ -0,0 +1,169 @@
|
||||
from enum import Enum
|
||||
|
||||
from kurocore import Config
|
||||
from kurocore.logger import RequestLogger, BotLogger
|
||||
from kurocore.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():
|
||||
BotLogger.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/vk/upload.py
Normal file
64
kurocore/vk/upload.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import json
|
||||
|
||||
from aiohttp import FormData
|
||||
|
||||
from kurocore.logger import LoggingClientSession
|
||||
from kurocore.main.message import load_attachments
|
||||
from kurocore.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)])
|
49
kurocore/vk/utils.py
Normal file
49
kurocore/vk/utils.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from json import JSONEncoder
|
||||
import io
|
||||
from random import randint
|
||||
|
||||
from aiohttp import ClientSession
|
||||
|
||||
|
||||
class EnumEncoder(JSONEncoder):
|
||||
def default(self, obj):
|
||||
return obj.value
|
||||
|
||||
|
||||
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(user_id, api, name_case='nom'):
|
||||
user = (await api.users.get(user_ids=user_id, name_case=name_case))[0]
|
||||
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):
|
||||
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
|
117
kurocore/vk/vk.py
Normal file
117
kurocore/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
|
Reference in New Issue
Block a user