Advertisement
OValt

Suggested Edits Reviewer Helper

May 2nd, 2021 (edited)
29
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. type StackAPIBatchResponse<T> = {
  2.   has_more: boolean;
  3.   items: T[];
  4.   quota_max: number;
  5.   quota_remaining: number;
  6. };
  7.  
  8. type ReputationInfo = {
  9.   on_date: number;
  10.   post_id: number;
  11.   post_type: "answer" | "question";
  12.   reputation_change: number;
  13.   user_id: number;
  14.   vote_type: "up_votes";
  15. };
  16.  
  17. type UserType =
  18.   | "unregistered"
  19.   | "registered"
  20.   | "moderator"
  21.   | "team_admin"
  22.   | "does_not_exist";
  23.  
  24. type BadgeCounts = {
  25.   bronze: number;
  26.   silver: number;
  27.   gold: number;
  28. };
  29.  
  30. type UserInfo = {
  31.   creation_date: number;
  32.   is_employee: boolean;
  33.   last_access_date: number;
  34.   last_modified_date: number;
  35.   reputation: number;
  36.   reputation_change_day: number;
  37.   reputation_change_month: number;
  38.   reputation_change_quarter: number;
  39.   reputation_change_week: number;
  40.   reputation_change_year: number;
  41.   user_id: number;
  42.   display_name: string;
  43.   website_url: string;
  44.   profile_image: string;
  45.   link: string;
  46.   location: string;
  47.   user_type: UserType;
  48.   badge_counts: BadgeCounts;
  49. };
  50.  
  51. type SuggestedEditInfo = {
  52.   approval_date?: number;
  53.   comment: string;
  54.   creation_date: number;
  55.   post_id: number;
  56.   post_type: "question" | "answer" | "article";
  57.   proposing_user?: {}; //TODO: expand
  58.   rejection_date?: string;
  59.   suggested_edit_id: number;
  60.   tags: string[];
  61.   title: string;
  62. };
  63.  
  64. type GetSuggestedEditsStatsOptions = {
  65.   from?: Date;
  66.   to?: Date;
  67.   site?: string;
  68. };
  69.  
  70. type ListOptions = { header?: string; items: (string | HTMLElement)[] };
  71.  
  72. (async () => {
  73.   const toApiDate = (date: Date) => (date.valueOf() / 1e3).toString();
  74.  
  75.   const toPercent = (ratio: number) => `${Math.trunc(ratio * 100)}%`;
  76.  
  77.   const last = <A extends any[]>(arr: A): A[number] => arr[arr.length - 1];
  78.  
  79.   const safeMatch = (text: string, regex: RegExp, def = "") =>
  80.     (text.match(regex) || [text, def]).slice(1) as [
  81.       full: string,
  82.       group1: string,
  83.       ...others: string[]
  84.     ];
  85.  
  86.   const API_BASE = "https://api.stackexchange.com";
  87.  
  88.   const DEF_SITE = "stackoverflow";
  89.  
  90.   const API_VER = 2.2;
  91.  
  92.   const config = {
  93.     page: {
  94.       suggestionId: last(location.pathname.split("/")),
  95.     },
  96.     classes: {
  97.       grid: {
  98.         container: "grid",
  99.         cell: "grid--cell",
  100.       },
  101.     },
  102.     selectors: {
  103.       actions: {
  104.         sidebar: ".js-actions-sidebar",
  105.         modal: {
  106.           form: "form[action='/suggested-edits/reject']",
  107.           votes: {
  108.             labels: "label[for^=rejection-reason].s-label",
  109.             counts: ".s-badge__votes",
  110.           },
  111.         },
  112.         inputs: {
  113.           reject: "#review-action-Reject",
  114.         },
  115.       },
  116.       buttons: {
  117.         submit: ".js-review-submit",
  118.         skip: ".js-review-actions:not(.d-none) .js-action-button[value=1]",
  119.         close: ".s-modal--close",
  120.       },
  121.       reviews: {
  122.         done: ".js-reviews-done",
  123.         daily: ".js-reviews-per-day",
  124.       },
  125.       diffs: {
  126.         deleted: ".full-diff .deleted > div",
  127.         added: ".full-diff .inserted > div",
  128.       },
  129.       page: {
  130.         links: {
  131.           question: "a[href*='/questions/']",
  132.           answer: "a.answer-hyperlink",
  133.         },
  134.       },
  135.       content: {
  136.         typeHint: ".js-review-content h2",
  137.         postSummary: ".s-post-summary",
  138.       },
  139.       title: {
  140.         description: ".s-page-title--description",
  141.         actions: ".s-page-title--actions a",
  142.         learnMore: ".js-show-modal-from-nav.s-link",
  143.         title: ".s-page-title--text",
  144.         header: ".s-page-title--header",
  145.       },
  146.       info: {
  147.         post: {
  148.           wrapper: ".postcell span",
  149.         },
  150.         editor: {
  151.           card: "a.s-user-card--link",
  152.         },
  153.       },
  154.     },
  155.   };
  156.  
  157.   const handleMatchFailure = <R extends null | false>(
  158.     selector: string,
  159.     returnValue: R
  160.   ) => {
  161.     console.debug(`Couldn't find the element with selector: ${selector}`);
  162.    return returnValue;
  163.  };
  164.  
  165.  const selectActions = () =>
  166.    Array.from(
  167.      document.querySelectorAll<HTMLAnchorElement>(
  168.        config.selectors.title.actions
  169.      )
  170.    );
  171.  
  172.  const getUserInfo = async (id: string, site = DEF_SITE) => {
  173.    const url = new URL(`${API_BASE}/${API_VER}/users/${id}`);
  174.    url.search = new URLSearchParams({ site }).toString();
  175.    const res = await fetch(url.toString());
  176.    if (!res.ok) return null;
  177.  
  178.    const {
  179.      items: [userInfo],
  180.    }: StackAPIBatchResponse<UserInfo> = await res.json();
  181.    return userInfo;
  182.  };
  183.  
  184.  const getSuggestionsUserStats = async (
  185.    id: string,
  186.    options: GetSuggestedEditsStatsOptions = {}
  187.  ) => {
  188.    const url = new URL(`${API_BASE}/${API_VER}/users/${id}/suggested-edits`);
  189.  
  190.    const params: Record<string, string> = {
  191.      site: options.site || DEF_SITE,
  192.    };
  193.  
  194.    if (Object.keys(options).length) {
  195.      const { from, to = new Date() } = options;
  196.  
  197.      if (from) params.from = toApiDate(from);
  198.      if (to) params.to = toApiDate(to);
  199.    }
  200.  
  201.    url.search = new URLSearchParams(params).toString();
  202.  
  203.    const res = await fetch(url.toString());
  204.    if (!res.ok) return [];
  205.  
  206.    const {
  207.      items,
  208.    }: StackAPIBatchResponse<SuggestedEditInfo> = await res.json();
  209.  
  210.    return items;
  211.  };
  212.  
  213.  const getAnswerId = (selector: string) => {
  214.    const link = document.querySelector<HTMLAnchorElement>(selector);
  215.    return safeMatch(
  216.      link?.href || "",
  217.      /\/questions\/\d+\/[\w-]+\/(\d+)/,
  218.      ""
  219.    )[0];
  220.  };
  221.  
  222.  const getQuestionId = (selector: string) => {
  223.    const link = document.querySelector<HTMLAnchorElement>(selector);
  224.    return safeMatch(link?.href || "", /\/questions\/(\d+)/, "")[0];
  225.  };
  226.  
  227.  const getPostId = ({
  228.    selectors: {
  229.      page: { links },
  230.    },
  231.  }: typeof config) =>
  232.    getAnswerId(links.answer) || getQuestionId(links.question);
  233.  
  234.  const getEditAuthorId = () => {
  235.    const postWrapSelector = config.selectors.info.post.wrapper;
  236.  
  237.    const spans = document.querySelectorAll(postWrapSelector);
  238.    if (!spans.length) return handleMatchFailure(postWrapSelector, null);
  239.  
  240.    const userSpan = Array.from(spans).find(({ textContent }) =>
  241.      /proposed/i.test(textContent || "")
  242.    );
  243.    if (!userSpan) return null;
  244.  
  245.    const cardSelector = config.selectors.info.editor.card;
  246.  
  247.    const { parentElement } = userSpan;
  248.    const link = parentElement!.querySelector<HTMLAnchorElement>(cardSelector);
  249.    if (!link) return handleMatchFailure(cardSelector, null);
  250.  
  251.    const { href } = link;
  252.    const [, userId] = href.match(/users\/(\d+)/) || [];
  253.    if (!userId) return null;
  254.  
  255.    return userId;
  256.  };
  257.  
  258.  const createGridCell = () => {
  259.    const elem = document.createElement("div");
  260.    elem.classList.add(config.classes.grid.cell);
  261.    return elem;
  262.  };
  263.  
  264.  const createItem = (...contents: Node[]) => {
  265.    const elem = document.createElement("div");
  266.    elem.classList.add(config.classes.grid.cell, "p12");
  267.    elem.append(...contents);
  268.    return elem;
  269.  };
  270.  
  271.  const text = (text: string) => document.createTextNode(text);
  272.  
  273.  const br = () => document.createElement("br");
  274.  
  275.  const a = (link: string, text = link) => {
  276.    const anchor = document.createElement("a");
  277.    anchor.href = link;
  278.    anchor.textContent = text;
  279.    anchor.target = "_blank";
  280.    anchor.referrerPolicy = "no-referrer";
  281.    return anchor;
  282.  };
  283.  
  284.  const p = (text: string) => {
  285.    const par = document.createElement("p");
  286.    par.style.marginBottom = "0";
  287.    par.innerText = text;
  288.    return par;
  289.  };
  290.  
  291.  const li = (content: string | HTMLElement) => {
  292.    const item = document.createElement("li");
  293.  
  294.    if (typeof content === "string") {
  295.      item.textContent = content;
  296.      return item;
  297.    }
  298.  
  299.    item.append(content);
  300.    return item;
  301.  };
  302.  
  303.  const ul = ({ header, items }: ListOptions) => {
  304.    const list = document.createElement("ul");
  305.    const { style } = list;
  306.    style.listStyle = "none";
  307.    style.margin = "0";
  308.  
  309.    if (header) {
  310.      const head = document.createElement("h3");
  311.      head.classList.add("mb8");
  312.      head.textContent = header;
  313.      list.append(head);
  314.    }
  315.  
  316.    const listItems = items.map(li);
  317.  
  318.    list.append(...listItems);
  319.    return list;
  320.  };
  321.  
  322.  const createEditAuthorItem = ({
  323.    display_name,
  324.    reputation,
  325.    link,
  326.  }: UserInfo) => {
  327.    const namePar = p(`Name: `);
  328.    namePar.append(a(link, display_name));
  329.  
  330.    return createItem(
  331.      ul({
  332.        header: "Edit Author",
  333.        items: [namePar, `Reputation: ${reputation}`],
  334.      })
  335.    );
  336.  };
  337.  
  338.  type CommonOptions = {
  339.    site?: string;
  340.  };
  341.  
  342.  type SuggestedEditStatus = "approved" | "rejected" | "all" | "pending";
  343.  
  344.  type SuggestedEditsByPostOptions = {
  345.    type: SuggestedEditStatus;
  346.  } & CommonOptions;
  347.  
  348.  const getSuggestionsByPost = async (
  349.    postId: string,
  350.    { site = DEF_SITE, type = "all" }: SuggestedEditsByPostOptions
  351.  ) => {
  352.    const url = new URL(
  353.      `${API_BASE}/${API_VER}/posts/${postId}/suggested-edits`
  354.    );
  355.  
  356.    url.search = new URLSearchParams({ site }).toString();
  357.  
  358.    const res = await fetch(url.toString());
  359.  
  360.    if (!res.ok) return [];
  361.  
  362.    const {
  363.      items,
  364.    } = (await res.json()) as StackAPIBatchResponse<SuggestedEditInfo>;
  365.  
  366.    const filters: {
  367.      [P in SuggestedEditStatus]?: (val: SuggestedEditInfo) => boolean;
  368.    } = {
  369.      approved: ({ approval_date }) => !!approval_date,
  370.      rejected: ({ rejection_date }) => !!rejection_date,
  371.      pending: ({ approval_date, rejection_date }) =>
  372.        !approval_date && !rejection_date,
  373.    };
  374.  
  375.    const predicate = filters[type];
  376.  
  377.    return predicate ? items.filter(predicate) : items;
  378.  };
  379.  
  380.  const getSuggestedEditsInfo = async (...ids: string[]) => {
  381.    const url = new URL(
  382.      `${API_BASE}/${API_VER}/suggested-edits/${ids.join(",")}`
  383.    );
  384.  
  385.    const res = await fetch(url.toString());
  386.  
  387.    if (!res.ok) return [];
  388.  
  389.    const {
  390.      items,
  391.    } = (await res.json()) as StackAPIBatchResponse<SuggestedEditInfo>;
  392.  
  393.    return items;
  394.  };
  395.  
  396.  const getSuggestionTotals = (suggestions: SuggestedEditInfo[]) => {
  397.    const stats = {
  398.      get ratio() {
  399.        const { approved, rejected, total } = this;
  400.        return {
  401.          ofApproved: approved / total,
  402.          ofRejected: rejected / total,
  403.          approvedToRejected: approved / (rejected === 0 ? 1 : rejected),
  404.        };
  405.      },
  406.      approved: 0,
  407.      rejected: 0,
  408.      total: 0,
  409.    };
  410.  
  411.    suggestions.forEach(({ approval_date, rejection_date }) => {
  412.      stats.total += 1;
  413.      if (approval_date) stats.approved += 1;
  414.      if (rejection_date) stats.rejected += 1;
  415.    });
  416.  
  417.    return stats;
  418.  };
  419.  
  420.  const decolorDiff = (cnf: typeof config) => {
  421.    const { added, deleted } = cnf.selectors.diffs;
  422.  
  423.    const addWrapper = document.querySelector<HTMLDivElement>(added);
  424.    const delWrapper = document.querySelector<HTMLDivElement>(deleted);
  425.  
  426.    if (!addWrapper || !delWrapper) return false;
  427.  
  428.    addWrapper.style.backgroundColor = "unset";
  429.    delWrapper.style.backgroundColor = "unset";
  430.    return true;
  431.  };
  432.  
  433.  const createEditorStatsItem = (
  434.    { link }: UserInfo,
  435.    suggestions: SuggestedEditInfo[]
  436.  ) => {
  437.    const {
  438.      approved,
  439.      rejected,
  440.      total,
  441.      ratio: { approvedToRejected, ofApproved, ofRejected },
  442.    } = getSuggestionTotals(suggestions);
  443.  
  444.    const itemParams: ListOptions = {
  445.      header: "Author Stats",
  446.      items: [],
  447.    };
  448.  
  449.    if (!total) {
  450.      const infoPar = p(`Tag wiki/excerpt edits are not returned.`);
  451.  
  452.      infoPar.append(
  453.        br(),
  454.        text(`See their `),
  455.        a(`${link}?tab=activity`, "activity tab")
  456.      );
  457.  
  458.      itemParams.items.push(infoPar);
  459.      return createItem(ul(itemParams));
  460.    }
  461.  
  462.    itemParams.items.push(
  463.      `Approved: ${approved} (${toPercent(ofApproved)})`,
  464.      `Rejected: ${rejected} (${toPercent(ofRejected)})`,
  465.      `Of total: ${total}`,
  466.      `Ratio: ${approvedToRejected}`
  467.    );
  468.  
  469.    return createItem(ul(itemParams));
  470.  };
  471.  
  472.  const trimNumericString = (text: string) => text.replace(/\D/g, "");
  473.  
  474.  const goParentUp = (element: Element | null, times = 1): Element | null => {
  475.    if (times === 0 || !element) return element;
  476.    return goParentUp(element.parentElement, times - 1);
  477.  };
  478.  
  479.  const arraySelect = <R extends Element = Element>(
  480.    ctxt: Element,
  481.    selector: string
  482.  ) => Array.from(ctxt.querySelectorAll<R>(selector));
  483.  
  484.  const removeProgressBar = (reviewStatsElement: Element) => {
  485.    const wrapper = goParentUp(reviewStatsElement, 3);
  486.    if (!wrapper) return false;
  487.    wrapper.remove();
  488.    return true;
  489.  };
  490.  
  491.  const removeTitleLines = (cnf: typeof config, wrapper?: Element) =>
  492.    (wrapper || document)
  493.      .querySelectorAll(cnf.selectors.title.description)
  494.      .forEach((elem) => elem.remove());
  495.  
  496.  const optimizePageTitle = (cnf: typeof config) => {
  497.    const titleSelector = cnf.selectors.title.title;
  498.  
  499.    const titleWrap = document.querySelector(titleSelector);
  500.    if (!titleWrap) return handleMatchFailure(titleSelector, false);
  501.  
  502.    titleWrap.classList.add(cnf.classes.grid.container);
  503.  
  504.    const header = document.querySelector(cnf.selectors.title.header);
  505.  
  506.    const titleCell = createGridCell();
  507.    titleCell.classList.add("ml12");
  508.    if (header) titleCell.append(header);
  509.  
  510.    const learnMoreBtn = titleWrap.querySelector(cnf.selectors.title.learnMore);
  511.  
  512.    const linkCell = titleCell.cloneNode() as HTMLDivElement;
  513.    if (learnMoreBtn) linkCell.append(learnMoreBtn);
  514.  
  515.    removeTitleLines(cnf, titleWrap);
  516.  
  517.    titleWrap.append(titleCell, linkCell);
  518.    return true;
  519.  };
  520.  
  521.  const moveProgressToTabs = ({ selectors }: typeof config) => {
  522.    const actions = selectActions();
  523.    const action = actions.find(({ href }) =>
  524.      /\/review\/suggested-edits/.test(href)
  525.    );
  526.  
  527.    const dailyElem = document.querySelector(selectors.reviews.daily);
  528.    const reviewedElem = document.querySelector(selectors.reviews.done);
  529.  
  530.    if (!dailyElem || !reviewedElem) return false;
  531.  
  532.    const daily = trimNumericString(dailyElem!.textContent || "0");
  533.    const reviewed = trimNumericString(reviewedElem!.textContent || "0");
  534.  
  535.    const ratio = +reviewed / +daily;
  536.    const percentDone = toPercent(ratio);
  537.  
  538.    if (!action) return false;
  539.    const { style } = action;
  540.  
  541.    style.background = `linear-gradient(90deg, var(--theme-primary-color) ${percentDone}, var(--black-075) ${percentDone})`;
  542.    style.color = `var(--black-600)`;
  543.  
  544.    action.textContent += ` (${reviewed}/${daily})`;
  545.  
  546.    return removeProgressBar(dailyElem);
  547.  };
  548.  
  549.  const addAuditNotification = async (
  550.    { selectors: { content } }: typeof config,
  551.    postId: string
  552.  ) => {
  553.    const auditId = "audit_notification";
  554.  
  555.    if (document.getElementById(auditId)) return true; //early exit if already added
  556.  
  557.    const { length } = await getSuggestionsByPost(postId, {
  558.      type: "pending",
  559.    });
  560.    if (length) return true;
  561.  
  562.    const editTypeHint = document.querySelector(content.typeHint);
  563.    const summary = document.querySelector(content.postSummary);
  564.  
  565.    if (!editTypeHint) return false;
  566.  
  567.    const quote = document.createElement("blockquote");
  568.    quote.id = auditId;
  569.    quote.classList.add("mb12", "fs-headline1");
  570.    quote.textContent = "This is an Audit. Tread carefully";
  571.  
  572.    editTypeHint.after(quote);
  573.    editTypeHint.remove();
  574.    summary?.remove();
  575.  
  576.    return true;
  577.  };
  578.  
  579.  type RejectionCount = {
  580.    spam: number;
  581.    improvement: number;
  582.    intent: number;
  583.    reply: number;
  584.    harm: number;
  585.  };
  586.  
  587.  const callRejectionModal = (cnf: typeof config) => {
  588.    const {
  589.      selectors: {
  590.        buttons,
  591.        actions: { inputs, modal },
  592.      },
  593.    } = cnf;
  594.  
  595.    const rejectInput = document.querySelector<HTMLInputElement>(inputs.reject);
  596.    const submitButton = document.querySelector<HTMLButtonElement>(
  597.      buttons.submit
  598.    );
  599.    if (!rejectInput || !submitButton) return null;
  600.  
  601.    rejectInput.click();
  602.    submitButton.click();
  603.  
  604.    const modalWrapper = document.querySelector<HTMLFormElement>(modal.form);
  605.    if (!modalWrapper) return null;
  606.  
  607.    const dolly = modalWrapper.cloneNode(true) as HTMLDivElement;
  608.  
  609.    const closeBtn = modalWrapper.querySelector<HTMLButtonElement>(
  610.      buttons.close
  611.    )!;
  612.  
  613.    closeBtn.click();
  614.    return dolly;
  615.  };
  616.  
  617.  const getRejectionCount = (cnf: typeof config) => {
  618.    const {
  619.      selectors: {
  620.        actions: { modal },
  621.      },
  622.    } = cnf;
  623.  
  624.    const modalWrapper = callRejectionModal(cnf);
  625.    if (!modalWrapper) return handleMatchFailure(modal.form, null);
  626.  
  627.    const withVotes = arraySelect<HTMLLabelElement>(
  628.      modalWrapper,
  629.      modal.votes.labels
  630.    );
  631.  
  632.    const count: RejectionCount = {
  633.      spam: 0,
  634.      improvement: 0,
  635.      intent: 0,
  636.      reply: 0,
  637.      harm: 0,
  638.    };
  639.  
  640.    const reasonMap: { [P in keyof RejectionCount as string]: P } = {
  641.      102: "improvement",
  642.      101: "spam",
  643.      104: "intent",
  644.      105: "reply",
  645.      0: "harm",
  646.    };
  647.  
  648.    const voteSelector = modal.votes.counts;
  649.  
  650.    withVotes.forEach((label) => {
  651.      const { htmlFor } = label;
  652.      const [_full, reasonId] = htmlFor.match(/(\d+$)/) || [];
  653.      const reason = reasonMap[reasonId];
  654.      if (label.querySelector(voteSelector)) count[reason] += 1;
  655.    });
  656.  
  657.    return count;
  658.  };
  659.  
  660.  const scase = (word: string) =>
  661.    word[0].toUpperCase() + word.slice(1).toLowerCase();
  662.  
  663.  const createRejectionCountItem = (count: RejectionCount) => {
  664.    const withVotes = Object.entries(count).filter(([_k, v]) => !!v);
  665.    const items = withVotes.map(([k, v]) => `${scase(k)}: ${v}`);
  666.    return createItem(ul({ items, header: "Reject votes" }));
  667.  };
  668.  
  669.  const addStatsSidebar = async (cnf: typeof config) => {
  670.    const sidebar = document.querySelector(cnf.selectors.actions.sidebar);
  671.  
  672.    if (!sidebar) return false;
  673.  
  674.    const dialog = document.createElement("div");
  675.    dialog.classList.add("s-sidebarwidget", "ml24", "mt24");
  676.  
  677.    const header = document.createElement("div");
  678.    header.classList.add("s-sidebarwidget--header");
  679.    header.textContent = "Extra Info";
  680.  
  681.    const itemWrap = document.createElement("div");
  682.    itemWrap.classList.add(cnf.classes.grid.container, "fd-column");
  683.  
  684.    const authorId = getEditAuthorId();
  685.  
  686.    if (!authorId) return false;
  687.  
  688.    const [editAuthorInfo, editAuthorStats] = await Promise.all([
  689.      getUserInfo(authorId),
  690.      getSuggestionsUserStats(authorId),
  691.    ]);
  692.  
  693.    const rejectCount = getRejectionCount(cnf);
  694.  
  695.    if (!editAuthorInfo || !rejectCount) return false;
  696.  
  697.    const items: HTMLDivElement[] = [];
  698.  
  699.    items.push(
  700.      createEditAuthorItem(editAuthorInfo),
  701.      createEditorStatsItem(editAuthorInfo, editAuthorStats),
  702.      createRejectionCountItem(rejectCount)
  703.    );
  704.  
  705.    itemWrap.append(...items);
  706.  
  707.    dialog.append(header, itemWrap);
  708.  
  709.    editAuthorInfo && sidebar.append(dialog);
  710.  
  711.    return true;
  712.  };
  713.  
  714.  const postId = getPostId(config);
  715.  
  716.  if (!postId) return;
  717.  
  718.  const handlerMap: {
  719.    [x: string]: (
  720.      cnf: typeof config,
  721.      postId: string
  722.    ) => boolean | Promise<boolean>;
  723.  } = {
  724.    moveProgressToTabs,
  725.    optimizePageTitle,
  726.    decolorDiff,
  727.    addAuditNotification,
  728.  };
  729.  
  730.  const promises = Object.entries(handlerMap).map(([key, handler]) => [
  731.    key,
  732.    handler(config, postId),
  733.  ]);
  734.  
  735.  const statuses = await Promise.all(promises);
  736.  
  737.  const statusMsg = statuses.reduce(
  738.    (acc, [k, v]) => `${acc}\n${k} - ${v ? "ok" : "failed"}`,
  739.    "Status: "
  740.  );
  741.  
  742.  console.debug(statusMsg);
  743.  
  744.  await addStatsSidebar(config);
  745. })();
  746.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement