├── src ├── bin │ ├── migrations │ │ ├── index.js │ │ ├── migrate_null.js │ │ ├── setMsatNum.js │ │ └── setDates.js │ ├── runTotals.js │ ├── manfeed.js │ ├── dbconnect.js │ ├── calcFeedTimes.js │ ├── email.ts │ ├── feed.js │ ├── ordersByDay.js │ ├── report.js │ ├── feeder.js │ ├── unresponsiveCheck.ts │ ├── totals.js │ ├── email.js │ └── unresponsiveCheck.js ├── img │ ├── banner.png │ ├── daisy.jpg │ ├── BigBlack.jpg │ ├── Parkour.jpg │ ├── KarenLori.jpg │ ├── pollofeed.png │ └── icons │ │ ├── icon-72x72.png │ │ ├── icon-96x96.png │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ ├── icon-192x192.png │ │ ├── icon-384x384.png │ │ └── icon-512x512.png ├── invoices │ ├── models │ │ ├── PollofeedMetadata.ts │ │ ├── InvoiceResponse.js │ │ ├── PollofeedMetadata.js │ │ ├── InvoiceResponse.ts │ │ ├── PolloFeedOrder.ts │ │ ├── LightningInvoice.js │ │ ├── PolloFeedOrder.js │ │ └── LightningInvoice.ts │ ├── feedCost.js │ ├── feedCost.ts │ ├── dao.js │ └── router.js ├── style.css ├── app.js ├── www.js └── client.js ├── playbooks ├── group_vars │ └── all ├── roles │ ├── pollofeed │ │ ├── defaults │ │ │ └── main.yml │ │ ├── tasks │ │ │ ├── nginx.yml │ │ │ ├── main.yml │ │ │ └── cron.yml │ │ └── files │ │ │ ├── mb-docker-compose.yml │ │ │ └── nginx-docker-compose.yml │ └── pi │ │ ├── files │ │ ├── requirements.txt │ │ ├── motion.service │ │ ├── pin.py │ │ ├── ngrok.service │ │ ├── pollofeed.service │ │ ├── setup.py │ │ ├── pollofeed.py │ │ └── motion.conf │ │ └── tasks │ │ └── main.yml ├── hosts.example ├── deploy.yml └── pi.yml ├── .dockerignore ├── dev.sh ├── .idea ├── encodings.xml ├── watcherTasks.xml ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── misc.xml ├── vcs.xml ├── dictionaries │ └── joe.xml ├── reactTemplatesPlugin.xml ├── jsLibraryMappings.xml ├── compiler.xml ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml ├── pollofeed.iml └── workspace.xml ├── views ├── success.pug ├── header.pug ├── footer.pug ├── payment.pug ├── about.pug └── index.pug ├── deploy.sh ├── .travis.yml ├── example.env ├── healthcheck.js ├── Dockerfile ├── test ├── feedCost.test.js ├── app.test.js ├── PolloFeedInvoice.test.js └── calcFeedTimes.test.js ├── docker-compose.yml ├── README.md ├── LICENSE ├── package.json └── .gitignore /src/bin/migrations/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playbooks/group_vars/all: -------------------------------------------------------------------------------- 1 | ansible_python_interpreter: /usr/bin/python3 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | *Dockerfile* 3 | *docker-compose* 4 | node_modules 5 | dump 6 | -------------------------------------------------------------------------------- /src/img/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j-chimienti/pollofeed/HEAD/src/img/banner.png -------------------------------------------------------------------------------- /src/img/daisy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j-chimienti/pollofeed/HEAD/src/img/daisy.jpg -------------------------------------------------------------------------------- /src/img/BigBlack.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j-chimienti/pollofeed/HEAD/src/img/BigBlack.jpg -------------------------------------------------------------------------------- /src/img/Parkour.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j-chimienti/pollofeed/HEAD/src/img/Parkour.jpg -------------------------------------------------------------------------------- /src/img/KarenLori.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j-chimienti/pollofeed/HEAD/src/img/KarenLori.jpg -------------------------------------------------------------------------------- /src/img/pollofeed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j-chimienti/pollofeed/HEAD/src/img/pollofeed.png -------------------------------------------------------------------------------- /playbooks/roles/pollofeed/defaults/main.yml: -------------------------------------------------------------------------------- 1 | APP_DIR: pollofeed 2 | BIN_DIR: /home/joe/pollofeed/src/bin 3 | -------------------------------------------------------------------------------- /dev.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | nodemon -w ./src/ -w ./views/ -e js,css,pug --exec "./build.sh && npm start" 3 | -------------------------------------------------------------------------------- /playbooks/hosts.example: -------------------------------------------------------------------------------- 1 | [pi] 2 | 192.168.1.1 3 | [myhostname] 4 | 127.0.0.1 ansible_become_pass=super_secret 5 | -------------------------------------------------------------------------------- /src/img/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j-chimienti/pollofeed/HEAD/src/img/icons/icon-72x72.png -------------------------------------------------------------------------------- /src/img/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j-chimienti/pollofeed/HEAD/src/img/icons/icon-96x96.png -------------------------------------------------------------------------------- /playbooks/roles/pi/files/requirements.txt: -------------------------------------------------------------------------------- 1 | gpiozero 2 | bson 3 | setuptools 4 | wheel 5 | pyyaml 6 | pymongo==3.5.1 7 | -------------------------------------------------------------------------------- /src/img/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j-chimienti/pollofeed/HEAD/src/img/icons/icon-128x128.png -------------------------------------------------------------------------------- /src/img/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j-chimienti/pollofeed/HEAD/src/img/icons/icon-144x144.png -------------------------------------------------------------------------------- /src/img/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j-chimienti/pollofeed/HEAD/src/img/icons/icon-152x152.png -------------------------------------------------------------------------------- /src/img/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j-chimienti/pollofeed/HEAD/src/img/icons/icon-192x192.png -------------------------------------------------------------------------------- /src/img/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j-chimienti/pollofeed/HEAD/src/img/icons/icon-384x384.png -------------------------------------------------------------------------------- /src/img/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/j-chimienti/pollofeed/HEAD/src/img/icons/icon-512x512.png -------------------------------------------------------------------------------- /src/invoices/models/PollofeedMetadata.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface PollofeedMetadata { 3 | feedTimes: number 4 | } 5 | -------------------------------------------------------------------------------- /src/invoices/models/InvoiceResponse.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /src/invoices/models/PollofeedMetadata.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /playbooks/deploy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: pollofeed 3 | become: yes 4 | become_user: root 5 | gather_facts: no 6 | roles: 7 | - pollofeed 8 | 9 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/watcherTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /playbooks/pi.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: pi 3 | remote_user: pi 4 | gather_facts: yes 5 | vars: 6 | pollofeed_dir: /home/pi/Documents/pollofeed 7 | roles: 8 | - pi 9 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /views/success.pug: -------------------------------------------------------------------------------- 1 | .modal.fade 2 | .modal-dialog.modal-sm 3 | .modal-content.alert.alert-success 4 | .modal-body.text-center 5 | h5 Yummy yummy meal worms! 6 | p.mb-0 Payment successful! 7 | -------------------------------------------------------------------------------- /.idea/dictionaries/joe.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | msatoshi 5 | webhook 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/reactTemplatesPlugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ansible-playbook playbooks/deploy.yml --tags git,reboot #,cron,env_files, 3 | 4 | #ansible-playbook --limit $PI_HOST \ 5 | #playbooks/pi.yml \ 6 | #--tags git,install,config,tunnel,service 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 11 4 | cache: npm 5 | services: 6 | - docker 7 | script: 8 | - npm test 9 | - docker build -t pollofeed . 10 | 11 | if: type = pull_request || branch = master 12 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /playbooks/roles/pollofeed/tasks/nginx.yml: -------------------------------------------------------------------------------- 1 | - name: copy docker compose file 2 | tags: copy_docker_compose 3 | copy: 4 | src: ./nginx-docker-compose.yml 5 | dest: ./nginx/docker-compose.yml 6 | 7 | - name: up 8 | args: 9 | chdir: ./nginx 10 | command: docker-compose up --build -d 11 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | HOST='0.0.0.0' 2 | URL=mywebsite.com 3 | 4 | PORT=9999 5 | VIRTUAL_PORT=9999 6 | 7 | TITLE=pollofeed 8 | THEME=lumen 9 | SHOW_BOLT11=true 10 | FEED_PRICE=1000 # satoshis 11 | 12 | POLLOFEED_MONGO_URI=mongodb://mydatabase 13 | 14 | # lightning charge 15 | CHARGE_URL=https://charge.com 16 | CHARGE_TOKEN=token 17 | -------------------------------------------------------------------------------- /playbooks/roles/pi/files/motion.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Motion daemon 3 | After=network.service 4 | 5 | [Service] 6 | Type=simple 7 | ExecStart=/usr/bin/motion -c /etc/motion/motion.conf 8 | KillMode=process 9 | Restart=on-failure 10 | User=root 11 | Group=root 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /playbooks/roles/pi/files/pin.py: -------------------------------------------------------------------------------- 1 | import time 2 | from gpiozero import LED 3 | 4 | led = LED(18) 5 | 6 | def toggle(): 7 | led.on() 8 | print("ON") 9 | time.sleep(5) 10 | led.off() 11 | print("OFF") 12 | time.sleep(5) 13 | 14 | if __name__ == "__main__": 15 | while True: 16 | toggle() 17 | -------------------------------------------------------------------------------- /src/bin/runTotals.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | require('dotenv').config({path: path.join(__dirname, '..', "..", '.env.development')}) 3 | const dbconnect = require('./dbconnect') 4 | const totals = require('./totals') 5 | 6 | 7 | async function main() { 8 | await dbconnect() 9 | await totals() 10 | } 11 | 12 | main().then(process.exit) 13 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /src/bin/migrations/migrate_null.js: -------------------------------------------------------------------------------- 1 | var collection = db.orders 2 | 3 | var updates = [ 4 | {query: {quoted_currency: {$eq: null}}, update: {$set: {quoted_currency: ""}}}, 5 | {query: {quoted_amount: {$eq: null}}, update: {$set: {quoted_amount: 0}}}, 6 | ] 7 | 8 | 9 | updates.forEach(u => collection.update(u.query, u.update, {multi: true})) 10 | 11 | 12 | -------------------------------------------------------------------------------- /playbooks/roles/pi/files/ngrok.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Ngrok 3 | After=network.service 4 | 5 | [Service] 6 | Type=simple 7 | User=pi 8 | WorkingDirectory=/home/pi/Documents/pollofeed 9 | ExecStart=/usr/bin/ngrok start pollofeed --config="/home/pi/Documents/pollofeed/ngrok.yml" 10 | Restart=on-failure 11 | RestartSec=30s 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /playbooks/roles/pi/files/pollofeed.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=pollofeed 3 | After=network.service 4 | 5 | [Service] 6 | Type=simple 7 | User=pi 8 | WorkingDirectory=/home/pi/Documents/pollofeed 9 | ExecStart=/usr/bin/python3 /home/pi/Documents/pollofeed/pollofeed.py 10 | KillMode=process 11 | Restart=on-failure 12 | RestartSec=30s 13 | 14 | [Install] 15 | WantedBy=multi-user.target 16 | -------------------------------------------------------------------------------- /src/bin/manfeed.js: -------------------------------------------------------------------------------- 1 | const p = require('path') 2 | require('dotenv').config({path: p.join(__dirname, "..", "..", ".env.development")}) 3 | const feed = require('./feed') 4 | const dbconnect = require('./dbconnect') 5 | async function main() { 6 | await dbconnect() 7 | const times = parseInt(process.argv[2]) || 1 8 | await feed(times) 9 | process.exit(0) 10 | } 11 | 12 | 13 | main() 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/bin/migrations/setMsatNum.js: -------------------------------------------------------------------------------- 1 | 2 | const coll = db.orders 3 | const docs = coll.find({$or: [{msatoshi: {$type: "double"}}, {msatoshi: {$type: "string"}} ]}).toArray() 4 | 5 | const doc = docs[0] 6 | 7 | const msatoshi = NumberInt(doc.msatoshi) 8 | 9 | docs.forEach(doc => { 10 | const msatoshi = NumberInt(doc.msatoshi) 11 | coll.updateOne({_id: doc._id}, {$set: { 12 | msatoshi, 13 | }}) 14 | }) 15 | -------------------------------------------------------------------------------- /src/invoices/models/InvoiceResponse.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface InvoiceResponse { 3 | id: string 4 | msatoshi: string 5 | quoted_currency?: string 6 | quoted_amount?: string 7 | rhash: string 8 | payreq: string 9 | pay_index?: number 10 | description: string 11 | metadata?: any 12 | created_at: number 13 | expires_at: number 14 | paid_at?: number 15 | msatoshi_received?: string 16 | status: string 17 | } 18 | -------------------------------------------------------------------------------- /.idea/pollofeed.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /healthcheck.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | 3 | const options = { 4 | timeout: 3000, 5 | host: "localhost", 6 | port: process.env.PORT, 7 | path: "/health" 8 | } 9 | 10 | const request = http.get(options, (res) => { 11 | const {statusCode} = res 12 | console.log("status", statusCode) 13 | process.exitCode = statusCode === 200 ? 0 : 1 14 | process.exit() 15 | }) 16 | 17 | request.on("error", err => { 18 | console.error('ERROR', err); 19 | process.exit(1); 20 | }) 21 | 22 | request.end() 23 | -------------------------------------------------------------------------------- /playbooks/roles/pi/files/setup.py: -------------------------------------------------------------------------------- 1 | import os, yaml 2 | 3 | WORKING_DIR = os.path.dirname(os.path.realpath(__file__)) 4 | 5 | configPath = os.path.join(WORKING_DIR, "config.yml") 6 | 7 | try: 8 | with open(configPath, 'r') as ymlfile: 9 | cfg = yaml.load(ymlfile) 10 | except Exception: 11 | print( 12 | "Config file not found or invalid. Please provide a valid config.yml file. See exampleconfig.yml for reference") 13 | exit() 14 | 15 | if 'Settings' in cfg: 16 | settings = cfg['Settings'] 17 | else: 18 | settings = {} 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:carbon 2 | MAINTAINER joe chimienti 3 | ARG NODE_ENV=production 4 | ENV NODE_ENV $NODE_ENV 5 | ARG PORT=4321 6 | ENV PORT $PORT 7 | EXPOSE $PORT 8 | 9 | # get latest npm 10 | RUN npm i -g npm@latest 11 | RUN mkdir /pollofeed 12 | RUN chown -R node:node /pollofeed 13 | WORKDIR /pollofeed 14 | USER node 15 | COPY --chown=node:node package.json package-lock.json ./ 16 | RUN npm install 17 | COPY --chown=node:node . . 18 | RUN npm run build 19 | HEALTHCHECK --interval=30s CMD node healthcheck.js 20 | CMD node ./src/www.js 21 | -------------------------------------------------------------------------------- /src/bin/migrations/setDates.js: -------------------------------------------------------------------------------- 1 | const coll = db.orders 2 | 3 | 4 | const docs = coll.find({expires_at: {$type: "int"}, created_at: {$type: "int"}, paid_at: {$type: "int"}}).toArray() 5 | 6 | docs.forEach(doc => { 7 | 8 | const created_at = new Date(doc.created_at * 1000) 9 | const expires_at = new Date(doc.expires_at * 1000) 10 | const paid_at = new Date(doc.paid_at * 1000) 11 | 12 | //print(created_at, expires_at, paid_at) 13 | coll.updateOne({_id: doc._id}, {$set: { 14 | created_at, 15 | expires_at, 16 | paid_at 17 | }}) 18 | }) 19 | -------------------------------------------------------------------------------- /views/header.pug: -------------------------------------------------------------------------------- 1 | div 2 | nav.navbar.mb-4 3 | a.d-none.d-sm-block.nav-link.nav-link-text-lg.font-weight-bold(href='https://btcpal.online/apps/ZTDhnT9yQ52MLG9qqgT2hG8q1Uu/pos') 4 | | Swag 5 | a.d-none.d-sm-block.nav-link.nav-link-text-lg.font-weight-bold(href='/about') 6 | | About 7 | div.row.mb-1.d-block.d-sm-none.d-flex.justify-content-center.align-items-center 8 | a.nav-link.nav-link-text-md.font-weight-bold(href='https://btcpal.online/apps/ZTDhnT9yQ52MLG9qqgT2hG8q1Uu/pos') 9 | | Swag 10 | a.nav-link.nav-link-text-md.font-weight-bold.ml-2(href='/about') 11 | | About 12 | -------------------------------------------------------------------------------- /test/feedCost.test.js: -------------------------------------------------------------------------------- 1 | const feedCost = require('../src/invoices/feedCost.js') 2 | const test = require('tape') 3 | 4 | test("feedCost", function (t) { 5 | 6 | let fedToday = 0 7 | let baseCost = 1000 8 | 9 | let r = feedCost(baseCost, fedToday) 10 | 11 | t.assert(r === 1000) 12 | 13 | fedToday = 21 14 | r = feedCost(baseCost, fedToday) 15 | t.assert(r === 1500) 16 | 17 | 18 | fedToday = 31 19 | r = feedCost(baseCost, fedToday) 20 | t.assert(r === 2000) 21 | 22 | fedToday = 41 23 | r = feedCost(baseCost, fedToday) 24 | t.assert(r === 3000) 25 | 26 | 27 | 28 | t.end() 29 | }) 30 | -------------------------------------------------------------------------------- /src/invoices/feedCost.js: -------------------------------------------------------------------------------- 1 | var pricingFactor = { 2 | under20: 1, 3 | under30: 1.5, 4 | under40: 2, 5 | over40: 3 6 | }; 7 | function feedCost(baseCost, timesFedToday) { 8 | var factor = getFactor(timesFedToday); 9 | return Math.floor(baseCost * factor); 10 | } 11 | function getFactor(timesFedToday) { 12 | if (timesFedToday < 20) 13 | return pricingFactor.under20; 14 | else if (timesFedToday < 30) 15 | return pricingFactor.under30; 16 | else if (timesFedToday < 40) 17 | return pricingFactor.under40; 18 | else 19 | return pricingFactor.over40; 20 | } 21 | module.exports = feedCost; 22 | -------------------------------------------------------------------------------- /src/invoices/feedCost.ts: -------------------------------------------------------------------------------- 1 | const pricingFactor = { 2 | under20: 1, 3 | under30: 1.5, 4 | under40: 2, 5 | over40: 3 6 | } 7 | 8 | function feedCost(baseCost: number, timesFedToday: number): number { 9 | const factor = getFactor(timesFedToday) 10 | return Math.floor(baseCost * factor) 11 | } 12 | 13 | function getFactor(timesFedToday: number) : number { 14 | if (timesFedToday < 20) return pricingFactor.under20 15 | else if (timesFedToday < 30) return pricingFactor.under30 16 | else if (timesFedToday < 40) return pricingFactor.under40 17 | else return pricingFactor.over40 18 | } 19 | 20 | module.exports = feedCost 21 | -------------------------------------------------------------------------------- /test/app.test.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = "production" 2 | process.env.CHARGE_TOKEN = require('crypto').randomBytes(32).toString('hex') 3 | const app = require('../src/app') 4 | , request = require('supertest') 5 | , assert = require('assert') 6 | , test = require('tape') 7 | 8 | 9 | test('should extend the request prototype', function(t){ 10 | 11 | request(app) 12 | .get('/') 13 | 14 | .expect('Content-Type', "text/html; charset=utf-8") 15 | .expect(200) 16 | .end(function(err, res) { 17 | if (err) throw err; 18 | t.assert(res) 19 | t.end() 20 | }); 21 | }) 22 | -------------------------------------------------------------------------------- /src/bin/dbconnect.js: -------------------------------------------------------------------------------- 1 | const {MongoClient} = require('mongodb') 2 | 3 | module.exports = async function() { 4 | 5 | const client = await MongoClient.connect(process.env.POLLOFEED_MONGO_URI, {poolSize: 5, useNewUrlParser: true}).catch(err => { 6 | console.error('error connecting to server @', process.env.POLLOFEED_MONGO_URI) 7 | console.error(err) 8 | process.exit(1) 9 | }) 10 | console.log('Connected successfully to server') 11 | 12 | const dbName = process.env.MONGO_DB_NAME || (console.error('no MONGO_DB_NAME env'), process.exit(1)) 13 | 14 | global.db = client.db(dbName) 15 | 16 | return global.db 17 | } 18 | -------------------------------------------------------------------------------- /src/bin/calcFeedTimes.js: -------------------------------------------------------------------------------- 1 | 2 | function calcFeedTimes(hours = new Date().getHours(), todayFeedCount = 0, yesterdayFeedCount = 0) { 3 | const [threshold1, threshold2, threshold3] = [10,10,12] 4 | let feedTimes = 0 5 | if (hours >= 17) feedTimes = threshold3 - todayFeedCount 6 | else if (hours >= 13) feedTimes = threshold2 - todayFeedCount 7 | else if (hours >= 5) { 8 | if (yesterdayFeedCount >= 30) feedTimes = 0 9 | else if (todayFeedCount >= 5) feedTimes = 0 10 | else feedTimes = threshold1 - todayFeedCount 11 | } 12 | // feed b/w 0 - 2 times 13 | return Math.min(2, Math.max(0, feedTimes)) 14 | } 15 | 16 | module.exports = calcFeedTimes 17 | -------------------------------------------------------------------------------- /playbooks/roles/pollofeed/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: fetch latest git version 3 | tags: git 4 | # register: repo 5 | # set_fact: 6 | # repo_changed: repo.changed 7 | git: 8 | repo: "https://github.com/j-chimienti/pollofeed.git" 9 | update: yes 10 | dest: "./{{APP_DIR}}" 11 | 12 | - name: env files 13 | tags: env_files 14 | with_items: 15 | - .env 16 | - .env.mongo 17 | - .env.development 18 | copy: 19 | src: "../../../../{{ item }}" 20 | dest: "./{{APP_DIR}}/{{ item }}" 21 | 22 | 23 | - name: build docker container 24 | command: docker-compose up --build -d 25 | tags: reboot 26 | args: 27 | chdir: "./{{APP_DIR}}" 28 | 29 | 30 | - name: cron 31 | include: cron.yml 32 | tags: cron 33 | -------------------------------------------------------------------------------- /src/bin/email.ts: -------------------------------------------------------------------------------- 1 | const sgMail = require('@sendgrid/mail'); 2 | sgMail.setApiKey(process.env.SENDGRID_API_KEY); 3 | 4 | import {ClientResponse} from "@sendgrid/client/src/response"; 5 | import {MailData} from "@sendgrid/helpers/classes/mail"; 6 | 7 | 8 | async function send(options: MailData): Promise<[ClientResponse, {}]> { 9 | return await sgMail.send(options); 10 | } 11 | 12 | async function sendFromDefaultUser(subject: string, text: string) { 13 | 14 | const options = { 15 | from: process.env.GMAIL_USER, // sender address 16 | to: process.env.GMAIL_USER, // list of receivers 17 | subject, 18 | text 19 | } 20 | return send(options) 21 | } 22 | 23 | module.exports = { 24 | send, 25 | sendFromDefaultUser 26 | } 27 | 28 | 29 | -------------------------------------------------------------------------------- /views/footer.pug: -------------------------------------------------------------------------------- 1 | footer.bg-secondary.container-fluid.d-flex.justify-content-center.align-items-center(style="height: 10vh; min-height: 200px") 2 | .row.d-flex.justify-content-around.align-items-center.h-100(style="font-size: 1.6rem;") 3 | a.d-flex.justify-content-center.align-items-center(class="nav-link" href="https://tip.pollofeed.com") 4 | i(class="fa fa-thumbs-up fa-2x mr-2") 5 | b Tips appreciated 6 | a.d-flex.justify-content-center.align-items-center(class="nav-link" href="https://github.com/j-chimienti/pollofeed") 7 | i(class="fa fa-github fa-2x mr-2") 8 | b Repo 9 | a.d-flex.justify-content-center.align-items-center(class="nav-link" href="/about") 10 | i.fa.fa-info-circle.fa-2x.mr-2 11 | b About 12 | -------------------------------------------------------------------------------- /playbooks/roles/pollofeed/files/mb-docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | services: 3 | metabase: 4 | external_links: 5 | - db 6 | volumes: 7 | - /metabase-data:/metabase-data 8 | container_name: metabase_pollofeed 9 | networks: 10 | - pollofeed 11 | - generated_default 12 | image: metabase/metabase 13 | ports: 14 | - "8788:3000" 15 | environment: 16 | JAVA_TIMEZONE: US/Eastern 17 | VIRTUAL_HOST: metabase.btcpal.online 18 | LETSENCRYPT_HOST: metabase.btcpal.online 19 | LETSENCRYPT_EMAIL: jchimien@gmail.com 20 | VIRTUAL_PORT: 8788 21 | restart: always 22 | networks: 23 | pollofeed: 24 | external: 25 | name: pollofeed_pollofeed 26 | generated_default: 27 | external: 28 | name: generated_default 29 | -------------------------------------------------------------------------------- /src/invoices/models/PolloFeedOrder.ts: -------------------------------------------------------------------------------- 1 | import {LightningInvoice} from './LightningInvoice' 2 | import {InvoiceResponse} from "./InvoiceResponse"; 3 | import {PollofeedMetadata} from "./PollofeedMetadata"; 4 | 5 | 6 | export class PolloFeedOrder extends LightningInvoice { 7 | 8 | feed: boolean = true 9 | acknowledged_at: boolean = false 10 | metadata: PollofeedMetadata 11 | constructor(invoice : InvoiceResponse) { 12 | super(invoice) 13 | if (!(this.id && this.payreq && this.status && this.rhash && this.pay_index)) { 14 | console.log(this.id, this.payreq, this.status, this.rhash, this.pay_index) 15 | throw new Error("Invalid order " + JSON.stringify(this)) 16 | } 17 | this.metadata = { feedTimes: this.metadata && 18 | 'feedTimes' in this.metadata && this.metadata.feedTimes || 1} 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /views/payment.pug: -------------------------------------------------------------------------------- 1 | - msat2sat = require('fmtbtc').msat2sat 2 | 3 | .modal.fade 4 | .modal-dialog.modal-sm 5 | .modal-content 6 | .modal-body.text-center 7 | h5 Pay with Lightning 8 | if msatoshi 9 | p.font-weight-light.small.text-monospace #{ msat2sat(msatoshi, true) } satoshi 10 | div 11 | img.pollofeed_logo(src="pollofeed.png") 12 | img.d-block.w-100.mb-3(src=qr) 13 | if show_bolt11 14 | .input-group 15 | input.form-control(type='text', value=payreq, readonly id="payreq") 16 | .input-group-append 17 | a.btn.btn-outline-dark(href='lightning:'+payreq) 18 | i.fa.fa-bolt.text-warning.font-weight-bold 19 | .input-group-append 20 | a.btn.btn-outline-dark(id="copyPayReq") 21 | i.fa.fa-copy.text-white.font-weight-bold 22 | p.text-muted.small.font-weight-light.mt-1.mb-0 Invoice expires in #[span(data-countdown-to=expires_at)] 23 | -------------------------------------------------------------------------------- /playbooks/roles/pollofeed/files/nginx-docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | nginx: 5 | restart: always 6 | image: jwilder/nginx-proxy 7 | container_name: nginx 8 | ports: 9 | - 80:80 10 | - 443:443 11 | networks: 12 | - nginxproxy 13 | volumes: 14 | - /etc/nginx/certs 15 | - /etc/nginx/vhost.d 16 | - /usr/share/nginx/html 17 | - /var/run/docker.sock:/tmp/docker.sock:ro 18 | # - /data/letsencrypt-nginx-proxy-companion/certs/:/etc/nginx/certs:ro 19 | 20 | letsencrypt-nginx-proxy-companion: 21 | restart: always 22 | image: jrcs/letsencrypt-nginx-proxy-companion 23 | container_name: le 24 | networks: 25 | - nginxproxy 26 | volumes_from: 27 | - nginx 28 | volumes: 29 | - /var/run/docker.sock:/var/run/docker.sock:ro 30 | # - /data/letsencrypt-nginx-proxy-companion/certs/:/etc/nginx/certs:ro 31 | 32 | #networks: 33 | # nginxproxy: 34 | # external: 35 | # name: nginxproxy 36 | 37 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | services: 3 | db: 4 | container_name: db 5 | image: mongo 6 | volumes: 7 | - /mongo/data/pollofeed:/data/db 8 | env_file: 9 | - .env.mongo 10 | ports: 11 | - 27017:27017 12 | restart: always 13 | networks: 14 | - nginxproxy 15 | healthcheck: 16 | test: "[ `echo 'db.runCommand(\"ping\").ok' | mongo localhost/${MONGO_DB_NAME} --quiet` ] && echo 0 || echo 1" 17 | interval: 5s 18 | start_period: 10s 19 | timeout: 4s 20 | retries: 3 21 | pollofeed: 22 | volumes: 23 | - "/etc/timezone:/etc/timezone:ro" 24 | - "/etc/localtime:/etc/localtime:ro" 25 | container_name: pollofeed 26 | build: . 27 | restart: unless-stopped 28 | depends_on: 29 | - db 30 | networks: 31 | - nginxproxy 32 | env_file: 33 | - .env 34 | ports: 35 | - "${PORT}:${PORT}" 36 | 37 | networks: 38 | nginxproxy: 39 | external: 40 | name: nginxproxy 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/bin/feed.js: -------------------------------------------------------------------------------- 1 | 2 | async function feed(feedTimes = 1) { 3 | 4 | const futureDate = new Date(new Date().getTime() + (86400000 + 100)).getTime() 5 | const options = {upsert: true, returnNewDocument: true, returnOriginal: false} 6 | const testOrder = { 7 | feed: true, 8 | acknowledged_at: null, 9 | msatoshi: 0, 10 | paid_at: futureDate, 11 | pay_index: -1, 12 | metadata: {feedTimes} 13 | } 14 | return await global.db.collection('orders').findOneAndUpdate({id: "testing"}, {$set: testOrder}, options) 15 | } 16 | 17 | 18 | const sleep = (ms = 1000) => new Promise(r => setTimeout(r, ms)) 19 | 20 | 21 | async function main(times = 2) { 22 | console.log("feed %s", times) 23 | await feed(times); 24 | await sleep(7000) 25 | const orderOpt = await global.db.collection('orders').find({id: /test/, feed: false}) 26 | // The feeder can be offline, so only delete if chickens actually fed 27 | if (orderOpt) 28 | return await global.db.collection('orders').deleteMany({id: /test/}) 29 | } 30 | 31 | 32 | module.exports = main 33 | -------------------------------------------------------------------------------- /playbooks/roles/pollofeed/tasks/cron.yml: -------------------------------------------------------------------------------- 1 | - name: cron feeder 2 | # The “name” parameter should be unique, 3 | # and changing the “name” value will result in a new cron task being created (or a different one being removed). 4 | # remove via state: absent 5 | cron: 6 | name: feed chickens if underfed 7 | hour: "7,13,17" 8 | minute: "0" 9 | state: present 10 | job: "node {{ BIN_DIR }}/feeder.js" 11 | 12 | - name: daily reporting 13 | cron: 14 | name: feed report 15 | hour: "23" 16 | minute: "59" 17 | state: present 18 | job: "node {{ BIN_DIR }}/report.js" 19 | 20 | - name: update btcpayserver 21 | cron: 22 | name: update btcpayserver 23 | hour: "1" 24 | minute: "0" 25 | state: present 26 | job: btcpay-update.sh 27 | 28 | - name: pi unresponsive check 29 | cron: 30 | name: pi unresponsive check 31 | state: absent 32 | 33 | - name: pi and lightning health check 34 | cron: 35 | name: pi and lightning health check 36 | job: "node {{BIN_DIR}}/unresponsiveCheck.js >> /unresponsiveCheck.log 2>&1" 37 | hour: "*" 38 | minute: "*/5" 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚡ Pollofeed ⚡ 🐔😂 2 | **Bitcoin Lightning-powered chicken feeder on Raspberry Pi** 3 | Real BTC payment → chickens get fed in <3 seconds. 100 % automated, zero disputes ever. 4 | 5 | ![Pollofeed in action](demo.gif) 6 | 7 | ### Live demos 8 | - https://www.youtube.com/watch?v=a0_dqDxx7Oo 9 | - https://www.youtube.com/watch?v=jXC39uCSrfA 10 | 11 | ### Production RabbitMQ flow (async + live broadcast) 12 | ```mermaid 13 | graph TD 14 | A[Lightning Invoice Paid] --> B[order_new queue] 15 | B --> C[Worker picks up] 16 | C --> D[order_processing queue] 17 | D --> E[Servo drops feed + ffmpeg records] 18 | E --> F[order_complete queue] 19 | F --> G[WebSocket broadcast → every viewer sees chickens go nuts] 20 | style A fill:#f9f,stroke:#333 21 | style G fill:#bbf,stroke:#333 22 | ``` 23 | 24 | Queues: order_new → order_processing → order_complete 25 | Tech stack 26 | Scala • WebSockets • RabbitMQ • Bitcoin Lightning • Raspberry Pi • Arduino • Docker • Cloudflare • ffmpeg 27 | Run locally (<5 min) 28 | ``` 29 | Bashcp example.env .env 30 | npm install 31 | npm run start:dev 32 | ``` 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Joe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/bin/ordersByDay.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | require('dotenv').config({path: path.join(__dirname, '..', "..", '.env.development')}) 3 | const orderDao = require('../invoices/dao') 4 | const moment = require('moment/moment') 5 | const dbconnect = require('./dbconnect') 6 | 7 | async function main() { 8 | await dbconnect() 9 | const yesterday = moment().subtract(1, 'day').toDate(); 10 | const todayOrders = await orderDao.getOrdersByDate() 11 | console.log(todayOrders) 12 | const yesterdayOrders = await orderDao.getOrdersByDate(yesterday) 13 | let t = getTotals(todayOrders); 14 | let y = getTotals(yesterdayOrders); 15 | console.log("today", t) 16 | console.log("yesterday", y) 17 | return t; 18 | 19 | } 20 | function getTotals(orders) { 21 | const totalMSats = orders.reduce((accum, order) => { 22 | const {msatoshi} = order; 23 | return parseInt(msatoshi) + accum; 24 | }, 0) 25 | const totalSats = totalMSats / 1000; 26 | return {orders: orders.length, satsoshis: totalSats} 27 | } 28 | 29 | 30 | 31 | main().then(() => process.exit(0)) 32 | .catch(err => process.exit(1)) 33 | -------------------------------------------------------------------------------- /test/PolloFeedInvoice.test.js: -------------------------------------------------------------------------------- 1 | const {LightningInvoice} = require("../src/invoices/models/LightningInvoice"); 2 | 3 | const {PolloFeedOrder} = require('../src/invoices/models/PolloFeedOrder') 4 | 5 | const test = require('tape') 6 | 7 | 8 | test("Creates order", function (t) { 9 | 10 | const _inv = {id: "hello", payreq: "payreq", status: "paid", rhash: "rhash", pay_index: 999} 11 | const invoice = new PolloFeedOrder(_inv) 12 | t.equal(_inv.id, invoice.id) 13 | t.equal(_inv.id, invoice.id) 14 | t.equal(_inv.status, invoice.status) 15 | t.equal(_inv.payreq, invoice.payreq) 16 | t.equal(invoice.feed, true) 17 | t.equal(invoice.acknowledged_at, false) 18 | t.end() 19 | 20 | }) 21 | 22 | test("Throws error with missing LN fields", function (t) { 23 | 24 | const _order = {id: "hello"} 25 | t.throws(() => new PolloFeedOrder(_order)) 26 | t.throws(() => new PolloFeedOrder({payreq: "i"})) 27 | t.throws(() => new PolloFeedOrder({status: "one"})) 28 | t.throws(() => new PolloFeedOrder({payreq: "i", status: "o"})) 29 | t.throws(() => new PolloFeedOrder({id: "i", status: "o"})) 30 | t.throws(() => new PolloFeedOrder({id: "i", payreq: "o"})) 31 | 32 | t.end() 33 | }) 34 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | 2 | :root { 3 | 4 | --pollofeed_blue: #67c6df; 5 | --pollofeed_blue_dark: #1f7c95; 6 | 7 | } 8 | 9 | .app { 10 | font-size: 1.5rem; 11 | height: 90vh; 12 | min-height: 700px; 13 | } 14 | 15 | a { 16 | color: var(--pollofeed_blue); 17 | } 18 | 19 | a:hover { 20 | 21 | color: var(--pollofeed_blue_dark); 22 | cursor: pointer; 23 | } 24 | 25 | .pollofeed_logo { 26 | width: 65px; 27 | position: absolute; 28 | left: 118px; 29 | top: 160px; 30 | } 31 | 32 | .btn_feed { 33 | 34 | max-width: 275px; 35 | font-size: 1.6rem; 36 | letter-spacing: .2rem; 37 | background-color: var(--pollofeed_blue); 38 | transition: background-color 0.2s linear; 39 | 40 | } 41 | 42 | .btn_feed:hover { 43 | 44 | background-color: var(--pollofeed_blue_dark); 45 | } 46 | 47 | @media all and (max-width: 576px) { 48 | .pollofeed_logo { 49 | visibility: hidden; 50 | } 51 | } 52 | .navbar { 53 | height: 100px; 54 | background: url(banner.png) no-repeat center; 55 | background-size: contain; 56 | } 57 | 58 | 59 | .nav-link-text-lg { 60 | 61 | font-size: 1.5rem; 62 | } 63 | 64 | .nav-link-text-md { 65 | 66 | font-size: 1.25rem; 67 | padding: 0; 68 | } 69 | 70 | .img-fluid { 71 | max-width: 800px; 72 | } 73 | -------------------------------------------------------------------------------- /test/calcFeedTimes.test.js: -------------------------------------------------------------------------------- 1 | const calcFeedTimes = require('../src/bin/calcFeedTimes') 2 | const test = require('tape') 3 | 4 | 5 | test("feed zero times if fed a lot", function (t) { 6 | 7 | const results = [] 8 | 9 | for (let i = 0; i <= 24; i++) { 10 | results.push(calcFeedTimes(i, 99)) 11 | } 12 | results.forEach(result => t.assert(result === 0)) 13 | t.end() 14 | }) 15 | 16 | test("max is 2", function (t) { 17 | 18 | const results = [] 19 | 20 | for (let i = 5; i <= 24; i++) { 21 | results.push(calcFeedTimes(i, 0, 0)) 22 | } 23 | 24 | results.forEach(result => t.assert(result === 2, result)) 25 | t.end() 26 | }) 27 | 28 | test("should return 2", function (t) { 29 | 30 | 31 | const results = [ 32 | calcFeedTimes(10, 3), 33 | calcFeedTimes(14, 8), 34 | calcFeedTimes(20, 10), 35 | ] 36 | 37 | results.forEach(result => t.assert(result === 2, result)) 38 | t.end() 39 | }) 40 | 41 | test("feed 0 times if fed a lot yesterday", function (t) { 42 | 43 | const r = calcFeedTimes(5, 0, 35) 44 | 45 | t.assert(r === 0) 46 | t.end() 47 | }) 48 | 49 | 50 | test("feed 0 times if fed over 5 today", function (t) { 51 | const r = calcFeedTimes(5, 5, 0) 52 | t.assert(r === 0) 53 | t.end() 54 | }) 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/invoices/models/LightningInvoice.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var LightningInvoice = /** @class */ (function () { 4 | function LightningInvoice(_a) { 5 | var id = _a.id, msatoshi = _a.msatoshi, quoted_currency = _a.quoted_currency, quoted_amount = _a.quoted_amount, rhash = _a.rhash, payreq = _a.payreq, pay_index = _a.pay_index, description = _a.description, _b = _a.metadata, metadata = _b === void 0 ? {} : _b, created_at = _a.created_at, expires_at = _a.expires_at, paid_at = _a.paid_at, msatoshi_received = _a.msatoshi_received, status = _a.status; 6 | this.id = id; 7 | this.msatoshi = parseInt(msatoshi); 8 | this.quoted_currency = quoted_currency || ""; 9 | this.quoted_amount = quoted_amount && parseInt(quoted_amount) || 0; 10 | this.rhash = rhash; 11 | this.payreq = payreq; 12 | this.pay_index = pay_index; 13 | this.description = description; 14 | this.metadata = metadata; 15 | this.created_at = new Date(created_at * 1000); 16 | this.expires_at = new Date(expires_at * 1000); 17 | this.paid_at = new Date(paid_at * 1000); 18 | this.msatoshi_received = msatoshi_received; 19 | this.status = status; 20 | } 21 | return LightningInvoice; 22 | }()); 23 | exports.LightningInvoice = LightningInvoice; 24 | -------------------------------------------------------------------------------- /playbooks/roles/pi/files/pollofeed.py: -------------------------------------------------------------------------------- 1 | import time 2 | import datetime 3 | from gpiozero import LED 4 | import datetime 5 | from pymongo import MongoClient, ASCENDING 6 | from setup import settings 7 | 8 | url = settings['mongo']['url'] 9 | client = MongoClient(url) 10 | db = client[settings['mongo']['db']] 11 | order_collection = db.orders 12 | 13 | print("connected to db") 14 | 15 | pin = 18 16 | motor = LED(pin) 17 | 18 | def activate(seconds): 19 | print("LED on") 20 | start = time.time() 21 | motor.on() 22 | time.sleep(seconds) 23 | motor.off() 24 | end = time.time() 25 | print("LED off") 26 | print('LED time:', end - start) 27 | 28 | 29 | def find_one(): 30 | return order_collection.find_one_and_update( 31 | {"feed": True}, 32 | {"$set": { 33 | "acknowledged_at": datetime.datetime.utcnow(), 34 | "feed": False, 35 | }}, 36 | sort=[('paid_at', ASCENDING)] 37 | ) 38 | 39 | 40 | def run(): 41 | order_opt = find_one() 42 | if order_opt is None: 43 | print("no orders found", datetime.datetime.now().strftime("%Y-%m-%d %I:%M:%S %p")) 44 | time.sleep(0.1) 45 | return True 46 | print('Order = {}'.format(order_opt.get("id")), datetime.datetime.now().strftime("%Y-%m-%d %I:%M:%S %p")) 47 | seconds = int(order_opt.get('metadata', {"feedTimes": 1}).get('feedTimes', 1)) * 9 48 | print("Feed {}".format(seconds)) 49 | activate(seconds) 50 | return True 51 | 52 | 53 | if __name__ == '__main__': 54 | while True: 55 | run() 56 | -------------------------------------------------------------------------------- /src/bin/report.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | require('dotenv').config({path: path.join(__dirname, '..', "..", '.env.development')}) 3 | const orderDao = require('../invoices/dao') 4 | const dbconnect = require('./dbconnect') 5 | const {send} = require('./email') 6 | // const totals = require('./totals') 7 | async function main() { 8 | 9 | await dbconnect() 10 | const todayOrders = await orderDao.getOrdersByDate() 11 | let todayTotals = getTotals(todayOrders); 12 | const text = `Todays feedings: ${todayTotals.orders}, Satoshis: ${todayTotals.satsoshis.toLocaleString()}` 13 | // const { 14 | // oldestOrderDate, 15 | // newEstOrderDate, 16 | // days, 17 | // min, // max feeding day 18 | // max, // min feedin day 19 | // avgDay, 20 | // satsTotal, 21 | // btc, 22 | // } = await totals() 23 | 24 | const html = `

