rss-bridge/bridges/Vk2Bridge.php
2024-07-28 22:34:12 +02:00

329 lines
13 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
class Vk2Bridge extends BridgeAbstract
{
const MAINTAINER = 'em92';
const NAME = 'ВКонтакте';
const URI = 'https://vk.com';
const DESCRIPTION = 'Выводит записи на стене';
const CACHE_TIMEOUT = 300; // 5 minutes
const PARAMETERS = [
[
'u' => [
'name' => 'Короткое имя группы или профиля (из ссылки)',
'exampleValue' => 'goblin_oper_ru',
'required' => true
],
'hide_reposts' => [
'name' => 'Скрыть репосты',
'type' => 'checkbox',
]
]
];
const CONFIGURATION = [
'access_token' => [
'required' => true,
],
];
const TEST_DETECT_PARAMETERS = [
'https://vk.com/id1' => ['u' => 'id1'],
'https://vk.com/groupname' => ['u' => 'groupname'],
'https://m.vk.com/groupname' => ['u' => 'groupname'],
'https://vk.com/groupname/anythingelse' => ['u' => 'groupname'],
'https://vk.com/groupname?w=somethingelse' => ['u' => 'groupname'],
'https://vk.com/with_underscore' => ['u' => 'with_underscore'],
'https://vk.com/vk.cats' => ['u' => 'vk.cats'],
];
protected $ownerNames = [];
protected $pageName;
private $urlRegex = '/vk\.com\/([\w.]+)/';
private $rateLimitCacheKey = 'vk2_rate_limit';
public function getURI()
{
if (!is_null($this->getInput('u'))) {
return urljoin(static::URI, urlencode($this->getInput('u')));
}
return parent::getURI();
}
public function getName()
{
if ($this->pageName) {
return $this->pageName;
}
return parent::getName();
}
public function detectParameters($url)
{
if (preg_match($this->urlRegex, $url, $matches)) {
return ['u' => $matches[1]];
}
return null;
}
protected function getPostURI($post)
{
$r = 'https://vk.com/wall' . $post['owner_id'] . '_';
if (isset($post['reply_post_id'])) {
$r .= $post['reply_post_id'] . '?reply=' . $post['id'] . '&thread=' . $post['parents_stack'][0];
} else {
$r .= $post['id'];
}
return $r;
}
// This function is based on SlackCoyote's vkfeed2rss
// https://github.com/em92/vkfeed2rss
protected function generateContentFromPost($post)
{
// it's what we will return
$ret = $post['text'];
// html special characters convertion
$ret = htmlentities($ret, ENT_QUOTES | ENT_HTML401);
// change all linebreak to HTML compatible <br />
$ret = nl2br($ret);
$ret = "<p>$ret</p>";
// find URLs
$ret = preg_replace(
'/((https?|ftp|gopher)\:\/\/[a-zA-Z0-9\-\.]+(:[a-zA-Z0-9]*)?\/?([@\w\-\+\.\?\,\'\/&amp;%\$#\=~\x5C])*)/',
"<a href='$1'>$1</a>",
$ret
);
// find [id1|Pawel Durow] form links
$ret = preg_replace('/\[(\w+)\|([^\]]+)\]/', "<a href='https://vk.com/$1'>$2</a>", $ret);
// attachments
if (isset($post['attachments'])) {
// level 1
foreach ($post['attachments'] as $attachment) {
if ($attachment['type'] == 'video') {
// VK videos
$title = e($attachment['video']['title']);
$photo = e($this->getImageURLWithLargestWidth($attachment['video']['image']));
$href = "https://vk.com/video{$attachment['video']['owner_id']}_{$attachment['video']['id']}";
$ret .= "<p><a href='{$href}'><img src='{$photo}' alt='Video: {$title}'><br/>Video: {$title}</a></p>";
} elseif ($attachment['type'] == 'audio') {
// VK audio
$artist = e($attachment['audio']['artist']);
$title = e($attachment['audio']['title']);
$ret .= "<p>Audio: {$artist} - {$title}</p>";
} elseif ($attachment['type'] == 'doc' and $attachment['doc']['ext'] != 'gif') {
// any doc apart of gif
$doc_url = e($attachment['doc']['url']);
$title = e($attachment['doc']['title']);
$ret .= "<p><a href='{$doc_url}'>Документ: {$title}</a></p>";
}
}
// level 2
foreach ($post['attachments'] as $attachment) {
if ($attachment['type'] == 'photo') {
// JPEG, PNG photos
// GIF in vk is a document, so, not handled as photo
$photo = e($this->getImageURLWithLargestWidth($attachment['photo']['sizes']));
$text = e($attachment['photo']['text']);
$ret .= "<p><img src='{$photo}' alt='{$text}'></p>";
} elseif ($attachment['type'] == 'doc' and $attachment['doc']['ext'] == 'gif') {
// GIF docs
$url = e($attachment['doc']['url']);
$ret .= "<p><img src='{$url}'></p>";
} elseif ($attachment['type'] == 'link') {
// links
$url = e($attachment['link']['url']);
$url = str_replace('https://m.vk.com', 'https://vk.com', $url);
$title = e($attachment['link']['title']);
if (isset($attachment['link']['photo'])) {
$photo = $this->getImageURLWithLargestWidth($attachment['link']['photo']['sizes']);
$ret .= "<p><a href='{$url}'><img src='{$photo}' alt='{$title}'><br>{$title}</a></p>";
} else {
$ret .= "<p><a href='{$url}'>{$title}</a></p>";
}
} elseif ($attachment['type'] == 'note') {
// notes
$title = e($attachment['note']['title']);
$url = e($attachment['note']['view_url']);
$ret .= "<p><a href='{$url}'>{$title}</a></p>";
} elseif ($attachment['type'] == 'poll') {
// polls
$question = e($attachment['poll']['question']);
$vote_count = $attachment['poll']['votes'];
$answers = $attachment['poll']['answers'];
$ret .= "<p>Poll: {$question} ({$vote_count} votes)<br />";
foreach ($answers as $answer) {
$text = e($answer['text']);
$votes = $answer['votes'];
$rate = $answer['rate'];
$ret .= "* {$text}: {$votes} ({$rate}%)<br />";
}
$ret .= '</p>';
} elseif ($attachment['type'] == 'album') {
$album = $attachment['album'];
$url = "https://vk.com/album{$album['owner_id']}_{$album['id']}";
$title = 'Альбом: ' . $album['title'];
$photo = $this->getImageURLWithLargestWidth($album['thumb']['sizes']);
$ret .= "<p><a href='{$url}'><img src='{$photo}' alt='{$title}'><br>{$title}</a></p>";
} elseif (!in_array($attachment['type'], ['video', 'audio', 'doc'])) {
$ret .= "<p>Unknown attachment type: {$attachment['type']}</p>";
}
}
}
return $ret;
}
protected function getImageURLWithLargestWidth($items)
{
usort($items, function ($a, $b) {
return $b['width'] - $a['width'];
});
return $items[0]['url'];
}
public function collectData()
{
if ($this->cache->get($this->rateLimitCacheKey)) {
throw new HttpException('429 Too Many Requests', 429);
}
$u = $this->getInput('u');
$ownerId = null;
// getting ownerId from url
$r = preg_match('/^(club|public)(\d+)$/', $u, $matches);
if ($r) {
$ownerId = -intval($matches[2]);
} else {
$r = preg_match('/^(id)(\d+)$/', $u, $matches);
if ($r) {
$ownerId = intval($matches[2]);
}
}
// getting owner id from API
if (is_null($ownerId)) {
$r = $this->api('groups.getById', [
'group_ids' => $u,
], [100]);
if (isset($r['response'][0])) {
$ownerId = -$r['response'][0]['id'];
} else {
$r = $this->api('users.get', [
'user_ids' => $u,
]);
if (count($r['response']) > 0) {
$ownerId = $r['response'][0]['id'];
}
}
}
if (is_null($ownerId)) {
returnServerError('Could not detect owner id');
}
$r = $this->api('wall.get', [
'owner_id' => $ownerId,
'extended' => '1',
]);
// preparing ownerNames dictionary
foreach ($r['response']['profiles'] as $profile) {
$this->ownerNames[$profile['id']] = $profile['first_name'] . ' ' . $profile['last_name'];
}
foreach ($r['response']['groups'] as $group) {
$this->ownerNames[-$group['id']] = $group['name'];
}
$this->generateFeed($r);
}
protected function generateFeed($r)
{
$ownerId = 0;
foreach ($r['response']['items'] as $post) {
if (!$ownerId) {
$ownerId = $post['owner_id'];
}
$item = new FeedItem();
$content = $this->generateContentFromPost($post);
if (isset($post['copy_history'])) {
if ($this->getInput('hide_reposts')) {
continue;
}
$originalPost = $post['copy_history'][0];
if ($originalPost['from_id'] < 0) {
$originalPostAuthorScreenName = 'club' . (-$originalPost['owner_id']);
} else {
$originalPostAuthorScreenName = 'id' . $originalPost['owner_id'];
}
$originalPostAuthorURI = 'https://vk.com/' . $originalPostAuthorScreenName;
$originalPostAuthorName = $this->ownerNames[$originalPost['from_id']];
$originalPostAuthor = "<a href='$originalPostAuthorURI'>$originalPostAuthorName</a>";
$content .= '<p>Репост (<a href="';
$content .= $this->getPostURI($originalPost);
$content .= '">Пост</a> от ';
$content .= $originalPostAuthor;
$content .= '):</p>';
$content .= $this->generateContentFromPost($originalPost);
}
$item->setContent($content);
$item->setTimestamp($post['date']);
$item->setAuthor($this->ownerNames[$post['from_id']]);
$item->setTitle($this->getTitle(strip_tags($content)));
$item->setURI($this->getPostURI($post));
$this->items[] = $item;
}
$this->pageName = $this->ownerNames[$ownerId];
}
protected function getTitle($content)
{
$content = explode('<br>', $content)[0];
$content = strip_tags($content);
preg_match('/^[:\,"\w\ \p{L}\(\)\?#«»\-\\—||&\.%\\₽\/+\;\!]+/mu', htmlspecialchars_decode($content), $result);
if (count($result) == 0) {
return 'untitled';
}
return $result[0];
}
protected function api($method, array $params, $expected_error_codes = [])
{
$access_token = $this->getOption('access_token');
if (!$access_token) {
returnServerError('You cannot run VK API methods without access_token');
}
$params['v'] = '5.131';
$r = json_decode(
getContents(
'https://api.vk.com/method/' . $method . '?' . http_build_query($params),
['Authorization: Bearer ' . $access_token]
),
true
);
if (isset($r['error']) && !in_array($r['error']['error_code'], $expected_error_codes)) {
if ($r['error']['error_code'] == 6) {
$this->cache->set($this->rateLimitCacheKey, true, 5);
} else if ($r['error']['error_code'] == 29) {
// wall.get has limit of 5000 requests per day
// if that limit is hit, VK returns error 29
$this->cache->set($this->rateLimitCacheKey, true, 60 * 30);
}
returnServerError('API returned error: ' . $r['error']['error_msg'] . ' (' . $r['error']['error_code'] . ')');
}
return $r;
}
}