Created
January 22, 2017 03:34
-
-
Save Synvox/7061d46074af136db35d97d8a0bc06f6 to your computer and use it in GitHub Desktop.
I-Know crawler
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const isWeb = window.location.href.indexOf('http') !== -1 | |
import 'whatwg-fetch' | |
const feathers = require('feathers/client') | |
const rest = require('feathers-rest/client') | |
const hooks = require('feathers-hooks') | |
const seedrandom = require('seedrandom') | |
const insight = require('./insight') | |
const app = feathers() | |
.configure(hooks()) | |
.configure(rest('https://darkspace.synvox.net').fetch(window.fetch.bind(window))) | |
const storageService = app.service('storage') | |
const hash = (input)=>{ | |
const versions = { | |
v1:'3d7f97adaae0d132-aa1e80a9af07f7d8-c4a20e851f0f7758' | |
} | |
const version = 'v1' | |
const rng = seedrandom(`${input}-${versions[version]}`) | |
return `${version}-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx`.replace(/x/g, (c)=>{ | |
return `${(rng()*16|0).toString(16)}` | |
}) | |
} | |
const opts = { | |
storagePrefix: 'darkspace', | |
get host () { | |
return storage.get('host') || 'https://byui.brightspace.com' | |
} | |
} | |
const storage = { | |
get: (id)=>{ | |
const str = localStorage.getItem(`${opts.storagePrefix}-${id}`) | |
return str ? JSON.parse(str) : undefined | |
}, | |
set: (id,data)=>{ | |
localStorage.setItem(`${opts.storagePrefix}-${id}`,JSON.stringify(data)) | |
} | |
} | |
const getJSON = (url, cb)=>{ | |
return new Promise((Ok, fail)=>{ | |
let linker = url.indexOf('?') === -1 ? '?' : '&', | |
append = `${linker}darkspace-no-cache=${Math.round(Math.random() * 1000000)}` | |
fetch(`${ opts.host }${url}${append}`, { credentials: 'include' }) | |
.then((res)=>{ | |
if (res.status >= 200 && res.status < 300) { | |
return res | |
} else { | |
var error = new Error(res.statusText) | |
error.res = res | |
fail(error) | |
} | |
}) | |
.then(res=>res.json()) | |
.then(res=>Ok(res)) | |
.catch(fail) | |
}) | |
} | |
const getUser = ()=>{ | |
return new Promise((Ok, fail)=>{ | |
getJSON(`/d2l/api/lp/1.5/users/whoami`) | |
.then((userData)=>{ | |
Ok({ | |
id: userData.Identifier, | |
firstName: userData.FirstName, | |
lastName: userData.LastName, | |
unique: userData.UniqueName, | |
host: opts.host | |
}) | |
}) | |
.catch((e)=>{fail({error: 'Unable to find user\'s id.', text: 'Log In', href: opts.host })}) | |
}) | |
} | |
const getEnrollments = ()=>{ | |
return new Promise((Ok, fail)=>{ | |
getJSON(`/d2l/api/lp/1.5/enrollments/myenrollments/`).then((res)=>{ | |
let courses = res.Items.length ? res.Items : [] | |
Ok(courses.filter((courseDoc)=>{ | |
// For whatever reason, some classes are not reporting the right date. | |
// Reported by Alec Ian MathewsView | |
// && courseDoc.Access.EndDate && Date.parse(courseDoc.Access.EndDate) >= Date.now() | |
return courseDoc.OrgUnit.Type.Id === 3 | |
}).map((courseDoc)=>{ | |
return { | |
course: courseDoc.OrgUnit, | |
orgUnitId: ''+courseDoc.OrgUnit.Id, | |
name: courseDoc.OrgUnit.Name | |
} | |
})) | |
}) | |
}) | |
} | |
const getEvents = (orgUnitId)=>{ | |
return new Promise((Ok, fail)=>{ | |
getJSON(`/d2l/api/le/1.5/${ orgUnitId }/calendar/events/`).then((newEvents)=>{ | |
let events = newEvents.filter((e)=>{ | |
return e.IsAssociatedWithEntity | |
}).map((e)=>{ | |
return { | |
orgUnitId, | |
eventId: e.CalendarEventId, | |
startDate: e.StartDateTime ? new Date(e.StartDateTime) : new Date(e.StartDate), | |
endDate: e.EndDateTime ? new Date(e.EndDateTime) : new Date(e.EndDate), | |
title: e.Title, | |
courseName: e.OrgUnitName, | |
href: e.AssociatedEntity ? e.AssociatedEntity.Link : '#' | |
} | |
}) | |
Ok(events) | |
}) | |
}) | |
} | |
const getGrades = (name, orgUnitId)=>{ | |
return new Promise((Ok, fail)=>{ | |
const getGradeItems = (finalGrade={})=>{ | |
getJSON(`/d2l/api/le/1.5/${ orgUnitId }/grades/values/myGradeValues/`).then((newGrades)=>{ | |
let categories = [], | |
items = [], | |
numerator = finalGrade.PointsNumerator || null, | |
denominator = finalGrade.PointsDenominator || null, | |
percent = '-- %' | |
const parse = (grade)=>{ | |
let percent = (grade.PointsNumerator / grade.PointsDenominator * 100).toFixed(0) | |
return { | |
title: grade.GradeObjectName, | |
numerator: grade.PointsNumerator, | |
denominator: grade.PointsDenominator, | |
weightedNumerator: grade.WeightedNumerator || grade.PointsNumerator, | |
weightedDenominator: grade.WeightedDenominator || grade.PointsDenominator, | |
percent: `${ percent }%` | |
} | |
} | |
newGrades.forEach((grade)=>{ | |
if (grade.GradeObjectType === 9 /*Category*/) | |
categories.push(parse(grade)) | |
else if (grade.GradeObjectType === 1 /*Numeric*/) | |
items.push(parse(grade)) | |
}) | |
if (typeof numerator === 'number' && typeof denominator === 'number' && denominator > 0) | |
percent = `${ (numerator / denominator * 100).toFixed(2) }%` | |
Ok({ orgUnitId, name, categories, items, numerator, denominator, percent }) | |
}) | |
} | |
getJSON(`/d2l/api/le/1.5/${ orgUnitId }/grades/final/values/myGradeValue`).then((finalGrade)=>{ | |
getGradeItems(finalGrade) | |
}).catch((err)=>{ | |
console.error(err) | |
getGradeItems() | |
}) | |
}) | |
} | |
const refresh = ()=>{ | |
const now = new Date(), | |
msInDay = 86400000 | |
return new Promise((Ok, fail)=>{ | |
getUser() | |
.then((user)=>{ | |
let events = [], | |
grades = [], | |
expectedCallbacks = 0 | |
const finish = ()=>{ | |
events = events.sort((a,b)=>{ | |
let factor = a.endDate - b.endDate | |
if (factor === 0) { | |
return a.title.localeCompare(b.title) | |
} | |
return factor | |
}) | |
// Filter out duplicates | |
events = events.map((e,i)=>{ | |
let keep = true | |
for(let i2 = 0; i2 < i; i2++) { | |
const e2 = events[i2] | |
if (e2.eventId === e.eventId) { | |
// Avalible, Due, Ends, etc | |
keep = false | |
} if (e2.title === e.title && +e2.endDate === +e.endDate && e.orgUnitId === e2.orgUnitId) { | |
keep = false | |
} | |
} | |
return keep ? e : undefined | |
}).filter((e)=>{ | |
return e !== undefined | |
}).filter((e)=>{ | |
return +e.endDate > +now - msInDay * 7 && +e.endDate < +now + msInDay * 30 | |
}) | |
grades = grades.sort((a,b)=>{ | |
if (b.denominator === 0) return -1 | |
if (a.denominator === 0) return 1 | |
return (b.numerator/b.denominator) - (a.numerator/a.denominator) | |
}) | |
// This had some bugs before, be careful | |
grades = grades.map((e,i)=>{ | |
let keep = true | |
for(let i2 = 0; i2 < i; i2++) { | |
const e2 = grades[i2] | |
if (e.orgUnitId === e2.orgUnitId) { | |
keep = false | |
} | |
} | |
return keep ? e : undefined | |
}).filter((e)=>{ | |
return e !== undefined | |
}) | |
Ok({user, events , grades, updatedAt: +now}) | |
} | |
getEnrollments().then((enrollments)=>{ | |
enrollments.forEach((enrollment)=>{ | |
let course = enrollment.course, | |
orgUnitId = enrollment.orgUnitId, | |
name = enrollment.name | |
expectedCallbacks += 2 | |
getEvents(orgUnitId).then((newEvents)=>{ | |
events = events.concat(newEvents) | |
if (--expectedCallbacks === 0) finish() | |
}) | |
getGrades(name,orgUnitId).then((newGrades)=>{ | |
grades.push(newGrades) | |
if (--expectedCallbacks === 0) finish() | |
}) | |
}) | |
}) | |
}) | |
.catch((e)=>{ fail(e) }) | |
}) | |
} | |
const save = (data)=>{ | |
return new Promise((Ok, fail)=>{ | |
storage.set(data.user.id, data) | |
storage.set(`user`, data.user) | |
storage.set(`host`, data.user.host) | |
storageService.update(hash(data.user.id), data).then(()=>{ | |
console.info('Update Complete') | |
}).catch(()=>{ | |
console.error('Update Failed') | |
}) | |
getLevels(data) | |
.then((levels)=>{ | |
data.levels = levels | |
drawIcon(levels.urgent) | |
Ok(data) | |
}) | |
}) | |
} | |
const pull = ()=>{ | |
return new Promise((Ok, fail)=>{ | |
const user = storage.get(`user`) || null | |
if (user === null) { | |
Ok(null) | |
return | |
} | |
const data = storage.get(`${user.id}`) || null | |
getLevels(data).then((levels)=>{ | |
data.levels = levels | |
Ok(data) | |
}) | |
}) | |
} | |
const download = ()=>{ | |
return new Promise((Ok, fail)=>{ | |
const id = storage.get('web-user-id') | |
storageService.get(id).then((data)=>{ | |
console.info('Fetch Complete') | |
Ok(data) | |
}).catch(()=>{ | |
console.error('Fetch Failed') | |
Ok(storage.get(id)) | |
}) | |
}) | |
} | |
const merge = (current)=>{ | |
return new Promise((Ok, fail)=>{ | |
pull().then((previous)=>{ | |
if (previous === null) { | |
Ok(Object.assign({}, current)) | |
return | |
} | |
const finish = (server)=>{ | |
const events = current.events.map((currentEvent)=>{ | |
const previousEvent = (previous.events||[]).find((previousEvent)=>{ | |
return (previousEvent.eventId === currentEvent.eventId) | |
}) || {} | |
const serverEvent = (server.events||[]).find((serverEvent)=>{ | |
return (serverEvent.eventId === currentEvent.eventId) | |
}) || {} | |
return Object.assign({}, serverEvent, previousEvent, currentEvent) | |
}) | |
Ok(Object.assign({}, current, { events })) | |
} | |
storageService.get(hash(current.user.id)).then((data)=>{ | |
console.info('Fetch Complete') | |
finish(data) | |
}).catch(()=>{ | |
console.error('Fetch Failed') | |
finish({}) | |
}) | |
}) | |
}) | |
} | |
const getLevels = (data)=>{ | |
return new Promise((Ok, fail)=>{ | |
const msInDay = 86400000 | |
now = new Date() | |
let now = new Date(), | |
levels = { | |
panic: 0, | |
urgent: 0 | |
} | |
if (!data) | |
return Ok(levels) | |
data.events.forEach((event)=>{ | |
let date = new Date(event.endDate) | |
if (date >= now) { | |
let urgent = date - now < msInDay * 2 // 2 days | |
// disclude finished events | |
if (urgent && event.finished) urgent = false | |
if (urgent) { | |
levels.urgent++ | |
let difference = (date - now) / 3600000 // hour | |
if (difference < 6) { | |
levels.panic += Math.max(6-difference,0) / 2 | |
if (difference < 0.5) { | |
levels.panic += 1 | |
} | |
if (difference < 0.25) { | |
levels.panic += 1 | |
} | |
if (difference < 0.125) { | |
levels.panic += 1 | |
} | |
} | |
} | |
} | |
}) | |
if (Number.isNaN(levels.panic)) levels.panic = 5 | |
if (levels.panic > 5) levels.panic = 5 | |
if (levels.panic < 0) levels.panic = 0 | |
levels.panic = levels.panic.toFixed(1) | |
Ok(levels) | |
}) | |
} | |
const drawIcon = (number)=>{ | |
return new Promise((Ok, fail)=>{ | |
if (isWeb) { | |
Ok() | |
return | |
} | |
var canvas = document.createElement('canvas') | |
var cxt = canvas.getContext('2d') | |
canvas.width = 40 | |
canvas.height = 40 | |
cxt.textAlign = 'center' | |
cxt.textBaseline = 'middle' | |
cxt.font = '30px "Lucida Console", Monaco, monospace' | |
cxt.fillStyle = '#333' | |
cxt.strokeStyle = '#ececec' | |
cxt.lineWidth = 4 | |
cxt.strokeText(number, canvas.width/2, canvas.height/2) | |
cxt.fillText(number, canvas.width/2, canvas.height/2) | |
// Both are the same size. Chrome doesn't care. *troll face* | |
window.chrome.browserAction.setIcon({ | |
path: { | |
"19": canvas.toDataURL(), | |
"38": canvas.toDataURL() | |
} | |
}) | |
Ok() | |
}) | |
} | |
const main = ()=>{ | |
return new Promise((Ok, fail)=>{ | |
(isWeb ? download() : refresh()) | |
.then((data)=>{ return merge(data) }) | |
.then((data)=>{ return save(data) }) | |
.then(()=>{ | |
return pull() | |
}) | |
.then((data)=>{ | |
if (data) { | |
getLevels(data) | |
.then((levels)=>{ | |
drawIcon(levels.urgent) | |
data.levels = levels | |
Ok(data) | |
}) | |
} else Ok(data) | |
}) | |
.catch((err)=>{ | |
fail(err) | |
}) | |
}) | |
} | |
if (!isWeb && window.chrome && window.chrome.runtime.onInstalled) { | |
window.chrome.runtime.onInstalled.addListener(function(details){ | |
ga('send', 'event', 'Update') | |
}) | |
window.chrome.alarms.create("iknow-redraw", {periodInMinutes: 60} ) | |
window.chrome.alarms.onAlarm.addListener(function(){ | |
main() | |
}) | |
} | |
if (isWeb && window.location.hash !== '') { | |
storage.set('web-user-id', window.location.hash.substring(1)) | |
} | |
if (!isWeb && window.chrome && window.chrome.runtime.onInstalled) { | |
window.chrome.runtime.onInstalled.addListener(function(details){ | |
ga('send', 'event', 'Update') | |
}) | |
window.chrome.alarms.create("darkspace-redraw", {periodInMinutes: 60} ) | |
window.chrome.alarms.onAlarm.addListener(()=>main()) | |
} | |
window.addEventListener('error', function(e) { | |
try { | |
let report = `${e.message} at ${e.filename}:${e.lineno}:${e.colno}` | |
ga('send', 'event', 'Error', e.error.name, report) | |
} catch(e) { /* Errors should not loop... ever. */ } | |
}, false) | |
export default { | |
main, | |
pull, | |
save, | |
get: storage.get, | |
set: storage.set, | |
hash | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment