vk-arangodb/mirzaev/vk/arangodb/system/longpoll.php

438 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace mirzaev\vk\arangodb;
// Файлы проекта
use mirzaev\arangodb\connection,
mirzaev\arangodb\collection,
mirzaev\arangodb\terminal,
mirzaev\arangodb\document,
mirzaev\vk\arangodb\traits\HTTP\headers\content\disposition;
// Библиотека для работы с API-сервера ArangoDB
use ArangoDBClient\Document as _document;
// Библиотека браузера
use GuzzleHttp\Client as Guzzle;
// Встроенные библиотеки
use Exception;
/**
* LongPoll API ВКонтакте
*
* @todo
* 1. Проработать создание индексов в базе данных
*/
class longpoll
{
use disposition {
disposition::filename as disposition_filename;
}
const COLLECTION_ACCOUNTS = 'account';
const COLLECTION_GROUPS = 'group';
const COLLECTION_CHATS = 'chat';
const COLLECTION_MESSAGES = 'message';
const COLLECTION_ACCESSED = 'accessed';
public static string $path_storage = __DIR__ . '/storage';
public static string $path_storage_accounts = '/accounts';
public static string $path_storage_accounts_images = '/images';
public static string $path_storage_accounts_videos = '/videos';
public static string $path_storage_accounts_audios = '/audios';
public static string $path_storage_vk = '/vk';
public static string $path_storage_vk_stickers = '/stickers';
protected bool $journal = true;
public function __construct(protected connection $connection)
{
}
/**
* Сохранить событие в базу данных
*
* @param array $updates События
*
* @return bool Статус сохранения
*/
public function save(array $update): bool
{
try {
// Динамический вызов метода-обработчика события
if ($this->{$update['type']}($update['object'], $update['group_id'], $update['event_id'])) {
// Удалось сохранить в базу данных
return true;
}
} catch (Exception $e) {
// Запись ошибки в буфер вывода
terminal::write($e->getMessage() . PHP_EOL . $e->getFile() . ':' . $e->getLine());
}
return false;
}
/**
* Событие: "message_new"
*
* @param array $data Данные сообщения
* @param int $group Идентификатор группы
* @param string $event Идентификатор события
*
* @return bool
*/
public function message_new(array $data, int $group, string $event): bool
{
if ($this->connection->create) {
// Запрошено создание коллекций в случае их отсутствия
// Инициализация коллекций
collection::init($this->connection->session, static::COLLECTION_ACCOUNTS);
collection::init($this->connection->session, static::COLLECTION_MESSAGES);
}
if ($message = document::write($this->connection->session, static::COLLECTION_MESSAGES, static::messages($data, $data['message']['from_id']))) {
// Записано сообщение
// Инициализация субъектов
$from = ('mirzaev\\vk\\arangodb\\vk\\' . static::type($data['message']['from_id']))::init($this->connection->session, $data['message']['from_id']);
$to = ('mirzaev\\vk\\arangodb\\vk\\' . static::type($data['message']['peer_id']))::init($this->connection->session, $data['message']['peer_id']);
if ($from instanceof _document && $to instanceof _document) {
// Инициализированы аккаунты
if ($this->connection->create) {
// Запрошено создание коллекций в случае их отсутствия
// Инициализация коллекции
collection::init($this->connection->session, static::COLLECTION_ACCESSED, edge: true);
}
if (document::write($this->connection->session, static::COLLECTION_ACCESSED, ['_from' => $from->getId(), '_to' => $message])) {
// Записано ребро: АККАУНТ (отправитель) -> СООБЩЕНИЕ
}
if (document::write($this->connection->session, static::COLLECTION_ACCESSED, ['_from' => $message, '_to' => $to->getId()])) {
// Записно ребро: СООБЩЕНИЕ -> АККАУНТ (получатель)
}
}
// Журналирование
if ($this->journal && journal::init($this->connection->session, $message)->write('create', [
'account' => $from,
'changes' => [
'new' => collection::search($this->connection->session, sprintf(
<<<'AQL'
FOR a IN %s
FILTER a._id == "%s"
LIMIT 1
RETURN a.data
AQL,
static::COLLECTION_MESSAGES,
$message
)),
'old' => null
]
])) {
// Записано ребро: СООБЩЕНИЕ -> СООБЩЕНИЕ
}
return true;
}
throw new Exception('Не удалось сохранить сообщение в базу даннных', 500);
}
/**
* Обработка сообщений
*
* @param array $messages Сообщения или сообщение
* @param ?int $id Идентификатор аккаунта которому принадлежат вложения
* @param bool $clean Очистить массив от пустых данных?
*
* @return array Обработанные сообщения (зависит от входных данных)
*
* @todo
* 1. Переделать $message['vk']['metadata']['action']['cover']
* 2. Переделать $message['vk']['metadata']['payload']
* 3. Узнать про Notify API и добавить message_tag
* 4. В будущем удалить $message['message']['body']
* 5. Переделать $message['vk']['metadata']['conversation']['members']['amount_test'] (или удалить)
* 6. Разобраться с "Мультидиалогом" для старых версий API и существует ли он в новых
*/
public static function messages(array $messages, ?int $id = null, bool $clean = true): array
{
// Универсализация входных данных - передано одно сообщение
if (isset($messages['message'])) $buffer[] = &$messages;
foreach ($buffer ?? $messages as &$message) {
// Перебор сообщений
// Инициализация сообщения
$message = [
'id' => [
'global' => $message['message']['id'],
'local' => $message['message']['conversation_message_id']
],
'text' => $message['message']['text'] ?? $message['message']['body'],
'forward' => static::messages($message['message']['fwd_messages']),
'reply' => static::messages($message['message']['fwd_messages']),
'attachments' => static::attachments($message['message']['attachments'], $id, $clean),
'date' => [
'create' => $message['message']['date'] ?? null,
'update' => $message['message']['update_time'] ?? null
],
'action' => [
'type' => $message['message']['action'] ?? null,
'target' => [
'id' => $message['message']['action']['member_id'] ?? null
],
'text' => $message['message']['action']['text'] ?? null,
'email ' => $message['message']['action']['email'] ?? null,
'cover ' => $message['message']['action']['photo'] ?? null
],
'hash' => $message['message']['random_id'] ?? null,
'type' => $message['message']['out'] ?? null,
'admin' => [
'id' => $message['message']['admin_author_id'] ?? null
],
'pinned' => [
'date' => $message['message']['pinned_at'] ?? null
],
'emoji' => $message['message']['emoji'] ?? null,
'readed' => $message['message']['read_state'] ?? null,
'listened' => $message['message']['was_listened'] ?? null,
'hidden' => $message['message']['is_hidden'] ?? null,
'cropped' => $message['message']['is_cropped'] ?? null,
'deleted' => $message['message']['deleted'] ?? null,
'conversation' =>
[
'id' => $message['message']['chat_id'] ?? null,
'admin' => [
'id' => $message['message']['admin_id'] ?? null
],
'title' => $message['message']['title'] ?? null,
'members' => [
'amount' => $message['message']['members_count'] ?? null,
'amount_test' => $message['message']['users_count'] ?? null
],
'active' => $message['message']['chat_active'] ?? null,
'settings' => [
'push' => $message['message']['push_settings'] ?? null
]
],
'important' => $message['message']['important'] ?? null,
'source' => [
'from' => $message['message']['ref'] ?? null,
'data' => $message['message']['ref_source'] ?? null
],
'payload' => $message['message']['payload'] ?? null,
'geo' => [
$message['message']['geo'] ?? null
],
'keyboard' => [
'block' => $message['client_info']['keyboard'] ?? null,
'inline' => $message['client_info']['inline_keyboard'] ?? null,
'buttons' => $message['client_info']['button_actions'] ?? null,
],
'carousel' => $message['client_info']['carousel'] ?? null,
'language' => $message['client_info']['lang_id'] ?? null,
];
}
// Очистка массива
$clean and static::cleaner($messages);
return $messages;
}
/**
* Обработка вложений
*
* @param array $attachments Вложения
* @param ?int $id Идентификатор аккаунта которому принадлежат изображения
* @param bool $clean Очистить массив от пустых данных?
*
* @return array Обработанные вложения
*/
public static function attachments(array $attachments, ?int $id = null, bool $clean = true): array
{
foreach ($attachments as &$attachment) {
// Перебор вложений
if ($attachment['type'] === 'photo') {
// Изображение
$attachment = [
'data' => [
'date' => $attachment['photo']['date'] ?? null,
'id' => $attachment['photo']['id'] ?? null,
'album' => [
'id' => $attachment['photo']['album_id'] ?? null
],
'account' => [
'id' => $attachment['photo']['user_id'] ?? null,
'admin' => $attachment['photo']['owner_id'] ?? null
],
'tags' => $attachment['photo']['has_tags'] ?? null,
'access' => [
'key' => $attachment['photo']['access_key'] ?? null
],
'text' => $attachment['photo']['text'] ?? null,
'storage' => static::sizes($attachment['photo']['sizes'], $id) ?? null
],
'metadata' => [
'type' => $attachment['type'] ?? null
]
];
}
}
// Очистка массива
$clean and static::cleaner($attachments);
return $attachments;
}
/**
* Сортировка размеров изображения из вложения
*
* @see https://vk.com/dev/photo_sizes
*
* @param array $sizes Размеры изображения согласно спецификации в API
* @param ?int $id Идентификатор аккаунта которому принадлежат изображения
*
* @return array Обработанные размеры
*/
public static function sizes(array $sizes, ?int $id = null): array
{
foreach ($sizes as &$size) {
// Перебор размеров
if (isset($id)) {
// Запрошена запись файлов на сервер
// Инициализация
$browser = new Guzzle();
// Инициализация директории
if (!file_exists($path = static::$path_storage . static::$path_storage_accounts . PHP_EOL . $id . static::$path_storage_accounts_images . PHP_EOL . date('Y_m_d', time())))
if (!mkdir($path, 0755, true))
throw new Exception('Не удалось инициализировать директорию: ' . $path);
// Генерация временного файла с уникальным дескриптором
$file = tempnam($path, '');
// Сохранение в файл
$request = $browser->get($size['url'], ['sink' => $file]);
var_dump($request->getHeaders());
die;
// Чтение расширения файла
$ext = $request->getHeader('Mime-Type');
// Перезапись
rename($file, dirname($file) . PHP_EOL . (static::disposition_filename($request->getHeader('Content-Disposition')[0]) ?? 1));
}
// Инициализация
$size = [
'data' => [
'width' => [
'value' => $size['width'],
'unit' => 'px'
],
'height' => [
'value' => $size['height'],
'unit' => 'px'
],
'source' => [
'vk' => $size['url']
]
],
'metadata' => [
'type' => $size['type']
]
];
}
return $sizes;
}
/**
* Очистить массив от пустых значений
*
* @param array $target Обрабатываемый массив
*
* @return bool Массив был изменён?
*/
public static function cleaner(array &$target): bool
{
// Инициализация
$changes = false;
foreach ($target as $key => &$value) {
// Перебор элементов массива
if ($value === null || $value === []) {
// Пустое значение
// Удаление из массива по ключу
unset($target[$key]);
// Запись обозначения о том, что были произведены изменения
$changes = true;
} else if (is_array($value)) {
// Элемент является массивом
// Начало рекурсии (повторяется до тех пор пока производятся изменения за итерацию)
while (static::cleaner($value));
}
}
return $changes;
}
/**
* Очистить базу данных
*
* Предполагается использование для автоматизации тестирования
*
* @return void
*/
public function truncate(): void
{
collection::truncate($this->connection->session, static::COLLECTION_ACCOUNTS);
collection::truncate($this->connection->session, static::COLLECTION_GROUPS);
collection::truncate($this->connection->session, static::COLLECTION_CHATS);
collection::truncate($this->connection->session, static::COLLECTION_MESSAGES);
collection::truncate($this->connection->session, static::COLLECTION_ACCESSED);
collection::truncate($this->connection->session, journal::COLLECTION_JOURNAL);
}
/**
* Определить тип субъекта
*
* @param int $id Идентификатор
*
* @return string Возвращает static::COLLECTION_ACCOUNTS, если не прошли другие проверки
*/
public static function type(int $id): string
{
// Чат
if ($id - 2000000000 >= 0) return static::COLLECTION_CHATS;
// Группа
if ($id < 0) return static::COLLECTION_GROUPS;
// Аккаунт
return static::COLLECTION_ACCOUNTS;
}
}