Full Code of HaCk3Dq/vk for AI

master 4756b5912c8e cached
19 files
169.2 KB
44.5k tokens
1 requests
Download .txt
Repository: HaCk3Dq/vk
Branch: master
Commit: 4756b5912c8e
Files: 19
Total size: 169.2 KB

Directory structure:
gitextract__zc_jl6o/

├── .gitignore
├── LICENSE
├── README.md
├── buildRelease.sh
├── buildWithLDC.sh
├── buildWrapper.sh
├── dub.json
├── generateVersion.d
├── source/
│   ├── .editorconfig
│   ├── app.d
│   ├── cfg.d
│   ├── localization.d
│   ├── musicplayer.d
│   ├── namecache.d
│   ├── storedFunctions
│   ├── utils.d
│   ├── vkapi.d
│   └── vkshit.d
└── unused_depend

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
.dub
docs.json
__dummy.html
*.o
*.obj
vk-client
vk
out
.idea
*.iml
dbg
dub.selections.json
dub.userprefs
source/vkversion.d
vk.sublime-project
vk.sublime-workspace
debuildtmp
debs
*.7z



================================================
FILE: LICENSE
================================================

                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS


================================================
FILE: README.md
================================================
# vk
A console (ncurses) client for vk.com written in D

# Project is abandoned

vk-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.

If 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)

But if you just want CLI client for vk - we can't help with it anymore :)

# Screenshots

