|
|
# -*- coding: utf-8 -*-
|
|
|
"""
|
|
|
:authors: deker104, python273
|
|
|
:license: Apache License, Version 2.0, see LICENSE file
|
|
|
|
|
|
:copyright: (c) 2019 python273
|
|
|
"""
|
|
|
from enum import Enum
|
|
|
|
|
|
import requests
|
|
|
|
|
|
CHAT_START_ID = int(2E9)
|
|
|
|
|
|
|
|
|
class DotDict(dict):
|
|
|
__getattr__ = dict.get
|
|
|
__setattr__ = dict.__setitem__
|
|
|
__delattr__ = dict.__delitem__
|
|
|
|
|
|
|
|
|
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'
|
|
|
|
|
|
|
|
|
class VkBotEvent(object):
|
|
|
""" Событие Bots Long Poll
|
|
|
|
|
|
:ivar raw: событие, в каком виде было получено от сервера
|
|
|
|
|
|
:ivar type: тип события
|
|
|
:vartype type: VkBotEventType or str
|
|
|
|
|
|
:ivar t: сокращение для type
|
|
|
:vartype t: VkBotEventType or str
|
|
|
|
|
|
:ivar object: объект события, в каком виде был получен от сервера
|
|
|
:ivar obj: сокращение для object
|
|
|
|
|
|
:ivar group_id: ID группы бота
|
|
|
:vartype group_id: int
|
|
|
"""
|
|
|
|
|
|
__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 VkBotMessageEvent(VkBotEvent):
|
|
|
""" Событие с сообщением Bots Long Poll
|
|
|
|
|
|
:ivar from_user: сообщение от пользователя
|
|
|
:vartype from_user: bool
|
|
|
|
|
|
:ivar from_chat: сообщение из беседы
|
|
|
:vartype from_chat: bool
|
|
|
|
|
|
:ivar from_group: сообщение от группы
|
|
|
:vartype from_group: bool
|
|
|
|
|
|
:ivar chat_id: ID чата
|
|
|
:vartype chat_id: int
|
|
|
"""
|
|
|
|
|
|
__slots__ = ('from_user', 'from_chat', 'from_group', 'chat_id')
|
|
|
|
|
|
def __init__(self, raw):
|
|
|
super(VkBotMessageEvent, self).__init__(raw)
|
|
|
|
|
|
self.from_user = False
|
|
|
self.from_chat = False
|
|
|
self.from_group = False
|
|
|
self.chat_id = None
|
|
|
|
|
|
peer_id = self.obj.peer_id or self.message.peer_id
|
|
|
|
|
|
if peer_id < 0:
|
|
|
self.from_group = True
|
|
|
elif peer_id < CHAT_START_ID:
|
|
|
self.from_user = True
|
|
|
else:
|
|
|
self.from_chat = True
|
|
|
self.chat_id = peer_id - CHAT_START_ID
|
|
|
|
|
|
|
|
|
class VkBotLongPoll(object):
|
|
|
""" Класс для работы с Bots Long Poll сервером
|
|
|
|
|
|
`Подробнее в документации VK API <https://vk.com/dev/bots_longpoll>`__.
|
|
|
|
|
|
:param vk: объект :class:`VkApi`
|
|
|
:param group_id: id группы
|
|
|
:param wait: время ожидания
|
|
|
"""
|
|
|
|
|
|
__slots__ = (
|
|
|
'vk', 'wait', 'group_id',
|
|
|
'url', 'session',
|
|
|
'key', 'server', 'ts'
|
|
|
)
|
|
|
|
|
|
#: Классы для событий по типам
|
|
|
CLASS_BY_EVENT_TYPE = {
|
|
|
VkBotEventType.MESSAGE_NEW.value: VkBotMessageEvent,
|
|
|
VkBotEventType.MESSAGE_REPLY.value: VkBotMessageEvent,
|
|
|
VkBotEventType.MESSAGE_EDIT.value: VkBotMessageEvent,
|
|
|
}
|
|
|
|
|
|
#: Класс для событий
|
|
|
DEFAULT_EVENT_CLASS = VkBotEvent
|
|
|
|
|
|
def __init__(self, vk, group_id, wait=25):
|
|
|
self.vk = vk
|
|
|
self.group_id = group_id
|
|
|
self.wait = wait
|
|
|
|
|
|
self.url = None
|
|
|
self.key = None
|
|
|
self.server = None
|
|
|
self.ts = None
|
|
|
|
|
|
self.session = requests.Session()
|
|
|
|
|
|
self.update_longpoll_server()
|
|
|
|
|
|
def _parse_event(self, raw_event):
|
|
|
event_class = self.CLASS_BY_EVENT_TYPE.get(
|
|
|
raw_event['type'],
|
|
|
self.DEFAULT_EVENT_CLASS
|
|
|
)
|
|
|
return event_class(raw_event)
|
|
|
|
|
|
def update_longpoll_server(self, update_ts=True):
|
|
|
values = {
|
|
|
'group_id': self.group_id
|
|
|
}
|
|
|
response = self.vk.method('groups.getLongPollServer', values)
|
|
|
|
|
|
self.key = response['key']
|
|
|
self.server = response['server']
|
|
|
|
|
|
self.url = self.server
|
|
|
|
|
|
if update_ts:
|
|
|
self.ts = response['ts']
|
|
|
|
|
|
def check(self):
|
|
|
""" Получить события от сервера один раз
|
|
|
|
|
|
:returns: `list` of :class:`Event`
|
|
|
"""
|
|
|
|
|
|
values = {
|
|
|
'act': 'a_check',
|
|
|
'key': self.key,
|
|
|
'ts': self.ts,
|
|
|
'wait': self.wait,
|
|
|
}
|
|
|
|
|
|
response = self.session.get(
|
|
|
self.url,
|
|
|
params=values,
|
|
|
timeout=self.wait + 10
|
|
|
).json()
|
|
|
|
|
|
if 'failed' not in response:
|
|
|
self.ts = response['ts']
|
|
|
return [
|
|
|
self._parse_event(raw_event)
|
|
|
for raw_event in response['updates']
|
|
|
]
|
|
|
|
|
|
elif response['failed'] == 1:
|
|
|
self.ts = response['ts']
|
|
|
|
|
|
elif response['failed'] == 2:
|
|
|
self.update_longpoll_server(update_ts=False)
|
|
|
|
|
|
elif response['failed'] == 3:
|
|
|
self.update_longpoll_server()
|
|
|
|
|
|
return []
|
|
|
|
|
|
def listen(self):
|
|
|
""" Слушать сервер
|
|
|
|
|
|
:yields: :class:`Event`
|
|
|
"""
|
|
|
|
|
|
while True:
|
|
|
for event in self.check():
|
|
|
yield event
|