diff --git a/app/src/main/java/com/vk/im/ui/components/common/DialogAction.java b/app/src/main/java/com/vk/im/ui/components/common/DialogAction.java index 1079e7ef8..844757425 100644 --- a/app/src/main/java/com/vk/im/ui/components/common/DialogAction.java +++ b/app/src/main/java/com/vk/im/ui/components/common/DialogAction.java @@ -35,5 +35,6 @@ public enum DialogAction { pinmsg, unpinmsg, ENCRYPT_SETT, - ENCRYPT + ENCRYPT, + DOWNLOAD } diff --git a/app/src/main/java/ru/vtosters/lite/dnr/DNRInjector.java b/app/src/main/java/ru/vtosters/lite/dnr/DNRInjector.java index 1253c2933..31d76ff25 100644 --- a/app/src/main/java/ru/vtosters/lite/dnr/DNRInjector.java +++ b/app/src/main/java/ru/vtosters/lite/dnr/DNRInjector.java @@ -6,6 +6,7 @@ import static com.vk.im.engine.h.im_ic_more_vertical; import static com.vk.im.engine.h.im_ic_pinned_msg_hide; import static com.vk.im.engine.h.im_ic_pinned_msg_show; import static com.vk.im.engine.h.im_ic_write_msg; +import static com.vk.im.ui.c.im_ic_chat_settings; import static com.vk.im.ui.c.im_ic_keyboard; import static ru.vtosters.lite.dnr.DNRModule.hookDNR; import static ru.vtosters.lite.dnr.DNRModule.hookDNT; @@ -16,9 +17,11 @@ import static ru.vtosters.lite.proxy.ProxyUtils.getApi; import static ru.vtosters.lite.utils.AccountManagerUtils.getUserToken; import static ru.vtosters.lite.utils.AndroidUtils.getIdentifier; import static ru.vtosters.lite.utils.AndroidUtils.sendToast; +import static ru.vtosters.lite.utils.Preferences.getDownloadsDir; import android.annotation.SuppressLint; import android.content.Context; +import android.os.Environment; import android.util.Log; import com.vk.core.network.Network; @@ -34,6 +37,7 @@ import com.vk.im.ui.components.common.MsgAction; import com.vk.im.ui.components.viewcontrollers.popup.DelegateMsg; import com.vk.im.ui.views.dialog_actions.DialogActionsListView; +import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -42,6 +46,7 @@ import java.util.List; import okhttp3.Headers; import okhttp3.OkHttpClient; import okhttp3.Request; +import ru.vtosters.lite.downloaders.MessagesDownloader; import ru.vtosters.lite.encryption.EncryptProvider; import ru.vtosters.lite.encryption.base.IMProcessor; import ru.vtosters.lite.hooks.CryptImHook; @@ -56,6 +61,8 @@ public class DNRInjector { peerId = dialog.getId(); } + list.add(DialogAction.DOWNLOAD); + list.add(DialogAction.pinmsg); list.add(DialogAction.unpinmsg); @@ -79,6 +86,8 @@ public class DNRInjector { } public static LinkedHashMap injectToHashMap(LinkedHashMap hashMap) { + hashMap.put(DialogAction.DOWNLOAD, getIdentifier("download_dl", "string")); + hashMap.put(DialogAction.DNR_ON, getIdentifier("DNR_ON", "string")); hashMap.put(DialogAction.DNR_OFF, getIdentifier("DNR_OFF", "string")); hashMap.put(DialogAction.DNT_ON, getIdentifier("DNT_ON", "string")); @@ -95,6 +104,7 @@ public class DNRInjector { @SuppressLint("ResourceType") public static List injectToList(List actions) { var list = new ArrayList<>(actions); + list.add(new DialogActionsListView.b.a(DialogAction.DOWNLOAD, 1, im_ic_chat_settings, getIdentifier("download_dl", "string"))); // DialogAction, Int, Icon, String list.add(new DialogActionsListView.b.a(DialogAction.DNR_ON, 2, im_ic_pinned_msg_hide, getIdentifier("DNR_ON", "string"))); // DialogAction, Int, Icon, String list.add(new DialogActionsListView.b.a(DialogAction.DNR_OFF, 2, im_ic_pinned_msg_show, getIdentifier("DNR_OFF", "string"))); // DialogAction, Int, Icon, String @@ -123,6 +133,8 @@ public class DNRInjector { peerId = dialog.getId(); } + actions.add(DialogAction.DOWNLOAD); + if (isDnrEnabledFor(peerId)) { actions.add(DialogAction.DNR_OFF); } else { @@ -157,6 +169,16 @@ public class DNRInjector { forceInvalidateDialogActions(dialog); } + if (action == DialogAction.DOWNLOAD) { + File out = new File(Environment.getExternalStoragePublicDirectory(getDownloadsDir().getName()), peerId + "-dialog.html"); + try { + new MessagesDownloader().downloadDialog(peerId, new MessagesDownloader.HtmlDialogDownloaderFormatProvider(), out); + } catch (Exception e) { + e.printStackTrace(); + } + return true; + } + if (action == DialogAction.DNR_ON) { setDnr(dialog, true); forceInvalidateDialogActions(dialog); @@ -199,6 +221,16 @@ public class DNRInjector { DNRModule.hookRead(dialog); } + if (action == DialogAction.DOWNLOAD) { + File out = new File(Environment.getExternalStoragePublicDirectory(getDownloadsDir().getPath()), peerId + "-dialog.html"); + try { + new MessagesDownloader().downloadDialog(peerId, new MessagesDownloader.HtmlDialogDownloaderFormatProvider(), out); + } catch (Exception e) { + e.printStackTrace(); + } + return true; + } + if (action == DialogAction.DNR_ON) { setDnr(dialog, true); return true; diff --git a/app/src/main/java/ru/vtosters/lite/downloaders/MessagesDownloader.java b/app/src/main/java/ru/vtosters/lite/downloaders/MessagesDownloader.java new file mode 100644 index 000000000..2c96b77f1 --- /dev/null +++ b/app/src/main/java/ru/vtosters/lite/downloaders/MessagesDownloader.java @@ -0,0 +1,286 @@ +package ru.vtosters.lite.downloaders; +import static ru.vtosters.lite.encryption.EncryptProvider.decryptMessage; +import static ru.vtosters.lite.net.Request.makeRequest; +import static ru.vtosters.lite.proxy.ProxyUtils.getApi; +import static ru.vtosters.lite.utils.AccountManagerUtils.getUserId; +import static ru.vtosters.lite.utils.AccountManagerUtils.getUserToken; +import static ru.vtosters.lite.utils.AndroidUtils.getGlobalContext; +import static ru.vtosters.lite.utils.AndroidUtils.getResources; + +import android.util.SparseArray; +import android.widget.Toast; + +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Objects; + +public class MessagesDownloader{ + private static SimpleDateFormat sdformat; + + public static String formatTime(long now){ + return sdformat.format(new Date(now)); + } + + public static String join(CharSequence delimiter, Iterable elements){ + Objects.requireNonNull(delimiter); + Objects.requireNonNull(elements); + + StringBuilder builder = new StringBuilder(); + + for (CharSequence cs : elements) { + builder.append(cs).append(delimiter); + } + + return builder.toString(); + } + + public void downloadDialog(int peer_id, DialogDownloaderFormatProvider format, File out) throws Exception{ + sdformat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss", getResources().getConfiguration().locale); + + FileOutputStream fos = new FileOutputStream(out); + + // fix TODO with conversation name (API method messages.getConversationsById) + fos.write(format.provideDocumentStart("TODO", formatTime(System.currentTimeMillis())).getBytes(StandardCharsets.UTF_8)); + + fetchDialogInfo(peer_id, 0, ((users, messages) -> { + try { + fos.write(format.provideHeader("TODO", formatTime(System.currentTimeMillis())).getBytes(StandardCharsets.UTF_8)); + for (MiniMsg msg : messages) { + fos.write(format.provideMessage(msg, users.get(msg.fromId)).getBytes(StandardCharsets.UTF_8)); + } + fos.write(format.provideDocumentEnd().getBytes(StandardCharsets.UTF_8)); + fos.close(); + Toast.makeText(getGlobalContext(), "Сохранено как " + out.getAbsolutePath(), Toast.LENGTH_SHORT).show(); + } catch (IOException | JSONException e) { + e.printStackTrace(); + } + })); + } + + public void fetchDialogInfo(int peer_id, int offset, FetchListener fl){ + makeRequest("https://" + getApi() + "/method/messages.getHistory?extended=1&v=5.140&offset=" + offset + "&count=200&peer_id=" + peer_id + "&access_token=" + getUserToken(), + response -> { + try { + JSONObject mainJson = new JSONObject(response); + JSONObject obj = mainJson.getJSONObject("response"); + + SparseArray users = new SparseArray<>(); + parseUsers(users, obj.optJSONArray("profiles"), false); + parseUsers(users, obj.optJSONArray("groups"), true); + + List msgs = parseMessages(obj.getJSONArray("items")); + + fl.onReceive(users, msgs); + } catch (JSONException e) { + e.printStackTrace(); + } + }); + } + + private List parseMessages(JSONArray jsonArray) throws JSONException{ + ArrayList thing = new ArrayList<>(); + + for (int i = 0; i < jsonArray.length(); i++) { + MiniMsg msg = new MiniMsg(jsonArray.getJSONObject(i)); + thing.add(msg); + } + + return thing; + } + + private void parseUsers(SparseArray array, @Nullable JSONArray jsonArray, boolean groups) throws JSONException{ + if (jsonArray == null) return; + + for (int i = 0; i < jsonArray.length(); i++) { + MiniUser user = new MiniUser(jsonArray.getJSONObject(i)); + array.put(groups ? -user.id : user.id, user); + } + } + + interface FetchListener{ + void onReceive(SparseArray users, List messages); + } + + static class MiniUser{ + int id; + String firstName; + String lastName; + String photo100; + + public MiniUser(JSONObject obj) throws JSONException{ + id = obj.getInt("id"); + firstName = obj.optString("first_name", obj.optString("name")); + lastName = obj.optString("last_name", ""); + photo100 = obj.getString("photo_100"); + } + } + + static class MiniMsg{ + long date; + String text; + int fromId; + List attachments; + + public MiniMsg(JSONObject obj) throws JSONException{ + date = obj.getLong("date") * 1000; + fromId = obj.getInt("from_id"); + text = decryptMessage(obj.getString("text"), obj.getInt("peer_id")); + + attachments = new ArrayList<>(); + + JSONArray arr = obj.optJSONArray("attachments"); + if (arr == null) return; + for (int i = 0; i < arr.length(); i++) attachments.add(arr.getJSONObject(i)); + } + } + + // This class defines everything which is needed to create an output file + abstract static class DialogDownloaderFormatProvider{ + // Internal, for JSON/HTML + abstract String provideDocumentStart(String dialogName, String date); + + abstract String provideDocumentEnd(); + + abstract String provideHeader(String dialogName, String date); + + abstract String provideMessage(MiniMsg message, MiniUser user) throws JSONException; + } + + public static class HtmlDialogDownloaderFormatProvider extends DialogDownloaderFormatProvider{ + @Override + String provideDocumentStart(String dialogName, String date){ + String style = ".vtex-milk-msg {\n" + + " border-radius: 8px;\n" + + " padding: 24px;\n" + + " background: white;\n" + + " margin: 16px;\n" + + " box-shadow: 0 4px 8px 0 rgba(0,0,0,0.07);\n" + + "}\n" + + "\n" + + ".vtex-milk-msgcont br {\n" + + " display: none;\n" + + "}\n" + + "\n" + + ".vtex-milk-msg p {\n" + + " margin: 0;\n" + + "}\n" + + "\n" + + ".vtex-milk-msg.vtex-milk-msg-out {\n" + + " background: #ADD3FF;\n" + + "}\n" + + "\n" + + "p.msg-attaches {\n" + + " margin-top: 8px;\n" + + " font-style: italic;\n" + + "}\n" + + "\n" + + ".vtex-milk-header {\n" + + " border-radius: 8px;\n" + + " padding: 24px;\n" + + " background: white;\n" + + " margin: 16px;\n" + + " box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);\n" + + "}\n" + + "\n" + + "h4.vtex-milk-header-txt {\n" + + " margin: 0;\n" + + "}\n" + + "\n" + + "h5.vtex-milk-header-sub {\n" + + " margin: 0;\n" + + " padding-top: 8px;\n" + + "}\n" + + "\n" + + "a.msg-attach-link {\n" + + " color: #5181B8;\n" + + " text-decoration: none;\n" + + "}\n" + + "\n" + + ".msg-from a {\n" + + " color: #5181B8;\n" + + " text-decoration: none;\n" + + "}\n" + + "\n" + + ".msg-from-date {\n" + + " color: rgba(0, 0, 0, 0.75);\n" + + "}\n" + + "\n" + + "body {\n" + + " font-family: Roboto, system-ui, sans-serif;\n" + + "}\n" + + "\n" + + ".msg-body {\n" + + " padding-top: 4px;\n" + + "}\n" + + "\n" + + ""; + + return "Экспорт из VTLite"; + } + + @Override + String provideDocumentEnd(){ + return ""; + } + + @Override + String provideHeader(String dialogName, String date){ + return "

VTLite

Экспорт истории чата (" + date + ")
"; + } + + @Override + String provideMessage(MiniMsg message, MiniUser user) throws JSONException{ + String isOut = (user.id == getUserId()) ? "vtex-milk-msg-out" : ""; + String attaches = (message.attachments.size() > 0) ? "

Вложения: " + getHtmlForAttach(message.attachments) + "

" : ""; + + return "
" + + "

от " + user.firstName + " " + user.lastName + " " + formatTime(message.date) + "

" + // TODO photo100 + "

" + message.text + "

" + + attaches + + "
"; + } + + private String getHtmlForAttach(List attaches) throws JSONException{ + StringBuilder rs = new StringBuilder(); + + for (JSONObject attach : attaches) { + String type = attach.getString("type"); + JSONObject item = attach.optJSONObject(type); + + switch(type) { + case "photo": + rs.append("фотография"); + break; + case "doc": + rs.append("документ"); + break; + case "link": + rs.append("ссылка"); + break; + case "wall": + rs.append("запись на стене"); + break; + default: + rs.append("unsupported (").append(type).append(")"); + break; + } + + rs.append(" "); + } + + return rs.toString(); + } + } +} \ No newline at end of file diff --git a/smali/res/values-ru/strings.xml b/smali/res/values-ru/strings.xml index f7375164f..52fcf11dc 100644 --- a/smali/res/values-ru/strings.xml +++ b/smali/res/values-ru/strings.xml @@ -6183,4 +6183,6 @@ Вайтлист рекламы Вайтлист фильтров + + Скачать диалог \ No newline at end of file diff --git a/smali/res/values/strings.xml b/smali/res/values/strings.xml index 17575a4c6..2a2e29391 100644 --- a/smali/res/values/strings.xml +++ b/smali/res/values/strings.xml @@ -6321,4 +6321,6 @@ May look like a fake leaving the online" Ads whitelist Filters whitelist + + Download dialog