![alt tag](http://cs630123.vk.me/v630123942/25fc7/YOqfnerj4bE.jpg)
![alt tag](http://cs630123.vk.me/v630123942/25fd7/hcgITGtqEd0.jpg)

# Install

## ArchLinux

```sh
yaourt -S vk-cli # or vk-cli-git
vk
```

## Ubuntu 

```
sudo apt-add-repository ppa:mc3man/mpv-tests
sudo apt-get update
sudo apt-get install libncursesw5-dev libssl-dev curl mpv
```

then `Build` 

OR

install deb package from releases page `sudo dpkg -i vk-cli.deb`

## Gentoo

```sh
layman -fa glicOne
sudo emerge net-im/vk # for vk-9999 you need install dub, dmd and dlang-tools from dlang overlay
vk-cli
```
## MacOS

```
brew install dub dmd curl openssl mpv
brew install homebrew/dupes/ncurses
brew doctor
brew link ncurses -force
```

then `Build`

## Build

```
git clone https://github.com/vk-cli/vk
cd vk
git checkout VER
dub build
```
(where `VER` is the version number)

builds `vk` binary for your platform.

You can find number of latest version here: https://github.com/vk-cli/vk/releases

## Dependencies

+ ncurses >= 5.7
+ curl
+ openssl

Make dependencies:

+ dub 
+ dmd >= 2.071

Optional:

+ mpv >= 0.22.0: for music playback

### Our GPG keys

To verify signed files, first you need to import keys:

` $ gpg  --keyserver pgp.mit.edu --recv-keys 0x3457ECED `

Now you can verify files and install signed packages:

` $ gpg --verify signed-file.sig signed-file `

`gpg: Good signature from "vk-cli developers team <vk-cli.dev@ya.ru>"`

This output indicates that file is properly signed and isn't damaged



================================================
FILE: buildRelease.sh
================================================
#!/bin/bash

dubarch="x86_64"
dubtype="release"
dubconf=""

if [[ "$2" == '32' ]]; then
  dubarch="x86"
fi

if [[ "$1" == 'shared' ]]; then
  dubconf="release-shared"
elif [[ "$1" == 'static' ]]; then
  dubconf="release-static"
elif [[ "$1" == 'debug' ]]; then
  dubconf="debug"
  dubtype="debug"
fi

dubtype="debug" # for remove optimizations

echo "Building with:"
echo "config: $dubconf"
echo "build: $dubtype"
echo "arch: $dubarch"
echo ""
dub build --config=$dubconf --build=$dubtype --arch=$dubarch --force


================================================
FILE: buildWithLDC.sh
================================================
#!/bin/bash

export DFLAGS="-disable-linker-strip-dead $@"
dub build --force --compiler=ldc



================================================
FILE: buildWrapper.sh
================================================
#!/bin/bash

# buidWrapper [ver] [64/32]

TARCH="$2"
NAME="vk-$1-$TARCH"
NAMEBIN="$NAME-bin"
NAMEPACK="$NAMEBIN.7z"

rm vk
./buildRelease.sh static "$TARCH"
if [[ "$?" -eq "0" ]]; then
  mv vk "$NAMEBIN"
  7z a "$NAMEPACK" "$NAMEBIN"
  rm "$NAMEBIN"
fi


================================================
FILE: dub.json
================================================
{
  "name": "vk",
  "description": "Terminal client for vk.com",
  "authors": ["HaCk3D", "substanceof"],
  "homepage": "https://github.com/HaCk3Dq/vk",
  "license": "Apache-2.0",
  "dependencies": {
    "ncurses": "*"
  },
  "targetType": "executable",
  "mainSourceFile": "source/app.d",
  "platfroms": ["posix"],
  "preGenerateCommands": ["rdmd generateVersion.d"],
  "configurations": [
    {
      "name": "debug",
      "buildRequirements": ["allowWarnings"],
    },
    {
      "name": "release-shared",
      "buildRequirements": ["silenceDeprecations"],
    },
    {
      "name": "release-static",
      "buildRequirements": ["silenceDeprecations"],
      "lflags": ["-Bstatic"]
    }
  ]
}


================================================
FILE: generateVersion.d
================================================
/*
Copyright 2016 HaCk3D, substanceof

https://github.com/HaCk3Dq
https://github.com/substanceof

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import std.process, std.stdio, std.string, std.algorithm,
       std.file, std.regex, std.conv;

void main() {
  const versionNum = "0.7.6";
  const releaseFlag = false;
  string
    lastCommitHash,
    currentBranch;

  if(!releaseFlag) {
    lastCommitHash = matchFirst(executeShell("git log -1").output, regex(r"(?:^commit\s+)([0-9a-f]{40})"))[1][0..7];
    currentBranch = matchFirst(executeShell("git status").output, regex(r"(?:^On branch\s+)(.+)"))[1];
  }

  auto verTemplate = "
module vkversion;

const string
  currentVersion = \"master\";

";

  auto fileName = "source/vkversion.d";
  string[] text;

  if(!exists(fileName)) text = verTemplate.split("\n");
  else text = readText(fileName).split("\n");

  auto reg = regex("(^\\s*currentVersion\\s*=\\s*\")(.+)(\"\\s*;)");
  foreach (ref line; text) {
    auto match = matchFirst(line, reg);
    if (match.length == 4) {
      auto versionString = releaseFlag ? versionNum : versionNum ~ "-" ~ currentBranch ~ "-" ~ lastCommitHash;
      writeln("version string: " ~ versionString);
      line = match[1] ~ versionString ~ match[3];
      break;
    }
  }
  auto f = File(fileName, "w");
  f.write(text.join("\n"));
  f.close;
}


================================================
FILE: source/.editorconfig
================================================
root = true

[*]
ident_style = space
dfmt_brace_style = stroustrup
dfmt_space_after_cast = false
dfmt_space_after_keywords = true

[vkapi.d]
ident_size = 4

[app.d]
ident_size = 2

[musicplayer.d]
ident_size = 2


================================================
FILE: source/app.d
================================================
/*
Copyright 2016 HaCk3D, substanceof

https://github.com/HaCk3Dq
https://github.com/substanceof

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import deimos.ncurses.ncurses;
import core.stdc.locale, core.thread, core.stdc.stdlib:exit;
import core.sys.posix.signal;
import std.string, std.stdio, std.process,
       std.conv, std.array, std.encoding,
       std.range, std.algorithm, std.concurrency,
       std.datetime, std.utf, std.regex, std.random,
       std.math, std.json;
import vkapi, cfg, localization, utils, namecache, musicplayer, vkversion;
import magicstringz;

// INIT VARS
enum Sections { left, right }
enum Buffers { none, friends, dialogs, music, chat, help, settings }
enum Colors { white, red, green, yellow, blue, pink, mint, gray }
enum DrawSetting { allMessages, onlySelectedMessage, onlySelectedMessageAndUnread }

__gshared {
  string[string] storage;
  Win win;
  VkMan api;
}

public:

struct ListElement {
  string name, text;
  void function(ref ListElement) callback;
  ListElement[] function() getter;
  bool flag;
  int id;
  bool isConference;
}

void Exit(string msg = "", int ecode = 0, bool normalExit = false) {
  endwin;

  if (normalExit) {
    mplayer.exitPlayer();
  }
  else if (mplayer !is null && mplayer.player !is null) {
    mplayer.player.killPlayer();
    writeln("player killed");
  }

  if (msg != "") {
    //writeln("FAIL");
    writeln(msg);
  }
  exit(ecode);
}

vkAudio[] getShuffledMusic(int count, int offset) {
  if (win.workaroundCounter == floor(api.getServerCount(blockType.music)/100.0)+2) win.shuffleLoadingIsOver = true;
  if (!win.shuffleLoadingIsOver) {
    win.workaroundCounter++;
    win.shuffledMusic = api.getBufferedMusic(api.getServerCount(blockType.music), 0);
    auto step = (win.shuffledMusic.length.to!real / api.getServerCount(blockType.music).to!real) * 20;
    ("[" ~ "=".replicatestr(floor(step).to!int) ~ "|" ~ "=".replicatestr(20 - floor(step).to!int) ~ "]").SetStatusbar;
  } else {
    if (!win.shuffled) {
      SetStatusbar;
      randomShuffle(win.shuffledMusic);
      win.shuffled = true;
      win.savedShuffledLen = win.shuffledMusic.length.to!int;
    } else
      return win.shuffledMusic[offset..offset+count];
  }
  return api.getBufferedMusic(count, offset);
}

private:

const int
  // func keys
  k_up           = -2,
  k_down         = -3,
  k_right        = -4,
  k_left         = -5,
  k_home         = -6,
  k_ins          = -7,
  k_del          = -8,
  k_end          = -9,
  k_pageup       = -10,
  k_pagedown     = -11,
  k_enter        = 10,
  k_esc          = 27,
  k_tab          = 8,
  k_ctrl_bckspc  = 9,
  k_prev         = 91,
  k_rus_prev     = 133,
  k_next         = 93,
  k_rus_next     = 138,
  k_o            = 111,
  k_rus_o        = 137,
  k_m            = 109,
  k_rus_m        = 140,
  kg_rew_bck     = 60,
  kg_rew_fwd     = 62,
  kg_rew_bck_rus = 145,
  kg_rew_fwd_rus = 174,

  // keys
  k_q        = 113,
  k_rus_q    = 185,
  k_p        = 112,
  k_rus_p    = 183,
  k_r        = 114,
  k_rus_r    = 186,
  k_bckspc   = 127,
  k_w        = 119,
  k_s        = 115,
  k_shift_s  = 83,
  k_shift_rus_s = 171,
  k_a        = 97,
  k_d        = 100,
  k_rus_w    = 134,
  k_rus_a    = 132,
  k_rus_s    = 139,
  k_rus_d    = 178,
  k_shift_d  = 68,
  k_shift_rus_d  = 146,
  k_k        = 107,
  k_j        = 106,
  k_h        = 104,
  k_l        = 108,
  k_rus_h    = 128,
  k_rus_j    = 190,
  k_rus_k    = 187,
  k_rus_l    = 180,
  k_shift_l  = 76,
  k_shift_rus_l  = 148;

const int[]
  // key groups
  kg_esc     = [k_q, k_rus_q],
  kg_refresh = [k_r, k_rus_r],
  kg_up      = [k_up, k_w, k_k, k_rus_w, k_rus_k],
  kg_down    = [k_down, k_s, k_j, k_rus_s, k_rus_j],
  kg_left    = [k_left, k_a, k_h, k_rus_a, k_rus_h],
  kg_right   = [k_right, k_d, k_l, k_rus_d, k_rus_l, k_enter],
  kg_shift_right = [k_shift_d, k_shift_rus_d, k_shift_l, k_shift_rus_l],
  kg_shift_s = [k_shift_s, k_shift_rus_s],
  kg_ignore  = [k_right, k_left, k_up, k_down, k_bckspc, k_esc,
                k_pageup, k_pagedown, k_end, k_ins, k_del,
                k_home, k_tab, k_ctrl_bckspc],
  kg_pause   = [k_p, k_rus_p],
  kg_loop    = [k_o, k_rus_o],
  kg_mix     = [k_m, k_rus_m],
  kg_prev    = [k_prev, k_rus_prev],
  kg_next    = [k_next, k_rus_next],
  kg_rewind_backward = [kg_rew_bck, kg_rew_bck_rus],
  kg_rewind_forward  = [kg_rew_fwd, kg_rew_fwd_rus];

string getChar(string charName) {
  if (win.unicodeChars) {
    switch (charName) {
      case "unread" : return "⚫ ";
      case "fwd"    : return "➥ ";
      case "play"   : return " ▶  ";
      case "pause"  : return " ▮▮ ";
      case "outbox" : return " ⇡ ";
      case "inbox"  : return " ⇣ ";
      case "cross"  : return " ✖ ";
      case "mail"   : return " ✉ ";
      case "refresh": return " ⟲";
      case "repeat" : return "⟲ ";
      case "shuffle": return "⤮";
      default       : return charName;
    }
  } else {
    switch(charName) {
      case "unread" : return "! ";
      case "fwd"    : return "fwd ";
      case "play"   : return " >  ";
      case "pause"  : return " || ";
      case "outbox" : return " ^ ";
      case "inbox"  : return " v ";
      case "cross"  : return " X ";
      case "mail"   : return " M ";
      case "refresh": return " ?";
      case "repeat" : return "o ";
      case "shuffle": return "x";
      default       : return charName;
    }
  }
}

struct Notify {
  string text;
  TimeOfDay
    currentTime,
    clearTime;
}

struct Cursor {
  int x, y;
}

struct Track {
  string artist, title, duration;
}

struct Win {
  ListElement[]
  menu = [
    {callback:&open, getter: &GetFriends},
    {callback:&open, getter: &GetDialogs},
    {callback:&open, getter: &GetMusic},
    {callback:&open, getter: &GenerateHelp},
    {callback:&open, getter: &GenerateSettings},
    {callback:&exit}
  ],
  buffer, mbody, playerUI;
  vkAudio[] shuffledMusic;
  Notify notify;
  Cursor cursor;
  int
    namecolor = Colors.white,
    textcolor = Colors.gray,
    counter, active, section,
    menuActive, menuOffset = 15, key,
    scrollOffset, msgDrawSetting,
    activeBuffer, chatID, lastBuffer,
    lastScrollOffset, lastScrollActive,
    msgBufferSize, seekValue = 15,
    savedShuffledLen, workaroundCounter;
  string
    statusbarText, msgBuffer;
  bool
    isMusicPlaying, isConferenceOpened,
    isRainbowChat, isRainbowOnlyInGroupChats,
    isMessageWriting, showTyping, selectFlag,
    showConvNotifications, sendOnline,
    unicodeChars = true, shuffled, seekPercentFlag,
    shuffleLoadingIsOver, isInternalError;
}

void relocale() {
  win.menu[0].name = "m_friends".getLocal;
  win.menu[1].name = "m_conversations".getLocal;
  win.menu[2].name = "m_music".getLocal;
  win.menu[3].name = "m_help".getLocal;
  win.menu[4].name = "m_settings".getLocal;
  win.menu[5].name = "m_exit".getLocal;
}

void parse(ref string[string] storage) {
  if ("main_color" in storage) win.namecolor = storage["main_color"].to!int;
  if ("second_color" in storage) win.textcolor = storage["second_color"].to!int;
  if ("message_setting" in storage) win.msgDrawSetting = storage["message_setting"].to!int;
  if ("lang" in storage) if (storage["lang"] != getLang) swapLang;
  if ("rainbow" in storage) win.isRainbowChat = storage["rainbow"].to!bool;
  if ("rainbow_in_chat" in storage) win.isRainbowOnlyInGroupChats = storage["rainbow_in_chat"].to!bool;
  if ("show_typing" in storage) win.showTyping = storage["show_typing"].to!bool;
  if ("show_conv_notif" in storage) win.showConvNotifications = storage["show_conv_notif"].to!bool;
  if ("send_online" in storage) win.sendOnline = storage["send_online"].to!bool;
  if ("unicode_chars" in storage) win.unicodeChars = storage["unicode_chars"].to!bool;
  if ("seek_percent_or_value" in storage) win.seekPercentFlag = storage["seek_percent_or_value"].to!bool;

  relocale;

  if("longpoll_wait" !in storage) {
    storage["longpoll_wait"] = "25";
    storage.save;
  }
}

void update(ref string[string] storage) {
  storage["lang"] = getLang;
  storage["main_color"] = win.namecolor.to!string;
  storage["second_color"] = win.textcolor.to!string;
  storage["message_setting"] = win.msgDrawSetting.to!string;
  storage["rainbow"] = win.isRainbowChat.to!string;
  storage["rainbow_in_chat"] = win.isRainbowOnlyInGroupChats.to!string;
  storage["show_typing"] = win.showTyping.to!string;
  storage["show_conv_notif"] = win.showConvNotifications.to!string;
  storage["send_online"] = win.sendOnline.to!string;
  storage["unicode_chars"] = win.unicodeChars.to!string;
  storage["seek_percent_or_value"] = win.seekPercentFlag.to!string;
  storage.save;
}

void print(string s) {
  s.toStringz.addstr;
}

void print(int i) {
  i.to!string.toStringz.addstr;
}

string makeLink(string login, string passwd) {
  return "https://oauth.vk.com/token?grant_type=password" ~
        "&client_id=" ~ appCID ~
        "&client_secret=" ~ appSecret ~
        "&username=" ~ login ~
        "&password=" ~ passwd ~
        "&2fa_supported=1";
}

string getPassword() {
  version (linux) {
    import core.sys.linux.unistd;
    return getpass("Password (will not be echoed): ").to!string;
  }
  else {
    writeln("[ WARNING: password will be echoed in console ]");
    write("Password: ");
    return readln().chomp;
  }
}

VkMan get_token(ref string[string] storage) {

  auto tm = dur!"seconds"(10);

  write("Username (email or phone): ");
  string strusr = readln().chomp;

  string strpwd = getPassword();

  writeln("\nLogging in..");

  string url = makeLink(strusr, strpwd);
  auto got = AsyncMan.httpget(url, tm, 10);

  JSONValue resp = got.parseJSON;
  if("validation_type" in resp && (resp["validation_type"].str=="2fa_sms" || resp["validation_type"].str=="2fa_app")){
    if(resp["validation_type"].str=="2fa_sms")
      write("SMS Code ("~ resp["phone_mask"].str ~"): ");
    else if(resp["validation_type"].str=="2fa_app")
      write("App Code: ");
    string strcode = readln().chomp;

    url = makeLink(strusr, strpwd)~"&code="~strcode;
    got = AsyncMan.httpget(url, tm, 10);
    resp = got.parseJSON;
  }
  if("error" in resp) {
    writeln("\nError while auth: " ~ got);
    Exit();
  }

  string token = resp["access_token"].str;
  storage["token"] = token;
  storage["auth_v2"] = "true";

  return new VkMan(token);
}

void color() {
  if (!has_colors) {
    endwin;
    writeln("Your terminal does not support color");
  }
  start_color;
  use_default_colors;
  for (short i = 0; i < Colors.max; i++) init_pair(i, i, -1);
  for (short i = 1; i < Colors.max+1; i++) init_pair((Colors.max+1+i).to!short, i, -1.to!short);
  init_pair(Colors.max, 0, -1);
  init_pair(Colors.max+1, -1, -1);
  init_pair(Colors.max*2+1, 0, -1);
}

void selected(string text) {
  attron(A_REVERSE);
  text.regular;
  attroff(A_REVERSE);
}

void regular(string text) {
  attron(A_BOLD);
  attron(COLOR_PAIR(win.namecolor));
  text.print;
  attroff(A_BOLD);
  attroff(COLOR_PAIR(win.namecolor));
}

void colored(string text, int color) {
  int temp = win.namecolor;
  win.namecolor = color;
  text.regular;
  win.namecolor = temp;
}

void secondColor(string text) {
  attron(A_BOLD);
  attron(COLOR_PAIR(win.textcolor+Colors.max+1));
  text.print;
  attroff(A_BOLD);
  attroff(COLOR_PAIR(win.textcolor+Colors.max+1));
}

void graySelected(string text) {
  attron(A_REVERSE);
  attron(A_BOLD);
  attron(COLOR_PAIR(win.namecolor+Colors.max+1));
  text.print;
  attroff(A_BOLD);
  attroff(COLOR_PAIR(win.namecolor+Colors.max+1));
  attroff(A_REVERSE);
}

void regularWhite(string text) {
  attron(COLOR_PAIR(0));
  text.print;
  attroff(COLOR_PAIR(0));
}

void white(string text) {
  attron(A_BOLD);
  regularWhite(text);
  attroff(A_BOLD);
}

void notifyManager() {
  string notifyMsg = api.getLastLongpollMessage.replace("\n", " ");
  win.notify.currentTime = cast(TimeOfDay)Clock.currTime;

  if (notifyMsg != "" && notifyMsg != "-1") {
    if (notifyMsg.utfLength > COLS - 10) win.notify.text = notifyMsg.to!wstring[0..COLS-10].to!string;
    else win.notify.text = notifyMsg;
    win.notify.clearTime = win.notify.currentTime + seconds(1);
  }
  if (win.notify.currentTime > win.notify.clearTime) {
    win.notify.clearTime = TimeOfDay(23, 59, 59);
    win.notify.text = "";
  }
}

void statusbar() {
  string counterStr;
  notifyManager;
  win.counter = api.messagesCounter;
  if (win.counter == -1) {
    win.isInternalError = true;
    counterStr = getChar("cross");
    if (api.api.isTokenValid()) {
      "no_connection".getLocal.SetStatusbar;
    }
    else {
      "e_wrong_token".getLocal.SetStatusbar;
    }
  }
  else {
    if (win.isInternalError) SetStatusbar;
    win.isInternalError = false;
    counterStr = " " ~ win.counter.to!string ~ getChar("mail");
    if (api.isLoading) counterStr ~= getChar("refresh");
  }
  counterStr.selected;
  auto counterStrLen = counterStr.utfLength + (counterStr.utfLength == 7) * 2;
  if (win.notify.text != "") center(win.notify.text, COLS-counterStr.utfLength, ' ').selected;
  else center(win.statusbarText, COLS-counterStrLen, ' ').selected;
  " ".replicatestr((counterStr.utfLength == 7) * 2).selected;
  "\n".print;
}

void SetStatusbar(string s = "") {
  win.statusbarText = s;
}

void drawMenu() {
  foreach(i, le; win.menu) {
    auto space = (le.name.walkLength < win.menuOffset) ? " ".replicatestr(win.menuOffset-le.name.walkLength) : "";
    auto name = le.name ~ space ~ "\n";
    if (win.section == Sections.left) i == win.active ? name.selected : name.regular;
    else i == win.menuActive ? name.selected : name.regular;
  }
}

string cut(uint i, ListElement e) {
  wstring tempText = e.text.toUTF16wrepl;
  auto cut = (COLS-win.menuOffset-win.mbody[i].name.utfLength-1).to!uint;
  if (e.text.utfLength > cut) tempText = tempText[0..cut];
  return tempText.to!string;
}

void bodyToBuffer() {
  switch (win.activeBuffer) {
    case Buffers.chat: win.mbody = GetChat; break;
    case Buffers.dialogs: win.mbody = GetDialogs; break;
    case Buffers.friends: win.mbody = GetFriends; break;
    case Buffers.music: win.mbody = GetMusic; break;
    case Buffers.help: win.mbody = GenerateHelp; break;
    case Buffers.settings: win.mbody = GenerateSettings; break;
    default: break;
  }
  if (LINES-2 < win.mbody.length) win.buffer = win.mbody[0..LINES-2].dup;
  else win.buffer = win.mbody.dup;
  if (win.activeBuffer != Buffers.chat) {
    foreach(i, e; win.buffer) {
      if (e.name.utfLength.to!int + win.menuOffset+1 > COLS)
      try {
        win.buffer[i].name = e.name.to!wstring[0..COLS-win.menuOffset-1].to!string;
        }
      catch(Throwable) {
      }
      else
        win.buffer[i].name ~= " ".replicatestr(COLS - e.name.utfLength - win.menuOffset-1);
    }
  }
}

void drawDialogsList() {
  foreach(i, e; win.buffer) {
    wmove(stdscr, 2+i.to!int, win.menuOffset+1);
    if (i.to!int == win.active-win.scrollOffset) {
      e.name.selected;
      wmove(stdscr, 2+i.to!int, win.menuOffset+win.mbody[i].name.utfLength.to!int+1);
      cut(i.to!uint, e).graySelected;
    } else {
      switch (win.msgDrawSetting) {
        case DrawSetting.allMessages:
          allMessages(e, i.to!uint); break;
        case DrawSetting.onlySelectedMessage:
          onlySelectedMessage(e, i); break;
        case DrawSetting.onlySelectedMessageAndUnread:
          onlySelectedMessageAndUnread(e, i.to!uint); break;
        default: break;
      }
    }
  }
}

void allMessages(ListElement e, uint i) {
  e.flag ? e.name.regular : e.name.secondColor;
  wmove(stdscr, 2+i.to!int, win.menuOffset+win.mbody[i].name.walkLength.to!int+1);
  cut(i, e).white;
}

void onlySelectedMessage(ListElement e, ulong i) {
  e.flag ? e.name.regular : e.name.secondColor;
}

void onlySelectedMessageAndUnread(ListElement e, uint i) {
  e.flag ? e.name.regular : e.name.secondColor;
  if (e.name.indexOf(getChar("unread")) == 0) {
    wmove(stdscr, 2+i.to!int, win.menuOffset+win.mbody[i].name.walkLength.to!int+1);
    cut(i, e).white;
  }
}

void drawFriendsList() {
  foreach(i, e; win.buffer) {
    wmove(stdscr, 2+i.to!int, win.menuOffset+1);
    if (i.to!int == win.active-win.scrollOffset) {
      if (!e.flag) {
        e.name[0..$-e.text.utfLength].selected;
        e.text.selected;
      } else e.name.selected;
    } else if (e.flag) {
      e.name.regular;
    } else {
      e.name[0..$-e.text.utfLength].secondColor;
      e.text.secondColor;
    }
  }
}

void drawMusicList() {
  if (win.isMusicPlaying) {
    foreach(i, e; win.playerUI) {
      wmove(stdscr, 2+i.to!int, win.menuOffset);
      e.name.regular;
    }
    wmove(stdscr, 5, win.menuOffset+COLS/2+19);
    mplayer.repeatMode  ? getChar("repeat").regular  : getChar("repeat").secondColor;
    mplayer.shuffleMode ? getChar("shuffle").regular : getChar("shuffle").secondColor;
  }

  foreach(i, e; win.buffer) {
    wmove(stdscr, win.isMusicPlaying*5+2+i.to!int, win.menuOffset+1);
    if (!win.isMusicPlaying)
      i.to!int == win.active-win.scrollOffset ? e.name.selected : e.name.regular;
    else {
      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;
      else i.to!int == win.active-win.scrollOffset ? e.name.selected : e.name.secondColor;
    }
  }
}

void drawBuffer() {
  switch (win.activeBuffer) {
    case Buffers.dialogs: drawDialogsList; break;
    case Buffers.friends: drawFriendsList; break;
    case Buffers.music: drawMusicList; break;
    case Buffers.chat: drawChat; break;
    default: {
      foreach(i, e; win.buffer) {
        wmove(stdscr, 2+i.to!int, win.menuOffset+1);
        i.to!int == win.active ? e.name.selected : e.name.regular;
      }
      break;
    }
  }
}

int colorHash(string name) {
  int sum;
  foreach(e; name) sum += e;
  return sum % 5 + 1;
}

void renderColoredOrRegularText(string text) {
  if (win.isRainbowChat && (!win.isRainbowOnlyInGroupChats || win.isConferenceOpened))
    text == api.me.first_name~" "~api.me.last_name ? text.secondColor : text.colored(text.colorHash);
  else
    text == api.me.first_name~" "~api.me.last_name ? text.secondColor : text.regular;
}

void drawChat() {
  foreach(i, e; win.buffer) {
    wmove(stdscr, 2+i.to!int, 1);
    if (e.flag) {
      if (e.id == -1) {
        e.name.renderColoredOrRegularText;
        " ".replicatestr(COLS-e.name.utfLength-e.text.length-2).regular;
        e.text.secondColor;
      } else {
        e.name[0..e.id].regularWhite;
        e.name[e.id..$].renderColoredOrRegularText;
        wmove(stdscr, 2+i.to!int, (COLS-e.text.length-1).to!int);
        e.text.secondColor;
      }
    } else
      e.name.regularWhite;
  }
  if (win.isMessageWriting) {
    "\n: ".print;
    win.msgBuffer.print;
    wmove(stdscr, win.buffer.length.to!int+2, win.cursor.x+2);
    "".regular;
  }
}

int activeBufferMaxLen() {
  switch (win.activeBuffer) {
    case Buffers.dialogs: return api.getServerCount(blockType.dialogs);
    case Buffers.friends: return api.getServerCount(blockType.friends);
    case Buffers.music: return api.getServerCount(blockType.music);
    case Buffers.chat: return api.getChatLineCount(win.chatID, COLS-12);
    default: return 0;
  }
}

bool activeBufferEventsAllowed() {
  switch (win.activeBuffer) {
    case Buffers.dialogs: return api.isScrollAllowed(blockType.dialogs);
    case Buffers.friends: return api.isScrollAllowed(blockType.friends);
    case Buffers.music: return api.isScrollAllowed(blockType.music);
    case Buffers.chat: return api.isChatScrollAllowed(win.chatID);
    default: return true;
  }
}

void forceRefresh() {
  switch (win.activeBuffer) {
    case Buffers.dialogs: api.toggleForceUpdate(blockType.dialogs); break;
    case Buffers.friends: api.toggleForceUpdate(blockType.friends); break;
    case Buffers.music: api.toggleForceUpdate(blockType.music); break;
    default: return;
  }
}

public void jumpToBeginning() {
  win.active = 0;
  win.scrollOffset = 0;
}

void jumpToEnd() {
  if (win.shuffled && win.activeBuffer == Buffers.music) {
    win.active = win.savedShuffledLen-1-1*(win.isMusicPlaying);
    win.scrollOffset = win.savedShuffledLen-LINES+2+(win.isMusicPlaying)*4;
  } else {
    win.active = activeBufferMaxLen-1;
    win.scrollOffset = activeBufferMaxLen-LINES+2+(win.activeBuffer == Buffers.music && win.isMusicPlaying)*5;
  }
  if (win.scrollOffset < 0) win.scrollOffset = 0;
}

int _getch() {
  int key = getch;
  if (key == 27) {
    if (getch == -1) return k_esc;
    else {
      switch (getch) {
        case 65: return -2;         // Up
        case 66: return -3;         // Down
        case 67: return -4;         // Right
        case 68: return -5;         // Left
        case 49: getch; return -6;  // Home
        case 72: getch; return -6;  // Home
        case 50: getch; return -7;  // Ins
        case 51: getch; return -8;  // Del
        case 52: getch; return -9;  // End
        case 70: getch; return -9;  // End
        case 53: getch; return -10; // Pg Up
        case 54: getch; return -11; // Pg Down
        default: return -1;
      }
    }
  }
  return key;
}

void menuSelect(int position) {
  SetStatusbar;
  win.section = Sections.left;
  win.active  = position;
  win.menu[win.active].callback(win.menu[win.active]);
  win.menuActive = win.active;
  if (win.activeBuffer == Buffers.music) {
    win.active = mplayer.trackNum;
    win.scrollOffset = mplayer.offset;
  } else {
    win.active = 0;
    win.scrollOffset = 0;
  }
  win.section = Sections.right;
}

void controller() {
  while (true) {
    timeout(100);
    win.key = _getch;
    if (win.key == -1) win.selectFlag = false;
    if (!win.isMessageWriting && (win.key == 49 || win.key == 50 || win.key == 51)) { menuSelect(win.key-49); break; }
    else if (win.key != -1) break;
    else if (api.isSomethingUpdated) break;
    else if (win.activeBuffer == Buffers.music && mplayer.musicState && mplayer.playtimeUpdated) break;
  }
  //if (win.key != -1) win.key.print;
  if (win.isMessageWriting) msgBufferEvents;
  else if (canFind(kg_left, win.key)) backEvent;
  else if (activeBufferEventsAllowed) {
    if (win.activeBuffer != Buffers.chat) nonChatEvents;
    else chatEvents;
  }
  checkBounds;
}

void msgBufferEvents() {
  if (win.key == k_esc || win.key == k_enter) {
    if (win.key == k_enter) {
	    if (win.msgBuffer.utfLength != 0) api.asyncSendMessage(win.chatID, win.msgBuffer);
      else api.asyncMarkMessagesAsRead(win.chatID);
    }
    win.msgBuffer = "";
    win.cursor.x = win.cursor.y = 0;
    curs_set(0);
    win.isMessageWriting = false;
  }
  else if (win.key == k_bckspc && win.msgBuffer.utfLength != 0 && win.cursor.x != 0) {
    if (win.cursor.x == win.msgBuffer.utfLength) win.msgBuffer = win.msgBuffer.to!wstring[0..$-1].to!string;
    else win.msgBuffer = win.msgBuffer.to!wstring[0..win.cursor.x-1].to!string ~ win.msgBuffer.to!wstring[win.cursor.x..$].to!string;
    win.cursor.x--;
    win.msgBufferSize = win.msgBuffer.utfLength.to!int;
  }
  else if (win.key > 0 && !canFind(kg_ignore, win.key)) {
    try {
      validate(win.msgBuffer);
      win.msgBufferSize = win.msgBuffer.utfLength.to!int;
    } catch (UTFException e) {
      if (win.cursor.x-1 != win.msgBufferSize) {
        int i, count, offset;
        char chr;
        while (count != win.cursor.x) {
          chr = win.msgBuffer[i];
          if (chr != 208 && chr != 209) ++count;
          else ++offset;
          ++i;
        }
        chr = win.msgBuffer[count+offset];
        if (chr == 208 || chr == 209) --offset;
        win.msgBuffer = win.msgBuffer[0..count+offset-1] ~ win.key.to!char ~ win.msgBuffer[count+offset-1..$];
      }
      else win.msgBuffer ~= win.key.to!char;
      return;
    }
    if (win.cursor.x == win.msgBuffer.utfLength) win.msgBuffer ~= win.key.to!char;
    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;
    win.cursor.x++;
    if (win.showTyping) api.setTypingStatus(win.chatID);
  }
  else if (win.key == k_home) win.cursor.x = 0;
  else if (win.key == k_end) win.cursor.x = win.msgBuffer.utfLength.to!int;
  else if (win.key == k_left && win.cursor.x != 0) win.cursor.x--;
  else if (win.key == k_right && win.cursor.x != win.msgBuffer.utfLength) win.cursor.x++;
}

void globalMplayerShortcuts() {
  if (canFind(kg_pause, win.key)) mplayer.pause;
  if (canFind(kg_next, win.key)) {
    if (mplayer.repeatMode) mplayer.trackNum++;
    mplayer.trackOver;
  }
  if (canFind(kg_prev, win.key)) {
    mplayer.trackNum -= 2-mplayer.repeatMode;
    mplayer.trackOver;
  }
  if (canFind(kg_rewind_forward, win.key)) mplayer.player.relativeSeek(win.seekValue, win.seekPercentFlag);
  if (canFind(kg_rewind_backward, win.key)) mplayer.player.relativeSeek(-win.seekValue, win.seekPercentFlag);
  if (canFind(kg_loop, win.key)) mplayer.repeatMode = !mplayer.repeatMode;
  if (canFind(kg_mix, win.key) && win.activeBuffer == Buffers.music) toggleShuffleMode;
}

void nonChatEvents() {
  globalMplayerShortcuts;
  if (canFind(kg_down, win.key)) downEvent;
  if (canFind(kg_up, win.key)) upEvent;
  else if (canFind(kg_right, win.key) && !win.selectFlag) {
    win.selectFlag = true;
    selectEvent;
  }
  else if (win.section == Sections.right) {
    if (canFind(kg_refresh, win.key)) forceRefresh;
    if (win.key == k_home) jumpToBeginning;
    else if (win.key == k_end && win.activeBuffer != Buffers.none) jumpToEnd;
    else if (win.key == k_pagedown && win.activeBuffer != Buffers.none) {
      win.scrollOffset += LINES/2;
      win.active += LINES/2;
    }
    else if (win.key == k_pageup && win.activeBuffer != Buffers.none) {
      win.scrollOffset -= LINES/2;
      win.active -= LINES/2;
      if (win.active < 0) win.active = win.scrollOffset = 0;
      if (win.scrollOffset < 0) win.scrollOffset = 0;
    }
  }
}

void chatEvents() {
  globalMplayerShortcuts;
  if (canFind(kg_up, win.key)) win.scrollOffset += 2;
  else if (canFind(kg_down, win.key)) win.scrollOffset -= 2;
  else if (win.key == k_pagedown) win.scrollOffset -= LINES/2;
  else if (win.key == k_pageup) win.scrollOffset += LINES/2;
  else if (win.key == k_home) win.scrollOffset = 0;
  else if (canFind(kg_right, win.key)) {
    curs_set(1);
    win.isMessageWriting = true;
  }
  else if (canFind(kg_shift_right, win.key)) {
      dbm("Reading from file.\n");
      // TODO! Call vim to save text in vkcliTmpMsgFile.
      string text = getMessageFromTmpFile();
      if (!text.empty)
	  api.asyncSendMessage(win.chatID, text);
  }
  else if (canFind(kg_shift_s, win.key)) {
    api.asyncMarkMessagesAsRead(win.chatID);
  }
  else if (canFind(kg_refresh, win.key)) api.toggleChatForceUpdate(win.chatID);
  if (win.scrollOffset < 0) win.scrollOffset = 0;
  else if (activeBufferMaxLen != -1 && win.scrollOffset > activeBufferMaxLen-LINES+3) win.scrollOffset = activeBufferMaxLen-LINES+3;
}

void checkBounds() {
  if (win.activeBuffer != Buffers.none && activeBufferMaxLen > 0 && win.active > activeBufferMaxLen-1) jumpToBeginning;
  else if(win.activeBuffer != Buffers.none && activeBufferMaxLen > 0 && win.active < 0) jumpToEnd;
}

void downEvent() {
  if (win.section == Sections.left) win.active >= win.menu.length-1 ? win.active = 0 : win.active++;
  else {
    if (win.active == activeBufferMaxLen-1) jumpToBeginning;
    else {
      if (win.active-win.scrollOffset == LINES-3-(win.activeBuffer == Buffers.music && win.isMusicPlaying)*5)
        win.scrollOffset++;

      if (win.shuffled && win.activeBuffer == Buffers.music && win.scrollOffset+LINES-2-win.isMusicPlaying*4 > win.savedShuffledLen) {
        jumpToBeginning;
        win.active--;
      }

      if (win.activeBuffer != Buffers.none) {
        if (activeBufferEventsAllowed) win.active++;
      } else win.active >= win.buffer.length-1 ? win.active = 0 : win.active++;
    }
  }
}

void upEvent() {
  if (win.section == Sections.left) win.active == 0 ? win.active = win.menu.length.to!int-1 : win.active--;
  else {
    if (win.activeBuffer != Buffers.none) {
        if (win.active == 0) jumpToEnd;
        else {
          if (win.active == win.scrollOffset) win.scrollOffset--;
          win.active--;
          if (win.scrollOffset < 0) win.scrollOffset = 0;
        }
    } else {
      win.active == 0 ? win.active = win.buffer.length.to!int-1 : win.active--;
    }
  }
}

void selectEvent() {
  if (win.section == Sections.left) {
    if (win.menu[win.active].callback) win.menu[win.active].callback(win.menu[win.active]);
    win.menuActive = win.active;
    if (win.activeBuffer == Buffers.music) {
      win.active = mplayer.trackNum;
      win.scrollOffset = mplayer.offset;
    }
    else win.active = 0;
    win.section = Sections.right;
  } else {
    win.lastScrollOffset = win.scrollOffset;
    win.lastScrollActive = win.active;
    if (win.isMusicPlaying && win.activeBuffer == Buffers.music) {
      if (win.active-win.scrollOffset >= 0)
        win.mbody[win.active-win.scrollOffset].callback(win.mbody[win.active-win.scrollOffset]);
    } 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]);
    if (win.menuActive == 4) storage.update;
  }
}

void backEvent() {
  if (win.section == Sections.right) {
    if (win.lastBuffer != Buffers.none) {
      win.scrollOffset = win.lastScrollOffset;
      win.activeBuffer = win.lastBuffer;
      win.lastBuffer = Buffers.none;
      win.isConferenceOpened = false;
      SetStatusbar;
      if (win.scrollOffset != 0) win.active = win.lastScrollActive;
    } else {
      win.scrollOffset = 0;
      win.lastScrollOffset = 0;
      win.activeBuffer = Buffers.none;
      win.active = win.menuActive;
      win.section = Sections.left;
      win.mbody = new ListElement[0];
      win.buffer = new ListElement[0];
    }
  }
}

wstring[] run(string[] args) {
  wstring[] output;
  auto pipe = pipeProcess(args, Redirect.stdout);
  foreach(line; pipe.stdout.byLine) output ~= to!wstring(line.idup);
  return output;
}

void exit(ref ListElement le) {
  win.key = k_q;
}

void open(ref ListElement le) {
  win.mbody = le.getter();
}

void chat(ref ListElement le) {
  win.chatID = le.id;
  win.scrollOffset = 0;
  open(le);
  if (le.isConference) {
    auto len = getChar("unread").length;
    if (le.name[0..len] == getChar("unread")) le.name[len..$].SetStatusbar;
    else le.name.SetStatusbar;
    win.isConferenceOpened = true;
  }
  win.lastBuffer = win.activeBuffer;
  win.activeBuffer = Buffers.chat;
}

void run(ref ListElement le) {
  le.getter();
}

void changeLang(ref ListElement le) {
  swapLang;
  win.mbody = GenerateSettings;
  relocale;
  storage.update;
}

void changeMainColor(ref ListElement le) {
  win.namecolor == Colors.max ? win.namecolor = 0 : win.namecolor++;
  le.name = "main_color".getLocal ~ ("color"~win.namecolor.to!string).getLocal;
}

void changeSecondColor(ref ListElement le) {
  win.textcolor == Colors.max ? win.textcolor = 0 : win.textcolor++;
  le.name = "second_color".getLocal ~ ("color"~win.textcolor.to!string).getLocal;
}

void changeMsgSetting(ref ListElement le) {
  win.msgDrawSetting = win.msgDrawSetting != 2 ? win.msgDrawSetting+1 : 0;
  le.name = "msg_setting_info".getLocal ~ ("msg_setting"~win.msgDrawSetting.to!string).getLocal;
}

void toggleChatRender(ref ListElement le) {
  win.isRainbowChat = !win.isRainbowChat;
  win.mbody = GenerateSettings;
}

void toggleShowTyping(ref ListElement le) {
  win.showTyping = !win.showTyping;
  win.mbody = GenerateSettings;
}

void toggleUnicodeChars(ref ListElement le) {
  win.unicodeChars = !win.unicodeChars;
  win.mbody = GenerateSettings;
}

void toggleChatRenderOnlyGroup(ref ListElement le) {
  win.isRainbowOnlyInGroupChats = !win.isRainbowOnlyInGroupChats;
  le.name = "rainbow_in_chat".getLocal ~ (win.isRainbowOnlyInGroupChats.to!string).getLocal;
}

void toggleShowConvNotifications(ref ListElement le) {
  win.showConvNotifications = !win.showConvNotifications;
  api.showConvNotifications(win.showConvNotifications);
  le.name = "show_conv_notif".getLocal ~ (win.showConvNotifications.to!string).getLocal;
}

void toggleSendOnline(ref ListElement le) {
  win.sendOnline = !win.sendOnline;
  api.sendOnline(win.sendOnline);
  le.name = "send_online".getLocal ~ (win.sendOnline.to!string).getLocal;
}

void toggleSeekPercentOrValue(ref ListElement le) {
  win.seekPercentFlag = !win.seekPercentFlag;
  win.seekValue = win.seekPercentFlag ? 2 : 15;
  le.name = "seek_percent_or_value".getLocal ~ ("seek_" ~ win.seekPercentFlag.to!string).getLocal;
}

ListElement[] GenerateHelp() {
  win.activeBuffer = Buffers.help;
  return [
    ListElement(center("general_navig".getLocal, COLS-16, ' ')),
    ListElement("help_move".getLocal),
    ListElement("help_select".getLocal),
    ListElement("help_jump".getLocal),
    ListElement("help_homend".getLocal),
    ListElement("help_exit".getLocal),
    ListElement("help_refr".getLocal),
    ListElement("help_123".getLocal),
    ListElement("help_pause".getLocal),
    ListElement("help_loop".getLocal),
    ListElement("help_mix".getLocal),
    ListElement("help_rewind".getLocal),
  ];
}

ListElement[] GenerateSettings() {
  win.activeBuffer = Buffers.settings;
  ListElement[] list;
  list ~= [
    ListElement(center("display_settings".getLocal, COLS-16, ' ')),
    ListElement("main_color".getLocal ~ ("color"~win.namecolor.to!string).getLocal, "", &changeMainColor),
    ListElement("second_color".getLocal ~ ("color"~win.textcolor.to!string).getLocal, "", &changeSecondColor),
    ListElement("lang".getLocal, "", &changeLang, null),
    ListElement(center("convers_settings".getLocal, COLS-16, ' ')),
    ListElement("msg_setting_info".getLocal ~ ("msg_setting"~win.msgDrawSetting.to!string).getLocal, "", &changeMsgSetting),
    ListElement("rainbow".getLocal ~ (win.isRainbowChat.to!string).getLocal, "", &toggleChatRender),
  ];
  if (win.isRainbowChat) list ~= ListElement("rainbow_in_chat".getLocal ~ (win.isRainbowOnlyInGroupChats.to!string).getLocal, "", &toggleChatRenderOnlyGroup);
  list ~= ListElement("show_typing".getLocal ~ (win.showTyping.to!string).getLocal, "", &toggleShowTyping);
  list ~= ListElement("show_conv_notif".getLocal ~ (win.showConvNotifications.to!string).getLocal, "", &toggleShowConvNotifications);
  list ~= ListElement(center("general_settings".getLocal, COLS-16, ' '));
  list ~= ListElement("send_online".getLocal ~ (win.sendOnline.to!string).getLocal, "", &toggleSendOnline);
  list ~= ListElement("unicode_chars".getLocal ~ (win.unicodeChars.to!string).getLocal, "", &toggleUnicodeChars);
  list ~= ListElement(center("music_settings".getLocal, COLS-16, ' '));
  list ~= ListElement("seek_percent_or_value".getLocal ~ ("seek_" ~ win.seekPercentFlag.to!string).getLocal, "", &toggleSeekPercentOrValue);
  return list;
}

ListElement[] GetDialogs() {
  ListElement[] list;
  string
    newMsg,
    unreadText,
    lastMsg;
  uint space;

  win.activeBuffer = Buffers.dialogs;
  auto dialogs = api.getBufferedDialogs(LINES-2, win.scrollOffset);

  if (api.dialogsFactory.getBlockObject(win.scrollOffset) !is null && dialogs.length != LINES-2 && activeBufferMaxLen > LINES-2)
    dialogs = api.getBufferedDialogs(LINES-2, win.scrollOffset-(LINES-2-dialogs.length).to!int);

  foreach(e; dialogs) {
    unreadText = "";
    newMsg = e.unread ? getChar("unread") : "  ";
    if (e.outbox) newMsg = "  ";
    lastMsg = e.lastMessage.replace("\n", " ");
    if (lastMsg.utfLength > COLS-win.menuOffset-newMsg.utfLength-e.name.utfLength-3-e.unreadCount.to!string.length)
      try {
        lastMsg = lastMsg.toUTF16wrepl[0..COLS-win.menuOffset-newMsg.utfLength-e.name.utfLength-8-e.unreadCount.to!string.length].toUTF8wrepl;
      }
      catch(Throwable) {
      }
    if (e.unread) {
      if (e.outbox) unreadText ~= getChar("outbox");
      else if (e.unreadCount > 0) unreadText ~= e.unreadCount.to!string ~ getChar("inbox");
      space = COLS-win.menuOffset-newMsg.utfLength-e.name.utfLength-lastMsg.utfLength-unreadText.utfLength-4;
      if (space < COLS) unreadText = " ".replicatestr(space) ~ unreadText;
      else unreadText = "   " ~ unreadText;
    }
    list ~= ListElement(newMsg ~ e.name, ": " ~ lastMsg ~ unreadText, &chat, &GetChat, e.online, e.id, e.isChat);
  }
  return list;
}

ListElement[] GetFriends() {
  ListElement[] list;
  win.activeBuffer = Buffers.friends;
  auto friends = api.getBufferedFriends(LINES-2, win.scrollOffset);

  if (api.friendsFactory.getBlockObject(win.scrollOffset) !is null && friends.length != LINES-2 && activeBufferMaxLen > LINES-2)
    friends = api.getBufferedFriends(LINES-2, win.scrollOffset-(LINES-2-friends.length).to!int);

  foreach(e; friends)
    list ~= ListElement(e.first_name ~ " " ~ e.last_name, e.last_seen_str, &chat, &GetChat, e.online, e.id);
  return list;
}

ListElement[] setCurrentTrack() {
  if (!mplayer.player.isPlayerInit) "err_noplayer".getLocal.SetStatusbar;
  else {
    vkAudio track;
    if (!win.isMusicPlaying && LINES-3-(win.active-win.scrollOffset) <= 5) win.scrollOffset += 5-(LINES-3-(win.active-win.scrollOffset));
    if (win.isMusicPlaying && mplayer.sameTrack(win.active)) mplayer.pause;
    else {
      mplayer.play(win.active);
      mplayer.offset = win.scrollOffset;
      win.isMusicPlaying = true;
    }
  }
  return new ListElement[0];
}

void toggleShuffleMode() {
  mplayer.shuffleMode = !mplayer.shuffleMode;
  if (mplayer.shuffleMode) {
    randomShuffle(win.shuffledMusic);
    jumpToBeginning;
    mplayer.offset = win.scrollOffset;
  }
}

ListElement[] GetMusic() {
  ListElement[] list;
  string space, artistAndSong;
  int amount;
  vkAudio[] music;

  win.activeBuffer = Buffers.music;
  if (mplayer.shuffleMode)
    music = getShuffledMusic(LINES-2-win.isMusicPlaying*4, win.scrollOffset);
  else
    music = api.getBufferedMusic(LINES-2-win.isMusicPlaying*4, win.scrollOffset);
  win.playerUI = mplayer.getMplayerUI(COLS);

  foreach(e; music) {
    string indicator = (mplayer.currentTrack.id == e.id.to!string) ? mplayer.musicState ? getChar("play") : getChar("pause") : "    ";
    artistAndSong = indicator ~ e.artist ~ " - " ~ e.title;

    int width = COLS-4-win.menuOffset-e.duration_str.length.to!int;
    if (artistAndSong.utfLength > width) {
      artistAndSong = artistAndSong[0..width];
      amount = COLS-6-win.menuOffset-artistAndSong.utfLength.to!int;
    } else amount = COLS-9-win.menuOffset-e.artist.utfLength.to!int-e.title.utfLength.to!int-e.duration_str.length.to!int;

    space = " ".replicatestr(amount);
    list ~= ListElement(artistAndSong ~ space ~ e.duration_str, e.url, &run, &setCurrentTrack);
  }
  return list;
}

ListElement[] GetChat() {
  ListElement[] list;
  int verticalOffset;
  try {
    validate(win.msgBuffer);
    verticalOffset = win.msgBuffer.utfLength.to!int/COLS-1;
  } catch (UTFException e) { verticalOffset = win.msgBufferSize/COLS-1; }
  auto chat = api.getBufferedChatLines(LINES-4-verticalOffset, win.scrollOffset, win.chatID, COLS-12);
  foreach(e; chat) {
    if (e.isFwd) {
      ListElement line = {"    " ~ "| ".replicatestr(e.fwdDepth)};
      if (e.isName && !e.isSpacing) {
        line.flag = true;
        line.id = line.name.length.to!int + 4;
        line.name ~= getChar("fwd") ~ e.text;
        line.text = e.time;
      } else
        line.name ~= e.text;
      list ~= line;
    } else {
      string unreadSign = e.unread ? getChar("unread") : " ";
      list ~= !e.isName ? ListElement("  " ~ unreadSign ~ e.text) : ListElement(e.text, e.time, null, null, true, -1);
    }
  }
  return list;
}

void test() {
    //initFileDbm();
    localize();
    auto storage = load;
    if("token" !in storage) {
        writeln("cyka");
        return;
    }
    /*auto api = new VKapi(storage["token"]);
    if(!api.isTokenValid) {
        writeln("bad token");
        return;
    }

    int i = 0;
    while(true) {
        readln();
        auto pr = 2000000012;
        if(i > 4) {
            i = 0;
            pr = 2000000023;
        }
        api.setTypingStatus(pr);
        ++i;
    }*/
}

void clear() {
  for (int y = 0; y < LINES; y++) {
    wmove(stdscr, y, 0);
    print(" ".replicatestr(COLS));
  }
  wmove(stdscr, 0, 0);
}

void help() {
  writeln(
    // these help can be generated from actions (array -> hashmap)
    "-h, --help      This help page" ~ "\n" ~
    "-v, --version   Show client version" ~ "\n" ~
    "-r --reauth     Receive new auth token" ~ "\n" ~
    "Logs are here:  /tmp/vkcli-log/"
  );
}

void init() {
  updateGcSignals();
  setPosixSignals();
  setlocale(LC_CTYPE,"");
  win.lastBuffer = Buffers.none;
  setEnvLanguage;
  localize;
  relocale;
}

void main(string[] args) {
  string[] actions = ["version", "help", "reauth"];
  bool correct = false, reauth = false;

  if (args.length != 1) {
    foreach(arg; args) {
      foreach(act; actions) {
        if (arg == "-" ~ act[0] || arg == "--" ~ act) {
          correct = true;
          final switch (act) {
            case "version": writefln("vk-cli %s", currentVersion); exit(0); break;
            case "help": help(); exit(0); break;
            case "reauth": reauth = true; break;
          }
        }
      }
    }
    if (!correct) writeln("wrong arguments");
  }

  //test;
  initdbm();
  init();
  storage = load();
  storage.parse();

  try
    if(reauth == true || "token" !in storage || "auth_v2" !in storage) {
      api = storage.get_token;
      storage.save;
    }
    else api = new VkMan(storage["token"]);
  catch
    (BackendException e) Exit(e.msg);

  initscr();
  color();
  curs_set(0);
  noecho;

  mplayer = new MusicPlayer;
  mplayer.startPlayer(api);
  try
    api.setLongpollWait(storage["longpoll_wait"].to!int);
  catch(Exception)
    dbm("failed set longpoll_wait from config");
  api.showConvNotifications(win.showConvNotifications);
  api.sendOnline(win.sendOnline);

  while (!canFind(kg_esc, win.key) || win.isMessageWriting) {
    clear;
    statusbar;
    if (win.activeBuffer != Buffers.chat) drawMenu;
    bodyToBuffer;
    drawBuffer;
    refresh;
    controller;
  }

  Exit("", 0, true);
}


================================================
FILE: source/cfg.d
================================================
/*
Copyright 2016 HaCk3D, substanceof

https://github.com/HaCk3Dq
https://github.com/substanceof

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

module cfg;

import std.path, std.stdio, std.file, std.string;

string[string] load() {
  string[string] storage;
  auto config = expandTilde("~/.vkrc");
  if (config.exists) {
    auto f = File(config, "r");
    while (!f.eof) {
      auto line = f.readln.strip;
      if (line.length != 0) storage[line[0..line.indexOf("=")-1]] = line[line.indexOf("=")+2..$];
    }
    f.close;
  }
  return storage;
}

void save(string[string] stor) {
  auto config = expandTilde("~/.vkrc");
  auto f = File(config, "w");
  foreach(key, value; stor) {
    f.write(key ~ " = " ~ value ~ "\n");
  }
  f.close;
}

================================================
FILE: source/localization.d
================================================
/*
Copyright 2016 HaCk3D, substanceof

https://github.com/HaCk3Dq
https://github.com/substanceof

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

module localization;

import std.stdio, std.conv, std.string, std.process, utils;

struct lang {
  string en;
  string ru;
}

const int
  En = 0,
  Ru = 1;

__gshared {
  private lang[string] local;
  private int currentLang = 0;
}

void setEnvLanguage() {
  string output = environment["LANG"];
  currentLang = output.indexOf("RU") != -1 ? Ru : En;
}

void localize() {
  local["e_start_browser"]       = lang("You need to copy access token from your web browser. Would you like to launch it now? [Y/n] ",
                                        "Необходимо скопировать ваш access token из веб-браузера. Хотите запустить его сейчас? [Y/n] ");

  local["e_token_link"]          = lang("Follow this link to get your access_token: ",
                                        "Перейдите по следующей ссылке, чтобы получить ваш access_token: ");
  
  local["e_token_info"]          = lang("Please allow access to your data and then copy link from the address bar.",
                                        "Пожалуйста, разрешите приложению доступ к вашим данным, а затем скопируйте содержимое адресной строки.");
  
  local["e_input_token"]         = lang("Insert your link here: ",
                                        "Вставьте адрес из браузера сюда:  ");
  
  local["e_wrong_token"]         = lang("Wrong token, try again with -r",
                                        "Неверный access token, попробуйте еще раз с ключом -r");
  
  local["m_friends"]             = lang("Friends",
                                        "Друзья");
  
  local["m_conversations"]       = lang("Conversations",
                                        "Диалоги");
  
  local["m_music"]               = lang("Music",
                                        "Музыка");
  
  local["m_help"]                = lang("Help",
                                        "Помощь");
  
  local["m_settings"]            = lang("Settings",
                                        "Настройки");
  
  local["m_exit"]                = lang("Exit",
                                        "Выход");
  
  local["c_kick"]                = lang("kicked out ",
                                        "исключил из беседы пользователя ");

  local["c_invite"]              = lang("invited ",
                                        "пригласил пользователя ");

  local["c_kickself"]            = lang("left the conversation",
                                        "покинул беседу");

  local["c_inviteself"]          = lang("returned to the conversation",
                                        "вернулся в беседу");

  local["c_create"]              = lang("created chat ",
                                        "создал чат ");

  local["c_title"]               = lang("changed chat title to ",
                                        "изменил название беседы на ");

  local["c_setphoto"]            = lang("updated chat photo", 
                                        "обновил фотографию беседы");

  local["c_removephoto"]         = lang("removed chat photo", 
                                        "удалил фотографию беседы");
  
  local["main_color"]            = lang("Main color = ",
                                        "Основной цвет = ");
  
  local["second_color"]          = lang("Second color = ",
                                        "Дополнительный цвет = ");
  
  local["color0"]                = lang("White",
                                        "Белый");
  
  local["color1"]                = lang("Red",
                                        "Красный");
  
  local["color2"]                = lang("Green",
                                        "Зеленый");
  
  local["color3"]                = lang("Yellow",
                                        "Желтый");
  
  local["color4"]                = lang("Blue",
                                        "Синий");
  
  local["color5"]                = lang("Pink",
                                        "Розовый");
  
  local["color6"]                = lang("Mint",
                                        "Мятный");
  
  local["color7"]                = lang("Gray",
                                        "Серый");
  
  local["lang"]                  = lang("Language = English",
                                        "Язык = Русский");
  
  local["display_settings"]      = lang("[ Display Settings ]",
                                        "[ Настройки отображения ]");
  
  local["convers_settings"]      = lang("[ Conversations Settings ]",
                                        "[ Настройки диалогов ]");
  
  local["msg_setting_info"]      = lang("How to draw conversations list: ",
                                        "Как отображать список диалогов: ");
  
  local["msg_setting0"]          = lang("show everything",
                                        "показывать всё");
  
  local["msg_setting1"]          = lang("show the selected text only",
                                        "текст только выделенного диалога");
  
  local["msg_setting2"]          = lang("show the selected text and unread ones",
                                        "текст выделенного диалога и всех непрочитанных");
  
  local["loading"]               = lang("Loading",
                                        "Загрузка");
  
  local["general_navig"]         = lang("[ General navigation ]",
                                        "[ Общее управление ]");
  
  local["help_move"]             = lang("Arrow keys, WASD, HJKL         ->   Move cursor",
                                        "Стрелки, WASD, HJKL            ->   Двигать курсор");
  
  local["help_select"]           = lang("Enter, right arrow key, D, L   ->   Select item",
                                        "Enter, стрелка вправо, D, L    ->   Выбрать элемент");
  
  local["help_jump"]             = lang("Page Up/Down                   ->   Scroll up/down for a half of screen",
                                        "Page Up/Down                   ->   Прокрутить вверх/вниз на половину экрана");
  
  local["help_homend"]           = lang("Home/End                       ->   Jump to the beginning/end",
                                        "Home/End                       ->   Прыгнуть в начало/конец");
  
  local["help_exit"]             = lang("Q                              ->   Exit",
                                        "Q                              ->   Выход");
  
  local["help_refr"]             = lang("R                              ->   Refresh window",
                                        "R                              ->   Обновить окно");
  
  local["help_pause"]            = lang("P                              ->   Pause music",
                                        "P                              ->   Остановить музыку");
  
  local["help_loop"]             = lang("O                              ->   Toggle looping",
                                        "O                              ->   Поставить на повтор");
  
  local["help_mix"]              = lang("M                              ->   Shuffle tracks",
                                        "M                              ->   Перемешать треки");
  
  local["help_123"]              = lang("1-3                            ->   Friends, Chats, Music",
                                        "1-3                            ->   Друзья, Сообщения, Музыка");
  
  local["rainbow"]               = lang("Render rainbow in chat: ",
                                        "Рисовать радугу в диалогах: ");
  
  local["unicode_chars"]         = lang("Use unicode characters: ",
                                        "Использовать символы Unicode: ");
  
  local["true"]                  = lang("On",
                                        "Да");
  
  local["false"]                 = lang("Off",
                                        "Нет");
  
  local["rainbow_in_chat"]       = lang("Color only in group chats: ",
                                        "Выделять цветом только конференции: ");
  
  local["sending"]               = lang("Sending",
                                        "Отправка");
  
  local["sendfailed"]            = lang("Failed",
                                        "Ошибка");
  
  local["show_typing"]           = lang("Show that you are typing a message: ",
                                        "Показывать, что вы набираете сообщение: ");
  
  local["show_conv_notif"]       = lang("Show notifications from conferences: ",
                                        "Показывать уведомления из конференций: ");
  
  local["send_online"]           = lang("Send that you are online: ",
                                        "Показывать, что вы онлайн: ");
  
  local["banned"]                = lang("banned",
                                        "заблокирован");
  
  local["general_settings"]      = lang("[ General Settings ]",
                                        "[ Общие настройки ]");
  
  local["no_connection"]         = lang("No connection",
                                        "Нет соединения");
  
  local["err_noplayer"]          = lang("mpv is not installed or not working properly",
                                        "mpv не установлен или работает неверно");
  
  local["music_settings"]        = lang("[ Music Settings ]",
                                        "[ Настройки музыки ]");
  
  local["seek_percent_or_value"] = lang("Rewind track by: ",
                                        "Перематывать трек по: ");
  
  local["seek_true"]             = lang("2 %",
                                        "2 %");
  
  local["seek_false"]            = lang("15 sec",
                                        "15 сек");
  
  local["help_rewind"]           = lang("< >                            ->   Rewind track",
                                        "< >                            ->   Перематывать трек");
}

void swapLang() {
  currentLang = (currentLang == En) ? Ru : En;
}

string getLang() {
  return currentLang.to!string;
}

string getLocal(string id) {
  return currentLang == En ? local[id].en : local[id].ru;
}


================================================
FILE: source/musicplayer.d
================================================
/*
Copyright 2016 HaCk3D, substanceof

https://github.com/HaCk3Dq
https://github.com/substanceof

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import std.process, std.stdio, std.string,
       std.array, std.algorithm, std.conv,
       std.math, std.ascii,
       std.socket, std.json;
static import std.file;
import core.sys.posix.signal;
import core.thread;
import app, utils;
import vkapi: VkMan, blockType, vkAudio;

struct Track {
  string artist, title, duration, playtime, id;
  int durationSeconds;
}

__gshared MusicPlayer mplayer;
__gshared VkMan api;

class MusicPlayer {
  __gshared {
    mpv player;
    Track currentTrack;
    bool
      playtimeUpdated,
      trackOverStateCatched = true, //for reject empty strings before playback starts
      repeatMode,
      shuffleMode;
    Track[] playlist;
    string
      stockProgress = "=".replicate(50),
      realProgress  = "|" ~ "=".replicate(49);
    int position, trackNum, offset;
  }

  const updateWait = dur!"msecs"(1000);

  this() {
    player = new mpv(
      sec => setPlaytime(sec),
      {
        if(!trackOverStateCatched) trackOver();
      }
    );
  }

  void exitPlayer() {
    player.exit();
  }

  bool musicState() {
    return player.getMusicState();
  }

  void pause() {
    player.pause();
  }

  bool playerExit() {
    return player.isPlayerExit();
  }

  bool isInit() {
    return player.isPlayerInit();
  }

  void play(int position) {
    trackOverStateCatched = true;
    trackNum = position;

    vkAudio track;
    if (mplayer.shuffleMode)
      track = getShuffledMusic(1, position)[0];
    else
      track = api.getBufferedMusic(1, position)[0];

    currentTrack = Track(track.artist, track.title, track.duration_str, "", track.id.to!string, track.duration_sec);
    loadFile(track.url);
  }

  void loadFile(string url) {
    trackOverStateCatched = true;
    realProgress = "|" ~ "=".replicate(49);
    auto p = prepareTrackUrl(url);
    player.loadfile(p);
  }

  void startPlayer(VkMan vkapi) {
    currentTrack.playtime = "0:00";
    api = vkapi;
    player.start();
  }

  string durToStr(real duration) {
    auto intDuration = lround(duration);
    auto min = intDuration / 60;
    auto sec = intDuration - (60*min);
    return min.to!string ~ ":" ~ sec.to!int.tzr;
  }

  void setPlaytime(real sec) {
    real
      trackd = currentTrack.durationSeconds.to!real,
      step =  trackd / 50;
    int newPos = floor(sec / step).to!int;
    currentTrack.playtime = durToStr(sec);
    if (position != newPos) {
      position = newPos;

      if(newPos >= 50) newPos = 49;
      else if (newPos < 0) newPos = 0;

      auto newProgress = stockProgress.dup;
      newProgress[newPos] = '|';
      realProgress = newProgress.to!string;
    }
    playtimeUpdated = true;
    trackOverStateCatched = false;
  }

  string prepareTrackUrl(string trackurl) {
    if(trackurl.startsWith("https://")) return trackurl.replace("https://", "http://");
    else return trackurl;
  }

  void trackOver() {
    if (musicState) {
      dbm("catched trackOver");
      trackOverStateCatched = true;
      if (!repeatMode) trackNum++;
      int amountOfTracks = mplayer.shuffleMode ? win.savedShuffledLen : api.getServerCount(blockType.music);
      if (trackNum == amountOfTracks) {
        jumpToBeginning;
        mplayer.trackNum = win.active;
        mplayer.offset = win.scrollOffset;
      }

      vkAudio track;
      if (mplayer.shuffleMode)
        track = getShuffledMusic(1, trackNum)[0];
      else
        track = api.getBufferedMusic(1, trackNum)[0];

      loadFile(track.url);
      currentTrack = Track(track.artist, track.title, track.duration_str, "", track.id.to!string, track.duration_sec);
    }
    playtimeUpdated = true;
  }

  ListElement[] getMplayerUI(int cols) {
    ListElement[] playerUI;
    auto fcols = cols-16;
    if (currentTrack.artist.utfLength / 2 > fcols / 2) currentTrack.artist = currentTrack.artist[0..fcols-4];
    if (currentTrack.title.utfLength / 2 > fcols / 2) currentTrack.title = currentTrack.title[0..fcols-4];
    auto artistrepl = fcols / 2 - currentTrack.artist.utfLength / 2;
    auto titlerepl = fcols / 2 - currentTrack.title.utfLength / 2;

    if (fcols < 1) fcols = cols;
    if (artistrepl < 1) artistrepl = 1;
    if (titlerepl < 1) titlerepl = 1;

    playerUI ~= ListElement(" ".replicate(artistrepl)~currentTrack.artist);
    playerUI ~= ListElement(" ".replicate(titlerepl)~currentTrack.title);
    playerUI ~= ListElement(center(currentTrack.playtime ~ " / " ~ currentTrack.duration, fcols, ' '));
    playerUI ~= ListElement(center("[" ~ realProgress ~ "]", fcols, ' '));
    return playerUI;
  }

  bool sameTrack(int position) {
    vkAudio track;
    if (mplayer.shuffleMode)
      track = getShuffledMusic(1, position)[0];
    else
      track = api.getBufferedMusic(1, position)[0];
    return currentTrack.id == track.id.to!string;
  }
}

class mpv: Thread {

  enum ipcCmd {
    timeset,
    seek,
    pause,
    exit,
    load
  }

  struct ipcCmdParams {
    ipcCmd command;
    string strargument;
    real realargument;
  }

  string
    socketArgument = "--input-unix-socket=", //compatibility with older versions, changed to "--input-ipc-server=" in mpv 0.17.0
    socketPath,
    playerExec;

  const
    int
      endRequestId = 3,
      posPropertyId = 1,
      idlePropertyId = 2;

  alias posCallback = void delegate(real sec);
  alias endCallback = void delegate();

  posCallback posChanged;
  endCallback endReached;

  __gshared {
    string commandTemplate = "{ \"command\": [] }";
    Thread endChecker;
    Socket comm;
    Address commAddr;
    Pid pid = null;
    bool
      isInit,
      playerExit,
      notFirstPlay,
      musicState;
  }


  this(posCallback pos, endCallback end) {
    posChanged = pos;
    endReached = end;

    socketPath = getPlayerSocketName();
    playerExec = "mpv --idle --no-audio-display " ~ socketArgument ~ socketPath ~ " > /dev/null 2> /dev/null";

    super(&runPlayer);
  }


  private void req(string cmd) {
    //dbm("mpv <- " ~ cmd);
    if(!isInit) {
      dbm("mpv - req: noinit");
      return;
    }

    //dbm("mpv - req cmd: " ~ cmd);

    auto s_answ = comm.send(cmd ~ "\n");
    if(s_answ == Socket.ERROR) {
      dbm("mpv - req: s_answ error");
      return;
    }

  }

  private void mpvsend(ipcCmdParams c) {

    JSONValue cm = parseJSON(commandTemplate);

    switch(c.command) {
      case ipcCmd.seek:
        cm.object["command"].array ~= JSONValue("seek");
        cm.object["command"].array ~= JSONValue(c.realargument);
        cm.object["command"].array ~= JSONValue(c.strargument);
        break;
      case ipcCmd.timeset:
        cm.object["command"].array ~= JSONValue("set_property");
        cm.object["command"].array ~= JSONValue("playback-time");
        cm.object["command"].array ~= JSONValue(c.realargument);
        break;
      case ipcCmd.pause:
        cm.object["command"].array ~= JSONValue("set_property");
        cm.object["command"].array ~= JSONValue("pause");
        cm.object["command"].array ~= JSONValue(musicState);
        break;
      case ipcCmd.exit:
        cm.object["command"].array ~= JSONValue("quit");
        break;
      case ipcCmd.load:
        cm.object["command"].array ~= JSONValue("loadfile");
        cm.object["command"].array ~= JSONValue(c.strargument);
        break;
      default: assert(0);
    }

    dbm("mpv - cmd: " ~ c.command.to!string);

    req(cm.toString());
  }

  private void mpvhandle(string rc) {
    try {
      //dbm("mpv: " ~ rc);
      auto m = parseJSON(rc);
      if(m.type != JSON_TYPE.OBJECT) return;
      if(
        "request_id" in m &&
        m["request_id"].integer == endRequestId &&
        "data" in m &&
        m["data"].type == JSON_TYPE.TRUE
      ) {
        endReached();
      }
      else if("error" in m) {
        if(m["error"].str != "success") {
          dbm("mpv - error: " ~ rc);
        }
      }
      else if("event" in m) {
        auto e = m["event"].str;

        switch(e) {
          case "property-change":
            auto eid = m["id"].integer.to!int;
            if(eid == posPropertyId) posChanged(m["data"].floating.to!real);
            break;
          default: break;
        }

      }
    }
    catch(JSONException e) {
      dbm("mpv - json exception: " ~ e.msg);
    }
  }

  private void checkEnd() {
    logThread("check_end watchdog");
    while(!playerExit) {
      Thread.sleep(dur!"msecs"(500));
      if(musicState) req(checkEndCmd().toString());
    }
  }

  private JSONValue checkEndCmd() {
    JSONValue c = parseJSON("{ \"command\": [], \"request_id\": 0 }");
    c["command"].array ~= JSONValue("get_property");
    c["command"].array ~= JSONValue("idle-active");
    c["request_id"].integer = endRequestId;
    return c;
  }

  private JSONValue observePropertyCmd(int id, string prop) {
    auto c = parseJSON(commandTemplate);
    c["command"].array ~= JSONValue("observe_property");
    c["command"].array ~= JSONValue(id);
    c["command"].array ~= JSONValue(prop);
    return c;
  }

  private void setup() {
    req(observePropertyCmd(posPropertyId, "playback-time").toString());
    //req(observePropertyCmd(idlePropertyId, "end-file").toString());
    endChecker = new Thread(&checkEnd);
    endChecker.start();
  }

  void killPlayer() {
    int code = -1;
    if(pid !is null){
      code = kill(pid.processID + 1 , SIGTERM);
    }
  }

  void runPlayer() {
    logThread("runplayer");
    dbm("mpv - starting");
    auto pipe = pipeProcess("sh", Redirect.stdin);
    pipe.stdin.writeln(playerExec);
    pipe.stdin.flush;
    pid = pipe.pid;
    Thread.sleep(dur!"msecs"(500)); //wait for init
    dbm("mpv - running");

    uint spawntry;
    while(!std.file.exists(socketPath)) {
        dbm("mpv - waiting for socket spawn...");
        Thread.sleep(dur!"msecs"(400));

        ++spawntry;
        if(spawntry >= 25) {
            dbm("mpv - socket connection failed");
            return;
        }
    }
    commAddr = new UnixAddress(socketPath);
    comm = new Socket(AddressFamily.UNIX, SocketType.STREAM);
    comm.connect(commAddr);
    dbm("mpv - socket connected");

    isInit = true;
    long r_answ = -1;
    setup();

    while( r_answ != 0 ){
      char[1024] recv;
      string recv_str;
      r_answ = comm.receive(recv);
      if(r_answ != 0) {
        foreach(r; recv) {
          if(r == '\n' || r == '\x00') break;
          recv_str ~= r;
        }
        //dbm("mpv - recv: " ~ recv_str);
        mpvhandle(recv_str);
        Thread.sleep( dur!"msecs"(100) );
      }
    }

    dbm("PLAYER EXIT");
    playerExit = true;
  }

  void pause() {
    auto c = ipcCmdParams(ipcCmd.pause);
    mpvsend(c);
    musicState = !musicState;
  }

  bool getMusicState() {
    return musicState;
  }

  bool isPlayerExit() {
    return playerExit;
  }

  bool isPlayerInit() {
    return isInit;
  }

  void exit() {
    auto c = ipcCmdParams(ipcCmd.exit);
    mpvsend(c);
    Thread.sleep(dur!"msecs"(100));
    try {
        std.file.remove(socketPath);
    }
    catch(std.file.FileException e) {
        //dbm("socket not found")
    }
  }

  void loadfile(string p) {
    auto c = ipcCmdParams(ipcCmd.load, p);
    mpvsend(c);
    if(!notFirstPlay) {
      musicState = true;
      notFirstPlay = true;
    }
  }

  void setPosition(real sec) {
    auto c = ipcCmdParams(ipcCmd.timeset, "", sec);
    mpvsend(c);
  }

  void relativeSeek(real rval, bool percent) {
    auto mode = percent ? "relative-percent" : "relative";
    auto c = ipcCmdParams(ipcCmd.seek, mode, rval);
    mpvsend(c);
  }

}


================================================
FILE: source/namecache.d
================================================
/*
Copyright 2016 HaCk3D, substanceof

https://github.com/HaCk3Dq
https://github.com/substanceof

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

module namecache;

import std.stdio, std.conv, std.algorithm, std.array;
import vkapi, utils;

struct cachedName {
    string first_name;
    string last_name;
    bool online;
}

struct nameCacheStorage {
    cachedName[int] cache;
}

string strName(cachedName inp) {
    auto ln = inp.last_name;
    auto rt = inp.first_name;
    if(ln.length != 0) rt ~= " " ~ ln;
    return rt;
}

__gshared auto nc = nameCacheStorage();

class nameCache {
    VkApi api;
    int[] order;

    this(VkApi api) {
        this.api = api;
    }

    static void defeatNameCache() {
        nc = nameCacheStorage();
    }

    void requestId(int[] ids) {
        order ~= ids;
        dbm("requestId ids: " ~ ids.length.to!string ~ " order: " ~ order.length.to!string);
    }

    void requestId(int id) {
        order ~= id;
    }

    void addToCache(int id, cachedName name){
        nc.cache[id] = name;
    }

    void setOnline(int id, bool online) {
        auto c = id in nc.cache;
        if(c) {
            c.online = online;
        }
    }

    bool getOnline(int id, bool forced = false) {
        auto c = id in nc.cache;
        if(c) return c.online;
        else if (forced) return getName(id).online;
        else return false;
    }

    cachedName getName(int id) {
        if(id in nc.cache) {
            return nc.cache[id];
        }
        dbm("got non-cached name!");
        try {
            
            cachedName rt;
            int fid;
            if(id < 0) {
                dbm("got community id");
                auto c = api.groupsGetById([ id ]);
                if(c.length == 0) {
                    return cachedName("community", id.to!string);
                } else {
                    fid = c[0].id;
                    rt = cachedName(c[0].name, " ", true);
                }
            } else {
                auto resp = api.usersGet(id);
                fid = resp.id;
                rt = cachedName(resp.first_name, resp.last_name, resp.online);
            }
            nc.cache[fid] = rt;
            return rt;
        } catch (ApiErrorException e) {
            if (e.errorCode == 6) {
                dbm("too many requests, returning default name");
                return cachedName("default", "name");
            } else {
                //rethrow
                throw e;
            }
        }
    }

    void resolveNames() {
        dbm("start name resolving, order length: " ~ order.length.to!string);
        if(order.length == 0) return;
        int[] clean = order
                        .filter!(q => q !in nc.cache)
                        .array;

        const int max = 1000;
        int n = 0;
        int len = clean.length.to!int;
        bool cnt = true;
        while(cnt) {
            int d = len - n;
            int[] buf;
            if(d > max) {
                int up = n+max+1;
                buf = clean[n..up];
                n += max;
            } else {
                int up = n+d;
                buf = clean[n..up];
                cnt = false;
            }
            foreach(nm; api.usersGet( buf.filter!(d => d > 0 || d < mailStartId).array )) { //users
                nc.cache[nm.id] = cachedName(nm.first_name, nm.last_name, nm.online);
            }
            foreach(cnm; api.groupsGetById( buf.filter!(d => d < 0 && d > mailStartId).array )) { //communities
                nc.cache[cnm.id] = cachedName(cnm.name, " ", true);
            }
        }
        order = new int[0];
        dbm("cached " ~ nc.cache.length.to!string ~ " names, order length: " ~ order.length.to!string);
    }

}




================================================
FILE: source/storedFunctions
================================================
/*
Copyright 2016 HaCk3D, substanceof

https://github.com/HaCk3Dq
https://github.com/substanceof

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

//This file contains source code of stored execute.* vk.com functions


execute.vkGetDialogs {
    //returns dialogs with online fields

    var m = API.messages.getDialogs({"count": Args.count, "offset": Args.offset});
    var uids = m.items@.message@.user_id;
    var onl = API.users.get({"user_ids": uids, "fields": "online"});
    return {"conv": m, "ou": onl@.id, "os": onl@.online};
}


execute.accountInit {
    var me = API.users.get();
    var c = API.account.getCounters("messages");
    if(c.messages == null) {
        c.messages = 0;
    }

    var sc = [];
    sc.dialogs = API.messages.getDialogs().count;
    sc.friends = API.friends.get().count;
    sc.audio = API.audio.get().count;

    API.stats.trackVisitor();

    return {"me": me[0], "counters": c, "sc": sc};
}



================================================
FILE: source/utils.d
================================================
/*
Copyright 2016 HaCk3D, substanceof

https://github.com/HaCk3Dq
https://github.com/substanceof

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

module utils;

import std.stdio, std.array, std.range, std.string, std.file, std.random;
import std.datetime, std.conv, std.algorithm, std.utf, std.typecons;
import std.process, core.thread, core.sync.mutex, core.exception;
import core.sys.posix.signal;
import localization, app, vkversion, musicplayer;

const bool
    loggingEnabled = true,
    debugMessagesEnabled = false,
    showTokenInLog = false;

__gshared {
    File dbgff;
    File dbglat;
    bool dbmfe = loggingEnabled;
    string dbmlog = "";
    string vkcliTmpDir = "/tmp/vkcli-tmp";
    string vkcliLogDir = "/tmp/vkcli-log";
    string vkcliTmpMsgFile = "/tmp/vkcli-tmp/messagetext";
    string dbgfname = "vklog";
    string dbglatest = "-latest";
    string mpvsck = "vkmpv-socket-";
    string mpvsocketName;
    string logName;
    string logPath;
    Mutex dbgmutex;
}

private void appendDbg(string app) {
    synchronized(dbgmutex) {
        append(logPath, app);
    }
}

string toTmpDateString(SysTime t) {
    return t.day().to!string
            ~ t.month().to!string
            ~ t.year().to!string
            ~ "-"
            ~ t.hour().tzr ~ ":"
            ~ t.minute().tzr ~ ":"
            ~ t.second().tzr
            ~ "-"
            ~ t.timezone().stdName();
}

string getPlayerSocketName() {
    if(mpvsocketName == "") throw new Exception("bad player socket name");
    return vkcliTmpDir ~ "/" ~ mpvsocketName;
}

void initdbm() {
    auto ctime = Clock.currTime();
    dbgmutex = new Mutex();
    logName = dbgfname ~ "_" ~ ctime.toTmpDateString();
    logPath = vkcliLogDir ~ "/" ~ logName;
    mpvsocketName = mpvsck ~ genStr(8);

    if(!exists(vkcliTmpDir)) mkdir(vkcliTmpDir);
    if(!exists(vkcliLogDir)) mkdir(vkcliLogDir);

    if(dbmfe) {
        string logIntro = "vk-cli " ~ currentVersion ~ " log\n" ~ ctime.toSimpleString() ~ "\n";

        auto touchResult = executeShell("umask 0177\ntouch " ~ logPath);
        if(touchResult.status != 0) {
            auto ecode = touchResult.status.to!string;
            writeln("touch failed (" ~ ecode ~ ") - logging disabled");
            dbmfe = false;
        }

        dbgff = File(logPath, "w");
        dbgff.write(logIntro);
        dbgff.close();
    }
}

void dbm(string msg) {
    if(debugMessagesEnabled) writeln("[debug]" ~ msg);
    if(dbmfe) appendDbg(msg ~ "\n");
}

void dropClient(string msg) {
    Exit(msg);
}

string tzr(int inpt) {
    auto r = inpt.to!string;
    if(inpt > -1 && inpt < 10) return ("0" ~ r);
    else return r;
}

string vktime(SysTime ct, long ut) {
    auto t = SysTime(unixTimeToStdTime(ut));
    return (t.dayOfGregorianCal == ct.dayOfGregorianCal) ?
            (tzr(t.hour) ~ ":" ~ tzr(t.minute)) :
                (tzr(t.day) ~ "." ~ tzr(t.month) ~ ( t.year != ct.year ? "." ~ t.year.to!string[$-2..$] : "" ) );
}

string agotime (SysTime ct, long ut) { //not used
    auto pt = SysTime(ut.unixTimeToStdTime);
    auto ctm = ct.hour*60 + ct.minute;
    auto ptm = pt.hour*60 + pt.minute;
    auto tmdelta = ctm - ptm;
    const threshld = 240;

    if(
        pt.dayOfGregorianCal == ct.dayOfGregorianCal &&
        tmdelta < threshld
    ) {
        string rt;
        if(tmdelta > 60) {
            auto m = tmdelta % 60;
            auto h = (tmdelta-m) / 60;
            rt ~= h.to!string ~
             ( h == 1 ? getLocal("time_hour") : ( h > 0 && h < 5 ? getLocal("time_hours_l5") : getLocal("time_hours") ) );
            if(m != 0) rt ~= " " ~ m.to!string ~
             ( m == 1 ? getLocal("time_minute") : ( m > 0 && m < 5 ? getLocal("time_minutes_l5") : getLocal("time_minutes") ) );
        }
        else if(tmdelta == 1) rt = tmdelta.to!string ~ getLocal("time_minute");
        else rt = tmdelta.to!string ~ getLocal("time_minutes");

        return rt ~ getLocal("time_ago");
    }
    else return vktime(ct, ut);
}

/*
 local["time_minutes"] = lang(" minutes ago", " минут назад");
  local["time_minutes_l5"] = lang(" minutes ago", " минуты назад");
  local["time_minute"] = lang(" minute", " минуту");
  local["time_hours"] = lang(" hours", " часов");
  local["time_hours_l5"] = lang(" hours", " часа");
  local["time_hour"] = lang(" hour", " час");
  local["time_ago"] = lang(" ago" , " назад");
  local["lastseen"] = lang("last seen at ", "был в сети в ");
  */

string longpollReplaces(string inp) {
    return inp
        .replace("<br>", "\n")
        .replace("&quot;", "\"")
        .replace("&lt;", "<")
        .replace("&gt;", ">")
        .replace("&amp;", "&");
}

T[] slice(T)(ref T[] src, int count, int offset) {
    try {
        return src[offset..(offset+count)]; //.map!(d => &d).array;
    } catch (RangeError e) {
        dbm("utils slice count: " ~ count.to!string ~ ", offset: " ~ offset.to!string);
        dbm("catched slice ex: " ~ e.msg);
        return [];
    }
}

S[] wordwrap(S)(S s, size_t mln) {
    auto wrplines = s.wrap(mln).split("\n");
    S[] lines;
    foreach(ln; wrplines) {
        S[] crp = ["", ln];
        while(crp.length > 1) {
            crp = cropstr(crp[1] ,mln);
            lines ~= crp[0];
        }
    }
    return lines[0..$-1];
}

private S[] cropstr(S)(S s, size_t mln) {
    if(s.length > mln) return [ s[0..mln], s[mln..$] ];
    else return [s];
}

class JoinerBidirectionalResult(RoR)
if (isBidirectionalRange!RoR && isBidirectionalRange!(ElementType!RoR))
{

    alias rortype = ElementType!RoR;

    private {
        RoR range;
        rortype
            rfront = null,
            rback = null;
}

    this(RoR r) {
        range = r;
    }

    private void prepareFront() {
        if(range.empty) return;
        while(range.front.empty) {
            range.popFront();
            if(range.empty) return;
        }
        rfront = range.front;
    }

    private void prepareBack() {
        if(range.empty) return;
        while(range.back.empty) {
            range.popBack();
            if(range.empty) return;
        }
        rback = range.back;
    }

    @property bool empty() {
        return range.empty;
    }

    @property auto front() {
        if(rfront is null) prepareFront();
        assert(!empty);
        assert(!rfront.empty);
        return rfront.front;
    }

    @property auto back() {
        if(rback is null) prepareBack();
        assert(!empty);
        assert(!rback.empty);
        return rback.back;
    }

    void popFront() {
        if(rfront is null) prepareFront();
        else {
            rfront.popFront();

            if(rfront.empty) {
                range.popFront();
                prepareFront();
            }
        }
    }

    void popBack() {
        if(rback is null) prepareBack();
        else {
            rback.popBack();
            if(rback.empty) {
                range.popBack();
                prepareBack();
            }
        }
    }

    auto moveBack() {
        return back;
    }

    auto save() {
        return this;
    }

}

auto joinerBidirectional(RoR)(RoR range) {
    return new JoinerBidirectionalResult!RoR(range);
}

auto takeBackArray(R)(R range, size_t hm) {
    ElementType!R[] outr;
    size_t iter;
    while(iter < hm && !range.empty) {
        outr ~= range.back();
        range.popBack();
        ++iter;
    }
    reverse(outr);
    return outr;
}

class InputRetroResult(R)
if (isInputRange!R)
{
    private R rng;

    this(R range) {
        rng = range;
    }

    void popFront() {
        rng.popFront();
    }

    void popBack() {
        rng.popFront();
    }

    auto front() {
        return rng.front;
    }

    auto back() {
        return rng.front;
    }

    auto empty() {
        return rng.empty;
    }

    auto moveBack() {
        return back;
    }

    auto save() {
        return this;
    }

}

auto inputRetro(R)(R range) {
    return new InputRetroResult!R(range);
}

void logThread(string thrname = "") {
    if(thrname != "") dbm("thread started for: " ~ thrname);
}

void unwantedExit(int sig) {
    Exit("killed by signal " ~ sig.to!string, 2);
}

void writeCurrentTrack(int sig) {
    auto file = File(vkcliTmpDir ~ "/current-track", "w");
    auto track = mplayer ? mplayer.currentTrack : Track();
    file.write("[" ~ track.playtime ~ "/" ~ track.duration ~ "] " ~ track.artist ~ " - " ~ track.title);
}

void setPosixSignals() {
    version(posix) {
        sigset(SIGSEGV, a => unwantedExit(a));
        sigset(SIGUSR1, a => writeCurrentTrack(a));
    }
}

int gcSuspendSignal;
int gcResumeSignal;

void updateGcSignals() {
    version(linux) {
      gcSuspendSignal = SIGRTMIN;
      gcResumeSignal = SIGRTMIN+1;
      
      thread_term();
      thread_setGCSignals(gcSuspendSignal, gcResumeSignal);
      thread_init();
      dbm("GC signals: " ~ gcSuspendSignal.to!string ~ " " ~ gcResumeSignal.to!string);
    }
}


const uint maxuint = 4_294_967_295;
const uint maxint = 2_147_483_647;
const uint ridstart = 1;

int genId() {
    long rnd = uniform(ridstart, maxuint);
    if(rnd > maxint) {
        rnd = -(rnd-maxint);
    }
    dbm("rid: " ~ rnd.to!string);
    return rnd.to!int;
}

string genStrDict = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890";

string genStr(uint strln) {
    string output;
    for(uint i; i < strln; ++i) {
        size_t rnd = uniform!"[)"(0, genStrDict.length);
        output ~= genStrDict[rnd];
    }
    return output;
}

alias Repldchar = std.typecons.Flag!"useReplacementDchar";
const Repldchar repl = Repldchar.yes;

wstring toUTF16wrepl(in char[] s) {
    wchar[] r;
    size_t slen = s.length;

    r.length = slen;
    r.length = 0;
    for (size_t i = 0; i < slen; )
    {
        dchar c = s[i];
        if (c <= 0x7F)
        {
            i++;
            r ~= cast(wchar)c;
        }
        else
        {
            c = decode!repl(s, i);
            encode(r, c);
        }
    }

    return cast(wstring)r;
}

string toUTF8wrepl(in wchar[] s) {
    char[] r;
    size_t i;
    size_t slen = s.length;

    r.length = slen;
    for (i = 0; i < slen; i++)
    {
        wchar c = s[i];

        if (c <= 0x7F)
            r[i] = cast(char)c;     // fast path for ascii
        else
        {
            r.length = i;
            while (i < slen)
                encode(r, decode!repl(s, i));
            break;
        }
    }

    return cast(string)r;
}

struct utf {
  ulong
    start, end;
  int spaces;
}

const utfranges = [
  utf(19968, 40959, 1),
  utf(12288, 12351, 1),
  utf(11904, 12031, 1),
  utf(13312, 19903, 1),
  utf(63744, 64255, 1),
  utf(12800, 13055, 1),
  utf(13056, 13311, 1),
  utf(12736, 12783, 1),
  utf(12448, 12543, 1),
  utf(12352, 12447, 1),
  utf(110592, 110847, 1),
  utf(65280, 65519, 1)
  ];

uint utfLength(string inp) {
    uint s = 0;
    size_t inplen = inp.length;

    for (size_t i = 0; i < inplen; ) {
        auto ic = inp[i];
        ulong c;
        ++s;

        if(ic <= 0x7F) {
            c = cast(ulong)ic;
            ++i;
        }
        else {
            c = cast(ulong)(decode!repl(inp, i));
        }

        foreach (r; utfranges) {
            if (c >= r.start && c <= r.end) {
                s += r.spaces;
                break;
            }
        }
    }
    return s;
}

S replicatestr(S)(S str, ulong n) {
    S outstr = "";
    for(ulong i = 0; i < n; ++i) {
        outstr ~= str;
    }
    return outstr;
}


string getMessageFromTmpFile() {
    string text = "";
    if (std.file.exists(vkcliTmpMsgFile))
	  text = cast(string)std.file.read(vkcliTmpMsgFile);
    std.file.write(vkcliTmpMsgFile, "");
    return text;
}


================================================
FILE: source/vkapi.d
================================================
/*
Copyright 2016 HaCk3D, substanceof

https://github.com/HaCk3Dq
https://github.com/substanceof

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

module vkapi;

import std.stdio, std.conv, std.string, std.regex, std.array, std.datetime, std.random, core.time;
import std.exception, core.exception, std.process;
import std.net.curl, std.uri, std.json;
import std.range, std.algorithm;
import std.parallelism, std.concurrency, core.thread, core.sync.mutex;
import utils, namecache, localization;
import magicstringz;



// ===== vkapi const =====

const int convStartId = 2000000000;
const int mailStartId = convStartId*-1;
const int longpollGimStartId = 1000000000;
const bool return80mc = true;
const long needNameMaxDelta = 180; //seconds, 3 min
const int typingTimeout = 2;

const uint defaultBlock = 100;
const int chatBlock = 100;
const int chatUpd = 50;

// ===== networking const =====

const int connectionAttempts = 10;
const int mssleepBeforeAttempt = 600;
const int vkgetCurlTimeout = 6;
const int longpollCurlTimeout = 30;
const int longpollCurlAttempts = 5;
const string timeoutFormat = "seconds";

// ===== API objects =====

struct vkUser {
    string first_name;
    string last_name;
    int id;
    bool online;
}

struct vkDialog {
    string name;
    string lastMessage = "";
    int lastmid;
    int id = -1;
    int unreadCount;
    bool unread = false;
    bool outbox;
    bool online;
    bool isChat;
}

struct vkMessage {
    string author_name; // in format 'First Last'
    int author_id;
    int peer_id; // for users: user id, for conversations: 2000000000 + chat id
    int msg_id;
    bool outgoing;
    bool unread;
    bool needName;
    bool nmresolved;
    long utime; // unix time
    string time_str; // HH:MM (M = minutes), if >24 hours ago, then DD.mm (m = month)
    string[] body_lines; // Message text, splitted in lines (delimiter = '\n')
    int fwd_depth; // max fwd message deph (-1 if no fwd)
    vkFwdMessage[] fwd; // forwarded messages
    bool isLoading;
    bool isZombie;
    long rndid;
    int lineCount = -1;
    int wrap = -1;
}

struct vkMessageLine {
    string text;
    string time;
    bool unread;
    bool isName;
    bool isSpacing;
    bool isFwd;
    int fwdDepth;
}

auto emptyVkMessage = vkMessage();

struct vkFwdMessage {
    int author_id;
    string author_name;
    long utime;
    string time_str;
    string[] body_lines;
    vkFwdMessage[] fwd;
}

struct vkCounters {
    int friends = 0;
    int messages = 0;
    int notifications = 0;
    int groups = 0;
}

struct vkFriend {
    string first_name;
    string last_name;
    int id;
    long last_seen_utime;
    string last_seen_str;
    bool online;
}

struct vkAudio {
    int id;
    int owner;
    string artist;
    string title;
    int duration_sec;
    string duration_str; // MM:SS (len 5)
    string url;
}

struct vkAccountInit {
    int id;
    string
        first_name,
        last_name;
    uint
        c_messages,
        sc_dialogs,
        sc_friends,
        sc_audio;
}

struct vkGroup {
    int id;
    string name;
}

// ===== longpoll objects =====

struct vkLongpoll {
    string key;
    string server;
    int ts;
}

struct vkNextLp {
    int ts;
    int failed;
}

// === API state and meta =====

long[int] lasttypetimes;

struct apiState {
    bool lp80got = true;
    bool somethingUpdated;
    bool chatloading;
    bool showConvNotifies;
    int loadingiter = 0;
    string lastlp = "";
    uint countermsg = -1;
    sentMsg[long] sent; //by rid
    bool[int] unreadCountReview; //by peer
    bool shedForceUpdateFlag;
    bool shedForceUpdateSelfResolve;
}

struct ldFuncResult {
    bool success;
    int servercount = -1;
}

struct factoryData {
    int serverCount = -1;
    bool forceUpdate;
}

struct sentMsg {
    int rid;
    int peer;
    int author;
    sendState state = sendState.pending;
}

struct overridedLastseen {
    long last_seen_utime;
    string last_seen_str;
}

enum blockType {
    dialogs,
    music,
    friends,
    chat
}

enum sendState {
    pending,
    failed
}

struct apiFwdIter {
    vkFwdMessage[] fwd;
    int md;
}

__gshared {
    overridedLastseen[int] lsc; //by uid
    nameCache nc;
    apiState ps;
    Mutex
        sndMutex,
        pbMutex;
}


struct vkgetparams {
    bool setloading = true;
    int attempts = connectionAttempts;
    bool thrownf = false;
    bool notifynf = true;
}

class VkApi {

    private const string vkurl = "https://api.vk.com/method/";
    const string vkver = "5.50";
    private string vktoken;
    private bool isTokenValid_;
    private bool isInitAttemptFinished_;

    vkUser me;
    vkAccountInit initdata;

    alias nfnotifyfn = void delegate();
    nfnotifyfn connectionProblems;

    this(string token, nfnotifyfn nfnotify) {
        isTokenValid_ = true;
        isInitAttemptFinished_ = false;
        vktoken = token;
        connectionProblems = nfnotify;
    }
    
    bool isTokenValid() {
        return isTokenValid_;
    }
    
    bool isInitAttemptFinished() {
        return isInitAttemptFinished_;
    }

    void addMeNC() {
        nc.addToCache(me.id, cachedName(me.first_name, me.last_name));
    }

    void resolveMe() {
        isTokenValid_ = checkToken(vktoken);
    }

    private bool checkToken(string token) {
        try{
            initdata = executeAccountInit();
            me = vkUser(initdata.first_name, initdata.last_name, initdata.id);
        } catch (ApiErrorException e) {
            dbm("ApiErrorException: " ~ e.msg);
            if (e.errorCode == 5) {
                dbm("ApiError: Wrong token");
                return false;
            }
        }
        return true;
    }

    JSONValue vkget(string meth, string[string] params, bool dontRemoveResponse = false, vkgetparams gp = vkgetparams()) {
        if(gp.setloading) {
            enterLoading();
        }
        bool rmresp = !dontRemoveResponse;
        auto url = vkurl ~ meth ~ "?"; //so blue
        foreach(key; params.keys) {
            auto val = params[key];
            //dbm("up " ~ key ~ "=" ~ val);
            auto cval = val.encode.replace("+", "%2B");
            url ~= key ~ "=" ~ cval ~ "&";
        }
        url ~= "v=" ~ vkver ~ "&access_token=";
        if(!showTokenInLog) dbm("request: " ~ url ~ "***");
        url ~= vktoken;
        if(showTokenInLog) dbm("request: " ~ url);
        auto tm = dur!timeoutFormat(vkgetCurlTimeout);
        string got;

        bool htloop;
        while(!htloop) {
            try{
                got = AsyncMan.httpget(url, tm, gp.attempts);
                htloop = true;
            } catch(NetworkException e) {
                dbm(e.msg);
                if(gp.notifynf) connectionProblems();
                if(gp.thrownf) throw e;

                if(gp.notifynf) {
                    //dbm("vkget waits for api init..");
                    do {
                        Thread.sleep(dur!"msecs"(300));
                    } while(!isTokenValid);
                    //dbm("resume vkget");
                }
            }
        }

        JSONValue resp;
        try{
            resp = got.parseJSON;
            //dbm("json: " ~ resp.toPrettyString);
        }
        catch(JSONException e) {
            throw new ApiErrorException(resp.toPrettyString(), 0);
        }

        if(resp.type == JSON_TYPE.OBJECT) {
            if("error" in resp){
                try {
                    auto eobj = resp["error"];
                    immutable auto emsg = ("error_text" in eobj) ? eobj["error_text"].str : eobj["error_msg"].str;
                    immutable auto ecode = eobj["error_code"].integer.to!int;
                    throw new ApiErrorException(emsg, ecode);
                } catch (JSONException e) {
                    throw new ApiErrorException(resp.toPrettyString(), 0);
                }

            } else if ("response" !in resp) {
                rmresp = false;
            }
        } else rmresp = false;

        if(gp.setloading) leaveLoading();
        return rmresp ? resp["response"] : resp;
    }

    // ===== API method wrappers =====

    vkAccountInit executeAccountInit() {
        string[string] params;

        params["code"] =
`var me = API.users.get();
var c = API.account.getCounters("messages");
if(c.messages == null) {
    c.messages = 0;
}

var sc = [];
sc.dialogs = API.messages.getDialogs().count;
sc.friends = API.friends.get().count;
sc.audio = API.audio.get().count;

if(sc.audio == null) {
    sc.audio = 0;
}

return {"me": me[0], "counters": c, "sc": sc};`;

        vkgetparams gp = {notifynf: false};
        auto resp = vkget("execute", params, false, gp);
        vkAccountInit rt = {
            id:resp["me"]["id"].integer.to!int,
            first_name:resp["me"]["first_name"].str,
            last_name:resp["me"]["last_name"].str,
            c_messages:resp["counters"]["messages"].integer.to!int,
            sc_dialogs:resp["sc"]["dialogs"].integer.to!int,
            sc_friends:resp["sc"]["friends"].integer.to!int,
            sc_audio:resp["sc"]["audio"].integer.to!int
        };
        return rt;
    }

    vkUser usersGet(int userId = 0, string fields = "", string nameCase = "nom") {
        string[string] params;
        if(userId != 0) params["user_ids"] = userId.to!string;
        params["fields"] = fields != "" ? fields : "online";
        if(nameCase != "nom") params["name_case"] = nameCase;
        auto resp = vkget("users.get", params);

        if(resp.array.length != 1) throw new BackendException("users.get (one user) fail: response array length != 1");
        resp = resp[0];

        vkUser rt = {
            id:resp["id"].integer.to!int,
            first_name:resp["first_name"].str,
            last_name:resp["last_name"].str
        };

        if("online" in resp) rt.online = (resp["online"].integer == 1);

        return rt;
    }

    vkUser[] usersGet(int[] userIds, string fields = "", string nameCase = "nom") {
        if(userIds.length == 0) return [];
        string[string] params;
        params["user_ids"] = userIds.map!(i => i.to!string).join(",");
        params["fields"] = fields != "" ? fields : "online";
        if(nameCase != "nom") params["name_case"] = nameCase;
        auto resp = vkget("users.get", params).array;

        vkUser[] rt;
        foreach(t; resp){
            vkUser rti = {
                id:t["id"].integer.to!int,
                first_name:t["first_name"].str,
                last_name:t["last_name"].str
            };
            if("online" in t) rti.online = t["online"].integer == 1;
            rt ~= rti;
        }
        return rt;
    }

    void setActivityStatusImpl(int peer, string type) {
        vkgetparams gp = {
            setloading: false,
            attempts: 1,
            thrownf: true,
            notifynf: false
        };
        try{
            vkget("messages.setActivity", [ "peer_id": peer.to!string, "type": type ], false, gp);
        } catch (Exception e) {
            dbm("catched at setTypingStatus: " ~ e.msg);
        }
    }

    void accountSetOnline() {
        vkgetparams gp = {
            setloading: false,
            attempts: 10,
            thrownf: true,
            notifynf: false
        };
        string[string] emptyparam;
        try{
            vkget("account.setOnline", emptyparam, false, gp);
        } catch (Exception e) {
            dbm("catched at accountSetOnline: " ~ e.msg);
        }
    }

    void accountSetOffline() {
        vkgetparams gp = {
            setloading: false,
            attempts: 10,
            thrownf: false,
            notifynf: false
        };
        try{
            vkget("account.setOffline", [ "voip": "0" ], false, gp);
        } catch (Exception e) {
            dbm("catched at accountSetOnline: " ~ e.msg);
        }
    }

    vkDialog[] messagesGetDialogs(int count , int offset, out int serverCount) {
        string[string] params;

        params["code"] =
`var m = API.messages.getDialogs({"count": Args.count, "offset": Args.offset});
var uids = m.items@.message@.user_id;
var onl = API.users.get({"user_ids": uids, "fields": "online"});
return {"conv": m, "ou": onl@.id, "os": onl@.online};`;

        params["count"] = count.to!string;
        params["offset"] = offset.to!string;
        auto exresp = vkget("execute", params);
        auto resp = exresp["conv"];
        auto dcount = resp["count"].integer.to!int;
        dbm("dialogs count now: " ~ dcount.to!string);
        auto respt_items = resp["items"].array;
        auto respt = respt_items.map!(q => q["message"]);

        //name resolving
        int[] rootIds = respt
                        .filter!(q => "user_id" in q)
                        .map!(q => q["user_id"].integer.to!int)
                        .array;
        auto convAcvtives = respt
                        .filter!(q => "chat_active" in q)
                        .map!(q => q["chat_active"]);
        int[] convIds;
        foreach(ca; convAcvtives) {
            //dbm(ca.type.to!string);
            convIds ~= ca.array.map!(a => a.integer.to!int).array;
        }
        nc.requestId(rootIds);
        nc.requestId(convIds);
        nc.resolveNames();

        auto ou = exresp["ou"].array;
        auto os = exresp["os"].array;
        bool[int] online;
        foreach(n; 0..(ou.length)) online[ou[n].integer.to!int] = (os[n].integer == 1);

        vkDialog[] dialogs;
        foreach(ditem; respt_items){
            auto msg = ditem["message"];
            auto ds = vkDialog();
            if("chat_id" in msg){
                auto ctitle = msg["title"].str;
                auto cid = msg["chat_id"].integer.to!int + convStartId;
                nc.addToCache(cid, cachedName(ctitle, ""));

                ds.id = cid;
                ds.name = ctitle;
                ds.online = true;
                ds.isChat = true;
            } else {
                auto uid = msg["user_id"].integer.to!int;
                ds.id = uid;
                ds.name = nc.getName(ds.id).strName;
                ds.online = (uid in online) ? online[uid] : false;
                ds.isChat = false;
            }
            ds.lastMessage = msg["body"].str;
            ds.lastmid = msg["id"].integer.to!int;
            if(msg["read_state"].integer == 0) ds.unread = true;
            if(msg["out"].integer == 1) ds.outbox = true;
            if("unread" in ditem) ds.unreadCount = ditem["unread"].integer.to!int;
            dialogs ~= ds;
            //dbm(ds.id.to!string ~ " " ~ ds.unread.to!string ~ "   " ~ ds.name ~ " " ~ ds.lastMessage);
            //dbm(ds.formatted);
        }

        serverCount = dcount;
        return dialogs;
    }

    vkCounters accountGetCounters(string filter = "") {
        string ft = (filter == "") ? "friends,messages,groups,notifications" : filter;
        auto resp = vkget("account.getCounters", [ "filter": ft ]);
        vkCounters rt;

        if(resp.type == JSON_TYPE.ARRAY) return rt;

        foreach(c; resp.object.keys) switch (c) {
            case "messages": rt.messages = resp[c].integer.to!int; break;
            case "friends": rt.friends = resp[c].integer.to!int; break;
            case "notifications": rt.notifications = resp[c].integer.to!int; break;
            case "groups": rt.groups = resp[c].integer.to!int; break;
            default: break;
        }
        return rt;
    }

    int messagesCounter() {
        int u = ps.countermsg;
        if(ps.lp80got) {
            u = accountGetCounters("messages").messages;
            ps.countermsg = u;
            ps.lp80got = false;
        }
        return u;
    }

    vkFriend[] friendsGet(int count, int offset, out int serverCount, int user_id = 0) {
        auto params = [ "fields": "online,last_seen", "order": "hints"];
        if(user_id != 0) params["user_id"] = user_id.to!string;
        if(count != 0) params["count"] = count.to!string;
        if(offset != 0) params["offset"] = offset.to!string;

        auto resp = vkget("friends.get", params);
        serverCount = resp["count"].integer.to!int;


        auto ct = Clock.currTime();
        vkFriend[] rt;

        foreach(f; resp["items"].array) {
            auto last = "last_seen" in f ? f["last_seen"]["time"].integer.to!long : 0;

            //auto laststr = agotime(ct, last);
            //auto laststr = getLocal("lastseen") ~ vktime(ct, last);
            auto laststr = last > 0 ? vktime(ct, last) : getLocal("banned");

            vkFriend friend = {
              first_name: f["first_name"].str,
              last_name: f["last_name"].str,
              id: f["id"].integer.to!int,
              online: ( f["online"].integer.to!int == 1 ),
              last_seen_utime: last,  last_seen_str: laststr
            };

            nc.addToCache(friend.id, cachedName(friend.first_name, friend.last_name, friend.online));

            rt ~= friend;
        }

        return rt;
    }

    vkAudio[] audioGet(int count, int offset, out int serverCount, int owner_id = 0, int album_id = 0) {
        string[string] params;
        if(owner_id != 0) params["owner_id"] = owner_id.to!string;
        if(album_id != 0) params["album_id"] = album_id.to!string;
        if(count != 0) params["count"] = count.to!string;
        if(offset != 0) params["offset"] = offset.to!string;

        auto resp = vkget("audio.get", params);
        serverCount = resp["count"].integer.to!int;

        vkAudio[] rt;
        foreach(a; resp["items"].array) {
            int ad = a["duration"].integer.to!int;
            auto adm = ad.convert!("seconds", "minutes");
            auto ads = ad - (60*adm);

            vkAudio aud = {
                id: a["id"].integer.to!int, owner: a["owner_id"].integer.to!int,
                artist: a["artist"].str, title: a["title"].str, url: a["url"].str,
                duration_sec: ad, duration_str: (adm.to!string ~ ":" ~ tzr(ads.to!int))
            };
            rt ~= aud;
        }
        return rt;
    }

    vkGroup[] groupsGetById(int[] group_ids) {
        vkGroup[] rt;
        if(group_ids.length == 0) return rt;

        auto gids = group_ids.map!(g => (g * -1).to!string).join(",");
        //dbm("gids: " ~ gids);
        auto params = [ "group_ids": gids ];
        auto resp = vkget("groups.getById", params);

        if(resp.type != JSON_TYPE.ARRAY) return rt;

        foreach(g; resp.array) {
            rt ~= vkGroup(g["id"].integer.to!int * -1, g["name"].str);
        }
        return rt;
    }

    private apiFwdIter digFwd(JSONValue[] fwdp, SysTime ct, int mdp, int cdp) {
        int cmd = (cdp > mdp) ? cdp : mdp;
        vkFwdMessage[] rt;
        foreach(j; fwdp) {
            int fid = j["user_id"].integer.to!int;
            long ut = j["date"].integer.to!long;

            vkFwdMessage[] fw;
            if("fwd_messages" in j) {
                auto r = digFwd(j["fwd_messages"].array, ct, cmd, cdp+1);
                if(r.md > cmd) cmd = r.md;
                fw = r.fwd;
            }

            nc.requestId(fid);

            vkFwdMessage mm = {
                author_id: fid,
                utime: ut, time_str: vktime(ct, ut),
                body_lines: getmbody(j),
                fwd: fw
            };
            rt ~= mm;
        }
        return apiFwdIter(rt, cmd);
    }

    private void resolveFwdRecv(ref vkFwdMessage[] inp) {
        foreach(ref m; inp) {
            m.author_name = nc.getName(m.author_id).strName;
            if(m.fwd.length != 0) resolveFwdRecv(m.fwd);
        }
    }

    private void resolveFwdNames(ref vkMessage[] inp) {
        nc.resolveNames();
        inp.map!(q => q.fwd).filter!(q => q.length != 0).each!(q => resolveFwdRecv(q));
    }

    string action_prefix = " > ";
    private string[] getmbody(JSONValue m) {
        string[] mbody = m["body"].str.split("\n");
        if("attachments" in m) {
            foreach(att; m["attachments"].array) {
                switch(att["type"].str) {
                    case "photo":
                        JSONValue o = att["photo"].object;
                        int k = -1;
                        foreach(size; [75, 130, 604, 807, 1280, 2560])
                            if ("photo_" ~ to!string(size) in o)
                                k = size;
                        mbody ~= o["text"].str ~ " / " ~
                            SysTime.fromUnixTime(o["date"].integer).toSimpleString();
                        mbody ~= (o)["photo_" ~ to!string(k)].str;
                        break;
                    case "audio":
                        JSONValue o = att["audio"].object;
                        mbody ~= o["artist"].str ~ " - " ~ o["title"].str ~
                            format(" (%02d:%02d)", o["duration"].integer / 60, o["duration"].integer % 60);
                        mbody ~= o["url"].str.split("?extra")[0];
                        break;
                    case "doc":
                        JSONValue o = att["doc"].object;
                        mbody ~= o["title"].str ~ " (" ~ o["ext"].str ~ ", " ~ to!string(o["size"].integer) ~ "): " ~ o["url"].str;
                        break;
                    case "link":
                        JSONValue o = att["link"].object;
                        if (o["title"].str == o["url"].str)
                            mbody ~= o["url"].str;
                        else
                            mbody ~= o["title"].str ~ ": " ~ o["url"].str;
                        break;
                    case "sticker":
                        mbody ~= "Sticker: " ~ att["sticker"].object["photo_352"].str;
                        break;
                    case "video":
                        JSONValue o = att["video"].object;
                        mbody ~= o["title"].str ~ " (video): https://vk.com/video" ~ o["owner_id"].to!string ~ "_" ~ o["id"].to!string;
                        break;
                    case "wall":
                        JSONValue o = att["wall"].object;
                        mbody ~= o["text"].str ~ " (post): https://vk.com/wall" ~ o["from_id"].to!string ~ "_" ~ o["id"].to!string;
                        break;
                    default:
                        mbody ~= "Unsupported attachment: " ~ att["type"].str;
                        break;
                }
            }
        }

        if ("action" in m) {
            string action_user = action_prefix ~ nc.getName(m["user_id"].integer.to!int).strName;
            switch (m["action"].str) {
                case "chat_create":
                    mbody ~= action_user ~ " " ~ getLocal("c_create") ~ "\"" ~ m["action_text"].str ~ "\"";
                    break;
                case "chat_title_update":
                    mbody ~= action_user ~ " " ~ getLocal("c_title") ~ "\"" ~ m["action_text"].str ~ "\"";
                    break;
                case "chat_photo_update":
                    mbody ~= action_user ~ " " ~ getLocal("c_setphoto");
                    break;
                case "chat_photo_remove":
                    mbody ~= action_user ~ " " ~ getLocal("c_removephoto");
                    break;
                case "chat_invite_user":
                    if (m["action_mid"].integer > 0) {
                        if (m["action_mid"].integer == m["from_id"].integer)
                            mbody ~= action_user ~ " " ~ getLocal("c_inviteself");
                        else
                            mbody ~= action_user ~ " " ~ getLocal("c_invite") ~ nc.getName(to!int(m["action_mid"].integer)).strName;
                    }
                    else {
                        mbody ~= action_user ~ " " ~ getLocal("c_invite") ~ m["action_email"].str;
                    }
                    break;
                case "chat_kick_user":
                    if (m["action_mid"].integer > 0) {
                        if (m["action_mid"].integer == m["from_id"].integer)
                            mbody ~= action_user ~ " " ~ getLocal("c_kickself");
                        else
                            mbody ~= action_user ~ " " ~ getLocal("c_kick") ~ nc.getName(to!int(m["action_mid"].integer)).strName;
                    }
                    else {
                        mbody ~= action_user ~ " " ~ getLocal("c_kick") ~ m["action_email"].str;
                    }
                    break;
                default: break;
            }
        }
        return mbody;
    }

    private vkMessage[] parseMessageObjects(JSONValue[] items, SysTime ct) {
        vkMessage[] rt;
        int i = 0;
        foreach(m; items) {
            int fid;
            int mid = m["id"].integer.to!int;
            auto hascid = ("chat_id" in m);
            int uid = m["user_id"].integer.to!int;
            int pid = (hascid) ? (m["chat_id"].integer.to!int + convStartId) : uid;
            long rid = ("random_id" in m) ? m["random_id"].integer.to!long : 0;
            long ut = m["date"].integer.to!long;
            bool outg = (m["out"].integer.to!int == 1);
            bool rstate = (m["read_state"].integer.to!int == 1);
            //bool unr = (outg && !rstate);
            bool unr = !rstate;

            if("from_id" in m) {
                fid = m["from_id"].integer.to!int;
            } else {
                if(hascid || !outg) {
                    fid = uid;
                } else {
                    fid = me.id;
                }
            }

            string[] mbody = getmbody(m);
            string st = vktime(ct, ut);

            int fwdp = -1;
            vkFwdMessage[] fw;
            if("fwd_messages" in m) {
                auto r = digFwd(m["fwd_messages"].array, ct, 0, 1);
                fwdp = r.md;
                fw = r.fwd;
            }

            auto mo = vkMessage();
            mo.outgoing = outg; mo.unread = unr; mo.utime = ut;
            mo.author_name = nc.getName(fid).strName;
            mo.time_str = st; mo.body_lines = mbody;
            mo.fwd_depth = fwdp; mo.fwd = fw; mo.needName = true;
            mo.msg_id = mid; mo.author_id = fid; mo.peer_id = pid;
            mo.rndid = rid;

            rt ~= mo;
            ++i;
        }
        resolveFwdNames(rt);
        return rt;
    }

    vkMessage[] messagesGetHistory(int peer_id, int count, int offset, out int servercount, out int unreadcount, int start_message_id = -1, bool rev = false) {
        auto ct = Clock.currTime();
        auto params = [ "peer_id": peer_id.to!string ];
        if(count >= 0) params["count"] = count.to!string;
        if(offset != 0) params["offset"] = offset.to!string;
        if(start_message_id > 0) params["start_message_id"] = start_message_id.to!string;
        if(rev) params["rev"] = "1";

        auto resp = vkget("messages.getHistory", params);
        auto items = resp["items"].array;
        auto respuc = "unread" in resp;

        servercount = resp["count"].integer.to!int;
        unreadcount = respuc ? respuc.integer.to!int : 0;

        return parseMessageObjects(items, ct);
    }

    vkMessage[] messagesGetById(int[] mids) {
        auto ct = Clock.currTime();
        auto params = [ "message_ids": mids.map!(q => q.to!string).join(",") ];
        auto resp = vkget("messages.getById", params);
        auto items = resp["items"].array;

        return parseMessageObjects(items, ct);
    }

    int messagesSend(int pid, string msg, int rndid = 0, int[] fwd = [], string[] attaches = []) {
        if(msg.length == 0 && fwd.length == 0 && attaches.length == 0) return -1;

        vkgetparams gp = {
            setloading: true,
            attempts: 4,
            thrownf: true,
            notifynf: false
        };

        auto params = ["peer_id": pid.to!string ];
        params["message"] = msg;
        if(rndid != 0) params["random_id"] = rndid.to!string;
        if(fwd.length != 0) params["forward_messages"] = fwd.map!(q => q.to!string).join(",");
        if(attaches.length != 0) params["attachment"] = attaches.join(",");

        auto resp = vkget("messages.send", params, false, gp);
        return resp.integer.to!int;
    }

    void messagesMarkAsRead(int pid, int smid = 0) {
        auto params = ["peer_id": pid.to!string ];
        if(smid != 0) params["start_message_id"] = smid.to!string;

        auto resp = vkget("messages.markAsRead", params);
    }
}

class AsyncOrder : Thread {
    const int maxFails = 10;

    struct Task {
        int id;
        void delegate() dg;
    }

    private
        Task[] order;
        Mutex orderAccess;
        int failCount;
        string lastExp;
        string title;

    this(string t) {
        title = t;
        orderAccess = new Mutex();
        super(&procOrder);
    }

    void addToOrder(void delegate() d, int ordid) {
        synchronized(orderAccess) {
            immutable auto has = order.map!(q => q.id).canFind(ordid);
            if(!has) order ~= Task(ordid, d);
        }
    }

    bool hasId(int ordid) {
        return order.map!(q => q.id).canFind(ordid);
    }

    private void procOrder() {
        logThread("async_order " ~ title);
        dbm("procOrder start");
        while(true) {
            if(order.length != 0) {
                for (int i; i < order.length; ++i) {
                    try {
                        order[i].dg();
                    }
                    catch (Exception e) {
                        auto expstr = "procOrder " ~ title ~ " task: "
                              ~ order[i].id.to!string ~ " " ~ typeof(e).stringof ~ ": " ~ e.msg;
                        dbm(expstr);
                        lastExp = expstr;

                        ++failCount;
                        if(failCount > maxFails) {
                            dbm("too many errors in procOrder, performing exit");
                            dropClient("too many errors in procOrder, last: \n" ~ lastExp);
                        }
                    }
                }
                synchronized(orderAccess) {
                    order = [];
                }
            }
            Thread.sleep(dur!"msecs"(100));
        }
    }
}

class AsyncSingle : Thread {

    this(string t) {
        title = t;
        super(&func);
    }

    private {
        void delegate() dg;
        string title;
    }

    void startFunc(void delegate() d) {
        if(!this.isRunning) {
            dg = d;
            this.start();
        }
    }

    private void func() {
        logThread("async_single " ~ title);
        try {
            if(dg) dg();
        }
        catch (Exception e) {
            auto expstr = "procSingle " ~ title ~ " " ~ typeof(e).stringof ~ ": " ~ e.msg;
            dbm(expstr);
        }
    }

}

class AsyncMan {

    static string httpget(string addr, Duration timeout, uint attempts) {
        string content = "";
        auto client = HTTP();

        int tries = 0;
        bool ok = false;

        while (!ok) {
            try {
                client.method = HTTP.Method.get;
                client.url = addr;
                client.setUserAgent(appUserAgent);

                client.dataTimeout = timeout;
                client.operationTimeout = timeout;
                client.connectTimeout = timeout;

                client.onReceive = (ubyte[] data) {
                    auto sz = data.length;
                    content ~= (cast(immutable(char)*)data)[0..sz];
                    return sz;
                };
                client.perform();
                ok = true;
                //dbm("recv content: " ~ content);
            } catch (CurlException e) {
                ++tries;
                dbm("[attempt " ~ (tries.to!string) ~ "] network error: " ~ e.msg);
                if(tries >= attempts) {
                    throw new NetworkException("httpget");
                }
                Thread.sleep( dur!"msecs"(mssleepBeforeAttempt) );
            }
        }
        return content;
    }

    const string
        S_SELF_RESOLVE = "s_self_resolve",
        S_ONLINE_STATUS = "s_online_status",
        S_TYPING = "s_typing_",
        S_READ = "s_readm",
        O_LOADBLOCK = "o_loadblock",
        O_SENDMSG = "o_sendm";

    AsyncOrder[string] orders;
    AsyncSingle[string] singles;

    void orderedAsync(string orderkey, int ordid, void delegate() d) {
        auto so = orderkey in orders;
        if(!so) {
            orders[orderkey] = new AsyncOrder(orderkey);
            so = orderkey in orders;
        }

        so.addToOrder(d, ordid);
        if(!so.isRunning)
            so.start();
    }

    void singleAsync(string singlekey, void delegate() d) {
        auto ss = singlekey in singles;
        if(!ss) {
            singles[singlekey] = new AsyncSingle(singlekey);
            ss = singlekey in singles;
        }

        ss.startFunc(d);
    }

}


class Longpoll : Thread {

    private {
        VkMan man;
        VkApi api;
    }

    int longpollWait = 25;

    this(VkMan vkman) {
        man = vkman;
        api = man.api;
        super(&startSyncAsyncWrapper);
    }

    private void startSyncAsyncWrapper() {
        logThread("longpoll");
        startSync();
    }

    void startSync() {
        if(!api.isInitAttemptFinished()) {
            dbm("longpoll is waiting for api init...");
            while(!api.isInitAttemptFinished()) {
                Thread.sleep(dur!"msecs"(300));
            }
        }
        dbm("longpoll is starting...");
        while(true) {
            try {
                doLongpoll(getLongpollServer());
            }
            catch (InternalException e) {
                if(e.ecode == e.E_LPRESTART) {
                    dbm("Network error in longpoll, lp shutdown");
                    man.asyncAccountInit();
                    man.sheduleForceUpdate(false);
                    return;
                }
                else {
                    dbm("longpoll InternalException: " ~ e.msg);
                }
            }
            catch(Exception e) {
                dbm("longpoll exception: " ~ e.msg);
            }
            catch(Error e) {
                dbm("longpoll error exception: " ~ e.msg);
                dbm("longpoll exit");
                return;
            }
            dbm("longpoll is restarting...");
        }
    }


    vkLongpoll getLongpollServer() {
        auto resp = api.vkget("messages.getLongPollServer", [ "use_ssl": "1", "need_pts": "1" ]);
        vkLongpoll rt = {
            server: resp["server"].str,
            key: resp["key"].str,
            ts: resp["ts"].integer.to!int
        };
        return rt;
    }


    const bool longpollRethrow = true;

    void doLongpoll(vkLongpoll start) {
        auto tm = dur!timeoutFormat(longpollCurlTimeout);
        int cts = start.ts;
        auto mode = (2 + 128).to!string; //attaches + random_id
        auto wait = longpollWait.to!string;
        bool ok = true;
        dbm("longpoll works");
        while(ok) {
            try {
                man.doShedForceUpdate();
                if(cts < 1) break;
                string url = "https://" ~ start.server ~ "?act=a_check&key=" ~ start.key ~ "&ts=" ~ cts.to!string
                                                                                            ~ "&wait=" ~ wait
                                                                                            ~ "&mode=" ~ mode;
                auto resp = AsyncMan.httpget(url, tm, longpollCurlAttempts);
                immutable auto next = parseLongpoll(resp);
                if(next.failed != -1) {
                    dbm("longpoll got 'failed " ~ next.failed.to!string ~ "'");
                    if(next.failed == 1) {
                        dbm("requesting force update");
                        man.forceUpdateAll();
                    }
                    else if (next.failed == 4) {
                        dbm("unknown longpoll versoion  THEY'RE PURGED V0?");
                        throw new ApiErrorException("invalid longpoll version", -1);
                    }
                    else {
                        dbm("'key' expired or something");
                        dbm("requesting new lonpoll server");
                        ok = false;
                    }
                }
                cts = next.ts;
            }
            catch(NetworkException e) {
                dbm("longpoll can't get new events");
                throw new InternalException(InternalException.E_LPRESTART);
            }
        }
    }

    vkNextLp parseLongpoll(string resp) {
        JSONValue j = parseJSON(resp);
        vkNextLp rt;
        auto ct = Clock.currTime();
        auto failed = ("failed" in j ? j["failed"].integer.to!int : -1 );
        auto ts = ("ts" in j ? j["ts"].integer.to!int : -1 );
        if(failed == -1) {
            auto upd = j["updates"].array;
            dbm("new lp: " ~ j.toPrettyString());
            foreach(u; upd) {
                switch(u[0].integer.to!int) {
                    case 4: //new message
                        triggerNewMessage(u, ct);
                        break;
                    case 80: //counter update
                        if(return80mc) {
                            ps.countermsg = u[1].integer.to!int;
                        } else {
                            ps.lp80got = true;
                        }
                        man.toggleUpdate();
                        break;
                    case 6: //inbox read
                        triggerRead(u);
                        break;
                    case 7: //outbox read
                        triggerRead(u);
                        break;
                    case 8: //online\offline
                        triggerOnline(u, ct);
                        break;
                    case 9: //online\offline
                        triggerOnline(u, ct);
                        break;
                    default:
                        break;
                }
            }
        }
        resolveMidOrder();
        rt.ts = ts;
        rt.failed = failed;
        return rt;
    }

    alias processnmFunc = void delegate(vkMessage);

    processnmFunc[int] midResolveOrder;

    void resolveMidOrder() {
        if(midResolveOrder.length == 0) return;
        api.messagesGetById(midResolveOrder.keys)
                    .each!(q => midResolveOrder[q.msg_id](q));
        //midResolveOrder.clear(); - bad for ldc
        midResolveOrder = midResolveOrder.init;
        man.toggleUpdate();
    }

    void triggerNewMessage(JSONValue ui, SysTime ct) {
        auto u = ui.array;

        auto mid = u[1].integer.to!int;
        auto flags = u[2].integer.to!int;
        auto peer = u[3].integer.to!int;
        auto utime = u[4].integer.to!long;
        auto msg = u[6].str.longpollReplaces;
        auto att = u[7];
        long rndid = (u.length > 8) ? u[8].integer.to!long : 0;

        bool outbox = (flags & 2) == 2;
        bool unread = (flags & 1) == 1;
        bool hasattaches = att.object.keys.map!(a => (a == "fwd") || a.matchAll(r"attach.*")).any!"a";

        auto conv = (peer > convStartId);
        bool group = false;

        if(!conv && peer > longpollGimStartId) {
            peer = -(peer - longpollGimStartId);
            group = true;
        }

        auto from = conv ? att["from"].str.to!int : ( outbox ? api.me.id : peer );

        auto title = conv ? u[5].str : nc.getName(peer).strName;
        auto haspeer = (peer in man.chatFactory);

        if(!haspeer) {
            man.chatFactory[peer] = man.generateBF!ClMessage(ClMessage.getLoadFunc(peer, (u) => man.setUnreads(peer, u)));
            man.chatFactory[peer].data.forceUpdate = true;
        }

        auto cf = man.chatFactory[peer];

        auto processnm = delegate (vkMessage nmsg) {
            dbm("processnm");
            auto rid = nmsg.rndid;
            auto realmsg = cf.getLoadedObjects
                                        .filter!(q => !q.getObject.isZombie)
                                        .takeOne();
            if(!realmsg.empty) {
                auto lastm = realmsg.front.getObject;
                nmsg.needName = !(lastm.author_id == from && (utime-lastm.utime) <= needNameMaxDelta);
            }

            auto sent = rid in ps.sent;
            if(sent) {
                dbm("approved sent nm rid: " ~ rid.to!string);
                cf.removeZombie(rid);
                ps.sent.remove(rid);
            }
            cf.addBack(new ClMessage(nmsg));
            if(!outbox && unread) cf.unreadCount += 1;
        };

        auto df = man.dialogsFactory;

        if(!df.isOverrided(peer) && !df.isBlank()) {
            auto blockdlg = df.getLoadedObjects
                                .filter!(q => q.getPeer == peer)
                                .takeOne;
            auto uc = blockdlg.empty ? 0 : blockdlg.front.getObject.unreadCount;
            df.overrideDialog(new ClDialog(title, peer, uc, cf), ct.toUnixTime);
        }
        else df.overrideBump(peer, ct.toUnixTime);

        if(!hasattaches) {
            vkMessage lpnm = {
                author_id: from, peer_id: peer, msg_id: mid,
                outgoing: outbox, unread: unread, rndid: rndid,
                utime: utime, time_str: vktime(ct, utime),
                author_name: nc.getName(from).strName,
                body_lines: msg.split("\n"),
                fwd_depth: -1, needName: true
            };
            processnm(lpnm);
        } else {
            midResolveOrder[mid] = processnm;
        }

        if(from != api.me.id && ( ps.showConvNotifies ? true : !conv )) ps.lastlp = title ~ ": " ~ msg;
        man.toggleUpdate();
    }

    void triggerRead(JSONValue u) {
        bool inboxrd = (u[0].integer == 6);
        auto peer = u[1].integer.to!int;
        auto mid = u[2].integer.to!int;

        if(peer > longpollGimStartId && peer < convStartId) {
            peer = -(peer-longpollGimStartId);
        }

        dbm("rd trigger peer: " ~ peer.to!string ~ ", mid: " ~ mid.to!string ~ ", inbox: " ~ inboxrd.to!string);

        synchronized(pbMutex) {
            auto ch = peer in man.chatFactory;
            int unreadc;

            if(ch) {
                unreadc = ch.unreadCount;
                auto chl = ch.getLoadedObjects;
                chl
                  .map!(q => q.getObject.msg_id == mid)
                  .countUntil(true);


                while( !chl.empty && ( (chl.front !is null && chl.front.getObject.outgoing != inboxrd) ? chl.front.getObject.unread : true) ) {
                    auto mobj = chl.front.getObject;
                    if(mobj.outgoing != inboxrd) {
                        mobj.unread = false;
                        if(inboxrd) --unreadc;
                        chl.front.invalidateLineCache();
                    }
                    chl.popFront();
                }
                ch.unreadCount = unreadc < 0 ? 0 : unreadc;
            }
            else {
                auto dlone = man.dialogsFactory.getLoadedObjects
                    .map!(q => q.getObject)
                    .filter!(q => q.id == peer)
                    .takeOne();
                auto hasdlone = !dlone.empty;
                if(hasdlone) {
                   if(mid == dlone.front.lastmid) {
                        dlone.front.unreadCount = 0;
                        dlone.front.unread = false;
                   }
                }
            }

        }

        man.toggleUpdate();
    }

    void triggerOnline(JSONValue u, SysTime ct) {
        auto uid = u[1].integer.to!int * -1;
        auto flags = u[2].integer.to!int;
        auto event = u[0].integer.to!int;
        bool exit = (event == 9);
        if(!exit && event != 8) return;

        if(exit) {
            nc.setOnline(uid, false);
            //auto uct = ct.toUnixTime();
            //lsc[uid].last_seen_utime = uct; //todo check! Range Violation (override last seen)
            //lsc[uid].last_seen_str = vktime(ct, uct);
        }
        else nc.setOnline(uid, true);

        man.toggleUpdate();
    }

}

class OnlineNotifier : Thread {

    private {
        bool enabled = false;
        VkApi api;
        AsyncMan a;
        const int retryMin = 14;
    }

    this(VkApi wapi, AsyncMan wa) {
        api = wapi;
        a = wa;
        super(&onlineRoutine);
    }

    void tryStart() {
        if(!this.isRunning) this.start();
        else dbm("onlineNotifier running already");
    }

    void setOnlineSw(bool sw) {
        if(enabled == sw) return;
        enabled = sw;
        if(sw) {
            dbm("starting onlineNotifier...");
            tryStart();
        }
        else {
            a.singleAsync(a.S_ONLINE_STATUS, () => api.accountSetOffline());
            dbm("offline status sent (shed)");
            dbm("sheduled onlineNotifier shutdown");
            pragma(msg, "Reticulating splines...");
        }
    }

    private void onlineRoutine() {
        logThread("online_notifier");
        while(true) {
            if(enabled) {
                api.accountSetOnline();
                dbm("online status sent");
                Thread.sleep( dur!"minutes"(retryMin) );
            }
            else {
                dbm("onlineNotifier shutdown");
            }
        }
    }

}

class VkMan {

    alias ChatBlockFactory = BlockFactory!ClMessage;

    __gshared {
        VkApi api;
        vkUser* me;
        AsyncMan a;

        BlockFactory!ClDialog dialogsFactory;
        BlockFactory!ClFriend friendsFactory;
        BlockFactory!ClAudio musicFactory;
        ChatBlockFactory[int] chatFactory; //by peer

        Longpoll longpollThread;
        OnlineNotifier onlineThread;
    }

    this(string token) {
        a = new AsyncMan();
        api = new VkApi(token, &connectionProblems);
        baseInit();
        asyncAccountInit();
    }

    private void accountInit() {
        ps.countermsg = -1;
        dbm("asyncLongpoll called");
        asyncLongpoll();

        nc = new nameCache(api);

        selfResolve();
    }

    private void selfResolve(){
        api.resolveMe();
        if(!api.isTokenValid_) {
            toggleUpdate();
            return;
        }

        api.addMeNC();
        me = &(api.me);

        dialogsFactory.data.serverCount = api.initdata.sc_dialogs;
        friendsFactory.data.serverCount = api.initdata.sc_friends;
        musicFactory.data.serverCount = api.initdata.sc_audio;

        ps.countermsg = api.initdata.c_messages;

        toggleUpdate();
    }

    void setLongpollWait(int wait) {
        longpollThread.longpollWait = wait;
    }

    void connectionProblems() {

    }

    void forceUpdateAll(bool selfresolve = true) {
        if(selfresolve) a.singleAsync(a.S_SELF_RESOLVE, () => selfResolve());
        toggleForceUpdate(blockType.music);
        toggleForceUpdate(blockType.dialogs);
        toggleForceUpdate(blockType.friends);
        chatFactory.values.each!(q => q.data.forceUpdate = true);
    }

    void sheduleForceUpdate(bool selfresolve) {
        ps.shedForceUpdateFlag = true;
        ps.shedForceUpdateSelfResolve = selfresolve;
    }

    void doShedForceUpdate() {
        if(ps.shedForceUpdateFlag) {
            ps.shedForceUpdateFlag = false;
            forceUpdateAll(ps.shedForceUpdateSelfResolve);
        }
    }

    void asyncAccountInit() {
        a.singleAsync(a.S_SELF_RESOLVE, () => accountInit());
    }

    BlockFactory!T generateBF(T)(T[] delegate(VkApi, uint, uint, out int) ld, uint dwblock = defaultBlock) {
        return new BlockFactory!T(
            new BlockObjectParameters!T(api, dwblock, a, ld, &notifyBlockDownloadDone));
    }

    private void baseInit() {
        sndMutex = new Mutex();
        pbMutex = new Mutex();
        ps = apiState();

        longpollThread = new Longpoll(this);
        onlineThread = new OnlineNotifier(api, a);

        dialogsFactory = generateBF!ClDialog(ClDialog.getLoadFunc());
        friendsFactory = generateBF!ClFriend(ClFriend.getLoadFunc());
        musicFactory = generateBF!ClAudio(ClAudio.getLoadFunc());
    }

    bool isSomethingUpdated() {
        if(ps.somethingUpdated){
            ps.somethingUpdated = false;
            return true;
        }
        return false;
    }

    void toggleUpdate() {
        ps.somethingUpdated = true;
    }

    private auto getData(blockType tp) {
        switch(tp){
            case blockType.dialogs: return &(dialogsFactory.data);
            case blockType.friends: return &(friendsFactory.data);
            case blockType.music: return &(musicFactory.data);
            default: assert(0);
        }
    }

    void asyncLongpoll() {
        longpollThread.start();
    }

    void toggleForceUpdate(blockType tp) {
        getData(tp).forceUpdate = true;
        toggleUpdate();
    }

    void toggleChatForceUpdate(int peer) {
        chatFactory[peer].data.forceUpdate = true;
        toggleUpdate();
    }

    int getServerCount(blockType tp) {
        return getData(tp).serverCount;
    }

    bool isScrollAllowed(blockType tp) {
        return true;
    }

    bool isChatScrollAllowed(int peer) {
        return true;
    }

    bool isLoading() {
        return ps.loadingiter != 0;
    }

    int getChatServerCount(int peer) {
        auto c = peer in chatFactory;
        if(c) return c.data.serverCount;
        else return -1;
    }

    int getChatLineCount(int peer, int ww) {
        auto c = peer in chatFactory;
        if(c) return (*c).getServerLineCount(ww);
        else return -1;
    }

    string getLastLongpollMessage() {
        auto last = ps.lastlp;
        ps.lastlp = "";
        return last;
    }

    void notifyBlockDownloadDone() {
        toggleUpdate();
    }

    void sendOnline(bool state) {
        onlineThread.setOnlineSw(state);
    }

    void showConvNotifications(bool state) {
        ps.showConvNotifies = state;
    }

    vkFriend[] getBufferedFriends(int count, int offset) {
        return bufferedGet!vkFriend(friendsFactory, count, offset);
    }

    vkAudio[] getBufferedMusic(int count, int offset) {
        return bufferedGet!vkAudio(musicFactory, count, offset);
    }

    vkDialog[] getBufferedDialogs(int count, int offset) {
        return bufferedGet!vkDialog(dialogsFactory, count, offset);
    }

    void setUnreads(int peer, int uc) {
        auto cf = peer in chatFactory;
        if(cf) {
            cf.unreadCount = uc;
        }
    }

    vkMessageLine[] getBufferedChatLines(int count, int offset, int peer, int wrapwidth) {
        if(offset < 0) offset = 0;
        auto f = peer in chatFactory;
        if(!f) {
            chatFactory[peer] = generateBF!ClMessage(ClMessage.getLoadFunc(peer, (u) => setUnreads(peer, u)));
            f = peer in chatFactory;
            auto blockdlg = dialogsFactory.getLoadedObjects
                                .filter!(q => q.getPeer == peer)
                                .takeOne;
            auto uc = blockdlg.empty ? 0 : blockdlg.front.getObject.unreadCount;
        }

        /*dbm("bfcl p: " ~ peer.to!string ~ ", o: " ~ offset.to!string ~ ", c: " ~ count.to!string
                                ~ ", sc: " ~ f.getServerLineCount(wrapwidth).to!string ~ " sco: "
                                ~ f.data.serverCount.to!string);*/

        synchronized(pbMutex) {
            if(!f.prepare) return [];
            f.seek(0);

            f.isBlank();

            auto rt = (*f)
                    .filter!(q => q !is null)
                    .inputRetro
                    .map!(q => q.getLines(wrapwidth))
                    .joinerBidirectional
                    .dropBack(offset)
                    .takeBackArray(count);
            return rt;
        }
    }

    int messagesCounter() {
        return ps.countermsg;
    }

    private void sendMessageImpl(int rid, int peer, string msg) {
        try {
            auto sentmid = api.messagesSend(peer, msg, rid);
            dbm("message sent mid: " ~ sentmid.to!string ~ " rid: " ~ rid.to!string);
        }
        catch(Exception e) {
            dbm("catched at sendMessageImpl: " ~ e.msg);
            dbm("failed state will be set, rid: " ~ rid.to!string);
            setFailedState(rid, peer);
        }
    }

    void setFailedState(long rid, int peer) {
        auto cf = peer in chatFactory;
        if(cf) {
            auto z = cf.getZombie(rid, true);
            if(z !is null) {
                z.time_str = getLocal("sendfailed");
                toggleUpdate();
                dbm("failed state set for " ~ rid.to!string);
            }
        }
    }

    void asyncSendMessage(int peer, string msg) {
        auto rid = genId();
        auto aid = me.id;
        vkMessage zombie = {
            author_name: me.first_name ~ " " ~ me.last_name,
            author_id: aid, isZombie: true,
            body_lines: msg.split("\n"),
            time_str: getLocal("sending"),
            rndid: rid, msg_id: -1, outgoing: true, unread: true,
            peer_id: peer, utime: 1,
            needName: true, nmresolved: true
        };

        synchronized(pbMutex) {
            auto ch = peer in chatFactory;
            if(ch) {
                ch.addZombie(zombie);
            }
        }

        toggleUpdate();

        synchronized(sndMutex) {
            ps.sent[rid] = sentMsg(rid, peer, aid);
        }

        a.orderedAsync(a.O_SENDMSG, rid, () => sendMessageImpl(rid, peer, msg));
    }

    void asyncMarkMessagesAsRead(int pid, int smid = 0) {
        a.singleAsync(a.S_READ, () => api.messagesMarkAsRead(pid, smid));
    }

    void setTypingStatus(int peer) {
        auto thrid = a.S_TYPING ~ peer.to!string;
        long ctypetime = Clock.currTime().toUnixTime();
        if((peer in lasttypetimes) is null) lasttypetimes[peer] = 0;

        if (ctypetime - lasttypetimes[peer] >= cast(long)typingTimeout) {
          lasttypetimes[peer] = ctypetime;
          a.singleAsync(thrid, () {
            dbm("typing status set at " ~ to!string(ctypetime));
            api.setActivityStatusImpl(peer, "typing");
            //Thread.sleep(dur!"seconds"(typingTimeout)); //Commented out due to random freezes
          });
        }
    }

    R[] bufferedGet(R, T)(T factory, int count, int offset) {
        synchronized(pbMutex) {
            if(!factory.prepare) return [];
            factory.seek(offset);
            return factory
                    .filter!(q => q !is null)
                    .take(count)
                    .map!(q => *(q.getObject))
                    .array;
        }
    }

}

