This commit is contained in:
Jeffrey Paul 2020-03-10 06:44:28 -07:00
commit 3ae7b05b9b
15 changed files with 391 additions and 0 deletions

3
.dockerignore Normal file
View File

@ -0,0 +1,3 @@
dist/
db.sqlite
build/

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
dist/
db.sqlite
build/

75
Dockerfile Normal file
View File

@ -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/

15
Makefile Normal file
View File

@ -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" .

15
Pipfile Normal file
View File

@ -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"

50
Pipfile.lock generated Normal file
View File

@ -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": {}
}

18
README.md Normal file
View File

@ -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)>

9
bin/tvidd Normal file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env python3
from tvid import serve
def main():
serve()
if __name__ == "__main__":
main()

15
setup.py Normal file
View File

@ -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'
],
},
)

2
tvid/__init__.py Normal file
View File

@ -0,0 +1,2 @@
from .server import serve

16
tvid/db.py Normal file
View File

@ -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))

130
tvid/server.py Normal file
View File

@ -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)

20
views/displayid.tpl Normal file
View File

@ -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>

10
views/nocookie.tpl Normal file
View File

@ -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>

10
views/style.tpl Normal file
View File

@ -0,0 +1,10 @@
body {
font-family: sans-serif;
width: 100%;
text-align: center;
font-size: 20pt
}
h1 {
font-size: 48pt;
}