Бэкап сообщений Вконтакте с использованием VK API и Java 8

от
Java    vk api, java 8

За время использования vk.com накопилось немало диалогов, которые хотелось бы куда-то сохранить и изредка перечитывать. К тому же всегда интересно вспомнить с чего начиналась переписка с другом или подругой. Поэтому я решил написать для себя приложение, которое будет делать бэкап диалогов в html со всеми фотографиями, ссылками на видео и репостами.

Задачу можно разбить на два пункта:
1. Получение бэкапа диалогов.
2. Парсинг и вывод в нужный формат.
Цитата 16-фев-2017:
По многочисленным просьбам добавил скомпилированное приложение с текстовым файлом настроек.

Получение бэкапа диалогов
Вконтакте предоставляет свой API для работы приложений. Именно его мы и будем использовать для получения сообщений.
Первым делом нужно создать в вк своё приложение. Идём в раздел для разработчиков http://vk.com/dev и нажимаем справа вверху кнопку Создать приложение. Задаём название, например "Dialog Saver" и тип Standalone-приложение. Нажимаем "подключить приложение", вводим номер мобильного телефона, ждём SMS с кодом, вводим его и переключаемся на вкладку Настройки, где содержится необходимая нам информация - ID приложения. Сохраняем где-нибудь этот номер и переходим к созданию Java-приложения.

Создаём новый проект. Я буду использовать Java 8, чтобы показать всю его мощь и удобство.
Берём из полезных кодов или отсюда класс VkApi и закидываем в проект. Можно удалить методы getAlbums и getDialogs, они нам в этой задаче не нужны. Также можно убрать разрешение на доступ к фотографиям, в строке
  1. replace("{PERMISSIONS}", "photos,messages")
оставляем только messages.
Для работы библиотеки нужен ID приложения, которое мы создали и access token. Чтобы его получить нужно авторизоваться:
  1. public static void main(String[] args) throws IOException {
  2.     VkApi.with(APP_ID, null);
  3. }
После запуска откроется браузер со страницей подтверждения запросов приложения. Соглашаемся и копируем из адресной строки значение параметра access_token:
  vk_java8_1.png

Теперь можно полноценно работать с VK API.
Для получения списка сообщений в нужном диалоге, нам понадобится метод messages.getHistory. В вк хорошо описаны параметры всех функций, к тому же можно поиграть с параметрами прям на странице.
  vk_java8_2.png

В классе VkApi.java уже реализован этот метод. По аналогии можно сделать вызов любого другого.
Теперь нам надо достать всю переписку для указанного id пользователя. В методе getHistory есть ограничение - максимум 200 сообщений за раз, поэтому метод нужно вызывать циклично. Но тут срабатывает ещё одно ограничение - количество запросов в секунду не должно превышать 8-10, но и это лечится простой задержкой. Метод получения диалога с пользователем выглядит так:
  1. public static void save(String userId, int from, int to) throws IOException {
  2.     VkApi vkApi = VkApi.with(Config.APP_ID, Config.ACCESS_TOKEN);
  3.  
  4.     final int count = 200;
  5.     int offset = from;
  6.     while (offset < to) {
  7.         System.out.println(offset);
  8.         String text;
  9.         while (true) {
  10.             text = vkApi.getHistory(userId, offset, count, true);
  11.             if (!text.contains("Too many requests per second")) break;
  12.             System.out.println("Wait 1 second");
  13.             try {
  14.                 Thread.sleep(1000);
  15.             } catch (InterruptedException ex) {
  16.                 Thread.currentThread().interrupt();
  17.             }
  18.         }
  19.         write(offset, text);
  20.         offset += count;
  21.     }
  22. }
Количество сообщений всего можно узнать в поле count в примере запроса (для картинки, приведённой выше - 14664).
После вызова метода мы получим несколько десятков файлов в формате json.


Парсинг и вывод в нужный формат
Следующая часть нашей работы - парсинг полученных сообщений и вывод их в формат html.
Для работы с JSON нужна библиотека, качаем её из репозитория на GitHub и кидаем в папку проекта.
Далее нужно распарсить сообщения. Задача усложняется тем, что в сообщении могут быть различные вложения: картинки, документы, видео, записи со стены, музыка и так далее. Обрабатывать каждый из этих типов сложно и долго, поэтому на помощь нам приходит VK SDK для Android. В нём уже готовые классы для каждого объекта VK API, к тому же есть готовые методы для парсинга. Нам потребуется лишь пакет com.vk.sdk.api.model. Кидаем его в проект и в каждом классе удаляем все зависимости от стандартных Android-классов или создаём заглушки.