// ===== Model =====

abstract class ClObject(T) {

    private {
        private T obj;
    }

    bool ignored = false;

    this(T o) {
        obj = o;
    }

    T* getObject() {
        return &obj;
    }

}

class Block(T) {

    alias objectType = T;
    alias paramsType = BlockObjectParameters!T;

    private {
        uint
            ordernum,
            blocksz;
        int oid;
        bool filled;
        factoryData* fdata;
        paramsType params;
        AsyncMan a;
    }

    T[] block;

    this(uint ord, paramsType objparams, factoryData* fdt, int woid, bool filledblk = false) {
        params = objparams;
        a = params.asyncMan;
        blocksz = params.blockSize;
        ordernum = ord;
        fdata = fdt;
        filled = filledblk;
        oid = woid;
    }

    T[] getBlock() {
        downloadBlock();
        return block;
    }

    void downloadBlock(bool force = false) {
        if(!filled || force) a.orderedAsync(a.O_LOADBLOCK, oid, () {
            ldFuncResult res;
            block = params.downloadBlock(blocksz, blocksz*ordernum, res);
            if(res.success) {
                filled = true;
                fdata.serverCount = res.servercount;
                params.downloadNotify();
            }
        });
    }

    bool isFilled() {
        return filled;
    }

    uint getBlocksize() {
        return blocksz;
    }

