// ==UserScript==
// @name VK: Check Online
// @name:ru ВК: Проверка онлайна
// @description Checks the last online on page user and in dialog
// @description:ru Проверяет последний онлайн пользователя на странице и в диалогe
// @namespace vk-check-online.user.js
// @license MIT
// @author askornot
// @version 1.5.2
// @match https://vk.com/*
// @connect vk.com
// @compatible chrome Violentmonkey 2.18.0
// @compatible firefox Violentmonkey 2.13.3
// @compatible firefox Tampermonkey 4.18.1
// @homepageURL https://greasyfork.org/en/scripts/403717-vk-check-online
// @supportURL https://greasyfork.org/en/scripts/403717-vk-check-online/feedback
// @run-at document-start
// @noframes
// ==/UserScript==
(function (W) {
'use strict';
const IM_ID = 'payload,1,1,0,id';
const IM_ONLINE = 'payload,1,0,last_seen';
const PROFILE_ID = 'response,users.get,0,id';
const PROFILE_ONLINE = 'response,users.get,0,online_info';
const IM_INFO = {
can_see: 1,
platform: 0,
time: 0,
mobile: 0,
};
const PROFILE_INFO = {
visible: true,
last_seen: 0,
app_id: 0,
is_online: false,
is_mobile: false,
};
const last = (arr) => arr[arr.length - 1];
const match = (s) => /online_info|a_start/.test(s);
const getByPath = (data, dataPath) => {
const path = dataPath.split(',');
let object = data;
for (const name of path) {
const next = object[name];
if (next === null || next === undefined) return next;
object = next;
}
return object;
};
const setByPath = (data, dataPath, value) => {
const path = dataPath.split(',');
const len = path.length;
let obj = data;
let i = 0;
let next, prop;
for (;;) {
if (typeof obj !== 'object') return false;
prop = path[i];
if (i === len - 1) {
obj[prop] = value;
return true;
}
next = obj[prop];
if (next === undefined || next === null) {
next = {};
obj[prop] = next;
}
obj = next;
i++;
}
};
const parse = (text) => {
const parser = new DOMParser();
const body = parser.parseFromString(text, 'text/html');
const [element] = body.getElementsByTagName('ya:lastloggedin');
if (!element) return 0;
const date = element.getAttribute('dc:date');
return Date.parse(date) / 1000;
};
const request = (url, callback) => {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === xhr.DONE) {
if (xhr.status === 0 || xhr.status === 200) {
callback(xhr.responseText);
}
}
};
xhr.open('GET', url, true);
xhr.send();
};
const foaf = (id) => `https://vk.com/foaf.php?id=${id}`;
const profile = (data) => {
const info = getByPath(data, PROFILE_ONLINE);
if (info && info.visible === true) return Promise.resolve(data);
const id = getByPath(data, PROFILE_ID);
if (id === undefined) return Promise.resolve(data);
return fetch(foaf(id))
.then((res) => res.text())
.then(parse)
.then((time) => {
setByPath(data, PROFILE_ONLINE, { ...PROFILE_INFO, last_seen: time });
return data;
});
};
const im = (data, callback) => {
const info = getByPath(data, IM_ONLINE);
if (info && info.can_see === 1) return callback();
const id = getByPath(data, IM_ID);
if (id === undefined) return callback();
request(foaf(id), (text) => {
const time = parse(text);
setByPath(data, IM_ONLINE, { ...IM_INFO, time });
callback();
});
};
const queries = new WeakSet();
const xhr = W.XMLHttpRequest && W.XMLHttpRequest.prototype;
const onreadystatechange = (self) => {
self.onreadystatechange = new Proxy(self.onreadystatechange, {
apply(...args) {
if (self.readyState === xhr.DONE) {
const data = JSON.parse(self.responseText);
im(data, () => {
Object.defineProperty(self, 'responseText', {
value: JSON.stringify(data),
writable: true,
});
return Reflect.apply(...args);
});
}
},
});
};
xhr.open = new Proxy(xhr.open, {
apply(target, self, argumentsList) {
const url = argumentsList[1];
if (match(url)) queries.add(self);
return Reflect.apply(target, self, argumentsList);
},
});
xhr.send = new Proxy(xhr.send, {
apply(target, self, argumentsList) {
if (queries.has(self)) onreadystatechange(self);
return Reflect.apply(target, self, argumentsList);
},
});
W.fetch = new Proxy(W.fetch, {
apply(...args) {
const promise = Reflect.apply(...args);
const argumentsList = last(args);
const { body } = last(argumentsList);
if (!match(body)) return promise;
return promise
.then((res) => res.clone())
.then((res) => res.json())
.then(profile)
.then((patched) => new Response(JSON.stringify(patched), promise))
.catch(() => promise);
},
});
const patch = () => {
W.extend = new Proxy(W.extend, {
apply(target, self, argumentsList) {
const object = argumentsList[0];
const { apiPrefetchCache } = last(argumentsList);
if (!apiPrefetchCache) return Reflect.apply(target, self, argumentsList);
const filtered = apiPrefetchCache.filter(
({ method }) => method !== 'users.get',
);
return Reflect.apply(target, self, [
object,
{ apiPrefetchCache: filtered },
]);
},
});
};
new MutationObserver((_, observer) => {
try {
patch();
observer.disconnect();
} catch {}
}).observe(document.documentElement, {
childList: true,
subtree: true,
});
})(unsafeWindow || window);