Skip to main content
2 of 6
added link to SE API docs
0Valt
  • 11.2k
  • 5
  • 24
  • 52

The following is a similar query to the one in Glorfindel's answer, but implemented as a runnable snippet and using the Stack Exchange API (as the load on the API can be quite heavy, please procure the API key on StackApps or use an existing one).

The generated report includes the following stats:

  • link to the answering user profile
  • number of answers given by the user
  • total reputation earned via bounties
  • total score of answers

Example report for a user id 953887 on Stack Overflow:

sample report for a user id 953887 on Stack Overflow

/// <reference types="@userscripters/stackexchange-api-types" />

const API_BASE = "https://api.stackexchange.com";
const API_VER = 2.3;

/**
 * @summary delays script execution
 * @param {number} [ms] milliseconds to delay for
 * @returns {Promise<void>}
 */
const delay = (ms = 100) => new Promise((resolve) => setTimeout(resolve, ms));

/**
 * @summary makes a table cell
 * @param {string| Node} content
 * @returns {HTMLTableCellElement}
 */
const makeCell = (content) => {
    const td = document.createElement("td");
    td.append(content);
    return td;
};

/**
 * @summary makes a table row
 * @param {Array<string | Node>} columns
 * @returns {HTMLTableRowElement}
 */
const makeRow = (...columns) => {
    const tr = document.createElement("tr");
    const cells = columns.map(makeCell);
    tr.append(...cells);
    return tr;
};

const makeLink = (url, label) => {
    const a = document.createElement("a");
    a.target = "_blank";
    a.href = url;
    a.innerText = label;
    return a;
};

/**
 * @see https://api.stackexchange.com/docs/questions-on-users
 *
 * @summary gets question info by user id
 * @param {number} id user id
 * @param {{ site?: string, page?: number, ...rest }} [options] request configuration
 * @returns {Promise<StackExchangeAPI.Question[]>}
 */
const getQuestions = async (id, { site = "stackoverflow", page = 1, ...rest }) => {
    const url = new URL(`${API_BASE}/${API_VER}/users/${id}/questions`);
    url.search = new URLSearchParams({
        site,
        page: page.toString(),
        ...rest,
    }).toString();

    const res = await fetch(url.toString());
    if (!res.ok) return [];

    const { items = [], has_more = false, backoff } = await res.json();

    if (backoff) {
        await delay(backoff * 1e3);
        return getQuestions(id, { site, page, ...rest });
    }

    if (has_more) {
        items.push(
            ...(await getQuestions(id, {
                site,
                page: page + 1,
                ...rest,
            }))
        );
    }

    return items;
};

/**
 * @see https://api.stackexchange.com/docs/answers-on-questions
 *
 * @summary gets answers by question ids
 * @param {number[]} ids user id
 * @param {{ site?: string, page?: number, ...rest }} [options] request configuration
 * @returns {Promise<StackExchangeAPI.Answer[]>}
 */
const getAnswers = async (ids, { site = "stackoverflow", page = 1, ...rest }) => {
    const url = new URL(`${API_BASE}/${API_VER}/questions/${ids.join(";")}/answers`);
    url.search = new URLSearchParams({
        site,
        page: page.toString(),
        ...rest,
    }).toString();

    const res = await fetch(url.toString());
    if (!res.ok) return [];

    const { items = [], has_more = false, backoff } = await res.json();

    if (backoff) {
        await delay(backoff * 1e3);
        return getAnswers(ids, { site, page, ...rest });
    }

    if (has_more) {
        items.push(
            ...(await getAnswers(ids, {
                site,
                page: page + 1,
                ...rest,
            }))
        );
    }

    return items;
};

/**
 * @template {unknown} T
 *
 * @summary splits an array into chunks
 * @param {T[]} arr array to split
 * @param {number} size chunk size
 * @returns {T[][]}
 */