    uint length() {
        return block.length.to!uint;
    }

    void addBack(T obj) {
        block = obj ~ block;
    }

    void addFront(T obj) {
        block ~= obj;
    }
}

class BlockObjectParameters(O) {

    alias downloader = O[] delegate(VkApi, uint, uint, out int);
    alias loadnotify = void delegate();

    downloader loadFunc;
    loadnotify loadNotifyFunc;
    AsyncMan asyncMan;
    uint blockSize;

    private {
        VkApi api;
    }

    this(VkApi vkapi, uint blocksize, AsyncMan asyncm, downloader ld, loadnotify ldn) {
        assert(blocksize != 0);
        loadFunc = ld;
        loadNotifyFunc = ldn;
        asyncMan = asyncm;
        blockSize = blocksize;
        api = vkapi;
    }

    O[] downloadBlock(uint c, uint o, out ldFuncResult r) {
        r = apiCheck(api);
        if(!r.success) return new O[0];
        return loadFunc(api, c, o, r.servercount);
    }

    void downloadNotify() {
        loadNotifyFunc();
    }

}

class BlockFactory(T) {

    alias paramsType = BlockObjectParameters!T;

    private {
        uint
            blocksz;
        int
            oid,
            iter,
            backiter = -1;
        bool
            blank = true;
        Block!T[uint] blockst;
        Block!T backBlock;
    }

