plurk_lib

An unofficial library for Plurk

Este script não deve ser instalado diretamente. Este script é uma biblioteca de outros scripts para incluir com o diretório meta // @require https://update.greasyfork.org/scripts/432792/972862/plurk_lib.js

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

You will need to install an extension such as Tampermonkey to install this script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         plurk_lib
// @description  An unofficial library for Plurk
// @version      0.1.1
// @license      MIT
// @namespace    https://github.com/stdai1016
// @include      https://www.plurk.com/*
// @exclude      https://www.plurk.com/_*
// ==/UserScript==

/* jshint esversion: 6 */

const plurklib = (function () { // eslint-disable-line
  'use strict';
  /* class */

  class PlurkRecord {
    constructor (target, type = null) {
      this.target = target;
      this.type = type;
      this.plurks = [];
    }
  }

  class PlurkObserver {
    /**
     *  @param {Function} callback
     */
    constructor (callback) {
      this._observe = false;
      this._mo_tl = new MutationObserver(function (mrs) {
        const records = [];
        mrs.forEach(mr => {
          const pr = new PlurkRecord(mr.target, 'plurk');
          mr.addedNodes.forEach(node => {
            const plurk = Plurk.analysisElement(node);
            if (plurk) pr.plurks.push(plurk);
          });
          if (pr.plurks.length) records.push(pr);
        });
        callback(records);
      });
      this._mo_resp = new MutationObserver(function (mrs) {
        const records = [];
        mrs.forEach(mr => {
          const pr = new PlurkRecord(mr.target, 'plurk');
          mr.addedNodes.forEach(node => {
            const plurk = Plurk.analysisElement(node);
            if (plurk) pr.plurks.push(plurk);
          });
          if (pr.plurks.length) records.push(pr);
        });
        callback(records);
      });
    }

    observe (options = { plurk: false }) {
      if (options?.plurk) {
        this._observe = true;
        getElementAsync('#timeline_cnt .block_cnt', document) // timeline
          .then(tl => this._mo_tl.observe(tl, { childList: true }), e => {});
        getElementAsync('#cbox_response .list', document) // pop window
          .then(list => this._mo_resp.observe(list, { childList: true }));
        getElementAsync('#form_holder .list', document) // resp in timeline
          .then(list => this._mo_resp.observe(list, { childList: true }));
        // resp in article
        getElementAsync('#plurk_responses .list', document).then(
          list => this._mo_resp.observe(list, { childList: true }),
          e => {}
        );
      }
      if (!this._observe) throw Error();
    }

    disconnect () {
      this._mo_tl.disconnect();
      this._mo_resp.disconnect();
    }
  }

  class Plurk {
    /**
     * @param {object} pdata
     */
    constructor (pdata, target) {
      Plurk.ATTRIBUTES.forEach(a => { this[a] = pdata[a]; });
      this.target = target;
    }

    get isMute () { return this.is_unread === 2; }

    get isResponse () { return this.id !== this.plurk_id; }

    get isReplurk () {
      return !this.isResponse && this.user_id !== this.owner_id;
    }

    /**
     *  @param {HTMLElement} node
     *  @returns {Plurk}
     */
    static analysisElement (node) {
      if (!node.classList.contains('plurk')) return null;
      return new Plurk(analysisElement(node), node);
    }
  }

  /* eslint-disable no-multi-spaces */
  /** attributes for plurk | response */
  Plurk.ATTRIBUTES = [
    'owner_id',         // posted by
    'plurk_id',         // the plurk | the plurk that the response belongs to
    'user_id',          // which timeline does this Plurk belong to | unused
    'replurker_id',     // replurked by | unused
    'id',               // plurk id | response id
    'qualifier',        // qualifier
    'content',          // HTMLElement if exist
    // 'content_raw',
    // 'lang',
    'posted',           // the date this plurk was posted
    'last_edited',      // the last date this plurk was edited

    'plurk_type',       // 0: public, 1: private, 4: anonymous | unused
    // 'limited_to',
    // 'excluded',
    // 'publish_to_followers',
    // 'no_comments',
    'porn',             // has 'porn' tag | unused
    'anonymous',        // is anonymous

    'is_unread',        // 0: read, 1: unread, 2: muted  | unused
    // 'has_gift',      // current user sent a gift?
    'coins',            // number of users sent gift
    'favorite',         // favorited by current user
    'favorite_count',   // number of users favorite it
    // 'favorers',      // favorers
    'replurked',        // replurked by current user
    'replurkers_count', // number of users replurked it
    // 'replurkers',    // replurkers
    'replurkable',      // replurkable
    // 'responded',     // responded by current user
    'response_count'    // number of responses | unused
    // 'responses_seen',
    // 'bookmark',
    // 'mentioned'      // current user is mentioned
  ];
  /* eslist-enable */

  function getElementAsync (selectors, target, timeout = 100) {
    return new Promise((resolve, reject) => {
      const i = setTimeout(function () {
        stop();
        const el = target.querySelector(selectors);
        if (el) resolve(el);
        else reject(Error(`get "${selectors}" timeout`));
      }, timeout);
      const mo = new MutationObserver(r => r.forEach(mu => {
        const el = mu.target.querySelector(selectors);
        if (el) { stop(); resolve(el); }
      }));
      mo.observe(target, { childList: true, subtree: true });
      function stop () { clearTimeout(i); mo.disconnect(); }
    });
  }

  /**
   *  @param {HTMLElement} node
   *  @returns {object}
   */
  function analysisElement (node) {
    const user = node.querySelector('.td_qual a.name') ||
                 node.querySelector('.user a.name');
    const posted = node.querySelector('.posted');
    const isResponse = node.classList.contains('response');
    const isReplurk = !isResponse && user.dataset.uid !== node.dataset.uid;
    return {
      owner_id: parseInt(node.dataset.uid || user.dataset.uid),
      plurk_id: parseInt(node.dataset.pid),
      user_id: getPageUserData()?.id || parseInt(user.dataset.uid),
      posted: posted ? new Date(posted.dataset.posted) : null,
      replurker_id: isReplurk ? parseInt(user.dataset.uid) : null,
      id: parseInt(node.id.substr(1) || node.dataset.rid || node.dataset.pid),
      qualifier: (function () {
        const qualifier = node.querySelector('.text_holder .qualifier') ||
                          node.querySelector('.qualifier');
        for (const c of qualifier?.classList || []) {
          if (!c.startsWith('q_') || c === 'q_replurks') continue;
          return c.substr(2);
        }
        return ':';
      })(),
      content: node.querySelector('.text_holder .text_holder') ||
               node.querySelector('.text_holder'),
      // content_raw,
      // lang,
      response_count: parseInt(node.dataset.respcount) || 0,
      // responses_seen,
      // limited_to,
      // excluded,
      // no_comments,
      plurk_type: (function () {
        if (node.dataset.uid === '99999') return 4;
        if (node.querySelector('.private')) return 1;
        return 0;
      })(),
      is_unread: (function () {
        if (node.classList.contains('mute')) return 2;
        if (node.classList.contains('new')) return 1;
        return 0;
      })(),
      last_edited: posted?.dataset.edited
        ? new Date(posted.dataset.edited)
        : null,
      porn: node.classList.contains('porn'),
      // publish_to_followers,
      coins: parseInt(node.querySelector('a.gift')?.innerText) || 0,
      // has_gift,
      replurked: node.classList.contains('replurk'),
      // replurkers,
      replurkers_count:
        parseInt(node.querySelector('a.replurk')?.innerText) || 0,
      replurkable: node.querySelector('a.replurk') !== null,
      // favorers,
      favorite_count: parseInt(node.querySelector('a.like')?.innerText) || 0,
      anonymous: node.dataset.uid === '99999',
      // responded,
      favorite: node.classList.contains('favorite')
      // bookmark,
      // mentioned
    };
  }

  const _GLOBAL = (function () {
    function cp (o) {
      const n = {};
      for (const k in o) {
        if (o[k] instanceof Date) n[k] = new Date(o[k]);
        else if (typeof o[k] !== 'object') n[k] = o[k];
        else n[k] = cp(o[k]);
      }
      return n;
    }
    if (typeof unsafeWindow === 'undefined') {
      if (window.GLOBAL) return cp(window.GLOBAL);// eslint-disable-line
    // eslint-disable-next-line
    } else if (unsafeWindow.GLOBAL) return cp(unsafeWindow.GLOBAL);
    for (const scr of document.querySelectorAll('script')) {
      try {
        const text = scr.textContent
          .replace(/new Date\("([\w ,:]+)"\)/g, '"new Date(\\"$1\\")"');
        const i = text.indexOf('var GLOBAL = {');
        return (function dd (o) {
          for (const k in o) {
            if (typeof o[k] === 'object') dd(o[k]);
            else if (typeof o[k] === 'string' && o[k].startsWith('new Date')) {
              const m = o[k].match(/new Date\("([\w ,:]+)"\)/);
              o[k] = m ? new Date(m[1]) : null;
            }
          }
          return o;
        })(JSON.parse(text.substring(i + 13, text.indexOf('\n', i))));
      } catch {}
    }
  })();

  /**
   *  @returns {object}
   */
  function getUserData () { return _GLOBAL?.session_user; }

  /**
   *  @returns {object}
   */
  function getPageUserData () { return _GLOBAL?.page_user; }

  /* ## API */
  /**
   *  @param {string} path
   *  @param {object} options
   *  @returns {Promise<any>}
   */
  async function callApi (path, options = null) {
    options = options || {};
    let body = '';
    for (const k in options) {
      body += `&${encodeURIComponent(k)}=${encodeURIComponent(options[k])}`;
    }
    body = body.substr(1);
    const init = { method: 'POST', credentials: 'same-origin' };
    if (body.length) {
      init.body = body;
      init.headers = { 'content-type': 'application/x-www-form-urlencoded' };
    }
    path = path.startsWith('/') ? path : '/' + path;
    const resp = await fetch(`https://www.plurk.com${path}`, init);
    if (!resp.ok) {
      throw Error(`${resp.status} ${resp.statusText}: ${await resp.text()}`);
    }
    return resp.json();
  }

  /* ### Notifications */
  /**
   *  @param {number} limit
   *  @param {string|number|Date} offset
   *  @returns {Promise<object>}
   */
  async function getNotificationsMixed2 (limit = 20, offset = null) {
    const options = { limit: limit };
    if (offset) options.offset = (new Date(offset)).toISOString();
    return callApi('/Notifications/getMixed2', options);
  }

  /* ### Responses */
  async function getResponses (plurkId, from = 0) {
    return callApi('/Responses/get',
      { plurk_id: plurkId, from_response_id: from });
  }
  /* ### Users */
  async function fetchUserAliases () {
    return callApi('/Users/fetchUserAliases');
  }
  /**
   *  @param {number|string} userIdOrNickName
   *  @returns {Promise<object>}
   */
  async function fetchUserInfo (userIdOrNickName) {
    let id = null;
    if (/^\d+$/.test(`${userIdOrNickName}`)) id = `${userIdOrNickName}`;
    else {
      const resp = await fetch(`https://www.plurk.com/${userIdOrNickName}`);
      const html = resp.ok ? (await resp.text()) : '';
      const doc = (new DOMParser()).parseFromString(html, 'text/html');
      for (const scr of doc.head.querySelectorAll('script:not([src])')) {
        const i = scr.textContent.indexOf('"page_user"');
        if (i < 0) continue;
        const text = scr.textContent.substr(i, 128);
        id = text.match(/"id" *: *(\d+) *,/)?.[1];
        if (id) break;
      }
    }
    return callApi('/Users/fetchUserInfo', { user_id: id });
  }

  /**
   *  @param {number} userId
   *  @returns {Promise<string[]>}
   */
  async function getCustomCss (userId = null) {
    userId = userId || getPageUserData().id;
    const url = `https://www.plurk.com/Users/getCustomCss?user_id=${userId}`;
    const rules = await (await fetch(url)).text();
    return rules.split(/\r?\n/);
  }

  return {
    Plurk: Plurk,
    PlurkRecord: PlurkRecord,
    PlurkObserver: PlurkObserver,
    getUserData: getUserData,
    getPageUserData: getPageUserData,
    callApi: callApi,
    getNotificationsMixed2: getNotificationsMixed2,
    fetchUserAliases: fetchUserAliases,
    fetchUserInfo: fetchUserInfo,
    getResponses: getResponses,
    getCustomCss: getCustomCss
  };
})();