PyXiion / Return Pikabu minus

// ==UserScript==
// @name         Return Pikabu minus
// @version      0.8
// @namespace    pikabu-return-minus.pyxiion.ru
// @description  Возвращает минусы на Pikabu, а также фильтрацию по рейтингу.
// @author       PyXiion
// @match        *://pikabu.ru/*
// @connect      api.pikabu.ru
// @connect      pikabu.ru
// @connect      rpm.pyxiion.ru
// @grant        GM.xmlHttpRequest
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM.registerMenuCommand
// @require      https://openuserjs.org/src/libs/sizzle/GM_config.js
// @license      MIT
// ==/UserScript==
//#region Utils
function MD5(string) {
  function RotateLeft(lValue, iShiftBits) {
    return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits));
  }

  function AddUnsigned(lX, lY) {
    var lX4, lY4, lX8, lY8, lResult;
    lX8 = (lX & 0x80000000);
    lY8 = (lY & 0x80000000);
    lX4 = (lX & 0x40000000);
    lY4 = (lY & 0x40000000);
    lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF);
    if (lX4 & lY4) {
      return (lResult ^ 0x80000000 ^ lX8 ^ lY8);
    }
    if (lX4 | lY4) {
      if (lResult & 0x40000000) {
        return (lResult ^ 0xC0000000 ^ lX8 ^ lY8);
      }
      else {
        return (lResult ^ 0x40000000 ^ lX8 ^ lY8);
      }
    }
    else {
      return (lResult ^ lX8 ^ lY8);
    }
  }

  function F(x, y, z) {
    return (x & y) | ((~x) & z);
  }

  function G(x, y, z) {
    return (x & z) | (y & (~z));
  }

  function H(x, y, z) {
    return (x ^ y ^ z);
  }

  function I(x, y, z) {
    return (y ^ (x | (~z)));
  }

  function FF(a, b, c, d, x, s, ac) {
    a = AddUnsigned(a, AddUnsigned(AddUnsigned(F(b, c, d), x), ac));
    return AddUnsigned(RotateLeft(a, s), b);
  };

  function GG(a, b, c, d, x, s, ac) {
    a = AddUnsigned(a, AddUnsigned(AddUnsigned(G(b, c, d), x), ac));
    return AddUnsigned(RotateLeft(a, s), b);
  };

  function HH(a, b, c, d, x, s, ac) {
    a = AddUnsigned(a, AddUnsigned(AddUnsigned(H(b, c, d), x), ac));
    return AddUnsigned(RotateLeft(a, s), b);
  };

  function II(a, b, c, d, x, s, ac) {
    a = AddUnsigned(a, AddUnsigned(AddUnsigned(I(b, c, d), x), ac));
    return AddUnsigned(RotateLeft(a, s), b);
  };

  function ConvertToWordArray(string) {
    var lWordCount;
    var lMessageLength = string.length;
    var lNumberOfWords_temp1 = lMessageLength + 8;
    var lNumberOfWords_temp2 = (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64;
    var lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16;
    var lWordArray = Array(lNumberOfWords - 1);
    var lBytePosition = 0;
    var lByteCount = 0;
    while (lByteCount < lMessageLength) {
      lWordCount = (lByteCount - (lByteCount % 4)) / 4;
      lBytePosition = (lByteCount % 4) * 8;
      lWordArray[lWordCount] = (lWordArray[lWordCount] | (string.charCodeAt(lByteCount) << lBytePosition));
      lByteCount++;
    }
    lWordCount = (lByteCount - (lByteCount % 4)) / 4;
    lBytePosition = (lByteCount % 4) * 8;
    lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition);
    lWordArray[lNumberOfWords - 2] = lMessageLength << 3;
    lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;
    return lWordArray;
  };

  function WordToHex(lValue) {
    var WordToHexValue = "",
      WordToHexValue_temp = "",
      lByte, lCount;
    for (lCount = 0; lCount <= 3; lCount++) {
      lByte = (lValue >>> (lCount * 8)) & 255;
      WordToHexValue_temp = "0" + lByte.toString(16);
      WordToHexValue = WordToHexValue + WordToHexValue_temp.substr(WordToHexValue_temp.length - 2, 2);
    }
    return WordToHexValue;
  };

  function Utf8Encode(string) {
    string = string.replace(/\r\n/g, "\n");
    var utftext = "";
    for (var n = 0; n < string.length; n++) {
      var c = string.charCodeAt(n);
      if (c < 128) {
        utftext += String.fromCharCode(c);
      }
      else if ((c > 127) && (c < 2048)) {
        utftext += String.fromCharCode((c >> 6) | 192);
        utftext += String.fromCharCode((c & 63) | 128);
      }
      else {
        utftext += String.fromCharCode((c >> 12) | 224);
        utftext += String.fromCharCode(((c >> 6) & 63) | 128);
        utftext += String.fromCharCode((c & 63) | 128);
      }
    }
    return utftext;
  };
  var x = Array();
  var k, AA, BB, CC, DD, a, b, c, d;
  var S11 = 7,
    S12 = 12,
    S13 = 17,
    S14 = 22;
  var S21 = 5,
    S22 = 9,
    S23 = 14,
    S24 = 20;
  var S31 = 4,
    S32 = 11,
    S33 = 16,
    S34 = 23;
  var S41 = 6,
    S42 = 10,
    S43 = 15,
    S44 = 21;
  string = Utf8Encode(string);
  x = ConvertToWordArray(string);
  a = 0x67452301;
  b = 0xEFCDAB89;
  c = 0x98BADCFE;
  d = 0x10325476;
  for (k = 0; k < x.length; k += 16) {
    AA = a;
    BB = b;
    CC = c;
    DD = d;
    a = FF(a, b, c, d, x[k + 0], S11, 0xD76AA478);
    d = FF(d, a, b, c, x[k + 1], S12, 0xE8C7B756);
    c = FF(c, d, a, b, x[k + 2], S13, 0x242070DB);
    b = FF(b, c, d, a, x[k + 3], S14, 0xC1BDCEEE);
    a = FF(a, b, c, d, x[k + 4], S11, 0xF57C0FAF);
    d = FF(d, a, b, c, x[k + 5], S12, 0x4787C62A);
    c = FF(c, d, a, b, x[k + 6], S13, 0xA8304613);
    b = FF(b, c, d, a, x[k + 7], S14, 0xFD469501);
    a = FF(a, b, c, d, x[k + 8], S11, 0x698098D8);
    d = FF(d, a, b, c, x[k + 9], S12, 0x8B44F7AF);
    c = FF(c, d, a, b, x[k + 10], S13, 0xFFFF5BB1);
    b = FF(b, c, d, a, x[k + 11], S14, 0x895CD7BE);
    a = FF(a, b, c, d, x[k + 12], S11, 0x6B901122);
    d = FF(d, a, b, c, x[k + 13], S12, 0xFD987193);
    c = FF(c, d, a, b, x[k + 14], S13, 0xA679438E);
    b = FF(b, c, d, a, x[k + 15], S14, 0x49B40821);
    a = GG(a, b, c, d, x[k + 1], S21, 0xF61E2562);
    d = GG(d, a, b, c, x[k + 6], S22, 0xC040B340);
    c = GG(c, d, a, b, x[k + 11], S23, 0x265E5A51);
    b = GG(b, c, d, a, x[k + 0], S24, 0xE9B6C7AA);
    a = GG(a, b, c, d, x[k + 5], S21, 0xD62F105D);
    d = GG(d, a, b, c, x[k + 10], S22, 0x2441453);
    c = GG(c, d, a, b, x[k + 15], S23, 0xD8A1E681);
    b = GG(b, c, d, a, x[k + 4], S24, 0xE7D3FBC8);
    a = GG(a, b, c, d, x[k + 9], S21, 0x21E1CDE6);
    d = GG(d, a, b, c, x[k + 14], S22, 0xC33707D6);
    c = GG(c, d, a, b, x[k + 3], S23, 0xF4D50D87);
    b = GG(b, c, d, a, x[k + 8], S24, 0x455A14ED);
    a = GG(a, b, c, d, x[k + 13], S21, 0xA9E3E905);
    d = GG(d, a, b, c, x[k + 2], S22, 0xFCEFA3F8);
    c = GG(c, d, a, b, x[k + 7], S23, 0x676F02D9);
    b = GG(b, c, d, a, x[k + 12], S24, 0x8D2A4C8A);
    a = HH(a, b, c, d, x[k + 5], S31, 0xFFFA3942);
    d = HH(d, a, b, c, x[k + 8], S32, 0x8771F681);
    c = HH(c, d, a, b, x[k + 11], S33, 0x6D9D6122);
    b = HH(b, c, d, a, x[k + 14], S34, 0xFDE5380C);
    a = HH(a, b, c, d, x[k + 1], S31, 0xA4BEEA44);
    d = HH(d, a, b, c, x[k + 4], S32, 0x4BDECFA9);
    c = HH(c, d, a, b, x[k + 7], S33, 0xF6BB4B60);
    b = HH(b, c, d, a, x[k + 10], S34, 0xBEBFBC70);
    a = HH(a, b, c, d, x[k + 13], S31, 0x289B7EC6);
    d = HH(d, a, b, c, x[k + 0], S32, 0xEAA127FA);
    c = HH(c, d, a, b, x[k + 3], S33, 0xD4EF3085);
    b = HH(b, c, d, a, x[k + 6], S34, 0x4881D05);
    a = HH(a, b, c, d, x[k + 9], S31, 0xD9D4D039);
    d = HH(d, a, b, c, x[k + 12], S32, 0xE6DB99E5);
    c = HH(c, d, a, b, x[k + 15], S33, 0x1FA27CF8);
    b = HH(b, c, d, a, x[k + 2], S34, 0xC4AC5665);
    a = II(a, b, c, d, x[k + 0], S41, 0xF4292244);
    d = II(d, a, b, c, x[k + 7], S42, 0x432AFF97);
    c = II(c, d, a, b, x[k + 14], S43, 0xAB9423A7);
    b = II(b, c, d, a, x[k + 5], S44, 0xFC93A039);
    a = II(a, b, c, d, x[k + 12], S41, 0x655B59C3);
    d = II(d, a, b, c, x[k + 3], S42, 0x8F0CCC92);
    c = II(c, d, a, b, x[k + 10], S43, 0xFFEFF47D);
    b = II(b, c, d, a, x[k + 1], S44, 0x85845DD1);
    a = II(a, b, c, d, x[k + 8], S41, 0x6FA87E4F);
    d = II(d, a, b, c, x[k + 15], S42, 0xFE2CE6E0);
    c = II(c, d, a, b, x[k + 6], S43, 0xA3014314);
    b = II(b, c, d, a, x[k + 13], S44, 0x4E0811A1);
    a = II(a, b, c, d, x[k + 4], S41, 0xF7537E82);
    d = II(d, a, b, c, x[k + 11], S42, 0xBD3AF235);
    c = II(c, d, a, b, x[k + 2], S43, 0x2AD7D2BB);
    b = II(b, c, d, a, x[k + 9], S44, 0xEB86D391);
    a = AddUnsigned(a, AA);
    b = AddUnsigned(b, BB);
    c = AddUnsigned(c, CC);
    d = AddUnsigned(d, DD);
  }
  var temp = WordToHex(a) + WordToHex(b) + WordToHex(c) + WordToHex(d);
  return temp.toLowerCase();
}
class AbstractHttpRequest {
  constructor(url) {
    this.url = url;
    this.httpMethod = "POST";
    this.headers = new Map();
    this.timeout = 15000;
  }
  addHeader(key, value) {
    this.headers.set(key, value);
    return this;
  }
  setHttpMethod(httpMethod) {
    this.httpMethod = httpMethod;
    return this;
  }
  execute(callback) {
    const details = {
      url: this.url,
      method: this.httpMethod,
      headers: Object.fromEntries(this.headers),
      data: JSON.stringify(this.getData()),
      timeout: this.timeout,
      responseType: "json",
      onerror: callback.onError,
      onload: callback.onSuccess,
      // TODO: ontimeout
    };
    details.anonymous = true;
    GM.xmlHttpRequest(details);
  }
  executeAsync() {
    const promise = new Promise((resolve, reject) => {
      this.execute({
        onError: reject,
        onSuccess: resolve,
      });
    });
    promise.catch(error);
    return promise;
  }
}
class HttpRequest extends AbstractHttpRequest {
  constructor(url, method = "GET") {
    super(url);
    this.addHeader("Content-Type", "application/json");
    this.httpMethod = method;
  }
  setBody(body) {
    this.body = body;
  }
  getData() {
    return this.body;
  }
}
//#endregion
//#region Pikabu API
var Pikabu;
(function (Pikabu) {
  const DOMAIN = "https://api.pikabu.ru/";
  const API_V1 = DOMAIN + "v1/";
  const API_KEY = "kmq4!2cPl)peZ";
  class API {
    static getDeviceId() {
      return "0";
    }
  }
  API.USER_AGENT = "ru.pikabu.android/1.21.15 (SM-N975F Android 7.1.2)";
  API.COOKIE = "unqKms867=aba48a160c; rm5bH=8c68fbfe3dc5e5f5b23a9ec1a8f784f8";
  class Request extends AbstractHttpRequest {
    constructor(domain, controller, params) {
      super(domain + controller);
      this.controller = controller;
      this.params = params;
      this.setHttpMethod("GET");
      this.addHeader("DeviceId", API.getDeviceId());
      this.addHeader("User-Agent", API.USER_AGENT);
      this.addHeader("Cookie", API.COOKIE);
      this.addHeader("Content-Type", "application/json");
    }
    setParam(key, value) {
      this.params[key] = value;
    }
    static getHash(data, controller, ms) {
      const join = Object.values(data).sort().join(",");
      const toHash = [API_KEY, controller, ms, join].join(",");
      const hashed = MD5(toHash);
      return btoa(hashed);
    }
    getData() {
      const ms = Date.now();
      const data = {
        new_sort: 1,
        ...this.params,
      };
      return {
        ...data,
        id: "iws",
        hash: Request.getHash(data, this.controller, ms),
        token: ms,
      };
    }
    async executeAsync() {
      const response = await super.executeAsync();
      const data = response.response;
      if (!("response" in data)) {
        throw new Error(data?.error?.message ?? "Unknown error");
      }
      return data.response;
    }
  }
  class PostRequest extends Request {
    constructor(controller, params) {
      super(API_V1, controller, params);
      this.setHttpMethod("POST");
    }
  }
  class RatingObject {}
  Pikabu.RatingObject = RatingObject;
  class Post extends RatingObject {
    constructor(payload) {
      super();
      this.videos = [];
      this.id = payload.story_id;
      this.pluses = payload.story_pluses ?? 0;
      this.minuses = payload.story_minuses ?? 0;
      this.rating = payload.story_digs ?? (this.pluses - this.minuses);
      this.parseData(payload.story_data);
    }
    parseData(dataArr) {
      for (let data of dataArr) {
        if (data.type.includes('v')) { // v means video (maybe)
          data = data.data;
          let urls = [];
          let extensions = ['mp4', 'webm', 'av1'];
          for (let ext of extensions) {
            if (ext in data && data[ext].url) {
              urls.push(data[ext].url);
            }
          }
          this.videos.push(urls);
        }
      }
    }
  }
  Pikabu.Post = Post;
  class Comment extends RatingObject {
    constructor(payload) {
      super();
      this.id = payload.comment_id;
      this.parentId = payload.parent_id;
      this.rating = payload.comment_rating ?? 0;
      this.pluses = payload.comment_pluses ?? 0;
      this.minuses = payload.comment_minuses ?? 0;
      this.videos = (payload.comment_desc.videos ?? []).flatMap((v) => v.url);
    }
  }
  Pikabu.Comment = Comment;
  class StoryData {
    constructor(payload) {
      this.story = "story" in payload ? new Post(payload.story) : null;
    }
  }
  Pikabu.StoryData = StoryData;
  class CommentsData extends StoryData {
    constructor(payload) {
      super(payload);
      this.selectedCommentId = 0;
      this.comments = payload.comments.map((x) => new Comment(x));
      this.hasMoreComments = payload.has_next_page_comments;
    }
  }
  Pikabu.CommentsData = CommentsData;
  let DataService;
  (function (DataService) {
    async function fetchStory(storyId, commentsPage) {
      const params = {
        story_id: storyId,
        page: commentsPage,
      };
      try {
        const request = new PostRequest("story.get", params);
        const payload = (await request.executeAsync());
        const commentsData = new CommentsData(payload);
        return commentsData;
      }
      catch (error) {
        error(error);
        return null;
      }
    }
    DataService.fetchStory = fetchStory;
  })(DataService = Pikabu.DataService || (Pikabu.DataService = {}));
})(Pikabu || (Pikabu = {}));
//#endregion
//#region RPM API/Nodes
var RPM;
(function (RPM) {
  let Service;
  (function (Service) {
    const DOMAIN = 'https://rpm.pyxiion.ru/';
    // const DOMAIN = 'http://localhost:8000/'
    const USER_REQUEST_QUEUE_PERIOD = 300;
    let PERIOD_MULTIPLIER = 1;
    const USER_REQUEST_QUEUE_AT_ONCE = 50;

    function isAuthorized() {
      return GM_config.get('uuid') !== '';
    }
    Service.isAuthorized = isAuthorized;
    async function register() {
      const response = (await post(DOMAIN + 'register', {}));
      return response.secret;
    }
    Service.register = register;
    async function getFeedbacks() {
      const response = (await get(DOMAIN + 'meta/feedback'));
      return response;
    }
    Service.getFeedbacks = getFeedbacks;
    const userInfoRequestQueue = new Map();
    let isQueueRunning = false;

    function getUserInfo(id) {
      return new Promise((resolve) => {
        if (!userInfoRequestQueue.has(id)) {
          userInfoRequestQueue.set(id, [{
            callback: resolve
          }]);
        }
        else {
          userInfoRequestQueue.get(id).push({
            callback: resolve
          });
        }
        workQueue(USER_REQUEST_QUEUE_PERIOD * PERIOD_MULTIPLIER);
      });
    }
    Service.getUserInfo = getUserInfo;
    async function getBunchOfUserInfo(ids) {
      const body = {
        ids
      };
      const uuid = GM_config.get('uuid');
      if (uuid)
        body.user_uuid = uuid;
      const response = (await post(DOMAIN + 'user/info_bunch', body));
      const users = response.users;
      for (const id in users) {
        postprocessUserInfo(users[id]);
      }
      return users;
    }
    async function workQueue(sleepTime = 0) {
      if (userInfoRequestQueue.size === 0 || isQueueRunning)
        return;
      isQueueRunning = true;
      await sleep(sleepTime);
      // Извлекаем до N уникальных запросов из очереди
      const requestsToProcess = Array.from(userInfoRequestQueue.keys()).slice(0, USER_REQUEST_QUEUE_AT_ONCE);
      const ids = requestsToProcess;
      try {
        const usersInfo = await getBunchOfUserInfo(ids);
        // Вызываем все callback для каждого запроса
        requestsToProcess.forEach(id => {
          const userRequests = userInfoRequestQueue.get(id);
          if (userRequests) {
            const info = usersInfo[id] || null; // null если инфо не найдено
            userRequests.forEach(req => req.callback(info));
          }
          userInfoRequestQueue.delete(id); // Удаляем обработанные запросы
        });
      }
      catch (error) {
        error("Error processing user info requests:", error);
        PERIOD_MULTIPLIER += 1;
      }
      finally {
        isQueueRunning = false;
      }
      // Повторный запуск, если есть еще запросы
      setTimeout(workQueue, USER_REQUEST_QUEUE_PERIOD * PERIOD_MULTIPLIER);
    }

    function postprocessUserInfo(info) {
      if (info.own_vote) {
        // Removes own vote from other votes
        info.pluses -= info.own_vote === 1 ? 1 : 0;
        info.minuses -= info.own_vote === -1 ? 1 : 0;
      }
    }

    function voteUser(id, vote) {
      if (!isAuthorized())
        return null;
      return post(DOMAIN + `user/${id}/vote`, {
        user_uuid: GM_config.get('uuid'),
        vote
      });
    }
    Service.voteUser = voteUser;
    async function post(url, json) {
      const request = new HttpRequest(url, "POST");
      request.setBody(json);
      const response = await request.executeAsync();
      return response.response;
    }
    async function get(url) {
      const request = new HttpRequest(url, "GET");
      const response = await request.executeAsync();
      return response.response;
    }
  })(Service = RPM.Service || (RPM.Service = {}));
  let Nodes;
  (function (Nodes) {
    function createUserRatingNode(uid, infoConsumer = null) {
      const elem = document.createElement('div');
      elem.classList.add('rpm-user-rating', 'hint', 'rpm-not-ready', `rpm-user-rating-${uid}`);
      elem.setAttribute('aria-label', 'Рейтинг автора в RPM');
      elem.setAttribute('pikabu-user-id', uid.toString());

      function addSpan(cls) {
        const e = document.createElement('span');
        e.className = cls;
        elem.appendChild(e);
        e.innerText = '0';
        return e;
      }
      const loadingIcon = document.createElement('div');
      loadingIcon.classList.add('rpm-loading');
      elem.appendChild(loadingIcon);
      const plusElem = addSpan("rpm-pluses");
      addSpan("rpm-rating");
      const minusElem = addSpan("rpm-minuses");
      if (Service.isAuthorized()) {
        plusElem.addEventListener('click', () => UserRating.voteCallback(elem, uid, 1));
        minusElem.addEventListener('click', () => UserRating.voteCallback(elem, uid, -1));
      }
      else {
        const msgCallback = () => sendNotification('Ошибка', 'Авторизируйтесь в системе RPM в настройках скрипта, чтобы голосовать за авторов.');
        plusElem.addEventListener('click', msgCallback);
        minusElem.addEventListener('click', msgCallback);
      }
      UserRating.updateUserRatingElemAsync(elem, infoConsumer);
      return elem;
    }
    Nodes.createUserRatingNode = createUserRatingNode;
    let UserRating;
    (function (UserRating) {
      const userCache = new Map();

      function updateUserRatingElem(elem, info) {
        const pluses = info.pluses + (info.own_vote === 1 ? 1 : 0);
        const minuses = info.minuses + (info.own_vote === -1 ? 1 : 0);
        const rating = pluses - minuses + info.base_rating;
        if (info.own_vote !== undefined && info.own_vote !== null)
          elem.setAttribute('rpm-own-vote', info.own_vote.toString());
        elem.querySelector('.rpm-pluses').innerText = pluses.toString();
        elem.querySelector('.rpm-rating').innerText = rating.toString();
        elem.querySelector('.rpm-minuses').innerText = minuses.toString();
        elem.classList.remove('rpm-not-ready');
      }
      async function updateUserRatingElemAsync(elem, infoConsumer = null) {
        const uid = parseInt(elem.getAttribute('pikabu-user-id'));
        if (uid === null || uid === undefined || Number.isNaN(uid))
          return;
        let info = userCache.get(uid) ?? null;
        if (!info) {
          info = await Service.getUserInfo(uid);
          userCache.set(uid, info);
        }
        if (infoConsumer)
          infoConsumer(info);
        updateUserRatingElem(elem, info);
      }
      UserRating.updateUserRatingElemAsync = updateUserRatingElemAsync;
      async function voteUser(uid, vote) {
        const info = userCache.get(uid);
        info.own_vote = vote;
        updateAll(uid, info);
        const response = await Service.voteUser(uid, vote);
      }

      function updateAll(uid, info) {
        getAllElementsOfUser(uid).forEach(e => updateUserRatingElem(e, info));
      }

      function getAllElementsOfUser(uid) {
        return document.querySelectorAll(`.rpm-user-rating-${uid}`);
      }
      async function voteCallback(elem, uid, btn) {
        if (!Service.isAuthorized()) {
          sendNotification('Ошибка', 'Чтобы проголосовать за автора, нужно зарегистрироваться в системе RPM. Вы можете сделать это в настройках.');
          return;
        }
        const ownVote = parseInt(elem.getAttribute('rpm-own-vote') ?? '0');
        let vote = ownVote;
        if (ownVote === btn)
          vote = 0;
        else
          vote += btn;
        info(vote);
        await voteUser(uid, vote);
        // TODO: check response
      }
      UserRating.voteCallback = voteCallback;
    })(UserRating || (UserRating = {}));
  })(Nodes = RPM.Nodes || (RPM.Nodes = {}));
})(RPM || (RPM = {}));
//#endregion
class Deferred {
  constructor() {
    this.promise = new Promise((resolve, reject) => {
      this.reject = reject;
      this.resolve = resolve;
    });
  }
}

