forked from RSS-Bridge/rss-bridge
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Vk2Bridge] Alternative bridge for VK
- Loading branch information
Showing
2 changed files
with
364 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,323 @@ | ||
<?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\-\+\.\?\,\'\/&%\$#\=~\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 (!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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
Vk2Bridge | ||
========= | ||
|
||
Работа этого скрипта основана [VK API](https://dev.vk.com/reference). | ||
По сравнению с VkBridge у этого скрипта есть свои приемущества и недостатки. | ||
|
||
Приемущества | ||
------------ | ||
|
||
- Стабильность. | ||
Скрипт не зависит от HTML-структуры страницы VK групп или пользователей, которые могут поменяться в любой момент. | ||
|
||
Недостатки | ||
---------- | ||
|
||
- Требуется наличие зарегистированного в ВК пользователя. | ||
Данный пользователь должен получить `access_token`, который используется для этого скрипта. | ||
Подробнее в разделе "Настройка" | ||
|
||
- Количество запросов при выключенном кэше ограничено - [5000 запросов в сутки](https://dev.vk.com/ru/reference/roadmap#%D0%9E%D0%B3%D1%80%D0%B0%D0%BD%D0%B8%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%20API%20%D0%B4%D0%BB%D1%8F%20%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0) | ||
|
||
Настройка | ||
--------- | ||
|
||
1. Перейдите по [ссылке](https://oauth.vk.com/oauth/authorize?client_id=5149410&scope=offline&redirect_uri=https://oauth.vk.com/blank.html&display=page&response_type=token) | ||
|
||
2. Авторизуйтесь в приложение `my_personal_app` | ||
|
||
3. Получите ссылку вида `https://oauth.vk.com/blank.html#access_token=MNOGO_BUKAV&expires_in=0&user_id=123456`. | ||
Из этой ссылки скопируйте `MNOGO_BUKAV`. | ||
|
||
4. В `config.ini.php` в раздел Vk2Bridge вставьте `access_token` | ||
|
||
``` | ||
[Vk2Bridge] | ||
access_token = "MNOGO_BUKAV" | ||
``` | ||
|
||
Примечание: в данной инструкции используется приложение, администратор которого является [@em92](https://github.com/em92). | ||
Допускается вместо упомянутого приложения использование своего standalone-приложения. | ||
Для этого надо в ссылке из п.1. заменить значение `client_id` на свой. |