Skip to content

Instantly share code, notes, and snippets.

@Noble-Mushtak
Last active August 29, 2015 14:25
Show Gist options
  • Save Noble-Mushtak/360f38610288c395e928 to your computer and use it in GitHub Desktop.
Save Noble-Mushtak/360f38610288c395e928 to your computer and use it in GitHub Desktop.
KA Contest Judging System Server Bot
//Include Firebase and jQuery AJAX
var http = require("http");
var Firebase = require("Firebase");
var najax = require("najax");
var minuteInterval = 30;
var howOften = 60 * 1000 * minuteInterval; //Should evaluate to "minuteInterval" minutes
//ka_api.js
/***
* This is a "wrapper" of sorts, for the Khan Academy API.
* Anything relating to the Khan Academy API should be placed in this file.
***/
window.KA_API = {
/* Khan Academy API Urls */
urls: {
/* For getting contests: */
spotlight: "https://www.khanacademy.org/api/internal/scratchpads/top?casing=camel&topic_id=xffde7c31&sort=4&limit=40000&page=0&lang=en&_=1436581332879",
/* For getting contest entries where contest has ID of programID */
spinoffs: function(programID) {
return "https://www.khanacademy.org/api/internal/scratchpads/{PROGRAM}/top-forks?casing=camel&sort=2&limit=300000&page=0&lang=en".replace("{PROGRAM}", programID);
},
scratchpadInfo: function(scratchpadId) {
return "https://www.khanacademy.org/api/labs/scratchpads/{SCRATCHPAD}".replace("{SCRATCHPAD}", scratchpadId);
}
},
/* This function gets all the entries for a specific contest, and passes them into the callback. */
getContestEntries: function(contestID, callback) {
/* Any entries that we find, will be put into this object. (We'll also pass this object into callback.) */
var entries = {};
/* Send AJAX request for getting all the entries to the desired contest */
var apiQuery = najax({
type: 'GET',
url: this.urls.spinoffs(contestID),
success: function(apiResponse) {
/* When the AJAX request finishes */
/* allPrograms is a list of all of the contest entries */
var allPrograms = apiResponse.responseJSON.scratchpads
/* Loop through allPrograms */
for (var i = 0; i < allPrograms.length; i++) {
/* Program ID for this spin-off */
var id = allPrograms[i].url.split("/")[5];
/* Add JSON object for this spin-off into entries */
entries[id] = {
/* Program ID */
id: id,
/* Program Title */
name: allPrograms[i].translatedTitle,
/* Program Scores */
scores: {
rubric: {
"Clean_Code": {
"rough": 1,
"avg": 1
},
"Creativity": {
"rough": 1,
"avg": 1
},
"Level": {
"rough": 1,
"avg": 1
},
"Overall": {
"rough": 1,
"avg": 1
},
"NumberOfJudges": 1,
"judgesWhoVoted": [ ]
}
}
};
}
/* Finally, pass entries into callback */
callback(entries);
}
});
},
/* This function gets all of Pamela's contest programs from Khan Academy and passes them into the callback function. */
getContests: function(callback) {
/* This Bool is true iff the first AJAX request has finished. */
var apiQueryDone = false;
/* Any contests that we find, will be put into this object. (We'll also pass this object into callback.) */
var allContests = {};
/* Send AJAX request to get all the contests from Khan Academy */
var apiQuery = najax({
type: 'GET',
url: this.urls.spotlight,
success: function(apiResponse) {
/* When the AJAX request finishes */
/* allPrograms is a list of all of the contest entries */
var allPrograms = apiResponse.responseJSON.scratchpads;
/* Loop through allPrograms */
for (var i = 0; i < allPrograms.length; i++) {
/* For now, let's only accept contests from pamela. Also, all contests must have "Contest" in their title. */
if (allPrograms[i].authorNickname.match("pamela") !== null && allPrograms[i].translatedTitle.match("Contest") !== null) {
var programID = allPrograms[i].url.split("/")[5];
/* Make an empty object in allContests to alert the setTimeout() function below that such an object has yet to be set. */
allContests[programID] = {};
/* Put this in a function wrapper so the parameters will be saved. */
(function(programID, scratchpad, contests) {
najax({
type: 'GET',
url: KA_API.urls.scratchpadInfo(programID),
success: function(scratchpadData) {
/* Add in a JSON object for this contest into contests */
contests[programID] = {
/* Program ID */
id: programID,
/* Program Title */
name: scratchpad.translatedTitle,
/* Program Icon */
img: scratchpad.thumb,
/* Contest Description */
desc: scratchpadData.responseJSON.description
};
/* Fetch the contest entries for this contest and when done, set the entries property within the above JSON object. */
KA_API.getContestEntries(contests[programID].id, function(entries) {
contests[programID].entries = entries;
});
}
});
})(programID, allPrograms[i], allContests);
/* Pass in the parameters to this function as above. */
}
}
/* Once we're done with the AJAX request, set apiQueryDone to true. */
apiQueryDone = true;
}
});
/* Check to see if we're done every second; if we are, invoke the callback function. (Idea from @noble-mushtak) */
var finishTimeout = setInterval(function() {
if (apiQueryDone) {
/* If the first AJAX request has finished, make sure all of the other AJAX requests have finished. If we find a contest without the entries property, we know their AJAX request has not finished, so we return. */
for (var i in allContests) {
if (!allContests[i].hasOwnProperty("entries")) return;
}
/* At this point, we have made sure the request has finished, so we make a log in the console, stop looping this asynchronous function, and finally pass contests into callback. */
console.log("getContests() finished!");
/* Make sure clearTimeout() is called first. We don't know how long callback will take and if callback takes more than a second, then callback will be called multiple times. */
clearInterval(finishTimeout);
callback(allContests);
}
}, 1000);
}
};
//Contest_Judging_System (getStoredContests() and sync() only)
var Contest_Judging_System = {
/* This function gets all the contests that we have stored on Firebase and passes them into a callback function. */
getStoredContests: function(callback) {
/* This is the object for the contests within our Firebase database. */
var fbRef = new Firebase("https://contest-judging-sys.firebaseio.com/contests/");
/* This is an object to hold all of the data from Firebase. */
var fromFirebase = {};
/* Insert all of the entries in our database in order by key */
fbRef.orderByKey().on("child_added", function(item) {
fromFirebase[item.key()] = item.val();
});
/* Finally, pass fromFirebase into callback. */
fbRef.once("value", function(data) {
callback(fromFirebase);
});
},
sync: function(callback) {
/*
* sync() just fetches the latest data from Khan Academy and Firebase, and compares it.
* We have two objects; kaData and fbData. We get the data using the KA_API and the above getStoredContests() method.
* Once both requests have finished, we set fbData to kaData using the Firebase set() method.
* Originally authored by Gigabyte Giant
*/
/* These two Booleans check whether or not both requests have been completed. */
var completed = {
firebase: false,
khanacademy: false
};
/* Our two objects of data */
var kaData;
var fbData;
/* Get all of the contests from Khan Academy */
KA_API.getContests(function(response) {
/* When done, set kaData to the contests and set completed.khanacademy to true. */
kaData = response;
completed.khanacademy = true;
});
/* Get all of the known contests from Firebase */
this.getStoredContests(function(response) {
/* When done, set fbData to our stored contests and set completed.firebase to true. */
fbData = response;
completed.firebase = true;
});
/* Create a new reference to Firebase to use later on when pushing contests to Firebase. */
var fbRef = new Firebase("https://contest-judging-sys.firebaseio.com/contests/");
/* Every second, we check if both requests have been completed and if they have, we stop checking if both requests have been completed and set fbRef to kaData using the Firebase set() method. */
var recievedData = setInterval(function() {
if (completed.firebase && completed.khanacademy) {
clearInterval(recievedData);
/* The following objects are used for our "diff" checking */
var toAddToFirebase = { };
var toRemoveFromFirebase = { };
var entriesToAdd = { };
var entriesToRemove = { };
/* Loop through all the data we recieved from Khan Academy; and see if we already have it in Firebase. */
for (var i in kaData) {
if (!fbData.hasOwnProperty(i)) {
// Most likely a new contest; add it to Firebase!
console.log("[sync] We found a new contest! Contest ID: " + i);
toAddToFirebase[i] = kaData[i];
} else {
// We have this contest in Firebase; so now let's see if we have all the entries
for (var j in kaData[i].entries) {
if (!fbData[i].entries.hasOwnProperty(j)) {
// New entry! Add to Firebase.
console.log("[sync] We found a new entry! Contest ID: " + i + ". Entry ID: " + j);
/* TODO */
if (!entriesToAdd.hasOwnProperty(i)) {
entriesToAdd[i] = [];
entriesToAdd[i].push( kaData[i].entries[j] );
} else {
entriesToAdd[i].push( kaData[i].entries[j] );
}
}
}
}
}
/* Loop through all the data we recieved from Firebase; and see if it still exists on Khan Academy. */
for (var i in fbData) {
if (!kaData.hasOwnProperty(i)) {
// Contest removed. Delete from Firebase
console.log("[sync] We found a contest that no longer exists. ID: " + i);
toRemoveFromFirebase[i] = fbData[i];
} else {
// Contest still exists. Now let's see if any entries have been removed.
for (var j in fbData[i].entries) {
if (!kaData[i].entries.hasOwnProperty(j)) {
// Entry no longer exists on Khan Academy; delete from Firebase (or mark as archived).
console.log("[sync] We found an entry that doesn't exist anymore! Contest ID: " + i + ". Entry ID: " + j);
/* TODO */
if (!entriesToRemove.hasOwnProperty(i)) {
entriesToRemove[i] = [];
entriesToRemove[i].push(j);
} else {
entriesToRemove[i].push(j);
}
}
}
}
}
/* Add what we don't have; and remove what Khan Academy *doesn't* have. */
for (var a in toAddToFirebase) {
// Add to Firebase!
fbRef.child(a).set(toAddToFirebase[a]);
}
for (var r in toRemoveFromFirebase) {
// Remove from Firebase!
fbRef.child(r).set(null);
}
for (var ea in entriesToAdd) {
/* Add all the new entries to Firebase */
for (var i = 0; i < entriesToAdd[ea].length; i++) {
console.log("[sync] Adding " + entriesToAdd[ea][i].id + " to Firebase.");
fbRef.child(ea).child("entries").child(entriesToAdd[ea][i].id).set(entriesToAdd[ea][i]);
}
}
for (var er in entriesToRemove) {
/* Remove all the old entries from Firebase */
for (var i = 0; i < entriesToRemove[er].length; i++) {
console.log("[sync] Removing " + entriesToRemove[er][i] + " from Firebase!");
fbRef.child(er).child("entries").child(entriesToRemove[er][i]).set(null);
}
}
console.log("[sync] Done");
callback(kaData);
}
}, 1000);
}
};
//Run setInterval() for the bot
//.bind() is called so this inside sync() will be Contest_Judging_System
var bot = setTimeout(Contest_Judging_System.sync.bind(Contest_Judging_System), howOften);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment