commit
3ae7b05b9b
@ -0,0 +1,3 @@ |
|||||||
|
dist/ |
||||||
|
db.sqlite |
||||||
|
build/ |
@ -0,0 +1,3 @@ |
|||||||
|
dist/ |
||||||
|
db.sqlite |
||||||
|
build/ |
@ -0,0 +1,75 @@ |
|||||||
|
# focal amd64 as of 2020-03-10 |
||||||
|
FROM ubuntu@sha256:d050ed7278c16ff627e4a70d7d353f1a2ec74d8a0b66e5a865356d92f5f6d87b |
||||||
|
|
||||||
|
#######################################################################33 |
||||||
|
## Mirror Setup |
||||||
|
## - option to use local mirror to speed build |
||||||
|
#######################################################################33 |
||||||
|
ARG UBUNTU_MIRROR=http://archive.ubuntu.com/ubuntu |
||||||
|
RUN echo "deb $UBUNTU_MIRROR focal main universe restricted multiverse" > /etc/apt/sources.list.new && \ |
||||||
|
echo "deb $UBUNTU_MIRROR focal-updates main universe restricted multiverse" >> /etc/apt/sources.list.new && \ |
||||||
|
echo "deb $UBUNTU_MIRROR focal-security main universe restricted multiverse" >> /etc/apt/sources.list.new && \ |
||||||
|
echo "deb $UBUNTU_MIRROR focal-backports main universe restricted multiverse" >> /etc/apt/sources.list.new && \ |
||||||
|
mv /etc/apt/sources.list.new /etc/apt/sources.list |
||||||
|
|
||||||
|
#######################################################################33 |
||||||
|
## Versions |
||||||
|
#######################################################################33 |
||||||
|
# master as of 2020-03-10 |
||||||
|
ARG PYENV_COMMIT=df9fa1dc30b6448ef8605e2c2d4dfc2a94d6a35d |
||||||
|
ARG PYTHON_VERSION=3.8.1 |
||||||
|
|
||||||
|
#######################################################################33 |
||||||
|
## Packages |
||||||
|
#######################################################################33 |
||||||
|
ARG DEBIAN_FRONTEND=noninteractive |
||||||
|
RUN apt update && \ |
||||||
|
apt upgrade -y && \ |
||||||
|
apt install -y \ |
||||||
|
build-essential \ |
||||||
|
curl \ |
||||||
|
git \ |
||||||
|
libbz2-dev \ |
||||||
|
libffi-dev \ |
||||||
|
liblzma-dev \ |
||||||
|
libncurses5-dev \ |
||||||
|
libncursesw5-dev \ |
||||||
|
libreadline-dev \ |
||||||
|
libsqlite3-dev \ |
||||||
|
libssl-dev \ |
||||||
|
llvm \ |
||||||
|
locales \ |
||||||
|
make \ |
||||||
|
python-openssl \ |
||||||
|
tk-dev \ |
||||||
|
wget \ |
||||||
|
xz-utils \ |
||||||
|
zlib1g-dev \ |
||||||
|
&& \ |
||||||
|
mkdir -p /var/app && \ |
||||||
|
git clone https://github.com/pyenv/pyenv.git /usr/local/pyenv && \ |
||||||
|
cd /usr/local/pyenv && \ |
||||||
|
git checkout $PYENV_COMMIT |
||||||
|
|
||||||
|
ENV PYENV_ROOT /usr/local/pyenv |
||||||
|
ENV PATH $PYENV_ROOT/bin:$PATH |
||||||
|
|
||||||
|
|
||||||
|
#######################################################################33 |
||||||
|
## Python |
||||||
|
#######################################################################33 |
||||||
|
RUN pyenv install $PYTHON_VERSION && \ |
||||||
|
pyenv global $PYTHON_VERISON && \ |
||||||
|
pip3 install pipenv |
||||||
|
|
||||||
|
#######################################################################33 |
||||||
|
## Install Deps |
||||||
|
#######################################################################33 |
||||||
|
ADD ./Pipfile ./Pipfile.lock /var/app/ |
||||||
|
WORKDIR /var/app |
||||||
|
RUN pipenv install --python $(which python3) |
||||||
|
|
||||||
|
#######################################################################33 |
||||||
|
## Install App |
||||||
|
#######################################################################33 |
||||||
|
ADD ./* /var/app/ |
@ -0,0 +1,15 @@ |
|||||||
|
export PYTHONPATH =: $(PWD)
|
||||||
|
|
||||||
|
default: docker |
||||||
|
|
||||||
|
peinstall: |
||||||
|
pipenv install --python $(shell which python3)
|
||||||
|
|
||||||
|
serve: |
||||||
|
pipenv run python ./bin/tvidd
|
||||||
|
|
||||||
|
clean: |
||||||
|
rm -rf build dist tvid.egg-info
|
||||||
|
|
||||||
|
docker: |
||||||
|
docker build --build-arg UBUNTU_MIRROR="http://ubuntu.datavi.be/ubuntu" .
|
@ -0,0 +1,15 @@ |
|||||||
|
[[source]] |
||||||
|
name = "pypi" |
||||||
|
url = "https://pypi.org/simple" |
||||||
|
verify_ssl = true |
||||||
|
|
||||||
|
[dev-packages] |
||||||
|
|
||||||
|
[packages] |
||||||
|
bottle = "*" |
||||||
|
pprint = "*" |
||||||
|
sqlalchemy = "*" |
||||||
|
bottle-sqlalchemy = "*" |
||||||
|
|
||||||
|
[requires] |
||||||
|
python_version = "3.8" |
@ -0,0 +1,50 @@ |
|||||||
|
{ |
||||||
|
"_meta": { |
||||||
|
"hash": { |
||||||
|
"sha256": "7216cb0c490cfe6852be9bc21eeac5f3f08cf65b39f7316fae3580a387938016" |
||||||
|
}, |
||||||
|
"pipfile-spec": 6, |
||||||
|
"requires": { |
||||||
|
"python_version": "3.8" |
||||||
|
}, |
||||||
|
"sources": [ |
||||||
|
{ |
||||||
|
"name": "pypi", |
||||||
|
"url": "https://pypi.org/simple", |
||||||
|
"verify_ssl": true |
||||||
|
} |
||||||
|
] |
||||||
|
}, |
||||||
|
"default": { |
||||||
|
"bottle": { |
||||||
|
"hashes": [ |
||||||
|
"sha256:0819b74b145a7def225c0e83b16a4d5711fde751cd92bae467a69efce720f69e", |
||||||
|
"sha256:43157254e88f32c6be16f8d9eb1f1d1472396a4e174ebd2bf62544854ecf37e7" |
||||||
|
], |
||||||
|
"index": "pypi", |
||||||
|
"version": "==0.12.18" |
||||||
|
}, |
||||||
|
"bottle-sqlalchemy": { |
||||||
|
"hashes": [ |
||||||
|
"sha256:ba6127f3aff2b78649781adbbee65518233dc481e9f9e32e3b050d1ad9551c17" |
||||||
|
], |
||||||
|
"index": "pypi", |
||||||
|
"version": "==0.4.3" |
||||||
|
}, |
||||||
|
"pprint": { |
||||||
|
"hashes": [ |
||||||
|
"sha256:c0fa22d1462351671ca098e9779bb26a23880011e93eea5f199a150ee7b92a16" |
||||||
|
], |
||||||
|
"index": "pypi", |
||||||
|
"version": "==0.1" |
||||||
|
}, |
||||||
|
"sqlalchemy": { |
||||||
|
"hashes": [ |
||||||
|
"sha256:b92d2de62e43499d85b1780274d1b562e5159c7996f6f04a9bb46cf681ced45f" |
||||||
|
], |
||||||
|
"index": "pypi", |
||||||
|
"version": "==1.3.14" |
||||||
|
} |
||||||
|
}, |
||||||
|
"develop": {} |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
# tvid |
||||||
|
|
||||||
|
This is an app that lets you set all the kiosk/display TVs in your |
||||||
|
organization to the same URL. Each will be cookied with a unique ID that |
||||||
|
will display on each display in big letters, with no preconfiguration |
||||||
|
required. |
||||||
|
|
||||||
|
Log in to the admin panel and enter the URL for that TV ID, and within 60 |
||||||
|
seconds, that display will bounce to that URL, or any other time it turns |
||||||
|
on. |
||||||
|
|
||||||
|
# todo |
||||||
|
|
||||||
|
* make display id animate: https://codepen.io/stezu/pen/cmLrI |
||||||
|
|
||||||
|
# author |
||||||
|
|
||||||
|
sneak <[sneak@sneak.berlin](mailto:sneak@sneak.berlin)> |
@ -0,0 +1,9 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
|
||||||
|
from tvid import serve |
||||||
|
|
||||||
|
def main(): |
||||||
|
serve() |
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
main() |
@ -0,0 +1,15 @@ |
|||||||
|
from setuptools import setup, find_packages |
||||||
|
|
||||||
|
setup( |
||||||
|
name='tvid', |
||||||
|
version='0.0.1', |
||||||
|
packages=find_packages(), |
||||||
|
license='WTFPL', |
||||||
|
long_description=open('README.md').read(), |
||||||
|
entry_points = { |
||||||
|
'console_scripts': [ |
||||||
|
'tvidd = tvid.tvid:serve' |
||||||
|
], |
||||||
|
}, |
||||||
|
) |
||||||
|
|
@ -0,0 +1,2 @@ |
|||||||
|
from .server import serve |
||||||
|
|
@ -0,0 +1,16 @@ |
|||||||
|
from sqlalchemy import Column, Sequence, Integer, String, DateTime |
||||||
|
from sqlalchemy.ext.declarative import declarative_base |
||||||
|
|
||||||
|
from .server import SQLBASE |
||||||
|
|
||||||
|
Base = SQLBASE |
||||||
|
# YAGNI just set admin pw in an env var for now and dont support changing it |
||||||
|
# yet |
||||||
|
#class Settings(Base): |
||||||
|
|
||||||
|
class TV(Base): |
||||||
|
__tablename__ = 'tvs' |
||||||
|
id = Column(Integer, Sequence('id_seq'), primary_key=True) |
||||||
|
displayid = Column(String(20)) |
||||||
|
lastSeen = Column(DateTime) |
||||||
|
target = Column(String(255)) |
@ -0,0 +1,130 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
|
||||||
|
# this is a quick hack, to be improved later. trying to do the simplest |
||||||
|
# thing that will possibly work. |
||||||
|
|
||||||
|
## # TODO |
||||||
|
## |
||||||
|
## * put page content into templates/static files |
||||||
|
## * clean up FIXME |
||||||
|
|
||||||
|
import bottle |
||||||
|
from bottle import route, run, request, response, redirect |
||||||
|
from bottle import HTTPError, template |
||||||
|
from sqlalchemy.ext.declarative import declarative_base |
||||||
|
from bottle.ext import sqlalchemy |
||||||
|
from pprint import pprint |
||||||
|
from sqlalchemy import create_engine |
||||||
|
import os |
||||||
|
import random |
||||||
|
import string |
||||||
|
from datetime import datetime |
||||||
|
|
||||||
|
VERSION = '0.0.1' |
||||||
|
PORT = os.environ.get('PORT', 8080) |
||||||
|
DEBUG = os.environ.get('DEBUG', False) |
||||||
|
SQLITE_FILENAME = os.environ.get('SQLITE_FILENAME','/data/db.sqlite') |
||||||
|
DATABASE_URL = os.environ.get('DATABASE_URL','sqlite:///' + SQLITE_FILENAME) |
||||||
|
ADMINPSK = os.environ.get('ADMINPSK','hunter2') |
||||||
|
|
||||||
|
# sorry for global |
||||||
|
SQLBASE = declarative_base() |
||||||
|
|
||||||
|
# FIXME make this skip letters not in base58 |
||||||
|
def randomUpper(count): |
||||||
|
return ''.join(random.choice(string.ascii_uppercase) for _ in range(count)) |
||||||
|
|
||||||
|
# FIXME maybe make the IDs longer |
||||||
|
def genRandomTVID(): |
||||||
|
tvid = str(randomUpper(3) + "-" + randomUpper(3)) |
||||||
|
return tvid |
||||||
|
|
||||||
|
def serve(): |
||||||
|
app = bottle.Bottle() |
||||||
|
|
||||||
|
# pull in models |
||||||
|
from .db import TV |
||||||
|
|
||||||
|
engine = create_engine(DATABASE_URL, echo=True) |
||||||
|
|
||||||
|
plugin = sqlalchemy.Plugin( |
||||||
|
engine, # SQLAlchemy engine created with create_engine function. |
||||||
|
SQLBASE.metadata, # SQLAlchemy metadata, required only if create=True. |
||||||
|
keyword='db', # Keyword used to inject session database in a route (default 'db'). |
||||||
|
create=True, # If it is true, execute `metadata.create_all(engine)` when plugin is applied (default False). |
||||||
|
commit=True, # If it is true, plugin commit changes after route is executed (default True). |
||||||
|
use_kwargs=False |
||||||
|
) |
||||||
|
|
||||||
|
app.install(plugin) |
||||||
|
|
||||||
|
# here we cookie them if they have no cookie, then redirect them to the |
||||||
|
# cookie'd value (whether preexisting or new). |
||||||
|
@app.get('/') |
||||||
|
def indexpage(): |
||||||
|
c = request.get_cookie("tvid") |
||||||
|
if c: |
||||||
|
# redirect |
||||||
|
redirect('/tv/' + c) |
||||||
|
else: |
||||||
|
newid = genRandomTVID() |
||||||
|
response.set_cookie("tvid", newid) |
||||||
|
redirect('/tv/' + newid) |
||||||
|
|
||||||
|
@app.get('/style.css') |
||||||
|
def stylesheet(): |
||||||
|
return template('style') |
||||||
|
|
||||||
|
# here we check to see if they have a redirect URL in the db. if they do |
||||||
|
# we send them there. if they don't, we display their ID really big, |
||||||
|
# reloading the page once per hour. |
||||||
|
@app.get('/tv/<displayid>') |
||||||
|
def tvpage(db, displayid=None): |
||||||
|
# FIXME regex check id to make sure displayid is right format, |
||||||
|
# return error if not |
||||||
|
|
||||||
|
if id is None: |
||||||
|
return template('nocookie') |
||||||
|
|
||||||
|
# check db for tv id |
||||||
|
tv = db.query(TV).filter_by(displayid=displayid).first() |
||||||
|
if tv: |
||||||
|
tv.lastSeen = datetime.now() |
||||||
|
db.add(tv) |
||||||
|
if tv.target: |
||||||
|
redirect(tv.target) |
||||||
|
else: |
||||||
|
return template('displayid', id=displayid, version=VERSION) |
||||||
|
else: |
||||||
|
# otherwise, just show their display ID bigly and keep them |
||||||
|
# bouncing periodically until some admin configures them |
||||||
|
# a target: |
||||||
|
# update lastseen here: |
||||||
|
newtv = TV(displayid=displayid,lastSeen=datetime.now()) |
||||||
|
db.add(newtv) |
||||||
|
return template('displayid', id=displayid, version=VERSION) |
||||||
|
|
||||||
|
# here we display the administration list of TVs if logged in |
||||||
|
# if logged out then redirect to /login |
||||||
|
# FIXME make this use sessions instead of just storing PSK in a cookie |
||||||
|
# https://bottlepy.org/docs/dev/recipes.html |
||||||
|
@app.get('/admin') |
||||||
|
def adminpage(): |
||||||
|
c = request.get_cookie("adminpw") |
||||||
|
|
||||||
|
# FIXME check their 'adminpw' cookie here, redirect to /loign |
||||||
|
return "Hello World!" |
||||||
|
|
||||||
|
# here we ask for a password and cookie them and bounce them back to /admin |
||||||
|
@app.get('/login') |
||||||
|
def checklogin(): |
||||||
|
raise NotImplementedError() |
||||||
|
#response.set_cookie("adminpw", whatever) |
||||||
|
redirect('/login') |
||||||
|
|
||||||
|
@app.get('/logut') |
||||||
|
def logout(): |
||||||
|
response.set_cookie("adminpw", "") |
||||||
|
redirect('/login') |
||||||
|
|
||||||
|
app.run(host='0.0.0.0', port=PORT, debug=DEBUG) |
@ -0,0 +1,20 @@ |
|||||||
|
<html> |
||||||
|
<head> |
||||||
|
<title>tv info page</title> |
||||||
|
<meta http-equiv="refresh" content="60"> |
||||||
|
<link rel="stylesheet" href="/style.css" /> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div id="main"> |
||||||
|
<p> |
||||||
|
<i>Display ID:</i> |
||||||
|
</p> |
||||||
|
<h1>{{id}}</h1> |
||||||
|
|
||||||
|
<small>(They're only letters. I like India, O like |
||||||
|
Oscar.)</small> |
||||||
|
|
||||||
|
<p><small>Powered by tvid v{{version}}</small></p> |
||||||
|
</div> |
||||||
|
</body> |
||||||
|
</html> |
@ -0,0 +1,10 @@ |
|||||||
|
<html> |
||||||
|
<head> |
||||||
|
<title>tv info page</title> |
||||||
|
<meta http-equiv="refresh" content="60; url=/"> |
||||||
|
<link rel="stylesheet" href="/style.css" /> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<h1>Please enable cookies, they're required.</h1> |
||||||
|
</body> |
||||||
|
</html> |
@ -0,0 +1,10 @@ |
|||||||
|
body { |
||||||
|
font-family: sans-serif; |
||||||
|
width: 100%; |
||||||
|
text-align: center; |
||||||
|
font-size: 20pt |
||||||
|
} |
||||||
|
|
||||||
|
h1 { |
||||||
|
font-size: 48pt; |
||||||
|
} |
Loading…
Reference in new issue