{"id":16812826,"url":"https://github.com/pennersr/shove","last_synced_at":"2025-04-07T06:09:00.332Z","repository":{"id":46321642,"uuid":"209633275","full_name":"pennersr/shove","owner":"pennersr","description":"Asynchronous \u0026 persistent push notification service, supporting APNS, FCM, Web Push, Telegram and Email. Written in Go (Golang). Mirror of https://codeberg.org/pennersr/shove","archived":false,"fork":false,"pushed_at":"2024-09-08T18:47:40.000Z","size":145,"stargazers_count":266,"open_issues_count":2,"forks_count":24,"subscribers_count":7,"default_branch":"main","last_synced_at":"2024-10-14T10:23:36.620Z","etag":null,"topics":["android","android-notifications","apns","fcm","firebase-cloud-messaging","gcm","go","golang","ios","ios-notifications","notifications","push-notifications","telegram-bot-api","webpush","webpush-notifications"],"latest_commit_sha":null,"homepage":"https://codeberg.org/pennersr/shove","language":"Go","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pennersr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"pennersr","patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"custom":null}},"created_at":"2019-09-19T19:29:09.000Z","updated_at":"2024-10-08T22:09:37.000Z","dependencies_parsed_at":"2023-12-22T14:10:08.777Z","dependency_job_id":"4c9c56fc-ea2e-4cc6-8f3f-471fd1513d42","html_url":"https://github.com/pennersr/shove","commit_stats":{"total_commits":68,"total_committers":2,"mean_commits":34.0,"dds":"0.014705882352941124","last_synced_commit":"0250509264fd0c503f79948d4f320d15bb9d5fee"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pennersr%2Fshove","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pennersr%2Fshove/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pennersr%2Fshove/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pennersr%2Fshove/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pennersr","download_url":"https://codeload.github.com/pennersr/shove/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247601448,"owners_count":20964864,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","android-notifications","apns","fcm","firebase-cloud-messaging","gcm","go","golang","ios","ios-notifications","notifications","push-notifications","telegram-bot-api","webpush","webpush-notifications"],"created_at":"2024-10-13T10:23:33.787Z","updated_at":"2025-04-07T06:09:00.313Z","avatar_url":"https://github.com/pennersr.png","language":"Go","funding_links":["https://github.com/sponsors/pennersr"],"categories":[],"sub_categories":[],"readme":"# When push comes to shove...\n\n[![Go Report Card](https://goreportcard.com/badge/codeberg.org/pennersr/shove)](https://goreportcard.com/report/codeberg.org/pennersr/shove) [![Written in Emacs](https://pennersr.github.io/img/emacs-badge.svg)](https://www.gnu.org/software/emacs/) [![Pipeline Status](https://ci.codeberg.org/api/badges/13727/status.svg)](https://ci.codeberg.org/repos/13727)\n\n## Background\n\nThis is the replacement for [Pulsus](https://github.com/pennersr/pulsus) which has been steadily serving up to 100M push notifications. But, given that it was still using the binary APNS protocol it was due for an upgrade.\n\n## Overview\n\nDesign:\n- Asynchronous: a push client can just fire \u0026 forget.\n- Multiple workers per push service.\n- Less moving parts: when using Redis, you can push directly to the queue, bypassing the need for the Shove server to be up and running.\n\nSupported push services:\n- APNS\n- Email: supports automatic creation of email digests in case the rate limit\n  is exceeded\n- FCM\n- Telegram: supports squashing multiple messages into one in case the rate limit\n  is exceeded\n- Webhook: issue arbitrary webhook posts\n- Web Push\n\nFeatures:\n- Feedback: asynchronously receive information on invalid device tokens.\n- Queueing: both in-memory and persistent via Redis.\n- Exponential back-off in case of failure.\n- Prometheus support.\n- Squashing of messages in case rate limits are exceeded.\n\n\n## Why?\n\n- https://github.com/appleboy/gorush/issues/386#issuecomment-479191179\n\n- https://github.com/mercari/gaurun/issues/115\n\n\n## Usage\n\n### Running\n\nUsage:\n\n    $ shove -h\n    Usage of ./shove:\n      -api-addr string\n            API address to listen to (default \":8322\")\n      -apns-certificate-path string\n            APNS certificate path\n      -apns-sandbox-certificate-path string\n            APNS sandbox certificate path\n      -apns-workers int\n            The number of workers pushing APNS messages (default 4)\n      -email-host string\n            Email host\n      -email-port int\n            Email port (default 25)\n      -email-rate-amount int\n            Email max. rate (amount)\n      -email-rate-per int\n            Email max. rate (per seconds)\n      -email-tls\n            Use TLS\n      -email-tls-insecure\n            Skip TLS verification\n      -fcm-credentials-file string\n            Path to FCM service account JSON file\n      -fcm-workers int\n            The number of workers pushing FCM messages (default 4)\n      -queue-redis string\n            Use Redis queue (Redis URL)\n      -telegram-bot-token string\n            Telegram bot token\n      -telegram-rate-amount int\n            Telegram max. rate (amount)\n      -telegram-rate-per int\n            Telegram max. rate (per seconds)\n      -telegram-workers int\n            The number of workers pushing Telegram messages (default 2)\n      -webhook-workers int\n            The number of workers pushing Webhook messages\n      -webpush-vapid-private-key string\n            VAPID public key\n      -webpush-vapid-public-key string\n            VAPID public key\n      -webpush-workers int\n            The number of workers pushing Web messages (default 8)\n\n\nStart the server:\n\n    $ shove \\\n        -api-addr localhost:8322 \\\n        -queue-redis redis://redis:6379 \\\n        -fcm-credentials-file /etc/shove/fcm/credentials.json \\\n        -apns-certificate-path /etc/shove/apns/production/bundle.pem -apns-sandbox-certificate-path /etc/shove/apns/sandbox/bundle.pem \\\n        -webpush-vapid-public-key=$VAPID_PUBLIC_KEY -webpush-vapid-private-key=$VAPID_PRIVATE_KEY \\\n        -telegram-bot-token $TELEGRAM_BOT_TOKEN\n\n\n### APNS\n\nPush an APNS notification:\n\n    $ curl  -i  --data '{\"service\": \"apns\", \"headers\": {\"apns-priority\": 10, \"apns-topic\": \"com.shove.app\"}, \"payload\": {\"aps\": { \"alert\": \"hi\"}}, \"token\": \"81b8ecff8cb6d22154404d43b9aeaaf6219dfbef2abb2fe313f3725f4505cb47\"}' http://localhost:8322/api/push/apns\n\n\nA successful push results in:\n\n    HTTP/1.1 202 Accepted\n    Date: Tue, 07 May 2019 19:00:15 GMT\n    Content-Length: 2\n    Content-Type: text/plain; charset=utf-8\n\n    OK\n\n\n### FCM\n\nPush an FCM notification:\n\n    $ curl  -i  --data '{\"message\": {\"notification\": {\"body\": \"Hello world!\", \"title\": \"Test\"}, \"token\": \"c7VmdNNHQaGTLkmi....15CmMs\"}}' http://localhost:8322/api/push/fcm\n\n### Webhook\n\nPush a Webhook call, containing arbitrary body content:\n\n    $ curl  -i  --data '{\"url\": \"http://localhost:8000/api/webhook\", \"headers\": {\"foo\": \"bar\"}, \"body\": \"Hello world!\"}' http://localhost:8322/api/push/webhook\n\nOr, post JSON:\n\n    $ curl  -i  --data '{\"url\": \"http://localhost:8000/api/webhook\", \"headers\": {\"foo\": \"bar\"}, \"data\": {\"hello\": \"world!\"}}' http://localhost:8322/api/push/webhook\n\n\n### WebPush\n\nPush a WebPush notification:\n\n    $ curl  -i  --data '{\"subscription\": {\"endpoint\":\"https://updates.push.services.mozilla.com/wpush/v2/gAAAAAc4BA....UrjGlg\",\"keys\":{\"auth\":\"Hbj3ap...al9ew\",\"p256dh\":\"BeKdTC3...KLGBJlgF\"}}, \"headers\": {\"ttl\": 3600, \"urgency\": \"high\"}, \"token\": \"use-this-for-feedback-instead-of-subscription\", \"payload\": {\"hello\":\"world\"}}' http://localhost:8322/api/push/webpush\n\nThe subscription (serialized as a JSON string) is used for receiving\nfeedback. Alternatively, you can specify an optional `token` parameter as done\nin the example above.\n\n\n### Telegram\n\nPush a Telegram notification:\n\n    $ curl  -i  --data '{\"method\": \"sendMessage\", \"payload\": {\"chat_id\": \"12345678\", \"text\": \"Hello!\"}}' http://localhost:8322/api/push/telegram\n\nNote that the Telegram Bot API documents `chat_id` as \"Integer or String\" --\nShove requires strings to be passed. For users that disconnected from your bot\nthe chat ID will be communicated back through the feedback mechanism. Here, the\ntoken will equal the unreachable chat ID.\n\n\n### Receive Feedback\n\nOutdated/invalid tokens are communicated back. To receive those, you can periodically query the feedback channel to receive token feedback, and remove those from your database:\n\n\n    $ curl -X POST 'http://localhost:8322/api/feedback'\n\n    {\n      \"feedback\": [\n        {\"service\":\"apns-sandbox\",\n         \"token\":\"881becff86cbd221544044d3b9aeaaf6314dfbef2abb2fe313f3725f4505cb47\",\n         \"reason\":\"invalid\"}\n      ]\n    }\n\n\n### Email\n\nIn order to keep your SMTP server safe from being blacklisted, the email service\nsupports rate limitting. When the rate is exceeded, multiple mails are\nautomatically digested.\n\n    $ shove \\\n        -email-host localhost \\\n        -email-port 1025 \\\n        -api-addr localhost:8322 \\\n        -email-rate-amount 3 \\\n        -email-rate-per 10 \\\n        -queue-redis redis://localhost:6379\n\nPush an email:\n\n\t$ curl -i -X POST --data @./scripts/email.json http://localhost:8322/api/push/email\n\nIf you send too many emails, you'll notice that they are digested, and at a\nlater time, one digest mail is being sent:\n\n    2021/03/23 21:15:57 Using Redis queue at redis://localhost:6379\n    2021/03/23 21:15:57 Initializing Email service\n    2021/03/23 21:15:57 Serving on localhost:8322\n    2021/03/23 21:15:57 Shove server started\n    2021/03/23 21:15:57 email: Worker started\n    2021/03/23 21:15:57 email: Digester started\n    2021/03/23 21:15:58 email: Sending email\n    2021/03/23 21:15:59 email: Sending email\n    2021/03/23 21:15:59 email: Sending email\n    2021/03/23 21:16:00 email: Rate to john@doe.org exceeded, email digested\n    2021/03/23 21:16:12 email: Rate to john@doe.org exceeded, email digested\n    2021/03/23 21:16:18 email: Sending digest email\n\n\n### Redis Queues\n\nShove is being used to push a high volume of notifications in a production\nenvironment, consisting of various microservices interacting together. In such a\nscenario, it is important that the various services are not too tightly coupled\nto one another.  For that purpose, Shove offers the ability to post\nnotifications directly to a Redis queue.\n\nPosting directly to the Redis queue, instead of using the HTTP service\nendpoints, has the advantage that you can take Shove offline without disturbing\nthe operation of the clients pushing the notifications.\n\nShove intentionally tries to make as little assumptions on the notification\npayloads being pushed, as they are mostly handed over as is to the upstream\nservices. So, when using Shove this way, the client is responsible for handing\nover a raw payload. Here's an example:\n\n\n    package main\n\n    import (\n    \t\"encoding/json\"\n    \t\"codeberg.org/pennersr/shove/pkg/shove\"\n    \t\"log\"\n    \t\"os\"\n    )\n\n    type FCMNotification struct {\n    \tTo       string            `json:\"to\"`\n    \tData     map[string]string `json:\"data,omitempty\"`\n    }\n\n    func main() {\n    \tredisURL := os.Getenv(\"REDIS_URL\")\n    \tif redisURL == \"\" {\n    \t\tredis_URL = \"redis://localhost:6379\"\n    \t}\n    \tclient := shove.NewRedisClient(redisURL)\n\n    \tnotification := FCMNotification{\n    \t\tTo:   \"token....\",\n    \t\tData: map[string]string{},\n    \t}\n\n    \traw, err := json.Marshal(notification)\n    \tif err != nil {\n    \t\tlog.Fatal(err)\n    \t}\n    \terr = client.PushRaw(\"fcm\", raw)\n    \tif err != nil {\n    \t\tlog.Fatal(err)\n    \t}\n    }\n\n\n## Status\n\nUsed in production, over at:\n\n- [Drakdoo: Indicator based signals \u0026 alerts](https://www.drakdoo.com): 365.251.428 alerts fired and counting.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpennersr%2Fshove","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpennersr%2Fshove","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpennersr%2Fshove/lists"}