    factoryData data;
    paramsType params;

    this(paramsType objectParams) {
        params = objectParams;
        blocksz = params.blockSize;
        iter = 0;
        data = factoryData();
        oid = genId();
        initBackBlock();
    }

    private void initBackBlock() {
        backBlock = new Block!T(-1, params, &data, oid, true);
    }

    uint objectCount() {
        return blockst.values.map!(q => q.length).sum();
    }

    bool isBlank() {
        return blank;
    }

    auto getLoadedObjects() {
        auto ldblocks = blockst.keys
            .map!(q => q in blockst)
            .filter!(q => q.isFilled)
            .map!(q => q.getBlock)
            .joiner;

        auto ldback = backBlock.getBlock;

        return chain(ldback, ldblocks);
    }

    private Block!T getblk(int i) {
        auto c = i in blockst;
        if(c) return *c;
        else {
            blockst[i] = new Block!T(i, params, &data, oid);
            return blockst[i];
        }
    }

    T getBlockObject(int off) {
        auto bk = backBlock.getBlock();
        if(off < bk.length) {
            auto bkobj = bk[off];
            if(bkobj.ignored) return null;
            return bkobj;
        }
        off -= bk.length;

        auto rel = off % blocksz;
        auto n = (off - rel) / blocksz;
        auto nblk = getblk(n);

        if(!nblk.isFilled) {
            nblk.downloadBlock();
            return null;
        }
        if(rel >= nblk.length) return null;

        getblk(n+1).downloadBlock(); //preload
        auto relobj = nblk.getBlock[rel];

        if(relobj.ignored) return null;

        static if(is(T == ClDialog)) {
            if(isOverrided(relobj.getObject.id)) return null;
        }

        blank = false;
        return relobj;
    }