function waitForElement(selector, timeout = 5000) {
  return new Promise((resolve, reject) => {
    const startTime = Date.now();
    const checkExistence = () => {
      const element = document.querySelector(selector);
      if (element) {
        resolve(element);
      }
      else if (Date.now() - startTime >= timeout) {
        reject(new Error(`Element with selector "${selector}" not found within ${timeout}ms`));
      }
      else {
        setTimeout(checkExistence, 100);
      }
    };
    checkExistence();
  });
}
let enableFilters = null;
const isStoryPage = window.location.href.includes("/story/");
const currentStoryId = parseInt(["0", ...window.location.href.split('_')].pop());

function makeEval(args, str, defaultFunc) {
  try {
    return new Function(args, "return " + str);
  }
  catch {
    return defaultFunc;
  }
}
class Formats {}
var SettingEnums;
(function (SettingEnums) {
  let UnrollComments;
  (function (UnrollComments) {
    UnrollComments["NONE"] = "Стандартная пикабушная кнопка";
    UnrollComments["UNROLL_ALL_BUTTON"] = "Дополнительная кнопка \"Раскрыть всё\"";
    UnrollComments["AUTO_UNROLL"] = "Автоматическая раскрутка всех комментариев";
  })(UnrollComments = SettingEnums.UnrollComments || (SettingEnums.UnrollComments = {}));
})(SettingEnums || (SettingEnums = {}));
const formats = new Formats();
let isConfigInit = false;
let frame = document.createElement('div');
document.body.appendChild(frame);
async function handleOldConfigFields() {
  const config = JSON.parse(await GM.getValue('prm', '{}'));
  let changed = false;
  if ('unrollCommentariesAutomatically' in config) {
    if (config.unrollCommentariesAutomatically) {
      config['unrollCommentatries'] = SettingEnums.UnrollComments.AUTO_UNROLL;
    }
    delete config.unrollCommentariesAutomatically;
  }
  if (changed)
    await GM.setValue('prm', JSON.stringify(config));
}
async function handleConfig() {
  await handleOldConfigFields();
  GM_config.init({
    id: "prm",
    title: (() => {
      const div = document.createElement('div');
      const p1 = document.createElement('p');
      p1.textContent = "Return Pikabu minus";
      const links = [];

      function addLink(text, url) {
        const link = document.createElement('a');
        link.href = url;
        link.textContent = text;
        links.push(link);
      }
      addLink("Телеграм", "https://t.me/return_pikabu");
      addLink("GitHub", "https://github.com/PyXiion/Pikabu-Return-Minus");
      div.append(p1, ...links);
      return div;
    })(),
    fields: {
      // ОБЩИЕ НАСТРОЙКИ
      summary: {
        section: [
          "Общие настройки",
        ],
        type: "checkbox",
        default: true,
        label: "Отображение суммарного рейтинга у постов и комментариев.",
      },
      minRatesCountToShowRatingBar: {
        type: "int",
        default: 3,
        label: "Минимальное количество оценок у поста или комментария для отображения соотношения плюсов и минусов. " +
          "Установите на 0, чтобы всегда показывать.",
      },
      // НАСТРОЙКИ ПОСТОВ
      minStoryRating: {
        section: [
          "Настройки постов",
        ],
        type: "int",
        default: 100,
        label: "Посты с рейтингом ниже указанного будут удаляться из ленты. Вы сможете увидеть удалённые посты в списке просмотренных.",
      },
      ratingBar: {
        type: "checkbox",
        default: true,
        label: "Отображение соотношения плюсов и минусов у постов. При отсутствии оценок у поста будет показано соотношение 1:1.",
      },
      showBlockAuthorForeverButton: {
        type: "checkbox",
        default: true,
        label: "Отображение кнопки, которая блокирует автора поста навсегда. То есть добавляет в игнор-лист. " +
          "Вы должны быть авторизированы на сайте, иначе кнопка работать не будет.",
      },
      blockPaidAuthors: {
        type: "checkbox",
        default: true,
        label: "Удаляет из ленты посты от проплаченных авторов (которые с подпиской Пикабу+).",
      },
      videoDownloadButtons: {
        type: "checkbox",
        default: true,
        label: "Добавляет ко всем видео в постах ссылки на источники, если их возможно найти.",
      },
      socialLinks: {
        type: "checkbox",
        default: false,
        label: "Добавляет в начале заголовка поста значки Телеграма, ВК, Тиктока, если в посте есть соответствующие ссылки.",
      },
      // НАСТРОЙКИ КОММЕНТАРИЕВ
      ratingBarComments: {
        section: [
          "Настройки комментариев",
        ],
        type: "checkbox",
        default: true,
        label: "Отображение соотношения плюсов и минусов у комментариев.",
      },
      allCommentsLoadedNotification: {
        type: "checkbox",
        default: false,
        label: "Показывать уведомление о загрузке всех комментариев под постом."
      },
      commentVideoDownloadButtons: {
        type: "checkbox",
        default: true,
        label: "Добавляет ко всем видео в комментариях ссылки на источники, если их возможно найти."
      },
      unrollCommentariesAutomatically: {
        type: "hidden",
        default: undefined
      },
      unrollCommentaries: {
        type: 'select',
        label: 'Раскрутка комментариев.',
        options: [
          SettingEnums.UnrollComments.NONE,
          SettingEnums.UnrollComments.UNROLL_ALL_BUTTON,
          SettingEnums.UnrollComments.AUTO_UNROLL
        ],
        default: SettingEnums.UnrollComments.NONE
      },
      // ВКЛАДКИ ПИКАБ
      hotTab: {
        section: [
          "Вкладки Пикабу",
          "Включает/выключает вкладки сверху. Работает только на ПК"
        ],
        type: "checkbox",
        default: true,
        label: "Горячее",
      },
      bestTab: {
        type: "checkbox",
        default: true,
        label: "Лучшее",
      },
      newTab: {
        type: "checkbox",
        default: true,
        label: "Свежее",
      },
      subsTab: {
        type: "checkbox",
        default: true,
        label: "Подписки",
      },
      communitiesTab: {
        type: "checkbox",
        default: true,
        label: "Сообщества",
      },
      blogsTab: {
        type: "checkbox",
        default: true,
        label: "Блоги",
      },
      expertsTab: {
        type: "checkbox",
        default: true,
        label: "Эксперты",
      },
      // НАСТРОЙКИ RPM
      rpmEnabled: {
        section: ["Настройки RPM", "Дополнительные функции скрипта. Используется сервер rpm.pyxiion.ru"],
        type: "checkbox",
        default: true,
        label: "Включить для постов."
      },
      rpmMinStoryRating: {
        type: "int",
        default: 0,
        label: "Минимальный рейтинг автора в системе RPM. Если рейтинг автора меньше его значения, то его посты будут удалены из ленты."
      },
      rpmIgnoreDownvoted: {
        type: "checkbox",
        default: true,
        label: "Скрытие постов с вашим минусом в системе RPM. Типа игнор-листа."
      },
      rpmComments: {
        type: "checkbox",
        default: true,
        label: "Включить для комментариев."
      },
      registerRpm: {
        type: "button",
        label: "Зарегистрироваться в системе RPM. После нажатия страница перезагрузится.",
        async click() {
          const uuid = GM_config.get('uuid');
          if (uuid === null || uuid === undefined || uuid === '') {
            GM_config.set('uuid', await RPM.Service.register());
            GM_config.save();
            sendNotification('Успешно', 'Вы успешно зарегистрировались. Или нет. Проверки успешности не существует.');
            await sleep(300);
            window.location.reload();
          }
          else {
            sendNotification('Вы уже зарегистрированы', 'Вы не можете зарегистрироватся ещё раз.');
          }
        }
      },
      // БОЛЕЕ СЛОЖНЫЕ НАСТРОЙКИ
      filteringPageRegex: {
        section: ["Продвинутые настройки"],
        type: "text",
        label: "Страницы, на которых работает фильтрация по рейтингу (регулярное выражение).",
        default: "^https?:\\/\\/pikabu.ru\\/(|best|companies|browse|disputed|most-saved)$",
      },
      minusesPattern: {
        type: "text",
        default: "story.minuses",
        label: "Шаблон отображения минусов у постов (JS). Пример: `story.minuses * 5000`. story: {id, rating, pluses, minuses}. Внутри может выполняться любой код, поэтому используйте с осторожностью.\n" +
          "Шаблоны гарантированно работают только на Tampermonkey.",
      },
      minusesCommentPattern: {
        type: "text",
        default: "comment.minuses",
        label: "Шаблон отображения минусов у комментариев (JS). Пример: `comment.minuses * 5000`. comment: {id, rating, pluses, minuses}.",
      },
      ownCommentPattern: {
        type: "text",
        default: "comment.pluses == 0 && comment.minuses == 0 ? 0 : comment.pluses == comment.minuses ? `+${comment.pluses} / -${comment.minuses}` : comment.pluses == 0 ? `-${comment.minuses}` : comment.minuses == 0 ? `+${comment.pluses}` : `+${comment.pluses} / ${comment.rating} / -${comment.minuses}`",
        label: "Шаблон отображения рейтинга у ВАШИХ комментариев (JS). Пример: `comment.minuses * 5000`. comment: {id, rating, pluses, minuses}.",
      },
      analytics: {
        type: "checkbox",
        label: "Отправка всякой информации на сервера RPM, если включено. Пока что никакой информации не собирается, но вы можете выключить это заранее, если таковая появится.",
        default: true
      },
      debug: {
        type: "checkbox",
        label: "Включить дополнительные логи в консоли. Для разработки и отладки.",
        default: false
      },
      uuid: {
        type: "hidden",
        // label:
        //   "Ваш уникальный UUID в системе рейтинга RPM. Позволяет вам оценивать профили других пользователей.",
        default: ''
      },
    },
    events: {
      init() {
        isConfigInit = true;
        formats.formatStoryMinuses = makeEval("story", this.get('minusesPattern'), (x) => x.minuses);
        formats.formatCommentMinuses = makeEval("comment", this.get('minusesCommentPattern'), (x) => x.minuses);
        formats.formatOwnComment = makeEval("comment", this.get('ownCommentPattern'), (x) => (x.pluses == 0 && x.minuses == 0) ? 0 : `${x.pluses}/${x.minuses}`);
        enableFilters = new RegExp(this.get('filteringPageRegex')).test(window.location.href);
        this.css.basic = [];
        if (this.get('unrollCommentariesAutomatically')) {
          this.set('unrollCommentatries', SettingEnums.UnrollComments.AUTO_UNROLL);
          this.set('unrollCommentariesAutomatically', null);
          this.save();
        }
      }
    },
    frame: frame
  });
}
handleConfig();
const logPrefix = "[RPM]";

