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


# 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(""", "\"")
.replace("<", "<")
.replace(">", ">")
.replace("&", "&");
}
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, ¬ifyBlockDownloadDone));
}
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"
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.