Skip to main content
inlined the API key
Source Link
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).

sample report for a user id 953887 on Stack Overflowsample 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;
const API_KEY = "46zkb16JUl3s3jZdIsJDVA((";

/**
 * @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;
};

/**
 * @template {object} T
 *
 * @summary get a sum of items by key
 * @param {T[]} items items to process
 * @param {keyof T} key key to use for the ssum
 * @returns {number}
 */
const getSum = (items, key) => items.reduce((a, c) => a + (c[key] || 0), 0);

/**
/**
 * @template {object} T
 *
 * @summary get a total of items by key
 * @param {T[]} items items to process
 * @param {keyof T} key key to use for the total
 * @returns {number}
 */
const getTotal = (items, key) => items.reduce((a, c) => a + (c[key] ? 1 : 0), 0);

/**
 * @summary sums score of all posts
 * @param {Array<StackExchangeAPI.Post>} posts posts to process
 * @returns {number}
 */
const getTotalScore = (posts) => getSum(posts, "score");

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: API_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: API_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 upvoteRep = 10;
        const acceptRep = 15;

        const rows = answersByUser.flatMap(({ link, name, answers }) => {
            const userLink = makeLink(link, name);
            const totalAnswers = answers.length;
            const totalAccepted = getTotal(answers, "is_accepted");
            const totalWikis = getTotal(answers.filter((a) => a.community_owned_date), "community_owned_date");
            const totalBounties = getSum(answers, "awarded_bounty_amount");
            const totalScore = getTotalScore(answers);
            const totalNonWikiScore = getTotalScore(answers.filter((a) => !a.community_owned_date));
            const repEstimate = totalNonWikiScore * upvoteRep + totalAccepted * acceptRep + totalBounties;

            return makeRow(
                userLink,
                totalAnswers,
                totalWikis,
                totalAccepted,
                totalBounties,
                totalScore,
                repEstimate
            );
        });

        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>Wikis</th>
      <th>Accepted</th>
      <th>Bounty rep</th>
      <th>Score</th>
      <th>Rep Estimate</th>
    </tr>
  </thead>
  <tbody>
  </tbody>
</table>

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).

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;
};

/**
 * @template {object} T
 *
 * @summary get a sum of items by key
 * @param {T[]} items items to process
 * @param {keyof T} key key to use for the ssum
 * @returns {number}
 */
const getSum = (items, key) => items.reduce((a, c) => a + (c[key] || 0), 0);

/**
/**
 * @template {object} T
 *
 * @summary get a total of items by key
 * @param {T[]} items items to process
 * @param {keyof T} key key to use for the total
 * @returns {number}
 */
const getTotal = (items, key) => items.reduce((a, c) => a + (c[key] ? 1 : 0), 0);

/**
 * @summary sums score of all posts
 * @param {Array<StackExchangeAPI.Post>} posts posts to process
 * @returns {number}
 */
const getTotalScore = (posts) => getSum(posts, "score");

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 upvoteRep = 10;
        const acceptRep = 15;

        const rows = answersByUser.flatMap(({ link, name, answers }) => {
            const userLink = makeLink(link, name);
            const totalAnswers = answers.length;
            const totalAccepted = getTotal(answers, "is_accepted");
            const totalWikis = getTotal(answers.filter((a) => a.community_owned_date), "community_owned_date");
            const totalBounties = getSum(answers, "awarded_bounty_amount");
            const totalScore = getTotalScore(answers);
            const totalNonWikiScore = getTotalScore(answers.filter((a) => !a.community_owned_date));
            const repEstimate = totalNonWikiScore * upvoteRep + totalAccepted * acceptRep + totalBounties;

            return makeRow(
                userLink,
                totalAnswers,
                totalWikis,
                totalAccepted,
                totalBounties,
                totalScore,
                repEstimate
            );
        });

        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>Wikis</th>
      <th>Accepted</th>
      <th>Bounty rep</th>
      <th>Score</th>
      <th>Rep Estimate</th>
    </tr>
  </thead>
  <tbody>
  </tbody>
</table>

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.

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;
const API_KEY = "46zkb16JUl3s3jZdIsJDVA((";

/**
 * @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;
};

/**
 * @template {object} T
 *
 * @summary get a sum of items by key
 * @param {T[]} items items to process
 * @param {keyof T} key key to use for the ssum
 * @returns {number}
 */
const getSum = (items, key) => items.reduce((a, c) => a + (c[key] || 0), 0);

