content: /*
   * Copyright (C) 2015 Actor LLC. <https://actor.im>
   */
  
  package im.actor.core.js;
  
  import com.google.gwt.core.client.JsArray;
  import com.google.gwt.dom.client.Element;
  import com.google.gwt.http.client.URL;
  import com.google.gwt.i18n.client.LocaleInfo;
  import com.google.gwt.i18n.client.TimeZone;
  import com.google.gwt.user.client.Event;
  
  import im.actor.core.*;
  import im.actor.core.api.ApiAuthSession;
  import im.actor.core.api.ApiDialog;
  import im.actor.core.api.rpc.ResponseLoadArchived;
  import im.actor.core.entity.BotCommand;
  import im.actor.core.entity.EntityConverter;
  import im.actor.core.entity.MentionFilterResult;
  import im.actor.core.entity.MessageSearchEntity;
  import im.actor.core.entity.Peer;
  import im.actor.core.entity.PeerSearchEntity;
  import im.actor.core.entity.PeerSearchType;
  import im.actor.core.entity.PeerType;
  import im.actor.core.entity.User;
  import im.actor.core.js.annotations.UsedByApp;
  import im.actor.core.js.entity.*;
  import im.actor.core.js.modules.JsBindedValueCallback;
  import im.actor.core.js.providers.JsNotificationsProvider;
  import im.actor.core.js.providers.JsPhoneBookProvider;
  import im.actor.core.js.providers.JsCallsProvider;
  import im.actor.core.js.providers.electron.JsElectronApp;
  import im.actor.core.js.utils.HtmlMarkdownUtils;
  import im.actor.core.js.utils.IdentityUtils;
  import im.actor.core.network.RpcCallback;
  import im.actor.core.network.RpcException;
  import im.actor.core.viewmodel.CommandCallback;
  import im.actor.core.viewmodel.UserVM;
  import im.actor.runtime.Log;
  import im.actor.runtime.Storage;
  import im.actor.runtime.actors.messages.*;
  import im.actor.runtime.actors.messages.Void;
  import im.actor.runtime.function.Consumer;
  import im.actor.runtime.js.JsFileSystemProvider;
  import im.actor.runtime.js.JsLogProvider;
  import im.actor.runtime.js.fs.JsBlob;
  import im.actor.runtime.js.fs.JsFile;
  import im.actor.runtime.js.mvvm.JsDisplayListCallback;
  import im.actor.runtime.js.utils.JsPromise;
  import im.actor.runtime.js.utils.JsPromiseDispatcher;
  import im.actor.runtime.js.utils.JsPromiseExecutor;
  import im.actor.runtime.markdown.MarkdownParser;
  
  import org.timepedia.exporter.client.Export;
  import org.timepedia.exporter.client.ExportPackage;
  import org.timepedia.exporter.client.Exportable;
  
  import java.util.ArrayList;
  import java.util.Date;
  import java.util.List;
  
  @ExportPackage("actor")
  @Export("ActorApp")
  @UsedByApp
  public class JsFacade implements Exportable {
  
      private static final String TAG = "JsMessenger";
  
      private static final String APP_NAME = "Actor Web App";
      private static final int APP_ID = 3;
      private static final String APP_KEY = "278f13e07eee8398b189bced0db2cf66703d1746e2b541d85f5b42b1641aae0e";
  
      private JsMessenger messenger;
      private JsFileSystemProvider provider;
      private Peer lastVisiblePeer;
  
      @Export
      @UsedByApp
      public JsFacade() {
  
      }
  
      @UsedByApp
      public void init(JsConfig config) {
  
          provider = (JsFileSystemProvider) Storage.getFileSystemRuntime();
  
          String clientName = IdentityUtils.getClientName();
          String uniqueId = IdentityUtils.getUniqueId();
  
          ConfigurationBuilder configuration = new ConfigurationBuilder();
          configuration.setApiConfiguration(new ApiConfiguration(APP_NAME, APP_ID, APP_KEY, clientName, uniqueId));
          configuration.setPhoneBookProvider(new JsPhoneBookProvider());
          configuration.setNotificationProvider(new JsNotificationsProvider());
          configuration.setCallsProvider(new JsCallsProvider());
  
          // Setting locale
          String locale = LocaleInfo.getCurrentLocale().getLocaleName();
          if (locale.equals("default")) {
              Log.d(TAG, "Default locale found");
              configuration.addPreferredLanguage("en");
          } else {
              Log.d(TAG, "Locale found:" + locale);
              configuration.addPreferredLanguage(locale.toLowerCase());
          }
  
          // Setting timezone
          int offset = new Date().getTimezoneOffset();
          String timeZone = TimeZone.createTimeZone(offset).getID();
          Log.d(TAG, "TimeZone found:" + timeZone + " for delta " + offset);
          configuration.setTimeZone(timeZone);
  
          // LocaleInfo.getCurrentLocale().getLocaleName()
  
          // Is Web application
          configuration.setPlatformType(PlatformType.WEB);
  
          // Device Category
          // Only Desktop is supported for JS library
          configuration.setDeviceCategory(DeviceCategory.DESKTOP);
  
          // Adding endpoints
          for (String endpoint : config.getEndpoints()) {
              configuration.addEndpoint(endpoint);
          }
  
          if (config.getLogHandler() != null) {
              final JsLogCallback callback = config.getLogHandler();
              JsLogProvider.setLogCallback(new JsLogProvider.LogCallback() {
                  @Override
                  public void log(String tag, String level, String message) {
                      callback.log(tag, level, message);
                  }
              });
          }
  
          messenger = new JsMessenger(configuration.build());
  
          Log.d(TAG, "JsMessenger created");
      }
  
      @UsedByApp
      public boolean isLoggedIn() {
          return messenger.isLoggedIn();
      }
  
      @UsedByApp
      public int getUid() {
          return messenger.myUid();
      }
  
      @UsedByApp
      public boolean isElectron() {
          return messenger.isElectron();
      }
  
      // Auth
  
      @UsedByApp
      public String getAuthState() {
          return Enums.convert(messenger.getAuthState());
      }
  
      @UsedByApp
      public String getAuthPhone() {
          return "" + messenger.getAuthPhone();
      }
  
      @UsedByApp
      public void requestSms(String phone, final JsAuthSuccessClosure success,
                             final JsAuthErrorClosure error) {
          try {
              long res = Long.parseLong(phone);
              messenger.requestStartPhoneAuth(res).start(new CommandCallback<AuthState>() {
                  @Override
                  public void onResult(AuthState res) {
                      success.onResult(Enums.convert(res));
                  }
  
                  @Override
                  public void onError(Exception e) {
                      String tag = "INTERNAL_ERROR";
                      String message = "Internal error";
                      boolean canTryAgain = false;
                      if (e instanceof RpcException) {
                          tag = ((RpcException) e).getTag();
                          message = e.getMessage();
                          canTryAgain = ((RpcException) e).isCanTryAgain();
                      }
                      error.onError(tag, message, canTryAgain, getAuthState());
                  }
              });
          } catch (Exception e) {
              Log.e(TAG, e);
              im.actor.runtime.Runtime.postToMainThread(new Runnable() {
                  @Override
                  public void run() {
                      error.onError("PHONE_NUMBER_INVALID", "Invalid phone number", false,
                              getAuthState());
                  }
              });
          }
      }
  
      @UsedByApp
      public void requestCodeEmail(String email, final JsAuthSuccessClosure success,
                                   final JsAuthErrorClosure error) {
          messenger.requestStartEmailAuth(email).start(new CommandCallback<AuthState>() {
              @Override
              public void onResult(AuthState res) {
                  success.onResult(Enums.convert(res));
              }
  
              @Override
              public void onError(Exception e) {
                  String tag = "INTERNAL_ERROR";
                  String message = "Internal error";
                  boolean canTryAgain = false;
                  if (e instanceof RpcException) {
                      tag = ((RpcException) e).getTag();
                      message = e.getMessage();
                      canTryAgain = ((RpcException) e).isCanTryAgain();
                  }
                  error.onError(tag, message, canTryAgain, getAuthState());
              }
          });
      }
  
      @UsedByApp
      public void sendCode(String code, final JsAuthSuccessClosure success,
                           final JsAuthErrorClosure error) {
          try {
              messenger.validateCode(code).start(new CommandCallback<AuthState>() {
                  @Override
                  public void onResult(AuthState res) {
                      success.onResult(Enums.convert(res));
                  }
  
                  @Override
                  public void onError(Exception e) {
                      String tag = "INTERNAL_ERROR";
                      String message = "Internal error";
                      boolean canTryAgain = false;
                      if (e instanceof RpcException) {
                          tag = ((RpcException) e).getTag();
                          message = e.getMessage();
                          canTryAgain = ((RpcException) e).isCanTryAgain();
                      }
                      error.onError(tag, message, canTryAgain, getAuthState());
                  }
              });
          } catch (Exception e) {
              Log.e(TAG, e);
              im.actor.runtime.Runtime.postToMainThread(new Runnable() {
                  @Override
                  public void run() {
                      error.onError("PHONE_CODE_INVALID", "Invalid code number", false,
                              getAuthState());
                  }
              });
          }
      }
  
      @UsedByApp
      public void signUp(String name, final JsAuthSuccessClosure success,
                         final JsAuthErrorClosure error) {
          messenger.signUp(name, null, null).start(new CommandCallback<AuthState>() {
              @Override
              public void onResult(AuthState res) {
                  success.onResult(Enums.convert(res));
              }
  
              @Override
              public void onError(Exception e) {
                  String tag = "INTERNAL_ERROR";
                  String message = "Internal error";
                  boolean canTryAgain = false;
                  if (e instanceof RpcException) {
                      tag = ((RpcException) e).getTag();
                      message = e.getMessage();
                      canTryAgain = ((RpcException) e).isCanTryAgain();
                  }
                  error.onError(tag, message, canTryAgain, getAuthState());
              }
          });
      }
  
      @UsedByApp
      public JsPromise loadSessions() {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  messenger.loadSessions().start(new CommandCallback<List<ApiAuthSession>>() {
                      @Override
                      public void onResult(List<ApiAuthSession> res) {
                          JsArray<JsAuthSession> jsSessions = JsArray.createArray().cast();
  
                          for (ApiAuthSession session : res) {
                              jsSessions.push(JsAuthSession.create(session));
                          }
  
                          resolve(jsSessions);
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.e(TAG, e);
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      @UsedByApp
      public JsPromise terminateSession(final int id) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  messenger.terminateSession(id).start(new CommandCallback<Void>() {
                      @Override
                      public void onResult(Void res) {
                          resolve();
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.e(TAG, e);
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      @UsedByApp
      public JsPromise terminateAllSessions() {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  messenger.terminateAllSessions().start(new CommandCallback<Void>() {
                      @Override
                      public void onResult(Void res) {
                          resolve();
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.e(TAG, e);
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      // Dialogs
  
      @UsedByApp
      public void bindDialogs(JsDisplayListCallback<JsDialog> callback) {
          if (callback == null) {
              return;
          }
          messenger.getSharedDialogList().subscribe(callback, false);
      }
  
      @UsedByApp
      public void unbindDialogs(JsDisplayListCallback<JsDialog> callback) {
          if (callback == null) {
              return;
          }
          messenger.getSharedDialogList().unsubscribe(callback);
      }
  
      @UsedByApp
      public void bindGroupDialogs(JsBindedValueCallback callback) {
          if (callback == null) {
              return;
          }
  
          messenger.getDialogsGroupedList().subscribe(callback);
      }
  
      @UsedByApp
      public void unbindGroupDialogs(JsBindedValueCallback callback) {
          if (callback == null) {
              return;
          }
  
          messenger.getDialogsGroupedList().unsubscribe(callback);
      }
  
      // Contacts
  
      @UsedByApp
      public void bindContacts(JsDisplayListCallback<JsContact> callback) {
          if (callback == null) {
              return;
          }
          messenger.getSharedContactList().subscribe(callback, true);
      }
  
      @UsedByApp
      public void unbindContacts(JsDisplayListCallback<JsContact> callback) {
          if (callback == null) {
              return;
          }
          messenger.getSharedContactList().unsubscribe(callback);
      }
  
      // Search
  
      @UsedByApp
      public void bindSearch(JsDisplayListCallback<JsSearchEntity> callback) {
          if (callback == null) {
              return;
          }
          messenger.getSharedSearchList().subscribe(callback, false);
      }
  
      @UsedByApp
      public void unbindSearch(JsDisplayListCallback<JsSearchEntity> callback) {
          if (callback == null) {
              return;
          }
          messenger.getSharedSearchList().unsubscribe(callback);
      }
  
      // Chats
  
      @UsedByApp
      public void preInitChat(JsPeer peer) {
          messenger.onConversationPreLoad(peer.convert());
      }
  
      @UsedByApp
      public JsMessagesBind bindMessages(JsPeer peer, JsMessagesBindClosure callback) {
          if (callback == null) {
              return null;
          }
          Peer peerC = peer.convert();
  
          return new JsMessagesBind(callback, messenger.getSharedChatList(peerC), messenger.getConversationVM(peerC));
      }
  
      public JsPromise editMessage(JsPeer peer, String id, String newText) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  messenger.updateMessage(peer.convert(), newText, Long.parseLong(id))
                          .then(r -> resolve())
                          .failure(e -> reject(e.getMessage()));
              }
          });
      }
  
      @UsedByApp
      public void deleteMessage(JsPeer peer, String id) {
          messenger.deleteMessages(peer.convert(), new long[]{Long.parseLong(id)});
      }
  
      @UsedByApp
      public JsPromise deleteChat(final JsPeer peer) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  messenger.deleteChat(peer.convert()).start(new CommandCallback<Void>() {
                      @Override
                      public void onResult(Void res) {
                          Log.d(TAG, "deleteChat:result");
                          resolve();
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.d(TAG, "deleteChat:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      @UsedByApp
      public JsPromise clearChat(final JsPeer peer) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  messenger.clearChat(peer.convert()).start(new CommandCallback<Void>() {
                      @Override
                      public void onResult(Void res) {
                          Log.d(TAG, "clearChat:result");
                          resolve();
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.d(TAG, "clearChat:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      @UsedByApp
      public JsPromise archiveChat(final JsPeer peer) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  messenger.archiveChat(peer.convert()).start(new CommandCallback<Void>() {
                      @Override
                      public void onResult(Void res) {
                          Log.d(TAG, "archiveChat:result");
                          resolve();
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.d(TAG, "archiveChat:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      @UsedByApp
      public JsPromise favoriteChat(final JsPeer peer) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  messenger.favouriteChat(peer.convert()).start(new CommandCallback<Void>() {
                      @Override
                      public void onResult(Void res) {
                          Log.d(TAG, "favouriteChat:result");
                          resolve();
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.d(TAG, "favouriteChat:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      @UsedByApp
      public JsPromise unfavoriteChat(final JsPeer peer) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  messenger.unfavoriteChat(peer.convert()).start(new CommandCallback<Void>() {
                      @Override
                      public void onResult(Void res) {
                          Log.d(TAG, "unfavouriteChat:result");
                          resolve();
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.d(TAG, "unfavouriteChat:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      // Peers
  
      @UsedByApp
      public JsPeer getUserPeer(int uid) {
          return JsPeer.create(Peer.user(uid));
      }
  
      @UsedByApp
      public JsPeer getGroupPeer(int gid) {
          return JsPeer.create(Peer.group(gid));
      }
  
      // Stickers
  
      @UsedByApp
      public JsArray<JsSticker> getStickers() {
          return messenger.getStickers().get();
      }
  
      @UsedByApp
      public void bindStickers(JsBindedValueCallback callback) {
          messenger.getStickers().subscribe(callback);
      }
  
      @UsedByApp
      public void unbindStickers(JsBindedValueCallback callback) {
          messenger.getStickers().unsubscribe(callback);
      }
  
      // Users
  
      @UsedByApp
      public JsUser getUser(int uid) {
          return messenger.getJsUser(uid).get();
      }
  
      @UsedByApp
      public void bindUser(int uid, JsBindedValueCallback callback) {
          if (callback == null) {
              return;
          }
          messenger.getJsUser(uid).subscribe(callback);
      }
  
      @UsedByApp
      public void unbindUser(int uid, JsBindedValueCallback callback) {
          if (callback == null) {
              return;
          }
          messenger.getJsUser(uid).unsubscribe(callback);
      }
  
      @UsedByApp
      public void bindUserOnline(int uid, JsBindedValueCallback callback) {
          if (callback == null) {
              return;
          }
          messenger.getJsUserOnline(uid).subscribe(callback);
      }
  
      @UsedByApp
      public void unbindUserOnline(int uid, JsBindedValueCallback callback) {
          if (callback == null) {
              return;
          }
          messenger.getJsUserOnline(uid).unsubscribe(callback);
      }
  
      @UsedByApp
      public void bindUserBlocked(int uid, JsBindedValueCallback callback) {
          if (callback == null) {
              return;
          }
          messenger.getJsUserBlocked(uid).subscribe(callback);
      }
  
      @UsedByApp
      public void unbindUserBlocked(int uid, JsBindedValueCallback callback) {
          if (callback == null) {
              return;
          }
          messenger.getJsUserBlocked(uid).unsubscribe(callback);
      }
  
      @UsedByApp
      public JsPromise blockUser(final int uid) {
          return JsPromise.from(messenger.blockUser(uid));
      }
  
      @UsedByApp
      public JsPromise unblockUser(final int uid) {
          return JsPromise.from(messenger.unblockUser(uid));
      }
  
      @UsedByApp
      public JsPromise isStarted(final int uid) {
          return JsPromise.from(messenger.isStarted(uid));
      }
  
      // Groups
  
      @UsedByApp
      public JsGroup getGroup(int gid) {
          return messenger.getJsGroup(gid).get();
      }
  
      @UsedByApp
      public void bindGroup(int gid, JsBindedValueCallback callback) {
          if (callback == null) {
              return;
          }
          messenger.getJsGroup(gid).subscribe(callback);
      }
  
      @UsedByApp
      public void unbindGroup(int gid, JsBindedValueCallback callback) {
          if (callback == null) {
              return;
          }
          messenger.getJsGroup(gid).unsubscribe(callback);
      }
  
      @UsedByApp
      public void bindGroupOnline(int gid, JsBindedValueCallback callback) {
          if (callback == null) {
              return;
          }
          messenger.getJsGroupOnline(gid).subscribe(callback);
      }
  
      @UsedByApp
      public void unbindGroupOnline(int gid, JsBindedValueCallback callback) {
          if (callback == null) {
              return;
          }
          messenger.getJsGroupOnline(gid).unsubscribe(callback);
      }
  
      // Calls
  
      @UsedByApp
      public JsPromise doCall(final int uid) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  messenger.doCall(uid).start(new CommandCallback<Long>() {
                      @Override
                      public void onResult(Long res) {
                          Log.d(TAG, "doCall:result");
                          resolve();
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.d(TAG, "doCall:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      @UsedByApp
      public JsPromise doGroupCall(final int gid) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  messenger.doGroupCall(gid).start(new CommandCallback<Long>() {
                      @Override
                      public void onResult(Long res) {
                          Log.d(TAG, "doGroupCall:result");
                          resolve();
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.d(TAG, "doGroupCall:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      @UsedByApp
      public void answerCall(String callId) {
          messenger.answerCall(Long.parseLong(callId));
      }
  
      @UsedByApp
      public void endCall(String callId) {
          messenger.endCall(Long.parseLong(callId));
      }
  
      @UsedByApp
      public void toggleCallMute(String callId) {
          messenger.toggleCallMute(Long.parseLong(callId));
      }
  
      @UsedByApp
      public void bindCall(String id, JsBindedValueCallback callback) {
          if (callback == null) {
              return;
          }
          messenger.getJsCall(id).subscribe(callback);
      }
  
      @UsedByApp
      public void unbindCall(String id, JsBindedValueCallback callback) {
          if (callback == null) {
              return;
          }
          messenger.getJsCall(id).unsubscribe(callback);
      }
  
      // Event Bus
  
      @UsedByApp
      public void bindEventBus(JsEventBusCallback callback) {
          if (callback == null) {
              return;
          }
  
          messenger.subscribeEventBus(callback);
      }
  
      @UsedByApp
      public void unbindEventBus(JsEventBusCallback callback) {
          if (callback == null) {
              return;
          }
  
          messenger.unsubscribeEventBus(callback);
      }
  
      // Actions
  
      @UsedByApp
      public void sendMessage(JsPeer peer, String text) {
          messenger.sendMessageWithMentionsDetect(peer.convert(), text);
      }
  
      @UsedByApp
      public void sendFile(JsPeer peer, JsFile file) {
          String descriptor = provider.registerUploadFile(file);
          messenger.sendDocument(peer.convert(),
                  file.getName(), file.getMimeType(), descriptor);
      }
  
      @UsedByApp
      public void sendPhoto(final JsPeer peer, final JsFile file) {
          messenger.sendPhoto(peer.convert(), file);
      }
  
      @UsedByApp
      public void sendAnimation(final JsPeer peer, final JsFile file) {
          messenger.sendAnimation(peer.convert(), file);
      }
  
      @UsedByApp
      public void sendClipboardPhoto(final JsPeer peer, final JsBlob blob) {
          messenger.sendClipboardPhoto(peer.convert(), blob);
      }
  
      @UsedByApp
      public void sendVoiceMessage(final JsPeer peer, int duration, final JsBlob blob) {
          String descriptor = provider.registerUploadFile(blob);
          messenger.sendAudio(peer.convert(), "voice.opus", duration, descriptor);
      }
  
      @UsedByApp
      public void sendSticker(JsPeer peer, JsSticker sticker) {
          messenger.sendSticker(peer.convert(), sticker.getSticker());
      }
  
      // Drafts
  
      @UsedByApp
      public void saveDraft(JsPeer peer, String text) {
          messenger.saveDraft(peer.convert(), text);
      }
  
      @UsedByApp
      public String loadDraft(JsPeer peer) {
          return messenger.loadDraft(peer.convert());
      }
  
      @UsedByApp
      public JsArray<JsMentionFilterResult> findMentions(int gid, String query) {
          List<MentionFilterResult> res = messenger.findMentions(gid, query);
          JsArray<JsMentionFilterResult> mentions = JsArray.createArray().cast();
          for (MentionFilterResult m : res) {
              mentions.push(JsMentionFilterResult.create(m));
          }
          return mentions;
      }
  
      @UsedByApp
      public JsArray<JsBotCommand> findBotCommands(int uid, String query) {
          JsArray<JsBotCommand> commands = JsArray.createArray().cast();
          for (BotCommand c : messenger.getUser(uid).getBotCommands().get()) {
              if (c.getSlashCommand().startsWith(query)) {
                  commands.push(JsBotCommand.create(c.getSlashCommand(), c.getDescription()));
              }
          }
          return commands;
      }
  
      // Typing
  
      @UsedByApp
      public void onTyping(JsPeer peer) {
          messenger.onTyping(peer.convert());
      }
  
      @UsedByApp
      public JsTyping getTyping(JsPeer peer) {
          return messenger.getTyping(peer.convert()).get();
      }
  
      @UsedByApp
      public void bindTyping(JsPeer peer, JsBindedValueCallback callback) {
          messenger.getTyping(peer.convert()).subscribe(callback);
      }
  
      @UsedByApp
      public void unbindTyping(JsPeer peer, JsBindedValueCallback callback) {
          messenger.getTyping(peer.convert()).unsubscribe(callback);
      }
  
      // Updating state
  
      @UsedByApp
      public void bindConnectState(JsBindedValueCallback callback) {
          messenger.getOnlineStatus().subscribe(callback);
      }
  
      @UsedByApp
      public void unbindConnectState(JsBindedValueCallback callback) {
          messenger.getOnlineStatus().unsubscribe(callback);
      }
  
      @UsedByApp
      public void bindGlobalCounter(JsBindedValueCallback callback) {
          messenger.getGlobalCounter().subscribe(callback);
      }
  
      @UsedByApp
      public void unbindGlobalCounter(JsBindedValueCallback callback) {
          messenger.getGlobalCounter().unsubscribe(callback);
      }
  
      @UsedByApp
      public void bindTempGlobalCounter(JsBindedValueCallback callback) {
          messenger.getTempGlobalCounter().subscribe(callback);
      }
  
      @UsedByApp
      public void unbindTempGlobalCounter(JsBindedValueCallback callback) {
          messenger.getTempGlobalCounter().unsubscribe(callback);
      }
  
      // Events
  
      @UsedByApp
      public void onAppVisible() {
          // Ignore for electron runtime
          if (isElectron()) {
              return;
          }
  
          messenger.getJsIdleModule().onVisible();
      }
  
      @UsedByApp
      public void onAppHidden() {
          // Ignore for electron runtime
          if (isElectron()) {
              return;
          }
  
          messenger.getJsIdleModule().onHidden();
      }
  
      @UsedByApp
      public void onConversationOpen(JsPeer peer) {
          Log.d(TAG, "onConversationOpen | " + peer);
          lastVisiblePeer = peer.convert();
          messenger.onConversationOpen(lastVisiblePeer);
          onUserVisible(peer);
      }
  
      @UsedByApp
      public void onConversationClosed(JsPeer peer) {
          Log.d(TAG, "onConversationClosed | " + peer);
          if (lastVisiblePeer != null && lastVisiblePeer.equals(peer.convert())) {
              lastVisiblePeer = null;
              Log.d(TAG, "onConversationClosed | Closing");
          }
          messenger.onConversationClosed(peer.convert());
      }
  
      @UsedByApp
      public void onUserVisible(JsPeer peer) {
          if (peer.convert().getPeerType() == PeerType.PRIVATE) {
              messenger.onUserVisible(peer.getPeerId());
          }
      }
  
      @UsedByApp
      public void onDialogsOpen() {
          messenger.onDialogsOpen();
      }
  
      @UsedByApp
      public void onDialogsClosed() {
          messenger.onDialogsClosed();
      }
  
      @UsedByApp
      public void onProfileOpen(int uid) {
          messenger.onProfileOpen(uid);
      }
  
      @UsedByApp
      public void onProfileClosed(int uid) {
          messenger.onProfileClosed(uid);
      }
  
      @UsedByApp
      public void onDialogsEnd() {
          messenger.loadMoreDialogs();
      }
  
      @UsedByApp
      public JsPromise loadArchivedDialogs() {
          return loadArchivedDialogs(true);
      }
  
      @UsedByApp
      public JsPromise loadMoreArchivedDialogs() {
          return loadArchivedDialogs(false);
      }
  
      @UsedByApp
      private JsPromise loadArchivedDialogs(final boolean init) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  messenger.loadArchivedDialogs(init, new RpcCallback<ResponseLoadArchived>() {
                      @Override
                      public void onResult(ResponseLoadArchived response) {
                          JsArray<JsDialogShort> res = JsArray.createArray().cast();
                          for (ApiDialog d : response.getDialogs()) {
                              res.push(JsDialogShort.create(messenger.buildPeerInfo(EntityConverter.convert(d.getPeer())), d.getUnreadCount()));
                          }
                          Log.d(TAG, "loadArchivedDialogs:result");
                          resolve(res);
                      }
  
                      @Override
                      public void onError(RpcException e) {
                          Log.d(TAG, "loadArchivedDialogs:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      @UsedByApp
      public JsPromise loadBlockedUsers() {
          return JsPromise.from(messenger.loadBlockedUsers()
                  .map(users -> {
                      JsArray<JsUser> res = JsArray.createArray().cast();
                      for (User u : users) {
                          res.push(getUser(u.getUid()));
                      }
                      return res;
                  })
          );
      }
  
      @UsedByApp
      public void onChatEnd(JsPeer peer) {
          messenger.loadMoreHistory(peer.convert());
      }
  
      // Profile
  
      @UsedByApp
      public JsPromise editMyName(final String newName) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  //noinspection ConstantConditions
                  messenger.editMyName(newName).start(new CommandCallback<Boolean>() {
                      @Override
                      public void onResult(Boolean res) {
                          Log.d(TAG, "editMyName:result");
                          resolve();
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.d(TAG, "editMyName:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      @UsedByApp
      public JsPromise editMyNick(final String newNick) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  //noinspection ConstantConditions
                  messenger.editMyNick(newNick).start(new CommandCallback<Boolean>() {
                      @Override
                      public void onResult(Boolean res) {
                          Log.d(TAG, "editMyNick:result");
                          resolve();
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.d(TAG, "editMyNick:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      @UsedByApp
      public JsPromise editMyAbout(final String newAbout) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  //noinspection ConstantConditions
                  messenger.editMyAbout(newAbout).start(new CommandCallback<Boolean>() {
                      @Override
                      public void onResult(Boolean res) {
                          Log.d(TAG, "editMyAbout:result");
                          resolve();
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.d(TAG, "editMyAbout:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      @UsedByApp
      public JsPromise findAllText(final JsPeer peer, final String query) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  messenger.findTextMessages(peer.convert(), query).start(new CommandCallback<List<MessageSearchEntity>>() {
                      @Override
                      public void onResult(List<MessageSearchEntity> res) {
                          resolve(convertSearchRes(res));
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.d(TAG, "findAllText:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      @UsedByApp
      public JsPromise findAllPhotos(final JsPeer peer) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  messenger.findAllPhotos(peer.convert()).start(new CommandCallback<List<MessageSearchEntity>>() {
                      @Override
                      public void onResult(List<MessageSearchEntity> res) {
                          resolve(convertSearchRes(res));
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.d(TAG, "findAllText:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      @UsedByApp
      public JsPromise findAllDocs(final JsPeer peer) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  messenger.findAllDocs(peer.convert()).start(new CommandCallback<List<MessageSearchEntity>>() {
                      @Override
                      public void onResult(List<MessageSearchEntity> res) {
                          resolve(convertSearchRes(res));
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.d(TAG, "findAllText:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      @UsedByApp
      public JsPromise findAllLinks(final JsPeer peer) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  messenger.findAllLinks(peer.convert()).start(new CommandCallback<List<MessageSearchEntity>>() {
                      @Override
                      public void onResult(List<MessageSearchEntity> res) {
                          resolve(convertSearchRes(res));
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.d(TAG, "findAllText:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      private JsArray<JsMessageSearchEntity> convertSearchRes(List<MessageSearchEntity> res) {
          JsArray<JsMessageSearchEntity> jsRes = JsArray.createArray().cast();
          for (MessageSearchEntity e : res) {
              jsRes.push(JsMessageSearchEntity.create(e.getRid() + "",
                      messenger.buildPeerInfo(Peer.user(e.getSenderId())),
                      messenger.getFormatter().formatDate(e.getDate()),
                      JsContent.createContent(e.getContent(),
                              e.getSenderId())));
          }
          return jsRes;
      }
  
  //    @UsedByApp
  //    public JsPromise findGroups() {
  //        return JsPromise.create(new JsPromiseExecutor() {
  //            @Override
  //            public void execute() {
  //                messenger.findPeers(PeerSearchType.GROUPS).start(new CommandCallback<List<PeerSearchEntity>>() {
  //                    @Override
  //                    public void onResult(List<PeerSearchEntity> res) {
  //                        Log.d(TAG, "findGroups:result");
  //                        JsArray<JsPeerSearchResult> jsRes = JsArray.createArray().cast();
  //                        for (PeerSearchEntity s : res) {
  //                            if (s.getPeer().getPeerType() == PeerType.GROUP) {
  //                                jsRes.push(JsPeerSearchResult.create(messenger.buildPeerInfo(s.getPeer()),
  //                                        s.getDescription(), s.getMembersCount(), (int) (s.getDate() / 1000L),
  //                                        messenger.buildPeerInfo(Peer.user(s.getCreatorUid())), s.isPublic(),
  //                                        s.isJoined()));
  //                            } else if (s.getPeer().getPeerType() == PeerType.PRIVATE) {
  //                                jsRes.push(JsPeerSearchResult.create(messenger.buildPeerInfo(s.getPeer())));
  //                            }
  //                            // jsRes.push();
  //                        }
  //                        resolve(jsRes);
  //                    }
  //
  //                    @Override
  //                    public void onError(Exception e) {
  //                        Log.d(TAG, "findGroups:error");
  //                        reject(e.getMessage());
  //                    }
  //                });
  //            }
  //        });
  //    }
  
      @UsedByApp
      public void changeMyAvatar(final JsFile file) {
          String descriptor = provider.registerUploadFile(file);
          messenger.changeMyAvatar(descriptor);
      }
  
      @UsedByApp
      public void removeMyAvatar() {
          messenger.removeMyAvatar();
      }
  
      @UsedByApp
      public JsPromise editName(final int uid, final String newName) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  //noinspection ConstantConditions
                  messenger.editName(uid, newName).start(new CommandCallback<Boolean>() {
                      @Override
                      public void onResult(Boolean res) {
                          Log.d(TAG, "editName:result");
                          resolve();
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.d(TAG, "editName:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      @UsedByApp
      public JsPromise joinGroupViaLink(final String url) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  //noinspection ConstantConditions
                  messenger.joinGroupViaToken(url).start(new CommandCallback<Integer>() {
                      @Override
                      public void onResult(Integer res) {
                          Log.d(TAG, "joinGroupViaLink:result");
                          resolve(JsPeer.create(Peer.group(res)));
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.d(TAG, "joinGroupViaLink:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      @UsedByApp
      public JsPromise editGroupTitle(final int gid, final String newTitle) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
  
                  messenger.editGroupTitle(gid, newTitle)
                          .then(r -> resolve())
                          .failure(e -> reject(e.getMessage()));
              }
  
          });
      }
  
  
      @UsedByApp
      public JsPromise editGroupAbout(final int gid, final String newAbout) {
          return JsPromise.from(messenger.editGroupAbout(gid, newAbout).map(r -> null));
      }
  
      @UsedByApp
      public void changeGroupAvatar(final int gid, final JsFile file) {
          String descriptor = provider.registerUploadFile(file);
          messenger.changeGroupAvatar(gid, descriptor);
      }
  
      @UsedByApp
      public void removeGroupAvatar(final int gid) {
          messenger.removeGroupAvatar(gid);
      }
  
      @UsedByApp
      public JsPromise createGroup(final String title, final JsFile file, final int[] uids) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  String avatarDescriptor = file != null ? provider.registerUploadFile(file) : null;
                  //noinspection ConstantConditions
                  messenger.createGroup(title, avatarDescriptor, uids)
                          .then(r -> resolve(JsPeer.create(Peer.group(r))))
                          .failure(e -> reject(e.getMessage()));
              }
          });
      }
  
      @UsedByApp
      public JsPromise inviteMember(final int gid, final int uid) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  //noinspection ConstantConditions
                  messenger.inviteMember(gid, uid).start(new CommandCallback<Void>() {
                      @Override
                      public void onResult(Void res) {
                          Log.d(TAG, "inviteMember:result");
                          resolve();
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.d(TAG, "inviteMember:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      @UsedByApp
      public JsPromise kickMember(final int gid, final int uid) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  //noinspection ConstantConditions
                  messenger.kickMember(gid, uid).start(new CommandCallback<Void>() {
                      @Override
                      public void onResult(Void res) {
                          Log.d(TAG, "kickMember:result");
                          resolve();
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.d(TAG, "kickMember:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      @UsedByApp
      public JsPromise leaveGroup(final int gid) {
          return JsPromise.from(messenger.leaveGroup(gid).map(r -> null));
      }
  
      @UsedByApp
      public JsPromise getIntegrationToken(final int gid) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  //noinspection ConstantConditions
                  messenger.requestIntegrationToken(gid).start(new CommandCallback<String>() {
                      @Override
                      public void onResult(String res) {
                          Log.d(TAG, "getIntegrationToken:result");
                          resolve(res);
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.d(TAG, "getIntegrationToken:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      @UsedByApp
      public JsPromise revokeIntegrationToken(final int gid) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  //noinspection ConstantConditions
                  messenger.revokeIntegrationToken(gid).start(new CommandCallback<String>() {
                      @Override
                      public void onResult(String res) {
                          Log.d(TAG, "revokeIntegrationToken:result");
                          resolve(res);
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.d(TAG, "revokeIntegrationToken:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      @UsedByApp
      public JsPromise getInviteLink(final int gid) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  //noinspection ConstantConditions
                  messenger.requestInviteLink(gid).start(new CommandCallback<String>() {
                      @Override
                      public void onResult(String res) {
                          Log.d(TAG, "getInviteLink:result");
                          resolve(res);
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.d(TAG, "getInviteLink:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      @UsedByApp
      public JsPromise revokeInviteLink(final int gid) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  //noinspection ConstantConditions
                  messenger.revokeInviteLink(gid).start(new CommandCallback<String>() {
                      @Override
                      public void onResult(String res) {
                          Log.d(TAG, "revokeInviteLink:result");
                          resolve(res);
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.d(TAG, "revokeInviteLink:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      @UsedByApp
      public JsPromise addContact(final int uid) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  //noinspection ConstantConditions
                  messenger.addContact(uid).start(new CommandCallback<Boolean>() {
                      @Override
                      public void onResult(Boolean res) {
                          Log.d(TAG, "addContact:result");
                          resolve();
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.d(TAG, "addContact:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      @UsedByApp
      public JsPromise addLike(final JsPeer peer, final String rid) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  messenger.addReaction(peer.convert(), Long.parseLong(rid), "\u2764")
                          .start(new CommandCallback<Void>() {
                              @Override
                              public void onResult(Void res) {
                                  resolve();
                              }
  
                              @Override
                              public void onError(Exception e) {
                                  reject(e.getMessage());
                              }
                          });
  
              }
          });
      }
  
      @UsedByApp
      public JsPromise removeLike(final JsPeer peer, final String rid) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  messenger.removeReaction(peer.convert(), Long.parseLong(rid), "\u2764")
                          .start(new CommandCallback<Void>() {
                              @Override
                              public void onResult(Void res) {
                                  resolve();
                              }
  
                              @Override
                              public void onError(Exception e) {
                                  reject(e.getMessage());
                              }
                          });
  
              }
          });
      }
  
      @UsedByApp
      public JsPromise findUsers(final String query) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  messenger.findUsers(query).start(new CommandCallback<UserVM[]>() {
                      @Override
                      public void onResult(UserVM[] users) {
                          Log.d(TAG, "findUsers:result");
                          JsArray<JsUser> jsUsers = JsArray.createArray().cast();
  
                          for (UserVM user : users) {
                              jsUsers.push(messenger.getJsUser(user.getId()).get());
                          }
  
                          resolve(jsUsers);
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.d(TAG, "findUsers:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      @UsedByApp
      public JsPromise removeContact(final int uid) {
          return JsPromise.create(new JsPromiseExecutor() {
              @Override
              public void execute() {
                  //noinspection ConstantConditions
                  messenger.removeContact(uid).start(new CommandCallback<Boolean>() {
                      @Override
                      public void onResult(Boolean res) {
                          Log.d(TAG, "removeContact:result");
                          resolve();
                      }
  
                      @Override
                      public void onError(Exception e) {
                          Log.d(TAG, "removeContact:error");
                          reject(e.getMessage());
                      }
                  });
              }
          });
      }
  
      // Settings
  
      @UsedByApp
      public void changeNotificationsEnabled(JsPeer peer, boolean isEnabled) {
          messenger.changeNotificationsEnabled(peer.convert(), isEnabled);
      }
  
      @UsedByApp
      public boolean isNotificationsEnabled(JsPeer peer) {
          return messenger.isNotificationsEnabled(peer.convert());
      }
  
      @UsedByApp
      public void changeAnimationAutoPlayEnabled(boolean isEnabled) {
          messenger.changeAnimationAutoPlayEnabled(isEnabled);
      }
  
      @UsedByApp
      public boolean isAnimationAutoPlayEnabled() {
          return messenger.isAnimationAutoPlayEnabled();
      }
  
      @UsedByApp
      public boolean isSendByEnterEnabled() {
          return messenger.isSendByEnterEnabled();
      }
  
      @UsedByApp
      public void changeSendByEnter(boolean sendByEnter) {
          messenger.changeSendByEnter(sendByEnter);
      }
  
      @UsedByApp
      public boolean isGroupsNotificationsEnabled() {
          return messenger.isGroupNotificationsEnabled();
      }
  
      @UsedByApp
      public void changeGroupNotificationsEnabled(boolean enabled) {
          messenger.changeGroupNotificationsEnabled(enabled);
      }
  
      @UsedByApp
      public boolean isOnlyMentionNotifications() {
          return messenger.isGroupNotificationsOnlyMentionsEnabled();
      }
  
      @UsedByApp
      public void changeIsOnlyMentionNotifications(boolean enabled) {
          messenger.changeGroupNotificationsOnlyMentionsEnabled(enabled);
      }
  
      @UsedByApp
      public boolean isSoundEffectsEnabled() {
          return messenger.isConversationTonesEnabled();
      }
  
      @UsedByApp
      public boolean isShowNotificationsTextEnabled() {
          return messenger.isShowNotificationsText();
      }
  
      @UsedByApp
      public void changeIsShowNotificationTextEnabled(boolean value) {
          messenger.changeShowNotificationTextEnabled(value);
      }
  
      @UsedByApp
      public void changeSoundEffectsEnabled(boolean enabled) {
          messenger.changeConversationTonesEnabled(enabled);
      }
  
      @UsedByApp
      public String renderMarkdown(final String markdownText) {
          try {
              return HtmlMarkdownUtils.processText(markdownText, MarkdownParser.MODE_FULL);
          } catch (Exception e) {
              Log.e("Markdown", e);
              return "[Error while processing text]";
          }
      }
  
      @UsedByApp
      public void handleLinkClick(Event event) {
          Element target = Element.as(event.getEventTarget());
          String href = target.getAttribute("href");
          if (href.startsWith("send:")) {
              String msg = href.substring("send:".length());
              msg = URL.decode(msg);
              if (lastVisiblePeer != null) {
                  messenger.sendMessage(lastVisiblePeer, msg);
                  event.preventDefault();
              }
          } else {
              if (JsElectronApp.isElectron()) {
                  JsElectronApp.openUrlExternal(href);
                  event.preventDefault();
              }
          }
      }
  }
  