Теперь считываем какой-нибудь файл с данными в строку. В Java 8 это делается одной строчкой:
  1. String content = new String(Files.readAllBytes(fullPath), "UTF-8");
Парсим сообщения:
  1. JSONObject jsonObject = new JSONObject(content);
  2. VKList<VKApiMessage> messages = new VKList<>(jsonObject, VKApiMessage.class);
И выводим простенькую информацию - дата и тело сообщения.
  1. messages.stream().forEach(msg -> {
  2.     System.out.println("Date: " + LocalDateTime.ofEpochSecond(msg.date, 0, ZoneOffset.ofHours(+3)));
  3.     System.out.println(msg.body);
  4.     System.out.println();
  5. });
Больше никаких циклов! Получаем stream и работаем с ним в функциональном стиле.
Если нужно вывести только 5 входящих сообщений после 2012 года, не беда - используем фильтр:
  1. long from = LocalDateTime.of(2012, Month.JANUARY, 1, 0, 0).toEpochSecond(ZoneOffset.ofHours(+3));
  2. messages.stream()
  3.         .filter(msg -> !msg.out)
  4.         .filter(msg -> msg.date > from)
  5.         .limit(5)
  6.         .forEach(msg -> System.out.println(msg.body));
С использованием for эта задача решалась бы намного дольше.

Но, продолжим. Мы можем вывести сообщения, дату, проверить входящее сообщение или исходящее. Кроме того, класс VkApiMessage содержит список вложений или репостов и ещё кучу полезной информации. Мы собирались вывести всё в HTML, поэтому направим поток вывода в файл:
  1. final String outPath = fullPath + ".html";
  2. System.setOut(new PrintStream(new File(outPath)));
Теперь операции вывода System.out.print будут записывать всё в файл.

Разберёмся с вложениями. Для каждого из типов вложений есть свой класс, наследованный от VKAttachments.VKApiAttachment. Создадим в VkApiAttachment метод
  1. public CharSequence toHtml() {
  2.     return toAttachmentString();
  3. }
И переопределим в каждом из вложений этот метод.
Фотографии. Выводим маленькую картинку и ссылку на оригинал.
  1. @Override
  2. public CharSequence toHtml() {
  3.     final int size = src.size();
  4.     VKApiPhotoSize small = src.get(0);
  5.     if (size > 1) small = src.get(1);
  6.     VKApiPhotoSize big = src.get(size - 1);
  7.     return String.format("<a href='%s'><img src='%s'/></a>", big.src, small.src);
  8. }

Видео. Выводим превью и ссылку на просмотр с заголовком и длительностью.
  1. @Override
  2. public CharSequence toHtml() {
  3.     String preview = photo.get(0).src;
  4.     String url = "http://vk.com/" + TYPE_VIDEO + owner_id + "_" + id;
  5.     String d = TextUtils.formatDuration(duration);
  6.     return String.format("<a href='%s'><img src='%s'/><br/>%s (%s)</a>", url, preview, title, d);
  7. }

И так далее для документов, музыки, постов. Поигравшись со стилями, можно прийти к такому результату
  vk_java8_3.png

Интересно получилось с пересланными сообщениями. Как известно, они имеют вложенную структуру, значит надо вызывать метод парсинга рекурсивно. Но в Java 8 можно поступить проще. Всё, что внутри forEach (лямбды), можно поместить в отдельный класс
  1. private static class MessageConsumer implements Consumer<VKApiMessage> {
  2.     @Override
  3.     public void accept(VKApiMessage message) {}
  4. }
Тогда в методе accept будет происходить всё то, что было бы в теле forEach или в цикле.
Используем так:
  1. messages.stream().forEach(new MessageConsumer());