/**
/**
 * @template {object} T
 *
 * @summary get a total of items by key
 * @param {T[]} items items to process
 * @param {keyof T} key key to use for the total
 * @returns {number}
 */
const getTotal = (items, key) => items.reduce((a, c) => a + (c[key] ? 1 : 0), 0);

/**
 * @summary sums score of all posts
 * @param {Array<StackExchangeAPI.Post>} posts posts to process
 * @returns {number}
 */
const getTotalScore = (posts) => getSum(posts, "score");

window.addEventListener("load", () => {
    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: site } = siteInput;
        const { value: userId } = accountInput;

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

        const questions = await getQuestions(userId, { key: API_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: API_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 upvoteRep = 10;
        const acceptRep = 15;

        const rows = answersByUser.flatMap(({ link, name, answers }) => {
            const userLink = makeLink(link, name);
            const totalAnswers = answers.length;
            const totalAccepted = getTotal(answers, "is_accepted");
            const totalWikis = getTotal(answers.filter((a) => a.community_owned_date), "community_owned_date");
            const totalBounties = getSum(answers, "awarded_bounty_amount");
            const totalScore = getTotalScore(answers);
            const totalNonWikiScore = getTotalScore(answers.filter((a) => !a.community_owned_date));
            const repEstimate = totalNonWikiScore * upvoteRep + totalAccepted * acceptRep + totalBounties;

            return makeRow(
                userLink,
                totalAnswers,
                totalWikis,
                totalAccepted,
                totalBounties,
                totalScore,
                repEstimate
            );
        });

        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="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>Wikis</th>
      <th>Accepted</th>
      <th>Bounty rep</th>
      <th>Score</th>
      <th>Rep Estimate</th>
    </tr>
  </thead>
  <tbody>
  </tbody>
</table>
improved rep estimate by excluding community wiki answers
Source Link
0Valt
  • 11.2k
  • 5
  • 24
  • 52
/// <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;
};

/**
 * @template {object} T
 *
 * @summary get a sum of items by key
 * @param {T[]} items items to process
 * @param {keyof T} key key to use for the ssum
 * @returns {number}
 */
const getSum = (items, key) => items.reduce((a, c) => a + (c[key] || 0), 0);

/**
/**
 * @template {object} T
 *
 * @summary get a total of items by key
 * @param {T[]} items items to process
 * @param {keyof T} key key to use for the total
 * @returns {number}
 */
const getTotal = (items, key) => items.reduce((a, c) => a + (c[key] ? 1 : 0), 0);

/**
 * @summary sums score of all posts
 * @param {Array<StackExchangeAPI.Post>} posts posts to process
 * @returns {number}
 */
const getTotalScore = (posts) => getSum(posts, "score");

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 upvoteRep = 10;
        const acceptRep = 15;

        const rows = answersByUser.flatMap(({ link, name, answers }) => {
            const userLink = makeLink(link, name);
            const totalAnswers = answers.length;
            const totalAccepted = getTotal(answers, "is_accepted");
            const totalWikis = getTotal(answers.filter((a) => a.community_owned_date), "community_owned_date");
            const totalBounties = getSum(answers, "awarded_bounty_amount");
            const totalScore = getTotalScore(answers);
            const totalNonWikiScore = getTotalScore(answers.filter((a) => !a.community_owned_date));
            const repEstimate = totalScoretotalNonWikiScore * upvoteRep + totalAccepted * acceptRep + totalBounties;

            return makeRow(
                userLink,
                totalAnswers,
                totalWikis,
                totalAccepted,
                totalBounties,
                totalScore,
                repEstimate
            );
        });

        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>Wikis</th>
      <th>Accepted</th>
      <th>Bounty rep</th>
      <th>Score</th>
      <th>Rep Estimate</th>
    </tr>
  </thead>
  <tbody>
  </tbody>
</table>
/// <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;
};

/**
 * @template {object} T
 *
 * @summary get a sum of items by key
 * @param {T[]} items items to process
 * @param {keyof T} key key to use for the ssum
 * @returns {number}
 */
const getSum = (items, key) => items.reduce((a, c) => a + (c[key] || 0), 0);

/**
/**
 * @template {object} T
 *
 * @summary get a total of items by key
 * @param {T[]} items items to process
 * @param {keyof T} key key to use for the total
 * @returns {number}
 */
const getTotal = (items, key) => items.reduce((a, c) => a + (c[key] ? 1 : 0), 0);

/**
 * @summary sums score of all posts
 * @param {Array<StackExchangeAPI.Post>} posts posts to process
 * @returns {number}
 */
const getTotalScore = (posts) => getSum(posts, "score");

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 upvoteRep = 10;
        const acceptRep = 15;

        const rows = answersByUser.flatMap(({ link, name, answers }) => {
            const userLink = makeLink(link, name);
            const totalAnswers = answers.length;
            const totalAccepted = getTotal(answers, "is_accepted");
            const totalWikis = getTotal(answers.filter((a) => a.community_owned_date), "community_owned_date");
            const totalBounties = getSum(answers, "awarded_bounty_amount");
            const totalScore = getTotalScore(answers);
            const repEstimate = totalScore * upvoteRep + totalAccepted * acceptRep + totalBounties;

            return makeRow(
                userLink,
                totalAnswers,
                totalWikis,
                totalAccepted,
                totalBounties,
                totalScore,
                repEstimate
            );
        });

        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>Wikis</th>
      <th>Accepted</th>
      <th>Bounty rep</th>
      <th>Score</th>
      <th>Rep Estimate</th>
    </tr>
  </thead>
  <tbody>
  </tbody>
</table>
/// <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;
};

