├── .gitignore ├── Driver ├── .env.example ├── .gitignore ├── firefly-api.js ├── package-lock.json ├── package.json └── test.js ├── Examples ├── auth.js ├── environment.example.json ├── misc.js ├── package-lock.json └── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Git ignore 2 | 3 | #Node modules 4 | Examples/node_modules 5 | Examples/environment.json 6 | 7 | Examples/test.js -------------------------------------------------------------------------------- /Driver/.env.example: -------------------------------------------------------------------------------- 1 | CODE= 2 | HOST= 3 | 4 | DEVICE_ID= 5 | XML= -------------------------------------------------------------------------------- /Driver/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /Driver/firefly-api.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Firefly API Driver 4 | 5 | */ 6 | const axios = require('axios'); 7 | const xml = require('fast-xml-parser'); 8 | const { v4: uuidv4 } = require('uuid'); 9 | 10 | class Firefly { 11 | constructor(host, appId = 'Firefly Node.JS Driver') { 12 | if (!host) throw 'Invalid host'; 13 | 14 | this.host = host; 15 | this.appId = appId; 16 | } 17 | 18 | // Get a school from a code 19 | static getHost(code, appId = 'Firefly Node.JS Driver', deviceId = null) { 20 | if (!code) throw 'Invalid code'; 21 | if (!deviceId) deviceId = uuidv4(); 22 | 23 | return new Promise((resolve, reject) => { 24 | axios.get('https://appgateway.fireflysolutions.co.uk/appgateway/school/' + code) 25 | .then(response => { 26 | try { 27 | response = xml.parse(response.data, { ignoreAttributes: false, allowBooleanAttributes: true }) 28 | } 29 | catch (error) { return reject(error) } 30 | 31 | if ((!response.response) || response.response['@_exists'] === 'false') return resolve(null); 32 | 33 | let host = response.response.address['#text']; 34 | let ssl = response.response.address['@_ssl'] === 'true'; 35 | let url = (ssl ? 'https://' : 'http://') + host; 36 | let tokenUrl = encodeURIComponent(`${url}/Login/api/gettoken?ffauth_device_id=${deviceId}&ffauth_secret=&device_id=${deviceId}&app_id=${appId}`); 37 | 38 | return resolve({ 39 | enabled: response.response['@_enabled'] === 'true', 40 | name: response.response.name, 41 | id: response.response.installationId, 42 | host: host, 43 | ssl: ssl, 44 | url: url, 45 | tokenUrl: `${url}/login/login.aspx?prelogin=${tokenUrl}`, 46 | deviceId: deviceId 47 | }); 48 | }) 49 | .catch(error => reject(error)); 50 | }); 51 | } 52 | 53 | // Get the host api version 54 | get apiVersion() { 55 | return new Promise((resolve, reject) => { 56 | axios.get(this.host + '/login/api/version') 57 | .then(response => { 58 | try { 59 | response = xml.parse(response.data) 60 | } 61 | catch (error) { return reject(error) } 62 | 63 | return resolve({ 64 | major: response.version.majorVersion, 65 | minor: response.version.minorVersion, 66 | increment: response.version.incrementVersion 67 | }); 68 | }) 69 | .catch(error => reject(error)); 70 | }); 71 | } 72 | 73 | // Set or create the device id 74 | setDeviceId(id = null) { 75 | if (!id) id = uuidv4(); 76 | this.deviceId = id; 77 | return id; 78 | } 79 | 80 | // Get an authentication url 81 | get authUrl() { 82 | if (!this.deviceId) this.setDeviceId(); 83 | let redirect = encodeURIComponent(`${this.host}/Login/api/gettoken?ffauth_device_id=${this.deviceId}&ffauth_secret=&device_id=${this.deviceId}&app_id=${this.appId}`); 84 | return `${this.host}/login/login.aspx?prelogin=${redirect}`; 85 | } 86 | 87 | authenticate() { 88 | console.log(`Please go to Firefly, login and paste 'document.documentElement.outerHTML' in the browser console (CTRL+SHIFT+I). Then add it's response in completeAuthentication() - it should be something like "..."`); 89 | console.log('Device ID: ' + this.deviceId); 90 | console.log(this.authUrl); 91 | } 92 | completeAuthentication(xmlResponse) { 93 | if (!xmlResponse) throw 'Invalid xml'; 94 | if (xmlResponse.slice(0, 1) === '"') xmlResponse = xmlResponse.slice(1); 95 | if (xmlResponse.slice(-1, -0) === '"') xmlResponse = xmlResponse.slice(0, -1); 96 | xmlResponse = xmlResponse.replace(/\\/g, ''); 97 | 98 | if (xml.validate(xmlResponse) !== true) throw 'Invalid xml'; 99 | 100 | try { 101 | var json = xml.parse(xmlResponse, { ignoreAttributes: false }); 102 | } 103 | catch (err) { throw 'Invalid xml' } 104 | 105 | this.secret = json.token.secret; 106 | this.user = { 107 | username: json.token.user['@_username'], 108 | fullname: json.token.user['@_fullname'], 109 | email: json.token.user['@_email'], 110 | role: json.token.user['@_role'], 111 | guid: json.token.user['@_guid'], 112 | } 113 | this.classes = json.token.user.classes.class.map(_class => ({ guid: _class['@_guid'], name: _class['@_name'], subject: _class['@_subject'] })); 114 | 115 | return true; 116 | } 117 | 118 | // Verify token 119 | verifyCredentials() { 120 | if (!this.deviceId) return false; 121 | if (!this.secret) return false; 122 | 123 | return new Promise((resolve, reject) => { 124 | axios.get(this.host + `/Login/api/verifytoken?ffauth_device_id=${this.deviceId}&ffauth_secret=${this.secret}`) 125 | .then(response => { 126 | return resolve(response.data.valid); 127 | }) 128 | .catch(error => reject(error)); 129 | }); 130 | } 131 | 132 | graphQuery(query) { 133 | return axios.post(this.host + `/_api/1.0/graphql?ffauth_device_id=${this.deviceId}&ffauth_secret=${this.secret}`, 'data='+encodeURIComponent(query), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }}); 134 | } 135 | 136 | // Get app configuration 137 | get configuration() { 138 | if (!this.user) throw 'Not authenticated'; 139 | 140 | return new Promise((resolve, reject) => { 141 | this.graphQuery(`query Query { 142 | configuration { 143 | week_start_day, weekend_days, native_app_capabilities, notice_group_guid 144 | } 145 | }`) 146 | .then(response => resolve(response.data.data.configuration)) 147 | .catch(err => reject(err)); 148 | }); 149 | } 150 | 151 | // Get styles 152 | get appStyles() { 153 | if (!this.user) throw 'Not authenticated'; 154 | 155 | return new Promise((resolve, reject) => { 156 | this.graphQuery(`query Query { 157 | app_styles { 158 | value, name, type, file 159 | } 160 | }`) 161 | .then(response => resolve(response.data.data.app_styles)) 162 | .catch(err => reject(err)); 163 | }); 164 | } 165 | 166 | // Get general data 167 | get bookmarks() { 168 | if (!this.user) throw 'Not authenticated'; 169 | 170 | return new Promise((resolve, reject) => { 171 | this.graphQuery(`query Query { 172 | users(guid: "${this.user.guid}") { 173 | bookmarks { 174 | simple_url, deletable, position, read, from { 175 | guid, name 176 | }, type, title, is_form, form_answered, breadcrumb, guid, created 177 | } 178 | } 179 | }`) 180 | .then(response => resolve(response.data.data.users[0].messages)) 181 | .catch(err => reject(err)); 182 | }); 183 | } 184 | get messages() { 185 | if (!this.user) throw 'Not authenticated'; 186 | 187 | return new Promise((resolve, reject) => { 188 | this.graphQuery(`query Query { 189 | users(guid: "${this.user.guid}") { 190 | messages { 191 | from { 192 | guid, name 193 | }, sent, archived, id, single_to { 194 | guid, name 195 | }, all_recipients, read, body 196 | } 197 | } 198 | }`) 199 | .then(response => resolve(response.data.data.users[0].messages)) 200 | .catch(err => reject(err)); 201 | }); 202 | } 203 | get groups() { 204 | if (!this.user) throw 'Not authenticated'; 205 | 206 | return new Promise((resolve, reject) => { 207 | this.graphQuery(`query Query { 208 | users(guid: "${this.user.guid}") { 209 | participating_in { 210 | guid, sort_key, name, personal_colour 211 | } 212 | } 213 | }`) 214 | .then(response => resolve(response.data.data.users[0].participating_in)) 215 | .catch(err => reject(err)); 216 | }); 217 | } 218 | 219 | // Get events 220 | getEvents(start, end) { 221 | if (!this.user) throw 'Not authenticated'; 222 | 223 | return new Promise((resolve, reject) => { 224 | this.graphQuery(`query Query { 225 | events(start: "${start.toISOString().slice(0, -5)+'Z'}", for_guid: "${this.user.guid}", end: "${end.toISOString().slice(0, -5)+'Z'}") { 226 | end, location, start, subject, description, guild, attendees { role, principal { guid, name }} 227 | } 228 | }`) 229 | .then(response => resolve(response.data.data.events)) 230 | .catch(err => reject(err)); 231 | }); 232 | } 233 | 234 | //Import & export as json 235 | get export() { 236 | return JSON.stringify({ 237 | deviceId: this.deviceId, 238 | secret: this.secret, 239 | user: this.user, 240 | classes: this.classes, 241 | }); 242 | } 243 | import(json) { 244 | if (!json) throw 'Invalid JSON'; 245 | 246 | try { 247 | json = JSON.parse(json); 248 | } 249 | catch(err) { 250 | throw 'Invalid JSON'; 251 | } 252 | 253 | if (!json.deviceId) throw 'No device ID'; 254 | if (!json.secret) throw 'No secret'; 255 | if (!json.user) throw 'No user' 256 | if (!json.classes) throw 'No classes'; 257 | 258 | this.deviceId = json.deviceId; 259 | this.secret = json.secret; 260 | this.user = json.user; 261 | this.classes = json.classes; 262 | 263 | return true; 264 | } 265 | 266 | } 267 | 268 | module.exports = Firefly; -------------------------------------------------------------------------------- /Driver/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firefly-api", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "axios": { 8 | "version": "0.21.2", 9 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.2.tgz", 10 | "integrity": "sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg==", 11 | "requires": { 12 | "follow-redirects": "^1.14.0" 13 | } 14 | }, 15 | "dotenv": { 16 | "version": "8.2.0", 17 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", 18 | "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" 19 | }, 20 | "fast-xml-parser": { 21 | "version": "4.2.5", 22 | "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz", 23 | "integrity": "sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==", 24 | "requires": { 25 | "strnum": "^1.0.5" 26 | } 27 | }, 28 | "follow-redirects": { 29 | "version": "1.15.6", 30 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", 31 | "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" 32 | }, 33 | "strnum": { 34 | "version": "1.0.5", 35 | "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", 36 | "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" 37 | }, 38 | "uuid": { 39 | "version": "8.3.0", 40 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", 41 | "integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Driver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firefly-api", 3 | "version": "1.0.0", 4 | "description": "A Node.JS API driver for the Firefly Schools Virtual Learning Environment", 5 | "main": "firefly-api.js", 6 | "scripts": { 7 | "test": "node test.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/JoshHeng/FireflyAPI.git" 12 | }, 13 | "keywords": [ 14 | "firefly", 15 | "vle", 16 | "schools", 17 | "api", 18 | "driver", 19 | "fireflycloud" 20 | ], 21 | "author": "Josh Heng", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/JoshHeng/FireflyAPI/issues" 25 | }, 26 | "homepage": "https://github.com/JoshHeng/FireflyAPI#readme", 27 | "dependencies": { 28 | "axios": "^0.21.2", 29 | "dotenv": "^8.2.0", 30 | "fast-xml-parser": "^4.2.5", 31 | "uuid": "^8.3.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Driver/test.js: -------------------------------------------------------------------------------- 1 | const Firefly = require('./firefly-api.js'); 2 | require('dotenv').config(); 3 | 4 | async function test() { 5 | // Fetch school from code 6 | //console.log(await Firefly.getHost(process.env.CODE)); 7 | 8 | // Create instance 9 | const instance = new Firefly(process.env.HOST); 10 | instance.setDeviceId(process.env.DEVICE_ID); 11 | 12 | // API version 13 | //console.log(await instance.apiVersion); 14 | 15 | // Authentication URL 16 | //console.log(instance.authUrl); 17 | //instance.authenticate(); 18 | instance.completeAuthentication(process.env.XML); 19 | 20 | //console.log(await instance.verifyCredentials()); 21 | 22 | // Events/timetable 23 | //let finalDate = new Date(); 24 | //finalDate.setFullYear(2021); 25 | //console.log(await instance.getEvents(new Date(), finalDate)); 26 | 27 | // Messages 28 | //console.log(await instance.messages); 29 | 30 | // Bookmarks 31 | //console.log(await instance.bookmarks); 32 | 33 | // Groups 34 | //console.log(await instance.groups); 35 | } 36 | 37 | test(); -------------------------------------------------------------------------------- /Examples/auth.js: -------------------------------------------------------------------------------- 1 | // Require modules 2 | const request = require("request"); 3 | const parseString = require("xml2js").parseString; 4 | const environment = require("./environment.json"); 5 | 6 | // Verify the token 7 | function VerifyToken() { 8 | request({ 9 | method: 'GET', 10 | uri: environment.host + '/Login/api/verifytoken', 11 | qs: { 12 | ffauth_device_id: environment.deviceID, 13 | ffauth_secret: environment.secret 14 | }, 15 | headers: { 16 | 'User-Agent': 'test' 17 | } 18 | }, function (error, response, body) { 19 | result = JSON.parse(body); 20 | console.log("\nToken Validity:") 21 | if (result.valid) { 22 | console.log("Valid Token"); 23 | } 24 | else { 25 | console.log("Invalid Token"); 26 | } 27 | }); 28 | } 29 | VerifyToken(); 30 | 31 | function DeleteToken(app_id) { 32 | request({ 33 | method: 'GET', 34 | uri: environment.host + '/login/api/deletetoken', 35 | qs: { 36 | ffauth_device_id: environment.deviceID, 37 | ffauth_secret: environment.secret, 38 | app_id: app_id 39 | }, 40 | headers: { 41 | 'User-Agent': 'test' 42 | } 43 | }, function (error, response, body) { 44 | console.log("\nToken Deletion:") 45 | if (body == 'OK') { 46 | console.log("Success"); 47 | } 48 | else { 49 | console.log("Failed"); 50 | } 51 | }); 52 | } 53 | //DeleteToken('app_id'); 54 | 55 | -------------------------------------------------------------------------------- /Examples/environment.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "schoolCode": "SCHOOLCODE", 3 | "host": "https://host.fireflycloud.net", 4 | 5 | "deviceID": "DEVICE-ID-001", 6 | "secret": "rAnDoM-sEcReT-0123456-789" 7 | } -------------------------------------------------------------------------------- /Examples/misc.js: -------------------------------------------------------------------------------- 1 | // Require modules 2 | const request = require("request"); 3 | const parseString = require("xml2js").parseString; 4 | const environment = require("./environment.json"); 5 | 6 | // Get a school instance 7 | function GetSchoolInstance() { 8 | request.get("https://appgateway.fireflysolutions.co.uk/appgateway/school/" + environment.schoolCode, function (error, response, body) { 9 | parseString(body, function (error, result) { 10 | console.log("\nSchool Instance"); 11 | console.log(`Exists - ${result.response.$.exists}`); 12 | console.log(`Enabled - ${result.response.$.enabled}`); 13 | console.log(`School Name - ${result.response.name}`); 14 | console.log(`SSL - ${result.response.address[0].$.ssl}`); 15 | console.log(`Host - ${result.response.address[0]._}`); 16 | console.log(`Installation ID - ${result.response.installationId}`); 17 | }) 18 | }) 19 | } 20 | GetSchoolInstance(); 21 | 22 | // Get the API version 23 | function GetAPIVersion() { 24 | request.get(environment.host + "/login/api/version", function (error, response, body) { 25 | parseString(body, function (error, result) { 26 | console.log("\nFirefly API Version") 27 | console.log(`Firefly Version 6 - ${result.version.$.firefly6}`); 28 | console.log(`Version - ${result.version.majorVersion}.${result.version.minorVersion}.${result.version.incrementVersion}`); 29 | }); 30 | }); 31 | } 32 | GetAPIVersion(); -------------------------------------------------------------------------------- /Examples/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firefly-api", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "ajv": { 8 | "version": "6.12.6", 9 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 10 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 11 | "requires": { 12 | "fast-deep-equal": "^3.1.1", 13 | "fast-json-stable-stringify": "^2.0.0", 14 | "json-schema-traverse": "^0.4.1", 15 | "uri-js": "^4.2.2" 16 | }, 17 | "dependencies": { 18 | "fast-deep-equal": { 19 | "version": "3.1.3", 20 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 21 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" 22 | } 23 | } 24 | }, 25 | "asn1": { 26 | "version": "0.2.4", 27 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 28 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 29 | "requires": { 30 | "safer-buffer": "~2.1.0" 31 | } 32 | }, 33 | "assert-plus": { 34 | "version": "1.0.0", 35 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 36 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 37 | }, 38 | "asynckit": { 39 | "version": "0.4.0", 40 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 41 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 42 | }, 43 | "aws-sign2": { 44 | "version": "0.7.0", 45 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 46 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 47 | }, 48 | "aws4": { 49 | "version": "1.8.0", 50 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", 51 | "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" 52 | }, 53 | "bcrypt-pbkdf": { 54 | "version": "1.0.2", 55 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 56 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 57 | "requires": { 58 | "tweetnacl": "^0.14.3" 59 | } 60 | }, 61 | "caseless": { 62 | "version": "0.12.0", 63 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 64 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 65 | }, 66 | "combined-stream": { 67 | "version": "1.0.8", 68 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 69 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 70 | "requires": { 71 | "delayed-stream": "~1.0.0" 72 | } 73 | }, 74 | "core-util-is": { 75 | "version": "1.0.2", 76 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 77 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 78 | }, 79 | "dashdash": { 80 | "version": "1.14.1", 81 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 82 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 83 | "requires": { 84 | "assert-plus": "^1.0.0" 85 | } 86 | }, 87 | "delayed-stream": { 88 | "version": "1.0.0", 89 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 90 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 91 | }, 92 | "ecc-jsbn": { 93 | "version": "0.1.2", 94 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 95 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 96 | "requires": { 97 | "jsbn": "~0.1.0", 98 | "safer-buffer": "^2.1.0" 99 | } 100 | }, 101 | "extend": { 102 | "version": "3.0.2", 103 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 104 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 105 | }, 106 | "extsprintf": { 107 | "version": "1.3.0", 108 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 109 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 110 | }, 111 | "fast-json-stable-stringify": { 112 | "version": "2.0.0", 113 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 114 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" 115 | }, 116 | "forever-agent": { 117 | "version": "0.6.1", 118 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 119 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 120 | }, 121 | "form-data": { 122 | "version": "2.3.3", 123 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 124 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 125 | "requires": { 126 | "asynckit": "^0.4.0", 127 | "combined-stream": "^1.0.6", 128 | "mime-types": "^2.1.12" 129 | } 130 | }, 131 | "getpass": { 132 | "version": "0.1.7", 133 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 134 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 135 | "requires": { 136 | "assert-plus": "^1.0.0" 137 | } 138 | }, 139 | "har-schema": { 140 | "version": "2.0.0", 141 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 142 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 143 | }, 144 | "har-validator": { 145 | "version": "5.1.3", 146 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", 147 | "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", 148 | "requires": { 149 | "ajv": "^6.5.5", 150 | "har-schema": "^2.0.0" 151 | } 152 | }, 153 | "http-signature": { 154 | "version": "1.2.0", 155 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 156 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 157 | "requires": { 158 | "assert-plus": "^1.0.0", 159 | "jsprim": "^1.2.2", 160 | "sshpk": "^1.7.0" 161 | } 162 | }, 163 | "is-typedarray": { 164 | "version": "1.0.0", 165 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 166 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 167 | }, 168 | "isstream": { 169 | "version": "0.1.2", 170 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 171 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 172 | }, 173 | "jsbn": { 174 | "version": "0.1.1", 175 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 176 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" 177 | }, 178 | "json-schema-traverse": { 179 | "version": "0.4.1", 180 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 181 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 182 | }, 183 | "json-stringify-safe": { 184 | "version": "5.0.1", 185 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 186 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 187 | }, 188 | "jsprim": { 189 | "version": "1.4.2", 190 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", 191 | "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", 192 | "requires": { 193 | "assert-plus": "1.0.0", 194 | "extsprintf": "1.3.0", 195 | "json-schema": "0.4.0", 196 | "verror": "1.10.0" 197 | }, 198 | "dependencies": { 199 | "json-schema": { 200 | "version": "0.4.0", 201 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", 202 | "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" 203 | } 204 | } 205 | }, 206 | "mime-db": { 207 | "version": "1.40.0", 208 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", 209 | "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" 210 | }, 211 | "mime-types": { 212 | "version": "2.1.24", 213 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", 214 | "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", 215 | "requires": { 216 | "mime-db": "1.40.0" 217 | } 218 | }, 219 | "oauth-sign": { 220 | "version": "0.9.0", 221 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 222 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" 223 | }, 224 | "performance-now": { 225 | "version": "2.1.0", 226 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 227 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 228 | }, 229 | "psl": { 230 | "version": "1.4.0", 231 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.4.0.tgz", 232 | "integrity": "sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw==" 233 | }, 234 | "punycode": { 235 | "version": "2.1.1", 236 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 237 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 238 | }, 239 | "qs": { 240 | "version": "6.5.3", 241 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", 242 | "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" 243 | }, 244 | "request": { 245 | "version": "2.88.0", 246 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", 247 | "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", 248 | "requires": { 249 | "aws-sign2": "~0.7.0", 250 | "aws4": "^1.8.0", 251 | "caseless": "~0.12.0", 252 | "combined-stream": "~1.0.6", 253 | "extend": "~3.0.2", 254 | "forever-agent": "~0.6.1", 255 | "form-data": "~2.3.2", 256 | "har-validator": "~5.1.0", 257 | "http-signature": "~1.2.0", 258 | "is-typedarray": "~1.0.0", 259 | "isstream": "~0.1.2", 260 | "json-stringify-safe": "~5.0.1", 261 | "mime-types": "~2.1.19", 262 | "oauth-sign": "~0.9.0", 263 | "performance-now": "^2.1.0", 264 | "qs": "~6.5.2", 265 | "safe-buffer": "^5.1.2", 266 | "tough-cookie": "~2.4.3", 267 | "tunnel-agent": "^0.6.0", 268 | "uuid": "^3.3.2" 269 | } 270 | }, 271 | "safe-buffer": { 272 | "version": "5.2.0", 273 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", 274 | "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" 275 | }, 276 | "safer-buffer": { 277 | "version": "2.1.2", 278 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 279 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 280 | }, 281 | "sax": { 282 | "version": "1.2.4", 283 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", 284 | "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" 285 | }, 286 | "sshpk": { 287 | "version": "1.16.1", 288 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", 289 | "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", 290 | "requires": { 291 | "asn1": "~0.2.3", 292 | "assert-plus": "^1.0.0", 293 | "bcrypt-pbkdf": "^1.0.0", 294 | "dashdash": "^1.12.0", 295 | "ecc-jsbn": "~0.1.1", 296 | "getpass": "^0.1.1", 297 | "jsbn": "~0.1.0", 298 | "safer-buffer": "^2.0.2", 299 | "tweetnacl": "~0.14.0" 300 | } 301 | }, 302 | "tough-cookie": { 303 | "version": "2.4.3", 304 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", 305 | "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", 306 | "requires": { 307 | "psl": "^1.1.24", 308 | "punycode": "^1.4.1" 309 | }, 310 | "dependencies": { 311 | "punycode": { 312 | "version": "1.4.1", 313 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 314 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" 315 | } 316 | } 317 | }, 318 | "tunnel-agent": { 319 | "version": "0.6.0", 320 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 321 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 322 | "requires": { 323 | "safe-buffer": "^5.0.1" 324 | } 325 | }, 326 | "tweetnacl": { 327 | "version": "0.14.5", 328 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 329 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" 330 | }, 331 | "uri-js": { 332 | "version": "4.2.2", 333 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 334 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 335 | "requires": { 336 | "punycode": "^2.1.0" 337 | } 338 | }, 339 | "uuid": { 340 | "version": "3.3.3", 341 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", 342 | "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" 343 | }, 344 | "verror": { 345 | "version": "1.10.0", 346 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 347 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 348 | "requires": { 349 | "assert-plus": "^1.0.0", 350 | "core-util-is": "1.0.2", 351 | "extsprintf": "^1.2.0" 352 | } 353 | }, 354 | "xml2js": { 355 | "version": "0.5.0", 356 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", 357 | "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", 358 | "requires": { 359 | "sax": ">=0.6.0", 360 | "xmlbuilder": "~11.0.0" 361 | } 362 | }, 363 | "xmlbuilder": { 364 | "version": "11.0.1", 365 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", 366 | "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" 367 | } 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /Examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firefly-api", 3 | "version": "1.0.0", 4 | "description": "An unofficial API for the Firefly VLE", 5 | "main": "misc.js", 6 | "dependencies": { 7 | "request": "^2.88.0", 8 | "xml2js": "^0.5.0" 9 | }, 10 | "devDependencies": {}, 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/JoshHeng/Firefly-API.git" 17 | }, 18 | "author": "Josh Heng", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/JoshHeng/Firefly-API/issues" 22 | }, 23 | "homepage": "https://github.com/JoshHeng/Firefly-API#readme" 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to the unofficial Firefly API Documentation 2 | [Firefly](https://www.fireflylearning.com) is a virtual learning environment for schools. This repository has been made by monitoring the API requests made by the mobile app on a single pupil user account on a single school instance. 3 | 4 | This repository provides a Node.JS driver and example code in Node.JS. You will also need to install the needed modules and libraries using `npm install`. 5 | Please see the [wiki](https://github.com/JoshHeng/Firefly-API/wiki) for detailed information about the API. 6 | 7 | ## environment.json 8 | You will need to configure `environment.json` with your own environment data. You can copy `environment.example.json` to do this. 9 | * `"schoolCode": "SCHOOLCODE"` This is the code for the school which is provided by your school. It is needed to determine the host for all other requests, and is used within the mobile app to login. 10 | * `"host": "https://host.fireflycloud.net"` This is the base API endpoint which is generally the main page of the Firefly instance. This can be obtained by a school code by submitting an API request - please see the misc section for details. 11 | --------------------------------------------------------------------------------