function info(...args) {
  if (GM_config.get('debug')) {
    console.info(logPrefix, ...args);
  }
}

function warn(...args) {
  if (GM_config.get('debug')) {
    console.warn(logPrefix, ...args);
  }
}

function error(...args) {
  if (GM_config.get('debug')) {
    console.error(logPrefix, ...args);
  }
}
const waitConfig = new Promise((resolve) => {
  let isInit = () => setTimeout(() => (isConfigInit ? resolve() : isInit()), 1);
  isInit();
});
const supportMenuCommands = GM.registerMenuCommand !== undefined;
GM.registerMenuCommand("Открыть настройки", () => {
  info("Открыты настройки.");
  GM_config.open();
});

function addCss(css) {
  const styleSheet = document.createElement("style");
  styleSheet.innerText = css;
  // is added to the end of the body because it must override some of the original styles
  document.body.appendChild(styleSheet);
  info("Добавлен CSS");
}
class CommentData {
  constructor(data) {
    this.id = data.id;
    this.rating = data.rating;
    this.pluses = data.pluses;
    this.minuses = data.minuses;
    this.videos = data.videos;
  }
}
// Variables
const cachedComments = new Map();
let oldInterface = null;
const deferredComments = new Map();
const cachedPostVideos = new Map();
const blockIconTemplate = (function () {
  const div = document.createElement("div");
  div.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="icon icon--ui__save"><use xlink:href="#icon--ui__ban"></use></svg>`;
  return div.firstChild;
})();
// UI functions
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}
async function sendNotification(title, description, timeout = 2000) {
  function construct() {
    const notification = document.createElement('div');
    notification.classList.add('rpm-notification');
    const header = document.createElement('div');
    header.classList.add('rpm-notification-header');
    header.textContent = title;
    const content = document.createElement('div');
    content.classList.add('rpm-notification-content');
    content.textContent = description;
    notification.append(header, content);
    return notification;
  }
  const notification = construct();
  document.body.append(notification);
  const animationTime = 600.0;
  notification.style.animationDuration = `${animationTime / 1000.0}s`;
  notification.style.animationTimingFunction = "cubic-bezier(.18,.89,.32,1.28)";
  // Intro
  notification.style.animationName = "rpm-notification-intro";
  await sleep(animationTime);
  await sleep(timeout);
  // Outro
  notification.style.animationTimingFunction = "linear";
  notification.style.animationName = "rpm-notification-outro";
  notification.style.opacity = "0";
  await sleep(animationTime);
  // Remove
  notification.remove();
}
// Functions
async function blockAuthorForever(button, authorId) {
  button.disabled = true;
  // const fetch = unsafeWindow.fetch;
  try {
    await fetch(`https://pikabu.ru/ajax/ignore_actions.php?authors=${authorId}&story_id=0&period=forever&action=add_rule`, {
      method: "POST",
    });
    button.remove();
    info("Автор с ID", authorId, "заблокирован");
  }
  catch {
    button.disabled = false;
    error("Не получилось заблокировать автора с ID", authorId, ", возможно отсутствует Интернет-соединение");
  }
}