/**
 * @template {object} T
 *
 * @summary get a sum of items by key
 * @param {T[]} items items to process
 * @param {keyof T} key key to use for the ssum
 * @returns {number}
 */
const getSum = (items, key) => items.reduce((a, c) => a + (c[key] || 0), 0);

/**
/**
 * @template {object} T
 *
 * @summary get a total of items by key
 * @param {T[]} items items to process
 * @param {keyof T} key key to use for the total
 * @returns {number}
 */
const getTotal = (items, key) => items.reduce((a, c) => a + (c[key] ? 1 : 0), 0);

/**
 * @summary sums score of all posts
 * @param {Array<StackExchangeAPI.Post>} posts posts to process
 * @returns {number}
 */
const getTotalScore = (posts) => getSum(posts, "score");

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 upvoteRep = 10;
        const acceptRep = 15;

        const rows = answersByUser.flatMap(({ link, name, answers }) => {
            const userLink = makeLink(link, name);
            const totalAnswers = answers.length;
            const totalAccepted = getTotal(answers, "is_accepted");
            const totalWikis = getTotal(answers.filter((a) => a.community_owned_date), "community_owned_date");
            const totalBounties = getSum(answers, "awarded_bounty_amount");
            const totalScore = getTotalScore(answers);
            const totalNonWikiScore = getTotalScore(answers.filter((a) => !a.community_owned_date));
            const repEstimate = totalNonWikiScore * upvoteRep + totalAccepted * acceptRep + totalBounties;

            return makeRow(
                userLink,
                totalAnswers,
                totalWikis,
                totalAccepted,
                totalBounties,
                totalScore,
                repEstimate
            );
        });

        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>Wikis</th>
      <th>Accepted</th>
      <th>Bounty rep</th>
      <th>Score</th>
      <th>Rep Estimate</th>
    </tr>
  </thead>
  <tbody>
  </tbody>
</table>
additional stats (CW total) + fixes
Source Link
0Valt
  • 11.2k
  • 5
  • 24
  • 52
  • link to the answering user profile
  • total number of answers given by the user
  • total number of community wiki answers
  • total number of accepted answers
  • total reputation earned via bounties
  • total score of answers
  • rough total reputation earned estimate
sample report for a user id 953887 on Stack Overflowsample 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;
};

/**
 * @template {object} T
 *
 * @summary get a sum of items by key
 * @param {T[]} items items to process
 * @param {keyof T} key key to use for the ssum
 * @returns {number}
 */
const getSum = (items, key) => items.reduce((a, c) => a + (c[key] || 0), 0);

/**
/**
 * @template {object} T
 *
 * @summary get a total of items by key
 * @param {T[]} items items to process
 * @param {keyof T} key key to use for the total
 * @returns {number}
 */
const getTotal = (items, key) => items.reduce((a, c) => a + (c[key] ||? 1 : 0), 0);

/**
 * @summary sums score of all posts
 * @param {Array<StackExchangeAPI.Post>} posts posts to process
 * @returns {number}
 */
