User:Dagger/Widget drafts/API cache.js

From Guild Wars 2 Wiki
Jump to navigationJump to search

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
var API = {
  key: localStorage.getItem("api-key"),
  perms: localStorage.getItem("api-key-permissions"),
  cacheTime: localStorage.getItem("api-cachetime") || 300 * 1000 /* 5 minutes */,
  updates: {},
  endpoints: {
    "characters": {
      url: "https://api.guildwars2.com/v2/characters?wiki=1&page=0&page_size=200",
      requiresAuth: true,
      requiresPerms: ["characters"],
      postProcessing: function (data) {
        data.forEach(function (character, index) {
          // Pre-calculate a list of active crafting disciplines on this character.
          data[index].activeCrafting = [];
          character.crafting.forEach(function (craft) {
            if (craft.active)
              data[index].activeCrafting.push(craft.discipline.toLowerCase());
          });
        });
        // Sort characters by play time. This is probably roughly what people want.
        return data.sort(function(a, b) { return a.age < b.age; });
      },
    },
    "account/skins": {
      url: "https://api.guildwars2.com/v2/account/skins?wiki=1",
      requiresAuth: true,
      requiresPerms: ["unlocks"],
    },
    "account/dyes": {
      url: "https://api.guildwars2.com/v2/account/dyes?wiki=1",
      requiresAuth: true,
      requiresPerms: ["unlocks"],
    },
    "account/minis": {
      url: "https://api.guildwars2.com/v2/account/minis?wiki=1",
      requiresAuth: true,
      requiresPerms: ["unlocks"],
    },
    "account/materials": {
      url: "https://api.guildwars2.com/v2/account/materials?wiki=1",
      requiresAuth: true,
      requiresPerms: ["inventories"],
      postProcessing: function(data) {
        // Reshape the response so we can look up materials by ID rather
        // than having to iterate through the whole array to find them.
        var items = {};
        data.forEach(function(item) { items[item.id] = item; });
        return items;
      },
    },
    "account/bank": {
      url: "https://api.guildwars2.com/v2/account/bank?wiki=1",
      requiresAuth: true,
      requiresPerms: ["inventories"],
    },
    "account/wallet": {
      url: "https://api.guildwars2.com/v2/account/wallet?wiki=1",
      requiresAuth: true,
      requiresPerms: ["wallet"],
      postProcessing: function(data) {
        // Reshape the response so we can look up currencies by ID rather
        // than having to iterate through the whole array to find them.
        var currencies = {};
        data.forEach(function(currency) {
          currencies[currency.id] = currency.value;
        });
        return currencies;
      },
    },
  },

  fetch: function API_fetchAPI(name) {
    var endpoint = this.endpoints[name];

    // If this endpoint has already been requested, return the first request.
    if (this.updates[name]) { return this.updates[name]; }

    var dfr = new $.Deferred();
    this.updates[name] = dfr.promise();
    
    // If the API is disabled, don't resolve the promise -- just hang.
    // This exists to make testing code changes easier.
    if (localStorage.getItem("api-disabled")) { return this.updates[name]; }
    
    // If there's no API key set then don't do any queries.
    if (!API.key) { return dfr.reject("No API key set."); }
    
    // Check if our key has the required permissions for this query. No sense
    // in hitting the API endpoint constantly if it's just going to reject us.
    if (endpoint.requiresPerms && endpoint.requiresPerms.some(function(perm) { return API.perms.indexOf(perm) == -1 })) {
      return dfr.reject("API key is missing at least one necessary permission. Please set a new key.");
    }

    // Return the cached response if it's recent enough.
    var cacheKey = "api-cache-" + name;
    var cachedResponse = localStorage.getItem(cacheKey);
    var cachedResponseAge = new Date() - new Date(localStorage.getItem(cacheKey + "-updated"));
    if (cachedResponse && cachedResponseAge < API.cacheTime) {
      API.log("Using cached response for " + name);
      dfr.resolve(JSON.parse(cachedResponse));
    } else {
      // It's not recent. Time to fetch from the API.
      var url = endpoint.url;
      if (endpoint.requiresAuth) {
        url += (url.indexOf('?') > -1 ? "&" : "?");
        url += "access_token=" + this.key;
      }
      API.log("Querying API: " + url);
      $.getJSON(url).then(function(data) {
        if (endpoint.postProcessing) data = endpoint.postProcessing(data);
        localStorage.setItem(cacheKey, JSON.stringify(data));
        localStorage.setItem(cacheKey + "-updated", new Date().toISOString());
        dfr.resolve(data);
      }, function onFail(reason) { dfr.reject(reason); });
    }
    return this.updates[name];
  },

  log: function API_log() {
    if (console && console.log) console.log.apply(console, arguments);
  },
}

// If a key is set, show all API-related elements.
if (API.key) $(".api.hide").removeClass("hide");



// Load the other scripts. If we don't do this from this page, these scripts end up racing this one...
importScript("User:Dagger/Widget drafts/API recipe unlocks.js");
importScript("User:Dagger/Widget drafts/API item counts.js");
importScript("User:Dagger/Widget drafts/API mini unlocks.js");
importScript("User:Dagger/Widget drafts/API dye unlocks.js");
importScript("User:Dagger/Widget drafts/API skin unlocks.js");
importScript("User:Dagger/Widget drafts/API wallet.js");
importScript("User:Dagger/Widget drafts/API preferences.js");

// This should be in a <style> element in the final widget.
var style = document.createElement("style");
document.querySelector("head").appendChild(style);
style.sheet.insertRule(".api-craftinginactive { opacity: 0.5; }", 0);