feat: the rest of the fucking owl

This commit is contained in:
Ashhhleyyy 2024-02-17 21:47:42 +00:00
parent 7094084017
commit bad0ebf5d7
Signed by: ash
GPG key ID: 83B789081A0878FB
18 changed files with 270 additions and 2 deletions

0
backend/aci/__init__.py Normal file
View file

13
backend/aci/db.py Normal file
View file

@ -0,0 +1,13 @@
#!/usr/bin/env python3
import sqlite3
def get_db():
conn = sqlite3.connect('./checkin.db')
db = conn.cursor()
try:
yield db
finally:
conn.commit()
db.close()
conn.close()

7
backend/aci/models.py Normal file
View file

@ -0,0 +1,7 @@
#!/usr/bin/env python3
from pydantic import BaseModel, Field
class CodeSubmission(BaseModel):
code: str = Field(pattern=r'[0-9]{6}')
success: bool

7
backend/aci/utils.py Normal file
View file

@ -0,0 +1,7 @@
#!/usr/bin/env python3
from datetime import datetime
def today() -> str:
now = datetime.now()
#return now.strftime('%A') + ' ' + str(now.day) + ' ' + now.strftime('%B')
return 'Friday 16 February'

62
backend/app.py Normal file
View file

@ -0,0 +1,62 @@
#!/usr/bin/env python3
from typing import Union
import hashlib
import sqlite3
from fastapi import Depends, FastAPI
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
from aci.db import get_db
from aci.models import CodeSubmission
from aci import utils
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origin_regex=r"moz-extension://.*",
allow_methods=["GET"],
allow_headers="*",
)
@app.post('/api/codes')
def submit_code(date: str, time: str, activity: str, space: str, submission: CodeSubmission, db: sqlite3.Cursor=Depends(get_db)):
# TODO: validate parameters meet expected form.
db.execute('INSERT INTO code_results (date, time, space, activity, code, result) VALUES (?,?,?,?,?,?)', (date, time, space, activity, submission.code, submission.success))
return {'ok': True}
@app.get('/api/codes')
def current_codes(db: sqlite3.Cursor=Depends(get_db)):
result = db.execute('SELECT date, time, space, activity, code, result FROM code_results WHERE date = ?', (utils.today(),))
codes = result.fetchall()
code_scores = {}
code_info = {}
for date, time, space, activity, code, result in codes:
k = str((date, time, space, activity))
code_info[k] = (date, time, space, activity)
if k not in code_scores:
code_scores[k] = {}
if code not in code_scores[k]:
code_scores[k][code] = 0
code_scores[k][code] += 1 if result else -1
ret = []
for k, v in code_scores.items():
date, time, space, activity = code_info[k]
codes = list(map(lambda c: {'code': c[0], 'score': c[1]}, v.items()))
codes.sort(key=lambda c: -c['score'])
ret.append({
'date': date,
'time': time,
'space': space,
'activity': activity,
'codes': codes,
})
return {
'activities': ret * 30,
}
if __name__ == '__main__':
uvicorn.run(app)

BIN
backend/checkin.db Normal file

Binary file not shown.

11
backend/migrate.py Normal file
View file

@ -0,0 +1,11 @@
#!/usr/bin/env python3
import sqlite3
conn = sqlite3.connect('./checkin.db')
db = conn.cursor()
try:
db.execute("CREATE TABLE IF NOT EXISTS code_results (date text, time text, space text, activity text, code text, result boolean)")
finally:
db.close()
conn.close()

9
backend/setup.py Normal file
View file

@ -0,0 +1,9 @@
#!/usr/bin/env python3
from setuptools import setup, find_packages
setup(name='aci',
version='1.0.0',
packages=find_packages(),
scripts=['app.py', 'migrate.py'],
)

12
docs/PRIVACY.md Normal file
View file

@ -0,0 +1,12 @@
# Privacy policy
Here is a list of all the components of this project and the data they collect:
- Backend - logs requests, including the IP address of the reverse proxy. Also stores codes which are submitted to the service.
- NGINX - logs all requests, including IP client addresses for monitoring and rate limiting.
- Browser extension - collects data from requests made on the checkin domain. the data is anonymous (just the code and details about the session it corresponds to) and is only ever sent to the backend.
## Additional details
After 7 days, checkin session information is anonymised by replacing the details with randomised IDs, such that the data about code submissions can still be correlated, but not including any details such as textual descriptions of the activity.

6
docs/TOS.md Normal file
View file

@ -0,0 +1,6 @@
# Terms of Service
Don't cause problems.
Seriously; I don't want to write a TOS, don't be the reason I have to.

View file

@ -19,5 +19,8 @@
"scripts": ["src/background.js"], "scripts": ["src/background.js"],
"persistent": false, "persistent": false,
"type": "module" "type": "module"
},
"browser_action": {
"default_popup": "src/popup.html"
} }
} }

View file