    void seek(int off) {
        iter = off;
        backiter = data.serverCount;
    }

    void addBack(T addobj) {
        backBlock.addBack(addobj);
        data.serverCount += 1;
    }

    bool prepare() {
        if(data.serverCount != -1){
            if(backiter == -1) backiter = data.serverCount;
            return true;
        }
        getblk(0).downloadBlock();
        return false;
    }

    bool empty() {
        return iter >= (backiter + backBlock.length);
    }

    T front() {
        if(data.forceUpdate) {
            data.forceUpdate = false;
            data.serverCount = -1;
            static if(is(T == ClDialog)) clearOverrides();
            initBackBlock();
            //blockst.clear(); - bad for ldc
            blockst = blockst.init;
            prepare();
            return null;
        }
        return getBlockObject(iter);
    }

    void popFront() {
        ++iter;
    }

    typeof(this) save() {
        return this;
    }

    // ===== special magic =====

    //pragma(msg, "T: " ~ T.stringof ~ ", equals ClMessage: " ~ is(T == ClMessage).stringof);

    static if (is(T == ClMessage)) {
        private int serverLineCount = -1;
        int unreadCount = -1;

        int getServerLineCount(int ww) {
            if(serverLineCount == -1 && objectCount == data.serverCount) {
                serverLineCount = blockst.values
                                    .map!(q => q.getBlock())
                                    .joiner
                                    .map!(q => q.getLineCount(ww))
                                    .sum.to!int + 1;
            }
            return serverLineCount;
        }

        void addZombie(vkMessage z) {
            //auto rid = z.rndid;
            auto clz = new ClMessage(z);
            //zombies[rid] = clz;
            addBack(clz);
        }

        void removeZombie(long rid) {
            backBlock
                .getBlock
                .filter!(q => q.getObject.rndid == rid)
                .takeOne
                .each!(q => q.ignored = true);
        }

        vkMessage* getZombie(long rid, bool defeatCache) {
            auto one = backBlock
                .getBlock
                .filter!(q => q.getObject.rndid == rid)
                .takeOne;
            if(!one.empty) {
                if(defeatCache) {
                    one.front.invalidateLineCache();
                }
                return one.front.getObject;
            }
            else return null;
        }

    }