const getTotalScore = (posts) => getTotalgetSum(posts, "score");

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 upvoteRep = 10;
        const acceptRep = 15;

        const rows = answersByUser.flatMap(({ link, name, answers }) => {
            const userLink = makeLink(link, name);
            const totalAnswers = answers.length;
            const totalAccepted = getTotal(answers, "is_accepted");
            const totalBountiestotalWikis = getTotal(answers.filter((a) => a.community_owned_date), "community_owned_date");
            const totalBounties = getSum(answers, "awarded_bounty_amount");
            const totalScore = getTotalScore(answers);
            const repEstimate = totalScore * upvoteRep + totalAccepted * acceptRep + totalBounties;

            return makeRow(
                userLink,
                totalAnswers,
                totalWikis,
                totalAccepted,
                totalBounties,
                totalScore,
                repEstimate
            );
        });

        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>Wikis</th>
      <th>Accepted</th>
      <th>Bounty rep</th>
      <th>Score</th>
      <th>Rep Estimate</th>
    </tr>
  </thead>
  <tbody>
  </tbody>
</table>
  • link to the answering user profile
  • number of answers given by the user
  • total number of accepted answers
  • total reputation earned via bounties
  • total score of answers
  • rough total reputation earned estimate
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;
};

/**
 * @template {object} T
 *
 * @summary get a total of items by key
 * @param {T[]} items items to process
 * @param {keyof T} key key to use for the total
 * @returns {number}
 */
const getTotal = (items, key) => items.reduce((a, c) => a + (c[key] || 0), 0);

/**
 * @summary sums score of all posts
 * @param {Array<StackExchangeAPI.Post>} posts posts to process
 * @returns {number}
 */
const getTotalScore = (posts) => getTotal(posts, "score");

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 upvoteRep = 10;
        const acceptRep = 15;

        const rows = answersByUser.flatMap(({ link, name, answers }) => {
            const userLink = makeLink(link, name);
            const totalAnswers = answers.length;
            const totalAccepted = getTotal(answers, "is_accepted");
            const totalBounties = getTotal(answers, "awarded_bounty_amount");
            const totalScore = getTotalScore(answers);
            const repEstimate = totalScore * upvoteRep + totalAccepted * acceptRep + totalBounties;

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

        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>Accepted</th>
      <th>Bounty rep</th>
      <th>Score</th>
      <th>Rep Estimate</th>
    </tr>
  </thead>
  <tbody>
  </tbody>
</table>
  • link to the answering user profile
  • total number of answers by the user
  • total number of community wiki answers
  • total number of accepted answers
  • total reputation earned via bounties
  • total score of answers
  • rough total reputation earned estimate
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;
};

/**
 * @template {object} T
 *
 * @summary get a sum of items by key
 * @param {T[]} items items to process
 * @param {keyof T} key key to use for the ssum
 * @returns {number}
 */
const getSum = (items, key) => items.reduce((a, c) => a + (c[key] || 0), 0);

/**
/**
 * @template {object} T
 *
 * @summary get a total of items by key
 * @param {T[]} items items to process
 * @param {keyof T} key key to use for the total
 * @returns {number}
 */
const getTotal = (items, key) => items.reduce((a, c) => a + (c[key] ? 1 : 0), 0);

/**
 * @summary sums score of all posts
 * @param {Array<StackExchangeAPI.Post>} posts posts to process
 * @returns {number}
 */
const getTotalScore = (posts) => getSum(posts, "score");

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 upvoteRep = 10;
        const acceptRep = 15;

        const rows = answersByUser.flatMap(({ link, name, answers }) => {
            const userLink = makeLink(link, name);
            const totalAnswers = answers.length;
            const totalAccepted = getTotal(answers, "is_accepted");
            const totalWikis = getTotal(answers.filter((a) => a.community_owned_date), "community_owned_date");
            const totalBounties = getSum(answers, "awarded_bounty_amount");
            const totalScore = getTotalScore(answers);
            const repEstimate = totalScore * upvoteRep + totalAccepted * acceptRep + totalBounties;

            return makeRow(
                userLink,
                totalAnswers,
                totalWikis,
                totalAccepted,
                totalBounties,
                totalScore,
                repEstimate
            );
        });

        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>Wikis</th>
      <th>Accepted</th>
      <th>Bounty rep</th>
      <th>Score</th>
      <th>Rep Estimate</th>
    </tr>
  </thead>
  <tbody>
  </tbody>
</table>
improved stats (added acceptance stat + rough rep estimate)
Source Link
0Valt
  • 11.2k
  • 5
  • 24
  • 52
Loading
added link to SE API docs
Source Link
0Valt
  • 11.2k
  • 5
  • 24
  • 52
Loading
Source Link
0Valt
  • 11.2k
  • 5
  • 24
  • 52
Loading