Так вот, в VkApiMessage есть поле fwd_messages, в котором хранится список из объектов пересланных сообщений VkApiMessage. Чтобы вывести эти сообщения на экран достаточно одной строки (или в случае проверки чуть более):
  1. if (!message.fwd_messages.isEmpty()) {
  2.     System.out.println("<div class='wall'>");
  3.     message.fwd_messages.forEach(new MessageConsumer());
  4.     System.out.println("</div>");
  5. }
Результат выглядит ничем не хуже оригинала в вк:
  vk_java8_4.png

Ещё один интересный момент - смайлы Emoji. В тексте они выглядят как нечитаемые символы с кодами вида D83DDCAC, D83CACDC. Мне было откровенно лень самому разбираться с их перекодировкой, поэтому я решил декомпилировать официальное приложение Вконтакте для Android и взять код оттуда.
Качаем последнюю версию клиента, dex2jar и нормальный декомпилятор. Конвертируем apk в jar с помощью dex2jar, затем декомпилируем класс com.vkontakte.android.Global и находим метод replaceEmoji. Немного подправив, получаем готовый класс для замены символов смайлов на их картинки. Результат:
  vk_java8_5.png

Вот, собственно и всё, теперь осталось прочитать все файлы сообщений, разбить по годам, чтобы не было большого файла и сохранить всё в html.
Вот только проблема, файлы нужно отсортировать не в алфавитном порядке (0, 1000, 1200, ..., 200, 2000, ...), а в числовом (0, 200, 400, 600, 800, 1000, 1200, ...). Благо, всё уже давно за нас написано. Вот тут The Alphanum Algorithm можно скачать исходник компаратора под любой язык.
Вот теперь можно считывать все файлы, парсить и добавлять в список всех сообщений. В Java 8 это не занимает много места:
  1. VKList<VKApiMessage> messages = new VKList<>();
  2. Files.list(Paths.get(Config.WORK_DIR))
  3.         .filter(p -> p.toString().endsWith(".txt"))
  4.         .sorted(new AlphanumComparator())
  5.         .forEach(p -> messages.addAll(readMessages(p)));
Что здесь происходит? Получаем путь к указанной папке, затем получаем список файлов в ней, затем фильтруем только те файлы, которые заканчиваются на txt. После этого сортируем список файлов и уже после этого добавляем распарсенные сообщения в общий список.
Теперь в messages у нас абсолютно все сообщения. Можно поиграться с этими данными, например отобрать лишь те, в которых есть аудиозаписи:
  1. VKList<VKApiMessage> query = new VKList<>(messages.stream()
  2.         .filter(msg -> hasAttachment(msg.attachments, VKAttachments.TYPE_AUDIO))
  3.         .collect(Collectors.toList()));
  4.  
  5. private static boolean hasAttachment(VKAttachments attachments, String type) {
  6.     return attachments.stream().anyMatch(a -> a.getType().equals(type));
  7. }

Сообщения с текстом "как дела?":
  1. VKList<VKApiMessage> query = new VKList<>(messages.stream()
  2.         .filter(msg -> msg.body.toLowerCase().contains("как дела?"))
  3.         .collect(Collectors.toList()));

Теперь сгруппируем все сообщения по годам и сохраним в отдельный файл.
  1. for (int year = 2010; year < 2015; year++) {
  2.     LocalDateTime from = LocalDateTime.of(year, Month.JANUARY, 1, 0, 0);
  3.     LocalDateTime to = from.plusYears(1);
  4.     VKList<VKApiMessage> query = new VKList<>(messages.stream()
  5.             .filter(msg -> getDateTime(msg.date).isAfter(from)
  6.                       && getDateTime(msg.date).isBefore(to))
  7.             .collect(Collectors.toList()));
  8.     generate(query, String.valueOf(year));
  9. }

После пятисекундной работы приложения в папке появится 5 файлов с сообщениями за конкретный год. Теперь можно открыть страницы в Google Chrome, нажать Ctrl+P и сохранить страницу в PDF.

Исходники проекта: DialogSaver.zip
Не забудьте вставить main.css в папку styles рабочей директории.

Готовое приложение: VkDialogSaver_app.zip
В настройках config.txt указывайте APP_ID, id диалога и нужный режим.
Режим get - получает бэкап сообщений в сырой формат json.
Режим generate - генерирует из ранее полученных сообщений html-страницы.
  • +13
  • views 35145