Summary

25 |

Today = ${todayTotals.orders}, sats = ${todayTotals.satsoshis.toLocaleString()}

26 | ` 27 | 28 | const mailOptions = { 29 | from: process.env.GMAIL_USER, // sender address 30 | to: process.env.GMAIL_USER, // list of receivers 31 | subject: text, 32 | html 33 | } 34 | await send(mailOptions) 35 | return todayTotals; 36 | } 37 | 38 | function getTotals(orders) { 39 | const totalMSats = orders.reduce((accum, order) => { 40 | const {msatoshi} = order; 41 | return parseInt(msatoshi) + accum; 42 | }, 0) 43 | const totalSats = totalMSats / 1000; 44 | return {orders: orders.length, satsoshis: totalSats} 45 | } 46 | 47 | main().then(process.exit).catch(process.exit) 48 | -------------------------------------------------------------------------------- /src/invoices/models/PolloFeedOrder.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __extends = (this && this.__extends) || (function () { 3 | var extendStatics = function (d, b) { 4 | extendStatics = Object.setPrototypeOf || 5 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 6 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; 7 | return extendStatics(d, b); 8 | }; 9 | return function (d, b) { 10 | extendStatics(d, b); 11 | function __() { this.constructor = d; } 12 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 13 | }; 14 | })(); 15 | Object.defineProperty(exports, "__esModule", { value: true }); 16 | var LightningInvoice_1 = require("./LightningInvoice"); 17 | var PolloFeedOrder = /** @class */ (function (_super) { 18 | __extends(PolloFeedOrder, _super); 19 | function PolloFeedOrder(invoice) { 20 | var _this = _super.call(this, invoice) || this; 21 | _this.feed = true; 22 | _this.acknowledged_at = false; 23 | if (!(_this.id && _this.payreq && _this.status && _this.rhash && _this.pay_index)) { 24 | console.log(_this.id, _this.payreq, _this.status, _this.rhash, _this.pay_index); 25 | throw new Error("Invalid order " + JSON.stringify(_this)); 26 | } 27 | _this.metadata = { feedTimes: _this.metadata && 28 | 'feedTimes' in _this.metadata && _this.metadata.feedTimes || 1 }; 29 | return _this; 30 | } 31 | return PolloFeedOrder; 32 | }(LightningInvoice_1.LightningInvoice)); 33 | exports.PolloFeedOrder = PolloFeedOrder; 34 | -------------------------------------------------------------------------------- /src/invoices/models/LightningInvoice.ts: -------------------------------------------------------------------------------- 1 | import {InvoiceResponse} from "./InvoiceResponse"; 2 | 3 | export class LightningInvoice { 4 | id: string 5 | status: string 6 | msatoshi: number 7 | quoted_currency: string 8 | quoted_amount: number 9 | rhash: string 10 | payreq: string 11 | pay_index: number 12 | description: string 13 | metadata?: any 14 | created_at: Date 15 | expires_at: Date 16 | paid_at?: Date 17 | msatoshi_received: string 18 | constructor({ 19 | id, 20 | msatoshi, 21 | quoted_currency, 22 | quoted_amount, 23 | rhash, 24 | payreq, 25 | pay_index, 26 | description, 27 | metadata = {}, 28 | created_at, 29 | expires_at, 30 | paid_at, 31 | msatoshi_received, 32 | status 33 | } : InvoiceResponse) { 34 | 35 | this.id = id 36 | this.msatoshi = parseInt(msatoshi) 37 | this.quoted_currency = quoted_currency || "" 38 | this.quoted_amount = quoted_amount && parseInt(quoted_amount) || 0 39 | this.rhash = rhash 40 | this.payreq = payreq 41 | this.pay_index = pay_index 42 | this.description = description 43 | this.metadata = metadata 44 | this.created_at = new Date(created_at * 1000) 45 | this.expires_at = new Date(expires_at * 1000) 46 | this.paid_at = new Date(paid_at * 1000) 47 | this.msatoshi_received = msatoshi_received 48 | this.status = status 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /views/about.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | 3 | html 4 | title= settings.title 5 | meta(charset='utf-8') 6 | meta(name='viewport', content='width=device-width, initial-scale=1') 7 | meta(name='apple-mobile-web-app-capable', content='yes') 8 | meta(name="mobile-web-app-capable" content="yes") 9 | link(rel="shortcut icon" href="pollofeed.png") 10 | meta(name="application-name" content="pollofeed") 11 | meta(name="apple-mobile-web-app-title" content="pollofeed") 12 | meta(name="msapplication-starturl" content="/") 13 | meta(name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no") 14 | link(rel='stylesheet', href='bootswatch/'+ settings.theme +'/bootstrap.min.css') 15 | link(rel='stylesheet', href="style.css") 16 | link(rel='stylesheet', href='https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css') 17 | 18 | body.text-center 19 | h1.text-center About Us 20 | a.ml-2(href="/") 21 | i.fa.fa-home 22 | .container.mb-4 23 | h2.my-2 Karen and Lori 24 | p.my-1 Always together. The oldest 2 Rhode Island Reds. Hatched 15/Aug/2016 25 | img.my-1(class="img-fluid" src="KarenLori.jpg") 26 | 27 | h2.my-2 Parkour 28 | p.my-1 Always jumping around and loves to hit pen with both feet 29 | img.my-1(class="img-fluid" src="Parkour.jpg") 30 | 31 | h2.my-2 Daisy 32 | p.my-1 Because she's lazy. Digs holes and lays in them 33 | img.my-1(class="img-fluid" src="daisy.jpg") 34 | 35 | //RED IS DEAD 36 | //h2.my-2 RED 37 | //p.my-1 Retired Extremely Dangerous (to your toes and legs) 38 | //img.my-1(class="img-fluid" src="RED.jpg") 39 | 40 | h2.my-2 Big Black 41 | p.my-1 The alpha hen. Lord of the Seven Kingdoms and Protector of the Realm 42 | img.my-1(class="img-fluid" src="BigBlack.jpg") 43 | 44 | 45 | -------------------------------------------------------------------------------- /views/index.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | 3 | html 4 | title= settings.title 5 | meta(charset='utf-8') 6 | meta(name="apple-mobile-web-app-status-bar" content="#67c6df") 7 | meta(name="theme-color" content="#67c6df") 8 | meta(name='viewport', content='width=device-width, initial-scale=1') 9 | meta(name='csrf', content=req.csrfToken()) 10 | meta(name='show-bolt11', content=settings.show_bolt11 ? 1 : '') 11 | meta(name='apple-mobile-web-app-capable', content='yes') 12 | meta(name="mobile-web-app-capable" content="yes") 13 | meta(name="application-name" content="pollofeed") 14 | meta(name="apple-mobile-web-app-title" content="pollofeed") 15 | meta(name="msapplication-starturl" content="/") 16 | meta(name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no") 17 | link(rel="shortcut icon" href="pollofeed.png") 18 | link(rel='stylesheet', href='bootswatch/' + settings.theme + '/bootstrap.min.css') 19 | link(rel='stylesheet', href="style.css") 20 | link(rel='stylesheet', href='https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css') 21 | 22 | body 23 | include header.pug 24 | div.app.container.mb-4 25 | .row 26 | button( 27 | class="btn btn-primary btn-block btn_feed mx-auto mb-4" 28 | type="submit" data-buy-item="pollofeed" 29 | ) 30 | i.fa.fa-bolt.font-weight-bold.mr-1 31 | span.font-weight-bold.text-uppercase feed 32 | .row.mb-4 33 | iframe.rounded.mx-auto.shadow( 34 | title='live stream' 35 | src='https://pollofeed.ngrok.io' 36 | height='480' 37 | style="border: none;" 38 | width="640" 39 | ) 40 | include footer.pug 41 | script(src='script.js') 42 | -------------------------------------------------------------------------------- /src/bin/feeder.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | require('dotenv').config({path: path.resolve(__dirname, '../../.env.development')}) 3 | 4 | const orderDao = require('../invoices/dao') 5 | const feed = require('./feed') 6 | const moment = require('moment') 7 | const calcFeedTimes = require('./calcFeedTimes') 8 | const dbconnect = require('./dbconnect') 9 | 10 | const {send} = require('./email') 11 | 12 | async function main() { 13 | 14 | await dbconnect() 15 | 16 | const todayOrders = await orderDao.getOrdersByDate() 17 | 18 | const yesterday = moment().subtract(1, 'day').toDate(); 19 | const yesterDayOrders = await orderDao.getOrdersByDate(yesterday) 20 | 21 | const feedTimes = calcFeedTimes(new Date().getHours(), todayOrders.length, yesterDayOrders.length) 22 | 23 | const totals = getTotals(todayOrders) 24 | const yTotals = getTotals(yesterDayOrders) 25 | const shouldFeed = feedTimes > 0 26 | if (shouldFeed) await feed(feedTimes) 27 | 28 | let text = `pollofeed - fed ${todayOrders.length} times today.` 29 | if (shouldFeed) text += `\tjust fed ${feedTimes} times.` 30 | const mailOptions = { 31 | from: process.env.GMAIL_USER, // sender address 32 | to: process.env.GMAIL_USER, // list of receivers 33 | subject: text, 34 | html: `
35 |