function addBlockButton(story) {
  const saveButton = story.querySelector(".story__save");
  if (saveButton === null) {
    warn("Failed to add a block button to", story);
    return;
  }
  const button = document.createElement("button");
  button.classList.add("rpm-block-author", "hint");
  button.setAttribute("aria-label", "Заблокировать автора навсегда");
  button.appendChild(blockIconTemplate.cloneNode(true));
  const authorId = parseInt(story.getAttribute("data-author-id"));
  button.addEventListener("click", () => {
    blockAuthorForever(button, authorId);
  });
  saveButton.parentElement.insertBefore(button, saveButton);
}

function processComment(comment) {
  const commentElem = document.getElementById(`comment_${comment.id}`);
  if (commentElem === null) {
    if (comment instanceof Pikabu.Comment) {
      cachedComments[comment.id] = new CommentData(comment);
      info('Закэшировал комментарий', comment.id);
    }
    return;
  }
  if (GM_config.get('rpmComments'))
    processCommentRpm(commentElem);
  const userElem = commentElem.querySelector(".comment__user");
  const ratingDown = commentElem.querySelector(".comment__rating-down");
  if (!userElem || !ratingDown) {
    // Defer comment
    info('У комментария', comment.id, ' нет юзера или кнопок рейтинга, кэширую его');
    // setTimeout(() => processComment(comment), 400);
    cachedComments[comment.id] = (comment instanceof Pikabu.Comment) ? new CommentData(comment) : comment;
    return;
  }
  if (userElem.hasAttribute("data-own") &&
    userElem.getAttribute("data-own") === "true") {
    const textRatingElem = commentElem.querySelector(".comment__rating-count");
    textRatingElem.innerText = formats.formatOwnComment(comment);
    info('Обработал "свой" комментарий', comment.id);
    return;
  }
  const minusesText = document.createElement("div");
  minusesText.classList.add("comment__rating-count");
  ratingDown.prepend(minusesText);
  minusesText.textContent = formats.formatCommentMinuses(comment);
  if (GM_config.get('summary')) {
    const summary = document.createElement("div");
    summary.classList.add("comment__rating-count", "rpm-summary");
    summary.textContent = comment.rating.toString();
    ratingDown.parentElement.insertBefore(summary, ratingDown);
  }
  const totalRates = comment.pluses + comment.minuses;
  if (GM_config.get('ratingBarComments') &&
    totalRates >= GM_config.get('minRatesCountToShowRatingBar')) {
    let ratio = 0.5;
    if (totalRates > 0)
      ratio = comment.pluses / totalRates;
    addRatingBar(commentElem, ratio);
  }
  // Comment videos
  if (GM_config.get('commentVideoDownloadButtons')) {
    const videoElements = commentElem.querySelectorAll(':scope > .comment__body .comment-external-video');
    const videoCount = Math.min(videoElements.length, comment.videos.length);
    for (let i = 0; i < videoCount; ++i) {
      const elem = videoElements[i];
      const url = comment.videos[i];
      const linkElem = document.createElement('a');
      linkElem.classList.add('rpm-download-video-button');
      linkElem.href = url;
      linkElem.text = 'Источник';
      linkElem.target = '_blank';
      elem.parentNode.insertBefore(linkElem, elem.nextSibling);
    }
  }
  info('Обработал комметарий', comment.id);
}
async function processStoryComments(storyId, storyData, page) {
  if (!isStoryPage || storyId != currentStoryId) {
    return;
  }
  for (const comment of storyData.comments) {
    processComment(comment);
  }
  if (storyData.hasMoreComments) {
    storyData = await Pikabu.DataService.fetchStory(storyId, page + 1);
    await processStoryComments(storyId, storyData, page + 1);
  }
  else if (GM_config.get('allCommentsLoadedNotification')) {
    sendNotification('Return Pikabu Minus', 'Все рейтинги комментариев загружены!');
  }
}