    static if(is(T == ClDialog)) {
        struct DialogOverrider {
            long utime;
            ClDialog dialog;
        }

        private DialogOverrider[int] store; //by peer

        private void updateBackBlock() {
            backBlock.block = getOverrided().array;
        }

        void clearOverrides() {
            //store.clear(); - bad for ldc
            store = store.init;
        }

        void overrideDialog(ClDialog dlg, long ut) {
            store[dlg.getPeer] = DialogOverrider(ut, dlg);
            updateBackBlock();
        }

        void overrideBump(int peer, long ut) {
            auto b = peer in store;
            if(b) {
                b.utime = ut;
            }
            updateBackBlock();
        }

        bool isOverrided(int peer) {
            auto ptr = peer in store;
            return ptr !is null;
        }

        auto getOverridedByPeer(int peer) {
            return peer in store;
        }

        auto getOverrided() {
            return store.values
                            .sort!((a, b) => a.utime > b.utime)
                            .map!(q => q.dialog);
        }

        auto getOverridedUnsorted() {
            return store.values
                            .map!(q => q.dialog);
        }
    }

}

ldFuncResult apiCheck(VkApi api) {
    return ldFuncResult(api.isTokenValid);
}

void enterLoading() {
    ++ps.loadingiter;
    gltoggleUpdate();
}