36 | Today Orders : ${JSON.stringify(totals, null, 2)} 37 |

38 |

39 | Yesterday's Orders: ${JSON.stringify(yTotals, null, 2)} 40 |

41 |
` 42 | }; 43 | await send(mailOptions) 44 | process.exit(0) 45 | 46 | } 47 | 48 | function getTotals(orders) { 49 | const totalMSats = orders.reduce((accum, order) => { 50 | const {msatoshi} = order; 51 | return parseInt(msatoshi) + accum; 52 | }, 0) 53 | const totalSats = totalMSats / 1000; 54 | return {orders: orders.length, satsoshis: totalSats} 55 | } 56 | 57 | 58 | 59 | main() 60 | 61 | module.exports = { 62 | main, 63 | } 64 | -------------------------------------------------------------------------------- /src/invoices/dao.js: -------------------------------------------------------------------------------- 1 | const publicProjection = {acknowledged_at: 1, msatoshi: 1, paid_at: 1, pay_index: 1, _id: 0} 2 | module.exports = { 3 | 4 | 5 | getOrdersByDate: async (date = new Date()) => { 6 | 7 | const left = new Date(new Date(date).setHours(0, 0, 0, 0)); 8 | 9 | const right = new Date(new Date(date).setHours(23, 59, 59, 99)); 10 | 11 | return await global.db.collection('orders') 12 | .find({paid_at: {$gte: left, $lte: right}}) 13 | .toArray() 14 | }, 15 | 16 | countOrdersByDate: async (date = new Date()) => { 17 | 18 | const left = new Date(new Date(date).setHours(0, 0, 0, 0)); 19 | 20 | const right = new Date(new Date(date).setHours(23, 59, 59, 99)); 21 | 22 | return await global.db.collection('orders') 23 | .countDocuments({paid_at: {$gte: left, $lte: right}}) 24 | }, 25 | 26 | getOrders: async ({offset = 0, limit = 2000} = {}) => { 27 | return await global.db.collection('orders') 28 | .find() 29 | .project(publicProjection) 30 | .sort({paid_at: -1}) 31 | .skip(offset) 32 | .limit(limit) 33 | .toArray() 34 | }, 35 | insert: async (order) => { 36 | return await global.db.collection('orders') 37 | .insertOne(order) 38 | }, 39 | 40 | count: async () => { 41 | 42 | return await global.db.collection('orders') 43 | .countDocuments() 44 | }, 45 | totalMsats: async () => { 46 | 47 | const intConversionStatement = { 48 | $addFields: {msat: {$toInt: "$msatoshi"}} 49 | } 50 | const sumStatement = {$group: { 51 | _id: null, 52 | msatoshiTotal: {$sum: "$msat"} 53 | }} 54 | return await global.db.collection('orders') 55 | .aggregate([ 56 | intConversionStatement, 57 | sumStatement 58 | ]).toArray() 59 | }, 60 | findById: async (id) => { 61 | 62 | return await global.db.collection('orders') 63 | .findOne({id}) 64 | 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /playbooks/roles/pi/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Update profile timezone 4 | become: yes 5 | lineinfile: 6 | dest: ~/.profile 7 | state: present 8 | line: "TZ='America/New_York'; export TZ" 9 | 10 | - name: Install packages 11 | become: yes 12 | vars: 13 | packages: 14 | - vim 15 | - motion 16 | - nodejs 17 | - npm 18 | apt: 19 | name: "{{ packages }}" 20 | update_cache: yes 21 | 22 | - name: Copy pollofeed app files 23 | tags: copy_files 24 | with_items: 25 | - pollofeed.py 26 | - pin.py 27 | - setup.py 28 | - config.yml 29 | - ngrok.yml 30 | - requirements.txt 31 | copy: 32 | src: "{{ item }}" 33 | dest: "{{ pollofeed_dir }}/{{ item }}" 34 | 35 | - name: Copy motion.conf 36 | tags: copy_motion_conf 37 | become: yes 38 | copy: 39 | src: motion.conf 40 | dest: /etc/motion/motion.conf 41 | 42 | 43 | 44 | - name: Install python requirements 45 | tags: install_requirements 46 | pip: 47 | requirements: "{{ pollofeed_dir }}/requirements.txt" 48 | 49 | 50 | ### NPM GLOBAL PACKAGES 51 | 52 | - name: Update npm global version 6.9.0 53 | become: yes 54 | tags: npm_1 55 | npm: 56 | global: yes 57 | name: npm 58 | version: "6.9.0" 59 | 60 | 61 | 62 | - name: Install ngrok globally 63 | become: yes 64 | tags: install_ngrok 65 | npm: 66 | global: yes 67 | name: ngrok 68 | unsafe_perm: yes 69 | 70 | 71 | ### SERVICES 72 | 73 | - name: Set variable - services 74 | tags: set_facts 75 | set_fact: 76 | services: 77 | - ngrok 78 | - pollofeed 79 | - motion 80 | 81 | - name: Copy service files 82 | become: yes 83 | tags: copy_services 84 | with_items: "{{ services }}" 85 | copy: 86 | src: "{{ item }}.service" 87 | dest: "/etc/systemd/system/{{ item }}.service" 88 | 89 | 90 | - name: Enable and start services 91 | with_items: "{{services}}" 92 | tags: enable_services 93 | become: yes 94 | systemd: 95 | name: "{{ item }}" 96 | enabled: yes 97 | state: started 98 | 99 | - name: reload systemd 100 | tags: reload_systemd 101 | systemd: 102 | daemon_reload: yes 103 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | const createError = require('http-errors') 2 | const express = require('express') 3 | const path = require('path') 4 | const logger = require('morgan') 5 | const csrf = require('csurf') 6 | const fs = require('fs') 7 | const helmet = require('helmet') 8 | const compression = require('compression') 9 | const bodyParser = require('body-parser') 10 | const cookieParser = require('cookie-parser'); 11 | const csrfProtection = csrf({cookie: true}); 12 | const invoicesRouter = require('./invoices/router') 13 | const app = express() 14 | 15 | app.set('view engine', 'pug') 16 | app.set('host', process.env.HOST || '0.0.0.0') 17 | app.set("url", process.env.URL || "https://pollofeed.com") 18 | app.set('port', process.env.PORT) 19 | app.set('title', process.env.TITLE || 'PolloFeed') 20 | app.set('theme', process.env.THEME || 'lumen') 21 | app.set('views', path.join(__dirname, '..', 'views')) 22 | app.enable('trust proxy') 23 | // app.set('trust proxy', process.env.PROXIED || 'loopback') 24 | app.set('show_bolt11', !!process.env.SHOW_BOLT11) 25 | 26 | app.use(cookieParser()) 27 | app.use(helmet()) 28 | app.use(compression()) 29 | app.use(bodyParser.urlencoded({extended: false})) 30 | app.use(bodyParser.json({strict: true})) 31 | app.use(logger('dev')) 32 | app.use('/invoice', invoicesRouter) 33 | app.use(express.static(path.join(__dirname, "..", 'dist'))) 34 | app.get('/', csrfProtection, (req, res) => res.render("index", {req})) 35 | app.get('/about', (_, res) => res.render("about")) 36 | 37 | 38 | app.get("/health", (_, res) => res.send("OK")) 39 | 40 | // use pre-compiled browserify bundle when available, or live-compile for dev 41 | const compiledBundle = path.join(__dirname, "..", "dist", 'client.bundle.min.js') 42 | if (fs.existsSync(compiledBundle)) app.get('/script.js', (req, res) => res.sendFile(compiledBundle)) 43 | else app.get('/script.js', require('browserify-middleware')(require.resolve('./client'))) 44 | app.use('/bootswatch', require('express').static(path.resolve(require.resolve('bootswatch/package'), '..', 'dist'))) 45 | 46 | 47 | // catch 404 and forward to error handler 48 | app.use(function (req, res, next) { 49 | next(createError(404)) 50 | }) 51 | 52 | 53 | // error handler 54 | app.use(function (err, req, res) { 55 | res.status(err.status || 500).send(err) 56 | }) 57 | 58 | 59 | module.exports = app 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/bin/unresponsiveCheck.ts: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | require('dotenv').config({path: path.join(__dirname, '..', "..", '.env.development')}) 3 | const orderDao = require('../invoices/dao') 4 | const dbconnect = require('./dbconnect') 5 | const {sendFromDefaultUser} = require('./email') 6 | 7 | const MS_PER_MINUTE = 60000; 8 | 9 | const lnCharge = require("lightning-charge-client")(process.env.CHARGE_URL, process.env.CHARGE_TOKEN) 10 | 11 | 12 | async function main() { 13 | await dbconnect() 14 | const todayOrders = await orderDao.getOrdersByDate() 15 | 16 | const nonAcknowledged = todayOrders 17 | .filter(notAcknoledged) 18 | .filter(notResponsive) 19 | .sort((a, b) => a.paid_at - b.paid_at) 20 | 21 | if (nonAcknowledged.length) { 22 | await notify(nonAcknowledged) 23 | } else { 24 | console.log("responsive") 25 | } 26 | 27 | const lnActive = await lightningClientActive() 28 | if (!lnActive) { 29 | const subject = "Lightning Client unresponsive" 30 | const text = `unresponsive @ ${new Date().toLocaleString()}` 31 | return await sendFromDefaultUser(subject, text) 32 | } else { 33 | console.log("lightning client active") 34 | } 35 | 36 | } 37 | 38 | async function notify(nonAcknowledged) { 39 | const text = `unresponsive orders = ${nonAcknowledged.length}, since = ${nonAcknowledged[0].paid_at.toLocaleString()}` 40 | const subject = "Raspberry pi unresponsive" 41 | return sendFromDefaultUser(subject, text) 42 | 43 | } 44 | 45 | function notAcknoledged(order): boolean { 46 | return order.feed === true && order.acknowledged_at === false 47 | } 48 | 49 | 50 | const now = new Date() 51 | function notResponsive(order, minutes = 5): boolean { 52 | const paidAtPlus5min = new Date(order.paid_at.getTime() + (MS_PER_MINUTE * minutes)) 53 | console.log(paidAtPlus5min.toLocaleString(), now.toLocaleString()) 54 | return paidAtPlus5min < now 55 | } 56 | 57 | async function lightningClientActive() { 58 | return lnCharge.info().then(result => { 59 | return true 60 | }).catch(err => { 61 | console.error(err) 62 | return false 63 | }) 64 | } 65 | 66 | 67 | main() 68 | .then(_ => process.exit(0)) 69 | .catch(e => { 70 | console.log(e) 71 | process.exit(1) 72 | }) 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pollofeed", 3 | "version": "0.1.0", 4 | "description": "Bitcoin Lightning Powered Chicken Feeder", 5 | "repository": "https://github.com/j-chimienti/pollofeed", 6 | "issues": "https://github.com/j-chimienti/pollofeed/issues", 7 | "homepage": "https://pollofeed.com", 8 | "license": "MIT", 9 | "maintainers": [ 10 | "joe chimienti " 11 | ], 12 | "private": false, 13 | "dependencies": { 14 | "@sendgrid/mail": "^6.4.0", 15 | "babel-cli": "^6.26.0", 16 | "babel-plugin-syntax-object-rest-spread": "^6.13.0", 17 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 18 | "babel-polyfill": "^6.26.0", 19 | "babel-preset-env": "^1.6.1", 20 | "babel-watch": "^2.0.7", 21 | "babelify": "^8.0.0", 22 | "body-parser": "^1.18.2", 23 | "bootstrap": "^4.0.0", 24 | "bootswatch": "^4.0.0-beta.3", 25 | "browserify": "^16.5.0", 26 | "browserify-middleware": "^8.0.0", 27 | "compression": "^1.7.4", 28 | "cookie-parser": "^1.4.3", 29 | "csurf": "^1.9.0", 30 | "dotenv": "^8.0.0", 31 | "express": "^4.16.2", 32 | "fmtbtc": "0.0.2", 33 | "helmet": "^3.21.1", 34 | "http-errors": "^1.7.2", 35 | "jquery": "^3.3.1", 36 | "lightning-charge-client": "^0.1.7", 37 | "moment": "^2.24.0", 38 | "mongodb": "^3.2.4", 39 | "morgan": "^1.9.0", 40 | "popper.js": "^1.15.0", 41 | "pug": "^2.0.0-rc.4", 42 | "pugify": "^2.2.0", 43 | "qrcode": "^1.2.0", 44 | "supertest": "^4.0.2", 45 | "tape": "^4.10.1", 46 | "terser": "^4.6.3", 47 | "only": "latest" 48 | }, 49 | "scripts": { 50 | "start": "node src/www.js", 51 | "start:dev": "./dev.sh", 52 | "dev": "./dev.sh", 53 | "build": "./build.sh", 54 | "deploy": "./deploy.sh", 55 | "predeploy": "npm run build", 56 | "test": "node ./node_modules/.bin/tape test/*.js", 57 | "lint:fix": "eslint --fix --ext .jsx,.js src src/server", 58 | "totals": "node src/bin/runTotals.js", 59 | "today": "node src/bin/ordersByDay.js", 60 | "feed": "node src/bin/manfeed.js", 61 | "build::readme": "npx readme-md-generator", 62 | "build::readme::default": "npx readme-md-generator -y" 63 | }, 64 | "browserify": { 65 | "transform": [ 66 | "babelify", 67 | "pugify" 68 | ] 69 | }, 70 | "devDependencies": { 71 | "depcheck": "^0.9.2", 72 | "eslint": "^6.8.0", 73 | "nodemon": "^1.19.0" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/bin/totals.js: -------------------------------------------------------------------------------- 1 | // get total balance of all orders 2 | async function main() { 3 | const orders = await global.db.collection('orders').find().sort({paid_at: -1}).toArray() 4 | const msatoshis = orders.map(o => o.msatoshi) 5 | const satsTotal = msatoshis.reduce((accum, msat) => (parseInt(msat) / 1000) + accum, 0) 6 | const btc = satsTotal / 1e8 7 | const newestOrder = orders[0] 8 | const oldestOrder = orders[orders.length - 1] 9 | const newEstOrderDate = new Date(newestOrder.paid_at) 10 | const oldestOrderDate = new Date(oldestOrder.paid_at) 11 | const days = (newEstOrderDate.getTime() - oldestOrderDate.getTime()) / 86400000 12 | const avgDay = orders.length / days 13 | const byDay = orders.reduce((accum, order) => { 14 | const day = new Date(order.paid_at).toLocaleDateString() 15 | if (!accum[day]) accum[day] = [order] 16 | else accum[day].push(order) 17 | return accum 18 | }, {}) 19 | 20 | 21 | function median(orders) { 22 | const ordersInDay = Object.values(orders).map(o => o.length); 23 | const sorted = ordersInDay.sort((a, b) => a - b); 24 | return sorted[parseInt(sorted.length / 2)] 25 | } 26 | 27 | const med = median(byDay); 28 | let max = {date: null, fed: 0} 29 | let min = {date: null, fed: Infinity} 30 | Object.values(byDay).forEach(day => { 31 | const date = new Date(day[0].paid_at) 32 | const data = {date, fed: day.length} 33 | if (day.length > max.fed) max = data 34 | if (day.length < min.fed) min = data 35 | 36 | }) 37 | console.log("\n") 38 | console.log("### Orders") 39 | console.table({ 40 | days: parseInt(days), 41 | first: oldestOrderDate.toLocaleString(), 42 | newest: newEstOrderDate.toLocaleString(), 43 | max: `${max.fed}, ${max.date.toLocaleDateString()}`, 44 | min: `${min.fed}, ${min.date.toLocaleDateString()}`, 45 | median: med, 46 | avg: parseInt(avgDay) 47 | 48 | }) 49 | console.log("\n") 50 | console.log("### TOTALS") 51 | console.table({ 52 | orders: orders.length.toLocaleString(), 53 | sats: satsTotal.toLocaleString(), 54 | btc: btc.toPrecision(8) 55 | } 56 | ) 57 | return { 58 | oldestOrderDate, 59 | newEstOrderDate, 60 | days, 61 | min, // max feeding day 62 | max, // min feedin day 63 | avgDay, 64 | satsTotal, 65 | btc, 66 | } 67 | 68 | 69 | } 70 | 71 | module.exports = main 72 | -------------------------------------------------------------------------------- /src/www.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path') 4 | const dotenv = require('dotenv') 5 | const http = require('http') 6 | 7 | 8 | dotenv.config({path: path.join(__dirname, "..", '.env')}) 9 | 10 | 11 | const app = require('./app') 12 | const LnChargeClient = require('lightning-charge-client') 13 | let port = normalizePort(process.env.VIRTUAL_PORT || '4321') 14 | let server = http.createServer(app) 15 | const MongoClient = require('mongodb').MongoClient 16 | 17 | 18 | global.lnCharge = LnChargeClient(process.env.CHARGE_URL, process.env.CHARGE_TOKEN) 19 | 20 | 21 | main() 22 | async function main() { 23 | const mongo = await MongoClient.connect(process.env.POLLOFEED_MONGO_URI, {poolSize: 5, useNewUrlParser: true}).catch(err => { 24 | console.error('error connecting to server @', process.env.POLLOFEED_MONGO_URI) 25 | console.error(err) 26 | process.exit(1) 27 | }) 28 | 29 | const dbName = process.env.MONGO_DB_NAME 30 | global.db = mongo.db(dbName) 31 | console.log('Connected to db') 32 | 33 | server.listen(port, app.get('host')) 34 | server.on('error', onError) 35 | server.on('listening', onListening) 36 | 37 | await Promise.all([ 38 | 39 | global.db.collection("orders").createIndex({ 40 | id: 1, 41 | }, {unique: true}), 42 | global.db.collection('orders').createIndex({ 43 | feed: 1 44 | }), 45 | global.db.collection('orders').createIndex({ 46 | paid_at: 1 47 | }), 48 | ]) 49 | 50 | 51 | 52 | } 53 | 54 | function normalizePort(val) { 55 | const port = parseInt(val, 10) 56 | 57 | if (isNaN(port)) { 58 | // named pipe 59 | return val 60 | } 61 | 62 | if (port >= 0) { 63 | // port number 64 | return port 65 | } 66 | 67 | return false 68 | } 69 | 70 | function onError(error) { 71 | if (error.syscall !== 'listen') { 72 | throw error 73 | } 74 | 75 | const bind = typeof port === 'string' 76 | ? 'Pipe ' + port 77 | : 'Port ' + port 78 | 79 | // handle specific listen errors with friendly messages 80 | switch (error.code) { 81 | case 'EACCES': 82 | console.error(bind + ' requires elevated privileges') 83 | process.exit(1) 84 | break 85 | case 'EADDRINUSE': 86 | console.error(bind + ' is already in use') 87 | process.exit(1) 88 | break 89 | default: 90 | throw error 91 | } 92 | } 93 | 94 | function onListening() { 95 | const addr = server.address() 96 | const bind = typeof addr === 'string' 97 | ? 'pipe ' + addr 98 | : 'port ' + addr.port 99 | console.log('Listening on ' + bind) 100 | app.emit('ready') 101 | } 102 | -------------------------------------------------------------------------------- /src/invoices/router.js: -------------------------------------------------------------------------------- 1 | const {PolloFeedOrder} = require('./models/PolloFeedOrder') 2 | const express = require('express') 3 | const orderDao = require('./dao') 4 | const router = express.Router() 5 | const crypto = require('crypto') 6 | const csrf = require('csurf') 7 | const csrfProtection = csrf({cookie: true}); 8 | const feedCost = require('./feedCost') 9 | const {sat2msat} = require('fmtbtc') 10 | const feedPrice = Math.floor(process.env.FEED_PRICE || 1000) 11 | const only = require('only') 12 | const webhookToken = crypto 13 | .createHmac('sha256', process.env.CHARGE_TOKEN) 14 | .update("pollofeed") 15 | .digest('hex') 16 | const tenMinutes = 600 // seconds 17 | const feedTimes = 1 18 | const publicFields = "id msatoshi status payreq expires_at" 19 | 20 | router.post('/', csrfProtection, async (req, res) => { 21 | const timesFedToday = await orderDao.countOrdersByDate() 22 | const feedSatoshis = feedCost(feedPrice, timesFedToday) 23 | const msatoshi = sat2msat(feedSatoshis) 24 | const inv = await global.lnCharge.invoice({ 25 | msatoshi, 26 | description: 'Feed Chickens @ pollofeed.com', 27 | expiry: tenMinutes, 28 | metadata: {feedTimes}, 29 | webhook: `${process.env.URL}/invoice/webhook/${webhookToken}` 30 | }).catch(err => { 31 | console.error("Invoice error:", err); 32 | return err; 33 | }) 34 | if (!(inv && inv.id && inv.rhash && inv.payreq)) 35 | return res.sendStatus(400) 36 | console.log(`[INVOICE] - ${ inv.id } created`) 37 | return res.json(only(inv, publicFields)) 38 | }) 39 | 40 | router.get('/:invoice/wait', async (req, res) => { 41 | const {invoice} = req.params 42 | if (!(invoice && invoice !== 'undefined')) return res.sendStatus(410) 43 | const orderOpt = await orderDao.findById(invoice) 44 | if (orderOpt) return res.status(200).json(orderOpt) 45 | const invoiceResult = await global.lnCharge.wait(invoice, 60).catch(err => { 46 | console.error(`[INVOICE] Error: ${err}`); 47 | return null 48 | }) 49 | if (invoiceResult === null) return res.sendStatus(402) 50 | const invoiceExpired = invoiceResult === false 51 | if (invoiceExpired) return res.sendStatus(410) 52 | const inv = new PolloFeedOrder(invoiceResult) 53 | await orderDao.insert(inv) 54 | log(inv) 55 | return res.status(201).json(only(inv, publicFields)) 56 | }) 57 | 58 | router.post(`/webhook/${webhookToken}`, async (req, res) => { 59 | const foundOrder = await orderDao.findById(req.body.id) 60 | if (foundOrder) return res.sendStatus(204) 61 | await orderDao.insert(new PolloFeedOrder(req.body)) 62 | return res.sendStatus(201) 63 | }) 64 | 65 | function log(order) { 66 | console.log(`[INVOICE] ${order.status}: { id : ${order.id}, 67 | timeToPay: ${new Date(order.paid_at * 1000) - new Date(order.created_at * 1000)} 68 | , payreq: ${order.payreq}, msatoshi: ${order.msatoshi}`) 69 | } 70 | 71 | 72 | 73 | module.exports = router 74 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | require('babel-polyfill') 2 | 3 | const $ = require('jquery') 4 | , B = require('bootstrap') 5 | , qrcode = require('qrcode') 6 | 7 | const payDialog = require('../views/payment.pug') 8 | , paidDialog = require('../views/success.pug') 9 | 10 | 11 | 12 | const csrf = $('meta[name=csrf]').attr('content') 13 | , show_bolt11 = !!$('meta[name=show-bolt11]').attr('content') 14 | 15 | 16 | $('[data-buy-item]').click(e => { 17 | e.preventDefault() 18 | return pay({}) 19 | }) 20 | 21 | 22 | 23 | 24 | const pay = async data => { 25 | $('[data-buy-item], [data-buy] :input').prop('disabled', true) 26 | 27 | try { 28 | const inv = await $.post('invoice', Object.assign({}, data, {_csrf: csrf}) ) 29 | , qr = await qrcode.toDataURL(`lightning:${ inv.payreq }`.toUpperCase(), { margin: 2, width: 300 }) 30 | , diag = $(payDialog(Object.assign({}, inv, { qr, show_bolt11 }))).modal() 31 | 32 | updateExp(diag.find('[data-countdown-to]')) 33 | 34 | const unlisten = listen(inv.id, paid => (diag.modal('hide'), paid && success())) 35 | diag.on('hidden.bs.modal', unlisten) 36 | setTimeout(() => { 37 | const $copyPayReq = document.getElementById('copyPayReq') 38 | $copyPayReq.addEventListener('click', e => { 39 | e.preventDefault() 40 | const $payReq = document.getElementById('payreq') 41 | $payReq.select() 42 | document.execCommand('copy') 43 | $copyPayReq.classList.remove("btn-outline-dark") 44 | $copyPayReq.classList.add("btn-outline-success") 45 | const $icon = $copyPayReq.firstElementChild 46 | $icon.classList.remove('text-white') 47 | $icon.classList.add("text-success") 48 | setTimeout(() => { 49 | $copyPayReq.classList.remove("btn-outline-success") 50 | $copyPayReq.classList.add("btn-outline-dark") 51 | $icon.classList.remove('text-success') 52 | $icon.classList.add("text-white") 53 | }, 2000) 54 | }) 55 | }, 500) 56 | } 57 | finally { $(':disabled').attr('disabled', false) } 58 | 59 | } 60 | 61 | const listen = (invid, cb) => { 62 | let retry = _ => listen(invid, cb) 63 | const req = $.get(`invoice/${ invid }/wait`) 64 | 65 | req.then(_ => cb(true)) 66 | .catch(err => 67 | err.status === 402 ? retry() // long polling timed out, invoice is still payable 68 | : err.status === 410 ? cb(false) // invoice expired and can no longer be paid 69 | : err.statusText === 'abort' ? null // user aborted, do nothing 70 | : setTimeout(retry, 10000)) // unknown error, re-poll after delay 71 | 72 | return _ => (retry = _ => null, req.abort()) 73 | } 74 | 75 | const success = _ => { 76 | const diag = $(paidDialog()).modal() 77 | setTimeout(_ => diag.modal('hide'), 2000) 78 | } 79 | 80 | const updateExp = el => { 81 | const left = +el.data('countdown-to') - (Date.now()/1000|0) 82 | if (left > 0) el.text(formatDur(left)) 83 | else el.closest('.modal').modal('hide') 84 | } 85 | 86 | const formatDur = x => { 87 | const h=x/3600|0, m=x%3600/60|0, s=x%60 88 | return ''+(h>0?h+':':'')+(m<10&&h>0?'0':'')+m+':'+(s<10?'0':'')+s 89 | } 90 | 91 | setInterval(_ => 92 | $('[data-countdown-to]').each((_, el) => 93 | updateExp($(el))) 94 | , 1000) 95 | 96 | $(document).on('hidden.bs.modal', '.modal', e => $(e.target).remove()) 97 | -------------------------------------------------------------------------------- /src/bin/email.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 8 | }); 9 | }; 10 | var __generator = (this && this.__generator) || function (thisArg, body) { 11 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 12 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 13 | function verb(n) { return function (v) { return step([n, v]); }; } 14 | function step(op) { 15 | if (f) throw new TypeError("Generator is already executing."); 16 | while (_) try { 17 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 18 | if (y = 0, t) op = [op[0] & 2, t.value]; 19 | switch (op[0]) { 20 | case 0: case 1: t = op; break; 21 | case 4: _.label++; return { value: op[1], done: false }; 22 | case 5: _.label++; y = op[1]; op = [0]; continue; 23 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 24 | default: 25 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 26 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 27 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 28 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 29 | if (t[2]) _.ops.pop(); 30 | _.trys.pop(); continue; 31 | } 32 | op = body.call(thisArg, _); 33 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 34 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 35 | } 36 | }; 37 | Object.defineProperty(exports, "__esModule", { value: true }); 38 | var sgMail = require('@sendgrid/mail'); 39 | sgMail.setApiKey(process.env.SENDGRID_API_KEY); 40 | function send(options) { 41 | return __awaiter(this, void 0, void 0, function () { 42 | return __generator(this, function (_a) { 43 | switch (_a.label) { 44 | case 0: return [4 /*yield*/, sgMail.send(options)]; 45 | case 1: return [2 /*return*/, _a.sent()]; 46 | } 47 | }); 48 | }); 49 | } 50 | function sendFromDefaultUser(subject, text) { 51 | return __awaiter(this, void 0, void 0, function () { 52 | var options; 53 | return __generator(this, function (_a) { 54 | options = { 55 | from: process.env.GMAIL_USER, 56 | to: process.env.GMAIL_USER, 57 | subject: subject, 58 | text: text 59 | }; 60 | return [2 /*return*/, send(options)]; 61 | }); 62 | }); 63 | } 64 | module.exports = { 65 | send: send, 66 | sendFromDefaultUser: sendFromDefaultUser 67 | }; 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | dump 4 | lnCli.test.js 5 | config.yml 6 | bin/*.sh 7 | # dependencies 8 | /node_modules 9 | /.pnp 10 | .pnp.js 11 | */ngrok.yml 12 | # testing 13 | /coverage 14 | hosts 15 | */hosts 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | 31 | .env.* 32 | 33 | .env 34 | 35 | # Byte-compiled / optimized / DLL files 36 | __pycache__/ 37 | *.py[cod] 38 | *$py.class 39 | 40 | # C extensions 41 | *.so 42 | 43 | */config.yml 44 | bin/*.sh 45 | 46 | # Distribution / packaging 47 | .Python 48 | build/ 49 | develop-eggs/ 50 | dist/ 51 | downloads/ 52 | eggs/ 53 | .eggs/ 54 | # lib/ 55 | lib64/ 56 | parts/ 57 | sdist/ 58 | var/ 59 | wheels/ 60 | pip-wheel-metadata/ 61 | share/python-wheels/ 62 | *.egg-info/ 63 | .installed.cfg 64 | *.egg 65 | MANIFEST 66 | 67 | # PyInstaller 68 | # Usually these files are written by a python script from a template 69 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 70 | *.manifest 71 | *.spec 72 | 73 | # Installer logs 74 | pip-log.txt 75 | pip-delete-this-directory.txt 76 | 77 | # Unit test / coverage reports 78 | htmlcov/ 79 | .tox/ 80 | .nox/ 81 | .coverage 82 | .coverage.* 83 | .cache 84 | nosetests.xml 85 | coverage.xml 86 | *.cover 87 | .hypothesis/ 88 | .pytest_cache/ 89 | 90 | # Translations 91 | *.mo 92 | *.pot 93 | 94 | # Django stuff: 95 | *.log 96 | local_settings.py 97 | db.sqlite3 98 | 99 | # Flask stuff: 100 | instance/ 101 | .webassets-cache 102 | 103 | # Scrapy stuff: 104 | .scrapy 105 | 106 | # Sphinx documentation 107 | docs/_build/ 108 | 109 | # PyBuilder 110 | target/ 111 | 112 | # Jupyter Notebook 113 | .ipynb_checkpoints 114 | 115 | # IPython 116 | profile_default/ 117 | ipython_config.py 118 | 119 | # pyenv 120 | .python-version 121 | 122 | # celery beat schedule file 123 | celerybeat-schedule 124 | 125 | # SageMath parsed files 126 | *.sage.py 127 | 128 | # Environments 129 | .env 130 | .venv 131 | env/ 132 | venv/ 133 | ENV/ 134 | env.bak/ 135 | venv.bak/ 136 | 137 | # Spyder project settings 138 | .spyderproject 139 | .spyproject 140 | 141 | # Rope project settings 142 | .ropeproject 143 | 144 | # mkdocs documentation 145 | /site 146 | 147 | # mypy 148 | .mypy_cache/ 149 | .dmypy.json 150 | dmypy.json 151 | 152 | # Pyre type checker 153 | .pyre/ 154 | 155 | 156 | 157 | # NODE 158 | 159 | 160 | # Logs 161 | logs 162 | *.log 163 | npm-debug.log* 164 | yarn-debug.log* 165 | yarn-error.log* 166 | 167 | # Runtime data 168 | pids 169 | *.pid 170 | *.seed 171 | *.pid.lock 172 | 173 | # Directory for instrumented libs generated by jscoverage/JSCover 174 | lib-cov 175 | 176 | # Coverage directory used by tools like istanbul 177 | coverage 178 | 179 | # nyc test coverage 180 | .nyc_output 181 | 182 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 183 | .grunt 184 | 185 | # Bower dependency directory (https://bower.io/) 186 | bower_components 187 | 188 | # node-waf configuration 189 | .lock-wscript 190 | 191 | # Compiled binary addons (https://nodejs.org/api/addons.html) 192 | build/Release 193 | 194 | # Dependency directories 195 | node_modules/ 196 | jspm_packages/ 197 | 198 | # TypeScript v1 declaration files 199 | typings/ 200 | 201 | # Optional npm cache directory 202 | .npm 203 | 204 | # Optional eslint cache 205 | .eslintcache 206 | 207 | # Optional REPL history 208 | .node_repl_history 209 | 210 | # Output of 'npm pack' 211 | *.tgz 212 | 213 | # Yarn Integrity file 214 | .yarn-integrity 215 | 216 | # dotenv environment variables file 217 | .env 218 | .env.test 219 | 220 | # parcel-bundler cache (https://parceljs.org/) 221 | .cache 222 | 223 | # next.js build output 224 | .next 225 | 226 | # nuxt.js build output 227 | .nuxt 228 | 229 | # vuepress build output 230 | .vuepress/dist 231 | 232 | # Serverless directories 233 | .serverless/ 234 | 235 | # FuseBox cache 236 | .fusebox/ 237 | 238 | # DynamoDB Local files 239 | .dynamodb/ 240 | 241 | config.yml 242 | client_secret.json 243 | 244 | ngrok.yml 245 | 246 | 247 | *.retry 248 | -------------------------------------------------------------------------------- /src/bin/unresponsiveCheck.js: -------------------------------------------------------------------------------- 1 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 2 | return new (P || (P = Promise))(function (resolve, reject) { 3 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 4 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 5 | function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } 6 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 7 | }); 8 | }; 9 | var __generator = (this && this.__generator) || function (thisArg, body) { 10 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 11 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 12 | function verb(n) { return function (v) { return step([n, v]); }; } 13 | function step(op) { 14 | if (f) throw new TypeError("Generator is already executing."); 15 | while (_) try { 16 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 17 | if (y = 0, t) op = [op[0] & 2, t.value]; 18 | switch (op[0]) { 19 | case 0: case 1: t = op; break; 20 | case 4: _.label++; return { value: op[1], done: false }; 21 | case 5: _.label++; y = op[1]; op = [0]; continue; 22 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 23 | default: 24 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 25 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 26 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 27 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 28 | if (t[2]) _.ops.pop(); 29 | _.trys.pop(); continue; 30 | } 31 | op = body.call(thisArg, _); 32 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 33 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 34 | } 35 | }; 36 | var path = require('path'); 37 | require('dotenv').config({ path: path.join(__dirname, '..', "..", '.env.development') }); 38 | var orderDao = require('../invoices/dao'); 39 | var dbconnect = require('./dbconnect'); 40 | var sendFromDefaultUser = require('./email').sendFromDefaultUser; 41 | var MS_PER_MINUTE = 60000; 42 | var lnCharge = require("lightning-charge-client")(process.env.CHARGE_URL, process.env.CHARGE_TOKEN); 43 | function main() { 44 | return __awaiter(this, void 0, void 0, function () { 45 | var todayOrders, nonAcknowledged, lnActive, subject, text; 46 | return __generator(this, function (_a) { 47 | switch (_a.label) { 48 | case 0: return [4 /*yield*/, dbconnect()]; 49 | case 1: 50 | _a.sent(); 51 | return [4 /*yield*/, orderDao.getOrdersByDate()]; 52 | case 2: 53 | todayOrders = _a.sent(); 54 | nonAcknowledged = todayOrders 55 | .filter(notAcknoledged) 56 | .filter(notResponsive) 57 | .sort(function (a, b) { return a.paid_at - b.paid_at; }); 58 | if (!nonAcknowledged.length) return [3 /*break*/, 4]; 59 | return [4 /*yield*/, notify(nonAcknowledged)]; 60 | case 3: 61 | _a.sent(); 62 | return [3 /*break*/, 5]; 63 | case 4: 64 | console.log("responsive"); 65 | _a.label = 5; 66 | case 5: return [4 /*yield*/, lightningClientActive()]; 67 | case 6: 68 | lnActive = _a.sent(); 69 | if (!!lnActive) return [3 /*break*/, 8]; 70 | subject = "Lightning Client unresponsive"; 71 | text = "unresponsive @ " + new Date().toLocaleString(); 72 | return [4 /*yield*/, sendFromDefaultUser(subject, text)]; 73 | case 7: return [2 /*return*/, _a.sent()]; 74 | case 8: 75 | console.log("lightning client active"); 76 | _a.label = 9; 77 | case 9: return [2 /*return*/]; 78 | } 79 | }); 80 | }); 81 | } 82 | function notify(nonAcknowledged) { 83 | return __awaiter(this, void 0, void 0, function () { 84 | var text, subject; 85 | return __generator(this, function (_a) { 86 | text = "unresponsive orders = " + nonAcknowledged.length + ", since = " + nonAcknowledged[0].paid_at.toLocaleString(); 87 | subject = "Raspberry pi unresponsive"; 88 | return [2 /*return*/, sendFromDefaultUser(subject, text)]; 89 | }); 90 | }); 91 | } 92 | function notAcknoledged(order) { 93 | return order.feed === true && order.acknowledged_at === false; 94 | } 95 | var now = new Date(); 96 | function notResponsive(order, minutes) { 97 | if (minutes === void 0) { minutes = 5; } 98 | var paidAtPlus5min = new Date(order.paid_at.getTime() + (MS_PER_MINUTE * minutes)); 99 | console.log(paidAtPlus5min.toLocaleString(), now.toLocaleString()); 100 | return paidAtPlus5min < now; 101 | } 102 | function lightningClientActive() { 103 | return __awaiter(this, void 0, void 0, function () { 104 | return __generator(this, function (_a) { 105 | return [2 /*return*/, lnCharge.info().then(function (result) { 106 | return true; 107 | }).catch(function (err) { 108 | console.error(err); 109 | return false; 110 | })]; 111 | }); 112 | }); 113 | } 114 | main() 115 | .then(function (_) { return process.exit(0); }) 116 | .catch(function (e) { 117 | console.log(e); 118 | process.exit(1); 119 | }); 120 | -------------------------------------------------------------------------------- /playbooks/roles/pi/files/motion.conf: -------------------------------------------------------------------------------- 1 | # Rename this distribution example file to motion.conf 2 | # 3 | # This config file was generated by motion 4.0 4 | 5 | 6 | ############################################################ 7 | # Daemon 8 | ############################################################ 9 | 10 | # Start in daemon (background) mode and release terminal (default: off) 11 | daemon on 12 | 13 | # File to store the process ID, also called pid file. (default: not defined) 14 | process_id_file /var/run/motion/motion.pid 15 | 16 | ############################################################ 17 | # Basic Setup Mode 18 | ############################################################ 19 | 20 | # Start in Setup-Mode, daemon disabled. (default: off) 21 | setup_mode off 22 | 23 | 24 | # Use a file to save logs messages, if not defined stderr and syslog is used. (default: not defined) 25 | logfile /var/log/motion/motion.log 26 | 27 | # Level of log messages [1..9] (EMG, ALR, CRT, ERR, WRN, NTC, INF, DBG, ALL). (default: 6 / NTC) 28 | log_level 6 29 | 30 | # Filter to log messages by type (COR, STR, ENC, NET, DBL, EVT, TRK, VID, ALL). (default: ALL) 31 | log_type all 32 | 33 | ########################################################### 34 | # Capture device options 35 | ############################################################ 36 | 37 | # Videodevice to be used for capturing (default /dev/video0) 38 | # for FreeBSD default is /dev/bktr0 39 | videodevice /dev/video0 40 | 41 | # v4l2_palette allows one to choose preferable palette to be use by motion 42 | # to capture from those supported by your videodevice. (default: 17) 43 | # E.g. if your videodevice supports both V4L2_PIX_FMT_SBGGR8 and 44 | # V4L2_PIX_FMT_MJPEG then motion will by default use V4L2_PIX_FMT_MJPEG. 45 | # Setting v4l2_palette to 2 forces motion to use V4L2_PIX_FMT_SBGGR8 46 | # instead. 47 | # 48 | # Values : 49 | # V4L2_PIX_FMT_SN9C10X : 0 'S910' 50 | # V4L2_PIX_FMT_SBGGR16 : 1 'BYR2' 51 | # V4L2_PIX_FMT_SBGGR8 : 2 'BA81' 52 | # V4L2_PIX_FMT_SPCA561 : 3 'S561' 53 | # V4L2_PIX_FMT_SGBRG8 : 4 'GBRG' 54 | # V4L2_PIX_FMT_SGRBG8 : 5 'GRBG' 55 | # V4L2_PIX_FMT_PAC207 : 6 'P207' 56 | # V4L2_PIX_FMT_PJPG : 7 'PJPG' 57 | # V4L2_PIX_FMT_MJPEG : 8 'MJPEG' 58 | # V4L2_PIX_FMT_JPEG : 9 'JPEG' 59 | # V4L2_PIX_FMT_RGB24 : 10 'RGB3' 60 | # V4L2_PIX_FMT_SPCA501 : 11 'S501' 61 | # V4L2_PIX_FMT_SPCA505 : 12 'S505' 62 | # V4L2_PIX_FMT_SPCA508 : 13 'S508' 63 | # V4L2_PIX_FMT_UYVY : 14 'UYVY' 64 | # V4L2_PIX_FMT_YUYV : 15 'YUYV' 65 | # V4L2_PIX_FMT_YUV422P : 16 '422P' 66 | # V4L2_PIX_FMT_YUV420 : 17 'YU12' 67 | # 68 | v4l2_palette 17 69 | 70 | # Tuner device to be used for capturing using tuner as source (default /dev/tuner0) 71 | # This is ONLY used for FreeBSD. Leave it commented out for Linux 72 | ; tunerdevice /dev/tuner0 73 | 74 | # The video input to be used (default: -1) 75 | # Should normally be set to 0 or 1 for video/TV cards, and -1 for USB cameras 76 | # Set to 0 for uvideo(4) on OpenBSD 77 | input -1 78 | 79 | # The video norm to use (only for video capture and TV tuner cards) 80 | # Values: 0 (PAL), 1 (NTSC), 2 (SECAM), 3 (PAL NC no colour). Default: 0 (PAL) 81 | norm 0 82 | 83 | # The frequency to set the tuner to (kHz) (only for TV tuner cards) (default: 0) 84 | frequency 0 85 | 86 | # Override the power line frequency for the webcam. (normally not necessary) 87 | # Values: 88 | # -1 : Do not modify device setting 89 | # 0 : Power line frequency Disabled 90 | # 1 : 50hz 91 | # 2 : 60hz 92 | # 3 : Auto 93 | power_line_frequency -1 94 | 95 | # Rotate image this number of degrees. The rotation affects all saved images as 96 | # well as movies. Valid values: 0 (default = no rotation), 90, 180 and 270. 97 | rotate 0 98 | 99 | # Image width (pixels). Valid range: Camera dependent, default: 352 100 | width 640 # 320 101 | 102 | # Image height (pixels). Valid range: Camera dependent, default: 288 103 | height 480 # 240 104 | 105 | # Maximum number of frames to be captured per second. 106 | # Valid range: 2-100. Default: 100 (almost no limit). 107 | framerate 100 108 | 109 | # Minimum time in seconds between capturing picture frames from the camera. 110 | # Default: 0 = disabled - the capture rate is given by the camera framerate. 111 | # This option is used when you want to capture images at a rate lower than 2 per second. 112 | minimum_frame_time 0 113 | 114 | # URL to use if you are using a network camera, size will be autodetected (incl http:// ftp:// mjpg:// rtsp:// mjpeg:// or file:///) 115 | # Must be a URL that returns single jpeg pictures or a raw mjpeg stream. A trailing slash may be required for some cameras. 116 | # Default: Not defined 117 | ; netcam_url value 118 | 119 | # Username and password for network camera (only if required). Default: not defined 120 | # Syntax is user:password 121 | ; netcam_userpass value 122 | 123 | # The setting for keep-alive of network socket, should improve performance on compatible net cameras. 124 | # off: The historical implementation using HTTP/1.0, closing the socket after each http request. 125 | # force: Use HTTP/1.0 requests with keep alive header to reuse the same connection. 126 | # on: Use HTTP/1.1 requests that support keep alive as default. 127 | # Default: off 128 | netcam_keepalive off 129 | 130 | # URL to use for a netcam proxy server, if required, e.g. "http://myproxy". 131 | # If a port number other than 80 is needed, use "http://myproxy:1234". 132 | # Default: not defined 133 | ; netcam_proxy value 134 | 135 | # Set less strict jpeg checks for network cameras with a poor/buggy firmware. 136 | # Default: off 137 | netcam_tolerant_check off 138 | 139 | # RTSP connection uses TCP to communicate to the camera. Can prevent image corruption. 140 | # Default: on 141 | rtsp_uses_tcp on 142 | 143 | # Name of camera to use if you are using a camera accessed through OpenMax/MMAL 144 | # Default: Not defined 145 | ; mmalcam_name vc.ril.camera 146 | 147 | # Camera control parameters (see raspivid/raspistill tool documentation) 148 | # Default: Not defined 149 | ; mmalcam_control_params -hf 150 | 151 | # Let motion regulate the brightness of a video device (default: off). 152 | # The auto_brightness feature uses the brightness option as its target value. 153 | # If brightness is zero auto_brightness will adjust to average brightness value 128. 154 | # Only recommended for cameras without auto brightness 155 | auto_brightness off 156 | 157 | # Set the initial brightness of a video device. 158 | # If auto_brightness is enabled, this value defines the average brightness level 159 | # which Motion will try and adjust to. 160 | # Valid range 0-255, default 0 = disabled 161 | brightness 0 162 | 163 | # Set the contrast of a video device. 164 | # Valid range 0-255, default 0 = disabled 165 | contrast 0 166 | 167 | # Set the saturation of a video device. 168 | # Valid range 0-255, default 0 = disabled 169 | saturation 0 170 | 171 | # Set the hue of a video device (NTSC feature). 172 | # Valid range 0-255, default 0 = disabled 173 | hue 0 174 | 175 | 176 | ############################################################ 177 | # Round Robin (multiple inputs on same video device name) 178 | ############################################################ 179 | 180 | # Number of frames to capture in each roundrobin step (default: 1) 181 | roundrobin_frames 1 182 | 183 | # Number of frames to skip before each roundrobin step (default: 1) 184 | roundrobin_skip 1 185 | 186 | # Try to filter out noise generated by roundrobin (default: off) 187 | switchfilter off 188 | 189 | 190 | ############################################################ 191 | # Motion Detection Settings: 192 | ############################################################ 193 | 194 | # Threshold for number of changed pixels in an image that 195 | # triggers motion detection (default: 1500) 196 | threshold 1500 197 | 198 | # Automatically tune the threshold down if possible (default: off) 199 | threshold_tune off 200 | 201 | # Noise threshold for the motion detection (default: 32) 202 | noise_level 32 203 | 204 | # Automatically tune the noise threshold (default: on) 205 | noise_tune on 206 | 207 | # Despeckle motion image using (e)rode or (d)ilate or (l)abel (Default: not defined) 208 | # Recommended value is EedDl. Any combination (and number of) of E, e, d, and D is valid. 209 | # (l)abeling must only be used once and the 'l' must be the last letter. 210 | # Comment out to disable 211 | despeckle_filter EedDl 212 | 213 | # Detect motion in predefined areas (1 - 9). Areas are numbered like that: 1 2 3 214 | # A script (on_area_detected) is started immediately when motion is 4 5 6 215 | # detected in one of the given areas, but only once during an event. 7 8 9 216 | # One or more areas can be specified with this option. Take care: This option 217 | # does NOT restrict detection to these areas! (Default: not defined) 218 | ; area_detect value 219 | 220 | # PGM file to use as a sensitivity mask. 221 | # Full path name to. (Default: not defined) 222 | ; mask_file value 223 | 224 | # Dynamically create a mask file during operation (default: 0) 225 | # Adjust speed of mask changes from 0 (off) to 10 (fast) 226 | smart_mask_speed 0 227 | 228 | # Ignore sudden massive light intensity changes given as a percentage of the picture 229 | # area that changed intensity. Valid range: 0 - 100 , default: 0 = disabled 230 | lightswitch 0 231 | 232 | # Picture frames must contain motion at least the specified number of frames 233 | # in a row before they are detected as true motion. At the default of 1, all 234 | # motion is detected. Valid range: 1 to thousands, recommended 1-5 235 | minimum_motion_frames 1 236 | 237 | # Specifies the number of pre-captured (buffered) pictures from before motion 238 | # was detected that will be output at motion detection. 239 | # Recommended range: 0 to 5 (default: 0) 240 | # Do not use large values! Large values will cause Motion to skip video frames and 241 | # cause unsmooth movies. To smooth movies use larger values of post_capture instead. 242 | pre_capture 0 243 | 244 | # Number of frames to capture after motion is no longer detected (default: 0) 245 | post_capture 0 246 | 247 | # Event Gap is the seconds of no motion detection that triggers the end of an event. 248 | # An event is defined as a series of motion images taken within a short timeframe. 249 | # Recommended value is 60 seconds (Default). The value -1 is allowed and disables 250 | # events causing all Motion to be written to one single movie file and no pre_capture. 251 | # If set to 0, motion is running in gapless mode. Movies don't have gaps anymore. An 252 | # event ends right after no more motion is detected and post_capture is over. 253 | event_gap 60 254 | 255 | # Maximum length in seconds of a movie 256 | # When value is exceeded a new movie file is created. (Default: 0 = infinite) 257 | max_movie_time 0 258 | 259 | # Always save images even if there was no motion (default: off) 260 | emulate_motion off 261 | 262 | 263 | ############################################################ 264 | # Image File Output 265 | ############################################################ 266 | 267 | # Output 'normal' pictures when motion is detected (default: on) 268 | # Valid values: on, off, first, best, center 269 | # When set to 'first', only the first picture of an event is saved. 270 | # Picture with most motion of an event is saved when set to 'best'. 271 | # Picture with motion nearest center of picture is saved when set to 'center'. 272 | # Can be used as preview shot for the corresponding movie. 273 | output_pictures off 274 | 275 | # Output pictures with only the pixels moving object (ghost images) (default: off) 276 | output_debug_pictures off 277 | 278 | # The quality (in percent) to be used by the jpeg compression (default: 75) 279 | quality 75 280 | 281 | # Type of output images 282 | # Valid values: jpeg, ppm (default: jpeg) 283 | picture_type jpeg 284 | 285 | ############################################################ 286 | # FFMPEG related options 287 | # Film (movies) file output, and deinterlacing of the video input 288 | # The options movie_filename and timelapse_filename are also used 289 | # by the ffmpeg feature 290 | ############################################################ 291 | 292 | # Use ffmpeg to encode movies in realtime (default: off) 293 | ffmpeg_output_movies on 294 | 295 | # Use ffmpeg to make movies with only the pixels moving 296 | # object (ghost images) (default: off) 297 | ffmpeg_output_debug_movies off 298 | 299 | # Use ffmpeg to encode a timelapse movie 300 | # Default value 0 = off - else save frame every Nth second 301 | ffmpeg_timelapse 0 302 | 303 | # The file rollover mode of the timelapse video 304 | # Valid values: hourly, daily (default), weekly-sunday, weekly-monday, monthly, manual 305 | ffmpeg_timelapse_mode daily 306 | 307 | # Bitrate to be used by the ffmpeg encoder (default: 400000) 308 | # This option is ignored if ffmpeg_variable_bitrate is not 0 (disabled) 309 | ffmpeg_bps 400000 310 | 311 | # Enables and defines variable bitrate for the ffmpeg encoder. 312 | # ffmpeg_bps is ignored if variable bitrate is enabled. 313 | # Valid values: 0 (default) = fixed bitrate defined by ffmpeg_bps, 314 | # or the range 1 - 100 where 1 means worst quality and 100 is best. 315 | ffmpeg_variable_bitrate 0 316 | 317 | # Codec to used by ffmpeg for the video compression. 318 | # Timelapse videos have two options. 319 | # mpg - Creates mpg file with mpeg-2 encoding. 320 | # If motion is shutdown and restarted, new pics will be appended 321 | # to any previously created file with name indicated for timelapse. 322 | # mpeg4 - Creates avi file with the default encoding. 323 | # If motion is shutdown and restarted, new pics will create a 324 | # new file with the name indicated for timelapse. 325 | # Supported formats are: 326 | # mpeg4 or msmpeg4 - gives you files with extension .avi 327 | # msmpeg4 is recommended for use with Windows Media Player because 328 | # it requires no installation of codec on the Windows client. 329 | # swf - gives you a flash film with extension .swf 330 | # flv - gives you a flash video with extension .flv 331 | # ffv1 - FF video codec 1 for Lossless Encoding 332 | # mov - QuickTime 333 | # mp4 - MPEG-4 Part 14 H264 encoding 334 | # mkv - Matroska H264 encoding 335 | # hevc - H.265 / HEVC (High Efficiency Video Coding) 336 | ffmpeg_video_codec mpeg4 337 | 338 | # When creating videos, should frames be duplicated in order 339 | # to keep up with the requested frames per second 340 | # (default: true) 341 | ffmpeg_duplicate_frames true 342 | 343 | ############################################################ 344 | # SDL Window 345 | ############################################################ 346 | 347 | # Number of motion thread to show in SDL Window (default: 0 = disabled) 348 | #sdl_threadnr 0 349 | 350 | ############################################################ 351 | # External pipe to video encoder 352 | # Replacement for FFMPEG builtin encoder for ffmpeg_output_movies only. 353 | # The options movie_filename and timelapse_filename are also used 354 | # by the ffmpeg feature 355 | ############################################################# 356 | 357 | # Bool to enable or disable extpipe (default: off) 358 | use_extpipe off 359 | 360 | # External program (full path and opts) to pipe raw video to 361 | # Generally, use '-' for STDIN... 362 | ;extpipe mencoder -demuxer rawvideo -rawvideo w=%w:h=%h:i420 -ovc x264 -x264encopts bframes=4:frameref=1:subq=1:scenecut=-1:nob_adapt:threads=1:keyint=1000:8x8dct:vbv_bufsize=4000:crf=24:partitions=i8x8,i4x4:vbv_maxrate=800:no-chroma-me -vf denoise3d=16:12:48:4,pp=lb -of avi -o %f.avi - -fps %fps 363 | ;extpipe x264 - --input-res %wx%h --fps %fps --bitrate 2000 --preset ultrafast --quiet -o %f.mp4 364 | ;extpipe mencoder -demuxer rawvideo -rawvideo w=%w:h=%h:fps=%fps -ovc x264 -x264encopts preset=ultrafast -of lavf -o %f.mp4 - -fps %fps 365 | ;extpipe ffmpeg -y -f rawvideo -pix_fmt yuv420p -video_size %wx%h -framerate %fps -i pipe:0 -vcodec libx264 -preset ultrafast -f mp4 %f.mp4 366 | 367 | 368 | ############################################################ 369 | # Snapshots (Traditional Periodic Webcam File Output) 370 | ############################################################ 371 | 372 | # Make automated snapshot every N seconds (default: 0 = disabled) 373 | snapshot_interval 0 374 | 375 | 376 | ############################################################ 377 | # Text Display 378 | # %Y = year, %m = month, %d = date, 379 | # %H = hour, %M = minute, %S = second, %T = HH:MM:SS, 380 | # %v = event, %q = frame number, %t = camera id number, 381 | # %D = changed pixels, %N = noise level, \n = new line, 382 | # %i and %J = width and height of motion area, 383 | # %K and %L = X and Y coordinates of motion center 384 | # %C = value defined by text_event - do not use with text_event! 385 | # You can put quotation marks around the text to allow 386 | # leading spaces 387 | ############################################################ 388 | 389 | # Locate and draw a box around the moving object. 390 | # Valid values: on, off, preview (default: off) 391 | # Set to 'preview' will only draw a box in preview_shot pictures. 392 | locate_motion_mode off 393 | 394 | # Set the look and style of the locate box if enabled. 395 | # Valid values: box, redbox, cross, redcross (default: box) 396 | # Set to 'box' will draw the traditional box. 397 | # Set to 'redbox' will draw a red box. 398 | # Set to 'cross' will draw a little cross to mark center. 399 | # Set to 'redcross' will draw a little red cross to mark center. 400 | locate_motion_style box 401 | 402 | # Draws the timestamp using same options as C function strftime(3) 403 | # Default: %Y-%m-%d\n%T = date in ISO format and time in 24 hour clock 404 | # Text is placed in lower right corner 405 | text_right %Y-%m-%d\n%T-%q 406 | 407 | # Draw a user defined text on the images using same options as C function strftime(3) 408 | # Default: Not defined = no text 409 | # Text is placed in lower left corner 410 | ; text_left CAMERA %t 411 | 412 | # Draw the number of changed pixed on the images (default: off) 413 | # Will normally be set to off except when you setup and adjust the motion settings 414 | # Text is placed in upper right corner 415 | text_changes off 416 | 417 | # This option defines the value of the special event conversion specifier %C 418 | # You can use any conversion specifier in this option except %C. Date and time 419 | # values are from the timestamp of the first image in the current event. 420 | # Default: %Y%m%d%H%M%S 421 | # The idea is that %C can be used filenames and text_left/right for creating 422 | # a unique identifier for each event. 423 | text_event %Y%m%d%H%M%S 424 | 425 | # Draw characters at twice normal size on images. (default: off) 426 | text_double off 427 | 428 | 429 | # Text to include in a JPEG EXIF comment 430 | # May be any text, including conversion specifiers. 431 | # The EXIF timestamp is included independent of this text. 432 | ;exif_text %i%J/%K%L 433 | 434 | ############################################################ 435 | # Target Directories and filenames For Images And Films 436 | # For the options snapshot_, picture_, movie_ and timelapse_filename 437 | # you can use conversion specifiers 438 | # %Y = year, %m = month, %d = date, 439 | # %H = hour, %M = minute, %S = second, 440 | # %v = event, %q = frame number, %t = camera id number, 441 | # %D = changed pixels, %N = noise level, 442 | # %i and %J = width and height of motion area, 443 | # %K and %L = X and Y coordinates of motion center 444 | # %C = value defined by text_event 445 | # Quotation marks round string are allowed. 446 | ############################################################ 447 | 448 | # Target base directory for pictures and films 449 | # Recommended to use absolute path. (Default: current working directory) 450 | target_dir /var/lib/motion 451 | 452 | # File path for snapshots (jpeg or ppm) relative to target_dir 453 | # Default: %v-%Y%m%d%H%M%S-snapshot 454 | # Default value is equivalent to legacy oldlayout option 455 | # For Motion 3.0 compatible mode choose: %Y/%m/%d/%H/%M/%S-snapshot 456 | # File extension .jpg or .ppm is automatically added so do not include this. 457 | # Note: A symbolic link called lastsnap.jpg created in the target_dir will always 458 | # point to the latest snapshot, unless snapshot_filename is exactly 'lastsnap' 459 | snapshot_filename %v-%Y%m%d%H%M%S-snapshot 460 | 461 | # File path for motion triggered images (jpeg or ppm) relative to target_dir 462 | # Default: %v-%Y%m%d%H%M%S-%q 463 | # Default value is equivalent to legacy oldlayout option 464 | # For Motion 3.0 compatible mode choose: %Y/%m/%d/%H/%M/%S-%q 465 | # File extension .jpg or .ppm is automatically added so do not include this 466 | # Set to 'preview' together with best-preview feature enables special naming 467 | # convention for preview shots. See motion guide for details 468 | picture_filename %v-%Y%m%d%H%M%S-%q 469 | 470 | # File path for motion triggered ffmpeg films (movies) relative to target_dir 471 | # Default: %v-%Y%m%d%H%M%S 472 | # File extensions(.mpg .avi) are automatically added so do not include them 473 | movie_filename %v-%Y%m%d%H%M%S 474 | 475 | # File path for timelapse movies relative to target_dir 476 | # Default: %Y%m%d-timelapse 477 | # File extensions(.mpg .avi) are automatically added so do not include them 478 | timelapse_filename %Y%m%d-timelapse 479 | 480 | ############################################################ 481 | # Global Network Options 482 | ############################################################ 483 | # Enable IPv6 (default: off) 484 | ipv6_enabled off 485 | 486 | ############################################################ 487 | # Live Stream Server 488 | ############################################################ 489 | 490 | # The mini-http server listens to this port for requests (default: 0 = disabled) 491 | stream_port 8081 492 | 493 | # Quality of the jpeg (in percent) images produced (default: 50) 494 | stream_quality 50 495 | 496 | # Output frames at 1 fps when no motion is detected and increase to the 497 | # rate given by stream_maxrate when motion is detected (default: off) 498 | stream_motion off 499 | 500 | # Maximum framerate for stream streams (default: 1) 501 | stream_maxrate 1 502 | 503 | # Restrict stream connections to localhost only (default: on) 504 | stream_localhost on 505 | 506 | # Limits the number of images per connection (default: 0 = unlimited) 507 | # Number can be defined by multiplying actual stream rate by desired number of seconds 508 | # Actual stream rate is the smallest of the numbers framerate and stream_maxrate 509 | stream_limit 0 510 | 511 | # Set the authentication method (default: 0) 512 | # 0 = disabled 513 | # 1 = Basic authentication 514 | # 2 = MD5 digest (the safer authentication) 515 | stream_auth_method 0 516 | 517 | # Authentication for the stream. Syntax username:password 518 | # Default: not defined (Disabled) 519 | ; stream_authentication username:password 520 | 521 | # Percentage to scale the stream image for preview 522 | # Default: 25 523 | ; stream_preview_scale 25 524 | 525 | # Have stream preview image start on a new line 526 | # Default: no 527 | ; stream_preview_newline no 528 | 529 | ############################################################ 530 | # HTTP Based Control 531 | ############################################################ 532 | 533 | # TCP/IP port for the http server to listen on (default: 0 = disabled) 534 | webcontrol_port 8080 535 | 536 | # Restrict control connections to localhost only (default: on) 537 | webcontrol_localhost on 538 | 539 | # Output for http server, select off to choose raw text plain (default: on) 540 | webcontrol_html_output on 541 | 542 | # Authentication for the http based control. Syntax username:password 543 | # Default: not defined (Disabled) 544 | ; webcontrol_authentication username:password 545 | 546 | 547 | ############################################################ 548 | # Tracking (Pan/Tilt) 549 | ############################################################# 550 | 551 | # Type of tracker (0=none (default), 1=stepper, 2=iomojo, 3=pwc, 4=generic, 5=uvcvideo, 6=servo) 552 | # The generic type enables the definition of motion center and motion size to 553 | # be used with the conversion specifiers for options like on_motion_detected 554 | track_type 0 555 | 556 | # Enable auto tracking (default: off) 557 | track_auto off 558 | 559 | # Serial port of motor (default: none) 560 | ;track_port /dev/ttyS0 561 | 562 | # Motor number for x-axis (default: 0) 563 | ;track_motorx 0 564 | 565 | # Set motorx reverse (default: 0) 566 | ;track_motorx_reverse 0 567 | 568 | # Motor number for y-axis (default: 0) 569 | ;track_motory 1 570 | 571 | # Set motory reverse (default: 0) 572 | ;track_motory_reverse 0 573 | 574 | # Maximum value on x-axis (default: 0) 575 | ;track_maxx 200 576 | 577 | # Minimum value on x-axis (default: 0) 578 | ;track_minx 50 579 | 580 | # Maximum value on y-axis (default: 0) 581 | ;track_maxy 200 582 | 583 | # Minimum value on y-axis (default: 0) 584 | ;track_miny 50 585 | 586 | # Center value on x-axis (default: 0) 587 | ;track_homex 128 588 | 589 | # Center value on y-axis (default: 0) 590 | ;track_homey 128 591 | 592 | # ID of an iomojo camera if used (default: 0) 593 | track_iomojo_id 0 594 | 595 | # Angle in degrees the camera moves per step on the X-axis 596 | # with auto-track (default: 10) 597 | # Currently only used with pwc type cameras 598 | track_step_angle_x 10 599 | 600 | # Angle in degrees the camera moves per step on the Y-axis 601 | # with auto-track (default: 10) 602 | # Currently only used with pwc type cameras 603 | track_step_angle_y 10 604 | 605 | # Delay to wait for after tracking movement as number 606 | # of picture frames (default: 10) 607 | track_move_wait 10 608 | 609 | # Speed to set the motor to (stepper motor option) (default: 255) 610 | track_speed 255 611 | 612 | # Number of steps to make (stepper motor option) (default: 40) 613 | track_stepsize 40 614 | 615 | 616 | ############################################################ 617 | # External Commands, Warnings and Logging: 618 | # You can use conversion specifiers for the on_xxxx commands 619 | # %Y = year, %m = month, %d = date, 620 | # %H = hour, %M = minute, %S = second, 621 | # %v = event, %q = frame number, %t = camera id number, 622 | # %D = changed pixels, %N = noise level, 623 | # %i and %J = width and height of motion area, 624 | # %K and %L = X and Y coordinates of motion center 625 | # %C = value defined by text_event 626 | # %f = filename with full path 627 | # %n = number indicating filetype 628 | # Both %f and %n are only defined for on_picture_save, 629 | # on_movie_start and on_movie_end 630 | # Quotation marks round string are allowed. 631 | ############################################################ 632 | 633 | # Do not sound beeps when detecting motion (default: on) 634 | # Note: Motion never beeps when running in daemon mode. 635 | quiet on 636 | 637 | # Command to be executed when an event starts. (default: none) 638 | # An event starts at first motion detected after a period of no motion defined by event_gap 639 | ; on_event_start value 640 | 641 | # Command to be executed when an event ends after a period of no motion 642 | # (default: none). The period of no motion is defined by option event_gap. 643 | ; on_event_end value 644 | 645 | # Command to be executed when a picture (.ppm|.jpg) is saved (default: none) 646 | # To give the filename as an argument to a command append it with %f 647 | ; on_picture_save value 648 | 649 | # Command to be executed when a motion frame is detected (default: none) 650 | ; on_motion_detected value 651 | 652 | # Command to be executed when motion in a predefined area is detected 653 | # Check option 'area_detect'. (default: none) 654 | ; on_area_detected value 655 | 656 | # Command to be executed when a movie file (.mpg|.avi) is created. (default: none) 657 | # To give the filename as an argument to a command append it with %f 658 | ; on_movie_start value 659 | 660 | # Command to be executed when a movie file (.mpg|.avi) is closed. (default: none) 661 | # To give the filename as an argument to a command append it with %f 662 | ; on_movie_end value 663 | 664 | # Command to be executed when a camera can't be opened or if it is lost 665 | # NOTE: There is situations when motion don't detect a lost camera! 666 | # It depends on the driver, some drivers dosn't detect a lost camera at all 667 | # Some hangs the motion thread. Some even hangs the PC! (default: none) 668 | ; on_camera_lost value 669 | 670 | ##################################################################### 671 | # Common Options for database features. 672 | # Options require database options to be active also. 673 | ##################################################################### 674 | 675 | # Log to the database when creating motion triggered picture file (default: on) 676 | ; sql_log_picture on 677 | 678 | # Log to the database when creating a snapshot image file (default: on) 679 | ; sql_log_snapshot on 680 | 681 | # Log to the database when creating motion triggered movie file (default: off) 682 | ; sql_log_movie off 683 | 684 | # Log to the database when creating timelapse movies file (default: off) 685 | ; sql_log_timelapse off 686 | 687 | # SQL query string that is sent to the database 688 | # Use same conversion specifiers has for text features 689 | # Additional special conversion specifiers are 690 | # %n = the number representing the file_type 691 | # %f = filename with full path 692 | # Default value: 693 | # Create tables : 694 | ## 695 | # Mysql 696 | # CREATE TABLE security (camera int, filename char(80) not null, frame int, file_type int, time_stamp timestamp(14), event_time_stamp timestamp(14)); 697 | # 698 | # Postgresql 699 | # CREATE TABLE security (camera int, filename char(80) not null, frame int, file_type int, time_stamp timestamp without time zone, event_time_stamp timestamp without time zone); 700 | # 701 | # insert into security(camera, filename, frame, file_type, time_stamp, text_event) values('%t', '%f', '%q', '%n', '%Y-%m-%d %T', '%C') 702 | ; sql_query insert into security(camera, filename, frame, file_type, time_stamp, event_time_stamp) values('%t', '%f', '%q', '%n', '%Y-%m-%d %T', '%C') 703 | 704 | 705 | ############################################################ 706 | # Database Options 707 | ############################################################ 708 | 709 | # database type : mysql, postgresql, sqlite3 (default : not defined) 710 | ; database_type value 711 | 712 | # database to log to (default: not defined) 713 | # for sqlite3, the full path and name for the database. 714 | ; database_dbname value 715 | 716 | # The host on which the database is located (default: localhost) 717 | ; database_host value 718 | 719 | # User account name for database (default: not defined) 720 | ; database_user value 721 | 722 | # User password for database (default: not defined) 723 | ; database_password value 724 | 725 | # Port on which the database is located 726 | # mysql 3306 , postgresql 5432 (default: not defined) 727 | ; database_port value 728 | 729 | # Database wait time in milliseconds for locked database to 730 | # be unlocked before returning database locked error (default 0) 731 | ; database_busy_timeout 0 732 | 733 | 734 | 735 | ############################################################ 736 | # Video Loopback Device (vloopback project) 737 | ############################################################ 738 | 739 | # Output images to a video4linux loopback device 740 | # The value '-' means next available (default: not defined) 741 | ; video_pipe value 742 | 743 | # Output motion images to a video4linux loopback device 744 | # The value '-' means next available (default: not defined) 745 | ; motion_video_pipe value 746 | 747 | 748 | ############################################################## 749 | # camera config files - One for each camera. 750 | # Except if only one camera - You only need this config file. 751 | # If you have more than one camera you MUST define one camera 752 | # config file for each camera in addition to this config file. 753 | ############################################################## 754 | 755 | # Remember: If you have more than one camera you must have one 756 | # camera file for each camera. E.g. 2 cameras requires 3 files: 757 | # This motion.conf file AND camera1.conf and camera2.conf. 758 | # Only put the options that are unique to each camera in the 759 | # camera config files. 760 | ; camera /etc/motion/camera1.conf 761 | ; camera /etc/motion/camera2.conf 762 | ; camera /etc/motion/camera3.conf 763 | ; camera /etc/motion/camera4.conf 764 | 765 | 766 | ############################################################## 767 | # Camera config directory - One for each camera. 768 | ############################################################## 769 | # 770 | ; camera_dir /etc/motion/conf.d 771 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 73 | 74 | 75 | 76 | pollofeed_logo 77 | countdown-to 78 | updateExp 79 | payDialog 80 | meow 81 | js-yaml 82 | bootstrap 83 | createE 84 | babel 85 | ... 86 | browserify 87 | babel- 88 | uglifyjs 89 | pugify 90 | uglify 91 | client.bundle.min.js 92 | BIN_DIR 93 | 640 94 | abset 95 | banner 96 | img 97 | joe 98 | font: 99 | googleapi 100 | text 101 | PolloFeedInvoice 102 | PolloFeedOrder 103 | msatoshi 104 | inv 105 | package 106 | 107 | 108 | URL 109 | pollofeed 110 | MONGO_DB_NAME 111 | PolloFeedOrder 112 | 113 | 114 | $PROJECT_DIR$/ 115 | $PROJECT_DIR$/lib 116 | $PROJECT_DIR$/node_modules/twilio 117 | $PROJECT_DIR$/node_modules/mocha 118 | $PROJECT_DIR$/src 119 | $PROJECT_DIR$/views 120 | $PROJECT_DIR$ 121 | 122 | 123 | 124 | 126 | 127 | 182 | 183 | 184 | true 185 | 186 | true 187 | true 188 | 189 | 190 | 191 | 192 | 193 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 |