feat: the rest of the fucking owl
This commit is contained in:
parent
7094084017
commit
bad0ebf5d7
18 changed files with 270 additions and 2 deletions
0
backend/aci/__init__.py
Normal file
0
backend/aci/__init__.py
Normal file
13
backend/aci/db.py
Normal file
13
backend/aci/db.py
Normal 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
7
backend/aci/models.py
Normal 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
7
backend/aci/utils.py
Normal 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
62
backend/app.py
Normal 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
BIN
backend/checkin.db
Normal file
Binary file not shown.
11
backend/migrate.py
Normal file
11
backend/migrate.py
Normal 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
9
backend/setup.py
Normal 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
12
docs/PRIVACY.md
Normal 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
6
docs/TOS.md
Normal 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.
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
1
extension/src/config.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
this.SERVER_URL = 'http://localhost:8000';
|
|
@ -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
18
extension/src/popup.html
Normal 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
53
extension/src/popup.js
Normal 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
14
extension/src/styles.css
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
19
flake.nix
19
flake.nix
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue