[
  {
    "path": ".gitignore",
    "content": ".dub\ndocs.json\n__dummy.html\n*.o\n*.obj\nvk-client\nvk\nout\n.idea\n*.iml\ndbg\ndub.selections.json\ndub.userprefs\nsource/vkversion.d\nvk.sublime-project\nvk.sublime-workspace\ndebuildtmp\ndebs\n*.7z\n\n"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "README.md",
    "content": "# vk\nA console (ncurses) client for vk.com written in D\n\n# Project is abandoned\n\nvk-cli is mostly abandoned due to lack of time, very poor state of codebase (which definitely needs to be rewritten from scratch) and mainly - due to vk politics which becomes worse from day to day, starting from music, online and ending with advertisements in newsfeed. So we (devs) won't continue to support this project.\n\nIf you're looking for a way to not use actual vk.com but want to save ability to get content from there, there's some projects from us such as [vktotg](https://github.com/HaCk3Dq/vktotg) tool which helps to reupload music from your page to private channel in telegram, and planned news aggregator, which will anonymously (without your access_token) gather news from public pages you interested in, and forward it to the destination you prefer (telegram bot, e-mail, etc)\n\nBut if you just want CLI client for vk - we can't help with it anymore :)\n\n# Screenshots\n\n![alt tag](http://cs630123.vk.me/v630123942/25fc7/YOqfnerj4bE.jpg)\n![alt tag](http://cs630123.vk.me/v630123942/25fd7/hcgITGtqEd0.jpg)\n\n# Install\n\n## ArchLinux\n\n```sh\nyaourt -S vk-cli # or vk-cli-git\nvk\n```\n\n## Ubuntu \n\n```\nsudo apt-add-repository ppa:mc3man/mpv-tests\nsudo apt-get update\nsudo apt-get install libncursesw5-dev libssl-dev curl mpv\n```\n\nthen `Build` \n\nOR\n\ninstall deb package from releases page `sudo dpkg -i vk-cli.deb`\n\n## Gentoo\n\n```sh\nlayman -fa glicOne\nsudo emerge net-im/vk # for vk-9999 you need install dub, dmd and dlang-tools from dlang overlay\nvk-cli\n```\n## MacOS\n\n```\nbrew install dub dmd curl openssl mpv\nbrew install homebrew/dupes/ncurses\nbrew doctor\nbrew link ncurses -force\n```\n\nthen `Build`\n\n## Build\n\n```\ngit clone https://github.com/vk-cli/vk\ncd vk\ngit checkout VER\ndub build\n```\n(where `VER` is the version number)\n\nbuilds `vk` binary for your platform.\n\nYou can find number of latest version here: https://github.com/vk-cli/vk/releases\n\n## Dependencies\n\n+ ncurses >= 5.7\n+ curl\n+ openssl\n\nMake dependencies:\n\n+ dub \n+ dmd >= 2.071\n\nOptional:\n\n+ mpv >= 0.22.0: for music playback\n\n### Our GPG keys\n\nTo verify signed files, first you need to import keys:\n\n` $ gpg  --keyserver pgp.mit.edu --recv-keys 0x3457ECED `\n\nNow you can verify files and install signed packages:\n\n` $ gpg --verify signed-file.sig signed-file `\n\n`gpg: Good signature from \"vk-cli developers team <vk-cli.dev@ya.ru>\"`\n\nThis output indicates that file is properly signed and isn't damaged\n\n"
  },
  {
    "path": "buildRelease.sh",
    "content": "#!/bin/bash\n\ndubarch=\"x86_64\"\ndubtype=\"release\"\ndubconf=\"\"\n\nif [[ \"$2\" == '32' ]]; then\n  dubarch=\"x86\"\nfi\n\nif [[ \"$1\" == 'shared' ]]; then\n  dubconf=\"release-shared\"\nelif [[ \"$1\" == 'static' ]]; then\n  dubconf=\"release-static\"\nelif [[ \"$1\" == 'debug' ]]; then\n  dubconf=\"debug\"\n  dubtype=\"debug\"\nfi\n\ndubtype=\"debug\" # for remove optimizations\n\necho \"Building with:\"\necho \"config: $dubconf\"\necho \"build: $dubtype\"\necho \"arch: $dubarch\"\necho \"\"\ndub build --config=$dubconf --build=$dubtype --arch=$dubarch --force\n"
  },
  {
    "path": "buildWithLDC.sh",
    "content": "#!/bin/bash\n\nexport DFLAGS=\"-disable-linker-strip-dead $@\"\ndub build --force --compiler=ldc\n\n"
  },
  {
    "path": "buildWrapper.sh",
    "content": "#!/bin/bash\n\n# buidWrapper [ver] [64/32]\n\nTARCH=\"$2\"\nNAME=\"vk-$1-$TARCH\"\nNAMEBIN=\"$NAME-bin\"\nNAMEPACK=\"$NAMEBIN.7z\"\n\nrm vk\n./buildRelease.sh static \"$TARCH\"\nif [[ \"$?\" -eq \"0\" ]]; then\n  mv vk \"$NAMEBIN\"\n  7z a \"$NAMEPACK\" \"$NAMEBIN\"\n  rm \"$NAMEBIN\"\nfi\n"
  },
  {
    "path": "dub.json",
    "content": "{\n  \"name\": \"vk\",\n  \"description\": \"Terminal client for vk.com\",\n  \"authors\": [\"HaCk3D\", \"substanceof\"],\n  \"homepage\": \"https://github.com/HaCk3Dq/vk\",\n  \"license\": \"Apache-2.0\",\n  \"dependencies\": {\n    \"ncurses\": \"*\"\n  },\n  \"targetType\": \"executable\",\n  \"mainSourceFile\": \"source/app.d\",\n  \"platfroms\": [\"posix\"],\n  \"preGenerateCommands\": [\"rdmd generateVersion.d\"],\n  \"configurations\": [\n    {\n      \"name\": \"debug\",\n      \"buildRequirements\": [\"allowWarnings\"],\n    },\n    {\n      \"name\": \"release-shared\",\n      \"buildRequirements\": [\"silenceDeprecations\"],\n    },\n    {\n      \"name\": \"release-static\",\n      \"buildRequirements\": [\"silenceDeprecations\"],\n      \"lflags\": [\"-Bstatic\"]\n    }\n  ]\n}\n"
  },
  {
    "path": "generateVersion.d",
    "content": "/*\nCopyright 2016 HaCk3D, substanceof\n\nhttps://github.com/HaCk3Dq\nhttps://github.com/substanceof\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport std.process, std.stdio, std.string, std.algorithm,\n       std.file, std.regex, std.conv;\n\nvoid main() {\n  const versionNum = \"0.7.6\";\n  const releaseFlag = false;\n  string\n    lastCommitHash,\n    currentBranch;\n\n  if(!releaseFlag) {\n    lastCommitHash = matchFirst(executeShell(\"git log -1\").output, regex(r\"(?:^commit\\s+)([0-9a-f]{40})\"))[1][0..7];\n    currentBranch = matchFirst(executeShell(\"git status\").output, regex(r\"(?:^On branch\\s+)(.+)\"))[1];\n  }\n\n  auto verTemplate = \"\nmodule vkversion;\n\nconst string\n  currentVersion = \\\"master\\\";\n\n\";\n\n  auto fileName = \"source/vkversion.d\";\n  string[] text;\n\n  if(!exists(fileName)) text = verTemplate.split(\"\\n\");\n  else text = readText(fileName).split(\"\\n\");\n\n  auto reg = regex(\"(^\\\\s*currentVersion\\\\s*=\\\\s*\\\")(.+)(\\\"\\\\s*;)\");\n  foreach (ref line; text) {\n    auto match = matchFirst(line, reg);\n    if (match.length == 4) {\n      auto versionString = releaseFlag ? versionNum : versionNum ~ \"-\" ~ currentBranch ~ \"-\" ~ lastCommitHash;\n      writeln(\"version string: \" ~ versionString);\n      line = match[1] ~ versionString ~ match[3];\n      break;\n    }\n  }\n  auto f = File(fileName, \"w\");\n  f.write(text.join(\"\\n\"));\n  f.close;\n}\n"
  },
  {
    "path": "source/.editorconfig",
    "content": "root = true\n\n[*]\nident_style = space\ndfmt_brace_style = stroustrup\ndfmt_space_after_cast = false\ndfmt_space_after_keywords = true\n\n[vkapi.d]\nident_size = 4\n\n[app.d]\nident_size = 2\n\n[musicplayer.d]\nident_size = 2\n"
  },
  {
    "path": "source/app.d",
    "content": "/*\nCopyright 2016 HaCk3D, substanceof\n\nhttps://github.com/HaCk3Dq\nhttps://github.com/substanceof\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport deimos.ncurses.ncurses;\nimport core.stdc.locale, core.thread, core.stdc.stdlib:exit;\nimport core.sys.posix.signal;\nimport std.string, std.stdio, std.process,\n       std.conv, std.array, std.encoding,\n       std.range, std.algorithm, std.concurrency,\n       std.datetime, std.utf, std.regex, std.random,\n       std.math, std.json;\nimport vkapi, cfg, localization, utils, namecache, musicplayer, vkversion;\nimport magicstringz;\n\n// INIT VARS\nenum Sections { left, right }\nenum Buffers { none, friends, dialogs, music, chat, help, settings }\nenum Colors { white, red, green, yellow, blue, pink, mint, gray }\nenum DrawSetting { allMessages, onlySelectedMessage, onlySelectedMessageAndUnread }\n\n__gshared {\n  string[string] storage;\n  Win win;\n  VkMan api;\n}\n\npublic:\n\nstruct ListElement {\n  string name, text;\n  void function(ref ListElement) callback;\n  ListElement[] function() getter;\n  bool flag;\n  int id;\n  bool isConference;\n}\n\nvoid Exit(string msg = \"\", int ecode = 0, bool normalExit = false) {\n  endwin;\n\n  if (normalExit) {\n    mplayer.exitPlayer();\n  }\n  else if (mplayer !is null && mplayer.player !is null) {\n    mplayer.player.killPlayer();\n    writeln(\"player killed\");\n  }\n\n  if (msg != \"\") {\n    //writeln(\"FAIL\");\n    writeln(msg);\n  }\n  exit(ecode);\n}\n\nvkAudio[] getShuffledMusic(int count, int offset) {\n  if (win.workaroundCounter == floor(api.getServerCount(blockType.music)/100.0)+2) win.shuffleLoadingIsOver = true;\n  if (!win.shuffleLoadingIsOver) {\n    win.workaroundCounter++;\n    win.shuffledMusic = api.getBufferedMusic(api.getServerCount(blockType.music), 0);\n    auto step = (win.shuffledMusic.length.to!real / api.getServerCount(blockType.music).to!real) * 20;\n    (\"[\" ~ \"=\".replicatestr(floor(step).to!int) ~ \"|\" ~ \"=\".replicatestr(20 - floor(step).to!int) ~ \"]\").SetStatusbar;\n  } else {\n    if (!win.shuffled) {\n      SetStatusbar;\n      randomShuffle(win.shuffledMusic);\n      win.shuffled = true;\n      win.savedShuffledLen = win.shuffledMusic.length.to!int;\n    } else\n      return win.shuffledMusic[offset..offset+count];\n  }\n  return api.getBufferedMusic(count, offset);\n}\n\nprivate:\n\nconst int\n  // func keys\n  k_up           = -2,\n  k_down         = -3,\n  k_right        = -4,\n  k_left         = -5,\n  k_home         = -6,\n  k_ins          = -7,\n  k_del          = -8,\n  k_end          = -9,\n  k_pageup       = -10,\n  k_pagedown     = -11,\n  k_enter        = 10,\n  k_esc          = 27,\n  k_tab          = 8,\n  k_ctrl_bckspc  = 9,\n  k_prev         = 91,\n  k_rus_prev     = 133,\n  k_next         = 93,\n  k_rus_next     = 138,\n  k_o            = 111,\n  k_rus_o        = 137,\n  k_m            = 109,\n  k_rus_m        = 140,\n  kg_rew_bck     = 60,\n  kg_rew_fwd     = 62,\n  kg_rew_bck_rus = 145,\n  kg_rew_fwd_rus = 174,\n\n  // keys\n  k_q        = 113,\n  k_rus_q    = 185,\n  k_p        = 112,\n  k_rus_p    = 183,\n  k_r        = 114,\n  k_rus_r    = 186,\n  k_bckspc   = 127,\n  k_w        = 119,\n  k_s        = 115,\n  k_shift_s  = 83,\n  k_shift_rus_s = 171,\n  k_a        = 97,\n  k_d        = 100,\n  k_rus_w    = 134,\n  k_rus_a    = 132,\n  k_rus_s    = 139,\n  k_rus_d    = 178,\n  k_shift_d  = 68,\n  k_shift_rus_d  = 146,\n  k_k        = 107,\n  k_j        = 106,\n  k_h        = 104,\n  k_l        = 108,\n  k_rus_h    = 128,\n  k_rus_j    = 190,\n  k_rus_k    = 187,\n  k_rus_l    = 180,\n  k_shift_l  = 76,\n  k_shift_rus_l  = 148;\n\nconst int[]\n  // key groups\n  kg_esc     = [k_q, k_rus_q],\n  kg_refresh = [k_r, k_rus_r],\n  kg_up      = [k_up, k_w, k_k, k_rus_w, k_rus_k],\n  kg_down    = [k_down, k_s, k_j, k_rus_s, k_rus_j],\n  kg_left    = [k_left, k_a, k_h, k_rus_a, k_rus_h],\n  kg_right   = [k_right, k_d, k_l, k_rus_d, k_rus_l, k_enter],\n  kg_shift_right = [k_shift_d, k_shift_rus_d, k_shift_l, k_shift_rus_l],\n  kg_shift_s = [k_shift_s, k_shift_rus_s],\n  kg_ignore  = [k_right, k_left, k_up, k_down, k_bckspc, k_esc,\n                k_pageup, k_pagedown, k_end, k_ins, k_del,\n                k_home, k_tab, k_ctrl_bckspc],\n  kg_pause   = [k_p, k_rus_p],\n  kg_loop    = [k_o, k_rus_o],\n  kg_mix     = [k_m, k_rus_m],\n  kg_prev    = [k_prev, k_rus_prev],\n  kg_next    = [k_next, k_rus_next],\n  kg_rewind_backward = [kg_rew_bck, kg_rew_bck_rus],\n  kg_rewind_forward  = [kg_rew_fwd, kg_rew_fwd_rus];\n\nstring getChar(string charName) {\n  if (win.unicodeChars) {\n    switch (charName) {\n      case \"unread\" : return \"⚫ \";\n      case \"fwd\"    : return \"➥ \";\n      case \"play\"   : return \" ▶  \";\n      case \"pause\"  : return \" ▮▮ \";\n      case \"outbox\" : return \" ⇡ \";\n      case \"inbox\"  : return \" ⇣ \";\n      case \"cross\"  : return \" ✖ \";\n      case \"mail\"   : return \" ✉ \";\n      case \"refresh\": return \" ⟲\";\n      case \"repeat\" : return \"⟲ \";\n      case \"shuffle\": return \"⤮\";\n      default       : return charName;\n    }\n  } else {\n    switch(charName) {\n      case \"unread\" : return \"! \";\n      case \"fwd\"    : return \"fwd \";\n      case \"play\"   : return \" >  \";\n      case \"pause\"  : return \" || \";\n      case \"outbox\" : return \" ^ \";\n      case \"inbox\"  : return \" v \";\n      case \"cross\"  : return \" X \";\n      case \"mail\"   : return \" M \";\n      case \"refresh\": return \" ?\";\n      case \"repeat\" : return \"o \";\n      case \"shuffle\": return \"x\";\n      default       : return charName;\n    }\n  }\n}\n\nstruct Notify {\n  string text;\n  TimeOfDay\n    currentTime,\n    clearTime;\n}\n\nstruct Cursor {\n  int x, y;\n}\n\nstruct Track {\n  string artist, title, duration;\n}\n\nstruct Win {\n  ListElement[]\n  menu = [\n    {callback:&open, getter: &GetFriends},\n    {callback:&open, getter: &GetDialogs},\n    {callback:&open, getter: &GetMusic},\n    {callback:&open, getter: &GenerateHelp},\n    {callback:&open, getter: &GenerateSettings},\n    {callback:&exit}\n  ],\n  buffer, mbody, playerUI;\n  vkAudio[] shuffledMusic;\n  Notify notify;\n  Cursor cursor;\n  int\n    namecolor = Colors.white,\n    textcolor = Colors.gray,\n    counter, active, section,\n    menuActive, menuOffset = 15, key,\n    scrollOffset, msgDrawSetting,\n    activeBuffer, chatID, lastBuffer,\n    lastScrollOffset, lastScrollActive,\n    msgBufferSize, seekValue = 15,\n    savedShuffledLen, workaroundCounter;\n  string\n    statusbarText, msgBuffer;\n  bool\n    isMusicPlaying, isConferenceOpened,\n    isRainbowChat, isRainbowOnlyInGroupChats,\n    isMessageWriting, showTyping, selectFlag,\n    showConvNotifications, sendOnline,\n    unicodeChars = true, shuffled, seekPercentFlag,\n    shuffleLoadingIsOver, isInternalError;\n}\n\nvoid relocale() {\n  win.menu[0].name = \"m_friends\".getLocal;\n  win.menu[1].name = \"m_conversations\".getLocal;\n  win.menu[2].name = \"m_music\".getLocal;\n  win.menu[3].name = \"m_help\".getLocal;\n  win.menu[4].name = \"m_settings\".getLocal;\n  win.menu[5].name = \"m_exit\".getLocal;\n}\n\nvoid parse(ref string[string] storage) {\n  if (\"main_color\" in storage) win.namecolor = storage[\"main_color\"].to!int;\n  if (\"second_color\" in storage) win.textcolor = storage[\"second_color\"].to!int;\n  if (\"message_setting\" in storage) win.msgDrawSetting = storage[\"message_setting\"].to!int;\n  if (\"lang\" in storage) if (storage[\"lang\"] != getLang) swapLang;\n  if (\"rainbow\" in storage) win.isRainbowChat = storage[\"rainbow\"].to!bool;\n  if (\"rainbow_in_chat\" in storage) win.isRainbowOnlyInGroupChats = storage[\"rainbow_in_chat\"].to!bool;\n  if (\"show_typing\" in storage) win.showTyping = storage[\"show_typing\"].to!bool;\n  if (\"show_conv_notif\" in storage) win.showConvNotifications = storage[\"show_conv_notif\"].to!bool;\n  if (\"send_online\" in storage) win.sendOnline = storage[\"send_online\"].to!bool;\n  if (\"unicode_chars\" in storage) win.unicodeChars = storage[\"unicode_chars\"].to!bool;\n  if (\"seek_percent_or_value\" in storage) win.seekPercentFlag = storage[\"seek_percent_or_value\"].to!bool;\n\n  relocale;\n\n  if(\"longpoll_wait\" !in storage) {\n    storage[\"longpoll_wait\"] = \"25\";\n    storage.save;\n  }\n}\n\nvoid update(ref string[string] storage) {\n  storage[\"lang\"] = getLang;\n  storage[\"main_color\"] = win.namecolor.to!string;\n  storage[\"second_color\"] = win.textcolor.to!string;\n  storage[\"message_setting\"] = win.msgDrawSetting.to!string;\n  storage[\"rainbow\"] = win.isRainbowChat.to!string;\n  storage[\"rainbow_in_chat\"] = win.isRainbowOnlyInGroupChats.to!string;\n  storage[\"show_typing\"] = win.showTyping.to!string;\n  storage[\"show_conv_notif\"] = win.showConvNotifications.to!string;\n  storage[\"send_online\"] = win.sendOnline.to!string;\n  storage[\"unicode_chars\"] = win.unicodeChars.to!string;\n  storage[\"seek_percent_or_value\"] = win.seekPercentFlag.to!string;\n  storage.save;\n}\n\nvoid print(string s) {\n  s.toStringz.addstr;\n}\n\nvoid print(int i) {\n  i.to!string.toStringz.addstr;\n}\n\nstring makeLink(string login, string passwd) {\n  return \"https://oauth.vk.com/token?grant_type=password\" ~\n        \"&client_id=\" ~ appCID ~\n        \"&client_secret=\" ~ appSecret ~\n        \"&username=\" ~ login ~\n        \"&password=\" ~ passwd ~\n        \"&2fa_supported=1\";\n}\n\nstring getPassword() {\n  version (linux) {\n    import core.sys.linux.unistd;\n    return getpass(\"Password (will not be echoed): \").to!string;\n  }\n  else {\n    writeln(\"[ WARNING: password will be echoed in console ]\");\n    write(\"Password: \");\n    return readln().chomp;\n  }\n}\n\nVkMan get_token(ref string[string] storage) {\n\n  auto tm = dur!\"seconds\"(10);\n\n  write(\"Username (email or phone): \");\n  string strusr = readln().chomp;\n\n  string strpwd = getPassword();\n\n  writeln(\"\\nLogging in..\");\n\n  string url = makeLink(strusr, strpwd);\n  auto got = AsyncMan.httpget(url, tm, 10);\n\n  JSONValue resp = got.parseJSON;\n  if(\"validation_type\" in resp && (resp[\"validation_type\"].str==\"2fa_sms\" || resp[\"validation_type\"].str==\"2fa_app\")){\n    if(resp[\"validation_type\"].str==\"2fa_sms\")\n      write(\"SMS Code (\"~ resp[\"phone_mask\"].str ~\"): \");\n    else if(resp[\"validation_type\"].str==\"2fa_app\")\n      write(\"App Code: \");\n    string strcode = readln().chomp;\n\n    url = makeLink(strusr, strpwd)~\"&code=\"~strcode;\n    got = AsyncMan.httpget(url, tm, 10);\n    resp = got.parseJSON;\n  }\n  if(\"error\" in resp) {\n    writeln(\"\\nError while auth: \" ~ got);\n    Exit();\n  }\n\n  string token = resp[\"access_token\"].str;\n  storage[\"token\"] = token;\n  storage[\"auth_v2\"] = \"true\";\n\n  return new VkMan(token);\n}\n\nvoid color() {\n  if (!has_colors) {\n    endwin;\n    writeln(\"Your terminal does not support color\");\n  }\n  start_color;\n  use_default_colors;\n  for (short i = 0; i < Colors.max; i++) init_pair(i, i, -1);\n  for (short i = 1; i < Colors.max+1; i++) init_pair((Colors.max+1+i).to!short, i, -1.to!short);\n  init_pair(Colors.max, 0, -1);\n  init_pair(Colors.max+1, -1, -1);\n  init_pair(Colors.max*2+1, 0, -1);\n}\n\nvoid selected(string text) {\n  attron(A_REVERSE);\n  text.regular;\n  attroff(A_REVERSE);\n}\n\nvoid regular(string text) {\n  attron(A_BOLD);\n  attron(COLOR_PAIR(win.namecolor));\n  text.print;\n  attroff(A_BOLD);\n  attroff(COLOR_PAIR(win.namecolor));\n}\n\nvoid colored(string text, int color) {\n  int temp = win.namecolor;\n  win.namecolor = color;\n  text.regular;\n  win.namecolor = temp;\n}\n\nvoid secondColor(string text) {\n  attron(A_BOLD);\n  attron(COLOR_PAIR(win.textcolor+Colors.max+1));\n  text.print;\n  attroff(A_BOLD);\n  attroff(COLOR_PAIR(win.textcolor+Colors.max+1));\n}\n\nvoid graySelected(string text) {\n  attron(A_REVERSE);\n  attron(A_BOLD);\n  attron(COLOR_PAIR(win.namecolor+Colors.max+1));\n  text.print;\n  attroff(A_BOLD);\n  attroff(COLOR_PAIR(win.namecolor+Colors.max+1));\n  attroff(A_REVERSE);\n}\n\nvoid regularWhite(string text) {\n  attron(COLOR_PAIR(0));\n  text.print;\n  attroff(COLOR_PAIR(0));\n}\n\nvoid white(string text) {\n  attron(A_BOLD);\n  regularWhite(text);\n  attroff(A_BOLD);\n}\n\nvoid notifyManager() {\n  string notifyMsg = api.getLastLongpollMessage.replace(\"\\n\", \" \");\n  win.notify.currentTime = cast(TimeOfDay)Clock.currTime;\n\n  if (notifyMsg != \"\" && notifyMsg != \"-1\") {\n    if (notifyMsg.utfLength > COLS - 10) win.notify.text = notifyMsg.to!wstring[0..COLS-10].to!string;\n    else win.notify.text = notifyMsg;\n    win.notify.clearTime = win.notify.currentTime + seconds(1);\n  }\n  if (win.notify.currentTime > win.notify.clearTime) {\n    win.notify.clearTime = TimeOfDay(23, 59, 59);\n    win.notify.text = \"\";\n  }\n}\n\nvoid statusbar() {\n  string counterStr;\n  notifyManager;\n  win.counter = api.messagesCounter;\n  if (win.counter == -1) {\n    win.isInternalError = true;\n    counterStr = getChar(\"cross\");\n    if (api.api.isTokenValid()) {\n      \"no_connection\".getLocal.SetStatusbar;\n    }\n    else {\n      \"e_wrong_token\".getLocal.SetStatusbar;\n    }\n  }\n  else {\n    if (win.isInternalError) SetStatusbar;\n    win.isInternalError = false;\n    counterStr = \" \" ~ win.counter.to!string ~ getChar(\"mail\");\n    if (api.isLoading) counterStr ~= getChar(\"refresh\");\n  }\n  counterStr.selected;\n  auto counterStrLen = counterStr.utfLength + (counterStr.utfLength == 7) * 2;\n  if (win.notify.text != \"\") center(win.notify.text, COLS-counterStr.utfLength, ' ').selected;\n  else center(win.statusbarText, COLS-counterStrLen, ' ').selected;\n  \" \".replicatestr((counterStr.utfLength == 7) * 2).selected;\n  \"\\n\".print;\n}\n\nvoid SetStatusbar(string s = \"\") {\n  win.statusbarText = s;\n}\n\nvoid drawMenu() {\n  foreach(i, le; win.menu) {\n    auto space = (le.name.walkLength < win.menuOffset) ? \" \".replicatestr(win.menuOffset-le.name.walkLength) : \"\";\n    auto name = le.name ~ space ~ \"\\n\";\n    if (win.section == Sections.left) i == win.active ? name.selected : name.regular;\n    else i == win.menuActive ? name.selected : name.regular;\n  }\n}\n\nstring cut(uint i, ListElement e) {\n  wstring tempText = e.text.toUTF16wrepl;\n  auto cut = (COLS-win.menuOffset-win.mbody[i].name.utfLength-1).to!uint;\n  if (e.text.utfLength > cut) tempText = tempText[0..cut];\n  return tempText.to!string;\n}\n\nvoid bodyToBuffer() {\n  switch (win.activeBuffer) {\n    case Buffers.chat: win.mbody = GetChat; break;\n    case Buffers.dialogs: win.mbody = GetDialogs; break;\n    case Buffers.friends: win.mbody = GetFriends; break;\n    case Buffers.music: win.mbody = GetMusic; break;\n    case Buffers.help: win.mbody = GenerateHelp; break;\n    case Buffers.settings: win.mbody = GenerateSettings; break;\n    default: break;\n  }\n  if (LINES-2 < win.mbody.length) win.buffer = win.mbody[0..LINES-2].dup;\n  else win.buffer = win.mbody.dup;\n  if (win.activeBuffer != Buffers.chat) {\n    foreach(i, e; win.buffer) {\n      if (e.name.utfLength.to!int + win.menuOffset+1 > COLS)\n      try {\n        win.buffer[i].name = e.name.to!wstring[0..COLS-win.menuOffset-1].to!string;\n        }\n      catch(Throwable) {\n      }\n      else\n        win.buffer[i].name ~= \" \".replicatestr(COLS - e.name.utfLength - win.menuOffset-1);\n    }\n  }\n}\n\nvoid drawDialogsList() {\n  foreach(i, e; win.buffer) {\n    wmove(stdscr, 2+i.to!int, win.menuOffset+1);\n    if (i.to!int == win.active-win.scrollOffset) {\n      e.name.selected;\n      wmove(stdscr, 2+i.to!int, win.menuOffset+win.mbody[i].name.utfLength.to!int+1);\n      cut(i.to!uint, e).graySelected;\n    } else {\n      switch (win.msgDrawSetting) {\n        case DrawSetting.allMessages:\n          allMessages(e, i.to!uint); break;\n        case DrawSetting.onlySelectedMessage:\n          onlySelectedMessage(e, i); break;\n        case DrawSetting.onlySelectedMessageAndUnread:\n          onlySelectedMessageAndUnread(e, i.to!uint); break;\n        default: break;\n      }\n    }\n  }\n}\n\nvoid allMessages(ListElement e, uint i) {\n  e.flag ? e.name.regular : e.name.secondColor;\n  wmove(stdscr, 2+i.to!int, win.menuOffset+win.mbody[i].name.walkLength.to!int+1);\n  cut(i, e).white;\n}\n\nvoid onlySelectedMessage(ListElement e, ulong i) {\n  e.flag ? e.name.regular : e.name.secondColor;\n}\n\nvoid onlySelectedMessageAndUnread(ListElement e, uint i) {\n  e.flag ? e.name.regular : e.name.secondColor;\n  if (e.name.indexOf(getChar(\"unread\")) == 0) {\n    wmove(stdscr, 2+i.to!int, win.menuOffset+win.mbody[i].name.walkLength.to!int+1);\n    cut(i, e).white;\n  }\n}\n\nvoid drawFriendsList() {\n  foreach(i, e; win.buffer) {\n    wmove(stdscr, 2+i.to!int, win.menuOffset+1);\n    if (i.to!int == win.active-win.scrollOffset) {\n      if (!e.flag) {\n        e.name[0..$-e.text.utfLength].selected;\n        e.text.selected;\n      } else e.name.selected;\n    } else if (e.flag) {\n      e.name.regular;\n    } else {\n      e.name[0..$-e.text.utfLength].secondColor;\n      e.text.secondColor;\n    }\n  }\n}\n\nvoid drawMusicList() {\n  if (win.isMusicPlaying) {\n    foreach(i, e; win.playerUI) {\n      wmove(stdscr, 2+i.to!int, win.menuOffset);\n      e.name.regular;\n    }\n    wmove(stdscr, 5, win.menuOffset+COLS/2+19);\n    mplayer.repeatMode  ? getChar(\"repeat\").regular  : getChar(\"repeat\").secondColor;\n    mplayer.shuffleMode ? getChar(\"shuffle\").regular : getChar(\"shuffle\").secondColor;\n  }\n\n  foreach(i, e; win.buffer) {\n    wmove(stdscr, win.isMusicPlaying*5+2+i.to!int, win.menuOffset+1);\n    if (!win.isMusicPlaying)\n      i.to!int == win.active-win.scrollOffset ? e.name.selected : e.name.regular;\n    else {\n      if (e.name.canFind(getChar(\"play\")) || e.name.canFind(getChar(\"pause\"))) if (i.to!int == win.active-win.scrollOffset) e.name.selected; else e.name.regular;\n      else i.to!int == win.active-win.scrollOffset ? e.name.selected : e.name.secondColor;\n    }\n  }\n}\n\nvoid drawBuffer() {\n  switch (win.activeBuffer) {\n    case Buffers.dialogs: drawDialogsList; break;\n    case Buffers.friends: drawFriendsList; break;\n    case Buffers.music: drawMusicList; break;\n    case Buffers.chat: drawChat; break;\n    default: {\n      foreach(i, e; win.buffer) {\n        wmove(stdscr, 2+i.to!int, win.menuOffset+1);\n        i.to!int == win.active ? e.name.selected : e.name.regular;\n      }\n      break;\n    }\n  }\n}\n\nint colorHash(string name) {\n  int sum;\n  foreach(e; name) sum += e;\n  return sum % 5 + 1;\n}\n\nvoid renderColoredOrRegularText(string text) {\n  if (win.isRainbowChat && (!win.isRainbowOnlyInGroupChats || win.isConferenceOpened))\n    text == api.me.first_name~\" \"~api.me.last_name ? text.secondColor : text.colored(text.colorHash);\n  else\n    text == api.me.first_name~\" \"~api.me.last_name ? text.secondColor : text.regular;\n}\n\nvoid drawChat() {\n  foreach(i, e; win.buffer) {\n    wmove(stdscr, 2+i.to!int, 1);\n    if (e.flag) {\n      if (e.id == -1) {\n        e.name.renderColoredOrRegularText;\n        \" \".replicatestr(COLS-e.name.utfLength-e.text.length-2).regular;\n        e.text.secondColor;\n      } else {\n        e.name[0..e.id].regularWhite;\n        e.name[e.id..$].renderColoredOrRegularText;\n        wmove(stdscr, 2+i.to!int, (COLS-e.text.length-1).to!int);\n        e.text.secondColor;\n      }\n    } else\n      e.name.regularWhite;\n  }\n  if (win.isMessageWriting) {\n    \"\\n: \".print;\n    win.msgBuffer.print;\n    wmove(stdscr, win.buffer.length.to!int+2, win.cursor.x+2);\n    \"\".regular;\n  }\n}\n\nint activeBufferMaxLen() {\n  switch (win.activeBuffer) {\n    case Buffers.dialogs: return api.getServerCount(blockType.dialogs);\n    case Buffers.friends: return api.getServerCount(blockType.friends);\n    case Buffers.music: return api.getServerCount(blockType.music);\n    case Buffers.chat: return api.getChatLineCount(win.chatID, COLS-12);\n    default: return 0;\n  }\n}\n\nbool activeBufferEventsAllowed() {\n  switch (win.activeBuffer) {\n    case Buffers.dialogs: return api.isScrollAllowed(blockType.dialogs);\n    case Buffers.friends: return api.isScrollAllowed(blockType.friends);\n    case Buffers.music: return api.isScrollAllowed(blockType.music);\n    case Buffers.chat: return api.isChatScrollAllowed(win.chatID);\n    default: return true;\n  }\n}\n\nvoid forceRefresh() {\n  switch (win.activeBuffer) {\n    case Buffers.dialogs: api.toggleForceUpdate(blockType.dialogs); break;\n    case Buffers.friends: api.toggleForceUpdate(blockType.friends); break;\n    case Buffers.music: api.toggleForceUpdate(blockType.music); break;\n    default: return;\n  }\n}\n\npublic void jumpToBeginning() {\n  win.active = 0;\n  win.scrollOffset = 0;\n}\n\nvoid jumpToEnd() {\n  if (win.shuffled && win.activeBuffer == Buffers.music) {\n    win.active = win.savedShuffledLen-1-1*(win.isMusicPlaying);\n    win.scrollOffset = win.savedShuffledLen-LINES+2+(win.isMusicPlaying)*4;\n  } else {\n    win.active = activeBufferMaxLen-1;\n    win.scrollOffset = activeBufferMaxLen-LINES+2+(win.activeBuffer == Buffers.music && win.isMusicPlaying)*5;\n  }\n  if (win.scrollOffset < 0) win.scrollOffset = 0;\n}\n\nint _getch() {\n  int key = getch;\n  if (key == 27) {\n    if (getch == -1) return k_esc;\n    else {\n      switch (getch) {\n        case 65: return -2;         // Up\n        case 66: return -3;         // Down\n        case 67: return -4;         // Right\n        case 68: return -5;         // Left\n        case 49: getch; return -6;  // Home\n        case 72: getch; return -6;  // Home\n        case 50: getch; return -7;  // Ins\n        case 51: getch; return -8;  // Del\n        case 52: getch; return -9;  // End\n        case 70: getch; return -9;  // End\n        case 53: getch; return -10; // Pg Up\n        case 54: getch; return -11; // Pg Down\n        default: return -1;\n      }\n    }\n  }\n  return key;\n}\n\nvoid menuSelect(int position) {\n  SetStatusbar;\n  win.section = Sections.left;\n  win.active  = position;\n  win.menu[win.active].callback(win.menu[win.active]);\n  win.menuActive = win.active;\n  if (win.activeBuffer == Buffers.music) {\n    win.active = mplayer.trackNum;\n    win.scrollOffset = mplayer.offset;\n  } else {\n    win.active = 0;\n    win.scrollOffset = 0;\n  }\n  win.section = Sections.right;\n}\n\nvoid controller() {\n  while (true) {\n    timeout(100);\n    win.key = _getch;\n    if (win.key == -1) win.selectFlag = false;\n    if (!win.isMessageWriting && (win.key == 49 || win.key == 50 || win.key == 51)) { menuSelect(win.key-49); break; }\n    else if (win.key != -1) break;\n    else if (api.isSomethingUpdated) break;\n    else if (win.activeBuffer == Buffers.music && mplayer.musicState && mplayer.playtimeUpdated) break;\n  }\n  //if (win.key != -1) win.key.print;\n  if (win.isMessageWriting) msgBufferEvents;\n  else if (canFind(kg_left, win.key)) backEvent;\n  else if (activeBufferEventsAllowed) {\n    if (win.activeBuffer != Buffers.chat) nonChatEvents;\n    else chatEvents;\n  }\n  checkBounds;\n}\n\nvoid msgBufferEvents() {\n  if (win.key == k_esc || win.key == k_enter) {\n    if (win.key == k_enter) {\n\t    if (win.msgBuffer.utfLength != 0) api.asyncSendMessage(win.chatID, win.msgBuffer);\n      else api.asyncMarkMessagesAsRead(win.chatID);\n    }\n    win.msgBuffer = \"\";\n    win.cursor.x = win.cursor.y = 0;\n    curs_set(0);\n    win.isMessageWriting = false;\n  }\n  else if (win.key == k_bckspc && win.msgBuffer.utfLength != 0 && win.cursor.x != 0) {\n    if (win.cursor.x == win.msgBuffer.utfLength) win.msgBuffer = win.msgBuffer.to!wstring[0..$-1].to!string;\n    else win.msgBuffer = win.msgBuffer.to!wstring[0..win.cursor.x-1].to!string ~ win.msgBuffer.to!wstring[win.cursor.x..$].to!string;\n    win.cursor.x--;\n    win.msgBufferSize = win.msgBuffer.utfLength.to!int;\n  }\n  else if (win.key > 0 && !canFind(kg_ignore, win.key)) {\n    try {\n      validate(win.msgBuffer);\n      win.msgBufferSize = win.msgBuffer.utfLength.to!int;\n    } catch (UTFException e) {\n      if (win.cursor.x-1 != win.msgBufferSize) {\n        int i, count, offset;\n        char chr;\n        while (count != win.cursor.x) {\n          chr = win.msgBuffer[i];\n          if (chr != 208 && chr != 209) ++count;\n          else ++offset;\n          ++i;\n        }\n        chr = win.msgBuffer[count+offset];\n        if (chr == 208 || chr == 209) --offset;\n        win.msgBuffer = win.msgBuffer[0..count+offset-1] ~ win.key.to!char ~ win.msgBuffer[count+offset-1..$];\n      }\n      else win.msgBuffer ~= win.key.to!char;\n      return;\n    }\n    if (win.cursor.x == win.msgBuffer.utfLength) win.msgBuffer ~= win.key.to!char;\n    else win.msgBuffer = win.msgBuffer.to!wstring[0..win.cursor.x].to!string ~ win.key.to!char ~ win.msgBuffer.to!wstring[win.cursor.x..$].to!string;\n    win.cursor.x++;\n    if (win.showTyping) api.setTypingStatus(win.chatID);\n  }\n  else if (win.key == k_home) win.cursor.x = 0;\n  else if (win.key == k_end) win.cursor.x = win.msgBuffer.utfLength.to!int;\n  else if (win.key == k_left && win.cursor.x != 0) win.cursor.x--;\n  else if (win.key == k_right && win.cursor.x != win.msgBuffer.utfLength) win.cursor.x++;\n}\n\nvoid globalMplayerShortcuts() {\n  if (canFind(kg_pause, win.key)) mplayer.pause;\n  if (canFind(kg_next, win.key)) {\n    if (mplayer.repeatMode) mplayer.trackNum++;\n    mplayer.trackOver;\n  }\n  if (canFind(kg_prev, win.key)) {\n    mplayer.trackNum -= 2-mplayer.repeatMode;\n    mplayer.trackOver;\n  }\n  if (canFind(kg_rewind_forward, win.key)) mplayer.player.relativeSeek(win.seekValue, win.seekPercentFlag);\n  if (canFind(kg_rewind_backward, win.key)) mplayer.player.relativeSeek(-win.seekValue, win.seekPercentFlag);\n  if (canFind(kg_loop, win.key)) mplayer.repeatMode = !mplayer.repeatMode;\n  if (canFind(kg_mix, win.key) && win.activeBuffer == Buffers.music) toggleShuffleMode;\n}\n\nvoid nonChatEvents() {\n  globalMplayerShortcuts;\n  if (canFind(kg_down, win.key)) downEvent;\n  if (canFind(kg_up, win.key)) upEvent;\n  else if (canFind(kg_right, win.key) && !win.selectFlag) {\n    win.selectFlag = true;\n    selectEvent;\n  }\n  else if (win.section == Sections.right) {\n    if (canFind(kg_refresh, win.key)) forceRefresh;\n    if (win.key == k_home) jumpToBeginning;\n    else if (win.key == k_end && win.activeBuffer != Buffers.none) jumpToEnd;\n    else if (win.key == k_pagedown && win.activeBuffer != Buffers.none) {\n      win.scrollOffset += LINES/2;\n      win.active += LINES/2;\n    }\n    else if (win.key == k_pageup && win.activeBuffer != Buffers.none) {\n      win.scrollOffset -= LINES/2;\n      win.active -= LINES/2;\n      if (win.active < 0) win.active = win.scrollOffset = 0;\n      if (win.scrollOffset < 0) win.scrollOffset = 0;\n    }\n  }\n}\n\nvoid chatEvents() {\n  globalMplayerShortcuts;\n  if (canFind(kg_up, win.key)) win.scrollOffset += 2;\n  else if (canFind(kg_down, win.key)) win.scrollOffset -= 2;\n  else if (win.key == k_pagedown) win.scrollOffset -= LINES/2;\n  else if (win.key == k_pageup) win.scrollOffset += LINES/2;\n  else if (win.key == k_home) win.scrollOffset = 0;\n  else if (canFind(kg_right, win.key)) {\n    curs_set(1);\n    win.isMessageWriting = true;\n  }\n  else if (canFind(kg_shift_right, win.key)) {\n      dbm(\"Reading from file.\\n\");\n      // TODO! Call vim to save text in vkcliTmpMsgFile.\n      string text = getMessageFromTmpFile();\n      if (!text.empty)\n\t  api.asyncSendMessage(win.chatID, text);\n  }\n  else if (canFind(kg_shift_s, win.key)) {\n    api.asyncMarkMessagesAsRead(win.chatID);\n  }\n  else if (canFind(kg_refresh, win.key)) api.toggleChatForceUpdate(win.chatID);\n  if (win.scrollOffset < 0) win.scrollOffset = 0;\n  else if (activeBufferMaxLen != -1 && win.scrollOffset > activeBufferMaxLen-LINES+3) win.scrollOffset = activeBufferMaxLen-LINES+3;\n}\n\nvoid checkBounds() {\n  if (win.activeBuffer != Buffers.none && activeBufferMaxLen > 0 && win.active > activeBufferMaxLen-1) jumpToBeginning;\n  else if(win.activeBuffer != Buffers.none && activeBufferMaxLen > 0 && win.active < 0) jumpToEnd;\n}\n\nvoid downEvent() {\n  if (win.section == Sections.left) win.active >= win.menu.length-1 ? win.active = 0 : win.active++;\n  else {\n    if (win.active == activeBufferMaxLen-1) jumpToBeginning;\n    else {\n      if (win.active-win.scrollOffset == LINES-3-(win.activeBuffer == Buffers.music && win.isMusicPlaying)*5)\n        win.scrollOffset++;\n\n      if (win.shuffled && win.activeBuffer == Buffers.music && win.scrollOffset+LINES-2-win.isMusicPlaying*4 > win.savedShuffledLen) {\n        jumpToBeginning;\n        win.active--;\n      }\n\n      if (win.activeBuffer != Buffers.none) {\n        if (activeBufferEventsAllowed) win.active++;\n      } else win.active >= win.buffer.length-1 ? win.active = 0 : win.active++;\n    }\n  }\n}\n\nvoid upEvent() {\n  if (win.section == Sections.left) win.active == 0 ? win.active = win.menu.length.to!int-1 : win.active--;\n  else {\n    if (win.activeBuffer != Buffers.none) {\n        if (win.active == 0) jumpToEnd;\n        else {\n          if (win.active == win.scrollOffset) win.scrollOffset--;\n          win.active--;\n          if (win.scrollOffset < 0) win.scrollOffset = 0;\n        }\n    } else {\n      win.active == 0 ? win.active = win.buffer.length.to!int-1 : win.active--;\n    }\n  }\n}\n\nvoid selectEvent() {\n  if (win.section == Sections.left) {\n    if (win.menu[win.active].callback) win.menu[win.active].callback(win.menu[win.active]);\n    win.menuActive = win.active;\n    if (win.activeBuffer == Buffers.music) {\n      win.active = mplayer.trackNum;\n      win.scrollOffset = mplayer.offset;\n    }\n    else win.active = 0;\n    win.section = Sections.right;\n  } else {\n    win.lastScrollOffset = win.scrollOffset;\n    win.lastScrollActive = win.active;\n    if (win.isMusicPlaying && win.activeBuffer == Buffers.music) {\n      if (win.active-win.scrollOffset >= 0)\n        win.mbody[win.active-win.scrollOffset].callback(win.mbody[win.active-win.scrollOffset]);\n    } else if (win.mbody.length != 0 && win.mbody[win.active-win.scrollOffset].callback) win.mbody[win.active-win.scrollOffset].callback(win.mbody[win.active-win.scrollOffset]);\n    if (win.menuActive == 4) storage.update;\n  }\n}\n\nvoid backEvent() {\n  if (win.section == Sections.right) {\n    if (win.lastBuffer != Buffers.none) {\n      win.scrollOffset = win.lastScrollOffset;\n      win.activeBuffer = win.lastBuffer;\n      win.lastBuffer = Buffers.none;\n      win.isConferenceOpened = false;\n      SetStatusbar;\n      if (win.scrollOffset != 0) win.active = win.lastScrollActive;\n    } else {\n      win.scrollOffset = 0;\n      win.lastScrollOffset = 0;\n      win.activeBuffer = Buffers.none;\n      win.active = win.menuActive;\n      win.section = Sections.left;\n      win.mbody = new ListElement[0];\n      win.buffer = new ListElement[0];\n    }\n  }\n}\n\nwstring[] run(string[] args) {\n  wstring[] output;\n  auto pipe = pipeProcess(args, Redirect.stdout);\n  foreach(line; pipe.stdout.byLine) output ~= to!wstring(line.idup);\n  return output;\n}\n\nvoid exit(ref ListElement le) {\n  win.key = k_q;\n}\n\nvoid open(ref ListElement le) {\n  win.mbody = le.getter();\n}\n\nvoid chat(ref ListElement le) {\n  win.chatID = le.id;\n  win.scrollOffset = 0;\n  open(le);\n  if (le.isConference) {\n    auto len = getChar(\"unread\").length;\n    if (le.name[0..len] == getChar(\"unread\")) le.name[len..$].SetStatusbar;\n    else le.name.SetStatusbar;\n    win.isConferenceOpened = true;\n  }\n  win.lastBuffer = win.activeBuffer;\n  win.activeBuffer = Buffers.chat;\n}\n\nvoid run(ref ListElement le) {\n  le.getter();\n}\n\nvoid changeLang(ref ListElement le) {\n  swapLang;\n  win.mbody = GenerateSettings;\n  relocale;\n  storage.update;\n}\n\nvoid changeMainColor(ref ListElement le) {\n  win.namecolor == Colors.max ? win.namecolor = 0 : win.namecolor++;\n  le.name = \"main_color\".getLocal ~ (\"color\"~win.namecolor.to!string).getLocal;\n}\n\nvoid changeSecondColor(ref ListElement le) {\n  win.textcolor == Colors.max ? win.textcolor = 0 : win.textcolor++;\n  le.name = \"second_color\".getLocal ~ (\"color\"~win.textcolor.to!string).getLocal;\n}\n\nvoid changeMsgSetting(ref ListElement le) {\n  win.msgDrawSetting = win.msgDrawSetting != 2 ? win.msgDrawSetting+1 : 0;\n  le.name = \"msg_setting_info\".getLocal ~ (\"msg_setting\"~win.msgDrawSetting.to!string).getLocal;\n}\n\nvoid toggleChatRender(ref ListElement le) {\n  win.isRainbowChat = !win.isRainbowChat;\n  win.mbody = GenerateSettings;\n}\n\nvoid toggleShowTyping(ref ListElement le) {\n  win.showTyping = !win.showTyping;\n  win.mbody = GenerateSettings;\n}\n\nvoid toggleUnicodeChars(ref ListElement le) {\n  win.unicodeChars = !win.unicodeChars;\n  win.mbody = GenerateSettings;\n}\n\nvoid toggleChatRenderOnlyGroup(ref ListElement le) {\n  win.isRainbowOnlyInGroupChats = !win.isRainbowOnlyInGroupChats;\n  le.name = \"rainbow_in_chat\".getLocal ~ (win.isRainbowOnlyInGroupChats.to!string).getLocal;\n}\n\nvoid toggleShowConvNotifications(ref ListElement le) {\n  win.showConvNotifications = !win.showConvNotifications;\n  api.showConvNotifications(win.showConvNotifications);\n  le.name = \"show_conv_notif\".getLocal ~ (win.showConvNotifications.to!string).getLocal;\n}\n\nvoid toggleSendOnline(ref ListElement le) {\n  win.sendOnline = !win.sendOnline;\n  api.sendOnline(win.sendOnline);\n  le.name = \"send_online\".getLocal ~ (win.sendOnline.to!string).getLocal;\n}\n\nvoid toggleSeekPercentOrValue(ref ListElement le) {\n  win.seekPercentFlag = !win.seekPercentFlag;\n  win.seekValue = win.seekPercentFlag ? 2 : 15;\n  le.name = \"seek_percent_or_value\".getLocal ~ (\"seek_\" ~ win.seekPercentFlag.to!string).getLocal;\n}\n\nListElement[] GenerateHelp() {\n  win.activeBuffer = Buffers.help;\n  return [\n    ListElement(center(\"general_navig\".getLocal, COLS-16, ' ')),\n    ListElement(\"help_move\".getLocal),\n    ListElement(\"help_select\".getLocal),\n    ListElement(\"help_jump\".getLocal),\n    ListElement(\"help_homend\".getLocal),\n    ListElement(\"help_exit\".getLocal),\n    ListElement(\"help_refr\".getLocal),\n    ListElement(\"help_123\".getLocal),\n    ListElement(\"help_pause\".getLocal),\n    ListElement(\"help_loop\".getLocal),\n    ListElement(\"help_mix\".getLocal),\n    ListElement(\"help_rewind\".getLocal),\n  ];\n}\n\nListElement[] GenerateSettings() {\n  win.activeBuffer = Buffers.settings;\n  ListElement[] list;\n  list ~= [\n    ListElement(center(\"display_settings\".getLocal, COLS-16, ' ')),\n    ListElement(\"main_color\".getLocal ~ (\"color\"~win.namecolor.to!string).getLocal, \"\", &changeMainColor),\n    ListElement(\"second_color\".getLocal ~ (\"color\"~win.textcolor.to!string).getLocal, \"\", &changeSecondColor),\n    ListElement(\"lang\".getLocal, \"\", &changeLang, null),\n    ListElement(center(\"convers_settings\".getLocal, COLS-16, ' ')),\n    ListElement(\"msg_setting_info\".getLocal ~ (\"msg_setting\"~win.msgDrawSetting.to!string).getLocal, \"\", &changeMsgSetting),\n    ListElement(\"rainbow\".getLocal ~ (win.isRainbowChat.to!string).getLocal, \"\", &toggleChatRender),\n  ];\n  if (win.isRainbowChat) list ~= ListElement(\"rainbow_in_chat\".getLocal ~ (win.isRainbowOnlyInGroupChats.to!string).getLocal, \"\", &toggleChatRenderOnlyGroup);\n  list ~= ListElement(\"show_typing\".getLocal ~ (win.showTyping.to!string).getLocal, \"\", &toggleShowTyping);\n  list ~= ListElement(\"show_conv_notif\".getLocal ~ (win.showConvNotifications.to!string).getLocal, \"\", &toggleShowConvNotifications);\n  list ~= ListElement(center(\"general_settings\".getLocal, COLS-16, ' '));\n  list ~= ListElement(\"send_online\".getLocal ~ (win.sendOnline.to!string).getLocal, \"\", &toggleSendOnline);\n  list ~= ListElement(\"unicode_chars\".getLocal ~ (win.unicodeChars.to!string).getLocal, \"\", &toggleUnicodeChars);\n  list ~= ListElement(center(\"music_settings\".getLocal, COLS-16, ' '));\n  list ~= ListElement(\"seek_percent_or_value\".getLocal ~ (\"seek_\" ~ win.seekPercentFlag.to!string).getLocal, \"\", &toggleSeekPercentOrValue);\n  return list;\n}\n\nListElement[] GetDialogs() {\n  ListElement[] list;\n  string\n    newMsg,\n    unreadText,\n    lastMsg;\n  uint space;\n\n  win.activeBuffer = Buffers.dialogs;\n  auto dialogs = api.getBufferedDialogs(LINES-2, win.scrollOffset);\n\n  if (api.dialogsFactory.getBlockObject(win.scrollOffset) !is null && dialogs.length != LINES-2 && activeBufferMaxLen > LINES-2)\n    dialogs = api.getBufferedDialogs(LINES-2, win.scrollOffset-(LINES-2-dialogs.length).to!int);\n\n  foreach(e; dialogs) {\n    unreadText = \"\";\n    newMsg = e.unread ? getChar(\"unread\") : \"  \";\n    if (e.outbox) newMsg = \"  \";\n    lastMsg = e.lastMessage.replace(\"\\n\", \" \");\n    if (lastMsg.utfLength > COLS-win.menuOffset-newMsg.utfLength-e.name.utfLength-3-e.unreadCount.to!string.length)\n      try {\n        lastMsg = lastMsg.toUTF16wrepl[0..COLS-win.menuOffset-newMsg.utfLength-e.name.utfLength-8-e.unreadCount.to!string.length].toUTF8wrepl;\n      }\n      catch(Throwable) {\n      }\n    if (e.unread) {\n      if (e.outbox) unreadText ~= getChar(\"outbox\");\n      else if (e.unreadCount > 0) unreadText ~= e.unreadCount.to!string ~ getChar(\"inbox\");\n      space = COLS-win.menuOffset-newMsg.utfLength-e.name.utfLength-lastMsg.utfLength-unreadText.utfLength-4;\n      if (space < COLS) unreadText = \" \".replicatestr(space) ~ unreadText;\n      else unreadText = \"   \" ~ unreadText;\n    }\n    list ~= ListElement(newMsg ~ e.name, \": \" ~ lastMsg ~ unreadText, &chat, &GetChat, e.online, e.id, e.isChat);\n  }\n  return list;\n}\n\nListElement[] GetFriends() {\n  ListElement[] list;\n  win.activeBuffer = Buffers.friends;\n  auto friends = api.getBufferedFriends(LINES-2, win.scrollOffset);\n\n  if (api.friendsFactory.getBlockObject(win.scrollOffset) !is null && friends.length != LINES-2 && activeBufferMaxLen > LINES-2)\n    friends = api.getBufferedFriends(LINES-2, win.scrollOffset-(LINES-2-friends.length).to!int);\n\n  foreach(e; friends)\n    list ~= ListElement(e.first_name ~ \" \" ~ e.last_name, e.last_seen_str, &chat, &GetChat, e.online, e.id);\n  return list;\n}\n\nListElement[] setCurrentTrack() {\n  if (!mplayer.player.isPlayerInit) \"err_noplayer\".getLocal.SetStatusbar;\n  else {\n    vkAudio track;\n    if (!win.isMusicPlaying && LINES-3-(win.active-win.scrollOffset) <= 5) win.scrollOffset += 5-(LINES-3-(win.active-win.scrollOffset));\n    if (win.isMusicPlaying && mplayer.sameTrack(win.active)) mplayer.pause;\n    else {\n      mplayer.play(win.active);\n      mplayer.offset = win.scrollOffset;\n      win.isMusicPlaying = true;\n    }\n  }\n  return new ListElement[0];\n}\n\nvoid toggleShuffleMode() {\n  mplayer.shuffleMode = !mplayer.shuffleMode;\n  if (mplayer.shuffleMode) {\n    randomShuffle(win.shuffledMusic);\n    jumpToBeginning;\n    mplayer.offset = win.scrollOffset;\n  }\n}\n\nListElement[] GetMusic() {\n  ListElement[] list;\n  string space, artistAndSong;\n  int amount;\n  vkAudio[] music;\n\n  win.activeBuffer = Buffers.music;\n  if (mplayer.shuffleMode)\n    music = getShuffledMusic(LINES-2-win.isMusicPlaying*4, win.scrollOffset);\n  else\n    music = api.getBufferedMusic(LINES-2-win.isMusicPlaying*4, win.scrollOffset);\n  win.playerUI = mplayer.getMplayerUI(COLS);\n\n  foreach(e; music) {\n    string indicator = (mplayer.currentTrack.id == e.id.to!string) ? mplayer.musicState ? getChar(\"play\") : getChar(\"pause\") : \"    \";\n    artistAndSong = indicator ~ e.artist ~ \" - \" ~ e.title;\n\n    int width = COLS-4-win.menuOffset-e.duration_str.length.to!int;\n    if (artistAndSong.utfLength > width) {\n      artistAndSong = artistAndSong[0..width];\n      amount = COLS-6-win.menuOffset-artistAndSong.utfLength.to!int;\n    } else amount = COLS-9-win.menuOffset-e.artist.utfLength.to!int-e.title.utfLength.to!int-e.duration_str.length.to!int;\n\n    space = \" \".replicatestr(amount);\n    list ~= ListElement(artistAndSong ~ space ~ e.duration_str, e.url, &run, &setCurrentTrack);\n  }\n  return list;\n}\n\nListElement[] GetChat() {\n  ListElement[] list;\n  int verticalOffset;\n  try {\n    validate(win.msgBuffer);\n    verticalOffset = win.msgBuffer.utfLength.to!int/COLS-1;\n  } catch (UTFException e) { verticalOffset = win.msgBufferSize/COLS-1; }\n  auto chat = api.getBufferedChatLines(LINES-4-verticalOffset, win.scrollOffset, win.chatID, COLS-12);\n  foreach(e; chat) {\n    if (e.isFwd) {\n      ListElement line = {\"    \" ~ \"| \".replicatestr(e.fwdDepth)};\n      if (e.isName && !e.isSpacing) {\n        line.flag = true;\n        line.id = line.name.length.to!int + 4;\n        line.name ~= getChar(\"fwd\") ~ e.text;\n        line.text = e.time;\n      } else\n        line.name ~= e.text;\n      list ~= line;\n    } else {\n      string unreadSign = e.unread ? getChar(\"unread\") : \" \";\n      list ~= !e.isName ? ListElement(\"  \" ~ unreadSign ~ e.text) : ListElement(e.text, e.time, null, null, true, -1);\n    }\n  }\n  return list;\n}\n\nvoid test() {\n    //initFileDbm();\n    localize();\n    auto storage = load;\n    if(\"token\" !in storage) {\n        writeln(\"cyka\");\n        return;\n    }\n    /*auto api = new VKapi(storage[\"token\"]);\n    if(!api.isTokenValid) {\n        writeln(\"bad token\");\n        return;\n    }\n\n    int i = 0;\n    while(true) {\n        readln();\n        auto pr = 2000000012;\n        if(i > 4) {\n            i = 0;\n            pr = 2000000023;\n        }\n        api.setTypingStatus(pr);\n        ++i;\n    }*/\n}\n\nvoid clear() {\n  for (int y = 0; y < LINES; y++) {\n    wmove(stdscr, y, 0);\n    print(\" \".replicatestr(COLS));\n  }\n  wmove(stdscr, 0, 0);\n}\n\nvoid help() {\n  writeln(\n    // these help can be generated from actions (array -> hashmap)\n    \"-h, --help      This help page\" ~ \"\\n\" ~\n    \"-v, --version   Show client version\" ~ \"\\n\" ~\n    \"-r --reauth     Receive new auth token\" ~ \"\\n\" ~\n    \"Logs are here:  /tmp/vkcli-log/\"\n  );\n}\n\nvoid init() {\n  updateGcSignals();\n  setPosixSignals();\n  setlocale(LC_CTYPE,\"\");\n  win.lastBuffer = Buffers.none;\n  setEnvLanguage;\n  localize;\n  relocale;\n}\n\nvoid main(string[] args) {\n  string[] actions = [\"version\", \"help\", \"reauth\"];\n  bool correct = false, reauth = false;\n\n  if (args.length != 1) {\n    foreach(arg; args) {\n      foreach(act; actions) {\n        if (arg == \"-\" ~ act[0] || arg == \"--\" ~ act) {\n          correct = true;\n          final switch (act) {\n            case \"version\": writefln(\"vk-cli %s\", currentVersion); exit(0); break;\n            case \"help\": help(); exit(0); break;\n            case \"reauth\": reauth = true; break;\n          }\n        }\n      }\n    }\n    if (!correct) writeln(\"wrong arguments\");\n  }\n\n  //test;\n  initdbm();\n  init();\n  storage = load();\n  storage.parse();\n\n  try\n    if(reauth == true || \"token\" !in storage || \"auth_v2\" !in storage) {\n      api = storage.get_token;\n      storage.save;\n    }\n    else api = new VkMan(storage[\"token\"]);\n  catch\n    (BackendException e) Exit(e.msg);\n\n  initscr();\n  color();\n  curs_set(0);\n  noecho;\n\n  mplayer = new MusicPlayer;\n  mplayer.startPlayer(api);\n  try\n    api.setLongpollWait(storage[\"longpoll_wait\"].to!int);\n  catch(Exception)\n    dbm(\"failed set longpoll_wait from config\");\n  api.showConvNotifications(win.showConvNotifications);\n  api.sendOnline(win.sendOnline);\n\n  while (!canFind(kg_esc, win.key) || win.isMessageWriting) {\n    clear;\n    statusbar;\n    if (win.activeBuffer != Buffers.chat) drawMenu;\n    bodyToBuffer;\n    drawBuffer;\n    refresh;\n    controller;\n  }\n\n  Exit(\"\", 0, true);\n}\n"
  },
  {
    "path": "source/cfg.d",
    "content": "/*\nCopyright 2016 HaCk3D, substanceof\n\nhttps://github.com/HaCk3Dq\nhttps://github.com/substanceof\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nmodule cfg;\n\nimport std.path, std.stdio, std.file, std.string;\n\nstring[string] load() {\n  string[string] storage;\n  auto config = expandTilde(\"~/.vkrc\");\n  if (config.exists) {\n    auto f = File(config, \"r\");\n    while (!f.eof) {\n      auto line = f.readln.strip;\n      if (line.length != 0) storage[line[0..line.indexOf(\"=\")-1]] = line[line.indexOf(\"=\")+2..$];\n    }\n    f.close;\n  }\n  return storage;\n}\n\nvoid save(string[string] stor) {\n  auto config = expandTilde(\"~/.vkrc\");\n  auto f = File(config, \"w\");\n  foreach(key, value; stor) {\n    f.write(key ~ \" = \" ~ value ~ \"\\n\");\n  }\n  f.close;\n}"
  },
  {
    "path": "source/localization.d",
    "content": "/*\nCopyright 2016 HaCk3D, substanceof\n\nhttps://github.com/HaCk3Dq\nhttps://github.com/substanceof\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nmodule localization;\n\nimport std.stdio, std.conv, std.string, std.process, utils;\n\nstruct lang {\n  string en;\n  string ru;\n}\n\nconst int\n  En = 0,\n  Ru = 1;\n\n__gshared {\n  private lang[string] local;\n  private int currentLang = 0;\n}\n\nvoid setEnvLanguage() {\n  string output = environment[\"LANG\"];\n  currentLang = output.indexOf(\"RU\") != -1 ? Ru : En;\n}\n\nvoid localize() {\n  local[\"e_start_browser\"]       = lang(\"You need to copy access token from your web browser. Would you like to launch it now? [Y/n] \",\n                                        \"Необходимо скопировать ваш access token из веб-браузера. Хотите запустить его сейчас? [Y/n] \");\n\n  local[\"e_token_link\"]          = lang(\"Follow this link to get your access_token: \",\n                                        \"Перейдите по следующей ссылке, чтобы получить ваш access_token: \");\n  \n  local[\"e_token_info\"]          = lang(\"Please allow access to your data and then copy link from the address bar.\",\n                                        \"Пожалуйста, разрешите приложению доступ к вашим данным, а затем скопируйте содержимое адресной строки.\");\n  \n  local[\"e_input_token\"]         = lang(\"Insert your link here: \",\n                                        \"Вставьте адрес из браузера сюда:  \");\n  \n  local[\"e_wrong_token\"]         = lang(\"Wrong token, try again with -r\",\n                                        \"Неверный access token, попробуйте еще раз с ключом -r\");\n  \n  local[\"m_friends\"]             = lang(\"Friends\",\n                                        \"Друзья\");\n  \n  local[\"m_conversations\"]       = lang(\"Conversations\",\n                                        \"Диалоги\");\n  \n  local[\"m_music\"]               = lang(\"Music\",\n                                        \"Музыка\");\n  \n  local[\"m_help\"]                = lang(\"Help\",\n                                        \"Помощь\");\n  \n  local[\"m_settings\"]            = lang(\"Settings\",\n                                        \"Настройки\");\n  \n  local[\"m_exit\"]                = lang(\"Exit\",\n                                        \"Выход\");\n  \n  local[\"c_kick\"]                = lang(\"kicked out \",\n                                        \"исключил из беседы пользователя \");\n\n  local[\"c_invite\"]              = lang(\"invited \",\n                                        \"пригласил пользователя \");\n\n  local[\"c_kickself\"]            = lang(\"left the conversation\",\n                                        \"покинул беседу\");\n\n  local[\"c_inviteself\"]          = lang(\"returned to the conversation\",\n                                        \"вернулся в беседу\");\n\n  local[\"c_create\"]              = lang(\"created chat \",\n                                        \"создал чат \");\n\n  local[\"c_title\"]               = lang(\"changed chat title to \",\n                                        \"изменил название беседы на \");\n\n  local[\"c_setphoto\"]            = lang(\"updated chat photo\", \n                                        \"обновил фотографию беседы\");\n\n  local[\"c_removephoto\"]         = lang(\"removed chat photo\", \n                                        \"удалил фотографию беседы\");\n  \n  local[\"main_color\"]            = lang(\"Main color = \",\n                                        \"Основной цвет = \");\n  \n  local[\"second_color\"]          = lang(\"Second color = \",\n                                        \"Дополнительный цвет = \");\n  \n  local[\"color0\"]                = lang(\"White\",\n                                        \"Белый\");\n  \n  local[\"color1\"]                = lang(\"Red\",\n                                        \"Красный\");\n  \n  local[\"color2\"]                = lang(\"Green\",\n                                        \"Зеленый\");\n  \n  local[\"color3\"]                = lang(\"Yellow\",\n                                        \"Желтый\");\n  \n  local[\"color4\"]                = lang(\"Blue\",\n                                        \"Синий\");\n  \n  local[\"color5\"]                = lang(\"Pink\",\n                                        \"Розовый\");\n  \n  local[\"color6\"]                = lang(\"Mint\",\n                                        \"Мятный\");\n  \n  local[\"color7\"]                = lang(\"Gray\",\n                                        \"Серый\");\n  \n  local[\"lang\"]                  = lang(\"Language = English\",\n                                        \"Язык = Русский\");\n  \n  local[\"display_settings\"]      = lang(\"[ Display Settings ]\",\n                                        \"[ Настройки отображения ]\");\n  \n  local[\"convers_settings\"]      = lang(\"[ Conversations Settings ]\",\n                                        \"[ Настройки диалогов ]\");\n  \n  local[\"msg_setting_info\"]      = lang(\"How to draw conversations list: \",\n                                        \"Как отображать список диалогов: \");\n  \n  local[\"msg_setting0\"]          = lang(\"show everything\",\n                                        \"показывать всё\");\n  \n  local[\"msg_setting1\"]          = lang(\"show the selected text only\",\n                                        \"текст только выделенного диалога\");\n  \n  local[\"msg_setting2\"]          = lang(\"show the selected text and unread ones\",\n                                        \"текст выделенного диалога и всех непрочитанных\");\n  \n  local[\"loading\"]               = lang(\"Loading\",\n                                        \"Загрузка\");\n  \n  local[\"general_navig\"]         = lang(\"[ General navigation ]\",\n                                        \"[ Общее управление ]\");\n  \n  local[\"help_move\"]             = lang(\"Arrow keys, WASD, HJKL         ->   Move cursor\",\n                                        \"Стрелки, WASD, HJKL            ->   Двигать курсор\");\n  \n  local[\"help_select\"]           = lang(\"Enter, right arrow key, D, L   ->   Select item\",\n                                        \"Enter, стрелка вправо, D, L    ->   Выбрать элемент\");\n  \n  local[\"help_jump\"]             = lang(\"Page Up/Down                   ->   Scroll up/down for a half of screen\",\n                                        \"Page Up/Down                   ->   Прокрутить вверх/вниз на половину экрана\");\n  \n  local[\"help_homend\"]           = lang(\"Home/End                       ->   Jump to the beginning/end\",\n                                        \"Home/End                       ->   Прыгнуть в начало/конец\");\n  \n  local[\"help_exit\"]             = lang(\"Q                              ->   Exit\",\n                                        \"Q                              ->   Выход\");\n  \n  local[\"help_refr\"]             = lang(\"R                              ->   Refresh window\",\n                                        \"R                              ->   Обновить окно\");\n  \n  local[\"help_pause\"]            = lang(\"P                              ->   Pause music\",\n                                        \"P                              ->   Остановить музыку\");\n  \n  local[\"help_loop\"]             = lang(\"O                              ->   Toggle looping\",\n                                        \"O                              ->   Поставить на повтор\");\n  \n  local[\"help_mix\"]              = lang(\"M                              ->   Shuffle tracks\",\n                                        \"M                              ->   Перемешать треки\");\n  \n  local[\"help_123\"]              = lang(\"1-3                            ->   Friends, Chats, Music\",\n                                        \"1-3                            ->   Друзья, Сообщения, Музыка\");\n  \n  local[\"rainbow\"]               = lang(\"Render rainbow in chat: \",\n                                        \"Рисовать радугу в диалогах: \");\n  \n  local[\"unicode_chars\"]         = lang(\"Use unicode characters: \",\n                                        \"Использовать символы Unicode: \");\n  \n  local[\"true\"]                  = lang(\"On\",\n                                        \"Да\");\n  \n  local[\"false\"]                 = lang(\"Off\",\n                                        \"Нет\");\n  \n  local[\"rainbow_in_chat\"]       = lang(\"Color only in group chats: \",\n                                        \"Выделять цветом только конференции: \");\n  \n  local[\"sending\"]               = lang(\"Sending\",\n                                        \"Отправка\");\n  \n  local[\"sendfailed\"]            = lang(\"Failed\",\n                                        \"Ошибка\");\n  \n  local[\"show_typing\"]           = lang(\"Show that you are typing a message: \",\n                                        \"Показывать, что вы набираете сообщение: \");\n  \n  local[\"show_conv_notif\"]       = lang(\"Show notifications from conferences: \",\n                                        \"Показывать уведомления из конференций: \");\n  \n  local[\"send_online\"]           = lang(\"Send that you are online: \",\n                                        \"Показывать, что вы онлайн: \");\n  \n  local[\"banned\"]                = lang(\"banned\",\n                                        \"заблокирован\");\n  \n  local[\"general_settings\"]      = lang(\"[ General Settings ]\",\n                                        \"[ Общие настройки ]\");\n  \n  local[\"no_connection\"]         = lang(\"No connection\",\n                                        \"Нет соединения\");\n  \n  local[\"err_noplayer\"]          = lang(\"mpv is not installed or not working properly\",\n                                        \"mpv не установлен или работает неверно\");\n  \n  local[\"music_settings\"]        = lang(\"[ Music Settings ]\",\n                                        \"[ Настройки музыки ]\");\n  \n  local[\"seek_percent_or_value\"] = lang(\"Rewind track by: \",\n                                        \"Перематывать трек по: \");\n  \n  local[\"seek_true\"]             = lang(\"2 %\",\n                                        \"2 %\");\n  \n  local[\"seek_false\"]            = lang(\"15 sec\",\n                                        \"15 сек\");\n  \n  local[\"help_rewind\"]           = lang(\"< >                            ->   Rewind track\",\n                                        \"< >                            ->   Перематывать трек\");\n}\n\nvoid swapLang() {\n  currentLang = (currentLang == En) ? Ru : En;\n}\n\nstring getLang() {\n  return currentLang.to!string;\n}\n\nstring getLocal(string id) {\n  return currentLang == En ? local[id].en : local[id].ru;\n}\n"
  },
  {
    "path": "source/musicplayer.d",
    "content": "/*\nCopyright 2016 HaCk3D, substanceof\n\nhttps://github.com/HaCk3Dq\nhttps://github.com/substanceof\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport std.process, std.stdio, std.string,\n       std.array, std.algorithm, std.conv,\n       std.math, std.ascii,\n       std.socket, std.json;\nstatic import std.file;\nimport core.sys.posix.signal;\nimport core.thread;\nimport app, utils;\nimport vkapi: VkMan, blockType, vkAudio;\n\nstruct Track {\n  string artist, title, duration, playtime, id;\n  int durationSeconds;\n}\n\n__gshared MusicPlayer mplayer;\n__gshared VkMan api;\n\nclass MusicPlayer {\n  __gshared {\n    mpv player;\n    Track currentTrack;\n    bool\n      playtimeUpdated,\n      trackOverStateCatched = true, //for reject empty strings before playback starts\n      repeatMode,\n      shuffleMode;\n    Track[] playlist;\n    string\n      stockProgress = \"=\".replicate(50),\n      realProgress  = \"|\" ~ \"=\".replicate(49);\n    int position, trackNum, offset;\n  }\n\n  const updateWait = dur!\"msecs\"(1000);\n\n  this() {\n    player = new mpv(\n      sec => setPlaytime(sec),\n      {\n        if(!trackOverStateCatched) trackOver();\n      }\n    );\n  }\n\n  void exitPlayer() {\n    player.exit();\n  }\n\n  bool musicState() {\n    return player.getMusicState();\n  }\n\n  void pause() {\n    player.pause();\n  }\n\n  bool playerExit() {\n    return player.isPlayerExit();\n  }\n\n  bool isInit() {\n    return player.isPlayerInit();\n  }\n\n  void play(int position) {\n    trackOverStateCatched = true;\n    trackNum = position;\n\n    vkAudio track;\n    if (mplayer.shuffleMode)\n      track = getShuffledMusic(1, position)[0];\n    else\n      track = api.getBufferedMusic(1, position)[0];\n\n    currentTrack = Track(track.artist, track.title, track.duration_str, \"\", track.id.to!string, track.duration_sec);\n    loadFile(track.url);\n  }\n\n  void loadFile(string url) {\n    trackOverStateCatched = true;\n    realProgress = \"|\" ~ \"=\".replicate(49);\n    auto p = prepareTrackUrl(url);\n    player.loadfile(p);\n  }\n\n  void startPlayer(VkMan vkapi) {\n    currentTrack.playtime = \"0:00\";\n    api = vkapi;\n    player.start();\n  }\n\n  string durToStr(real duration) {\n    auto intDuration = lround(duration);\n    auto min = intDuration / 60;\n    auto sec = intDuration - (60*min);\n    return min.to!string ~ \":\" ~ sec.to!int.tzr;\n  }\n\n  void setPlaytime(real sec) {\n    real\n      trackd = currentTrack.durationSeconds.to!real,\n      step =  trackd / 50;\n    int newPos = floor(sec / step).to!int;\n    currentTrack.playtime = durToStr(sec);\n    if (position != newPos) {\n      position = newPos;\n\n      if(newPos >= 50) newPos = 49;\n      else if (newPos < 0) newPos = 0;\n\n      auto newProgress = stockProgress.dup;\n      newProgress[newPos] = '|';\n      realProgress = newProgress.to!string;\n    }\n    playtimeUpdated = true;\n    trackOverStateCatched = false;\n  }\n\n  string prepareTrackUrl(string trackurl) {\n    if(trackurl.startsWith(\"https://\")) return trackurl.replace(\"https://\", \"http://\");\n    else return trackurl;\n  }\n\n  void trackOver() {\n    if (musicState) {\n      dbm(\"catched trackOver\");\n      trackOverStateCatched = true;\n      if (!repeatMode) trackNum++;\n      int amountOfTracks = mplayer.shuffleMode ? win.savedShuffledLen : api.getServerCount(blockType.music);\n      if (trackNum == amountOfTracks) {\n        jumpToBeginning;\n        mplayer.trackNum = win.active;\n        mplayer.offset = win.scrollOffset;\n      }\n\n      vkAudio track;\n      if (mplayer.shuffleMode)\n        track = getShuffledMusic(1, trackNum)[0];\n      else\n        track = api.getBufferedMusic(1, trackNum)[0];\n\n      loadFile(track.url);\n      currentTrack = Track(track.artist, track.title, track.duration_str, \"\", track.id.to!string, track.duration_sec);\n    }\n    playtimeUpdated = true;\n  }\n\n  ListElement[] getMplayerUI(int cols) {\n    ListElement[] playerUI;\n    auto fcols = cols-16;\n    if (currentTrack.artist.utfLength / 2 > fcols / 2) currentTrack.artist = currentTrack.artist[0..fcols-4];\n    if (currentTrack.title.utfLength / 2 > fcols / 2) currentTrack.title = currentTrack.title[0..fcols-4];\n    auto artistrepl = fcols / 2 - currentTrack.artist.utfLength / 2;\n    auto titlerepl = fcols / 2 - currentTrack.title.utfLength / 2;\n\n    if (fcols < 1) fcols = cols;\n    if (artistrepl < 1) artistrepl = 1;\n    if (titlerepl < 1) titlerepl = 1;\n\n    playerUI ~= ListElement(\" \".replicate(artistrepl)~currentTrack.artist);\n    playerUI ~= ListElement(\" \".replicate(titlerepl)~currentTrack.title);\n    playerUI ~= ListElement(center(currentTrack.playtime ~ \" / \" ~ currentTrack.duration, fcols, ' '));\n    playerUI ~= ListElement(center(\"[\" ~ realProgress ~ \"]\", fcols, ' '));\n    return playerUI;\n  }\n\n  bool sameTrack(int position) {\n    vkAudio track;\n    if (mplayer.shuffleMode)\n      track = getShuffledMusic(1, position)[0];\n    else\n      track = api.getBufferedMusic(1, position)[0];\n    return currentTrack.id == track.id.to!string;\n  }\n}\n\nclass mpv: Thread {\n\n  enum ipcCmd {\n    timeset,\n    seek,\n    pause,\n    exit,\n    load\n  }\n\n  struct ipcCmdParams {\n    ipcCmd command;\n    string strargument;\n    real realargument;\n  }\n\n  string\n    socketArgument = \"--input-unix-socket=\", //compatibility with older versions, changed to \"--input-ipc-server=\" in mpv 0.17.0\n    socketPath,\n    playerExec;\n\n  const\n    int\n      endRequestId = 3,\n      posPropertyId = 1,\n      idlePropertyId = 2;\n\n  alias posCallback = void delegate(real sec);\n  alias endCallback = void delegate();\n\n  posCallback posChanged;\n  endCallback endReached;\n\n  __gshared {\n    string commandTemplate = \"{ \\\"command\\\": [] }\";\n    Thread endChecker;\n    Socket comm;\n    Address commAddr;\n    Pid pid = null;\n    bool\n      isInit,\n      playerExit,\n      notFirstPlay,\n      musicState;\n  }\n\n\n  this(posCallback pos, endCallback end) {\n    posChanged = pos;\n    endReached = end;\n\n    socketPath = getPlayerSocketName();\n    playerExec = \"mpv --idle --no-audio-display \" ~ socketArgument ~ socketPath ~ \" > /dev/null 2> /dev/null\";\n\n    super(&runPlayer);\n  }\n\n\n  private void req(string cmd) {\n    //dbm(\"mpv <- \" ~ cmd);\n    if(!isInit) {\n      dbm(\"mpv - req: noinit\");\n      return;\n    }\n\n    //dbm(\"mpv - req cmd: \" ~ cmd);\n\n    auto s_answ = comm.send(cmd ~ \"\\n\");\n    if(s_answ == Socket.ERROR) {\n      dbm(\"mpv - req: s_answ error\");\n      return;\n    }\n\n  }\n\n  private void mpvsend(ipcCmdParams c) {\n\n    JSONValue cm = parseJSON(commandTemplate);\n\n    switch(c.command) {\n      case ipcCmd.seek:\n        cm.object[\"command\"].array ~= JSONValue(\"seek\");\n        cm.object[\"command\"].array ~= JSONValue(c.realargument);\n        cm.object[\"command\"].array ~= JSONValue(c.strargument);\n        break;\n      case ipcCmd.timeset:\n        cm.object[\"command\"].array ~= JSONValue(\"set_property\");\n        cm.object[\"command\"].array ~= JSONValue(\"playback-time\");\n        cm.object[\"command\"].array ~= JSONValue(c.realargument);\n        break;\n      case ipcCmd.pause:\n        cm.object[\"command\"].array ~= JSONValue(\"set_property\");\n        cm.object[\"command\"].array ~= JSONValue(\"pause\");\n        cm.object[\"command\"].array ~= JSONValue(musicState);\n        break;\n      case ipcCmd.exit:\n        cm.object[\"command\"].array ~= JSONValue(\"quit\");\n        break;\n      case ipcCmd.load:\n        cm.object[\"command\"].array ~= JSONValue(\"loadfile\");\n        cm.object[\"command\"].array ~= JSONValue(c.strargument);\n        break;\n      default: assert(0);\n    }\n\n    dbm(\"mpv - cmd: \" ~ c.command.to!string);\n\n    req(cm.toString());\n  }\n\n  private void mpvhandle(string rc) {\n    try {\n      //dbm(\"mpv: \" ~ rc);\n      auto m = parseJSON(rc);\n      if(m.type != JSON_TYPE.OBJECT) return;\n      if(\n        \"request_id\" in m &&\n        m[\"request_id\"].integer == endRequestId &&\n        \"data\" in m &&\n        m[\"data\"].type == JSON_TYPE.TRUE\n      ) {\n        endReached();\n      }\n      else if(\"error\" in m) {\n        if(m[\"error\"].str != \"success\") {\n          dbm(\"mpv - error: \" ~ rc);\n        }\n      }\n      else if(\"event\" in m) {\n        auto e = m[\"event\"].str;\n\n        switch(e) {\n          case \"property-change\":\n            auto eid = m[\"id\"].integer.to!int;\n            if(eid == posPropertyId) posChanged(m[\"data\"].floating.to!real);\n            break;\n          default: break;\n        }\n\n      }\n    }\n    catch(JSONException e) {\n      dbm(\"mpv - json exception: \" ~ e.msg);\n    }\n  }\n\n  private void checkEnd() {\n    logThread(\"check_end watchdog\");\n    while(!playerExit) {\n      Thread.sleep(dur!\"msecs\"(500));\n      if(musicState) req(checkEndCmd().toString());\n    }\n  }\n\n  private JSONValue checkEndCmd() {\n    JSONValue c = parseJSON(\"{ \\\"command\\\": [], \\\"request_id\\\": 0 }\");\n    c[\"command\"].array ~= JSONValue(\"get_property\");\n    c[\"command\"].array ~= JSONValue(\"idle-active\");\n    c[\"request_id\"].integer = endRequestId;\n    return c;\n  }\n\n  private JSONValue observePropertyCmd(int id, string prop) {\n    auto c = parseJSON(commandTemplate);\n    c[\"command\"].array ~= JSONValue(\"observe_property\");\n    c[\"command\"].array ~= JSONValue(id);\n    c[\"command\"].array ~= JSONValue(prop);\n    return c;\n  }\n\n  private void setup() {\n    req(observePropertyCmd(posPropertyId, \"playback-time\").toString());\n    //req(observePropertyCmd(idlePropertyId, \"end-file\").toString());\n    endChecker = new Thread(&checkEnd);\n    endChecker.start();\n  }\n\n  void killPlayer() {\n    int code = -1;\n    if(pid !is null){\n      code = kill(pid.processID + 1 , SIGTERM);\n    }\n  }\n\n  void runPlayer() {\n    logThread(\"runplayer\");\n    dbm(\"mpv - starting\");\n    auto pipe = pipeProcess(\"sh\", Redirect.stdin);\n    pipe.stdin.writeln(playerExec);\n    pipe.stdin.flush;\n    pid = pipe.pid;\n    Thread.sleep(dur!\"msecs\"(500)); //wait for init\n    dbm(\"mpv - running\");\n\n    uint spawntry;\n    while(!std.file.exists(socketPath)) {\n        dbm(\"mpv - waiting for socket spawn...\");\n        Thread.sleep(dur!\"msecs\"(400));\n\n        ++spawntry;\n        if(spawntry >= 25) {\n            dbm(\"mpv - socket connection failed\");\n            return;\n        }\n    }\n    commAddr = new UnixAddress(socketPath);\n    comm = new Socket(AddressFamily.UNIX, SocketType.STREAM);\n    comm.connect(commAddr);\n    dbm(\"mpv - socket connected\");\n\n    isInit = true;\n    long r_answ = -1;\n    setup();\n\n    while( r_answ != 0 ){\n      char[1024] recv;\n      string recv_str;\n      r_answ = comm.receive(recv);\n      if(r_answ != 0) {\n        foreach(r; recv) {\n          if(r == '\\n' || r == '\\x00') break;\n          recv_str ~= r;\n        }\n        //dbm(\"mpv - recv: \" ~ recv_str);\n        mpvhandle(recv_str);\n        Thread.sleep( dur!\"msecs\"(100) );\n      }\n    }\n\n    dbm(\"PLAYER EXIT\");\n    playerExit = true;\n  }\n\n  void pause() {\n    auto c = ipcCmdParams(ipcCmd.pause);\n    mpvsend(c);\n    musicState = !musicState;\n  }\n\n  bool getMusicState() {\n    return musicState;\n  }\n\n  bool isPlayerExit() {\n    return playerExit;\n  }\n\n  bool isPlayerInit() {\n    return isInit;\n  }\n\n  void exit() {\n    auto c = ipcCmdParams(ipcCmd.exit);\n    mpvsend(c);\n    Thread.sleep(dur!\"msecs\"(100));\n    try {\n        std.file.remove(socketPath);\n    }\n    catch(std.file.FileException e) {\n        //dbm(\"socket not found\")\n    }\n  }\n\n  void loadfile(string p) {\n    auto c = ipcCmdParams(ipcCmd.load, p);\n    mpvsend(c);\n    if(!notFirstPlay) {\n      musicState = true;\n      notFirstPlay = true;\n    }\n  }\n\n  void setPosition(real sec) {\n    auto c = ipcCmdParams(ipcCmd.timeset, \"\", sec);\n    mpvsend(c);\n  }\n\n  void relativeSeek(real rval, bool percent) {\n    auto mode = percent ? \"relative-percent\" : \"relative\";\n    auto c = ipcCmdParams(ipcCmd.seek, mode, rval);\n    mpvsend(c);\n  }\n\n}\n"
  },
  {
    "path": "source/namecache.d",
    "content": "/*\nCopyright 2016 HaCk3D, substanceof\n\nhttps://github.com/HaCk3Dq\nhttps://github.com/substanceof\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nmodule namecache;\n\nimport std.stdio, std.conv, std.algorithm, std.array;\nimport vkapi, utils;\n\nstruct cachedName {\n    string first_name;\n    string last_name;\n    bool online;\n}\n\nstruct nameCacheStorage {\n    cachedName[int] cache;\n}\n\nstring strName(cachedName inp) {\n    auto ln = inp.last_name;\n    auto rt = inp.first_name;\n    if(ln.length != 0) rt ~= \" \" ~ ln;\n    return rt;\n}\n\n__gshared auto nc = nameCacheStorage();\n\nclass nameCache {\n    VkApi api;\n    int[] order;\n\n    this(VkApi api) {\n        this.api = api;\n    }\n\n    static void defeatNameCache() {\n        nc = nameCacheStorage();\n    }\n\n    void requestId(int[] ids) {\n        order ~= ids;\n        dbm(\"requestId ids: \" ~ ids.length.to!string ~ \" order: \" ~ order.length.to!string);\n    }\n\n    void requestId(int id) {\n        order ~= id;\n    }\n\n    void addToCache(int id, cachedName name){\n        nc.cache[id] = name;\n    }\n\n    void setOnline(int id, bool online) {\n        auto c = id in nc.cache;\n        if(c) {\n            c.online = online;\n        }\n    }\n\n    bool getOnline(int id, bool forced = false) {\n        auto c = id in nc.cache;\n        if(c) return c.online;\n        else if (forced) return getName(id).online;\n        else return false;\n    }\n\n    cachedName getName(int id) {\n        if(id in nc.cache) {\n            return nc.cache[id];\n        }\n        dbm(\"got non-cached name!\");\n        try {\n            \n            cachedName rt;\n            int fid;\n            if(id < 0) {\n                dbm(\"got community id\");\n                auto c = api.groupsGetById([ id ]);\n                if(c.length == 0) {\n                    return cachedName(\"community\", id.to!string);\n                } else {\n                    fid = c[0].id;\n                    rt = cachedName(c[0].name, \" \", true);\n                }\n            } else {\n                auto resp = api.usersGet(id);\n                fid = resp.id;\n                rt = cachedName(resp.first_name, resp.last_name, resp.online);\n            }\n            nc.cache[fid] = rt;\n            return rt;\n        } catch (ApiErrorException e) {\n            if (e.errorCode == 6) {\n                dbm(\"too many requests, returning default name\");\n                return cachedName(\"default\", \"name\");\n            } else {\n                //rethrow\n                throw e;\n            }\n        }\n    }\n\n    void resolveNames() {\n        dbm(\"start name resolving, order length: \" ~ order.length.to!string);\n        if(order.length == 0) return;\n        int[] clean = order\n                        .filter!(q => q !in nc.cache)\n                        .array;\n\n        const int max = 1000;\n        int n = 0;\n        int len = clean.length.to!int;\n        bool cnt = true;\n        while(cnt) {\n            int d = len - n;\n            int[] buf;\n            if(d > max) {\n                int up = n+max+1;\n                buf = clean[n..up];\n                n += max;\n            } else {\n                int up = n+d;\n                buf = clean[n..up];\n                cnt = false;\n            }\n            foreach(nm; api.usersGet( buf.filter!(d => d > 0 || d < mailStartId).array )) { //users\n                nc.cache[nm.id] = cachedName(nm.first_name, nm.last_name, nm.online);\n            }\n            foreach(cnm; api.groupsGetById( buf.filter!(d => d < 0 && d > mailStartId).array )) { //communities\n                nc.cache[cnm.id] = cachedName(cnm.name, \" \", true);\n            }\n        }\n        order = new int[0];\n        dbm(\"cached \" ~ nc.cache.length.to!string ~ \" names, order length: \" ~ order.length.to!string);\n    }\n\n}\n\n\n"
  },
  {
    "path": "source/storedFunctions",
    "content": "/*\nCopyright 2016 HaCk3D, substanceof\n\nhttps://github.com/HaCk3Dq\nhttps://github.com/substanceof\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n//This file contains source code of stored execute.* vk.com functions\n\n\nexecute.vkGetDialogs {\n    //returns dialogs with online fields\n\n    var m = API.messages.getDialogs({\"count\": Args.count, \"offset\": Args.offset});\n    var uids = m.items@.message@.user_id;\n    var onl = API.users.get({\"user_ids\": uids, \"fields\": \"online\"});\n    return {\"conv\": m, \"ou\": onl@.id, \"os\": onl@.online};\n}\n\n\nexecute.accountInit {\n    var me = API.users.get();\n    var c = API.account.getCounters(\"messages\");\n    if(c.messages == null) {\n        c.messages = 0;\n    }\n\n    var sc = [];\n    sc.dialogs = API.messages.getDialogs().count;\n    sc.friends = API.friends.get().count;\n    sc.audio = API.audio.get().count;\n\n    API.stats.trackVisitor();\n\n    return {\"me\": me[0], \"counters\": c, \"sc\": sc};\n}\n\n"
  },
  {
    "path": "source/utils.d",
    "content": "/*\nCopyright 2016 HaCk3D, substanceof\n\nhttps://github.com/HaCk3Dq\nhttps://github.com/substanceof\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nmodule utils;\n\nimport std.stdio, std.array, std.range, std.string, std.file, std.random;\nimport std.datetime, std.conv, std.algorithm, std.utf, std.typecons;\nimport std.process, core.thread, core.sync.mutex, core.exception;\nimport core.sys.posix.signal;\nimport localization, app, vkversion, musicplayer;\n\nconst bool\n    loggingEnabled = true,\n    debugMessagesEnabled = false,\n    showTokenInLog = false;\n\n__gshared {\n    File dbgff;\n    File dbglat;\n    bool dbmfe = loggingEnabled;\n    string dbmlog = \"\";\n    string vkcliTmpDir = \"/tmp/vkcli-tmp\";\n    string vkcliLogDir = \"/tmp/vkcli-log\";\n    string vkcliTmpMsgFile = \"/tmp/vkcli-tmp/messagetext\";\n    string dbgfname = \"vklog\";\n    string dbglatest = \"-latest\";\n    string mpvsck = \"vkmpv-socket-\";\n    string mpvsocketName;\n    string logName;\n    string logPath;\n    Mutex dbgmutex;\n}\n\nprivate void appendDbg(string app) {\n    synchronized(dbgmutex) {\n        append(logPath, app);\n    }\n}\n\nstring toTmpDateString(SysTime t) {\n    return t.day().to!string\n            ~ t.month().to!string\n            ~ t.year().to!string\n            ~ \"-\"\n            ~ t.hour().tzr ~ \":\"\n            ~ t.minute().tzr ~ \":\"\n            ~ t.second().tzr\n            ~ \"-\"\n            ~ t.timezone().stdName();\n}\n\nstring getPlayerSocketName() {\n    if(mpvsocketName == \"\") throw new Exception(\"bad player socket name\");\n    return vkcliTmpDir ~ \"/\" ~ mpvsocketName;\n}\n\nvoid initdbm() {\n    auto ctime = Clock.currTime();\n    dbgmutex = new Mutex();\n    logName = dbgfname ~ \"_\" ~ ctime.toTmpDateString();\n    logPath = vkcliLogDir ~ \"/\" ~ logName;\n    mpvsocketName = mpvsck ~ genStr(8);\n\n    if(!exists(vkcliTmpDir)) mkdir(vkcliTmpDir);\n    if(!exists(vkcliLogDir)) mkdir(vkcliLogDir);\n\n    if(dbmfe) {\n        string logIntro = \"vk-cli \" ~ currentVersion ~ \" log\\n\" ~ ctime.toSimpleString() ~ \"\\n\";\n\n        auto touchResult = executeShell(\"umask 0177\\ntouch \" ~ logPath);\n        if(touchResult.status != 0) {\n            auto ecode = touchResult.status.to!string;\n            writeln(\"touch failed (\" ~ ecode ~ \") - logging disabled\");\n            dbmfe = false;\n        }\n\n        dbgff = File(logPath, \"w\");\n        dbgff.write(logIntro);\n        dbgff.close();\n    }\n}\n\nvoid dbm(string msg) {\n    if(debugMessagesEnabled) writeln(\"[debug]\" ~ msg);\n    if(dbmfe) appendDbg(msg ~ \"\\n\");\n}\n\nvoid dropClient(string msg) {\n    Exit(msg);\n}\n\nstring tzr(int inpt) {\n    auto r = inpt.to!string;\n    if(inpt > -1 && inpt < 10) return (\"0\" ~ r);\n    else return r;\n}\n\nstring vktime(SysTime ct, long ut) {\n    auto t = SysTime(unixTimeToStdTime(ut));\n    return (t.dayOfGregorianCal == ct.dayOfGregorianCal) ?\n            (tzr(t.hour) ~ \":\" ~ tzr(t.minute)) :\n                (tzr(t.day) ~ \".\" ~ tzr(t.month) ~ ( t.year != ct.year ? \".\" ~ t.year.to!string[$-2..$] : \"\" ) );\n}\n\nstring agotime (SysTime ct, long ut) { //not used\n    auto pt = SysTime(ut.unixTimeToStdTime);\n    auto ctm = ct.hour*60 + ct.minute;\n    auto ptm = pt.hour*60 + pt.minute;\n    auto tmdelta = ctm - ptm;\n    const threshld = 240;\n\n    if(\n        pt.dayOfGregorianCal == ct.dayOfGregorianCal &&\n        tmdelta < threshld\n    ) {\n        string rt;\n        if(tmdelta > 60) {\n            auto m = tmdelta % 60;\n            auto h = (tmdelta-m) / 60;\n            rt ~= h.to!string ~\n             ( h == 1 ? getLocal(\"time_hour\") : ( h > 0 && h < 5 ? getLocal(\"time_hours_l5\") : getLocal(\"time_hours\") ) );\n            if(m != 0) rt ~= \" \" ~ m.to!string ~\n             ( m == 1 ? getLocal(\"time_minute\") : ( m > 0 && m < 5 ? getLocal(\"time_minutes_l5\") : getLocal(\"time_minutes\") ) );\n        }\n        else if(tmdelta == 1) rt = tmdelta.to!string ~ getLocal(\"time_minute\");\n        else rt = tmdelta.to!string ~ getLocal(\"time_minutes\");\n\n        return rt ~ getLocal(\"time_ago\");\n    }\n    else return vktime(ct, ut);\n}\n\n/*\n local[\"time_minutes\"] = lang(\" minutes ago\", \" минут назад\");\n  local[\"time_minutes_l5\"] = lang(\" minutes ago\", \" минуты назад\");\n  local[\"time_minute\"] = lang(\" minute\", \" минуту\");\n  local[\"time_hours\"] = lang(\" hours\", \" часов\");\n  local[\"time_hours_l5\"] = lang(\" hours\", \" часа\");\n  local[\"time_hour\"] = lang(\" hour\", \" час\");\n  local[\"time_ago\"] = lang(\" ago\" , \" назад\");\n  local[\"lastseen\"] = lang(\"last seen at \", \"был в сети в \");\n  */\n\nstring longpollReplaces(string inp) {\n    return inp\n        .replace(\"<br>\", \"\\n\")\n        .replace(\"&quot;\", \"\\\"\")\n        .replace(\"&lt;\", \"<\")\n        .replace(\"&gt;\", \">\")\n        .replace(\"&amp;\", \"&\");\n}\n\nT[] slice(T)(ref T[] src, int count, int offset) {\n    try {\n        return src[offset..(offset+count)]; //.map!(d => &d).array;\n    } catch (RangeError e) {\n        dbm(\"utils slice count: \" ~ count.to!string ~ \", offset: \" ~ offset.to!string);\n        dbm(\"catched slice ex: \" ~ e.msg);\n        return [];\n    }\n}\n\nS[] wordwrap(S)(S s, size_t mln) {\n    auto wrplines = s.wrap(mln).split(\"\\n\");\n    S[] lines;\n    foreach(ln; wrplines) {\n        S[] crp = [\"\", ln];\n        while(crp.length > 1) {\n            crp = cropstr(crp[1] ,mln);\n            lines ~= crp[0];\n        }\n    }\n    return lines[0..$-1];\n}\n\nprivate S[] cropstr(S)(S s, size_t mln) {\n    if(s.length > mln) return [ s[0..mln], s[mln..$] ];\n    else return [s];\n}\n\nclass JoinerBidirectionalResult(RoR)\nif (isBidirectionalRange!RoR && isBidirectionalRange!(ElementType!RoR))\n{\n\n    alias rortype = ElementType!RoR;\n\n    private {\n        RoR range;\n        rortype\n            rfront = null,\n            rback = null;\n}\n\n    this(RoR r) {\n        range = r;\n    }\n\n    private void prepareFront() {\n        if(range.empty) return;\n        while(range.front.empty) {\n            range.popFront();\n            if(range.empty) return;\n        }\n        rfront = range.front;\n    }\n\n    private void prepareBack() {\n        if(range.empty) return;\n        while(range.back.empty) {\n            range.popBack();\n            if(range.empty) return;\n        }\n        rback = range.back;\n    }\n\n    @property bool empty() {\n        return range.empty;\n    }\n\n    @property auto front() {\n        if(rfront is null) prepareFront();\n        assert(!empty);\n        assert(!rfront.empty);\n        return rfront.front;\n    }\n\n    @property auto back() {\n        if(rback is null) prepareBack();\n        assert(!empty);\n        assert(!rback.empty);\n        return rback.back;\n    }\n\n    void popFront() {\n        if(rfront is null) prepareFront();\n        else {\n            rfront.popFront();\n\n            if(rfront.empty) {\n                range.popFront();\n                prepareFront();\n            }\n        }\n    }\n\n    void popBack() {\n        if(rback is null) prepareBack();\n        else {\n            rback.popBack();\n            if(rback.empty) {\n                range.popBack();\n                prepareBack();\n            }\n        }\n    }\n\n    auto moveBack() {\n        return back;\n    }\n\n    auto save() {\n        return this;\n    }\n\n}\n\nauto joinerBidirectional(RoR)(RoR range) {\n    return new JoinerBidirectionalResult!RoR(range);\n}\n\nauto takeBackArray(R)(R range, size_t hm) {\n    ElementType!R[] outr;\n    size_t iter;\n    while(iter < hm && !range.empty) {\n        outr ~= range.back();\n        range.popBack();\n        ++iter;\n    }\n    reverse(outr);\n    return outr;\n}\n\nclass InputRetroResult(R)\nif (isInputRange!R)\n{\n    private R rng;\n\n    this(R range) {\n        rng = range;\n    }\n\n    void popFront() {\n        rng.popFront();\n    }\n\n    void popBack() {\n        rng.popFront();\n    }\n\n    auto front() {\n        return rng.front;\n    }\n\n    auto back() {\n        return rng.front;\n    }\n\n    auto empty() {\n        return rng.empty;\n    }\n\n    auto moveBack() {\n        return back;\n    }\n\n    auto save() {\n        return this;\n    }\n\n}\n\nauto inputRetro(R)(R range) {\n    return new InputRetroResult!R(range);\n}\n\nvoid logThread(string thrname = \"\") {\n    if(thrname != \"\") dbm(\"thread started for: \" ~ thrname);\n}\n\nvoid unwantedExit(int sig) {\n    Exit(\"killed by signal \" ~ sig.to!string, 2);\n}\n\nvoid writeCurrentTrack(int sig) {\n    auto file = File(vkcliTmpDir ~ \"/current-track\", \"w\");\n    auto track = mplayer ? mplayer.currentTrack : Track();\n    file.write(\"[\" ~ track.playtime ~ \"/\" ~ track.duration ~ \"] \" ~ track.artist ~ \" - \" ~ track.title);\n}\n\nvoid setPosixSignals() {\n    version(posix) {\n        sigset(SIGSEGV, a => unwantedExit(a));\n        sigset(SIGUSR1, a => writeCurrentTrack(a));\n    }\n}\n\nint gcSuspendSignal;\nint gcResumeSignal;\n\nvoid updateGcSignals() {\n    version(linux) {\n      gcSuspendSignal = SIGRTMIN;\n      gcResumeSignal = SIGRTMIN+1;\n      \n      thread_term();\n      thread_setGCSignals(gcSuspendSignal, gcResumeSignal);\n      thread_init();\n      dbm(\"GC signals: \" ~ gcSuspendSignal.to!string ~ \" \" ~ gcResumeSignal.to!string);\n    }\n}\n\n\nconst uint maxuint = 4_294_967_295;\nconst uint maxint = 2_147_483_647;\nconst uint ridstart = 1;\n\nint genId() {\n    long rnd = uniform(ridstart, maxuint);\n    if(rnd > maxint) {\n        rnd = -(rnd-maxint);\n    }\n    dbm(\"rid: \" ~ rnd.to!string);\n    return rnd.to!int;\n}\n\nstring genStrDict = \"qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890\";\n\nstring genStr(uint strln) {\n    string output;\n    for(uint i; i < strln; ++i) {\n        size_t rnd = uniform!\"[)\"(0, genStrDict.length);\n        output ~= genStrDict[rnd];\n    }\n    return output;\n}\n\nalias Repldchar = std.typecons.Flag!\"useReplacementDchar\";\nconst Repldchar repl = Repldchar.yes;\n\nwstring toUTF16wrepl(in char[] s) {\n    wchar[] r;\n    size_t slen = s.length;\n\n    r.length = slen;\n    r.length = 0;\n    for (size_t i = 0; i < slen; )\n    {\n        dchar c = s[i];\n        if (c <= 0x7F)\n        {\n            i++;\n            r ~= cast(wchar)c;\n        }\n        else\n        {\n            c = decode!repl(s, i);\n            encode(r, c);\n        }\n    }\n\n    return cast(wstring)r;\n}\n\nstring toUTF8wrepl(in wchar[] s) {\n    char[] r;\n    size_t i;\n    size_t slen = s.length;\n\n    r.length = slen;\n    for (i = 0; i < slen; i++)\n    {\n        wchar c = s[i];\n\n        if (c <= 0x7F)\n            r[i] = cast(char)c;     // fast path for ascii\n        else\n        {\n            r.length = i;\n            while (i < slen)\n                encode(r, decode!repl(s, i));\n            break;\n        }\n    }\n\n    return cast(string)r;\n}\n\nstruct utf {\n  ulong\n    start, end;\n  int spaces;\n}\n\nconst utfranges = [\n  utf(19968, 40959, 1),\n  utf(12288, 12351, 1),\n  utf(11904, 12031, 1),\n  utf(13312, 19903, 1),\n  utf(63744, 64255, 1),\n  utf(12800, 13055, 1),\n  utf(13056, 13311, 1),\n  utf(12736, 12783, 1),\n  utf(12448, 12543, 1),\n  utf(12352, 12447, 1),\n  utf(110592, 110847, 1),\n  utf(65280, 65519, 1)\n  ];\n\nuint utfLength(string inp) {\n    uint s = 0;\n    size_t inplen = inp.length;\n\n    for (size_t i = 0; i < inplen; ) {\n        auto ic = inp[i];\n        ulong c;\n        ++s;\n\n        if(ic <= 0x7F) {\n            c = cast(ulong)ic;\n            ++i;\n        }\n        else {\n            c = cast(ulong)(decode!repl(inp, i));\n        }\n\n        foreach (r; utfranges) {\n            if (c >= r.start && c <= r.end) {\n                s += r.spaces;\n                break;\n            }\n        }\n    }\n    return s;\n}\n\nS replicatestr(S)(S str, ulong n) {\n    S outstr = \"\";\n    for(ulong i = 0; i < n; ++i) {\n        outstr ~= str;\n    }\n    return outstr;\n}\n\n\nstring getMessageFromTmpFile() {\n    string text = \"\";\n    if (std.file.exists(vkcliTmpMsgFile))\n\t  text = cast(string)std.file.read(vkcliTmpMsgFile);\n    std.file.write(vkcliTmpMsgFile, \"\");\n    return text;\n}\n"
  },
  {
    "path": "source/vkapi.d",
    "content": "/*\nCopyright 2016 HaCk3D, substanceof\n\nhttps://github.com/HaCk3Dq\nhttps://github.com/substanceof\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nmodule vkapi;\n\nimport std.stdio, std.conv, std.string, std.regex, std.array, std.datetime, std.random, core.time;\nimport std.exception, core.exception, std.process;\nimport std.net.curl, std.uri, std.json;\nimport std.range, std.algorithm;\nimport std.parallelism, std.concurrency, core.thread, core.sync.mutex;\nimport utils, namecache, localization;\nimport magicstringz;\n\n\n\n// ===== vkapi const =====\n\nconst int convStartId = 2000000000;\nconst int mailStartId = convStartId*-1;\nconst int longpollGimStartId = 1000000000;\nconst bool return80mc = true;\nconst long needNameMaxDelta = 180; //seconds, 3 min\nconst int typingTimeout = 2;\n\nconst uint defaultBlock = 100;\nconst int chatBlock = 100;\nconst int chatUpd = 50;\n\n// ===== networking const =====\n\nconst int connectionAttempts = 10;\nconst int mssleepBeforeAttempt = 600;\nconst int vkgetCurlTimeout = 6;\nconst int longpollCurlTimeout = 30;\nconst int longpollCurlAttempts = 5;\nconst string timeoutFormat = \"seconds\";\n\n// ===== API objects =====\n\nstruct vkUser {\n    string first_name;\n    string last_name;\n    int id;\n    bool online;\n}\n\nstruct vkDialog {\n    string name;\n    string lastMessage = \"\";\n    int lastmid;\n    int id = -1;\n    int unreadCount;\n    bool unread = false;\n    bool outbox;\n    bool online;\n    bool isChat;\n}\n\nstruct vkMessage {\n    string author_name; // in format 'First Last'\n    int author_id;\n    int peer_id; // for users: user id, for conversations: 2000000000 + chat id\n    int msg_id;\n    bool outgoing;\n    bool unread;\n    bool needName;\n    bool nmresolved;\n    long utime; // unix time\n    string time_str; // HH:MM (M = minutes), if >24 hours ago, then DD.mm (m = month)\n    string[] body_lines; // Message text, splitted in lines (delimiter = '\\n')\n    int fwd_depth; // max fwd message deph (-1 if no fwd)\n    vkFwdMessage[] fwd; // forwarded messages\n    bool isLoading;\n    bool isZombie;\n    long rndid;\n    int lineCount = -1;\n    int wrap = -1;\n}\n\nstruct vkMessageLine {\n    string text;\n    string time;\n    bool unread;\n    bool isName;\n    bool isSpacing;\n    bool isFwd;\n    int fwdDepth;\n}\n\nauto emptyVkMessage = vkMessage();\n\nstruct vkFwdMessage {\n    int author_id;\n    string author_name;\n    long utime;\n    string time_str;\n    string[] body_lines;\n    vkFwdMessage[] fwd;\n}\n\nstruct vkCounters {\n    int friends = 0;\n    int messages = 0;\n    int notifications = 0;\n    int groups = 0;\n}\n\nstruct vkFriend {\n    string first_name;\n    string last_name;\n    int id;\n    long last_seen_utime;\n    string last_seen_str;\n    bool online;\n}\n\nstruct vkAudio {\n    int id;\n    int owner;\n    string artist;\n    string title;\n    int duration_sec;\n    string duration_str; // MM:SS (len 5)\n    string url;\n}\n\nstruct vkAccountInit {\n    int id;\n    string\n        first_name,\n        last_name;\n    uint\n        c_messages,\n        sc_dialogs,\n        sc_friends,\n        sc_audio;\n}\n\nstruct vkGroup {\n    int id;\n    string name;\n}\n\n// ===== longpoll objects =====\n\nstruct vkLongpoll {\n    string key;\n    string server;\n    int ts;\n}\n\nstruct vkNextLp {\n    int ts;\n    int failed;\n}\n\n// === API state and meta =====\n\nlong[int] lasttypetimes;\n\nstruct apiState {\n    bool lp80got = true;\n    bool somethingUpdated;\n    bool chatloading;\n    bool showConvNotifies;\n    int loadingiter = 0;\n    string lastlp = \"\";\n    uint countermsg = -1;\n    sentMsg[long] sent; //by rid\n    bool[int] unreadCountReview; //by peer\n    bool shedForceUpdateFlag;\n    bool shedForceUpdateSelfResolve;\n}\n\nstruct ldFuncResult {\n    bool success;\n    int servercount = -1;\n}\n\nstruct factoryData {\n    int serverCount = -1;\n    bool forceUpdate;\n}\n\nstruct sentMsg {\n    int rid;\n    int peer;\n    int author;\n    sendState state = sendState.pending;\n}\n\nstruct overridedLastseen {\n    long last_seen_utime;\n    string last_seen_str;\n}\n\nenum blockType {\n    dialogs,\n    music,\n    friends,\n    chat\n}\n\nenum sendState {\n    pending,\n    failed\n}\n\nstruct apiFwdIter {\n    vkFwdMessage[] fwd;\n    int md;\n}\n\n__gshared {\n    overridedLastseen[int] lsc; //by uid\n    nameCache nc;\n    apiState ps;\n    Mutex\n        sndMutex,\n        pbMutex;\n}\n\n\nstruct vkgetparams {\n    bool setloading = true;\n    int attempts = connectionAttempts;\n    bool thrownf = false;\n    bool notifynf = true;\n}\n\nclass VkApi {\n\n    private const string vkurl = \"https://api.vk.com/method/\";\n    const string vkver = \"5.50\";\n    private string vktoken;\n    private bool isTokenValid_;\n    private bool isInitAttemptFinished_;\n\n    vkUser me;\n    vkAccountInit initdata;\n\n    alias nfnotifyfn = void delegate();\n    nfnotifyfn connectionProblems;\n\n    this(string token, nfnotifyfn nfnotify) {\n        isTokenValid_ = true;\n        isInitAttemptFinished_ = false;\n        vktoken = token;\n        connectionProblems = nfnotify;\n    }\n    \n    bool isTokenValid() {\n        return isTokenValid_;\n    }\n    \n    bool isInitAttemptFinished() {\n        return isInitAttemptFinished_;\n    }\n\n    void addMeNC() {\n        nc.addToCache(me.id, cachedName(me.first_name, me.last_name));\n    }\n\n    void resolveMe() {\n        isTokenValid_ = checkToken(vktoken);\n    }\n\n    private bool checkToken(string token) {\n        try{\n            initdata = executeAccountInit();\n            me = vkUser(initdata.first_name, initdata.last_name, initdata.id);\n        } catch (ApiErrorException e) {\n            dbm(\"ApiErrorException: \" ~ e.msg);\n            if (e.errorCode == 5) {\n                dbm(\"ApiError: Wrong token\");\n                return false;\n            }\n        }\n        return true;\n    }\n\n    JSONValue vkget(string meth, string[string] params, bool dontRemoveResponse = false, vkgetparams gp = vkgetparams()) {\n        if(gp.setloading) {\n            enterLoading();\n        }\n        bool rmresp = !dontRemoveResponse;\n        auto url = vkurl ~ meth ~ \"?\"; //so blue\n        foreach(key; params.keys) {\n            auto val = params[key];\n            //dbm(\"up \" ~ key ~ \"=\" ~ val);\n            auto cval = val.encode.replace(\"+\", \"%2B\");\n            url ~= key ~ \"=\" ~ cval ~ \"&\";\n        }\n        url ~= \"v=\" ~ vkver ~ \"&access_token=\";\n        if(!showTokenInLog) dbm(\"request: \" ~ url ~ \"***\");\n        url ~= vktoken;\n        if(showTokenInLog) dbm(\"request: \" ~ url);\n        auto tm = dur!timeoutFormat(vkgetCurlTimeout);\n        string got;\n\n        bool htloop;\n        while(!htloop) {\n            try{\n                got = AsyncMan.httpget(url, tm, gp.attempts);\n                htloop = true;\n            } catch(NetworkException e) {\n                dbm(e.msg);\n                if(gp.notifynf) connectionProblems();\n                if(gp.thrownf) throw e;\n\n                if(gp.notifynf) {\n                    //dbm(\"vkget waits for api init..\");\n                    do {\n                        Thread.sleep(dur!\"msecs\"(300));\n                    } while(!isTokenValid);\n                    //dbm(\"resume vkget\");\n                }\n            }\n        }\n\n        JSONValue resp;\n        try{\n            resp = got.parseJSON;\n            //dbm(\"json: \" ~ resp.toPrettyString);\n        }\n        catch(JSONException e) {\n            throw new ApiErrorException(resp.toPrettyString(), 0);\n        }\n\n        if(resp.type == JSON_TYPE.OBJECT) {\n            if(\"error\" in resp){\n                try {\n                    auto eobj = resp[\"error\"];\n                    immutable auto emsg = (\"error_text\" in eobj) ? eobj[\"error_text\"].str : eobj[\"error_msg\"].str;\n                    immutable auto ecode = eobj[\"error_code\"].integer.to!int;\n                    throw new ApiErrorException(emsg, ecode);\n                } catch (JSONException e) {\n                    throw new ApiErrorException(resp.toPrettyString(), 0);\n                }\n\n            } else if (\"response\" !in resp) {\n                rmresp = false;\n            }\n        } else rmresp = false;\n\n        if(gp.setloading) leaveLoading();\n        return rmresp ? resp[\"response\"] : resp;\n    }\n\n    // ===== API method wrappers =====\n\n    vkAccountInit executeAccountInit() {\n        string[string] params;\n\n        params[\"code\"] =\n`var me = API.users.get();\nvar c = API.account.getCounters(\"messages\");\nif(c.messages == null) {\n    c.messages = 0;\n}\n\nvar sc = [];\nsc.dialogs = API.messages.getDialogs().count;\nsc.friends = API.friends.get().count;\nsc.audio = API.audio.get().count;\n\nif(sc.audio == null) {\n    sc.audio = 0;\n}\n\nreturn {\"me\": me[0], \"counters\": c, \"sc\": sc};`;\n\n        vkgetparams gp = {notifynf: false};\n        auto resp = vkget(\"execute\", params, false, gp);\n        vkAccountInit rt = {\n            id:resp[\"me\"][\"id\"].integer.to!int,\n            first_name:resp[\"me\"][\"first_name\"].str,\n            last_name:resp[\"me\"][\"last_name\"].str,\n            c_messages:resp[\"counters\"][\"messages\"].integer.to!int,\n            sc_dialogs:resp[\"sc\"][\"dialogs\"].integer.to!int,\n            sc_friends:resp[\"sc\"][\"friends\"].integer.to!int,\n            sc_audio:resp[\"sc\"][\"audio\"].integer.to!int\n        };\n        return rt;\n    }\n\n    vkUser usersGet(int userId = 0, string fields = \"\", string nameCase = \"nom\") {\n        string[string] params;\n        if(userId != 0) params[\"user_ids\"] = userId.to!string;\n        params[\"fields\"] = fields != \"\" ? fields : \"online\";\n        if(nameCase != \"nom\") params[\"name_case\"] = nameCase;\n        auto resp = vkget(\"users.get\", params);\n\n        if(resp.array.length != 1) throw new BackendException(\"users.get (one user) fail: response array length != 1\");\n        resp = resp[0];\n\n        vkUser rt = {\n            id:resp[\"id\"].integer.to!int,\n            first_name:resp[\"first_name\"].str,\n            last_name:resp[\"last_name\"].str\n        };\n\n        if(\"online\" in resp) rt.online = (resp[\"online\"].integer == 1);\n\n        return rt;\n    }\n\n    vkUser[] usersGet(int[] userIds, string fields = \"\", string nameCase = \"nom\") {\n        if(userIds.length == 0) return [];\n        string[string] params;\n        params[\"user_ids\"] = userIds.map!(i => i.to!string).join(\",\");\n        params[\"fields\"] = fields != \"\" ? fields : \"online\";\n        if(nameCase != \"nom\") params[\"name_case\"] = nameCase;\n        auto resp = vkget(\"users.get\", params).array;\n\n        vkUser[] rt;\n        foreach(t; resp){\n            vkUser rti = {\n                id:t[\"id\"].integer.to!int,\n                first_name:t[\"first_name\"].str,\n                last_name:t[\"last_name\"].str\n            };\n            if(\"online\" in t) rti.online = t[\"online\"].integer == 1;\n            rt ~= rti;\n        }\n        return rt;\n    }\n\n    void setActivityStatusImpl(int peer, string type) {\n        vkgetparams gp = {\n            setloading: false,\n            attempts: 1,\n            thrownf: true,\n            notifynf: false\n        };\n        try{\n            vkget(\"messages.setActivity\", [ \"peer_id\": peer.to!string, \"type\": type ], false, gp);\n        } catch (Exception e) {\n            dbm(\"catched at setTypingStatus: \" ~ e.msg);\n        }\n    }\n\n    void accountSetOnline() {\n        vkgetparams gp = {\n            setloading: false,\n            attempts: 10,\n            thrownf: true,\n            notifynf: false\n        };\n        string[string] emptyparam;\n        try{\n            vkget(\"account.setOnline\", emptyparam, false, gp);\n        } catch (Exception e) {\n            dbm(\"catched at accountSetOnline: \" ~ e.msg);\n        }\n    }\n\n    void accountSetOffline() {\n        vkgetparams gp = {\n            setloading: false,\n            attempts: 10,\n            thrownf: false,\n            notifynf: false\n        };\n        try{\n            vkget(\"account.setOffline\", [ \"voip\": \"0\" ], false, gp);\n        } catch (Exception e) {\n            dbm(\"catched at accountSetOnline: \" ~ e.msg);\n        }\n    }\n\n    vkDialog[] messagesGetDialogs(int count , int offset, out int serverCount) {\n        string[string] params;\n\n        params[\"code\"] =\n`var m = API.messages.getDialogs({\"count\": Args.count, \"offset\": Args.offset});\nvar uids = m.items@.message@.user_id;\nvar onl = API.users.get({\"user_ids\": uids, \"fields\": \"online\"});\nreturn {\"conv\": m, \"ou\": onl@.id, \"os\": onl@.online};`;\n\n        params[\"count\"] = count.to!string;\n        params[\"offset\"] = offset.to!string;\n        auto exresp = vkget(\"execute\", params);\n        auto resp = exresp[\"conv\"];\n        auto dcount = resp[\"count\"].integer.to!int;\n        dbm(\"dialogs count now: \" ~ dcount.to!string);\n        auto respt_items = resp[\"items\"].array;\n        auto respt = respt_items.map!(q => q[\"message\"]);\n\n        //name resolving\n        int[] rootIds = respt\n                        .filter!(q => \"user_id\" in q)\n                        .map!(q => q[\"user_id\"].integer.to!int)\n                        .array;\n        auto convAcvtives = respt\n                        .filter!(q => \"chat_active\" in q)\n                        .map!(q => q[\"chat_active\"]);\n        int[] convIds;\n        foreach(ca; convAcvtives) {\n            //dbm(ca.type.to!string);\n            convIds ~= ca.array.map!(a => a.integer.to!int).array;\n        }\n        nc.requestId(rootIds);\n        nc.requestId(convIds);\n        nc.resolveNames();\n\n        auto ou = exresp[\"ou\"].array;\n        auto os = exresp[\"os\"].array;\n        bool[int] online;\n        foreach(n; 0..(ou.length)) online[ou[n].integer.to!int] = (os[n].integer == 1);\n\n        vkDialog[] dialogs;\n        foreach(ditem; respt_items){\n            auto msg = ditem[\"message\"];\n            auto ds = vkDialog();\n            if(\"chat_id\" in msg){\n                auto ctitle = msg[\"title\"].str;\n                auto cid = msg[\"chat_id\"].integer.to!int + convStartId;\n                nc.addToCache(cid, cachedName(ctitle, \"\"));\n\n                ds.id = cid;\n                ds.name = ctitle;\n                ds.online = true;\n                ds.isChat = true;\n            } else {\n                auto uid = msg[\"user_id\"].integer.to!int;\n                ds.id = uid;\n                ds.name = nc.getName(ds.id).strName;\n                ds.online = (uid in online) ? online[uid] : false;\n                ds.isChat = false;\n            }\n            ds.lastMessage = msg[\"body\"].str;\n            ds.lastmid = msg[\"id\"].integer.to!int;\n            if(msg[\"read_state\"].integer == 0) ds.unread = true;\n            if(msg[\"out\"].integer == 1) ds.outbox = true;\n            if(\"unread\" in ditem) ds.unreadCount = ditem[\"unread\"].integer.to!int;\n            dialogs ~= ds;\n            //dbm(ds.id.to!string ~ \" \" ~ ds.unread.to!string ~ \"   \" ~ ds.name ~ \" \" ~ ds.lastMessage);\n            //dbm(ds.formatted);\n        }\n\n        serverCount = dcount;\n        return dialogs;\n    }\n\n    vkCounters accountGetCounters(string filter = \"\") {\n        string ft = (filter == \"\") ? \"friends,messages,groups,notifications\" : filter;\n        auto resp = vkget(\"account.getCounters\", [ \"filter\": ft ]);\n        vkCounters rt;\n\n        if(resp.type == JSON_TYPE.ARRAY) return rt;\n\n        foreach(c; resp.object.keys) switch (c) {\n            case \"messages\": rt.messages = resp[c].integer.to!int; break;\n            case \"friends\": rt.friends = resp[c].integer.to!int; break;\n            case \"notifications\": rt.notifications = resp[c].integer.to!int; break;\n            case \"groups\": rt.groups = resp[c].integer.to!int; break;\n            default: break;\n        }\n        return rt;\n    }\n\n    int messagesCounter() {\n        int u = ps.countermsg;\n        if(ps.lp80got) {\n            u = accountGetCounters(\"messages\").messages;\n            ps.countermsg = u;\n            ps.lp80got = false;\n        }\n        return u;\n    }\n\n    vkFriend[] friendsGet(int count, int offset, out int serverCount, int user_id = 0) {\n        auto params = [ \"fields\": \"online,last_seen\", \"order\": \"hints\"];\n        if(user_id != 0) params[\"user_id\"] = user_id.to!string;\n        if(count != 0) params[\"count\"] = count.to!string;\n        if(offset != 0) params[\"offset\"] = offset.to!string;\n\n        auto resp = vkget(\"friends.get\", params);\n        serverCount = resp[\"count\"].integer.to!int;\n\n\n        auto ct = Clock.currTime();\n        vkFriend[] rt;\n\n        foreach(f; resp[\"items\"].array) {\n            auto last = \"last_seen\" in f ? f[\"last_seen\"][\"time\"].integer.to!long : 0;\n\n            //auto laststr = agotime(ct, last);\n            //auto laststr = getLocal(\"lastseen\") ~ vktime(ct, last);\n            auto laststr = last > 0 ? vktime(ct, last) : getLocal(\"banned\");\n\n            vkFriend friend = {\n              first_name: f[\"first_name\"].str,\n              last_name: f[\"last_name\"].str,\n              id: f[\"id\"].integer.to!int,\n              online: ( f[\"online\"].integer.to!int == 1 ),\n              last_seen_utime: last,  last_seen_str: laststr\n            };\n\n            nc.addToCache(friend.id, cachedName(friend.first_name, friend.last_name, friend.online));\n\n            rt ~= friend;\n        }\n\n        return rt;\n    }\n\n    vkAudio[] audioGet(int count, int offset, out int serverCount, int owner_id = 0, int album_id = 0) {\n        string[string] params;\n        if(owner_id != 0) params[\"owner_id\"] = owner_id.to!string;\n        if(album_id != 0) params[\"album_id\"] = album_id.to!string;\n        if(count != 0) params[\"count\"] = count.to!string;\n        if(offset != 0) params[\"offset\"] = offset.to!string;\n\n        auto resp = vkget(\"audio.get\", params);\n        serverCount = resp[\"count\"].integer.to!int;\n\n        vkAudio[] rt;\n        foreach(a; resp[\"items\"].array) {\n            int ad = a[\"duration\"].integer.to!int;\n            auto adm = ad.convert!(\"seconds\", \"minutes\");\n            auto ads = ad - (60*adm);\n\n            vkAudio aud = {\n                id: a[\"id\"].integer.to!int, owner: a[\"owner_id\"].integer.to!int,\n                artist: a[\"artist\"].str, title: a[\"title\"].str, url: a[\"url\"].str,\n                duration_sec: ad, duration_str: (adm.to!string ~ \":\" ~ tzr(ads.to!int))\n            };\n            rt ~= aud;\n        }\n        return rt;\n    }\n\n    vkGroup[] groupsGetById(int[] group_ids) {\n        vkGroup[] rt;\n        if(group_ids.length == 0) return rt;\n\n        auto gids = group_ids.map!(g => (g * -1).to!string).join(\",\");\n        //dbm(\"gids: \" ~ gids);\n        auto params = [ \"group_ids\": gids ];\n        auto resp = vkget(\"groups.getById\", params);\n\n        if(resp.type != JSON_TYPE.ARRAY) return rt;\n\n        foreach(g; resp.array) {\n            rt ~= vkGroup(g[\"id\"].integer.to!int * -1, g[\"name\"].str);\n        }\n        return rt;\n    }\n\n    private apiFwdIter digFwd(JSONValue[] fwdp, SysTime ct, int mdp, int cdp) {\n        int cmd = (cdp > mdp) ? cdp : mdp;\n        vkFwdMessage[] rt;\n        foreach(j; fwdp) {\n            int fid = j[\"user_id\"].integer.to!int;\n            long ut = j[\"date\"].integer.to!long;\n\n            vkFwdMessage[] fw;\n            if(\"fwd_messages\" in j) {\n                auto r = digFwd(j[\"fwd_messages\"].array, ct, cmd, cdp+1);\n                if(r.md > cmd) cmd = r.md;\n                fw = r.fwd;\n            }\n\n            nc.requestId(fid);\n\n            vkFwdMessage mm = {\n                author_id: fid,\n                utime: ut, time_str: vktime(ct, ut),\n                body_lines: getmbody(j),\n                fwd: fw\n            };\n            rt ~= mm;\n        }\n        return apiFwdIter(rt, cmd);\n    }\n\n    private void resolveFwdRecv(ref vkFwdMessage[] inp) {\n        foreach(ref m; inp) {\n            m.author_name = nc.getName(m.author_id).strName;\n            if(m.fwd.length != 0) resolveFwdRecv(m.fwd);\n        }\n    }\n\n    private void resolveFwdNames(ref vkMessage[] inp) {\n        nc.resolveNames();\n        inp.map!(q => q.fwd).filter!(q => q.length != 0).each!(q => resolveFwdRecv(q));\n    }\n\n    string action_prefix = \" > \";\n    private string[] getmbody(JSONValue m) {\n        string[] mbody = m[\"body\"].str.split(\"\\n\");\n        if(\"attachments\" in m) {\n            foreach(att; m[\"attachments\"].array) {\n                switch(att[\"type\"].str) {\n                    case \"photo\":\n                        JSONValue o = att[\"photo\"].object;\n                        int k = -1;\n                        foreach(size; [75, 130, 604, 807, 1280, 2560])\n                            if (\"photo_\" ~ to!string(size) in o)\n                                k = size;\n                        mbody ~= o[\"text\"].str ~ \" / \" ~\n                            SysTime.fromUnixTime(o[\"date\"].integer).toSimpleString();\n                        mbody ~= (o)[\"photo_\" ~ to!string(k)].str;\n                        break;\n                    case \"audio\":\n                        JSONValue o = att[\"audio\"].object;\n                        mbody ~= o[\"artist\"].str ~ \" - \" ~ o[\"title\"].str ~\n                            format(\" (%02d:%02d)\", o[\"duration\"].integer / 60, o[\"duration\"].integer % 60);\n                        mbody ~= o[\"url\"].str.split(\"?extra\")[0];\n                        break;\n                    case \"doc\":\n                        JSONValue o = att[\"doc\"].object;\n                        mbody ~= o[\"title\"].str ~ \" (\" ~ o[\"ext\"].str ~ \", \" ~ to!string(o[\"size\"].integer) ~ \"): \" ~ o[\"url\"].str;\n                        break;\n                    case \"link\":\n                        JSONValue o = att[\"link\"].object;\n                        if (o[\"title\"].str == o[\"url\"].str)\n                            mbody ~= o[\"url\"].str;\n                        else\n                            mbody ~= o[\"title\"].str ~ \": \" ~ o[\"url\"].str;\n                        break;\n                    case \"sticker\":\n                        mbody ~= \"Sticker: \" ~ att[\"sticker\"].object[\"photo_352\"].str;\n                        break;\n                    case \"video\":\n                        JSONValue o = att[\"video\"].object;\n                        mbody ~= o[\"title\"].str ~ \" (video): https://vk.com/video\" ~ o[\"owner_id\"].to!string ~ \"_\" ~ o[\"id\"].to!string;\n                        break;\n                    case \"wall\":\n                        JSONValue o = att[\"wall\"].object;\n                        mbody ~= o[\"text\"].str ~ \" (post): https://vk.com/wall\" ~ o[\"from_id\"].to!string ~ \"_\" ~ o[\"id\"].to!string;\n                        break;\n                    default:\n                        mbody ~= \"Unsupported attachment: \" ~ att[\"type\"].str;\n                        break;\n                }\n            }\n        }\n\n        if (\"action\" in m) {\n            string action_user = action_prefix ~ nc.getName(m[\"user_id\"].integer.to!int).strName;\n            switch (m[\"action\"].str) {\n                case \"chat_create\":\n                    mbody ~= action_user ~ \" \" ~ getLocal(\"c_create\") ~ \"\\\"\" ~ m[\"action_text\"].str ~ \"\\\"\";\n                    break;\n                case \"chat_title_update\":\n                    mbody ~= action_user ~ \" \" ~ getLocal(\"c_title\") ~ \"\\\"\" ~ m[\"action_text\"].str ~ \"\\\"\";\n                    break;\n                case \"chat_photo_update\":\n                    mbody ~= action_user ~ \" \" ~ getLocal(\"c_setphoto\");\n                    break;\n                case \"chat_photo_remove\":\n                    mbody ~= action_user ~ \" \" ~ getLocal(\"c_removephoto\");\n                    break;\n                case \"chat_invite_user\":\n                    if (m[\"action_mid\"].integer > 0) {\n                        if (m[\"action_mid\"].integer == m[\"from_id\"].integer)\n                            mbody ~= action_user ~ \" \" ~ getLocal(\"c_inviteself\");\n                        else\n                            mbody ~= action_user ~ \" \" ~ getLocal(\"c_invite\") ~ nc.getName(to!int(m[\"action_mid\"].integer)).strName;\n                    }\n                    else {\n                        mbody ~= action_user ~ \" \" ~ getLocal(\"c_invite\") ~ m[\"action_email\"].str;\n                    }\n                    break;\n                case \"chat_kick_user\":\n                    if (m[\"action_mid\"].integer > 0) {\n                        if (m[\"action_mid\"].integer == m[\"from_id\"].integer)\n                            mbody ~= action_user ~ \" \" ~ getLocal(\"c_kickself\");\n                        else\n                            mbody ~= action_user ~ \" \" ~ getLocal(\"c_kick\") ~ nc.getName(to!int(m[\"action_mid\"].integer)).strName;\n                    }\n                    else {\n                        mbody ~= action_user ~ \" \" ~ getLocal(\"c_kick\") ~ m[\"action_email\"].str;\n                    }\n                    break;\n                default: break;\n            }\n        }\n        return mbody;\n    }\n\n    private vkMessage[] parseMessageObjects(JSONValue[] items, SysTime ct) {\n        vkMessage[] rt;\n        int i = 0;\n        foreach(m; items) {\n            int fid;\n            int mid = m[\"id\"].integer.to!int;\n            auto hascid = (\"chat_id\" in m);\n            int uid = m[\"user_id\"].integer.to!int;\n            int pid = (hascid) ? (m[\"chat_id\"].integer.to!int + convStartId) : uid;\n            long rid = (\"random_id\" in m) ? m[\"random_id\"].integer.to!long : 0;\n            long ut = m[\"date\"].integer.to!long;\n            bool outg = (m[\"out\"].integer.to!int == 1);\n            bool rstate = (m[\"read_state\"].integer.to!int == 1);\n            //bool unr = (outg && !rstate);\n            bool unr = !rstate;\n\n            if(\"from_id\" in m) {\n                fid = m[\"from_id\"].integer.to!int;\n            } else {\n                if(hascid || !outg) {\n                    fid = uid;\n                } else {\n                    fid = me.id;\n                }\n            }\n\n            string[] mbody = getmbody(m);\n            string st = vktime(ct, ut);\n\n            int fwdp = -1;\n            vkFwdMessage[] fw;\n            if(\"fwd_messages\" in m) {\n                auto r = digFwd(m[\"fwd_messages\"].array, ct, 0, 1);\n                fwdp = r.md;\n                fw = r.fwd;\n            }\n\n            auto mo = vkMessage();\n            mo.outgoing = outg; mo.unread = unr; mo.utime = ut;\n            mo.author_name = nc.getName(fid).strName;\n            mo.time_str = st; mo.body_lines = mbody;\n            mo.fwd_depth = fwdp; mo.fwd = fw; mo.needName = true;\n            mo.msg_id = mid; mo.author_id = fid; mo.peer_id = pid;\n            mo.rndid = rid;\n\n            rt ~= mo;\n            ++i;\n        }\n        resolveFwdNames(rt);\n        return rt;\n    }\n\n    vkMessage[] messagesGetHistory(int peer_id, int count, int offset, out int servercount, out int unreadcount, int start_message_id = -1, bool rev = false) {\n        auto ct = Clock.currTime();\n        auto params = [ \"peer_id\": peer_id.to!string ];\n        if(count >= 0) params[\"count\"] = count.to!string;\n        if(offset != 0) params[\"offset\"] = offset.to!string;\n        if(start_message_id > 0) params[\"start_message_id\"] = start_message_id.to!string;\n        if(rev) params[\"rev\"] = \"1\";\n\n        auto resp = vkget(\"messages.getHistory\", params);\n        auto items = resp[\"items\"].array;\n        auto respuc = \"unread\" in resp;\n\n        servercount = resp[\"count\"].integer.to!int;\n        unreadcount = respuc ? respuc.integer.to!int : 0;\n\n        return parseMessageObjects(items, ct);\n    }\n\n    vkMessage[] messagesGetById(int[] mids) {\n        auto ct = Clock.currTime();\n        auto params = [ \"message_ids\": mids.map!(q => q.to!string).join(\",\") ];\n        auto resp = vkget(\"messages.getById\", params);\n        auto items = resp[\"items\"].array;\n\n        return parseMessageObjects(items, ct);\n    }\n\n    int messagesSend(int pid, string msg, int rndid = 0, int[] fwd = [], string[] attaches = []) {\n        if(msg.length == 0 && fwd.length == 0 && attaches.length == 0) return -1;\n\n        vkgetparams gp = {\n            setloading: true,\n            attempts: 4,\n            thrownf: true,\n            notifynf: false\n        };\n\n        auto params = [\"peer_id\": pid.to!string ];\n        params[\"message\"] = msg;\n        if(rndid != 0) params[\"random_id\"] = rndid.to!string;\n        if(fwd.length != 0) params[\"forward_messages\"] = fwd.map!(q => q.to!string).join(\",\");\n        if(attaches.length != 0) params[\"attachment\"] = attaches.join(\",\");\n\n        auto resp = vkget(\"messages.send\", params, false, gp);\n        return resp.integer.to!int;\n    }\n\n    void messagesMarkAsRead(int pid, int smid = 0) {\n        auto params = [\"peer_id\": pid.to!string ];\n        if(smid != 0) params[\"start_message_id\"] = smid.to!string;\n\n        auto resp = vkget(\"messages.markAsRead\", params);\n    }\n}\n\nclass AsyncOrder : Thread {\n    const int maxFails = 10;\n\n    struct Task {\n        int id;\n        void delegate() dg;\n    }\n\n    private\n        Task[] order;\n        Mutex orderAccess;\n        int failCount;\n        string lastExp;\n        string title;\n\n    this(string t) {\n        title = t;\n        orderAccess = new Mutex();\n        super(&procOrder);\n    }\n\n    void addToOrder(void delegate() d, int ordid) {\n        synchronized(orderAccess) {\n            immutable auto has = order.map!(q => q.id).canFind(ordid);\n            if(!has) order ~= Task(ordid, d);\n        }\n    }\n\n    bool hasId(int ordid) {\n        return order.map!(q => q.id).canFind(ordid);\n    }\n\n    private void procOrder() {\n        logThread(\"async_order \" ~ title);\n        dbm(\"procOrder start\");\n        while(true) {\n            if(order.length != 0) {\n                for (int i; i < order.length; ++i) {\n                    try {\n                        order[i].dg();\n                    }\n                    catch (Exception e) {\n                        auto expstr = \"procOrder \" ~ title ~ \" task: \"\n                              ~ order[i].id.to!string ~ \" \" ~ typeof(e).stringof ~ \": \" ~ e.msg;\n                        dbm(expstr);\n                        lastExp = expstr;\n\n                        ++failCount;\n                        if(failCount > maxFails) {\n                            dbm(\"too many errors in procOrder, performing exit\");\n                            dropClient(\"too many errors in procOrder, last: \\n\" ~ lastExp);\n                        }\n                    }\n                }\n                synchronized(orderAccess) {\n                    order = [];\n                }\n            }\n            Thread.sleep(dur!\"msecs\"(100));\n        }\n    }\n}\n\nclass AsyncSingle : Thread {\n\n    this(string t) {\n        title = t;\n        super(&func);\n    }\n\n    private {\n        void delegate() dg;\n        string title;\n    }\n\n    void startFunc(void delegate() d) {\n        if(!this.isRunning) {\n            dg = d;\n            this.start();\n        }\n    }\n\n    private void func() {\n        logThread(\"async_single \" ~ title);\n        try {\n            if(dg) dg();\n        }\n        catch (Exception e) {\n            auto expstr = \"procSingle \" ~ title ~ \" \" ~ typeof(e).stringof ~ \": \" ~ e.msg;\n            dbm(expstr);\n        }\n    }\n\n}\n\nclass AsyncMan {\n\n    static string httpget(string addr, Duration timeout, uint attempts) {\n        string content = \"\";\n        auto client = HTTP();\n\n        int tries = 0;\n        bool ok = false;\n\n        while (!ok) {\n            try {\n                client.method = HTTP.Method.get;\n                client.url = addr;\n                client.setUserAgent(appUserAgent);\n\n                client.dataTimeout = timeout;\n                client.operationTimeout = timeout;\n                client.connectTimeout = timeout;\n\n                client.onReceive = (ubyte[] data) {\n                    auto sz = data.length;\n                    content ~= (cast(immutable(char)*)data)[0..sz];\n                    return sz;\n                };\n                client.perform();\n                ok = true;\n                //dbm(\"recv content: \" ~ content);\n            } catch (CurlException e) {\n                ++tries;\n                dbm(\"[attempt \" ~ (tries.to!string) ~ \"] network error: \" ~ e.msg);\n                if(tries >= attempts) {\n                    throw new NetworkException(\"httpget\");\n                }\n                Thread.sleep( dur!\"msecs\"(mssleepBeforeAttempt) );\n            }\n        }\n        return content;\n    }\n\n    const string\n        S_SELF_RESOLVE = \"s_self_resolve\",\n        S_ONLINE_STATUS = \"s_online_status\",\n        S_TYPING = \"s_typing_\",\n        S_READ = \"s_readm\",\n        O_LOADBLOCK = \"o_loadblock\",\n        O_SENDMSG = \"o_sendm\";\n\n    AsyncOrder[string] orders;\n    AsyncSingle[string] singles;\n\n    void orderedAsync(string orderkey, int ordid, void delegate() d) {\n        auto so = orderkey in orders;\n        if(!so) {\n            orders[orderkey] = new AsyncOrder(orderkey);\n            so = orderkey in orders;\n        }\n\n        so.addToOrder(d, ordid);\n        if(!so.isRunning)\n            so.start();\n    }\n\n    void singleAsync(string singlekey, void delegate() d) {\n        auto ss = singlekey in singles;\n        if(!ss) {\n            singles[singlekey] = new AsyncSingle(singlekey);\n            ss = singlekey in singles;\n        }\n\n        ss.startFunc(d);\n    }\n\n}\n\n\nclass Longpoll : Thread {\n\n    private {\n        VkMan man;\n        VkApi api;\n    }\n\n    int longpollWait = 25;\n\n    this(VkMan vkman) {\n        man = vkman;\n        api = man.api;\n        super(&startSyncAsyncWrapper);\n    }\n\n    private void startSyncAsyncWrapper() {\n        logThread(\"longpoll\");\n        startSync();\n    }\n\n    void startSync() {\n        if(!api.isInitAttemptFinished()) {\n            dbm(\"longpoll is waiting for api init...\");\n            while(!api.isInitAttemptFinished()) {\n                Thread.sleep(dur!\"msecs\"(300));\n            }\n        }\n        dbm(\"longpoll is starting...\");\n        while(true) {\n            try {\n                doLongpoll(getLongpollServer());\n            }\n            catch (InternalException e) {\n                if(e.ecode == e.E_LPRESTART) {\n                    dbm(\"Network error in longpoll, lp shutdown\");\n                    man.asyncAccountInit();\n                    man.sheduleForceUpdate(false);\n                    return;\n                }\n                else {\n                    dbm(\"longpoll InternalException: \" ~ e.msg);\n                }\n            }\n            catch(Exception e) {\n                dbm(\"longpoll exception: \" ~ e.msg);\n            }\n            catch(Error e) {\n                dbm(\"longpoll error exception: \" ~ e.msg);\n                dbm(\"longpoll exit\");\n                return;\n            }\n            dbm(\"longpoll is restarting...\");\n        }\n    }\n\n\n    vkLongpoll getLongpollServer() {\n        auto resp = api.vkget(\"messages.getLongPollServer\", [ \"use_ssl\": \"1\", \"need_pts\": \"1\" ]);\n        vkLongpoll rt = {\n            server: resp[\"server\"].str,\n            key: resp[\"key\"].str,\n            ts: resp[\"ts\"].integer.to!int\n        };\n        return rt;\n    }\n\n\n    const bool longpollRethrow = true;\n\n    void doLongpoll(vkLongpoll start) {\n        auto tm = dur!timeoutFormat(longpollCurlTimeout);\n        int cts = start.ts;\n        auto mode = (2 + 128).to!string; //attaches + random_id\n        auto wait = longpollWait.to!string;\n        bool ok = true;\n        dbm(\"longpoll works\");\n        while(ok) {\n            try {\n                man.doShedForceUpdate();\n                if(cts < 1) break;\n                string url = \"https://\" ~ start.server ~ \"?act=a_check&key=\" ~ start.key ~ \"&ts=\" ~ cts.to!string\n                                                                                            ~ \"&wait=\" ~ wait\n                                                                                            ~ \"&mode=\" ~ mode;\n                auto resp = AsyncMan.httpget(url, tm, longpollCurlAttempts);\n                immutable auto next = parseLongpoll(resp);\n                if(next.failed != -1) {\n                    dbm(\"longpoll got 'failed \" ~ next.failed.to!string ~ \"'\");\n                    if(next.failed == 1) {\n                        dbm(\"requesting force update\");\n                        man.forceUpdateAll();\n                    }\n                    else if (next.failed == 4) {\n                        dbm(\"unknown longpoll versoion  THEY'RE PURGED V0?\");\n                        throw new ApiErrorException(\"invalid longpoll version\", -1);\n                    }\n                    else {\n                        dbm(\"'key' expired or something\");\n                        dbm(\"requesting new lonpoll server\");\n                        ok = false;\n                    }\n                }\n                cts = next.ts;\n            }\n            catch(NetworkException e) {\n                dbm(\"longpoll can't get new events\");\n                throw new InternalException(InternalException.E_LPRESTART);\n            }\n        }\n    }\n\n    vkNextLp parseLongpoll(string resp) {\n        JSONValue j = parseJSON(resp);\n        vkNextLp rt;\n        auto ct = Clock.currTime();\n        auto failed = (\"failed\" in j ? j[\"failed\"].integer.to!int : -1 );\n        auto ts = (\"ts\" in j ? j[\"ts\"].integer.to!int : -1 );\n        if(failed == -1) {\n            auto upd = j[\"updates\"].array;\n            dbm(\"new lp: \" ~ j.toPrettyString());\n            foreach(u; upd) {\n                switch(u[0].integer.to!int) {\n                    case 4: //new message\n                        triggerNewMessage(u, ct);\n                        break;\n                    case 80: //counter update\n                        if(return80mc) {\n                            ps.countermsg = u[1].integer.to!int;\n                        } else {\n                            ps.lp80got = true;\n                        }\n                        man.toggleUpdate();\n                        break;\n                    case 6: //inbox read\n                        triggerRead(u);\n                        break;\n                    case 7: //outbox read\n                        triggerRead(u);\n                        break;\n                    case 8: //online\\offline\n                        triggerOnline(u, ct);\n                        break;\n                    case 9: //online\\offline\n                        triggerOnline(u, ct);\n                        break;\n                    default:\n                        break;\n                }\n            }\n        }\n        resolveMidOrder();\n        rt.ts = ts;\n        rt.failed = failed;\n        return rt;\n    }\n\n    alias processnmFunc = void delegate(vkMessage);\n\n    processnmFunc[int] midResolveOrder;\n\n    void resolveMidOrder() {\n        if(midResolveOrder.length == 0) return;\n        api.messagesGetById(midResolveOrder.keys)\n                    .each!(q => midResolveOrder[q.msg_id](q));\n        //midResolveOrder.clear(); - bad for ldc\n        midResolveOrder = midResolveOrder.init;\n        man.toggleUpdate();\n    }\n\n    void triggerNewMessage(JSONValue ui, SysTime ct) {\n        auto u = ui.array;\n\n        auto mid = u[1].integer.to!int;\n        auto flags = u[2].integer.to!int;\n        auto peer = u[3].integer.to!int;\n        auto utime = u[4].integer.to!long;\n        auto msg = u[6].str.longpollReplaces;\n        auto att = u[7];\n        long rndid = (u.length > 8) ? u[8].integer.to!long : 0;\n\n        bool outbox = (flags & 2) == 2;\n        bool unread = (flags & 1) == 1;\n        bool hasattaches = att.object.keys.map!(a => (a == \"fwd\") || a.matchAll(r\"attach.*\")).any!\"a\";\n\n        auto conv = (peer > convStartId);\n        bool group = false;\n\n        if(!conv && peer > longpollGimStartId) {\n            peer = -(peer - longpollGimStartId);\n            group = true;\n        }\n\n        auto from = conv ? att[\"from\"].str.to!int : ( outbox ? api.me.id : peer );\n\n        auto title = conv ? u[5].str : nc.getName(peer).strName;\n        auto haspeer = (peer in man.chatFactory);\n\n        if(!haspeer) {\n            man.chatFactory[peer] = man.generateBF!ClMessage(ClMessage.getLoadFunc(peer, (u) => man.setUnreads(peer, u)));\n            man.chatFactory[peer].data.forceUpdate = true;\n        }\n\n        auto cf = man.chatFactory[peer];\n\n        auto processnm = delegate (vkMessage nmsg) {\n            dbm(\"processnm\");\n            auto rid = nmsg.rndid;\n            auto realmsg = cf.getLoadedObjects\n                                        .filter!(q => !q.getObject.isZombie)\n                                        .takeOne();\n            if(!realmsg.empty) {\n                auto lastm = realmsg.front.getObject;\n                nmsg.needName = !(lastm.author_id == from && (utime-lastm.utime) <= needNameMaxDelta);\n            }\n\n            auto sent = rid in ps.sent;\n            if(sent) {\n                dbm(\"approved sent nm rid: \" ~ rid.to!string);\n                cf.removeZombie(rid);\n                ps.sent.remove(rid);\n            }\n            cf.addBack(new ClMessage(nmsg));\n            if(!outbox && unread) cf.unreadCount += 1;\n        };\n\n        auto df = man.dialogsFactory;\n\n        if(!df.isOverrided(peer) && !df.isBlank()) {\n            auto blockdlg = df.getLoadedObjects\n                                .filter!(q => q.getPeer == peer)\n                                .takeOne;\n            auto uc = blockdlg.empty ? 0 : blockdlg.front.getObject.unreadCount;\n            df.overrideDialog(new ClDialog(title, peer, uc, cf), ct.toUnixTime);\n        }\n        else df.overrideBump(peer, ct.toUnixTime);\n\n        if(!hasattaches) {\n            vkMessage lpnm = {\n                author_id: from, peer_id: peer, msg_id: mid,\n                outgoing: outbox, unread: unread, rndid: rndid,\n                utime: utime, time_str: vktime(ct, utime),\n                author_name: nc.getName(from).strName,\n                body_lines: msg.split(\"\\n\"),\n                fwd_depth: -1, needName: true\n            };\n            processnm(lpnm);\n        } else {\n            midResolveOrder[mid] = processnm;\n        }\n\n        if(from != api.me.id && ( ps.showConvNotifies ? true : !conv )) ps.lastlp = title ~ \": \" ~ msg;\n        man.toggleUpdate();\n    }\n\n    void triggerRead(JSONValue u) {\n        bool inboxrd = (u[0].integer == 6);\n        auto peer = u[1].integer.to!int;\n        auto mid = u[2].integer.to!int;\n\n        if(peer > longpollGimStartId && peer < convStartId) {\n            peer = -(peer-longpollGimStartId);\n        }\n\n        dbm(\"rd trigger peer: \" ~ peer.to!string ~ \", mid: \" ~ mid.to!string ~ \", inbox: \" ~ inboxrd.to!string);\n\n        synchronized(pbMutex) {\n            auto ch = peer in man.chatFactory;\n            int unreadc;\n\n            if(ch) {\n                unreadc = ch.unreadCount;\n                auto chl = ch.getLoadedObjects;\n                chl\n                  .map!(q => q.getObject.msg_id == mid)\n                  .countUntil(true);\n\n\n                while( !chl.empty && ( (chl.front !is null && chl.front.getObject.outgoing != inboxrd) ? chl.front.getObject.unread : true) ) {\n                    auto mobj = chl.front.getObject;\n                    if(mobj.outgoing != inboxrd) {\n                        mobj.unread = false;\n                        if(inboxrd) --unreadc;\n                        chl.front.invalidateLineCache();\n                    }\n                    chl.popFront();\n                }\n                ch.unreadCount = unreadc < 0 ? 0 : unreadc;\n            }\n            else {\n                auto dlone = man.dialogsFactory.getLoadedObjects\n                    .map!(q => q.getObject)\n                    .filter!(q => q.id == peer)\n                    .takeOne();\n                auto hasdlone = !dlone.empty;\n                if(hasdlone) {\n                   if(mid == dlone.front.lastmid) {\n                        dlone.front.unreadCount = 0;\n                        dlone.front.unread = false;\n                   }\n                }\n            }\n\n        }\n\n        man.toggleUpdate();\n    }\n\n    void triggerOnline(JSONValue u, SysTime ct) {\n        auto uid = u[1].integer.to!int * -1;\n        auto flags = u[2].integer.to!int;\n        auto event = u[0].integer.to!int;\n        bool exit = (event == 9);\n        if(!exit && event != 8) return;\n\n        if(exit) {\n            nc.setOnline(uid, false);\n            //auto uct = ct.toUnixTime();\n            //lsc[uid].last_seen_utime = uct; //todo check! Range Violation (override last seen)\n            //lsc[uid].last_seen_str = vktime(ct, uct);\n        }\n        else nc.setOnline(uid, true);\n\n        man.toggleUpdate();\n    }\n\n}\n\nclass OnlineNotifier : Thread {\n\n    private {\n        bool enabled = false;\n        VkApi api;\n        AsyncMan a;\n        const int retryMin = 14;\n    }\n\n    this(VkApi wapi, AsyncMan wa) {\n        api = wapi;\n        a = wa;\n        super(&onlineRoutine);\n    }\n\n    void tryStart() {\n        if(!this.isRunning) this.start();\n        else dbm(\"onlineNotifier running already\");\n    }\n\n    void setOnlineSw(bool sw) {\n        if(enabled == sw) return;\n        enabled = sw;\n        if(sw) {\n            dbm(\"starting onlineNotifier...\");\n            tryStart();\n        }\n        else {\n            a.singleAsync(a.S_ONLINE_STATUS, () => api.accountSetOffline());\n            dbm(\"offline status sent (shed)\");\n            dbm(\"sheduled onlineNotifier shutdown\");\n            pragma(msg, \"Reticulating splines...\");\n        }\n    }\n\n    private void onlineRoutine() {\n        logThread(\"online_notifier\");\n        while(true) {\n            if(enabled) {\n                api.accountSetOnline();\n                dbm(\"online status sent\");\n                Thread.sleep( dur!\"minutes\"(retryMin) );\n            }\n            else {\n                dbm(\"onlineNotifier shutdown\");\n            }\n        }\n    }\n\n}\n\nclass VkMan {\n\n    alias ChatBlockFactory = BlockFactory!ClMessage;\n\n    __gshared {\n        VkApi api;\n        vkUser* me;\n        AsyncMan a;\n\n        BlockFactory!ClDialog dialogsFactory;\n        BlockFactory!ClFriend friendsFactory;\n        BlockFactory!ClAudio musicFactory;\n        ChatBlockFactory[int] chatFactory; //by peer\n\n        Longpoll longpollThread;\n        OnlineNotifier onlineThread;\n    }\n\n    this(string token) {\n        a = new AsyncMan();\n        api = new VkApi(token, &connectionProblems);\n        baseInit();\n        asyncAccountInit();\n    }\n\n    private void accountInit() {\n        ps.countermsg = -1;\n        dbm(\"asyncLongpoll called\");\n        asyncLongpoll();\n\n        nc = new nameCache(api);\n\n        selfResolve();\n    }\n\n    private void selfResolve(){\n        api.resolveMe();\n        if(!api.isTokenValid_) {\n            toggleUpdate();\n            return;\n        }\n\n        api.addMeNC();\n        me = &(api.me);\n\n        dialogsFactory.data.serverCount = api.initdata.sc_dialogs;\n        friendsFactory.data.serverCount = api.initdata.sc_friends;\n        musicFactory.data.serverCount = api.initdata.sc_audio;\n\n        ps.countermsg = api.initdata.c_messages;\n\n        toggleUpdate();\n    }\n\n    void setLongpollWait(int wait) {\n        longpollThread.longpollWait = wait;\n    }\n\n    void connectionProblems() {\n\n    }\n\n    void forceUpdateAll(bool selfresolve = true) {\n        if(selfresolve) a.singleAsync(a.S_SELF_RESOLVE, () => selfResolve());\n        toggleForceUpdate(blockType.music);\n        toggleForceUpdate(blockType.dialogs);\n        toggleForceUpdate(blockType.friends);\n        chatFactory.values.each!(q => q.data.forceUpdate = true);\n    }\n\n    void sheduleForceUpdate(bool selfresolve) {\n        ps.shedForceUpdateFlag = true;\n        ps.shedForceUpdateSelfResolve = selfresolve;\n    }\n\n    void doShedForceUpdate() {\n        if(ps.shedForceUpdateFlag) {\n            ps.shedForceUpdateFlag = false;\n            forceUpdateAll(ps.shedForceUpdateSelfResolve);\n        }\n    }\n\n    void asyncAccountInit() {\n        a.singleAsync(a.S_SELF_RESOLVE, () => accountInit());\n    }\n\n    BlockFactory!T generateBF(T)(T[] delegate(VkApi, uint, uint, out int) ld, uint dwblock = defaultBlock) {\n        return new BlockFactory!T(\n            new BlockObjectParameters!T(api, dwblock, a, ld, &notifyBlockDownloadDone));\n    }\n\n    private void baseInit() {\n        sndMutex = new Mutex();\n        pbMutex = new Mutex();\n        ps = apiState();\n\n        longpollThread = new Longpoll(this);\n        onlineThread = new OnlineNotifier(api, a);\n\n        dialogsFactory = generateBF!ClDialog(ClDialog.getLoadFunc());\n        friendsFactory = generateBF!ClFriend(ClFriend.getLoadFunc());\n        musicFactory = generateBF!ClAudio(ClAudio.getLoadFunc());\n    }\n\n    bool isSomethingUpdated() {\n        if(ps.somethingUpdated){\n            ps.somethingUpdated = false;\n            return true;\n        }\n        return false;\n    }\n\n    void toggleUpdate() {\n        ps.somethingUpdated = true;\n    }\n\n    private auto getData(blockType tp) {\n        switch(tp){\n            case blockType.dialogs: return &(dialogsFactory.data);\n            case blockType.friends: return &(friendsFactory.data);\n            case blockType.music: return &(musicFactory.data);\n            default: assert(0);\n        }\n    }\n\n    void asyncLongpoll() {\n        longpollThread.start();\n    }\n\n    void toggleForceUpdate(blockType tp) {\n        getData(tp).forceUpdate = true;\n        toggleUpdate();\n    }\n\n    void toggleChatForceUpdate(int peer) {\n        chatFactory[peer].data.forceUpdate = true;\n        toggleUpdate();\n    }\n\n    int getServerCount(blockType tp) {\n        return getData(tp).serverCount;\n    }\n\n    bool isScrollAllowed(blockType tp) {\n        return true;\n    }\n\n    bool isChatScrollAllowed(int peer) {\n        return true;\n    }\n\n    bool isLoading() {\n        return ps.loadingiter != 0;\n    }\n\n    int getChatServerCount(int peer) {\n        auto c = peer in chatFactory;\n        if(c) return c.data.serverCount;\n        else return -1;\n    }\n\n    int getChatLineCount(int peer, int ww) {\n        auto c = peer in chatFactory;\n        if(c) return (*c).getServerLineCount(ww);\n        else return -1;\n    }\n\n    string getLastLongpollMessage() {\n        auto last = ps.lastlp;\n        ps.lastlp = \"\";\n        return last;\n    }\n\n    void notifyBlockDownloadDone() {\n        toggleUpdate();\n    }\n\n    void sendOnline(bool state) {\n        onlineThread.setOnlineSw(state);\n    }\n\n    void showConvNotifications(bool state) {\n        ps.showConvNotifies = state;\n    }\n\n    vkFriend[] getBufferedFriends(int count, int offset) {\n        return bufferedGet!vkFriend(friendsFactory, count, offset);\n    }\n\n    vkAudio[] getBufferedMusic(int count, int offset) {\n        return bufferedGet!vkAudio(musicFactory, count, offset);\n    }\n\n    vkDialog[] getBufferedDialogs(int count, int offset) {\n        return bufferedGet!vkDialog(dialogsFactory, count, offset);\n    }\n\n    void setUnreads(int peer, int uc) {\n        auto cf = peer in chatFactory;\n        if(cf) {\n            cf.unreadCount = uc;\n        }\n    }\n\n    vkMessageLine[] getBufferedChatLines(int count, int offset, int peer, int wrapwidth) {\n        if(offset < 0) offset = 0;\n        auto f = peer in chatFactory;\n        if(!f) {\n            chatFactory[peer] = generateBF!ClMessage(ClMessage.getLoadFunc(peer, (u) => setUnreads(peer, u)));\n            f = peer in chatFactory;\n            auto blockdlg = dialogsFactory.getLoadedObjects\n                                .filter!(q => q.getPeer == peer)\n                                .takeOne;\n            auto uc = blockdlg.empty ? 0 : blockdlg.front.getObject.unreadCount;\n        }\n\n        /*dbm(\"bfcl p: \" ~ peer.to!string ~ \", o: \" ~ offset.to!string ~ \", c: \" ~ count.to!string\n                                ~ \", sc: \" ~ f.getServerLineCount(wrapwidth).to!string ~ \" sco: \"\n                                ~ f.data.serverCount.to!string);*/\n\n        synchronized(pbMutex) {\n            if(!f.prepare) return [];\n            f.seek(0);\n\n            f.isBlank();\n\n            auto rt = (*f)\n                    .filter!(q => q !is null)\n                    .inputRetro\n                    .map!(q => q.getLines(wrapwidth))\n                    .joinerBidirectional\n                    .dropBack(offset)\n                    .takeBackArray(count);\n            return rt;\n        }\n    }\n\n    int messagesCounter() {\n        return ps.countermsg;\n    }\n\n    private void sendMessageImpl(int rid, int peer, string msg) {\n        try {\n            auto sentmid = api.messagesSend(peer, msg, rid);\n            dbm(\"message sent mid: \" ~ sentmid.to!string ~ \" rid: \" ~ rid.to!string);\n        }\n        catch(Exception e) {\n            dbm(\"catched at sendMessageImpl: \" ~ e.msg);\n            dbm(\"failed state will be set, rid: \" ~ rid.to!string);\n            setFailedState(rid, peer);\n        }\n    }\n\n    void setFailedState(long rid, int peer) {\n        auto cf = peer in chatFactory;\n        if(cf) {\n            auto z = cf.getZombie(rid, true);\n            if(z !is null) {\n                z.time_str = getLocal(\"sendfailed\");\n                toggleUpdate();\n                dbm(\"failed state set for \" ~ rid.to!string);\n            }\n        }\n    }\n\n    void asyncSendMessage(int peer, string msg) {\n        auto rid = genId();\n        auto aid = me.id;\n        vkMessage zombie = {\n            author_name: me.first_name ~ \" \" ~ me.last_name,\n            author_id: aid, isZombie: true,\n            body_lines: msg.split(\"\\n\"),\n            time_str: getLocal(\"sending\"),\n            rndid: rid, msg_id: -1, outgoing: true, unread: true,\n            peer_id: peer, utime: 1,\n            needName: true, nmresolved: true\n        };\n\n        synchronized(pbMutex) {\n            auto ch = peer in chatFactory;\n            if(ch) {\n                ch.addZombie(zombie);\n            }\n        }\n\n        toggleUpdate();\n\n        synchronized(sndMutex) {\n            ps.sent[rid] = sentMsg(rid, peer, aid);\n        }\n\n        a.orderedAsync(a.O_SENDMSG, rid, () => sendMessageImpl(rid, peer, msg));\n    }\n\n    void asyncMarkMessagesAsRead(int pid, int smid = 0) {\n        a.singleAsync(a.S_READ, () => api.messagesMarkAsRead(pid, smid));\n    }\n\n    void setTypingStatus(int peer) {\n        auto thrid = a.S_TYPING ~ peer.to!string;\n        long ctypetime = Clock.currTime().toUnixTime();\n        if((peer in lasttypetimes) is null) lasttypetimes[peer] = 0;\n\n        if (ctypetime - lasttypetimes[peer] >= cast(long)typingTimeout) {\n          lasttypetimes[peer] = ctypetime;\n          a.singleAsync(thrid, () {\n            dbm(\"typing status set at \" ~ to!string(ctypetime));\n            api.setActivityStatusImpl(peer, \"typing\");\n            //Thread.sleep(dur!\"seconds\"(typingTimeout)); //Commented out due to random freezes\n          });\n        }\n    }\n\n    R[] bufferedGet(R, T)(T factory, int count, int offset) {\n        synchronized(pbMutex) {\n            if(!factory.prepare) return [];\n            factory.seek(offset);\n            return factory\n                    .filter!(q => q !is null)\n                    .take(count)\n                    .map!(q => *(q.getObject))\n                    .array;\n        }\n    }\n\n}\n\n// ===== Model =====\n\nabstract class ClObject(T) {\n\n    private {\n        private T obj;\n    }\n\n    bool ignored = false;\n\n    this(T o) {\n        obj = o;\n    }\n\n    T* getObject() {\n        return &obj;\n    }\n\n}\n\nclass Block(T) {\n\n    alias objectType = T;\n    alias paramsType = BlockObjectParameters!T;\n\n    private {\n        uint\n            ordernum,\n            blocksz;\n        int oid;\n        bool filled;\n        factoryData* fdata;\n        paramsType params;\n        AsyncMan a;\n    }\n\n    T[] block;\n\n    this(uint ord, paramsType objparams, factoryData* fdt, int woid, bool filledblk = false) {\n        params = objparams;\n        a = params.asyncMan;\n        blocksz = params.blockSize;\n        ordernum = ord;\n        fdata = fdt;\n        filled = filledblk;\n        oid = woid;\n    }\n\n    T[] getBlock() {\n        downloadBlock();\n        return block;\n    }\n\n    void downloadBlock(bool force = false) {\n        if(!filled || force) a.orderedAsync(a.O_LOADBLOCK, oid, () {\n            ldFuncResult res;\n            block = params.downloadBlock(blocksz, blocksz*ordernum, res);\n            if(res.success) {\n                filled = true;\n                fdata.serverCount = res.servercount;\n                params.downloadNotify();\n            }\n        });\n    }\n\n    bool isFilled() {\n        return filled;\n    }\n\n    uint getBlocksize() {\n        return blocksz;\n    }\n\n    uint length() {\n        return block.length.to!uint;\n    }\n\n    void addBack(T obj) {\n        block = obj ~ block;\n    }\n\n    void addFront(T obj) {\n        block ~= obj;\n    }\n}\n\nclass BlockObjectParameters(O) {\n\n    alias downloader = O[] delegate(VkApi, uint, uint, out int);\n    alias loadnotify = void delegate();\n\n    downloader loadFunc;\n    loadnotify loadNotifyFunc;\n    AsyncMan asyncMan;\n    uint blockSize;\n\n    private {\n        VkApi api;\n    }\n\n    this(VkApi vkapi, uint blocksize, AsyncMan asyncm, downloader ld, loadnotify ldn) {\n        assert(blocksize != 0);\n        loadFunc = ld;\n        loadNotifyFunc = ldn;\n        asyncMan = asyncm;\n        blockSize = blocksize;\n        api = vkapi;\n    }\n\n    O[] downloadBlock(uint c, uint o, out ldFuncResult r) {\n        r = apiCheck(api);\n        if(!r.success) return new O[0];\n        return loadFunc(api, c, o, r.servercount);\n    }\n\n    void downloadNotify() {\n        loadNotifyFunc();\n    }\n\n}\n\nclass BlockFactory(T) {\n\n    alias paramsType = BlockObjectParameters!T;\n\n    private {\n        uint\n            blocksz;\n        int\n            oid,\n            iter,\n            backiter = -1;\n        bool\n            blank = true;\n        Block!T[uint] blockst;\n        Block!T backBlock;\n    }\n\n    factoryData data;\n    paramsType params;\n\n    this(paramsType objectParams) {\n        params = objectParams;\n        blocksz = params.blockSize;\n        iter = 0;\n        data = factoryData();\n        oid = genId();\n        initBackBlock();\n    }\n\n    private void initBackBlock() {\n        backBlock = new Block!T(-1, params, &data, oid, true);\n    }\n\n    uint objectCount() {\n        return blockst.values.map!(q => q.length).sum();\n    }\n\n    bool isBlank() {\n        return blank;\n    }\n\n    auto getLoadedObjects() {\n        auto ldblocks = blockst.keys\n            .map!(q => q in blockst)\n            .filter!(q => q.isFilled)\n            .map!(q => q.getBlock)\n            .joiner;\n\n        auto ldback = backBlock.getBlock;\n\n        return chain(ldback, ldblocks);\n    }\n\n    private Block!T getblk(int i) {\n        auto c = i in blockst;\n        if(c) return *c;\n        else {\n            blockst[i] = new Block!T(i, params, &data, oid);\n            return blockst[i];\n        }\n    }\n\n    T getBlockObject(int off) {\n        auto bk = backBlock.getBlock();\n        if(off < bk.length) {\n            auto bkobj = bk[off];\n            if(bkobj.ignored) return null;\n            return bkobj;\n        }\n        off -= bk.length;\n\n        auto rel = off % blocksz;\n        auto n = (off - rel) / blocksz;\n        auto nblk = getblk(n);\n\n        if(!nblk.isFilled) {\n            nblk.downloadBlock();\n            return null;\n        }\n        if(rel >= nblk.length) return null;\n\n        getblk(n+1).downloadBlock(); //preload\n        auto relobj = nblk.getBlock[rel];\n\n        if(relobj.ignored) return null;\n\n        static if(is(T == ClDialog)) {\n            if(isOverrided(relobj.getObject.id)) return null;\n        }\n\n        blank = false;\n        return relobj;\n    }\n\n    void seek(int off) {\n        iter = off;\n        backiter = data.serverCount;\n    }\n\n    void addBack(T addobj) {\n        backBlock.addBack(addobj);\n        data.serverCount += 1;\n    }\n\n    bool prepare() {\n        if(data.serverCount != -1){\n            if(backiter == -1) backiter = data.serverCount;\n            return true;\n        }\n        getblk(0).downloadBlock();\n        return false;\n    }\n\n    bool empty() {\n        return iter >= (backiter + backBlock.length);\n    }\n\n    T front() {\n        if(data.forceUpdate) {\n            data.forceUpdate = false;\n            data.serverCount = -1;\n            static if(is(T == ClDialog)) clearOverrides();\n            initBackBlock();\n            //blockst.clear(); - bad for ldc\n            blockst = blockst.init;\n            prepare();\n            return null;\n        }\n        return getBlockObject(iter);\n    }\n\n    void popFront() {\n        ++iter;\n    }\n\n    typeof(this) save() {\n        return this;\n    }\n\n    // ===== special magic =====\n\n    //pragma(msg, \"T: \" ~ T.stringof ~ \", equals ClMessage: \" ~ is(T == ClMessage).stringof);\n\n    static if (is(T == ClMessage)) {\n        private int serverLineCount = -1;\n        int unreadCount = -1;\n\n        int getServerLineCount(int ww) {\n            if(serverLineCount == -1 && objectCount == data.serverCount) {\n                serverLineCount = blockst.values\n                                    .map!(q => q.getBlock())\n                                    .joiner\n                                    .map!(q => q.getLineCount(ww))\n                                    .sum.to!int + 1;\n            }\n            return serverLineCount;\n        }\n\n        void addZombie(vkMessage z) {\n            //auto rid = z.rndid;\n            auto clz = new ClMessage(z);\n            //zombies[rid] = clz;\n            addBack(clz);\n        }\n\n        void removeZombie(long rid) {\n            backBlock\n                .getBlock\n                .filter!(q => q.getObject.rndid == rid)\n                .takeOne\n                .each!(q => q.ignored = true);\n        }\n\n        vkMessage* getZombie(long rid, bool defeatCache) {\n            auto one = backBlock\n                .getBlock\n                .filter!(q => q.getObject.rndid == rid)\n                .takeOne;\n            if(!one.empty) {\n                if(defeatCache) {\n                    one.front.invalidateLineCache();\n                }\n                return one.front.getObject;\n            }\n            else return null;\n        }\n\n    }\n\n\n    static if(is(T == ClDialog)) {\n        struct DialogOverrider {\n            long utime;\n            ClDialog dialog;\n        }\n\n        private DialogOverrider[int] store; //by peer\n\n        private void updateBackBlock() {\n            backBlock.block = getOverrided().array;\n        }\n\n        void clearOverrides() {\n            //store.clear(); - bad for ldc\n            store = store.init;\n        }\n\n        void overrideDialog(ClDialog dlg, long ut) {\n            store[dlg.getPeer] = DialogOverrider(ut, dlg);\n            updateBackBlock();\n        }\n\n        void overrideBump(int peer, long ut) {\n            auto b = peer in store;\n            if(b) {\n                b.utime = ut;\n            }\n            updateBackBlock();\n        }\n\n        bool isOverrided(int peer) {\n            auto ptr = peer in store;\n            return ptr !is null;\n        }\n\n        auto getOverridedByPeer(int peer) {\n            return peer in store;\n        }\n\n        auto getOverrided() {\n            return store.values\n                            .sort!((a, b) => a.utime > b.utime)\n                            .map!(q => q.dialog);\n        }\n\n        auto getOverridedUnsorted() {\n            return store.values\n                            .map!(q => q.dialog);\n        }\n    }\n\n}\n\nldFuncResult apiCheck(VkApi api) {\n    return ldFuncResult(api.isTokenValid);\n}\n\nvoid enterLoading() {\n    ++ps.loadingiter;\n    gltoggleUpdate();\n}\n\nvoid leaveLoading() {\n    --ps.loadingiter;\n    if(ps.loadingiter < 0) ps.loadingiter = 0;\n    gltoggleUpdate();\n}\n\nvoid gltoggleUpdate() {\n    ps.somethingUpdated = true;\n}\n\n// ===== Implement objects =====\n\nclass ClDialog : ClObject!vkDialog {\n\n    alias objt = vkDialog;\n    alias clt = typeof(this);\n    alias ChatFactory = BlockFactory!ClMessage;\n\n    private {\n        bool lp;\n        ChatFactory cf;\n        objt uobj;\n    }\n\n    static auto getLoadFunc() {\n        return delegate (VkApi api, uint c, uint o, out int sc)\n                        =>  api.messagesGetDialogs(c, o, sc).map!(q => new clt(q)).array;\n    }\n\n    this(objt obj) {\n        super(obj);\n    }\n\n    this(string title, int cid, int unreadc, ChatFactory fac) {\n        lp = true;\n        cf = fac;\n        vkDialog lcobj = {\n            name: title,\n            id: cid\n        };\n        if(cf.unreadCount < 0) {\n            cf.unreadCount = unreadc;\n        }\n        super(lcobj);\n    }\n\n    int getPeer() {\n        return obj.id;\n    }\n\n    override vkDialog* getObject() {\n        if(lp) {\n            vkDialog rt = obj;\n            auto lastone = cf.getLoadedObjects\n                            .filter!(q => q !is null)\n                            .takeOne;\n\n            if(lastone.empty) return &obj;\n            auto lastm = lastone.front.getObject;\n\n            rt.lastMessage = lastm.body_lines.empty ? \"\" : lastm.body_lines[0];\n            rt.outbox = lastm.outgoing;\n            rt.unreadCount = cf.unreadCount;\n            rt.unread = rt.outbox ? (lastm.unread) : (cf.unreadCount > 0);\n            rt.lastmid = lastm.msg_id;\n            rt.online = rt.id >= longpollGimStartId ? true : nc.getOnline(rt.id);\n            rt.isChat = rt.id >= convStartId;\n            uobj = rt;\n            return &uobj;\n        }\n        else {\n            obj.online = obj.id >= longpollGimStartId ? true : nc.getOnline(obj.id);\n            return &obj;\n        }\n    }\n\n}\n\nclass ClFriend : ClObject!vkFriend {\n\n    alias objt = vkFriend;\n    alias clt = typeof(this);\n\n    static auto getLoadFunc() {\n        return delegate (VkApi api, uint c, uint o, out int sc)\n                        =>  api.friendsGet(c, o, sc).map!(q => new clt(q)).array;\n    }\n\n    this(objt obj) {\n        super(obj);\n    }\n\n    override vkFriend* getObject() {\n        obj.online = nc.getOnline(obj.id);\n        auto ovr_ls = obj.id in lsc;\n        if(ovr_ls) {\n            dbm(\"ovr_ls\");\n            //obj.last_seen_utime = ovr_ls.last_seen_utime;\n            //obj.last_seen_str = ovr_ls.last_seen_str;\n            //todo resolve this shit << Override lastseen on offline trigger\n        }\n        return &obj;\n    }\n\n}\n\nclass ClAudio : ClObject!vkAudio {\n\n    alias objt = vkAudio;\n    alias clt = typeof(this);\n\n    static auto getLoadFunc() {\n        return delegate (VkApi api, uint c, uint o, out int sc)\n                        =>  api.audioGet(c, o, sc).map!(q => new clt(q)).array;\n    }\n\n    this(objt obj) {\n        super(obj);\n    }\n\n}\n\nclass ClMessage : ClObject!vkMessage {\n\n    alias objt = vkMessage;\n    alias clt = typeof(this);\n\n    static private void resolveNeedNameLocal(ref vkMessage[] mw) {\n        int lastfid;\n        long lastut;\n        foreach(ref m; mw.retro) {\n            immutable bool nm = !(m.author_id == lastfid && (m.utime-lastut) <= needNameMaxDelta);\n            m.needName = nm;\n            lastfid = m.author_id;\n            lastut = m.utime;\n        }\n    }\n\n    static auto getLoadFunc(int peer, void delegate(int) setUnreads) {\n        return (VkApi api, uint c, uint o, out int sc) {\n            int uc;\n            auto h = api.messagesGetHistory(peer, c, o, sc, uc);\n            setUnreads(uc);\n            resolveNeedNameLocal(h);\n            return h.map!(q => new clt(q)).array;\n        };\n    }\n\n\n    this(objt obj) {\n        super(obj);\n    }\n\n    private {\n        vkMessageLine[] lines;\n        int lastww;\n    }\n\n    ulong getLineCount(int ww) {\n        fillLines(ww);\n        return lines.length;\n    }\n\n    vkMessageLine[] getLines(int ww) {\n        fillLines(ww);\n        return lines;\n    }\n\n    void invalidateLineCache() {\n        lines = [];\n    }\n\n    private void fillLines(int ww) {\n        if(lines.length == 0 || lastww != ww) {\n            lastww = ww;\n            lines = convertMessage(obj, ww);\n        }\n    }\n\n    vkMessageLine lspacing = {\n        text: \"\", isSpacing: true\n    };\n\n    const int wwmultiplier = 3;\n\n    private vkMessageLine[] convertMessage(ref vkMessage inp, int ww) {\n        immutable bool zombie = inp.isZombie || inp.msg_id < 1;\n\n        vkMessageLine[] rt;\n        rt ~= lspacing;\n        bool nofwd = (inp.fwd_depth == -1);\n\n        if(inp.needName) {\n            vkMessageLine name = {\n                text: inp.author_name,\n                time: inp.time_str,\n                isName: true\n            };\n            rt ~= name;\n            rt ~= lspacing;\n        }\n\n        if(inp.body_lines.length != 0) {\n            bool unrfl = inp.unread;\n            wstring[] wrapped;\n            inp.body_lines.map!(q => q.toUTF16wrepl.wordwrap(ww)).each!(q => wrapped ~= q);\n            foreach(l; wrapped) {\n                vkMessageLine msg = {\n                    text: l.toUTF8wrepl,\n                    unread: unrfl\n                };\n                rt ~= msg;\n                if(unrfl) unrfl = false;\n            }\n        } else if (nofwd) rt ~= lspacing;\n\n        if(!nofwd) {\n            rt ~= lspacing ~ renderFwd(inp.fwd, 0, ww);\n        }\n\n        return rt;\n    }\n\n    private vkMessageLine[] renderFwd(vkFwdMessage[] inp, int depth, int ww) {\n        ++depth;\n        auto lcww = ww - (depth * wwmultiplier);\n        if(lcww <= 0) lcww = 1;\n\n        vkMessageLine[] rt;\n        foreach(fm; inp) {\n            vkMessageLine name = {\n                text: fm.author_name,\n                time: fm.time_str,\n                isFwd: true, isName: true, fwdDepth: depth\n            };\n            rt ~= name;\n            wstring[] wrapped;\n            fm.body_lines.map!(q => q.toUTF16wrepl.wordwrap(lcww)).each!(q => wrapped ~= q);\n            foreach(l; wrapped) {\n                vkMessageLine msg = {\n                    text: l.toUTF8wrepl,\n                    isFwd: true, fwdDepth: depth\n                };\n                rt ~= msg;\n            }\n\n            vkMessageLine fwdspc;\n            fwdspc.isFwd = true; fwdspc.isSpacing = true;\n            fwdspc.fwdDepth = depth;\n            rt ~= fwdspc;\n\n            if(fm.fwd.length != 0) {\n                rt ~= renderFwd(fm.fwd, depth, ww);\n                rt ~= fwdspc;\n            }\n\n        }\n        return rt;\n    }\n\n}\n\n// ===== Exceptions =====\nabstract class VkException : Exception\n{\n    private this(string m, string f, size_t l, Throwable n) @safe pure nothrow\n    {\n        super(m, f, l, n);\n    }\n}\n\nclass BackendException : VkException {\n    public {\n        @safe pure nothrow this(string message,\n                                string file =__FILE__,\n                                size_t line = __LINE__,\n                                Throwable next = null) {\n            super(message, file, line, next);\n        }\n    }\n}\n\nclass NetworkException : VkException {\n    public {\n        @safe pure nothrow this(string loc,\n                                string message = \"Connection lost\",\n                                string file =__FILE__,\n                                size_t line = __LINE__,\n                                Throwable next = null) {\n            message = message ~ \": \" ~ loc;\n            super(message, file, line, next);\n        }\n    }\n}\n\n\nclass ApiErrorException : VkException {\n    public {\n        int errorCode;\n        @safe pure nothrow this(string message,\n                                int error_code,\n                                string file =__FILE__,\n                                size_t line = __LINE__,\n                                Throwable next = null) {\n            errorCode = error_code;\n            super(message, file, line, next);\n        }\n    }\n}\n\nclass InternalException : VkException {\n    public {\n\n        static const int E_NETWORKFAIL = 4;\n        static const int E_LPRESTART = 5;\n\n        string msg;\n        int ecode;\n        @safe pure nothrow this(int error,\n                                string appmsg = \"\",\n                                string file =__FILE__,\n                                size_t line = __LINE__,\n                                Throwable next = null) {\n            msg = \"client failed - unresolved internal exception: err\" ~ error.to!string ~ \" \" ~ appmsg;\n            ecode = error;\n            super(msg, file, line, next);\n        }\n    }\n}\n\nvoid debugThrow() {\n    throw new InternalException(228, \"DEBUGPOINT\");\n}\n"
  },
  {
    "path": "source/vkshit.d",
    "content": "module magicstringz;\n\nconst string appSecret = \"hHbZxrka2uZ6jB1inYsH\"; // android\nconst string appCID = \"2274003\";\nconst string appUserAgent = \"VKAndroidApp/4.9-1118\";\n"
  },
  {
    "path": "unused_depend",
    "content": "\"dcrypto\": \"~>1.2.2\"\n"
  }
]