├── KhanAcademyCheckPoints.png ├── Readme.md ├── TypingClubCheckPoints.png ├── database-script1.sh ├── database-script2.sh ├── database-script3.sh ├── database-script4.sh ├── database-script5.sh ├── khanAcademy.js ├── learning.js └── typingClub.js /KhanAcademyCheckPoints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1stOctet/YouWillUnderstandWhenYouAreOlder/5a8564c20f8b1f4b352d2d1324aa7fb7733a24fd/KhanAcademyCheckPoints.png -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | Goal: After 3 hours, block 1 or 2 sites that my 13 year old uses the most. Unblock those sites once they have earned 1 point on Typing Club or Khan Academy. Requires Pi-hole DNS software. Pi-hole 5.0 is official as of May 10, 2020. 2 | 3 | With the awesome per client features of Pi-hole 5.0 it will now be possible to add the ability to periodically block sites and unlock them after points are earned on khan academy, typing club or any site that has a web scrapable point system. 4 | 5 | Create an issue if you run into any problems. Happy to help everyone get up and running. Note some familiarity with the linux command line will be required. 6 | 7 | License to learn: Code can be used however you wish with no restrictions. 8 | 9 | 10 | 11 | 12 | 13 | 14 | Setup 15 | - Create an account on typing club or khan academy as a parent. Do not use a LINKED facebook or LINKED google account to log in. 16 | 17 | - For this to work you need to create an "old school" email login. This is important because we will have a nodejs script that will log in as you to check your child's account for points. 18 | 19 | - After you create an account as a parent, create a sub account for the child. Note typing club doesn't have a parent and a child account. 20 | 21 | - Todo (Explain how to install Linux on a raspberry pi or old computer) 22 | 23 | - Todo (Explain how to install pi-hole and upgrade to pi-hole 5.0 beta) 24 | 25 | - sudo apt install nodejs npm git 26 | 27 | - npm i puppeteer 28 | 29 | - npm i get-stdin 30 | 31 | - git clone https://github.com/1stOctet/YouWillUnderstandWhenYouAreOlder.git 32 | 33 | Currently using this guide https://github.com/pi-hole/docs/blob/release/v5.0/docs/database/gravity/example.md 34 | to understand how to flip a site from blocked to not blocked via the "Raw database instructions" 35 | 36 | Look at database-script1.sh first before running. Trust but verify. 37 | - sudo bash database-script1.sh 38 | 39 | - Todo (Explain how to figure out what your child's ip address is using the pi-hole web interface) 40 | 41 | Please first modify the database-script2.sh file and change 222.222.222.222 ip to your child's ip address. 42 | - sudo bash database-script2.sh 43 | 44 | Look at database-script3.sh first before running. Trust but verify. 45 | - sudo bash database-script3.sh 46 | 47 | Please first modify the database-script3.sh to include the domains you want to block. 48 | - sudo bash database-script4.sh 49 | 50 | This script is unecessary if you use the pi-hole web interface to link the domains to a group. 51 | If it doesn't make sense to you, please use the pi-hole gui. 52 | Be sure to edit this file for the number of domains you are blocking periodically to ensure your child goes to Harvard. 53 | - sudo bash database-script5.sh 54 | 55 | -Todo (add database-script6.sh to disable the blacklisted domains so that they work until the cronjob enables them) 56 | 57 | - Run a cronjob every 4 hours that enables the blacklisted domains. After sleeping for 20 seconds, do a point check to create user-typingclub-last.txt file and user-khanacademy-last.txt files. Create a file user-YouWillUnderstandWhenYouAreOlder.txt to signal to the other cronjob that sites are being blocked for that user(s). 58 | 59 | - If user-YouWillUnderstandWhenYouAreOlder.txt exists, this means the sites are blocked. We need to frequently check khan academy and typing club to see if 1 point has been earned. 60 | 61 | - When the point checking cronjob runs every minute, it will output a user-typingclub-current.txt file and a user-khanacademy-current.txt file. Using bash it will check if those files are diffent than user-typingclub-last.txt file and user-khanacademy-last.txt file. If they are different, this means the child has earned at least 1 point. Disable the blocking. If the files are the same, delete user-typingclub-current.txt and user-khanacademy-current.txt 62 | 63 | - Todo (It is unecessary to frequently check for points if the user's ip address has not sent a DNS request in the last 15 minutes. Some education sites will not like so many logins to check points all night. Figure out the table to determine if the user has sent a DNS query in the last 15 minutes. Ignore the apple / google domains that run often at night. 64 | 65 | Notes 66 | after running node khanacademy.js take the output and use this awk to put the point value in the variable $currentpoints 67 | currentpoints=(awk -F 'points earned\\\\\\t' 'print $2}' out.txt | awk -F '\' '{print $1}' 68 | -------------------------------------------------------------------------------- /TypingClubCheckPoints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1stOctet/YouWillUnderstandWhenYouAreOlder/5a8564c20f8b1f4b352d2d1324aa7fb7733a24fd/TypingClubCheckPoints.png -------------------------------------------------------------------------------- /database-script1.sh: -------------------------------------------------------------------------------- 1 | #backup sqldb 2 | echo "backing up /etc/pihole/gravity.db to /etc/pihole/gravity.$(date +%F_%R).db" 3 | cp /etc/pihole/gravity.db /etc/pihole/gravity.$(date +%F_%R).db 4 | 5 | echo "Running 3 insert statements" 6 | sqlite3 /etc/pihole/gravity.db < { 4 | try { 5 | await engine.page.goto('https://www.khanacademy.org/login?continue=%2F'); 6 | await engine.page.waitFor(1500); 7 | 8 | //add parent username here *** 9 | await engine.page.type('input[id$="email-or-username"]', 'youremail@gmail.com'); // <--------STEP 1 10 | 11 | //add parent password here *** 12 | await engine.page.type('input[id$="password"]', 'ChangeThisToYourParentPassword'); // <---------STEP 2 13 | 14 | 15 | const [button] = await engine.page.$x("//button[contains(., 'Log in')]"); 16 | if (button) {await button.click();} 17 | await engine.page.waitFor(1500); 18 | 19 | //fix this url, the kaid_763278946 will be for your child 20 | await engine.page.goto('https://www.khanacademy.org/profile/kaid_CHANGETHISID'); // <----------STEP 3 21 | 22 | await engine.page.waitFor(8000); 23 | const body = await engine.page.evaluate(() => { 24 | return {'body': document.body.innerText}; 25 | }); 26 | console.log('body:',body); 27 | 28 | 29 | } 30 | catch (error){ 31 | console.log("caught error"); 32 | throw error; 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /learning.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer'); 2 | const getStdin = require('get-stdin'); 3 | 4 | module.exports = async (f) => { 5 | var input = "" //JSON.parse(await getStdin()); 6 | 7 | var browser = await puppeteer.launch(); 8 | var page = await browser.newPage(); 9 | 10 | var output = { 11 | id: input.id, 12 | screenshots: 0 13 | }; 14 | 15 | await page.setViewport({width: 1920, height: 1080}); 16 | 17 | var engine = { 18 | page: page, 19 | browser: browser, 20 | input: input, 21 | output: output, 22 | 23 | 24 | autoScreenshot: async () => { 25 | output.screenshots++; 26 | return page.screenshot({path: "screenshots/" + input.id + "_" + output.screenshots + ".jpg"}); 27 | }, 28 | 29 | // Provide a basic sleep method 30 | sleep: (ms) => { 31 | return new Promise(resolve => setTimeout(resolve, ms)); 32 | }, 33 | 34 | text: async (selector) => { 35 | return await page.$$eval(selector, (all) => { 36 | return all.map((e) => e.innerText).join(' ').trim(); 37 | }); 38 | }, 39 | 40 | regexp: async (selector, pattern) => { 41 | var text = await engine.text(selector); 42 | if (text.length <= 0) { return ""; } 43 | var regexp = (pattern instanceof RegExp) ? pattern : (new RegExp(pattern, "gi")); 44 | var match = regexp.exec(text); 45 | return match ? String(match[1]).trim() : ""; 46 | }, 47 | 48 | regexpList: async (selector, pattern) => { 49 | return await page.$$eval(selector, (all, pattern) => { 50 | var list = []; 51 | 52 | all.forEach((e) => { 53 | var regexp = new RegExp(pattern, 'gi'); 54 | var match = regexp.exec(e.innerText); 55 | if (match && match.length > 1) { list.push(match[1]); } 56 | }); 57 | 58 | return list; 59 | }, pattern); 60 | }, 61 | 62 | dataList: async (key) => { 63 | return await page.$$eval("[data-" + key + "]", (all, key) => { 64 | var list = []; 65 | all.forEach((e) => list.push(e.attributes['data-' + key].value)); 66 | return list; 67 | }, key); 68 | }, 69 | 70 | sum: (list) => { 71 | var sum = 0; 72 | if (!list || list.length <= 0) { return; } 73 | list.forEach((x) => { sum += Number(x); }); 74 | return sum; 75 | }, 76 | 77 | clickAndWait: async (button, goodSelector, badSelector) => { 78 | await page.click(button, {delay: 10}); 79 | await page.waitForFunction((goodSelector, badSelector) => { 80 | var exists = false; 81 | 82 | function check(el) { 83 | if (window.getComputedStyle(el).display !== 'none') { 84 | exists = exists || (String(el.innerText || "").trim().length > 0); 85 | } 86 | } 87 | 88 | document.querySelectorAll(badSelector).forEach(check); 89 | document.querySelectorAll(goodSelector).forEach(check); 90 | return exists; 91 | }, {}, goodSelector, badSelector); 92 | 93 | var error = await engine.text(badSelector); 94 | 95 | if (error.length) { 96 | engine.problem(error); 97 | } 98 | }, 99 | 100 | problem: (reason) => { 101 | engine.output.problem = reason; 102 | throw new Error("Website reported a problem"); 103 | } 104 | }; 105 | 106 | var exitcode = 0; 107 | 108 | try { 109 | output.points = await f(engine); 110 | await engine.autoScreenshot(); 111 | } catch (error) { 112 | await engine.autoScreenshot(); 113 | process.stderr.write(String(error.stack)); 114 | output.error = String(error.message || "Script error"); 115 | process.stderr.write(output.error) 116 | exitcode = 1; 117 | } 118 | 119 | await page.close(); 120 | await browser.close(); 121 | 122 | //await process.stdout.write(JSON.stringify(output)); 123 | process.exit(exitcode); 124 | }; 125 | -------------------------------------------------------------------------------- /typingClub.js: -------------------------------------------------------------------------------- 1 | // NOT TESTED FOR MONTHS. DOES THIS LOG IN OK AND GET THE POINTS? 2 | var learning = require('./learning.js'); 3 | 4 | learning(async (engine) => { 5 | try { 6 | await engine.page.goto('https://www.typingclub.com/login.html'); 7 | await engine.page.waitForSelector('.inpt.ed-input-log-in'); 8 | 9 | //add username for typing club 10 | await engine.page.type('input[id="username"]', 'PUT EMAIL USERNAME HERE'); 11 | 12 | //add password for typing club 13 | await engine.page.type('input[id="password"]', 'PUT PASSWORD HERE'); 14 | 15 | await engine.page.click('#login-with-password.btn.log-in-with'); 16 | await engine.page.waitForSelector('.lp-card-details'); 17 | await engine.page.waitFor(1500); 18 | await engine.page.goto('https://www.typingclub.com/sportal/stats.html'); 19 | await engine.page.waitForSelector('.col-md-4'); 20 | await engine.page.waitFor(1500); 21 | 22 | const innerText = await engine.page.evaluate(() => document.querySelector('.col-md-4').innerText); 23 | console.log(innerText); 24 | } 25 | catch (error){ 26 | console.log("error"); 27 | throw error; 28 | } 29 | }); 30 | --------------------------------------------------------------------------------