Skip to content

Instantly share code, notes, and snippets.

@Synvox
Created January 22, 2017 03:34
Show Gist options
  • Save Synvox/7061d46074af136db35d97d8a0bc06f6 to your computer and use it in GitHub Desktop.
Save Synvox/7061d46074af136db35d97d8a0bc06f6 to your computer and use it in GitHub Desktop.
I-Know crawler
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