function addRatingBar(story, ratio) {
  const block = story.querySelector(".story__rating-block, .comment__body, .story__emotions");
  if (block !== null) {
    const bar = document.createElement("div");
    const inner = document.createElement("div");
    bar.append(inner);
    bar.classList.add("rpm-rating-bar");
    inner.classList.add("rpm-rating-bar-inner");
    inner.style.height = (ratio * 100).toFixed(1) + "%";
    block.prepend(bar);
  }
  else {
    // TODO mobile
  }
}
const linkTypes = [
  // Telegram
  {
    domains: [
      "t.me"
    ],
    iconHtml: `<svg xmlns="http://www.w3.org/2000/svg" class="rpm-story-icon icon icon--social__telegram"><use xlink:href="#icon--social__telegram"></use></svg>`,
    style: "fill: #24A1DE;",
  },
  // VK
  {
    domains: [
      "vk.com"
    ],
    iconHtml: `<svg xmlns="http://www.w3.org/2000/svg" class="icon icon--social__vk"><use xlink:href="#icon--social__vk"></use></svg>`,
    style: "fill: black;",
  },
  // TIKTOK
  {
    domains: [
      "tiktok.com"
    ],
    iconHtml: `<svg class="icon" fill="#000000" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" xml:space="preserve"><path d="M19.589 6.686a4.793 4.793 0 0 1-3.77-4.245V2h-3.445v13.672a2.896 2.896 0 0 1-5.201 1.743l-.002-.001.002.001a2.895 2.895 0 0 1 3.183-4.51v-3.5a6.329 6.329 0 0 0-5.394 10.692 6.33 6.33 0 0 0 10.857-4.424V8.687a8.182 8.182 0 0 0 4.773 1.526V6.79a4.831 4.831 0 0 1-1.003-.104z"/></svg>`
  },
  // Boosty
  {
    domains: [
      "boosty.to"
    ],
    iconHtml: `<svg class="icon" fill="#000000" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="50 50 217.4 197.4">
                <style type="text/css">
                  .st0{fill:#242B2C;}
                  .st1{fill:url(#SVGID_1_);}
                </style>
                <g id="sign">
                  <g id="b_1_">
                    <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="188.3014" y1="75.5591" x2="123.8106" y2="295.4895">
                      <stop offset="0" style="stop-color:#EF7829"/>
                      <stop offset="5.189538e-02" style="stop-color:#F07529"/>
                      <stop offset="0.3551" style="stop-color:#F0672B"/>
                      <stop offset="0.6673" style="stop-color:#F15E2C"/>
                      <stop offset="1" style="stop-color:#F15A2C"/>
                    </linearGradient>
                    <path class="st1" d="M87.5,163.9L120.2,51h50.1l-10.1,35c-0.1,0.2-0.2,0.4-0.3,0.6L133.3,179h24.8c-10.4,25.9-18.5,46.2-24.3,60.9    c-45.8-0.5-58.6-33.3-47.4-72.1 M133.9,240l60.4-86.9h-25.6l22.3-55.7c38.2,4,56.2,34.1,45.6,70.5C225.3,207,179.4,240,134.8,240    C134.5,240,134.2,240,133.9,240z"/>
                  </g>
                </g>
              </svg>`
  }
];
async function checkStoryLinks(story) {
  function addIcon(linkType, element) {
    const elem = element.cloneNode();
    elem.innerHTML = linkType.iconHtml.trim();
    if (linkType.style) {
      elem.setAttribute("style", linkType.style);
    }
    elem.classList.add('rpm-story-icon');
    // Add before the title
    const titleElem = story.querySelector('.story__title');
    titleElem.prepend(elem);
  }
  const linkElems = Array.from(story.querySelectorAll('.story__content a'));
  linkElems.reverse();
  // Iterate reversibly so that the last link is added to the icon list
  linkTypeFor: for (const linkType of linkTypes) {
    for (const domain of linkType.domains) {
      for (const linkElem of linkElems) {
        if (linkElem.href.includes(domain)) {
          addIcon(linkType, linkElem);
          continue linkTypeFor;
        }
      }
    }
  }
}

function removeStory(storyElem, reason, keepUser = false) {
  const titleElem = storyElem.querySelector('.story__title a.story__title-link');
  if (titleElem === null)
    return;
  const title = titleElem.textContent;
  const url = titleElem.href;
  const placeholder = document.createElement('div');
  placeholder.classList.add('rpm-placeholder');
  const urlElem = document.createElement('a');
  urlElem.textContent = title;
  urlElem.href = url;
  const userInfo = storyElem.querySelector('.story__user-info');
  if (keepUser && userInfo) {
    const userInfoContainer = document.createElement('div');
    userInfoContainer.append(userInfo.cloneNode(true));
    userInfoContainer.classList.add('rpm-user-info-container');
    // Update RPM ratings
    for (const ratingElem of userInfoContainer.querySelectorAll('.rpm-user-rating')) {
      const uid = parseInt(ratingElem.getAttribute('pikabu-user-id'));
      ratingElem.replaceWith(RPM.Nodes.createUserRatingNode(uid));
    }
    placeholder.append(urlElem, ` скрыт: ${reason}.`, userInfoContainer);
  }
  else {
    placeholder.append(urlElem, ` скрыт: ${reason}.`);
  }
  storyElem.parentElement.insertBefore(placeholder, storyElem);
  const collapseButton = document.createElement('div');
  collapseButton.classList.add("collapse-button", "collapse-button_active");
  collapseButton.append(document.createElement('div'), document.createElement('div'));
  collapseButton.addEventListener('click', () => {
    if (collapseButton.classList.contains('collapse-button_active'))
      collapseButton.classList.remove('collapse-button_active');
    else
      collapseButton.classList.add('collapse-button_active');
  });
  placeholder.prepend(collapseButton);
}

function processOldStory(story, storyData) {
  let ratingElem = story.querySelector(".story__footer-rating > div");
  let isMobile = false;
  if (ratingElem !== null) {
    // mobile
    isMobile = true;
  }
  else {
    // pc
    ratingElem = story.querySelector(".story__left .story__rating-block");
  }
  if (ratingElem === null) {
    return false;
  }
  oldInterface = true;
  let ratingDown = ratingElem.querySelector(".story__rating-minus, .story__rating-down");
  if (isMobile) {
    const buttonMinus = document.createElement("button");
    buttonMinus.classList.add("story__rating-minus");
    buttonMinus.innerHTML = `
    <span class="story__rating-rpm-count">${storyData.story.minuses}</span>
    <span type="button" class="tool story__rating-down" data-role="rating-down">
      <svg xmlns="http://www.w3.org/2000/svg" class="icon icon--ui__rating-down icon--ui__rating-down_story">
        <use xlink:href="#icon--ui__rating-down"></use>
      </svg>
    </span>`;
    ratingDown.replaceWith(buttonMinus);
    ratingDown = buttonMinus;
  }
  else {
    const minusesCounter = document.createElement("div");
    minusesCounter.classList.add("story__rating-count");
    minusesCounter.textContent = formats.formatStoryMinuses(storyData.story);
    ratingDown.prepend(minusesCounter);
  }
  if (GM_config.get('summary')) {
    const summary = document.createElement("div");
    if (isMobile)
      summary.classList.add("story__rating-rpm-count", "rpm-summary");
    else
      summary.classList.add("story__rating-count", "rpm-story-summary");
    summary.textContent = storyData.story.rating.toString();
    ratingDown.parentElement.insertBefore(summary, ratingDown);
  }
  const totalRates = storyData.story.pluses + storyData.story.minuses;
  if (GM_config.get('ratingBar') && totalRates >= GM_config.get('minRatesCountToShowRatingBar')) {
    let ratio = 0.5;
    if (totalRates > 0)
      ratio = storyData.story.pluses / totalRates;
    addRatingBar(story, ratio);
  }
  processStoryComments(storyData.story.id, storyData, 1);
  return true;
}
async function processStory(story, processComments) {
  // Block author button
  if (GM_config.get('showBlockAuthorForeverButton')) {
    addBlockButton(story);
  }
  // Links
  if (GM_config.get('socialLinks')) {
    checkStoryLinks(story);
  }
  // Block paid stories
  if (enableFilters &&
    GM_config.get('blockPaidAuthors') &&
    story.querySelector(".user__label[data-type=\"pikabu-plus\"]") !== null) {
    removeStory(story, "подписка Пикабу+", true);
    info("Удалил пост", story, "как проплаченный.");
    return;
  }
  const storyId = parseInt(story.getAttribute("data-story-id"));
  // get story data
  const storyData = await Pikabu.DataService.fetchStory(storyId, 1);
  if (storyData === null || storyData === undefined) {
    warn("Не удалось получить пост #", storyId);
    return;
  }
  // delete the story if its ratings < the min rating
  if (enableFilters &&
    storyData.story.rating < GM_config.get('minStoryRating')) {
    removeStory(story, `рейтинг поста (${storyData.story.rating})`);
    info("Удалил пост", story, "по фильтру рейтинга.");
    return;
  }
  // videos
  if (GM_config.get('videoDownloadButtons'))
    processPostVideos(story, storyData);
  if (GM_config.get('rpmEnabled'))
    processStoryRpm(story);
  processOldStory(story, storyData);
}
async function processStoryRpm(story) {
  const uid = parseInt(story.getAttribute('data-author-id'));
  const userInfoRowElem = story.querySelector('.story__community_after-author-panel, .story__user-info');
  const footerElem = story.querySelector('.story__footer-tools .story__comments-link.story__to-comments');

  function ratingCallback(userInfo) {
    if (!enableFilters)
      return;
    const rating = userInfo.base_rating + userInfo.pluses - userInfo.minuses + (userInfo.own_vote ?? 0);
    if (rating < GM_config.get('rpmMinStoryRating')) {
      removeStory(story, `RPM-рейтинг (${rating})`, true);
    }
  }
  const elem = RPM.Nodes.createUserRatingNode(uid, ratingCallback);
  if (userInfoRowElem)
    userInfoRowElem.prepend(elem);
  else
    footerElem.parentElement.insertBefore(elem, footerElem);
}

function getCommentAuthorId(comment) {
  if (comment.hasAttribute('data-author-id')) {
    return parseInt(comment.getAttribute('data-author-id'));
  }
  if (comment.hasAttribute('data-meta')) {
    return parseInt(comment.getAttribute('data-meta').match(/(?:^|;)aid=(\d+)(?:;|$)/)[1]);
  }
  return null;
}

function processCommentRpm(comment) {
  const uid = getCommentAuthorId(comment);
  if (!uid)
    return;
  const commentHeader = comment.querySelector('.comment__header');
  info(comment, uid);
  const elem = RPM.Nodes.createUserRatingNode(uid);
  commentHeader.insertBefore(elem, commentHeader.querySelector('.comment__right'));
}
async function processStories(stories) {
  for (const story of stories) {
    processStory(story, false);
  }
}

function processCached(commentElem) {
  const commentId = parseInt(commentElem.getAttribute("data-id"));
  if (commentId in cachedComments) {
    processComment(cachedComments[commentId]);
    delete cachedComments[commentId];
  }
}

function processPostVideos(story, storyData) {
  function getPostPlayers() {
    return Array.from(story.querySelectorAll('.story-block_type_video'));
  }

  function createUrl(text, url) {
    const urlElem = document.createElement('a');
    urlElem.target = '_blank';
    urlElem.textContent = text;
    urlElem.href = url;
    return urlElem;
  }

  function addUrlToPlayer(videoBlock, urls) {
    const player = videoBlock.querySelector('.player');
    // to check video origin
    const dataType = player.getAttribute("data-type");
    const urlListElem = document.createElement('p');
    urlListElem.classList.add('rpm-video-list');
    // if it's a pikabu video
    if (dataType == "video-file") {
      for (const url of urls) {
        const extension = '.' + url.split('.').pop();
        urlListElem.appendChild(createUrl(extension, url));
      }
    }
    else {
      // try get video url
      const dataSource = player.getAttribute('data-source');
      if (dataSource)
        urlListElem.appendChild(createUrl('Источник', dataSource));
    }
    if (urlListElem.hasChildNodes())
      videoBlock.parentElement.insertBefore(urlListElem, videoBlock.nextSibling);
    else
      urlListElem.remove();
  }
  const playerElements = getPostPlayers();
  for (const i in storyData.story.videos) {
    const player = playerElements[i];
    const videoUrls = storyData.story.videos[i];
    addUrlToPlayer(player, videoUrls);
  }
}

function addVideoDownloadButtons(postId, url) {
  const videoControls = Array.from(document.querySelectorAll(`.story[data-story-id="${postId}"] .player__controls`));

  function addButton(link, videoControls) {
    const a = document.createElement("a");
    a.classList.add("rpm-download-video-button");
    const name = link.split("/").pop(); // "https://example/com/some_cool_video.mp4" -> "some_cool_video.mp4"
    const extension = name.split(".").slice(1).join("."); // "some_cool_video.mp4" -> "mp4" (and "video.av1.mp4" -> "av1.mp4")
    a.href = link;
    a.download = name;
    a.textContent = extension;
    // add link to controls
    videoControls.append(a);
  }
  const videos = cachedPostVideos[postId];
  for (const i in videoControls) {
    for (const url of videos[i]) {
      addButton(url, videoControls[i]);
    }
  }
}

function mutationsListener(mutationList, observer) {
  for (const mutation of mutationList) {
    if (mutation.type === 'childList') {
      for (const node of mutation.addedNodes) {
        if (!(node instanceof HTMLElement))
          continue;
        if (node.hasAttribute('rpm-observer-ignore'))
          continue;
        if (node.matches(".comment__header")) {
          const commentElem = node.closest(".comment");
          info('Поймал голову комментария!', commentElem);
          processCached(commentElem);
        }
        else if (node.matches("article.story")) {
          const storyElem = node;
          info('Поймал пост!', storyElem);
          processStory(storyElem, false);
        }
        else if (node.matches(".comment__more:not(.rpm-unroll-all)")) {
          commentMoreBtn();
        }
      }
    }
  }
}
var observer = null;

function addSettingsOpenButton() {
  let block =
    // mobile version
    document.querySelector('.footer__links .accordion')
    // else PC version
    ??
    document.querySelector(".sidebar .sidebar__inner");
  if (block === null) {
    error("Не удалось найти место для создания кнопки открытия настроек.");
    return;
  }
  const button = document.createElement('button');
  button.innerText = "Открыть настройки Return Pikabu minus";
  button.classList.add('rpm-open-settings-button');
  button.addEventListener('click', () => {
    button.disabled = true;
    GM_config.open();
    button.disabled = false;
  });
  block.appendChild(button);
}
var FeedbackManager;
(function (FeedbackManager) {
  async function init() {
    const settings = await getSettings();
    const isToday = settings.lastCheckDate.toDateString() === new Date().toDateString();
    if (!isToday) {
      const feedbacks = await RPM.Service.getFeedbacks();
      feedbacks.forEach(feedback => {
        if (settings.completed.includes(feedback.id))
          return;
        if (settings.saved.find(([, fb]) => fb.id == feedback.id) !== undefined)
          return;
        settings.saved.push([new Date(), feedback]);
        showFeedback(feedback);
      });
      settings.lastCheckDate = new Date();
      await updateSettings(settings);
    }
    else {
      showSavedFeedback();
    }
  }
  FeedbackManager.init = init;
  async function showSavedFeedback() {
    const settings = await getSettings();
    const now = new Date();
    const saved = settings.saved.filter(([date]) => date <= now).map(([, fb]) => fb);
    if (saved.length === 0)
      return;
    for (const fb of saved) {
      await showFeedback(fb);
    }
  }
  async function getSettings() {
    const settings = JSON.parse(await GM.getValue("rpm-feedback", JSON.stringify({
      lastCheckDate: (() => {
        const date = new Date();
        date.setDate(date.getDate() - 1);
        return date;
      })(),
      completed: [],
      saved: []
    })));
    settings.lastCheckDate = new Date(settings.lastCheckDate);
    for (const entry of settings.saved) {
      entry[0] = new Date(entry[0]);
    }
    return settings;
  }

  function updateSettings(settings) {
    settings.saved = settings.saved.filter(fb => !settings.completed.includes(fb[1].id));
    return GM.setValue("rpm-feedback", JSON.stringify(settings));
  }
  async function markCompleted(feedbackId) {
    const settings = await getSettings();
    settings.completed.push(feedbackId);
    await updateSettings(settings);
  }
  let isFeedbackActive = false;

  function showFeedback(feedback) {
    if (isFeedbackActive)
      return Promise.resolve();
    isFeedbackActive = true;
    const deffered = new Deferred();
    const feedbackElem = document.createElement('div');
    feedbackElem.classList.add('rpm-feedback-window');
    const iframe = document.createElement('iframe');
    iframe.src = feedback.iframe_url;
    const completedButton = document.createElement('button');
    completedButton.classList.add('rpm-completed');
    completedButton.textContent = 'Закрыть';
    completedButton.addEventListener('click', () => {
      feedbackElem.remove();
      markCompleted(feedback.id);
      deffered.resolve();
    });
    feedbackElem.append(iframe, completedButton);
    document.body.append(feedbackElem);
    return deffered.promise;
  }
})(FeedbackManager || (FeedbackManager = {}));
async function processTabs() {
  const tabConfig = {
    hot: 'hotTab',
    best: 'bestTab',
    new: 'newTab',
    my_lent: 'subsTab',
    communities: 'communitiesTab',
    companies: 'blogsTab',
    experts: 'expertsTab'
  };
  await waitForElement('.header-menu__item');
  Object.entries(tabConfig).forEach(([key, field]) => {
    const selector = `.header-menu__item[data-feed-key="${key}"]`;
    if (!GM_config.get(field)) {
      const element = document.querySelector(selector);
      if (element) {
        element.remove();
      }
    }
  });
}

