├── Dockerfile ├── README.md ├── kubernetes └── deployment.yaml └── pushtify-listener.py /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.21 2 | RUN apk add --no-cache py3-pip && PIP_BREAK_SYSTEM_PACKAGES=1 pip3 install ntfy && PIP_BREAK_SYSTEM_PACKAGES=1 pip3 install websocket-client && rm -fr /var/cache/* 3 | RUN sed -i 's/getargspec/getfullargspec/' /usr/lib/python3.12/site-packages/ntfy/__init__.py 4 | RUN sed -i 's/getargspec/getfullargspec/' /usr/lib/python3.12/inspect.py 5 | COPY pushtify-listener.py /usr/local/bin 6 | ENTRYPOINT ["python3","/usr/local/bin/pushtify-listener.py"] 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pushtify 2 | 3 | Pushtify is a Gotify to Pushover forwarder: 4 | 5 | - Gotify: a self hosted Open Source notification system, with an Android app. 6 | - Pushover: a PaaS closed source notification system. Available on iOS and Android. 7 | 8 | ## Why this container image? 9 | 10 | When I moved from Android to iOS, I was just too lazy to reconfigure the multiple dozens of apps that were sending notifications to my Gotify instance. 11 | 12 | ## How it works 13 | 14 | This container constantly listens for Gotify notifications through websocket and forwards received notifications to Pushover. 15 | 16 | It uses the `ntfy` Python library to forward messages. 17 | 18 | ![image](https://github.com/sebw/pushtify/assets/2285094/3416109e-2a5a-4260-8b84-5baade964b10) 19 | 20 | If the connection to Gotify is lost, the container will reinitiate the connection. 21 | 22 | ## Requirements 23 | 24 | - a client token in Gotify 25 | - the gotify hostname 26 | - a pushover user key 27 | 28 | ## Installation with Docker or Podman 29 | 30 | ```bash 31 | docker run --name pushtify \ 32 | -e GOTIFY_TOKEN=zzz \ 33 | -e GOTIFY_HOST=gotify.example.org \ 34 | -e GOTIFY_PROTOCOL=https \ 35 | -e PUSHOVER_USERKEY=xxx \ 36 | ghcr.io/sebw/pushtify:latest 37 | ``` 38 | 39 | ```bash 40 | podman run --name pushtify \ 41 | -e GOTIFY_TOKEN=zzz \ 42 | -e GOTIFY_HOST=gotify.example.org \ 43 | -e GOTIFY_PROTOCOL=https \ 44 | -e PUSHOVER_USERKEY=xxx \ 45 | ghcr.io/sebw/pushtify:latest 46 | ``` 47 | 48 | If `GOTIFY_PROTOCOL` is not defined, HTTPS is assumed. 49 | 50 | ## Building the container image yourself 51 | 52 | ```bash 53 | git clone https://github.com/sebw/pushtify 54 | cd pushtify 55 | docker build -t pushtify:latest . 56 | ``` 57 | 58 | ## Running Pushtify on Kubernetes 59 | 60 | ```bash 61 | git clone https://github.com/sebw/pushtify 62 | cd pushtify/kubernetes 63 | vim deployment.yaml (edit your variables, ideally store them as k8s secrets) 64 | kubectl apply -f deployment.yaml 65 | ``` 66 | 67 | If connection to Gotify websocket is lost, the Python script will stop and the liveness probe will fail, triggering a restart of the pod. 68 | 69 | ## Message Priorities 70 | 71 | Gotify and Pushover implement priorities. 72 | 73 | I took the liberty to map priorities in such a way: 74 | 75 | | Gotify Behavior | Gotify Priority | Pushover Priority | Pushover Behavior | 76 | |- |-| -|-| 77 | | No notification | 0 | -1| Low priority| 78 | | Icon in notification bar | 1 - 3 | 0 | Normal priority| 79 | | Icon in notification bar + Sound | 4 - 7 | 1 | High Priority | 80 | | Icon in notification bar + Sound + Vibration | 8 - 10 | 2| Emergency priority| 81 | -------------------------------------------------------------------------------- /kubernetes/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: pushtify-deployment 5 | namespace: notify 6 | spec: 7 | progressDeadlineSeconds: 600 8 | replicas: 1 9 | revisionHistoryLimit: 10 10 | selector: 11 | matchLabels: 12 | app: pushtify 13 | strategy: 14 | rollingUpdate: 15 | maxSurge: 0 16 | maxUnavailable: 25% 17 | type: RollingUpdate 18 | template: 19 | metadata: 20 | labels: 21 | app: pushtify 22 | spec: 23 | containers: 24 | - env: 25 | - name: TZ 26 | value: "Europe/Brussels" 27 | - name: GOTIFY_TOKEN 28 | value: "xyz" 29 | - name: GOTIFY_HOST 30 | value: "gotify.example.com" 31 | - name: PUSHOVER_USERKEY 32 | value: "abcdef" 33 | livenessProbe: 34 | exec: 35 | command: 36 | - /bin/sh 37 | - -c 38 | - pgrep -f pushtify-listener 39 | initialDelaySeconds: 10 40 | periodSeconds: 10 41 | image: ghcr.io/sebw/pushtify:v0.5 42 | imagePullPolicy: IfNotPresent 43 | name: notify 44 | resources: {} 45 | securityContext: 46 | capabilities: {} 47 | terminationMessagePath: /dev/termination-log 48 | terminationMessagePolicy: File 49 | dnsPolicy: ClusterFirst 50 | restartPolicy: Always 51 | schedulerName: default-scheduler 52 | securityContext: {} 53 | terminationGracePeriodSeconds: 30 54 | -------------------------------------------------------------------------------- /pushtify-listener.py: -------------------------------------------------------------------------------- 1 | import websocket 2 | import ntfy 3 | import json 4 | import os 5 | 6 | pushover_userkey = os.environ['PUSHOVER_USERKEY'] 7 | gotify_host = os.environ['GOTIFY_HOST'] 8 | gotify_token = os.environ['GOTIFY_TOKEN'] 9 | 10 | if 'GOTIFY_PROTOCOL' not in os.environ: 11 | websocket_protocol = 'wss' 12 | else: 13 | if os.environ['GOTIFY_PROTOCOL'] == "http": 14 | websocket_protocol = 'ws' 15 | elif os.environ['GOTIFY_PROTOCOL'] == "https": 16 | websocket_protocol = 'wss' 17 | 18 | def on_message(ws, message): 19 | msg = json.loads(message) 20 | if msg['priority'] == 0: 21 | pushover_prio = "-1" 22 | elif 1 <= msg['priority'] <= 3: 23 | pushover_prio = "0" 24 | elif 4 <= msg['priority'] <= 7: 25 | pushover_prio = "1" 26 | elif msg['priority'] > 7: 27 | pushover_prio = "2" 28 | ntfy.notify(msg['message'],msg['title'], priority=pushover_prio, backend='pushover', user_key=pushover_userkey) 29 | 30 | def on_error(ws, error): 31 | print(error) 32 | 33 | def on_close(ws, close_status_code, close_msg): 34 | print("### closed connection ###") 35 | 36 | def on_open(ws): 37 | print("### opening connection ###") 38 | 39 | if __name__ == "__main__": 40 | wsapp = websocket.WebSocketApp(str(websocket_protocol) + "://" + str(gotify_host) + "/stream", header={"X-Gotify-Key": str(gotify_token)}, 41 | on_open=on_open, 42 | on_message=on_message, 43 | on_error=on_error, 44 | on_close=on_close) 45 | wsapp.run_forever() 46 | --------------------------------------------------------------------------------