0

I’m working on a re-creation of the flare image that Stack Exchange offers, and the re-creation is more responsive in that I can hover over a site icon and show my stats for a given Stack Exchange domain. I currently have to manually update my data which I plan to do twice a month or so, unless there’s a way to load that data directly from Stack Exchange via a web service or similar.

A few things to keep in mind:

  • I will be hosting this in an ASP.NET web application so C# APIs would be fine.
  • Web services would be perfect too since I can call them from JavaScript.
  • I would need links to documentation for any service provided.

Below is my current manual re-creation in case you’re curious or don’t know what the SE flair is, though it does need to be cleaned up and made to be more efficient.

var siteNames = [ 'Stack Exchange',
        'Puzzling',
        'Stack Overflow',
        'Software Engineering',
        'Mathematics',
        'Physical Fitness' ]
var reps = [ '6.2k', '4.3k', '954', '410', '224', '220' ];
var golds = [ '1', '0', '0', '1', '0', '0' ];
var silvers = [ '14', '7', '4', '2', '1', '0' ];
var bronzes = [ '98', '50', '20', '10', '8', '10' ];
function getSiteStats(siteID) {
 document.getElementById("site-name").innerText = siteNames[siteID];
 document.getElementById("rep").innerText = reps[siteID];
 document.getElementById("gold").innerText = golds[siteID];
 document.getElementById("silver").innerText = silvers[siteID];
 document.getElementById("bronze").innerText = bronzes[siteID];
}
function resetSiteStats() {
 getSiteStats(0);
}
html, body {
 margin: 0;
 height: 100%;
 width: 100%;
 display: flex;
 align-items: center;
 justify-content: center;
 flex-direction: column;
 background-color: #6aa4ed;
 background-image: linear-gradient(45deg, #6aa4ed, #141d33);
 background-image: -webkit-linear-gradient(45deg, #6aa4ed, #141d33);
}
h1, h5 {
 color: #fff;
 font-family: Arial, Helvetica, sans-serif;
 font-weight: 100;
 text-align: center;
 margin: 0;
}
h1 {
 font-size: 10vh;
}
h5 {
 margin-bottom: 10px;
}
.flair {
 padding: 15px;
 background-color: #fff;
 border-radius: 5px;
 box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.25);
 display: flex;
}
.flair img {
 width: 40px;
 height: 40px;
 margin: 5px;
 cursor: pointer;
}
.flair .profile {
 width: 175px;
 height: 175px;
 margin: 0;
 margin-right: 15px;
 box-shadow: 2px 2px 4px rgba(12,13,14,0.5);
 cursor: default;
}
.flair a {
 color: #37f;
 text-decoration: none;
 margin: 5px;
}
.flair a:hover {
 color: #15a;
}
.flair ul {
 list-style-type: none;
 margin: 0;
 padding: 0;
}
.flair ul > li {
 display: inline-block;
 margin: 5px;
}
.flair p {
 margin: 0;
 margin-left: 5px;
}
.badge div {
 display: inline-block;
 height: 7px;
 width: 7px;
 border-radius: 50%;
 transform: translateY(-3px) translateX(3px);
}
.gold {
 background-color: #fc0;
}
.silver {
 background-color: #ccc;
}
.bronze {
 background-color: #da6;
}
<h1>Stack Exchange Flair</h1>
<h5>Not Mobile Friendly (Yet)</h5>
<h5>Hover Over Site Icons</h5>
<div class="flair">
 <img class="profile" src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/2940219/blue.jpg" />
 <div class="account">
  <a href="#">PerpetualJ</a>
  <p id="site-name">Stack Exchange</p>
  <ul>
   <li><strong id="rep">6.2k</strong></li>
   <li>
    <div class="badge">
     <div class="gold"></div>
     <span id="gold">1</span>
    </div>
   </li>
   <li>
    <div class="badge">
     <div class="silver"></div>
     <span id="silver">14</span>
    </div>
   </li>
   <li>
    <div class="badge">
     <div class="bronze"></div>
     <span id="bronze">98</span>
    </div>
   </li>
  </ul>
  <ul>
      <li onmouseover="getSiteStats(1);" onmouseout="resetSiteStats();"><img src="https://cdn.sstatic.net/Sites/puzzling/img/icon-48.png"/></li>
   <li onmouseover="getSiteStats(2);" onmouseout="resetSiteStats();"><img src="https://cdn.sstatic.net/Sites/stackoverflow/img/apple-touch-icon.png"/></li>
   <li onmouseover="getSiteStats(3);" onmouseout="resetSiteStats();"><img src="https://cdn.sstatic.net/Sites/softwareengineering/img/icon-48.png"/></li>
   <li onmouseover="getSiteStats(4);" onmouseout="resetSiteStats();"><img src="https://cdn.sstatic.net/Sites/math/img/apple-touch-icon.png"/></li>
   <li onmouseover="getSiteStats(5);" onmouseout="resetSiteStats();"><img src="https://cdn.sstatic.net/Sites/fitness/img/icon-48.png?v=f5a02f85db94"/></li>
  </ul>
  <p>How fast do you have to slap a chicken to cook it?</p>
 </div>
</div>

Is there some way for me to call an API, web service, or similar that will allow me to pull my current stats for a given Stack Exchange site?

Also, I would prefer to not do any type of web scraping or similar. I’d prefer it come from a legitimate Stack Exchange service.

NOTE: If this belongs on meta please let me know so it can be migrated.

On-Topic: This question is considered as on-topic per the help center:

We feel the best Stack Overflow questions have a bit of source code in them, but if your question generally covers…

  • software tools commonly used by programmers; and is
  • a practical, answerable problem that is unique to software development

…then you’re in the right place to ask your question!

Given the above quote, API's are tools commonly used by programmers, and by asking if Stack Exchange has one, this question is a practical and answerable problem. However, I do believe this may have been better suited for Meta, but I am unable to migrate it.

Hazel へいぜる
  • 2,751
  • 1
  • 12
  • 44

1 Answers1

4

I found out recently that Stack Exchange does offer an API for these types of things. I heavily recommend reading over their documentation for the API prior to usage. In order to accomplish the task I've asked about here, I needed to utilize the following API calls:

I utilized both of these calls together to recreate the Stack Exchange flair, and just-in-case you do not know what the flair is:

profile for PerpetualJ on Stack Exchange, a network of free, community-driven Q&A sites

To get started I wrote a simple set of methods to process my requests to the API:

function getWebServiceResponse(requestUrl, callback) {
    let request = new XMLHttpRequest();
    request.open('GET', requestUrl, true);
    request.onload = function() {
        if (request.status < 200 || request.status >= 400)
            callback("An unexpected error occurred.");
        else
            callback(JSON.parse(this.response));
    };
    request.send();
}
function getSEWebServiceResponse(request, callback) {
    let apiRoot = 'https://api.stackexchange.com/2.2/';
    let key = 'key=s29XM)Eqn2x3YxhjLgFwBQ((';
    if (request.indexOf('?') >= 0)
        key = '&' + key;
    else
        key = '?' + key;

    getWebServiceResponse(apiRoot + request + key, function(response) { callback(response); });
}

Here, the key is needed to help prevent throttling of too many subsequent requests:

Every application is subject to an IP based concurrent request throttle. If a single IP is making more than 30 requests a second, new requests will be dropped.

From here the implementation is pretty straight-forward and was a great learning process!

/users/{ids}

Gets the users identified in ids in {ids}.

Typically this method will be called to fetch user profiles when you have obtained user ids from some other source, such as /questions.

{ids} can contain up to 100 semicolon delimited ids.

function getAssociatedAccountDetails(userID, siteName, fullSiteName, callback) {
    let url = 'users/' + userID +'?order=desc&sort=reputation&site=' + siteName;
    getSEWebServiceResponse(url, function(response) {
        if (!response.items)
            return;

        let account = response.items[0];
        userCard.reputation += account.reputation;
        userCard.badges.gold += account.badge_counts.gold;
        userCard.badges.silver += account.badge_counts.silver;
        userCard.badges.bronze += account.badge_counts.bronze;

        if (userCard.siteUrls.length < 7) {
            var siteProfileCombo = account.link + '|<IMG>|' + fullSiteName;
            siteProfileCombo = siteProfileCombo.replace('<IMG>', getSiteIcon(siteName));
            userCard.siteUrls.push(siteProfileCombo);
        }
        if (userCard.username.length < 1)
            userCard.username = account.display_name;
        if (userCard.profileImageUrl.length < 1)
            userCard.profileImageUrl = account.profile_image;

        callback();
    });
}

/users/{ids}/associated

Returns all of a user's associated accounts, given their account_ids in {ids}.

{ids} can contain up to 100 semicolon delimited ids.

function getAssociatedAccounts(accountID, callback) {
    let url = 'users/' + accountID + '/associated';
    getSEWebServiceResponse(url, function(response) {
        if (!response.items)
            return;

        var accounts = sortAccountsByReputation(response.items);
        var accountsProcessed = 0;
        for (let i = 0; i < accounts.length; i++) {
            let siteName = accounts[i].site_url.replace('https://', '');
            siteName = siteName.replace('.stackexchange', '');
            siteName = siteName.replace('.com', '');
            getAssociatedAccountDetails(accounts[i].user_id, siteName, accounts[i].site_name, function() {
                if (++accountsProcessed >= accounts.length)
                    callback();
            });
        }
    });
}

The Full Implementation

/* Definitions */
var CardType = { Wheel: "wheel", Card: "card", Box: "box" }
var userCard = {
 username: '',
 profileImageUrl: '',
 reputation: 0,
 badges: {
  gold: 0,
  silver: 0,
  bronze: 0
 },
 siteUrls: []
}

/* Initial Calls */
var accountID = '13342919';
generateCard('user-flair-wheel', accountID, CardType.Wheel);

/* Required Events */
function showSitename(tooltipID, siteName) {
 var tooltip = document.getElementById(tooltipID);
 tooltip.innerHTML = siteName.replace('Stack Exchange', '');
 tooltip.classList.add('active');
}
function hideSitename(tooltipID) {
 document.getElementById(tooltipID).classList.remove('active');
}

/* UI Generation Functions */
function generateCard(containerid, accountid, cardType) {
 getAssociatedAccounts(accountID, function() {
  var className = cardType.toString().toLowerCase();
  var container = document.getElementById(containerid);
  container.classList.add("flair");
  container.classList.add(className);
  
  // Build the card.
  addProfile(container);
  addScores(container, className);
  addSites(container, className);
  container.innerHTML += '<div id="' + containerid +
           '-tooltip" class="se-tooltip"></div>';
 });
}
function addProfile(container) {
 container.innerHTML += '<img class="user-image" src="' +
           userCard.profileImageUrl + '"/>';
 container.innerHTML += '<h1 class="username display-4">' +
         userCard.username + '</h1>';
}
function addScores(container, cardType) {
 var badges = '<ul class="badges">';
 badges += '<li><i class="fas fa-trophy"></i> <span id="reputation-' +
     cardType + '">' + userCard.reputation + '</span></li>';
 badges += '<li><span id="gold-badges-' + cardType + '">' +
     userCard.badges.gold + '</span></li>';
 badges += '<li><span id="silver-badges-' + cardType + '">' +
     userCard.badges.silver + '</span></li>';
 badges += '<li><span id="bronze-badges-' + cardType + '">' +
     userCard.badges.bronze + '</span></li>';
 badges += '</ul>';
 container.innerHTML += badges;
}
function addSites(container, cardType) {
 var sites = '<ul id="sites-' + cardType + '" class="sites">';
 for (var i = 0; i < userCard.siteUrls.length; i++) {
  var site = '<li>';
  var siteLinkSplit = userCard.siteUrls[i].split('|');
  site += '<a href="' + siteLinkSplit[0] + '">';
  
  var tooltipID = container.id +'-tooltip';
  var linkElement = '<a href="' + siteLinkSplit[0] + '"';
  linkElement += ' onmouseover="showSitename(\'' + tooltipID + '\',\'' + siteLinkSplit[2] + '\')"';
  linkElement += ' onmouseout="hideSitename(\'' + tooltipID + '\');"';
  site += linkElement + '>';
  site += '<img src="' + (siteLinkSplit[1] == '<IMG>' ? '#' : siteLinkSplit[1]) + '"/></a></li>';
  sites += site;
 }
 
 sites += '</ul>';
 container.innerHTML += sites;
}

/* Stack Exchange API Based Functions */
function getAssociatedAccounts(accountID, callback) {
 let url = 'users/' + accountID + '/associated';
 getSEWebServiceResponse(url, function(response) {
  if (!response.items)
   return;
  
  var accounts = sortAccountsByReputation(response.items);
  var accountsProcessed = 0;
  for (let i = 0; i < accounts.length; i++) {
   let siteName = accounts[i].site_url.replace('https://', '');
   siteName = siteName.replace('.stackexchange', '');
   siteName = siteName.replace('.com', '');
   getAssociatedAccountDetails(accounts[i].user_id, siteName, accounts[i].site_name, function() {
    if (++accountsProcessed >= accounts.length)
     callback();
   });
  }
 });
}
function getAssociatedAccountDetails(userID, siteName, fullSiteName, callback) {
 let url = 'users/' + userID +'?order=desc&sort=reputation&site=' + siteName;
 getSEWebServiceResponse(url, function(response) {
  if (!response.items)
   return;
  
  let account = response.items[0];
  userCard.reputation += account.reputation;
  userCard.badges.gold += account.badge_counts.gold;
  userCard.badges.silver += account.badge_counts.silver;
  userCard.badges.bronze += account.badge_counts.bronze;
  
  if (userCard.siteUrls.length < 7) {
   var siteProfileCombo = account.link + '|<IMG>|' + fullSiteName;
   siteProfileCombo = siteProfileCombo.replace('<IMG>', getSiteIcon(siteName));
   userCard.siteUrls.push(siteProfileCombo);
  }
  if (userCard.username.length < 1)
   userCard.username = account.display_name;
  if (userCard.profileImageUrl.length < 1)
   userCard.profileImageUrl = account.profile_image;
  
  callback();
 });
}

/* Helper Functions */
function getSEWebServiceResponse(request, callback) {
 let apiRoot = 'https://api.stackexchange.com/2.2/';
 let key = 'key=s29XM)Eqn2x3YxhjLgFwBQ((';
 if (request.indexOf('?') >= 0)
  key = '&' + key;
 else
  key = '?' + key;
 
 getWebServiceResponse(apiRoot + request + key, function(response) { callback(response); });
}
function getWebServiceResponse(requestUrl, callback) {
 let request = new XMLHttpRequest();
 request.open('GET', requestUrl, true);
 request.onload = function() {
  if (request.status < 200 || request.status >= 400)
   callback("An unexpected error occurred.");
  else
   callback(JSON.parse(this.response));
 };
 request.send();
}
function sortAccountsByReputation(accounts) {
 return accounts.sort(function(a, b) { return b.reputation - a.reputation; });
}
function getSiteIcon(siteName) {
 if (siteName == "meta")
  return 'https://meta.stackexchange.com/content/Sites/stackexchangemeta/img/icon-48.png';
 
 return 'https://cdn.sstatic.net/Sites/' + siteName + '/img/apple-touch-icon.png';
}
/* Flair Styles */
.flair {
 position: relative;
 margin: 15px;
}
.flair > .se-tooltip {
 position: absolute;
 left: 50%;
 transform: translate(-50%);
 width: 250px;
 bottom: 50px;
 opacity: 0;
 background-color: #fff;
 color: #555;
 text-shadow: none;
 border-radius: 25px;
 padding: 5px 10px;
 box-shadow: 2px 2px 3px #0005;
}
.flair > .se-tooltip.active {
 bottom: 10px;
 opacity: 1;
}

/* Flair Wheel Styles */
.flair.wheel {
 width: 200px;
 height: 250px;
 display: flex;
 align-items: center;
 justify-content: center;
 flex-direction: column;
 text-shadow: 1px 1px 2px #0005;
}
.flair.wheel .user-image {
 width: 100px;
 height: 100px;
 border-radius: 50%;
 box-shadow: 2px 2px 3px #0005;
}
.flair.wheel .username {
 font-size: 30px;
 margin: 0;
}
.flair.wheel .badges > li > span { position: relative; }
.flair.wheel .badges > li:first-of-type > i { color: #5c9; }
.flair.wheel .badges > li:not(:first-of-type) > span::before {
 content: '';
 position: absolute;
 top: 50%;
 left: -15px;
 transform: translateY(-40%);
 width: 10px;
 height: 10px;
 border-radius: 50%;
}
.flair.wheel .badges > li:nth-child(2) > span::before { background-color: #fb3; }
.flair.wheel .badges > li:nth-child(3) > span::before { background-color: #aaa; }
.flair.wheel .badges > li:nth-child(4) > span::before { background-color: #c95; }

.flair.wheel .sites {
 position: absolute;
 top: 10px;
 left: 0;
 width: 100%;
 height: 55%;
}
.flair.wheel .sites > li { position: absolute; }
.flair.wheel .sites > li > a > img {
 width: 35px;
 height: 35px;
 background-color: #fffa;
 border-radius: 50%;
 padding: 2px;
 box-shadow: 2px 2px 3px #0005;
 cursor: pointer;
 transition: 0.3s cubic-bezier(0.5, -2.5, 1.0, 1.2) all;
 z-index: 1;
}
.flair.wheel .sites > li > a:hover > img {
 width: 40px;
 height: 40px;
 background-color: #fff;
}
.flair.wheel .sites > li:nth-child(1) {
 top: -15px;
 left: 50%;
 transform: translate(-50%);
}
.flair.wheel .sites > li:nth-child(2) {
 top: 0px;
 left: 15%;
 transform: translate(-20%);
}
.flair.wheel .sites > li:nth-child(3) {
 top: 0px;
 left: 70%;
 transform: translate(-20%);
}
.flair.wheel .sites > li:nth-child(4) {
 top: 45%;
 left: 80%;
 transform: translate(-20%, -50%);
}
.flair.wheel .sites > li:nth-child(5) {
 top: 45%;
 left: -5px;
 transform: translateY(-50%);
}
.flair.wheel .sites > li:nth-child(6) {
 top: 79%;
 left: 3px;
 transform: translateY(-50%);
}
.flair.wheel .sites > li:nth-child(7) {
 top: 79%;
 right: 3px;
 transform: translateY(-50%);
}

/* To Organize in a Row instead of Column */
.user-flair-container {
 display: flex;
 flex-direction: row;
 align-items: center;
 justify-content: center;
 flex-wrap: wrap;
}

/* Global Styles */
ul {
 padding: 0;
 listy-style-type: none;
}
ul > li {
 display: inline-block;
 padding: 0 10px;
}

/* Template Overrides */
html, body {
  margin: 0;
  height: 100%;
 background-color: #333 !important;
 background-image: linear-gradient(45deg, #333, #555) !important;
 background-image: -webkit-linear-gradient(45deg, #333, #555) !important;
}
.primary-content {
  height: 100%;
 display: flex;
 flex-direction: column;
 align-items: center;
}
.primary-content > .lead { font-size: 25px; }
<link href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/2940219/PerpetualJ.css" rel="stylesheet"/>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet"/>
<link href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" rel="stylesheet"/>
<div id="primary-content" class="primary-content">
 <div class="user-flair-container">
  <div id="user-flair-wheel"></div>
 </div>
</div>

Best of luck to all of you in your future endeavors!

Hazel へいぜる
  • 2,751
  • 1
  • 12
  • 44