commit 3ae7b05b9b2492e8df4c255e30e2786e813cd647 Author: sneak Date: Tue Mar 10 06:44:28 2020 -0700 initial diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..947d3f5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +dist/ +db.sqlite +build/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..947d3f5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +dist/ +db.sqlite +build/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2cf867d --- /dev/null +++ b/Dockerfile @@ -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/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ece822b --- /dev/null +++ b/Makefile @@ -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" . diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..e482e03 --- /dev/null +++ b/Pipfile @@ -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" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..af6666b --- /dev/null +++ b/Pipfile.lock @@ -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": {} +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..77fdd65 --- /dev/null +++ b/README.md @@ -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)> diff --git a/bin/tvidd b/bin/tvidd new file mode 100644 index 0000000..7123ac3 --- /dev/null +++ b/bin/tvidd @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 + +from tvid import serve + +def main(): + serve() + +if __name__ == "__main__": + main() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..9f04bb2 --- /dev/null +++ b/setup.py @@ -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' + ], + }, +) + diff --git a/tvid/__init__.py b/tvid/__init__.py new file mode 100644 index 0000000..6640c20 --- /dev/null +++ b/tvid/__init__.py @@ -0,0 +1,2 @@ +from .server import serve + diff --git a/tvid/db.py b/tvid/db.py new file mode 100644 index 0000000..4b66f5d --- /dev/null +++ b/tvid/db.py @@ -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)) diff --git a/tvid/server.py b/tvid/server.py new file mode 100644 index 0000000..cad2385 --- /dev/null +++ b/tvid/server.py @@ -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/') + 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) diff --git a/views/displayid.tpl b/views/displayid.tpl new file mode 100644 index 0000000..8d3f926 --- /dev/null +++ b/views/displayid.tpl @@ -0,0 +1,20 @@ + + + tv info page + + + + +
+

+ Display ID: +

+

{{id}}

+ + (They're only letters. I like India, O like + Oscar.) + +

Powered by tvid v{{version}}

+
+ + diff --git a/views/nocookie.tpl b/views/nocookie.tpl new file mode 100644 index 0000000..a6354c8 --- /dev/null +++ b/views/nocookie.tpl @@ -0,0 +1,10 @@ + + + tv info page + + + + +

Please enable cookies, they're required.

+ + diff --git a/views/style.tpl b/views/style.tpl new file mode 100644 index 0000000..ef0985b --- /dev/null +++ b/views/style.tpl @@ -0,0 +1,10 @@ +body { + font-family: sans-serif; + width: 100%; + text-align: center; + font-size: 20pt +} + +h1 { + font-size: 48pt; +}