function init() {
  window.addEventListener("load", onLoad);
  main();
  // FeedbackManager.init();
}
async function main() {
  await waitConfig;
  if (GM_config.get('uuid')) {
    delete GM_config.fields['registerRpm'];
  }
  addCss(`.story__footer .story__rating-up {
  margin-right: 5px !important;
}
.prm-minuses {
  padding-left: 7px !important;
  margin: 0px !important;
}
.story__rating-count {
  margin: 7px 0 7px;
}
.rpm-story-summary {
  margin: 14px 0 4px;
}
.rpm-summary-comment {
  margin-right: 8px;
}
.comment__rating-down .comment__rating-count {
  margin-right: 8px;
}
.comment__rating-down {
  padding: 2px 8px;
}
.story__footer .story__rating-rpm-count {
  font-size: 13px;
  color: var(--color-black-700);
  margin: auto 7px auto 7px;
  line-height: 0;
  display: block;
}
.story__footer .rpm-summary,
.comment .rpm-summary {
  margin: auto 9px auto 0px;
  font-weight: 500;
}
.comment__rating-rpm-count {
  padding: 2px 8px;
  flex-shrink: 0;
  margin-left: auto;
}
.rpm-rating-bar {
  width: 5px;
  background: var(--color-danger-800);
  height: 90%;
  position: absolute;
  right: -9.5px;
  top: 5%;
  border-radius: 5px;
}
.rpm-rating-bar-inner {
  background: var(--color-primary-700);    /* width: 99%; */
  border-radius: 5px;
}
.comment__body {
  position: relative;
}
.comment .rpm-rating-bar {
  height: 70px;
  top: 15px;
  left: -10px;
}  /* old mobile interface */
.story__footer-rating .story__rating-minus {
  background-color:var(--color-black-300);
  border-radius:8px;
  overflow:hidden;
  padding:0;
  display:flex;
  align-items:center  ;
}
.story__footer-rating .story__rating-down {
  display:flex;
  align-items:center;
  justify-content:center;
  padding-left:2px  ;
}
.rpm-download-video-button {
  margin-left: 3px;
}
.rpm-block-author {
  overflow:hidden;
  margin-right:24px;
  cursor:pointer;
  display:flex;
  align-items:center;
  padding:0;
  background:0 0
}
.rpm-block-author:hover * {
  fill: var(--color-danger-800);
}
.story__footer-tools-inner .rpm-block-author {
  overflow: visible;
  margin-right: auto;
  margin-left:8px;
  transform: scale(1.3);
}
.rpm-open-settings-button {
  margin-top: 10px;
  text-align: center;
  width: 100%;
  font-size: 0.9em;
}
.rpm-video-list {
  text-align: center;
}
.rpm-video-list a {
  margin: 0 5px;
}
.rpm-story-icon {
  width: 24px;
  height: 24px;
  padding: 0 4px;
  margin-right: 6px;
  vertical-align: text-top;
  border-radius: 3px;
}
.rpm-story-icon svg {
  margin-top: auto;
  padding: 2px 0px;
  width: 16px;
  height: 16px;
  margin: 0;
  transition: all ease 300ms;
}
.rpm-story-icon:hover svg {
  width: 18px;
  height: 18px;
}
@media only screen and (max-width: 768px)  {
  #prm {
    left: 2.5% !important;
    width: 100% !important;
  }
}  /* Notifications */
@keyframes rpm-notification-intro {
  from {
    translate: -100%;
  }
  to {
    translate: 0%;
  }
}
@keyframes rpm-notification-outro {
  from {
    translate: 0 0%;
    opacity: 1.0;
  }
  to {
    translate: 0 -200%;
    opacity: 0;
  }
}
.rpm-notification {
  position: fixed;
  bottom: 15px;
  left: 15px;
  z-index: 9999;
  overflow: hidden;
  max-width: 250px;
  background: var(--color-black-100);
  border-radius: 10px;
  border: 1px solid var(--color-black-440);
  pointer-events: none;
}
.rpm-notification * {
  padding: 5px 15px;
}
.rpm-notification-header {
  background: var(--color-primary-800);
  color: white;
  font-weight: bolder;
}
.rpm-user-rating {
  font-size: 1.05em;
  padding: 0.3em 0.6em;
  border-radius: 1rem;
  display: inline-block;
  background-color: var(--color-black-alpha-005);
  user-select: none;
  white-space: nowrap;
  line-height: 1em;
}
.story__main .rpm-user-rating,
.rpm-placeholder .rpm-user-rating {
  margin-right: 10px;
}
.comment__header .rpm-user-rating {
  margin-left:auto;
  right: 0;
}
.rpm-user-rating + .comment__right {
  margin-left: unset;
}
.rpm-user-rating span {
  display: inline-block;
  min-width: 1.5ch;
  text-align: center;
}
.rpm-user-rating .rpm-rating {
  margin: 0 1ch;
}
.rpm-user-rating .rpm-pluses {
  color: var(--color-primary-700);
  cursor: pointer;
}
.rpm-user-rating .rpm-minuses {
  color: var(--color-danger-900);
  cursor: pointer;
}
.rpm-user-rating[rpm-own-vote="1"] {
  background-color: var(--color-primary-200)
}
.rpm-user-rating[rpm-own-vote="-1"] {
  background-color: var(--color-danger-200)
}
.rpm-placeholder {
  background-color: var(--color-bright-800);
  border: 1px solid var(--color-black-430);
  border-radius: 15px;
  width: max-content;
  height: max-content;
  text-align: center;
  margin: 10px auto 0;
  padding: 5px 20px;
  position: relative;
  max-width: 95%;
}
.rpm-placeholder .rpm-user-info-container {
  width: max-content;
  padding: 0.5em;
  border-radius: 10px;
  margin: 0 auto;
}
.rpm-placeholder .collapse-button {
  position: absolute;
  top: 0;
  left: -70px;
  translate: 0 -70%;
}
.mv .rpm-placeholder {
  font-size: 0.825em;
}
.mv .rpm-placeholder .collapse-button {
  display: inline flow-root list-item;
  position: initial;
  margin: 0 auto;
  translate: 0 0;
  transform: scale(0.75);
}
.rpm-placeholder:has(.collapse-button_active) + article {
  display: none;
}
.rpm-loading {
  display: none;
  min-width: 4px;
  min-height: 4px;
  border: 7px solid var(--color-primary-400);
  border-top: 7px solid var(--color-primary-700);
  border-radius: 50%;
  animation: spin 1.5s linear infinite;
}
.rpm-not-ready * {
  display: none !important;
}
.rpm-not-ready .rpm-loading {
  display: block !important;
}
.rpm-feedback-window {
  position: fixed;
  display: flex;
  left: 70px;
  top: 100px;
  width: 450px;
  height: 50%;
  border: 1px solid var(--color-black-430);
  border-radius: 8px;
  background-color: var(--color-bright-800);
  padding: 0;
  flex-direction: column;
}
.rpm-feedback-window iframe {
  width: 100%;
}
.comment__more:has(+.rpm-unroll-all),
.rpm-unroll-all {
  --gap: 10px;
  width: calc(50% - var(--gap) / 2);
  margin-right: var(--gap);
  text-align: center;
}
.rpm-unroll-all {
  display: none;
  margin: auto 0;
  background-color: var(--color-primary-700);
  color: var(--color-bright-900);
}
.comment__more + .rpm-unroll-all {
  display: inline-block;
}
#prm {
  background-color: var(--color-black-440);
  border-radius: 15px;
  border: none !important;
}
#prm .field_label {
  font-size: 14px;
  font-weight: bold;
}
#prm .radio_label {
  font-size: 14px;
}
#prm .config_var {
  margin: 0 0 4px;
}
#prm .section_header {
  color: #FFF;
  font-size: 13pt;
  margin: 0;
}
#prm .section_desc {
  color: var(--color-black-700);
  font-size: 9pt;
  margin: 0 0 6px;
}
#prm_wrapper {
  --gap: 10px;
  box-sizing: border-box;
  padding: 15px;
  margin: 0;
  display: flex;
  flex-flow: row wrap;
  align-content: space-evenly;
  gap: var(--gap);
}
#prm_header {
  height: max-content;
  flex: 0 0 100%;
}
#prm_header p {
  font-size: x-large;
}
#prm_header a {
  font-size: large;
  text-decoration: underline;
  margin: 0 10px;
}
.section_header_holder {
  margin: 8px auto 0;
  display: block;
  padding: 10px;
  flex-basis: 100%;
  overflow: hidden;
  border-radius: 10px;
  background-color: var(--color-black-430);
}
#prm_section_1,
#prm_section_2 {
  flex: 1;
}
#prm_buttons_holder {
  flex-basis: 100%;
  display: flex;
  justify-content: flex-end;
  gap: 5px;
  flex-flow: row wrap;
}
#prm_buttons_holder .reset_holder {
  flex-basis: 100%;
  text-align: right;
}
#prm_wrapper .section_header.center {
  font-size: large;
  display: block;
  width: calc(100% + 20px);
  text-align: center;
  margin: -10px -10px 0;
  background-color: var(--color-primary-700);
  padding: 2px 0;
}
#prm_wrapper .config_var {
  width: 100%;
  margin-top: 5px;
  font-size: medium;
}
#prm_wrapper .config_var input,
#prm_wrapper .config_var select {
  margin-right: 10px;
}
#prm_wrapper .config_var input[type=text] {
  background-color: var(--color-black-440);
  padding: 2px;
  border: solid 1px black;
  border-radius: 7px;
  padding: 4px;
  width: 30%;
}
#prm_wrapper input[type=button],
#prm_wrapper button {
  background-color: var(--color-black-440);
  color: var(--color-black-700);
  transition: background-color 200ms ease-out, filter 200ms, color 200ms;
  padding: 0px 20px;
  vertical-align: middle;
  box-sizing: border-box;
  border-radius: 8px;
  cursor: pointer;
  font-size: 14px;
  line-height: 32px;
  font-weight: 500;
  max-width: 100%;
  text-wrap: balance;
}
#prm_buttons_holder button {
  background-color: var(--color-black-300);
}
#prm_wrapper input[type=button]:hover {
  background-color: var(--color-black-500);
}
#prm_wrapper input[type=checkbox] {
  margin-right: 10px;
  width: 1.25em;
  height: 1.25em;
}
#prm_wrapper select {
  background-color: var(--color-black-440);
  color: var(--color-black-700);
  border: none;
  padding: 5px 10px;
  border-radius: 5px;
  outline: none;
}
#prm_section_3 .config_var {
  display: inline-block;
  width: calc(100%/7);
  min-width: max-content;
  padding: 0 5px;
  text-align: center;
}
#prm_section_5 .config_var input[type=text] {
  width: 30em;
  max-width: 100%;
}
@media only screen and (max-width: 768px)  {
  #prm {
    width: unset !important;
    height: unset !important;
    left: 10px !important;
    right: 10px !important;
    top: 10px !important;
    bottom: 10px !important;
  }
  #prm_section_1,
  #prm_section_2 {
    flex: unset;
  }
  #prm .config_var {
    margin-bottom: 20px;
  }
  #prm .config_var:last-child {
    margin-bottom: unset;
  }
}  /* Notifications */
@keyframes rpm-notification-intro {
  from {
    translate: -100%;
  }
  to {
    translate: 0%;
  }
}
`);
}

function unrollComments(button) {
  button.click();
  setTimeout(() => {
    if (document.body.contains(button)) {
      unrollComments(button);
    }
  }, 500);
}

function commentMoreBtn() {
  const value = GM_config.get('unrollCommentaries');
  if (value === SettingEnums.UnrollComments.NONE)
    return;
  const moreButton = document.querySelector('.comment__more');
  if (!moreButton)
    return;
  if (value === SettingEnums.UnrollComments.UNROLL_ALL_BUTTON) {
    const btn = document.createElement('button');
    btn.textContent = 'Раскрыть все комментарии';
    btn.classList.add('rpm-unroll-all');
    btn.addEventListener('click', () => {
      btn.remove();
      unrollComments(moreButton);
    });
    moreButton.parentElement.append(btn);
  }
  else if (value === SettingEnums.UnrollComments.AUTO_UNROLL) {
    unrollComments(moreButton);
  }
}
async function onLoad() {
  processTabs();
  observer = new MutationObserver(mutationsListener);
  observer.observe(document.querySelector(".app__content, .main__inner"), {
    childList: true,
    subtree: true,
  });
  processStories(document.querySelectorAll("article.story"));
  if (!supportMenuCommands)
    addSettingsOpenButton();
}
init();