438 lines
18 KiB
PHP
438 lines
18 KiB
PHP
<?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;
|
||
}
|
||
}
|