C# / .NET Core, публикация ВКонтакте
Публикацию в Twitter и Facebook мы уже запилили, настала очередь ВКонтакте.
Тут, конечно, получше дела обстоят, чем с Facebook, но местами всё равно пиздец.
Итак, наша цель - публиковать контент с сайта на стене публичной страницы (паблика) ВКонтакте. Пост должен содержать картинку, текст, хэштеги и ссылку. Выглядеть будет примерно так:
Делать будем через API ВКонтакте с реализацией на C# (из .NET Core MVC приложения).
Официальная документация весьма неплохо рассказывает о том что тут и как. Первым делом нужно создать приложение и получить через него токен доступа, который необходим для отправки запросов к API.
Платформа приложения должна быть Standalone
:
После создания открыть его настройки и изменить через Состояние
на включённое (я ещё Open API
добавил, хотя и не уверен, нужно ли это вообще):
Из всего этого нам нужен только ID приложения
.
Теперь можно получать токен. Откройте такую ссылку в браузере:
https://oauth.vk.com/authorize
?v=5.73
&client_id=YOUR-APPLICATION-ID
&display=page
&redirect_uri=https://oauth.vk.com/blank.html
&scope=wall,offline,groups,photos
&response_type=token
в которой:
YOUR-APPLICATION-ID
- ваш ID приложения;wall,offline,groups,photos
- так называемый scope, то есть что приложение будет уметь делать от имени вашего аккаунта. Кстати, возможно, я ему тут лишнего наразрешал. У меня там ещёmanage
был, но он-то вроде точно не нужен. Обратите внимание наoffline
- без него токен будет со сроком действия и через какое-то время протухнет.
Ладно, открываете ссылку в браузере, ВКонтакте покажет диалог подтверждения авторизации:
По нажатию на Разрешить
, откроется пустая страница, но нам нужно не её содержимое, а её адрес, вернее параметр access_token
:
https://oauth.vk.com/blank.html
#access_token=YOUR-ACCESS-TOKEN
&expires_in=0
&user_id=YOUR-ID
Всё, теперь можно отправлять запросы к API.
Как вообще составлять запросы, рассказывает документация. Среди параметров есть маленький, но, сука, гордый параметр версии API - v
, в котором нужно обязательно указать версию API. В нормальных API, если не указана конкретная версия, по умолчанию берётся свежайшая, но API ВКонтакте к нормальным относиться не планировал, как я понимаю. Хочешь использовать последнюю версию - иди ищи её вручную.
Чтобы постить на стене паблика (от имени паблика), вы должны быть его администратором или редактором. Проверить доступ к сообществам можно таким запросом:
https://api.vk.com/method/groups.get
?v=5.73
&access_token=YOUR-ACCESS-TOKEN
&filter=admin
Пример результата:
{
"response": {
"count": 1,
"items": [
YOUR-GROUP-ID
]
}
}
Я админ только в одной группе (которая и есть мой паблик), потому у меня тут только один результат. У вас может быть больше, разумеется.
На всякий случай, сравните ID из этого списка с реальным ID паблика, найти который можно открыв его страницу и нажав Записи сообщества
- в адресе страницы будет ID. Весьма тривиально, не правда ли.
Теперь можно постить на стену:
https://api.vk.com/method/wall.post
?v=5.73
&access_token=YOUR-ACCESS-TOKEN
&owner_id=-YOUR-GROUP-ID
&from_group=1
&message=TEXT
где:
-YOUR-GROUP-ID
- ID паблика. Здесь важно, чтобы он был со знаком-
. А вот во всех остальных запросах он будет без минуса. Как эту логику понимать?TEXT
- текст для публикации на стене паблика.
Этот запрос должен опубликовать запись на стене паблика. Я сначала по инерции передал ID приложения, а не паблика, потому получил:
Access denied: no access to call this method
Теперь как загружать картинку. ВКонтакте не искали лёгких путей при проектировании API, потому процесс состоит из трёх шагов:
- Получить сервер, на который будет загружаться картинка;
- Загрузить картинку на этот сервер;
- Сохранить картинку.
Всё просто пронизано заботой о разработчике (а также логикой). Но оставим психическое здоровье создателей в стороне и перейдём к реализации этого безумия на C#.
Продраться сквозь гениальный замысел сумрачного гения ВКонтакте помогла вот эта статья, хотя там и речь про загрузку фотографии в альбом, а не на стену.
Ну так вот, сначала через метод photos.getWallUploadServer надо получить сервер для загрузки картинки (фотографии). Да, вот так, API не может само как-то разобраться в своих серверах, потому для этого нужно выполнять аж целый отдельный запрос. Вот такой:
https://api.vk.com/method/photos.getWallUploadServer
?v=5.73
&access_token=YOUR-ACCESS-TOKEN
&group_id=YOUR-GROUP-ID
В ответ придёт следующее:
{
"response": {
"upload_url": "https:\/\/pu.vk.com\/c847219\/upload.php?act=do_add&mid=YOUR-ID&aid=-ALBUM-ID&gid=YOUR-GROUP-ID&hash=HASH-VALUE&rhash=RHASH-VALUE&swfupload=1&api=1&wallphoto=1",
"album_id": ALBUM-ID,
"user_id": YOUR-ID
}
}
Отсюда нам нужна вся строка из upload_url
- на этот URL и надо отправлять картинку в POST запросе. И хотя процесс описан в документации, с реальностью это описание слегка расходится. Вот цитата оттуда:
Передайте файл на адрес upload_url, полученный в предыдущем пункте, сформировав POST-запрос с полем photo. Это поле должно содержать изображение в формате multipart/form-data.
То есть, нужно просто запилить POST запрос с полем photo
, в котором будут лежать байты картинки? Окей, давайте попробуем:
// uploadServer - URL сервера, полученный на предыдущем шаге
// pathToImage - где у нас лежит файл с картинкой
// читаем содержимое файла картинки
byte[] imgdata = System.IO.File.ReadAllBytes(pathToImage);
var imageContent = new ByteArrayContent(imgdata);
// создаём тело запроса в формате multipart/form-data
var multipartContent = new MultipartFormDataContent();
// имя параметра "photo", как и просили
multipartContent.Add(imageContent, "photo");
using (var httpClient = new HttpClient())
{
// отправляем POST запрос
var httpResponse = await httpClient.PostAsync(uploadServer, multipartContent);
// вот сюда придёт JSON ответа
string httpContent = await httpResponse.Content.ReadAsStringAsync();
}
Кстати говоря, Twitter API принимает файлы таким же запросом, и там этого хватает без проблем, но API ВКонтакте - зверь особой породы.
Начнём с того, что вы получите вот такую ошибку в лицо:
The character set provided in ContentType is invalid. Cannot read content as string using an invalid character set.
Что бы это могло означать? А то, что в 2018 году сраный ВКонтач отдаёт результат в кодировке Windows-1251
! И причём только вот в этом методе (в других не заметил), остальные выдают нормальный UTF-8. В документации такое поведение хуй упомянуто, то есть кодировку ответа каждого метода предлагается угадывать самостоятельно.
Ну ладно, будем работать с Windows-1251
:
using (var httpClient = new HttpClient())
{
var httpResponse = await httpClient.PostAsync(server, multipartContent);
var httpContent = await httpResponse.Content.ReadAsByteArrayAsync();
// вот в этой строке теперь будет JSON ответа
string responseString = Encoding.GetEncoding(1251).GetString(
httpContent, 0, httpContent.Length
);
}
При сборке вы скорее всего вы получите такую ошибку:
No data is available for encoding 1251. For information on defining a custom encoding, see the documentation for the Encoding.RegisterProvider method.
Надо поставить NuGet пакет:
dotnet add package System.Text.Encoding.CodePages
И добавить провайдер в Startup.cs
(или в Main
, где у вас там):
public void ConfigureServices(IServiceCollection services)
{
// ...
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
// ...
}
С кодировкой справились, теперь наконец-то можно посмотреть ответ от API:
{
"server": SERVER-ID,
"photo": "[]",
"hash": "SOME-HASH"
}
Ошибок нет, значит это наш результат, правильно? Смотрим в документацию:
После успешной загрузки сервер возвращает в ответе JSON-объект с полями server, photo, hash...
Ну так и есть, вот мы получили ID сервера, получили хэш, но… почему поле photo
пустое? Судя по документации, там должен быть JSON-массив с информацией по фото, но почему там ничего нет? И почему ничего не сказано о том, что возвращается в случае неуспешной загрузки? Так, подождите… неужели… это бля и есть неуспешная загрузка?! И значит пустой элемент photo
означает ошибку? Какое охуенное, блядь, API.
Ну так и оказалось - если вам пришёл такой результат, то API что-то не понравилось в вашем запросе, и бря удачи в поисках что именно.
Нет, серьёзно, как из []
получить текст ошибки или хоть какой-то намёк на причину? Да никак, ебитесь в рот, ваш ВКонтакт. Единственное, что оставалось делать, это гуглить вариации “ВКонтакте API пустое photo”.
Поиски неистово отягощались тем, что подавляющее большинство вопросов и примеров в интернетах посвящены PHP-реализациям неосиляторов, которые сидят на Wordpress’е и тупо шлют запросы через cURL. Кроме PHP я видел даже пару примеров на C++, но ни одного на C#. И разумеется, нигде нет никаких конкретных решений, потому что из описания проблемы у всех только кофейная гуща, разлитая внутри квадратных скобок.
Убив чуть не целый день на поиски, я уже даже не помню где встретил какой-то пример отправки файла, в котором кроме содержимого файла передавалось также и его имя(!).
Я проверил это у себя:
var multipartContent = new MultipartFormDataContent();
multipartContent.Add(imageContent, "photo", "some.png");
И таки да, это и оказалось причиной. Какого же хера это не указано в документации?! И не возвращается в качестве ошибки?! А главное, я бы может понял, если бы имя файла хоть где-то потом использовалось, но что-то я нигде не нашёл его следов после загрузки. Кроме того, для всех загружаемых файлов можно передавать одно и то же имя (например, vkontakteebanytoeapi.png
), и это ни на что не повлияет. Пиздец.
…Вот теперь результат возвращается в том виде, в каком и было обещано:
{
"server": SERVER-ID,
"photo": "[{\"photo\":\"85602013a2:x\",\"sizes\":[[\"s\",924020981,\"9d1e\",\"dcvjUHL9SEs\",75,75],[\"m\",924020981,\"9d1f\",\"H_rKPVP2wrs\",130,130],[\"x\",924020981,\"9d20\",\"-jYMssa2aAc\",300,300],[\"o\",924020981,\"9d21\",\"o9ta1uV_0ik\",130,130],[\"p\",924020981,\"9d22\",\"ZCsZ--h1nlY\",200,200],[\"q\",924020981,\"9d23\",\"6vn_QEFgd4Y\",300,300],[\"r\",924020981,\"9d24\",\"6p4zQpQBfxU\",300,300]],\"latitude\":0,\"longitude\":0,\"kid\":\"80e1clo3ef1b24d2s6d2a6l41pe0d26b9\"}]",
"hash": "SOME-HASH"
}
Если интересно, вот как выглядят запрос и результат в “сыром” виде (хотя всё самое интересное скрыто в <Binary body>
):
Остался последний шаг - сохранить (г’споди, что за идиотизм) уже загруженную картинку в куда-то там через метод photos.saveWallPhoto, используя значения из полученных результатов отправки. Запрос такой:
https://api.vk.com/method/photos.saveWallPhoto
?v=5.73
&access_token=YOUR-ACCESS-TOKEN
&group_id=YOUR-GROUP-ID
&photo=PHOTO-VALUE
&server=SERVER-ID
&hash=SOME-HASH
Результат будет выглядеть так:
{
"response": [
{
"id": PHOTO-ID,
"album_id": -14,
"owner_id": YOUR-ID,
"photo_75": "https://pp.userapi.com/g745593/b946739573/e366/rdjtBFuL0AU.jpg",
"photo_130": "https://pp.userapi.com/g745593/b946739573/e367/F0YFn9dcxYw.jpg",
"photo_604": "https://pp.userapi.com/g745593/b946739573/e368/PPKmXvdSSHc.jpg",
"photo_807": "https://pp.userapi.com/g745593/b946739573/e369/XQ9yKDshx-k.jpg",
"photo_1280": "https://pp.userapi.com/g745593/b946739573/e36a/OdfOPoXXz3k.jpg",
"width": 1000,
"height": 1000,
"text": "",
"date": 1521917953,
"access_key": "n3e12036c6dOeQQ4b8"
}
]
}
Почти всё готово, но напоследок ВКонтакте предлагает ещё и пособирать пазл, потому что ведь нельзя же просто так взять и вернуть финальный пригодный для использования ID картинки. Нет, требуемое значение нужно собрать руками по следующей формуле: photo[owner_id]_[id]
, то есть photoYOUR-ID_PHOTO-ID
. И вот это уже то самое значение, которое можно использовать для публикации поста с картинкой на стене паблика:
https://api.vk.com/method/wall.post
?v=5.73
&access_token=YOUR-ACCESS-TOKEN
&owner_id=-YOUR-GROUP-ID
&from_group=1
&message=TEXT
&attachments=photoYOUR-ID_PHOTO-ID
В параметр attachments
можно также передать ссылку (прям втупую через запятую после фотки), но в пост она будет поставлена весьма по-уродски, так что нет, спасибо. К тому же никто не запрещает использовать ссылки в тексте поста, и они тоже будут кликабельными.
Весь C#-класс я на этот раз в статью пихать не буду, так что смотрите его отдельно.
Хочу обратить ваше внимание на разницу между GET
и POST
запросами. Я сначала всё делал через GET
, то есть в исходниках у меня было так:
public Task<Tuple<int, string>> WallPostWithPhoto(string post, string photoID)
{
post = WebUtility.UrlEncode(post);
return PostToWall(post, photoID);
}
async Task<Tuple<int, string>> PostToWall(string post, string photoID)
{
using (var httpClient = new HttpClient())
{
var httpResponse = await httpClient.GetAsync(
$"{_postToWallURL}&from_group=1&message={post}&attachments={photoID}"
);
var httpContent = await httpResponse.Content.ReadAsStringAsync();
return new Tuple<int, string>(
(int)httpResponse.StatusCode,
httpContent
);
}
}
И однажды я был за это наказан ошибкой:
<html>
<head><title>414 Request-URI Too Long</title></head>
<body bgcolor="white">
<center><h1>414 Request-URI Too Long</h1></center>
<hr><center>nginx/0.3.33</center>
</body>
</html>
Потому что такие вещи должны делаться через POST
, а не через GET
.
Исправленная версия запроса:
public Task<Tuple<int, string>> WallPostWithPhoto(string post, string photoID)
{
var postData = new Dictionary<string, string> {
{ "v", _APIver },
{ "owner_id", $"-{_PageID}" },
{ "access_token", _AccessToken},
{ "from_group", "1" },
{ "message", post },
{ "attachments", photoID }
};
return PostToWall(postData);
}
async Task<Tuple<int, string>> PostToWall(Dictionary<string, string> postData)
{
using (var httpClient = new HttpClient())
{
var httpResponse = await httpClient.PostAsync(
$"{_API}{_APImethodPost}",
new FormUrlEncodedContent(postData)
);
var httpContent = await httpResponse.Content.ReadAsStringAsync();
return new Tuple<int, string>(
(int)httpResponse.StatusCode,
httpContent
);
}
}
Я, кстати говоря, написал ВКонтакте обо всём этом бардаке, но так как спустя месяц ничего не исправлено, то можно сделать вывод, что они просто ложили хуй.
Social networks
Zuck: Just ask
Zuck: I have over 4,000 emails, pictures, addresses, SNS
smb: What? How'd you manage that one?
Zuck: People just submitted it.
Zuck: I don't know why.
Zuck: They "trust me"
Zuck: Dumb fucks