# -*- coding: utf-8 -*-
"""
@author: python273
@contact: https://vk.com/python273
@license Apache License, Version 2.0
Copyright (C) 2018
"""
from collections import defaultdict
from datetime import datetime
from enum import Enum, IntEnum
import requests
CHAT_START_ID = int(2E9) # id с которого начинаются беседы
[docs]class VkLongpollMode(IntEnum):
"""
Дополнительные опции ответа
`Подробнее в документации VK API <https://vk.com/dev/using_longpoll?f=1.+Подключение>`_
"""
GET_ATTACHMENTS = 2
"""Получать вложения"""
GET_EXTENDED = 2**3
"""Возвращать расширенный набор событий"""
GET_PTS = 2**5
"""возвращать pts для метода `messages.getLongPollHistory`"""
GET_EXTRA_ONLINE = 2**6
"""В событии с кодом 8 (друг стал онлайн) возвращать дополнительные данные в поле `extra`"""
GET_RANDOM_ID = 2**7
"""Возвращать поле `random_id`"""
DEFAULT_MODE = sum(VkLongpollMode)
[docs]class VkEventType(IntEnum):
"""
Перечисление событий, получаемых от longpoll-сервера.
`Подробнее в документации VK API <https://vk.com/dev/using_longpoll?f=3.+Структура+событий>`__
"""
MESSAGE_FLAGS_REPLACE = 1
"""Замена флагов сообщения (FLAGS:=$flags)"""
MESSAGE_FLAGS_SET = 2
"""Установка флагов сообщения (FLAGS|=$mask)"""
MESSAGE_FLAGS_RESET = 3
"""Сброс флагов сообщения (FLAGS&=~$mask)"""
MESSAGE_NEW = 4
"""Добавление нового сообщения."""
MESSAGE_EDIT = 5
"""Редактирование сообщения."""
READ_ALL_INCOMING_MESSAGES = 6
"""Прочтение всех входящих сообщений в $peer_id, пришедших до сообщения с $local_id."""
READ_ALL_OUTGOING_MESSAGES = 7
"""Прочтение всех исходящих сообщений в $peer_id, пришедших до сообщения с $local_id."""
USER_ONLINE = 8
"""
Друг $user_id стал онлайн. $extra не равен 0, если в mode был передан флаг 64.
В младшем байте (остаток от деления на 256) числа extra лежит идентификатор
платформы (см. :class:`VkPlatform`). $timestamp — время последнего действия
пользователя $user_id на сайте. """
USER_OFFLINE = 9
"""
Друг $user_id стал оффлайн ($flags равен 0, если пользователь покинул сайт и 1,
если оффлайн по таймауту) . $timestamp — время последнего действия пользователя
$user_id на сайте.
"""
PEER_FLAGS_RESET = 10
"""
Сброс флагов диалога $peer_id.
Соответствует операции (PEER_FLAGS &= ~$flags).
Только для диалогов сообществ.
"""
PEER_FLAGS_REPLACE = 11
"""
Замена флагов диалога $peer_id.
Соответствует операции (PEER_FLAGS:= $flags).
Только для диалогов сообществ.
"""
PEER_FLAGS_SET = 12
"""
Установка флагов диалога $peer_id.
Соответствует операции (PEER_FLAGS|= $flags).
Только для диалогов сообществ.
"""
PEER_DELETE_ALL = 13
"""Удаление всех сообщений в диалоге $peer_id с идентификаторами вплоть до $local_id."""
PEER_RESTORE_ALL = 14
"""Восстановление недавно удаленных сообщений в диалоге $peer_id с идентификаторами вплоть до $local_id."""
CHAT_EDIT = 51
"""
Один из параметров (состав, тема) беседы $chat_id были изменены.
$self — 1 или 0 (вызваны ли изменения самим пользователем).
"""
USER_TYPING = 61
"""
Пользователь $user_id набирает текст в диалоге.
Событие приходит раз в ~5 секунд при наборе текста. $flags = 1.
"""
USER_TYPING_IN_CHAT = 62
"""Пользователь $user_id набирает текст в беседе $chat_id."""
USER_CALL = 70
"""Пользователь $user_id совершил звонок с идентификатором $call_id."""
MESSAGES_COUNTER_UPDATE = 80
"""Счетчик в левом меню стал равен $count."""
NOTIFICATION_SETTINGS_UPDATE = 114
"""Изменились настройки оповещений.
$peer_id — идентификатор чата/собеседника,
$sound — 1/0, включены/выключены звуковые оповещения,
$disabled_until — выключение оповещений на необходимый срок.
"""
[docs]class VkOfflineType(IntEnum):
"""Выход из сети в событии :attr:`VkEventType.USER_OFFLINE`"""
EXIT = 0
"""Пользователь покинул сайт"""
AWAY = 1
"""Оффлайн по таймауту"""
[docs]class VkMessageFlag(IntEnum):
"""Флаги сообщений"""
UNREAD = 1
"""Cообщение не прочитано."""
OUTBOX = 2
"""Исходящее сообщение."""
REPLIED = 2**2
"""На сообщение был создан ответ."""
IMPORTANT = 2**3
"""Помеченное сообщение."""
CHAT = 2**4
"""Сообщение отправлено через чат."""
FRIENDS = 2**5
"""
Cообщение отправлено другом.
Не применяется для сообщений из групповых бесед.
"""
SPAM = 2**6
"""Cообщение помечено как "Спам"."""
DELETED = 2**7
"""Cообщение удалено (в корзине)."""
FIXED = 2**8
"""Cообщение проверено пользователем на спам."""
MEDIA = 2**9
"""Cообщение содержит медиаконтент"""
HIDDEN = 2**16
"""Приветственное сообщение от сообщества."""
DELETED_ALL = 2**17
"""Cообщение удалено для всех получателей."""
[docs]class VkPeerFlag(IntEnum):
"""Флаги диалогов"""
IMPORTANT = 1
"""Важный диалог"""
UNANSWERED = 2
"""Неотвеченный диалог"""
MESSAGE_EXTRA_FIELDS = [
'peer_id', 'timestamp', 'subject', 'text', 'attachments', 'random_id'
]
EVENT_ATTRS_MAPPING = {
VkEventType.MESSAGE_FLAGS_REPLACE: ['message_id', 'flags'] + MESSAGE_EXTRA_FIELDS,
VkEventType.MESSAGE_FLAGS_SET: ['message_id', 'mask'] + MESSAGE_EXTRA_FIELDS,
VkEventType.MESSAGE_FLAGS_RESET: ['message_id', 'mask'] + MESSAGE_EXTRA_FIELDS,
VkEventType.MESSAGE_NEW: ['message_id', 'flags'] + MESSAGE_EXTRA_FIELDS,
VkEventType.MESSAGE_EDIT: ['message_id', 'mask'] + MESSAGE_EXTRA_FIELDS,
VkEventType.READ_ALL_INCOMING_MESSAGES: ['peer_id', 'local_id'],
VkEventType.READ_ALL_OUTGOING_MESSAGES: ['peer_id', 'local_id'],
VkEventType.USER_ONLINE: ['user_id', 'extra', 'timestamp'],
VkEventType.USER_OFFLINE: ['user_id', 'extra', 'timestamp'],
VkEventType.PEER_FLAGS_RESET: ['peer_id', 'mask'],
VkEventType.PEER_FLAGS_REPLACE: ['peer_id', 'flags'],
VkEventType.PEER_FLAGS_SET: ['peer_id', 'mask'],
VkEventType.PEER_DELETE_ALL: ['peer_id', 'local_id'],
VkEventType.PEER_RESTORE_ALL: ['peer_id', 'local_id'],
VkEventType.CHAT_EDIT: ['chat_id', 'self'],
VkEventType.USER_TYPING: ['user_id', 'flags'],
VkEventType.USER_TYPING_IN_CHAT: ['user_id', 'chat_id'],
VkEventType.USER_CALL: ['user_id', 'call_id'],
VkEventType.MESSAGES_COUNTER_UPDATE: ['count'],
VkEventType.NOTIFICATION_SETTINGS_UPDATE: [
'peer_id', 'sound', 'disabled_until']
}
def get_all_event_attrs():
keys = set()
for l in EVENT_ATTRS_MAPPING.values():
keys.update(l)
return tuple(keys)
ALL_EVENT_ATTRS = get_all_event_attrs()
PARSE_PEER_ID_EVENTS = [
k for k, v in EVENT_ATTRS_MAPPING.items() if 'peer_id' in v
]
PARSE_MESSAGE_FLAGS_EVENTS = [
VkEventType.MESSAGE_FLAGS_REPLACE,
VkEventType.MESSAGE_NEW
]
[docs]class VkLongPoll(object):
"""
Класс для работы с longpoll-сервером
`Подробнее в документации VK API <https://vk.com/dev/using_longpoll>`__.
:param vk: объект :class:`VkApi`
:param wait: время ожидания
:param mode: дополнительные опции ответа
:param preload_messages: предзагрузка данных сообщений для
получения ссылок на прикрепленные файлы
"""
__slots__ = (
'vk', 'wait', 'mode', 'preload_messages',
'url', 'session',
'key', 'server', 'ts', 'pts'
)
PRELOAD_MESSAGE_EVENTS = [
VkEventType.MESSAGE_NEW,
VkEventType.MESSAGE_EDIT
]
def __init__(self, vk, wait=25, mode=DEFAULT_MODE, preload_messages=True):
self.vk = vk
self.wait = wait
self.mode = mode
self.preload_messages = preload_messages
self.url = None
self.key = None
self.server = None
self.ts = None
self.pts = mode & VkLongpollMode.GET_PTS
self.session = requests.Session()
self.update_longpoll_server()
def update_longpoll_server(self, update_ts=True):
values = {
'lp_version': '3',
'need_pts': self.pts
}
response = self.vk.method('messages.getLongPollServer', values)
self.key = response['key']
self.server = response['server']
self.url = 'https://' + self.server
if update_ts:
self.ts = response['ts']
if self.pts:
self.pts = response['pts']
[docs] def check(self):
"""
Получить события от сервера один раз
:returns: `list` of :class:`Event`
"""
values = {
'act': 'a_check',
'key': self.key,
'ts': self.ts,
'wait': self.wait,
'mode': self.mode,
'version': 1
}
response = self.session.get(
self.url,
params=values,
timeout=self.wait + 10
).json()
if 'failed' not in response:
self.ts = response['ts']
if self.pts:
self.pts = response['pts']
return [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 preload_message_events_data(self, events):
message_ids = set()
event_by_message_id = defaultdict(list)
for event in events:
if event.type in self.PRELOAD_MESSAGE_EVENTS:
message_ids.add(event.message_id)
event_by_message_id[event.message_id].append(event)
if not message_ids:
return
messages_data = self.vk.method(
'messages.getById',
{'message_ids': message_ids}
)
for message in messages_data['items']:
for event in event_by_message_id[message['id']]:
event.message_data = message
[docs] def listen(self):
"""
Слушать сервер
:yields: :class:`Event`
"""
while True:
events = self.check()
# Мне кажется, это должно быть в check
if self.preload_messages:
self.preload_message_events_data(events)
for event in events:
yield event
[docs]class Event(object):
"""
Событие, полученное от longpoll-сервера.
Имеет поля в соответствии с `документацией <https://vk.com/dev/using_longpoll?f=3.%20Структура%20событий>`_.
События с полем `timestamp` также дополнительно имеют поле `datetime`
"""
__slots__ = frozenset((
'raw', 'type', 'platform', 'offline_type',
'user_id', 'group_id', 'peer_id',
'flags', 'mask', 'datetime',
'message_flags', 'peer_flags',
'from_user', 'from_chat', 'from_group', 'from_me', 'to_me',
'message_data'
)).union(ALL_EVENT_ATTRS)
def __init__(self, raw):
# Reset attrs to None
for i in self.__slots__:
self.__setattr__(i, None)
self.raw = raw
self.from_user = False
self.from_chat = False
self.from_group = False
self.from_me = False
self.to_me = False
self.attachments = {}
self.message_data = None
try:
self.type = VkEventType(raw[0])
self._list_to_attr(raw[1:], EVENT_ATTRS_MAPPING[self.type])
except ValueError:
pass
if self.type in PARSE_PEER_ID_EVENTS:
self._parse_peer_id()
if self.type in PARSE_MESSAGE_FLAGS_EVENTS:
self._parse_message_flags()
if self.type is VkEventType.PEER_FLAGS_REPLACE:
self._parse_peer_flags()
if self.type is VkEventType.MESSAGE_NEW:
self._parse_message()
if self.type is VkEventType.MESSAGE_EDIT:
self.text = self.text.replace('<br>', '\n')
if self.type in [VkEventType.USER_ONLINE, VkEventType.USER_OFFLINE]:
self.user_id = abs(self.user_id)
self._parse_online_status()
if self.timestamp:
self.datetime = datetime.utcfromtimestamp(self.timestamp)
def _list_to_attr(self, raw, attrs):
for i in range(min(len(raw), len(attrs))):
self.__setattr__(attrs[i], raw[i])
def _parse_peer_id(self):
if self.peer_id < 0: # Сообщение от/для группы
self.from_group = True
self.group_id = abs(self.peer_id)
elif self.peer_id > CHAT_START_ID: # Сообщение из беседы
self.from_chat = True
self.chat_id = self.peer_id - CHAT_START_ID
if 'from' in self.attachments:
self.user_id = int(self.attachments['from'])
else: # Сообщение от/для пользователя
self.from_user = True
self.user_id = self.peer_id
def _parse_message_flags(self):
self.message_flags = set(
x for x in VkMessageFlag if self.flags & x
)
def _parse_peer_flags(self):
self.peer_flags = set(
x for x in VkPeerFlag if self.flags & x
)
def _parse_message(self):
if self.flags & VkMessageFlag.OUTBOX:
self.from_me = True
else:
self.to_me = True
self.text = self.text.replace('<br>', '\n')
def _parse_online_status(self):
try:
if self.type is VkEventType.USER_ONLINE:
self.platform = VkPlatform(self.extra & 0xFF)
elif self.type is VkEventType.USER_OFFLINE:
self.offline_type = VkOfflineType(self.flags)
except ValueError:
pass