const chunkify = (arr, size) => {
    /** @type {T[][]} */
    const output = [];

    arr.forEach((e, i) => {
        let chunk = output[output.length - 1];
        i % size ? chunk.push(e) : output.push([e]);
    });

    return output;
};

/**
 * @summary sums score of all posts
 * @param {Array<StackExchangeAPI.Post>} posts
 * @returns {number}
 */
const getTotalScore = (posts) => posts.reduce((a, c) => a + c.score, 0);

window.addEventListener("load", () => {
    const apiKeyInput = document.getElementById("api_key");
    const accountInput = document.getElementById("account_id");
    const submit = document.getElementById("search");
    const siteInput = document.getElementById("site");
    const progress = document.getElementById("status");
    progress.value = 0;

    submit.addEventListener("click", async () => {
        const { value: key } = apiKeyInput;
        const { value: site } = siteInput;
        const { value: userId } = accountInput;

        const report = document.querySelector("#preview > tbody");

        const questions = await getQuestions(userId, { key, site });

        progress.value = 25;

        const questionIds = questions.map(({ question_id }) => question_id);

        const chunks = chunkify(questionIds, 100); // max number of ids is 100
        const { length: numChunks } = chunks;

        const progressValuePerChunk = 75 / numChunks;

        /** @type {Record<string, { answers: StackExchangeAPI.Answer[], link: string, name: string }>} */
        const answers = {};
        for (const chunk of chunks) {
            const items = await getAnswers(chunk, { key, site, filter: "!nKzQUREjAc" });
            items.forEach((answer) => {
                const { owner: { user_id, display_name, link } } = answer;
                if (!answers[user_id]) answers[user_id] = { answers: [] };
                const userAnswers = answers[user_id];
                userAnswers.answers.push(answer);
                userAnswers.link = link;
                userAnswers.name = display_name;
            });

            progress.value += progressValuePerChunk;
        }

        report.querySelectorAll("tr").forEach((r) => r.remove());

        const answersByUser = Object.values(answers);

        answersByUser.sort((a, b) => getTotalScore(b.answers) - getTotalScore(a.answers));

        const rows = answersByUser.flatMap(({ link, name, answers }) => {
            const userLink = makeLink(link, name);
            const totalAnswers = answers.length;
            const totalBounties = answers.reduce((c, a) => c + (a.awarded_bounty_amount || 0), 0);
            const totalScore = getTotalScore(answers);

            return makeRow(
                userLink,
                totalAnswers,
                totalBounties,
                totalScore
            );
        });

        report.append(...rows);

        report.closest("table").classList.remove("hidden");

        progress.value = 100;
    });

});
label {
  display: block;
  font-weight: 600;
}

caption,
label {
  font-weight: 600;
  margin-bottom: 0.5vh;
}

table {
  margin-top: 4vh;
  border-collapse: collapse;
}

th,
td {
  padding: 2vh 2vw;
  border: 1px solid lightgray;
}

.hidden {
  display: none;
}

progress {
  margin-top: 2vh;
  height: 20px;
  border-radius: 0;
}
<form>
  <label for="api_key">SE API key</label>
  <input id="api_key" type="text" title="SE API key" placeholder="abc12345" />
  
  <label for="site">Site API Slug</label>
  <input id="site" type="text" title="Site API slug" placeholder="stackoverflow" value="stackoverflow" />

  <label for="account_id">User Account Id</label>
  <input id="account_id" type="text" title="Account Id" placeholder="12345" />
  <button id="search" type="button">Get Info</button>
</form>

<progress id="status" max="100" value="0"></progress>

<table id="preview" class="hidden" cellspacing="0">
  <caption>Stack Exchange Post Activity</caption>
  <thead>
    <tr>
      <th>Account</th>
      <th>Answers</th>
      <th>Bounty rep</th>
      <th>Score</th>
    </tr>
  </thead>
  <tbody>
  </tbody>
</table>

0Valt
  • 11.2k
  • 5
  • 24
  • 52