VK: Check Online

Checks the last online on page user and in dialog

目前為 2022-10-22 提交的版本,檢視 最新版本

// ==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.3.0
// @match           https://vk.com/*
// @connect         vk.com
// @compatible      chrome     Violentmonkey 2.13.3
// @compatible      firefox    Violentmonkey 2.13.3
// @compatible      firefox    Tampermonkey 4.18.0
// @homepageURL     https://greasyfork.org/en/scripts/403717-vk-check-online
// @supportURL      https://greasyfork.org/en/scripts/403717-vk-check-online/feedback
// @run-at          document-end
// @noframes
// ==/UserScript==

(function (W, D, T) {
  'use strict';
  
  const IM_ID = 'payload,1,1,0,id';
  const IM_ONLINE = 'payload,1,0,last_seen';
  
  const IM_INFO = {
    can_see: 1,
    platform: 0,
    time: 0,
    mobile: 0,
  };

  const ID_PATH = 'response,users.get,0,id';

  const ONLINE_PATH = 'response,users.get,0,online_info';

  const ONLINE_INFO = {
    visible: true,
    last_seen: 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');
    const uts = Math.floor(Date.parse(date) / 1000);
    return uts;
  };

  const online = (id) =>
    fetch(`/foaf.php?id=${id}`)
      .then((res) => res.text())
      .then(parse);

  const patch = (data) => {
    const info = getByPath(data, ONLINE_PATH);
    if (info && info.visible === true) return data;
    const id = getByPath(data, ID_PATH);
    return online(id).then((timestamp) => {
      const info = { ...ONLINE_INFO, last_seen: timestamp };
      setByPath(data, ONLINE_PATH, info);
      return data;
    });
  };

  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 im = (data, callback) => {
    const info = getByPath(data, IM_ONLINE);
    if (info && info.can_see === 1) return callback(data);
    const id = getByPath(data, IM_ID);
    if (id === undefined) return callback(data);
    request(`foaf.php?id=${id}`, (text) => {
      const time = parse(text);
      setByPath(data, IM_ONLINE, { ...IM_INFO, time });
      callback(data);
    });
  };

  const proto = W.XMLHttpRequest && W.XMLHttpRequest.prototype;
  proto.send = new Proxy(proto.send, {
    apply(...args) {
      const argumentsList = last(args);
      const self = args[1];
      const body = last(argumentsList);
      if (match(body)) {
        self.onreadystatechange = new Proxy(self.onreadystatechange, {
          apply(...pars) {
            if (self.readyState === proto.DONE) {
              const data = JSON.parse(self.responseText);
              im(data, (patched) => {
                Object.defineProperty(self, 'responseText', {
                  value: JSON.stringify(patched),
                  writable: true,
                });
                Reflect.apply(...pars);
              });
            }
          },
        });
      }
      Reflect.apply(...args);
    },
  });

  W[T] = new Proxy(W[T], {
    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(patch)
        .then((patched) => new Response(JSON.stringify(patched), promise))
        .catch(() => promise);
    },
  });
})(unsafeWindow || window, document, 'fetch');