@ -2,17 +2,19 @@ console.log('i am a background script');
const datastore = new Map(); const datastore = new Map();
function attachActivityDetails(activityId, date, time, name) { function attachActivityDetails(activityId, date, time, name, space) {
if (datastore.has(activityId)) { if (datastore.has(activityId)) {
const activity = datastore.get(activityId); const activity = datastore.get(activityId);
activity.name = name; activity.name = name;
activity.date = date; activity.date = date;
activity.time = time; activity.time = time;
activity.space = space;
} else { } else {
datastore.set(activityId, { datastore.set(activityId, {
date, date,
time, time,
name, name,
space,
code: null, code: null,
}); });
} }
@ -31,7 +33,7 @@ function interceptSubmit(details) {
return; return;
} }
const activity = datastore.get(activityId); const activity = datastore.get(activityId);
if (!activity.date || !activity.time || !activity.name || !activity.code) { if (!activity.date || !activity.time || !activity.name || !activity.space || !activity.code) {
console.error('we do not have all the information about the activity', activityId); console.error('we do not have all the information about the activity', activityId);
return; return;
} }
@ -63,14 +65,26 @@ function interceptCode(details) {
date: null, date: null,
time: null, time: null,
name: null, name: null,
space: null,
code, code,
}); });
} }
} }
function handleMessage(message) {
if (message['type'] === 'link-activity-details') {
const { activityId, date, time, name, space } = message;
attachActivityDetails(activityId, date, time, name, space);
} else {
console.warn('recieved unknown message:', message);
}
}
browser.webRequest.onBeforeRequest.addListener(interceptCode, { browser.webRequest.onBeforeRequest.addListener(interceptCode, {
urls: ['https://checkin.york.ac.uk/api/selfregistration/*/present'], urls: ['https://checkin.york.ac.uk/api/selfregistration/*/present'],
}, ['requestBody']); }, ['requestBody']);
browser.webRequest.onCompleted.addListener(interceptSubmit, { browser.webRequest.onCompleted.addListener(interceptSubmit, {
urls: ['https://checkin.york.ac.uk/api/selfregistration/*/present'], urls: ['https://checkin.york.ac.uk/api/selfregistration/*/present'],
}); });
browser.runtime.onMessage.addListener()

1
extension/src/config.js Normal file
View file

@ -0,0 +1 @@
this.SERVER_URL = 'http://localhost:8000';

View file

@ -1 +1,20 @@
console.log('hello'); console.log('hello');
(async function() {
const sections = document.querySelectorAll('section[data-activities-id]')
for (const section of sections) {
const activityId = section.dataset.activitiesId;
const date = section.querySelector('span.pull-right').innerText;
const time = section.querySelector('.row:nth-child(1) > .col-md-4').innerText;
const name = section.querySelector('.row:nth-child(2) > .col-md-4').innerText;
const space = section.querySelector('.row:nth-child(4) > .col-md-4').innerText;
browser.runtime.sendMessage({
type: 'link-activity-details',
activityId,
date,
time,
name,
space,
});
}
})();

18
extension/src/popup.html Normal file
View file

@ -0,0 +1,18 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Checkin Codes</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<script src="config.js"></script>
<script src="popup.js" defer></script>
</head>
<body>
<main id="root">
<h1>Available codes</h1>
</main>
</body>
</html>

53
extension/src/popup.js Normal file
View file

@ -0,0 +1,53 @@
(async function() {
const root = document.querySelector('#root');
function codeTable(codes) {
const table = document.createElement('table');
table.innerHTML = '<thead><tr><td>Code</td><td>Score</td></tr></thead>';
const body = document.createElement('tbody');
for (const { code, score } of codes) {
const row = document.createElement('tr');
const codeEle = document.createElement('td');
codeEle.innerText = code;
row.appendChild(codeEle);
const scoreEle = document.createElement('td');
scoreEle.innerText = score.toString();
row.appendChild(scoreEle);
body.appendChild(row);
}
table.appendChild(body);
return table;
}
function dataRow(label, value) {
const ele = document.createElement('p');
ele.innerText = value;
const labelEle = document.createElement('b');
labelEle.innerText = label + ': ';
ele.insertBefore(labelEle, ele.firstChild);
return ele;
}
function activityEle(activity) {
const root = document.createElement('div');
const title = document.createElement('h2');
title.innerText = activity.activity;
root.appendChild(title);
const details = document.createElement('p');
details.innerHTML += dataRow('Time', activity.time).innerHTML;
details.innerHTML += '<br>';
details.innerHTML += dataRow('Location', activity.space).innerHTML;
root.appendChild(details);
root.appendChild(codeTable(activity.codes));
return root;
}
const res = await fetch(SERVER_URL + '/api/codes');
if (!res.ok) {
// TODO: Handle error
return;
}
const { activities } = await res.json();
activities.sort(activity => activity.time);
for (const activity of activities) {
root.appendChild(activityEle(activity));
}
})();

14
extension/src/styles.css Normal file
View file

@ -0,0 +1,14 @@
html, body {
margin: 0;
padding: 0;
font-family: sans-serif;
font-size: 16px;
}
main {
padding: 8px;
}
h1 {
margin: 0;
}

View file

@ -19,8 +19,27 @@
uvicorn uvicorn
python-lsp-server python-lsp-server
]; ];
version = builtins.substring 0 8 self.lastModifiedDate;
in in
{ {
packages = {
default = pkgs.python311Packages.buildPythonApplication {
pname = "aci-backend";
inherit version;
propagatedBuildInputs = dependencies pkgs.python311Packages;
src = ./backend;
};
docker = pkgs.dockerTools.buildLayeredImage {
name = self.packages.${system}.default.pname;
tag = version;
contents = [ self.packages.${system}.default pkgs.busybox ];
config = {
Cmd = [ "/bin/app.py" ];
WorkingDir = "/state";
};
};
};
devShells.default = pkgs.mkShell { devShells.default = pkgs.mkShell {
nativeBuildInputs = with pkgs; [ nativeBuildInputs = with pkgs; [
(python311.withPackages devDependencies) (python311.withPackages devDependencies)