void leaveLoading() {
    --ps.loadingiter;
    if(ps.loadingiter < 0) ps.loadingiter = 0;
    gltoggleUpdate();
}

void gltoggleUpdate() {
    ps.somethingUpdated = true;
}

// ===== Implement objects =====

class ClDialog : ClObject!vkDialog {

    alias objt = vkDialog;
    alias clt = typeof(this);
    alias ChatFactory = BlockFactory!ClMessage;

    private {
        bool lp;
        ChatFactory cf;
        objt uobj;
    }

    static auto getLoadFunc() {
        return delegate (VkApi api, uint c, uint o, out int sc)
                        =>  api.messagesGetDialogs(c, o, sc).map!(q => new clt(q)).array;
    }

    this(objt obj) {
        super(obj);
    }

    this(string title, int cid, int unreadc, ChatFactory fac) {
        lp = true;
        cf = fac;
        vkDialog lcobj = {
            name: title,
            id: cid
        };
        if(cf.unreadCount < 0) {
            cf.unreadCount = unreadc;
        }
        super(lcobj);
    }

    int getPeer() {
        return obj.id;
    }

    override vkDialog* getObject() {
        if(lp) {
            vkDialog rt = obj;
            auto lastone = cf.getLoadedObjects
                            .filter!(q => q !is null)
                            .takeOne;

            if(lastone.empty) return &obj;
            auto lastm = lastone.front.getObject;

            rt.lastMessage = lastm.body_lines.empty ? "" : lastm.body_lines[0];
            rt.outbox = lastm.outgoing;
            rt.unreadCount = cf.unreadCount;
            rt.unread = rt.outbox ? (lastm.unread) : (cf.unreadCount > 0);
            rt.lastmid = lastm.msg_id;
            rt.online = rt.id >= longpollGimStartId ? true : nc.getOnline(rt.id);
            rt.isChat = rt.id >= convStartId;
            uobj = rt;
            return &uobj;
        }
        else {
            obj.online = obj.id >= longpollGimStartId ? true : nc.getOnline(obj.id);
            return &obj;
        }
    }

}

class ClFriend : ClObject!vkFriend {

    alias objt = vkFriend;
    alias clt = typeof(this);

    static auto getLoadFunc() {
        return delegate (VkApi api, uint c, uint o, out int sc)
                        =>  api.friendsGet(c, o, sc).map!(q => new clt(q)).array;
    }

    this(objt obj) {
        super(obj);
    }

    override vkFriend* getObject() {
        obj.online = nc.getOnline(obj.id);
        auto ovr_ls = obj.id in lsc;
        if(ovr_ls) {
            dbm("ovr_ls");
            //obj.last_seen_utime = ovr_ls.last_seen_utime;
            //obj.last_seen_str = ovr_ls.last_seen_str;
            //todo resolve this shit << Override lastseen on offline trigger
        }
        return &obj;
    }

}

class ClAudio : ClObject!vkAudio {

    alias objt = vkAudio;
    alias clt = typeof(this);

    static auto getLoadFunc() {
        return delegate (VkApi api, uint c, uint o, out int sc)
                        =>  api.audioGet(c, o, sc).map!(q => new clt(q)).array;
    }

    this(objt obj) {
        super(obj);
    }

}

class ClMessage : ClObject!vkMessage {

    alias objt = vkMessage;
    alias clt = typeof(this);

    static private void resolveNeedNameLocal(ref vkMessage[] mw) {
        int lastfid;
        long lastut;
        foreach(ref m; mw.retro) {
            immutable bool nm = !(m.author_id == lastfid && (m.utime-lastut) <= needNameMaxDelta);
            m.needName = nm;
            lastfid = m.author_id;
            lastut = m.utime;
        }
    }

    static auto getLoadFunc(int peer, void delegate(int) setUnreads) {
        return (VkApi api, uint c, uint o, out int sc) {
            int uc;
            auto h = api.messagesGetHistory(peer, c, o, sc, uc);
            setUnreads(uc);
            resolveNeedNameLocal(h);
            return h.map!(q => new clt(q)).array;
        };
    }


    this(objt obj) {
        super(obj);
    }

    private {
        vkMessageLine[] lines;
        int lastww;
    }

    ulong getLineCount(int ww) {
        fillLines(ww);
        return lines.length;
    }

    vkMessageLine[] getLines(int ww) {
        fillLines(ww);
        return lines;
    }

    void invalidateLineCache() {
        lines = [];
    }

    private void fillLines(int ww) {
        if(lines.length == 0 || lastww != ww) {
            lastww = ww;
            lines = convertMessage(obj, ww);
        }
    }

    vkMessageLine lspacing = {
        text: "", isSpacing: true
    };

    const int wwmultiplier = 3;

    private vkMessageLine[] convertMessage(ref vkMessage inp, int ww) {
        immutable bool zombie = inp.isZombie || inp.msg_id < 1;

        vkMessageLine[] rt;
        rt ~= lspacing;
        bool nofwd = (inp.fwd_depth == -1);

        if(inp.needName) {
            vkMessageLine name = {
                text: inp.author_name,
                time: inp.time_str,
                isName: true
            };
            rt ~= name;
            rt ~= lspacing;
        }

        if(inp.body_lines.length != 0) {
            bool unrfl = inp.unread;
            wstring[] wrapped;
            inp.body_lines.map!(q => q.toUTF16wrepl.wordwrap(ww)).each!(q => wrapped ~= q);
            foreach(l; wrapped) {
                vkMessageLine msg = {
                    text: l.toUTF8wrepl,
                    unread: unrfl
                };
                rt ~= msg;
                if(unrfl) unrfl = false;
            }
        } else if (nofwd) rt ~= lspacing;

        if(!nofwd) {
            rt ~= lspacing ~ renderFwd(inp.fwd, 0, ww);
        }

        return rt;
    }

    private vkMessageLine[] renderFwd(vkFwdMessage[] inp, int depth, int ww) {
        ++depth;
        auto lcww = ww - (depth * wwmultiplier);
        if(lcww <= 0) lcww = 1;

        vkMessageLine[] rt;
        foreach(fm; inp) {
            vkMessageLine name = {
                text: fm.author_name,
                time: fm.time_str,
                isFwd: true, isName: true, fwdDepth: depth
            };
            rt ~= name;
            wstring[] wrapped;
            fm.body_lines.map!(q => q.toUTF16wrepl.wordwrap(lcww)).each!(q => wrapped ~= q);
            foreach(l; wrapped) {
                vkMessageLine msg = {
                    text: l.toUTF8wrepl,
                    isFwd: true, fwdDepth: depth
                };
                rt ~= msg;
            }

            vkMessageLine fwdspc;
            fwdspc.isFwd = true; fwdspc.isSpacing = true;
            fwdspc.fwdDepth = depth;
            rt ~= fwdspc;

            if(fm.fwd.length != 0) {
                rt ~= renderFwd(fm.fwd, depth, ww);
                rt ~= fwdspc;
            }

        }
        return rt;
    }

}

// ===== Exceptions =====
abstract class VkException : Exception
{
    private this(string m, string f, size_t l, Throwable n) @safe pure nothrow
    {
        super(m, f, l, n);
    }
}

class BackendException : VkException {
    public {
        @safe pure nothrow this(string message,
                                string file =__FILE__,
                                size_t line = __LINE__,
                                Throwable next = null) {
            super(message, file, line, next);
        }
    }
}

class NetworkException : VkException {
    public {
        @safe pure nothrow this(string loc,
                                string message = "Connection lost",
                                string file =__FILE__,
                                size_t line = __LINE__,
                                Throwable next = null) {
            message = message ~ ": " ~ loc;
            super(message, file, line, next);
        }
    }
}


class ApiErrorException : VkException {
    public {
        int errorCode;
        @safe pure nothrow this(string message,
                                int error_code,
                                string file =__FILE__,
                                size_t line = __LINE__,
                                Throwable next = null) {
            errorCode = error_code;
            super(message, file, line, next);
        }
    }
}

class InternalException : VkException {
    public {

        static const int E_NETWORKFAIL = 4;
        static const int E_LPRESTART = 5;

        string msg;
        int ecode;
        @safe pure nothrow this(int error,
                                string appmsg = "",
                                string file =__FILE__,
                                size_t line = __LINE__,
                                Throwable next = null) {
            msg = "client failed - unresolved internal exception: err" ~ error.to!string ~ " " ~ appmsg;
            ecode = error;
            super(msg, file, line, next);
        }
    }
}

void debugThrow() {
    throw new InternalException(228, "DEBUGPOINT");
}


================================================
FILE: source/vkshit.d
================================================
module magicstringz;

const string appSecret = "hHbZxrka2uZ6jB1inYsH"; // android
const string appCID = "2274003";
const string appUserAgent = "VKAndroidApp/4.9-1118";


================================================
FILE: unused_depend
================================================
"dcrypto": "~>1.2.2"
Download .txt
gitextract__zc_jl6o/

├── .gitignore
├── LICENSE
├── README.md
├── buildRelease.sh
├── buildWithLDC.sh
├── buildWrapper.sh
├── dub.json
├── generateVersion.d
├── source/
│   ├── .editorconfig
│   ├── app.d
│   ├── cfg.d
│   ├── localization.d
│   ├── musicplayer.d
│   ├── namecache.d
│   ├── storedFunctions
│   ├── utils.d
│   ├── vkapi.d
│   └── vkshit.d
└── unused_depend
Condensed preview — 19 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (184K chars).
[
  {
    "path": ".gitignore",
    "chars": 186,
    "preview": ".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/vkversio"
  },
  {
    "path": "LICENSE",
    "chars": 10174,
    "preview": "\n                                 Apache License\n                           Version 2.0, January 2004\n                  "
  },
  {
    "path": "README.md",
    "chars": 2462,
    "preview": "# 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 "
  },
  {
    "path": "buildRelease.sh",
    "chars": 513,
    "preview": "#!/bin/bash\n\ndubarch=\"x86_64\"\ndubtype=\"release\"\ndubconf=\"\"\n\nif [[ \"$2\" == '32' ]]; then\n  dubarch=\"x86\"\nfi\n\nif [[ \"$1\" ="
  },
  {
    "path": "buildWithLDC.sh",
    "chars": 93,
    "preview": "#!/bin/bash\n\nexport DFLAGS=\"-disable-linker-strip-dead $@\"\ndub build --force --compiler=ldc\n\n"
  },
  {
    "path": "buildWrapper.sh",
    "chars": 253,
    "preview": "#!/bin/bash\n\n# buidWrapper [ver] [64/32]\n\nTARCH=\"$2\"\nNAME=\"vk-$1-$TARCH\"\nNAMEBIN=\"$NAME-bin\"\nNAMEPACK=\"$NAMEBIN.7z\"\n\nrm "
  },
  {
    "path": "dub.json",
    "chars": 700,
    "preview": "{\n  \"name\": \"vk\",\n  \"description\": \"Terminal client for vk.com\",\n  \"authors\": [\"HaCk3D\", \"substanceof\"],\n  \"homepage\": \""
  },
  {
    "path": "generateVersion.d",
    "chars": 1815,
    "preview": "/*\nCopyright 2016 HaCk3D, substanceof\n\nhttps://github.com/HaCk3Dq\nhttps://github.com/substanceof\n\nLicensed under the Apa"
  },
  {
    "path": "source/.editorconfig",
    "chars": 212,
    "preview": "root = true\n\n[*]\nident_style = space\ndfmt_brace_style = stroustrup\ndfmt_space_after_cast = false\ndfmt_space_after_keywor"
  },
  {
    "path": "source/app.d",
    "chars": 42617,
    "preview": "/*\nCopyright 2016 HaCk3D, substanceof\n\nhttps://github.com/HaCk3Dq\nhttps://github.com/substanceof\n\nLicensed under the Apa"
  },
  {
    "path": "source/cfg.d",
    "chars": 1219,
    "preview": "/*\nCopyright 2016 HaCk3D, substanceof\n\nhttps://github.com/HaCk3Dq\nhttps://github.com/substanceof\n\nLicensed under the Apa"
  },
  {
    "path": "source/localization.d",
    "chars": 10760,
    "preview": "/*\nCopyright 2016 HaCk3D, substanceof\n\nhttps://github.com/HaCk3Dq\nhttps://github.com/substanceof\n\nLicensed under the Apa"
  },
  {
    "path": "source/musicplayer.d",
    "chars": 12094,
    "preview": "/*\nCopyright 2016 HaCk3D, substanceof\n\nhttps://github.com/HaCk3Dq\nhttps://github.com/substanceof\n\nLicensed under the Apa"
  },
  {
    "path": "source/namecache.d",
    "chars": 4222,
    "preview": "/*\nCopyright 2016 HaCk3D, substanceof\n\nhttps://github.com/HaCk3Dq\nhttps://github.com/substanceof\n\nLicensed under the Apa"
  },
  {
    "path": "source/storedFunctions",
    "chars": 1410,
    "preview": "/*\nCopyright 2016 HaCk3D, substanceof\n\nhttps://github.com/HaCk3Dq\nhttps://github.com/substanceof\n\nLicensed under the Apa"
  },
  {
    "path": "source/utils.d",
    "chars": 12130,
    "preview": "/*\nCopyright 2016 HaCk3D, substanceof\n\nhttps://github.com/HaCk3Dq\nhttps://github.com/substanceof\n\nLicensed under the Apa"
  },
  {
    "path": "source/vkapi.d",
    "chars": 72257,
    "preview": "/*\nCopyright 2016 HaCk3D, substanceof\n\nhttps://github.com/HaCk3Dq\nhttps://github.com/substanceof\n\nLicensed under the Apa"
  },
  {
    "path": "source/vkshit.d",
    "chars": 168,
    "preview": "module magicstringz;\n\nconst string appSecret = \"hHbZxrka2uZ6jB1inYsH\"; // android\nconst string appCID = \"2274003\";\nconst"
  },
  {
    "path": "unused_depend",
    "chars": 21,
    "preview": "\"dcrypto\": \"~>1.2.2\"\n"
  }
]

About this extraction

This page contains the full source code of the HaCk3Dq/vk GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 19 files (169.2 KB), approximately 44.5k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!