Merge branch 'main' of https://github.com/binwiederhier/ntfy
This commit is contained in:
		
						commit
						b1819d4766
					
				
					 20 changed files with 838 additions and 464 deletions
				
			
		
							
								
								
									
										36
									
								
								.github/workflows/docs.yaml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								.github/workflows/docs.yaml
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | ||||||
|  | name: docs | ||||||
|  | on: | ||||||
|  |   push: | ||||||
|  |     branches: | ||||||
|  |       - main | ||||||
|  | jobs: | ||||||
|  |   publish-docs: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - | ||||||
|  |         name: Checkout ntfy code | ||||||
|  |         uses: actions/checkout@v3 | ||||||
|  |       - | ||||||
|  |         name: Checkout docs pages code | ||||||
|  |         uses: actions/checkout@v3 | ||||||
|  |         with: | ||||||
|  |           repository: binwiederhier/ntfy-docs.github.io | ||||||
|  |           path: build/ntfy-docs.github.io | ||||||
|  |           token: ${{secrets.NTFY_DOCS_PUSH_TOKEN}} | ||||||
|  |           # Expires after 1 year, re-generate via | ||||||
|  |           # User -> Settings -> Developer options -> Personal Access Tokens -> Fine Grained Token | ||||||
|  |       - | ||||||
|  |         name: Build docs | ||||||
|  |         run: make docs | ||||||
|  |       - | ||||||
|  |         name: Copy generated docs | ||||||
|  |         run: rsync -av --exclude CNAME --delete server/docs/ build/ntfy-docs.github.io/docs/ | ||||||
|  |       - | ||||||
|  |         name: Publish docs | ||||||
|  |         run: | | ||||||
|  |           cd build/ntfy-docs.github.io | ||||||
|  |           git config user.name "GitHub Actions Bot" | ||||||
|  |           git config user.email "<>"           | ||||||
|  |           git add docs/ | ||||||
|  |           git commit -m "Updated docs" | ||||||
|  |           git push origin main | ||||||
|  | @ -95,6 +95,10 @@ appreciated. A big fat **Thank You** to the folks already sponsoring ntfy: | ||||||
| <a href="https://github.com/mnault"><img src="https://github.com/mnault.png" width="40px" /></a> | <a href="https://github.com/mnault"><img src="https://github.com/mnault.png" width="40px" /></a> | ||||||
| <a href="https://github.com/nwithan8"><img src="https://github.com/nwithan8.png" width="40px" /></a> | <a href="https://github.com/nwithan8"><img src="https://github.com/nwithan8.png" width="40px" /></a> | ||||||
| <a href="https://github.com/peterleiser"><img src="https://github.com/peterleiser.png" width="40px" /></a> | <a href="https://github.com/peterleiser"><img src="https://github.com/peterleiser.png" width="40px" /></a> | ||||||
|  | <a href="https://github.com/portothree"><img src="https://github.com/portothree.png" width="40px" /></a> | ||||||
|  | <a href="https://github.com/finngreig"><img src="https://github.com/finngreig.png" width="40px" /></a> | ||||||
|  | <a href="https://github.com/skrollme"><img src="https://github.com/skrollme.png" width="40px" /></a> | ||||||
|  | <a href="https://github.com/gergepalfi"><img src="https://github.com/gergepalfi.png" width="40px" /></a> | ||||||
| 
 | 
 | ||||||
| ## License | ## License | ||||||
| Made with ❤️ by [Philipp C. Heckel](https://heckel.io).    | Made with ❤️ by [Philipp C. Heckel](https://heckel.io).    | ||||||
|  |  | ||||||
|  | @ -17,6 +17,7 @@ func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 	app, _, _, _ := newTestApp() | 	app, _, _, _ := newTestApp() | ||||||
| 	require.Nil(t, app.Run([]string{"ntfy", "publish", "ntfytest", "ntfy unit test " + testMessage})) | 	require.Nil(t, app.Run([]string{"ntfy", "publish", "ntfytest", "ntfy unit test " + testMessage})) | ||||||
|  | 	time.Sleep(3 * time.Second) // Since #502, ntfy.sh writes messages to the cache asynchronously, after a timeout of ~1.5s | ||||||
| 
 | 
 | ||||||
| 	app2, _, stdout, _ := newTestApp() | 	app2, _, stdout, _ := newTestApp() | ||||||
| 	require.Nil(t, app2.Run([]string{"ntfy", "subscribe", "--poll", "ntfytest"})) | 	require.Nil(t, app2.Run([]string{"ntfy", "subscribe", "--poll", "ntfytest"})) | ||||||
|  |  | ||||||
|  | @ -44,6 +44,8 @@ var flagsServe = append( | ||||||
| 	altsrc.NewStringFlag(&cli.StringFlag{Name: "firebase-key-file", Aliases: []string{"firebase_key_file", "F"}, EnvVars: []string{"NTFY_FIREBASE_KEY_FILE"}, Usage: "Firebase credentials file; if set additionally publish to FCM topic"}), | 	altsrc.NewStringFlag(&cli.StringFlag{Name: "firebase-key-file", Aliases: []string{"firebase_key_file", "F"}, EnvVars: []string{"NTFY_FIREBASE_KEY_FILE"}, Usage: "Firebase credentials file; if set additionally publish to FCM topic"}), | ||||||
| 	altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-file", Aliases: []string{"cache_file", "C"}, EnvVars: []string{"NTFY_CACHE_FILE"}, Usage: "cache file used for message caching"}), | 	altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-file", Aliases: []string{"cache_file", "C"}, EnvVars: []string{"NTFY_CACHE_FILE"}, Usage: "cache file used for message caching"}), | ||||||
| 	altsrc.NewDurationFlag(&cli.DurationFlag{Name: "cache-duration", Aliases: []string{"cache_duration", "b"}, EnvVars: []string{"NTFY_CACHE_DURATION"}, Value: server.DefaultCacheDuration, Usage: "buffer messages for this time to allow `since` requests"}), | 	altsrc.NewDurationFlag(&cli.DurationFlag{Name: "cache-duration", Aliases: []string{"cache_duration", "b"}, EnvVars: []string{"NTFY_CACHE_DURATION"}, Value: server.DefaultCacheDuration, Usage: "buffer messages for this time to allow `since` requests"}), | ||||||
|  | 	altsrc.NewIntFlag(&cli.IntFlag{Name: "cache-batch-size", Aliases: []string{"cache_batch_size"}, EnvVars: []string{"NTFY_BATCH_SIZE"}, Usage: "max size of messages to batch together when writing to message cache (if zero, writes are synchronous)"}), | ||||||
|  | 	altsrc.NewDurationFlag(&cli.DurationFlag{Name: "cache-batch-timeout", Aliases: []string{"cache_batch_timeout"}, EnvVars: []string{"NTFY_CACHE_BATCH_TIMEOUT"}, Usage: "timeout for batched async writes to the message cache (if zero, writes are synchronous)"}), | ||||||
| 	altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-startup-queries", Aliases: []string{"cache_startup_queries"}, EnvVars: []string{"NTFY_CACHE_STARTUP_QUERIES"}, Usage: "queries run when the cache database is initialized"}), | 	altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-startup-queries", Aliases: []string{"cache_startup_queries"}, EnvVars: []string{"NTFY_CACHE_STARTUP_QUERIES"}, Usage: "queries run when the cache database is initialized"}), | ||||||
| 	altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"auth_file", "H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}), | 	altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"auth_file", "H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}), | ||||||
| 	altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"auth_default_access", "p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}), | 	altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"auth_default_access", "p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}), | ||||||
|  | @ -110,6 +112,8 @@ func execServe(c *cli.Context) error { | ||||||
| 	cacheFile := c.String("cache-file") | 	cacheFile := c.String("cache-file") | ||||||
| 	cacheDuration := c.Duration("cache-duration") | 	cacheDuration := c.Duration("cache-duration") | ||||||
| 	cacheStartupQueries := c.String("cache-startup-queries") | 	cacheStartupQueries := c.String("cache-startup-queries") | ||||||
|  | 	cacheBatchSize := c.Int("cache-batch-size") | ||||||
|  | 	cacheBatchTimeout := c.Duration("cache-batch-timeout") | ||||||
| 	authFile := c.String("auth-file") | 	authFile := c.String("auth-file") | ||||||
| 	authDefaultAccess := c.String("auth-default-access") | 	authDefaultAccess := c.String("auth-default-access") | ||||||
| 	attachmentCacheDir := c.String("attachment-cache-dir") | 	attachmentCacheDir := c.String("attachment-cache-dir") | ||||||
|  | @ -233,6 +237,8 @@ func execServe(c *cli.Context) error { | ||||||
| 	conf.CacheFile = cacheFile | 	conf.CacheFile = cacheFile | ||||||
| 	conf.CacheDuration = cacheDuration | 	conf.CacheDuration = cacheDuration | ||||||
| 	conf.CacheStartupQueries = cacheStartupQueries | 	conf.CacheStartupQueries = cacheStartupQueries | ||||||
|  | 	conf.CacheBatchSize = cacheBatchSize | ||||||
|  | 	conf.CacheBatchTimeout = cacheBatchTimeout | ||||||
| 	conf.AuthFile = authFile | 	conf.AuthFile = authFile | ||||||
| 	conf.AuthDefaultRead = authDefaultRead | 	conf.AuthDefaultRead = authDefaultRead | ||||||
| 	conf.AuthDefaultWrite = authDefaultWrite | 	conf.AuthDefaultWrite = authDefaultWrite | ||||||
|  |  | ||||||
|  | @ -309,6 +309,25 @@ with the given username/password. Be sure to use HTTPS to avoid eavesdropping an | ||||||
|     ])); |     ])); | ||||||
|     ``` |     ``` | ||||||
| 
 | 
 | ||||||
|  | ### Example: UnifiedPush | ||||||
|  | [UnifiedPush](https://unifiedpush.org) requires that the [application server](https://unifiedpush.org/spec/definitions/#application-server) (e.g. Synapse, Fediverse Server, …)  | ||||||
|  | has anonymous write access to the [topic](https://unifiedpush.org/spec/definitions/#endpoint) used for push messages.  | ||||||
|  | The topic names used by UnifiedPush all start with the `up*` prefix. Please refer to the  | ||||||
|  | **[UnifiedPush documentation](https://unifiedpush.org/users/distributors/ntfy/#limit-access-to-some-users)** for more details. | ||||||
|  | 
 | ||||||
|  | To enable support for UnifiedPush for private servers (i.e. `auth-default-access: "deny-all"`), you should either  | ||||||
|  | allow anonymous write access for the entire prefix or explicitly per topic: | ||||||
|  | 
 | ||||||
|  | === "Prefix" | ||||||
|  |     ``` | ||||||
|  |     $ ntfy access '*' 'up*' write-only | ||||||
|  |     ``` | ||||||
|  | 
 | ||||||
|  | === "Explicitly" | ||||||
|  |     ``` | ||||||
|  |     $ ntfy access '*' upYzMtZGZiYTY5 write-only | ||||||
|  |     ``` | ||||||
|  | 
 | ||||||
| ## E-mail notifications | ## E-mail notifications | ||||||
| To allow forwarding messages via e-mail, you can configure an **SMTP server for outgoing messages**. Once configured,  | To allow forwarding messages via e-mail, you can configure an **SMTP server for outgoing messages**. Once configured,  | ||||||
| you can set the `X-Email` header to [send messages via e-mail](publish.md#e-mail-notifications) (e.g.  | you can set the `X-Email` header to [send messages via e-mail](publish.md#e-mail-notifications) (e.g.  | ||||||
|  | @ -806,19 +825,27 @@ out [this discussion on Reddit](https://www.reddit.com/r/golang/comments/r9u4ee/ | ||||||
| 
 | 
 | ||||||
| Depending on *how you run it*, here are a few limits that are relevant: | Depending on *how you run it*, here are a few limits that are relevant: | ||||||
| 
 | 
 | ||||||
| ### WAL for message cache | ### Message cache | ||||||
| By default, the [message cache](#message-cache) (defined by `cache-file`) uses the SQLite default settings, which means it | By default, the [message cache](#message-cache) (defined by `cache-file`) uses the SQLite default settings, which means it | ||||||
| syncs to disk on every write. For personal servers, this is perfectly adequate. For larger installations, such as ntfy.sh, | syncs to disk on every write. For personal servers, this is perfectly adequate. For larger installations, such as ntfy.sh, | ||||||
| the [write-ahead log (WAL)](https://sqlite.org/wal.html) should be enabled, and the sync mode should be adjusted.  | the [write-ahead log (WAL)](https://sqlite.org/wal.html) should be enabled, and the sync mode should be adjusted.  | ||||||
| See [this article](https://phiresky.github.io/blog/2020/sqlite-performance-tuning/) for details. | See [this article](https://phiresky.github.io/blog/2020/sqlite-performance-tuning/) for details. | ||||||
| 
 | 
 | ||||||
|  | In addition to that, for very high load servers (such as ntfy.sh), it may be beneficial to write messages to the cache | ||||||
|  | in batches, and asynchronously. This can be enabled with the `cache-batch-size` and `cache-batch-timeout`. If you start | ||||||
|  | seeing `database locked` messages in the logs, you should probably enable that. | ||||||
|  | 
 | ||||||
| Here's how ntfy.sh has been tuned in the `server.yml` file: | Here's how ntfy.sh has been tuned in the `server.yml` file: | ||||||
| 
 | 
 | ||||||
| ``` yaml | ``` yaml | ||||||
|  | cache-batch-size: 25 | ||||||
|  | cache-batch-timeout: "1s" | ||||||
| cache-startup-queries: | | cache-startup-queries: | | ||||||
|     pragma journal_mode = WAL; |     pragma journal_mode = WAL; | ||||||
|     pragma synchronous = normal; |     pragma synchronous = normal; | ||||||
|     pragma temp_store = memory; |     pragma temp_store = memory; | ||||||
|  |     pragma busy_timeout = 15000; | ||||||
|  |     vacuum; | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ### For systemd services | ### For systemd services | ||||||
|  | @ -971,6 +998,8 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`). | ||||||
| | `cache-file`                               | `NTFY_CACHE_FILE`                               | *filename*                                          | -                 | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See [message cache](#message-cache).             | | | `cache-file`                               | `NTFY_CACHE_FILE`                               | *filename*                                          | -                 | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See [message cache](#message-cache).             | | ||||||
| | `cache-duration`                           | `NTFY_CACHE_DURATION`                           | *duration*                                          | 12h               | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. Set this to `0` to disable the cache entirely.                                        | | | `cache-duration`                           | `NTFY_CACHE_DURATION`                           | *duration*                                          | 12h               | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. Set this to `0` to disable the cache entirely.                                        | | ||||||
| | `cache-startup-queries`                    | `NTFY_CACHE_STARTUP_QUERIES`                    | *string (SQL queries)*                              | -                 | SQL queries to run during database startup; this is useful for tuning and [enabling WAL mode](#wal-for-message-cache)                                                                                                           | | | `cache-startup-queries`                    | `NTFY_CACHE_STARTUP_QUERIES`                    | *string (SQL queries)*                              | -                 | SQL queries to run during database startup; this is useful for tuning and [enabling WAL mode](#wal-for-message-cache)                                                                                                           | | ||||||
|  | | `cache-batch-size`                         | `NTFY_CACHE_BATCH_SIZE`                         | *int*                                               | 0                 | Max size of messages to batch together when writing to message cache (if zero, writes are synchronous)                                                                                                                          | | ||||||
|  | | `cache-batch-timeout`                      | `NTFY_CACHE_BATCH_TIMEOUT`                      | *duration*                                          | 0s                | Timeout for batched async writes to the message cache (if zero, writes are synchronous)                                                                                                                                         | | ||||||
| | `auth-file`                                | `NTFY_AUTH_FILE`                                | *filename*                                          | -                 | Auth database file used for access control. If set, enables authentication and access control. See [access control](#access-control).                                                                                           | | | `auth-file`                                | `NTFY_AUTH_FILE`                                | *filename*                                          | -                 | Auth database file used for access control. If set, enables authentication and access control. See [access control](#access-control).                                                                                           | | ||||||
| | `auth-default-access`                      | `NTFY_AUTH_DEFAULT_ACCESS`                      | `read-write`, `read-only`, `write-only`, `deny-all` | `read-write`      | Default permissions if no matching entries in the auth database are found. Default is `read-write`.                                                                                                                             | | | `auth-default-access`                      | `NTFY_AUTH_DEFAULT_ACCESS`                      | `read-write`, `read-only`, `write-only`, `deny-all` | `read-write`      | Default permissions if no matching entries in the auth database are found. Default is `read-write`.                                                                                                                             | | ||||||
| | `behind-proxy`                             | `NTFY_BEHIND_PROXY`                             | *bool*                                              | false             | If set, the X-Forwarded-For header is used to determine the visitor IP address instead of the remote address of the connection.                                                                                                 | | | `behind-proxy`                             | `NTFY_BEHIND_PROXY`                             | *bool*                                              | false             | If set, the X-Forwarded-For header is used to determine the visitor IP address instead of the remote address of the connection.                                                                                                 | | ||||||
|  | @ -1035,6 +1064,8 @@ OPTIONS: | ||||||
|    --behind-proxy, --behind_proxy, -P                                                                  if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting) (default: false) [$NTFY_BEHIND_PROXY] |    --behind-proxy, --behind_proxy, -P                                                                  if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting) (default: false) [$NTFY_BEHIND_PROXY] | ||||||
|    --cache-duration since, --cache_duration since, -b since                                            buffer messages for this time to allow since requests (default: 12h0m0s) [$NTFY_CACHE_DURATION] |    --cache-duration since, --cache_duration since, -b since                                            buffer messages for this time to allow since requests (default: 12h0m0s) [$NTFY_CACHE_DURATION] | ||||||
|    --cache-file value, --cache_file value, -C value                                                    cache file used for message caching [$NTFY_CACHE_FILE] |    --cache-file value, --cache_file value, -C value                                                    cache file used for message caching [$NTFY_CACHE_FILE] | ||||||
|  |    --cache-batch-size value, --cache_batch_size value                                                  max size of messages to batch together when writing to message cache (if zero, writes are synchronous) (default: 0) [$NTFY_BATCH_SIZE] | ||||||
|  |    --cache-batch-timeout value, --cache_batch_timeout value                                            timeout for batched async writes to the message cache (if zero, writes are synchronous) (default: 0s) [$NTFY_CACHE_BATCH_TIMEOUT]    | ||||||
|    --cache-startup-queries value, --cache_startup_queries value                                        queries run when the cache database is initialized [$NTFY_CACHE_STARTUP_QUERIES] |    --cache-startup-queries value, --cache_startup_queries value                                        queries run when the cache database is initialized [$NTFY_CACHE_STARTUP_QUERIES] | ||||||
|    --cert-file value, --cert_file value, -E value                                                      certificate file, if listen-https is set [$NTFY_CERT_FILE] |    --cert-file value, --cert_file value, -E value                                                      certificate file, if listen-https is set [$NTFY_CERT_FILE] | ||||||
|    --config value, -c value                                                                            config file (default: /etc/ntfy/server.yml) [$NTFY_CONFIG_FILE] |    --config value, -c value                                                                            config file (default: /etc/ntfy/server.yml) [$NTFY_CONFIG_FILE] | ||||||
|  |  | ||||||
|  | @ -122,6 +122,19 @@ to ntfy at its default URL (`attrs` and other attributes are optional): | ||||||
|           priority: 1 |           priority: 1 | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
|  | ## GitHub Actions | ||||||
|  | You can send a message during a workflow run with curl. Here is an example sending info about the repo, commit and job status. | ||||||
|  | ``` yaml | ||||||
|  | - name: Actions Ntfy | ||||||
|  |   run: | | ||||||
|  |     curl \ | ||||||
|  |       -u ${{ secrets.NTFY_CRED }} \ | ||||||
|  |       -H "Title: Title here" \ | ||||||
|  |       -H "Content-Type: text/plain" \ | ||||||
|  |       -d $'Repo: ${{ github.repository }}\nCommit: ${{ github.sha }}\nRef: ${{ github.ref }}\nStatus: ${{ job.status}}' \ | ||||||
|  |       ${{ secrets.NTFY_URL }} | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
| ## Watchtower (shoutrrr) | ## Watchtower (shoutrrr) | ||||||
| You can use [shoutrrr](https://github.com/containrrr/shoutrrr) generic webhook support to send  | You can use [shoutrrr](https://github.com/containrrr/shoutrrr) generic webhook support to send  | ||||||
| [Watchtower](https://github.com/containrrr/watchtower/) notifications to your ntfy topic. | [Watchtower](https://github.com/containrrr/watchtower/) notifications to your ntfy topic. | ||||||
|  |  | ||||||
|  | @ -26,37 +26,37 @@ deb/rpm packages. | ||||||
| 
 | 
 | ||||||
| === "x86_64/amd64" | === "x86_64/amd64" | ||||||
|     ```bash |     ```bash | ||||||
|     wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_x86_64.tar.gz |     wget https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_x86_64.tar.gz | ||||||
|     tar zxvf ntfy_1.28.0_linux_x86_64.tar.gz |     tar zxvf ntfy_1.29.0_linux_x86_64.tar.gz | ||||||
|     sudo cp -a ntfy_1.28.0_linux_x86_64/ntfy /usr/bin/ntfy |     sudo cp -a ntfy_1.29.0_linux_x86_64/ntfy /usr/bin/ntfy | ||||||
|     sudo mkdir /etc/ntfy && sudo cp ntfy_1.28.0_linux_x86_64/{client,server}/*.yml /etc/ntfy |     sudo mkdir /etc/ntfy && sudo cp ntfy_1.29.0_linux_x86_64/{client,server}/*.yml /etc/ntfy | ||||||
|     sudo ntfy serve |     sudo ntfy serve | ||||||
|     ``` |     ``` | ||||||
| 
 | 
 | ||||||
| === "armv6" | === "armv6" | ||||||
|     ```bash |     ```bash | ||||||
|     wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_armv6.tar.gz |     wget https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_armv6.tar.gz | ||||||
|     tar zxvf ntfy_1.28.0_linux_armv6.tar.gz |     tar zxvf ntfy_1.29.0_linux_armv6.tar.gz | ||||||
|     sudo cp -a ntfy_1.28.0_linux_armv6/ntfy /usr/bin/ntfy |     sudo cp -a ntfy_1.29.0_linux_armv6/ntfy /usr/bin/ntfy | ||||||
|     sudo mkdir /etc/ntfy && sudo cp ntfy_1.28.0_linux_armv6/{client,server}/*.yml /etc/ntfy |     sudo mkdir /etc/ntfy && sudo cp ntfy_1.29.0_linux_armv6/{client,server}/*.yml /etc/ntfy | ||||||
|     sudo ntfy serve |     sudo ntfy serve | ||||||
|     ``` |     ``` | ||||||
| 
 | 
 | ||||||
| === "armv7/armhf" | === "armv7/armhf" | ||||||
|     ```bash |     ```bash | ||||||
|     wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_armv7.tar.gz |     wget https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_armv7.tar.gz | ||||||
|     tar zxvf ntfy_1.28.0_linux_armv7.tar.gz |     tar zxvf ntfy_1.29.0_linux_armv7.tar.gz | ||||||
|     sudo cp -a ntfy_1.28.0_linux_armv7/ntfy /usr/bin/ntfy |     sudo cp -a ntfy_1.29.0_linux_armv7/ntfy /usr/bin/ntfy | ||||||
|     sudo mkdir /etc/ntfy && sudo cp ntfy_1.28.0_linux_armv7/{client,server}/*.yml /etc/ntfy |     sudo mkdir /etc/ntfy && sudo cp ntfy_1.29.0_linux_armv7/{client,server}/*.yml /etc/ntfy | ||||||
|     sudo ntfy serve |     sudo ntfy serve | ||||||
|     ``` |     ``` | ||||||
| 
 | 
 | ||||||
| === "arm64" | === "arm64" | ||||||
|     ```bash |     ```bash | ||||||
|     wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_arm64.tar.gz |     wget https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_arm64.tar.gz | ||||||
|     tar zxvf ntfy_1.28.0_linux_arm64.tar.gz |     tar zxvf ntfy_1.29.0_linux_arm64.tar.gz | ||||||
|     sudo cp -a ntfy_1.28.0_linux_arm64/ntfy /usr/bin/ntfy |     sudo cp -a ntfy_1.29.0_linux_arm64/ntfy /usr/bin/ntfy | ||||||
|     sudo mkdir /etc/ntfy && sudo cp ntfy_1.28.0_linux_arm64/{client,server}/*.yml /etc/ntfy |     sudo mkdir /etc/ntfy && sudo cp ntfy_1.29.0_linux_arm64/{client,server}/*.yml /etc/ntfy | ||||||
|     sudo ntfy serve |     sudo ntfy serve | ||||||
|     ``` |     ``` | ||||||
| 
 | 
 | ||||||
|  | @ -106,7 +106,7 @@ Manually installing the .deb file: | ||||||
| 
 | 
 | ||||||
| === "x86_64/amd64" | === "x86_64/amd64" | ||||||
|     ```bash |     ```bash | ||||||
|     wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_amd64.deb |     wget https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_amd64.deb | ||||||
|     sudo dpkg -i ntfy_*.deb |     sudo dpkg -i ntfy_*.deb | ||||||
|     sudo systemctl enable ntfy |     sudo systemctl enable ntfy | ||||||
|     sudo systemctl start ntfy |     sudo systemctl start ntfy | ||||||
|  | @ -114,7 +114,7 @@ Manually installing the .deb file: | ||||||
| 
 | 
 | ||||||
| === "armv6" | === "armv6" | ||||||
|     ```bash |     ```bash | ||||||
|     wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_armv6.deb |     wget https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_armv6.deb | ||||||
|     sudo dpkg -i ntfy_*.deb |     sudo dpkg -i ntfy_*.deb | ||||||
|     sudo systemctl enable ntfy |     sudo systemctl enable ntfy | ||||||
|     sudo systemctl start ntfy |     sudo systemctl start ntfy | ||||||
|  | @ -122,7 +122,7 @@ Manually installing the .deb file: | ||||||
| 
 | 
 | ||||||
| === "armv7/armhf" | === "armv7/armhf" | ||||||
|     ```bash |     ```bash | ||||||
|     wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_armv7.deb |     wget https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_armv7.deb | ||||||
|     sudo dpkg -i ntfy_*.deb |     sudo dpkg -i ntfy_*.deb | ||||||
|     sudo systemctl enable ntfy |     sudo systemctl enable ntfy | ||||||
|     sudo systemctl start ntfy |     sudo systemctl start ntfy | ||||||
|  | @ -130,7 +130,7 @@ Manually installing the .deb file: | ||||||
| 
 | 
 | ||||||
| === "arm64" | === "arm64" | ||||||
|     ```bash |     ```bash | ||||||
|     wget https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_arm64.deb |     wget https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_arm64.deb | ||||||
|     sudo dpkg -i ntfy_*.deb |     sudo dpkg -i ntfy_*.deb | ||||||
|     sudo systemctl enable ntfy |     sudo systemctl enable ntfy | ||||||
|     sudo systemctl start ntfy |     sudo systemctl start ntfy | ||||||
|  | @ -140,28 +140,28 @@ Manually installing the .deb file: | ||||||
| 
 | 
 | ||||||
| === "x86_64/amd64" | === "x86_64/amd64" | ||||||
|     ```bash |     ```bash | ||||||
|     sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_amd64.rpm |     sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_amd64.rpm | ||||||
|     sudo systemctl enable ntfy  |     sudo systemctl enable ntfy  | ||||||
|     sudo systemctl start ntfy |     sudo systemctl start ntfy | ||||||
|     ``` |     ``` | ||||||
| 
 | 
 | ||||||
| === "armv6" | === "armv6" | ||||||
|     ```bash |     ```bash | ||||||
|     sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_armv6.rpm |     sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_armv6.rpm | ||||||
|     sudo systemctl enable ntfy |     sudo systemctl enable ntfy | ||||||
|     sudo systemctl start ntfy |     sudo systemctl start ntfy | ||||||
|     ``` |     ``` | ||||||
| 
 | 
 | ||||||
| === "armv7/armhf" | === "armv7/armhf" | ||||||
|     ```bash |     ```bash | ||||||
|     sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_armv7.rpm |     sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_armv7.rpm | ||||||
|     sudo systemctl enable ntfy  |     sudo systemctl enable ntfy  | ||||||
|     sudo systemctl start ntfy |     sudo systemctl start ntfy | ||||||
|     ``` |     ``` | ||||||
| 
 | 
 | ||||||
| === "arm64" | === "arm64" | ||||||
|     ```bash |     ```bash | ||||||
|     sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_linux_arm64.rpm |     sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_linux_arm64.rpm | ||||||
|     sudo systemctl enable ntfy  |     sudo systemctl enable ntfy  | ||||||
|     sudo systemctl start ntfy |     sudo systemctl start ntfy | ||||||
|     ``` |     ``` | ||||||
|  | @ -189,18 +189,18 @@ NixOS also supports [declarative setup of the ntfy server](https://search.nixos. | ||||||
| 
 | 
 | ||||||
| ## macOS | ## macOS | ||||||
| The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well.  | The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well.  | ||||||
| To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_macOS_all.tar.gz),  | To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_macOS_all.tar.gz),  | ||||||
| extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`).  | extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`).  | ||||||
| 
 | 
 | ||||||
| If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at  | If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at  | ||||||
| `~/Library/Application Support/ntfy/client.yml` (sample included in the tarball). | `~/Library/Application Support/ntfy/client.yml` (sample included in the tarball). | ||||||
| 
 | 
 | ||||||
| ```bash | ```bash | ||||||
| curl -L https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_macOS_all.tar.gz > ntfy_1.28.0_macOS_all.tar.gz | curl -L https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_macOS_all.tar.gz > ntfy_1.29.0_macOS_all.tar.gz | ||||||
| tar zxvf ntfy_1.28.0_macOS_all.tar.gz | tar zxvf ntfy_1.29.0_macOS_all.tar.gz | ||||||
| sudo cp -a ntfy_1.28.0_macOS_all/ntfy /usr/local/bin/ntfy | sudo cp -a ntfy_1.29.0_macOS_all/ntfy /usr/local/bin/ntfy | ||||||
| mkdir ~/Library/Application\ Support/ntfy  | mkdir ~/Library/Application\ Support/ntfy  | ||||||
| cp ntfy_1.28.0_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml | cp ntfy_1.29.0_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml | ||||||
| ntfy --help | ntfy --help | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
|  | @ -212,7 +212,7 @@ ntfy --help | ||||||
| 
 | 
 | ||||||
| ## Windows | ## Windows | ||||||
| The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well. | The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well. | ||||||
| To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v1.28.0/ntfy_1.28.0_windows_x86_64.zip), | To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v1.29.0/ntfy_1.29.0_windows_x86_64.zip), | ||||||
| extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.  | extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.  | ||||||
| 
 | 
 | ||||||
| The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file). | The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file). | ||||||
|  | @ -431,7 +431,7 @@ Configuration is relatively straightforward. As an example, a minimal configurat | ||||||
|     metadata: |     metadata: | ||||||
|       name: ntfy |       name: ntfy | ||||||
|     data: |     data: | ||||||
|     server.yml: | |       server.yml: | | ||||||
|         # Template: https://github.com/binwiederhier/ntfy/blob/main/server/server.yml |         # Template: https://github.com/binwiederhier/ntfy/blob/main/server/server.yml | ||||||
|         base-url: https://ntfy.sh |         base-url: https://ntfy.sh | ||||||
|     ``` |     ``` | ||||||
|  |  | ||||||
|  | @ -47,6 +47,7 @@ messages until I finally finish implementing end-to-end encryption. | ||||||
| - [ntfy-middleman](https://github.com/nachotp/ntfy-middleman) - Wraps APIs and send notifications using ntfy.sh on schedule (Python) | - [ntfy-middleman](https://github.com/nachotp/ntfy-middleman) - Wraps APIs and send notifications using ntfy.sh on schedule (Python) | ||||||
| - [ntfy-dotnet](https://github.com/nwithan8/ntfy-dotnet) - .NET client library to interact with a ntfy server (C# / .NET) | - [ntfy-dotnet](https://github.com/nwithan8/ntfy-dotnet) - .NET client library to interact with a ntfy server (C# / .NET) | ||||||
| - [node-ntfy-publish](https://github.com/cityssm/node-ntfy-publish) - A Node package to publish notifications to an ntfy server (Node) | - [node-ntfy-publish](https://github.com/cityssm/node-ntfy-publish) - A Node package to publish notifications to an ntfy server (Node) | ||||||
|  | - [ntfy](https://github.com/jonocarroll/ntfy) - Wraps the ntfy API with pipe-friendly tooling (R) | ||||||
| 
 | 
 | ||||||
| ## CLIs + GUIs | ## CLIs + GUIs | ||||||
| 
 | 
 | ||||||
|  | @ -88,9 +89,16 @@ messages until I finally finish implementing end-to-end encryption. | ||||||
| - [ntfy-to-slack](https://github.com/ozskywalker/ntfy-to-slack) - Tool to subscribe to a ntfy topic and send the messages to a Slack webhook (Go) | - [ntfy-to-slack](https://github.com/ozskywalker/ntfy-to-slack) - Tool to subscribe to a ntfy topic and send the messages to a Slack webhook (Go) | ||||||
| - [ansible-ntfy](https://github.com/jpmens/ansible-ntfy) - Ansible action plugin to post JSON messages to ntfy (Python) | - [ansible-ntfy](https://github.com/jpmens/ansible-ntfy) - Ansible action plugin to post JSON messages to ntfy (Python) | ||||||
| - [ntfy-notification-channel](https://github.com/wijourdil/ntfy-notification-channel) - Laravel Notification channel for ntfy (PHP) | - [ntfy-notification-channel](https://github.com/wijourdil/ntfy-notification-channel) - Laravel Notification channel for ntfy (PHP) | ||||||
|  | - [ntfy_on_a_chip](https://github.com/gergepalfi/ntfy_on_a_chip) - ESP8266 and ESP32 client code to communicate with ntfy | ||||||
|  | - [ntfy-sdk](https://gitlab.com/p2kishimoto/ntfy-sdk) - ntfy client library to send notifications (Rust) | ||||||
| 
 | 
 | ||||||
| ## Blog + forum posts | ## Blog + forum posts | ||||||
| 
 | 
 | ||||||
|  | - [Tracking layoffs, tech worker demand still high, ntfy, devenv, Markdoc & Mike Bifulco](https://changelog.com/news/tracking-layoffs-tech-worker-demand-still-high-ntfy-devenv-markdoc-mike-bifulco-Y1jW) - 11/2022 | ||||||
|  | - [Pointer | Issue #367](https://www.pointer.io/archives/a9495a2a6f/) - 11/2022 | ||||||
|  | - [Envie Push Notifications por POST (de graça e sem cadastro)](https://www.tabnews.com.br/filipedeschamps/envie-push-notifications-por-post-de-graca-e-sem-cadastro) - 11/2022 | ||||||
|  | - [Push Notifications for KDE](https://volkerkrause.eu/2022/11/12/kde-unifiedpush-push-notifications.html) - 11/2022 | ||||||
|  | - [TLDR Newsletter Daily Update 2022-11-09](https://tldr.tech/tech/newsletter/2022-11-09) - 11/2022 | ||||||
| - [Ntfy.sh – Send push notifications to your phone via PUT/POST](https://news.ycombinator.com/item?id=33517944) ⭐ - 11/2022 | - [Ntfy.sh – Send push notifications to your phone via PUT/POST](https://news.ycombinator.com/item?id=33517944) ⭐ - 11/2022 | ||||||
| - [Ntfy et Jeedom : un plugin](https://lunarok-domotique.com/2022/11/ntfy-et-jeedom/) - 11/2022 | - [Ntfy et Jeedom : un plugin](https://lunarok-domotique.com/2022/11/ntfy-et-jeedom/) - 11/2022 | ||||||
| - [Crea tu propio servidor de notificaciones con Ntfy](https://blog.parravidales.es/crea-tu-propio-servidor-de-notificaciones-con-ntfy/) - 11/2022 | - [Crea tu propio servidor de notificaciones con Ntfy](https://blog.parravidales.es/crea-tu-propio-servidor-de-notificaciones-con-ntfy/) - 11/2022 | ||||||
|  |  | ||||||
|  | @ -4,12 +4,40 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release | ||||||
| 
 | 
 | ||||||
| ## ntfy Android app v1.14.0 (UNRELEASED) | ## ntfy Android app v1.14.0 (UNRELEASED) | ||||||
| 
 | 
 | ||||||
|  | **Bug fixes:** | ||||||
|  | 
 | ||||||
|  | * Remove timestamp when copying message text ([#471](https://github.com/binwiederhier/ntfy/issues/471), thanks to [@wunter8](https://github.com/wunter8)) | ||||||
|  | 
 | ||||||
| **Additional translations:** | **Additional translations:** | ||||||
| 
 | 
 | ||||||
| * Korean (thanks to [@YJSofta0f97461d82447ac](https://hosted.weblate.org/user/YJSofta0f97461d82447ac/)) | * Korean (thanks to [@YJSofta0f97461d82447ac](https://hosted.weblate.org/user/YJSofta0f97461d82447ac/)) | ||||||
| 
 | 
 | ||||||
|  | ## ntfy server v1.30.0 (UNRELREASED) | ||||||
| 
 | 
 | ||||||
| ## ntfy server v1.29.0 (UNRELEASED) | **Features:** | ||||||
|  | 
 | ||||||
|  | * High-load servers: Allow asynchronous batch-writing of messages to cache via `cache-batch-*` options ([#498](https://github.com/binwiederhier/ntfy/issues/498)/[#502](https://github.com/binwiederhier/ntfy/pull/502))    | ||||||
|  | 
 | ||||||
|  | **Documentation:** | ||||||
|  | 
 | ||||||
|  | * GitHub Actions example ([#492](https://github.com/binwiederhier/ntfy/pull/492), thanks to [@ksurl](https://github.com/ksurl)) | ||||||
|  | * UnifiedPush ACL clarification ([#497](https://github.com/binwiederhier/ntfy/issues/497), thanks to [@bt90](https://github.com/bt90))  | ||||||
|  | 
 | ||||||
|  | **Other things:** | ||||||
|  | 
 | ||||||
|  | * Put ntfy.sh docs on GitHub pages to reduce AWS outbound traffic cost ([#491](https://github.com/binwiederhier/ntfy/issues/491)) | ||||||
|  | * The ntfy.sh server hardware was upgraded to a bigger box. If you'd like to help out carrying the server cost, **[sponsorships and donations](https://github.com/sponsors/binwiederhier)** 💸 would be very much appreciated | ||||||
|  | 
 | ||||||
|  | ## ntfy server v1.29.0 | ||||||
|  | Released November 12, 2022 | ||||||
|  | 
 | ||||||
|  | This release adds the ability to add rate limit exemptions for IP ranges instead of just specific IP addresses. It also fixes  | ||||||
|  | a few bugs in the web app and the CLI and adds lots of new examples and install instructions. | ||||||
|  | 
 | ||||||
|  | Thanks to [some love on HN](https://news.ycombinator.com/item?id=33517944), we got so many new ntfy users trying out ntfy | ||||||
|  | and joining the [chat rooms](https://github.com/binwiederhier/ntfy#chat--forum). **Welcome to the ntfy community to all of you!**  | ||||||
|  | We also got a ton of new **[sponsors and donations](https://github.com/sponsors/binwiederhier)** 💸, which is amazing. I'd like to thank | ||||||
|  | all of you for believing in the project, and for helping me pay the server cost. The HN spike increased the AWS cost quite a bit. | ||||||
| 
 | 
 | ||||||
| **Features:** | **Features:** | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										12
									
								
								go.mod
									
										
									
									
									
								
							
							
						
						
									
										12
									
								
								go.mod
									
										
									
									
									
								
							|  | @ -14,8 +14,8 @@ require ( | ||||||
| 	github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6 | 	github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6 | ||||||
| 	github.com/stretchr/testify v1.8.1 | 	github.com/stretchr/testify v1.8.1 | ||||||
| 	github.com/urfave/cli/v2 v2.23.5 | 	github.com/urfave/cli/v2 v2.23.5 | ||||||
| 	golang.org/x/crypto v0.1.0 | 	golang.org/x/crypto v0.3.0 | ||||||
| 	golang.org/x/oauth2 v0.1.0 // indirect | 	golang.org/x/oauth2 v0.2.0 // indirect | ||||||
| 	golang.org/x/sync v0.1.0 | 	golang.org/x/sync v0.1.0 | ||||||
| 	golang.org/x/term v0.2.0 | 	golang.org/x/term v0.2.0 | ||||||
| 	golang.org/x/time v0.2.0 | 	golang.org/x/time v0.2.0 | ||||||
|  | @ -25,17 +25,19 @@ require ( | ||||||
| 
 | 
 | ||||||
| require github.com/pkg/errors v0.9.1 // indirect | require github.com/pkg/errors v0.9.1 // indirect | ||||||
| 
 | 
 | ||||||
| require firebase.google.com/go/v4 v4.9.0 | require firebase.google.com/go/v4 v4.10.0 | ||||||
| 
 | 
 | ||||||
| require ( | require ( | ||||||
| 	cloud.google.com/go v0.105.0 // indirect | 	cloud.google.com/go v0.107.0 // indirect | ||||||
| 	cloud.google.com/go/compute v1.12.1 // indirect | 	cloud.google.com/go/compute v1.12.1 // indirect | ||||||
| 	cloud.google.com/go/compute/metadata v0.2.1 // indirect | 	cloud.google.com/go/compute/metadata v0.2.1 // indirect | ||||||
| 	cloud.google.com/go/iam v0.7.0 // indirect | 	cloud.google.com/go/iam v0.7.0 // indirect | ||||||
| 	cloud.google.com/go/longrunning v0.3.0 // indirect | 	cloud.google.com/go/longrunning v0.3.0 // indirect | ||||||
| 	github.com/AlekSi/pointer v1.2.0 // indirect | 	github.com/AlekSi/pointer v1.2.0 // indirect | ||||||
|  | 	github.com/MicahParks/keyfunc v1.5.3 // indirect | ||||||
| 	github.com/davecgh/go-spew v1.1.1 // indirect | 	github.com/davecgh/go-spew v1.1.1 // indirect | ||||||
| 	github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead // indirect | 	github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead // indirect | ||||||
|  | 	github.com/golang-jwt/jwt/v4 v4.4.2 // indirect | ||||||
| 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect | 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect | ||||||
| 	github.com/golang/protobuf v1.5.2 // indirect | 	github.com/golang/protobuf v1.5.2 // indirect | ||||||
| 	github.com/google/go-cmp v0.5.9 // indirect | 	github.com/google/go-cmp v0.5.9 // indirect | ||||||
|  | @ -52,7 +54,7 @@ require ( | ||||||
| 	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect | 	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect | ||||||
| 	google.golang.org/appengine v1.6.7 // indirect | 	google.golang.org/appengine v1.6.7 // indirect | ||||||
| 	google.golang.org/appengine/v2 v2.0.2 // indirect | 	google.golang.org/appengine/v2 v2.0.2 // indirect | ||||||
| 	google.golang.org/genproto v0.0.0-20221107162902-2d387536bcdd // indirect | 	google.golang.org/genproto v0.0.0-20221116193143-41c2ba794472 // indirect | ||||||
| 	google.golang.org/grpc v1.50.1 // indirect | 	google.golang.org/grpc v1.50.1 // indirect | ||||||
| 	google.golang.org/protobuf v1.28.1 // indirect | 	google.golang.org/protobuf v1.28.1 // indirect | ||||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||||
|  |  | ||||||
							
								
								
									
										57
									
								
								go.sum
									
										
									
									
									
								
							
							
						
						
									
										57
									
								
								go.sum
									
										
									
									
									
								
							|  | @ -1,32 +1,30 @@ | ||||||
| cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= | ||||||
| cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y= | cloud.google.com/go v0.106.0 h1:AWaMWuZb2oFeiV91OfNHZbmwUhMVuXEaLPm9sqDAOl8= | ||||||
| cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= | cloud.google.com/go v0.106.0/go.mod h1:5NEGxGuIeMQiPaWLwLYZ7kfNWiP6w1+QJK+xqyIT+dw= | ||||||
|  | cloud.google.com/go v0.107.0 h1:qkj22L7bgkl6vIeZDlOY2po43Mx/TIa2Wsa7VR+PEww= | ||||||
|  | cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= | ||||||
| cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0= | cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0= | ||||||
| cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= | cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= | ||||||
| cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48= | cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48= | ||||||
| cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= | cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= | ||||||
| cloud.google.com/go/firestore v1.8.0 h1:HokMB9Io0hAyYzlGFeFVMgE3iaPXNvaIsDx5JzblGLI= | cloud.google.com/go/firestore v1.8.0 h1:HokMB9Io0hAyYzlGFeFVMgE3iaPXNvaIsDx5JzblGLI= | ||||||
| cloud.google.com/go/firestore v1.8.0/go.mod h1:r3KB8cAdRIe8znzoPWLw8S6gpDVd9treohhn8b09424= | cloud.google.com/go/firestore v1.8.0/go.mod h1:r3KB8cAdRIe8znzoPWLw8S6gpDVd9treohhn8b09424= | ||||||
| cloud.google.com/go/iam v0.6.0 h1:nsqQC88kT5Iwlm4MeNGTpfMWddp6NB/UOLFTH6m1QfQ= |  | ||||||
| cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= |  | ||||||
| cloud.google.com/go/iam v0.7.0 h1:k4MuwOsS7zGJJ+QfZ5vBK8SgHBAvYN/23BWsiihJ1vs= | cloud.google.com/go/iam v0.7.0 h1:k4MuwOsS7zGJJ+QfZ5vBK8SgHBAvYN/23BWsiihJ1vs= | ||||||
| cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= | cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= | ||||||
| cloud.google.com/go/longrunning v0.2.1 h1:x3E/YapFCMe2G1D9qCv9COrBldOwK/n0OC7w9PLzeX0= |  | ||||||
| cloud.google.com/go/longrunning v0.2.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= |  | ||||||
| cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs= | cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs= | ||||||
| cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= | cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= | ||||||
| cloud.google.com/go/storage v1.27.0 h1:YOO045NZI9RKfCj1c5A/ZtuuENUc8OAW+gHdGnDgyMQ= |  | ||||||
| cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= |  | ||||||
| cloud.google.com/go/storage v1.28.0 h1:DLrIZ6xkeZX6K70fU/boWx5INJumt6f+nwwWSHXzzGY= | cloud.google.com/go/storage v1.28.0 h1:DLrIZ6xkeZX6K70fU/boWx5INJumt6f+nwwWSHXzzGY= | ||||||
| cloud.google.com/go/storage v1.28.0/go.mod h1:qlgZML35PXA3zoEnIkiPLY4/TOkUleufRlu6qmcf7sI= | cloud.google.com/go/storage v1.28.0/go.mod h1:qlgZML35PXA3zoEnIkiPLY4/TOkUleufRlu6qmcf7sI= | ||||||
| firebase.google.com/go/v4 v4.9.0 h1:VCagv+hYOxUGeuyu7J+o2rKJkDp5JQBbA3Bzlof+LMk= | firebase.google.com/go/v4 v4.10.0 h1:dgK/8uwfJbzc5LZK/GyRRfIkZEDObN9q0kgEXsjlXN4= | ||||||
| firebase.google.com/go/v4 v4.9.0/go.mod h1:bHhRkM3VtGJx19rQdW7GDNLdnA8/T6SsnN5nXk/xdw8= | firebase.google.com/go/v4 v4.10.0/go.mod h1:m0gLwPY9fxKggizzglgCNWOGnFnVPifLpqZzo5u3e/A= | ||||||
| github.com/AlekSi/pointer v1.0.0/go.mod h1:1kjywbfcPFCmncIxtk6fIEub6LKrfMz3gc5QKVOSOA8= | github.com/AlekSi/pointer v1.0.0/go.mod h1:1kjywbfcPFCmncIxtk6fIEub6LKrfMz3gc5QKVOSOA8= | ||||||
| github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= | github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= | ||||||
| github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= | github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= | ||||||
| github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= | ||||||
| github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= | github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= | ||||||
| github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= | ||||||
|  | github.com/MicahParks/keyfunc v1.5.3 h1:Y+mv+kX3HtL7/dCXXzK4bIDBHg91eunnGGkdndO0RWk= | ||||||
|  | github.com/MicahParks/keyfunc v1.5.3/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= | ||||||
| github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= | ||||||
| github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= | ||||||
| github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= | ||||||
|  | @ -46,6 +44,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m | ||||||
| github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= | ||||||
| github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q= | github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q= | ||||||
| github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M= | github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M= | ||||||
|  | github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= | ||||||
|  | github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= | ||||||
| github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= | ||||||
| github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= | ||||||
| github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= | ||||||
|  | @ -79,8 +79,6 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= | ||||||
| github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||||
| github.com/googleapis/enterprise-certificate-proxy v0.2.0 h1:y8Yozv7SZtlU//QXbezB6QkpuE6jMD2/gfzk4AftXjs= | github.com/googleapis/enterprise-certificate-proxy v0.2.0 h1:y8Yozv7SZtlU//QXbezB6QkpuE6jMD2/gfzk4AftXjs= | ||||||
| github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= | github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= | ||||||
| github.com/googleapis/gax-go/v2 v2.6.0 h1:SXk3ABtQYDT/OH8jAyvEOQ58mgawq5C4o/4/89qN2ZU= |  | ||||||
| github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= |  | ||||||
| github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ= | github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ= | ||||||
| github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= | github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= | ||||||
| github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= | ||||||
|  | @ -101,27 +99,22 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ | ||||||
| github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | ||||||
| github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | ||||||
| github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||||||
| github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= |  | ||||||
| github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= |  | ||||||
| github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= |  | ||||||
| github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||||
| github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | ||||||
| github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= | ||||||
| github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||||||
| github.com/urfave/cli/v2 v2.23.0 h1:pkly7gKIeYv3olPAeNajNpLjeJrmTPYCoZWaV+2VfvE= |  | ||||||
| github.com/urfave/cli/v2 v2.23.0/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI= |  | ||||||
| github.com/urfave/cli/v2 v2.23.5 h1:xbrU7tAYviSpqeR3X4nEFWUdB/uDZ6DE+HxmRU7Xtyw= | github.com/urfave/cli/v2 v2.23.5 h1:xbrU7tAYviSpqeR3X4nEFWUdB/uDZ6DE+HxmRU7Xtyw= | ||||||
| github.com/urfave/cli/v2 v2.23.5/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= | github.com/urfave/cli/v2 v2.23.5/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= | ||||||
| github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= | ||||||
| github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= | ||||||
| go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= |  | ||||||
| go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= |  | ||||||
| go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= | ||||||
| go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= | ||||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||||
| golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||||
| golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= | golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE= | ||||||
| golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= | golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= | ||||||
|  | golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= | ||||||
|  | golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= | ||||||
| golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= | ||||||
| golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= | ||||||
| golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= | ||||||
|  | @ -135,13 +128,11 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR | ||||||
| golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= | ||||||
| golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | ||||||
| golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | ||||||
| golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= |  | ||||||
| golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= |  | ||||||
| golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= | golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= | ||||||
| golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= | ||||||
| golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | ||||||
| golang.org/x/oauth2 v0.1.0 h1:isLCZuhj4v+tYv7eskaN4v/TM+A1begWWgyVJDdl1+Y= | golang.org/x/oauth2 v0.2.0 h1:GtQkldQ9m7yvzCL1V+LrYow3Khe0eJH0w7RbX/VbaIU= | ||||||
| golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A= | golang.org/x/oauth2 v0.2.0/go.mod h1:Cwn6afJ8jrQwYMxQDTpISoXmXW9I6qF6vDeuuoX3Ibs= | ||||||
| golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
| golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
| golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
|  | @ -153,13 +144,9 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w | ||||||
| golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
| golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= |  | ||||||
| golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |  | ||||||
| golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= | golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= | ||||||
| golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||||||
| golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw= |  | ||||||
| golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= |  | ||||||
| golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM= | golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM= | ||||||
| golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= | ||||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||||
|  | @ -168,8 +155,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||||
| golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | ||||||
| golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= | golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= | ||||||
| golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | ||||||
| golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= |  | ||||||
| golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= |  | ||||||
| golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE= | golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE= | ||||||
| golang.org/x/time v0.2.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | golang.org/x/time v0.2.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | ||||||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||||
|  | @ -180,8 +165,6 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn | ||||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||||
| golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= | ||||||
| golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= | ||||||
| google.golang.org/api v0.102.0 h1:JxJl2qQ85fRMPNvlZY/enexbxpCjLwGhZUtgfGeQ51I= |  | ||||||
| google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= |  | ||||||
| google.golang.org/api v0.103.0 h1:9yuVqlu2JCvcLg9p8S3fcFLZij8EPSyvODIY1rkMizQ= | google.golang.org/api v0.103.0 h1:9yuVqlu2JCvcLg9p8S3fcFLZij8EPSyvODIY1rkMizQ= | ||||||
| google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= | google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= | ||||||
| google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= | ||||||
|  | @ -193,10 +176,10 @@ google.golang.org/appengine/v2 v2.0.2/go.mod h1:PkgRUWz4o1XOvbqtWTkBtCitEJ5Tp4Ho | ||||||
| google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= | ||||||
| google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= | ||||||
| google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= | ||||||
| google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c h1:QgY/XxIAIeccR+Ca/rDdKubLIU9rcJ3xfy1DC/Wd2Oo= | google.golang.org/genproto v0.0.0-20221111202108-142d8a6fa32e h1:azcyH5lGzGy7pkLCbhPe0KkKxsM7c6UA/FZIXImKE7M= | ||||||
| google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= | google.golang.org/genproto v0.0.0-20221111202108-142d8a6fa32e/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= | ||||||
| google.golang.org/genproto v0.0.0-20221107162902-2d387536bcdd h1:1eV6KuDTxraYYsYGWksp1thEGP+8dtX/TINL9h+ppiI= | google.golang.org/genproto v0.0.0-20221116193143-41c2ba794472 h1:kIfItBRE5gkUKpH4H5lNGciZbka1JrmRli3ArqrKFkA= | ||||||
| google.golang.org/genproto v0.0.0-20221107162902-2d387536bcdd/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= | google.golang.org/genproto v0.0.0-20221116193143-41c2ba794472/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= | ||||||
| google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= | ||||||
| google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= | ||||||
| google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= | ||||||
|  |  | ||||||
|  | @ -61,6 +61,8 @@ type Config struct { | ||||||
| 	CacheFile                            string | 	CacheFile                            string | ||||||
| 	CacheDuration                        time.Duration | 	CacheDuration                        time.Duration | ||||||
| 	CacheStartupQueries                  string | 	CacheStartupQueries                  string | ||||||
|  | 	CacheBatchSize                       int | ||||||
|  | 	CacheBatchTimeout                    time.Duration | ||||||
| 	AuthFile                             string | 	AuthFile                             string | ||||||
| 	AuthDefaultRead                      bool | 	AuthDefaultRead                      bool | ||||||
| 	AuthDefaultWrite                     bool | 	AuthDefaultWrite                     bool | ||||||
|  | @ -114,6 +116,8 @@ func NewConfig() *Config { | ||||||
| 		FirebaseKeyFile:                      "", | 		FirebaseKeyFile:                      "", | ||||||
| 		CacheFile:                            "", | 		CacheFile:                            "", | ||||||
| 		CacheDuration:                        DefaultCacheDuration, | 		CacheDuration:                        DefaultCacheDuration, | ||||||
|  | 		CacheBatchSize:                       0, | ||||||
|  | 		CacheBatchTimeout:                    0, | ||||||
| 		AuthFile:                             "", | 		AuthFile:                             "", | ||||||
| 		AuthDefaultRead:                      true, | 		AuthDefaultRead:                      true, | ||||||
| 		AuthDefaultWrite:                     true, | 		AuthDefaultWrite:                     true, | ||||||
|  |  | ||||||
|  | @ -44,6 +44,7 @@ const ( | ||||||
| 			published INT NOT NULL | 			published INT NOT NULL | ||||||
| 		); | 		); | ||||||
| 		CREATE INDEX IF NOT EXISTS idx_mid ON messages (mid); | 		CREATE INDEX IF NOT EXISTS idx_mid ON messages (mid); | ||||||
|  | 		CREATE INDEX IF NOT EXISTS idx_time ON messages (time); | ||||||
| 		CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic); | 		CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic); | ||||||
| 		COMMIT; | 		COMMIT; | ||||||
| 	` | 	` | ||||||
|  | @ -92,7 +93,7 @@ const ( | ||||||
| 
 | 
 | ||||||
| // Schema management queries | // Schema management queries | ||||||
| const ( | const ( | ||||||
| 	currentSchemaVersion          = 8 | 	currentSchemaVersion          = 9 | ||||||
| 	createSchemaVersionTableQuery = ` | 	createSchemaVersionTableQuery = ` | ||||||
| 		CREATE TABLE IF NOT EXISTS schemaVersion ( | 		CREATE TABLE IF NOT EXISTS schemaVersion ( | ||||||
| 			id INT PRIMARY KEY, | 			id INT PRIMARY KEY, | ||||||
|  | @ -185,15 +186,21 @@ const ( | ||||||
| 	migrate7To8AlterMessagesTableQuery = ` | 	migrate7To8AlterMessagesTableQuery = ` | ||||||
| 		ALTER TABLE messages ADD COLUMN icon TEXT NOT NULL DEFAULT(''); | 		ALTER TABLE messages ADD COLUMN icon TEXT NOT NULL DEFAULT(''); | ||||||
| 	` | 	` | ||||||
|  | 
 | ||||||
|  | 	// 8 -> 9 | ||||||
|  | 	migrate8To9AlterMessagesTableQuery = ` | ||||||
|  | 		CREATE INDEX IF NOT EXISTS idx_time ON messages (time);	 | ||||||
|  | 	` | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type messageCache struct { | type messageCache struct { | ||||||
| 	db  *sql.DB | 	db    *sql.DB | ||||||
| 	nop bool | 	queue *util.BatchingQueue[*message] | ||||||
|  | 	nop   bool | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // newSqliteCache creates a SQLite file-backed cache | // newSqliteCache creates a SQLite file-backed cache | ||||||
| func newSqliteCache(filename, startupQueries string, nop bool) (*messageCache, error) { | func newSqliteCache(filename, startupQueries string, batchSize int, batchTimeout time.Duration, nop bool) (*messageCache, error) { | ||||||
| 	db, err := sql.Open("sqlite3", filename) | 	db, err := sql.Open("sqlite3", filename) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
|  | @ -201,21 +208,28 @@ func newSqliteCache(filename, startupQueries string, nop bool) (*messageCache, e | ||||||
| 	if err := setupCacheDB(db, startupQueries); err != nil { | 	if err := setupCacheDB(db, startupQueries); err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	return &messageCache{ | 	var queue *util.BatchingQueue[*message] | ||||||
| 		db:  db, | 	if batchSize > 0 || batchTimeout > 0 { | ||||||
| 		nop: nop, | 		queue = util.NewBatchingQueue[*message](batchSize, batchTimeout) | ||||||
| 	}, nil | 	} | ||||||
|  | 	cache := &messageCache{ | ||||||
|  | 		db:    db, | ||||||
|  | 		queue: queue, | ||||||
|  | 		nop:   nop, | ||||||
|  | 	} | ||||||
|  | 	go cache.processMessageBatches() | ||||||
|  | 	return cache, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // newMemCache creates an in-memory cache | // newMemCache creates an in-memory cache | ||||||
| func newMemCache() (*messageCache, error) { | func newMemCache() (*messageCache, error) { | ||||||
| 	return newSqliteCache(createMemoryFilename(), "", false) | 	return newSqliteCache(createMemoryFilename(), "", 0, 0, false) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // newNopCache creates an in-memory cache that discards all messages; | // newNopCache creates an in-memory cache that discards all messages; | ||||||
| // it is always empty and can be used if caching is entirely disabled | // it is always empty and can be used if caching is entirely disabled | ||||||
| func newNopCache() (*messageCache, error) { | func newNopCache() (*messageCache, error) { | ||||||
| 	return newSqliteCache(createMemoryFilename(), "", true) | 	return newSqliteCache(createMemoryFilename(), "", 0, 0, true) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // createMemoryFilename creates a unique memory filename to use for the SQLite backend. | // createMemoryFilename creates a unique memory filename to use for the SQLite backend. | ||||||
|  | @ -228,14 +242,23 @@ func createMemoryFilename() string { | ||||||
| 	return fmt.Sprintf("file:%s?mode=memory&cache=shared", util.RandomString(10)) | 	return fmt.Sprintf("file:%s?mode=memory&cache=shared", util.RandomString(10)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // AddMessage stores a message to the message cache synchronously, or queues it to be stored at a later date asyncronously. | ||||||
|  | // The message is queued only if "batchSize" or "batchTimeout" are passed to the constructor. | ||||||
| func (c *messageCache) AddMessage(m *message) error { | func (c *messageCache) AddMessage(m *message) error { | ||||||
|  | 	if c.queue != nil { | ||||||
|  | 		c.queue.Enqueue(m) | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
| 	return c.addMessages([]*message{m}) | 	return c.addMessages([]*message{m}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // addMessages synchronously stores a match of messages. If the database is locked, the transaction waits until | ||||||
|  | // SQLite's busy_timeout is exceeded before erroring out. | ||||||
| func (c *messageCache) addMessages(ms []*message) error { | func (c *messageCache) addMessages(ms []*message) error { | ||||||
| 	if c.nop { | 	if c.nop { | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|  | 	start := time.Now() | ||||||
| 	tx, err := c.db.Begin() | 	tx, err := c.db.Begin() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
|  | @ -289,7 +312,12 @@ func (c *messageCache) addMessages(ms []*message) error { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return tx.Commit() | 	if err := tx.Commit(); err != nil { | ||||||
|  | 		log.Error("Cache: Writing %d message(s) failed (took %v)", len(ms), time.Since(start)) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	log.Debug("Cache: Wrote %d message(s) in %v", len(ms), time.Since(start)) | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (c *messageCache) Messages(topic string, since sinceMarker, scheduled bool) ([]*message, error) { | func (c *messageCache) Messages(topic string, since sinceMarker, scheduled bool) ([]*message, error) { | ||||||
|  | @ -395,8 +423,12 @@ func (c *messageCache) Topics() (map[string]*topic, error) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (c *messageCache) Prune(olderThan time.Time) error { | func (c *messageCache) Prune(olderThan time.Time) error { | ||||||
| 	_, err := c.db.Exec(pruneMessagesQuery, olderThan.Unix()) | 	start := time.Now() | ||||||
| 	return err | 	if _, err := c.db.Exec(pruneMessagesQuery, olderThan.Unix()); err != nil { | ||||||
|  | 		log.Warn("Cache: Pruning failed (after %v): %s", time.Since(start), err.Error()) | ||||||
|  | 	} | ||||||
|  | 	log.Debug("Cache: Pruning successful (took %v)", time.Since(start)) | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (c *messageCache) AttachmentBytesUsed(sender string) (int64, error) { | func (c *messageCache) AttachmentBytesUsed(sender string) (int64, error) { | ||||||
|  | @ -417,6 +449,17 @@ func (c *messageCache) AttachmentBytesUsed(sender string) (int64, error) { | ||||||
| 	return size, nil | 	return size, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (c *messageCache) processMessageBatches() { | ||||||
|  | 	if c.queue == nil { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	for messages := range c.queue.Dequeue() { | ||||||
|  | 		if err := c.addMessages(messages); err != nil { | ||||||
|  | 			log.Error("Cache: %s", err.Error()) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func readMessages(rows *sql.Rows) ([]*message, error) { | func readMessages(rows *sql.Rows) ([]*message, error) { | ||||||
| 	defer rows.Close() | 	defer rows.Close() | ||||||
| 	messages := make([]*message, 0) | 	messages := make([]*message, 0) | ||||||
|  | @ -542,6 +585,8 @@ func setupCacheDB(db *sql.DB, startupQueries string) error { | ||||||
| 		return migrateFrom6(db) | 		return migrateFrom6(db) | ||||||
| 	} else if schemaVersion == 7 { | 	} else if schemaVersion == 7 { | ||||||
| 		return migrateFrom7(db) | 		return migrateFrom7(db) | ||||||
|  | 	} else if schemaVersion == 8 { | ||||||
|  | 		return migrateFrom8(db) | ||||||
| 	} | 	} | ||||||
| 	return fmt.Errorf("unexpected schema version found: %d", schemaVersion) | 	return fmt.Errorf("unexpected schema version found: %d", schemaVersion) | ||||||
| } | } | ||||||
|  | @ -647,5 +692,16 @@ func migrateFrom7(db *sql.DB) error { | ||||||
| 	if _, err := db.Exec(updateSchemaVersion, 8); err != nil { | 	if _, err := db.Exec(updateSchemaVersion, 8); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  | 	return migrateFrom8(db) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func migrateFrom8(db *sql.DB) error { | ||||||
|  | 	log.Info("Migrating cache database schema: from 8 to 9") | ||||||
|  | 	if _, err := db.Exec(migrate8To9AlterMessagesTableQuery); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if _, err := db.Exec(updateSchemaVersion, 9); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
| 	return nil // Update this when a new version is added | 	return nil // Update this when a new version is added | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -450,7 +450,7 @@ func TestSqliteCache_StartupQueries_WAL(t *testing.T) { | ||||||
| 	startupQueries := `pragma journal_mode = WAL;  | 	startupQueries := `pragma journal_mode = WAL;  | ||||||
| pragma synchronous = normal;  | pragma synchronous = normal;  | ||||||
| pragma temp_store = memory;` | pragma temp_store = memory;` | ||||||
| 	db, err := newSqliteCache(filename, startupQueries, false) | 	db, err := newSqliteCache(filename, startupQueries, 0, 0, false) | ||||||
| 	require.Nil(t, err) | 	require.Nil(t, err) | ||||||
| 	require.Nil(t, db.AddMessage(newDefaultMessage("mytopic", "some message"))) | 	require.Nil(t, db.AddMessage(newDefaultMessage("mytopic", "some message"))) | ||||||
| 	require.FileExists(t, filename) | 	require.FileExists(t, filename) | ||||||
|  | @ -461,7 +461,7 @@ pragma temp_store = memory;` | ||||||
| func TestSqliteCache_StartupQueries_None(t *testing.T) { | func TestSqliteCache_StartupQueries_None(t *testing.T) { | ||||||
| 	filename := newSqliteTestCacheFile(t) | 	filename := newSqliteTestCacheFile(t) | ||||||
| 	startupQueries := "" | 	startupQueries := "" | ||||||
| 	db, err := newSqliteCache(filename, startupQueries, false) | 	db, err := newSqliteCache(filename, startupQueries, 0, 0, false) | ||||||
| 	require.Nil(t, err) | 	require.Nil(t, err) | ||||||
| 	require.Nil(t, db.AddMessage(newDefaultMessage("mytopic", "some message"))) | 	require.Nil(t, db.AddMessage(newDefaultMessage("mytopic", "some message"))) | ||||||
| 	require.FileExists(t, filename) | 	require.FileExists(t, filename) | ||||||
|  | @ -472,7 +472,7 @@ func TestSqliteCache_StartupQueries_None(t *testing.T) { | ||||||
| func TestSqliteCache_StartupQueries_Fail(t *testing.T) { | func TestSqliteCache_StartupQueries_Fail(t *testing.T) { | ||||||
| 	filename := newSqliteTestCacheFile(t) | 	filename := newSqliteTestCacheFile(t) | ||||||
| 	startupQueries := `xx error` | 	startupQueries := `xx error` | ||||||
| 	_, err := newSqliteCache(filename, startupQueries, false) | 	_, err := newSqliteCache(filename, startupQueries, 0, 0, false) | ||||||
| 	require.Error(t, err) | 	require.Error(t, err) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -501,7 +501,7 @@ func TestMemCache_NopCache(t *testing.T) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func newSqliteTestCache(t *testing.T) *messageCache { | func newSqliteTestCache(t *testing.T) *messageCache { | ||||||
| 	c, err := newSqliteCache(newSqliteTestCacheFile(t), "", false) | 	c, err := newSqliteCache(newSqliteTestCacheFile(t), "", 0, 0, false) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  | @ -513,7 +513,7 @@ func newSqliteTestCacheFile(t *testing.T) string { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func newSqliteTestCacheFromFile(t *testing.T, filename, startupQueries string) *messageCache { | func newSqliteTestCacheFromFile(t *testing.T, filename, startupQueries string) *messageCache { | ||||||
| 	c, err := newSqliteCache(filename, startupQueries, false) | 	c, err := newSqliteCache(filename, startupQueries, 0, 0, false) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -159,7 +159,7 @@ func createMessageCache(conf *Config) (*messageCache, error) { | ||||||
| 	if conf.CacheDuration == 0 { | 	if conf.CacheDuration == 0 { | ||||||
| 		return newNopCache() | 		return newNopCache() | ||||||
| 	} else if conf.CacheFile != "" { | 	} else if conf.CacheFile != "" { | ||||||
| 		return newSqliteCache(conf.CacheFile, conf.CacheStartupQueries, false) | 		return newSqliteCache(conf.CacheFile, conf.CacheStartupQueries, conf.CacheBatchSize, conf.CacheBatchTimeout, false) | ||||||
| 	} | 	} | ||||||
| 	return newMemCache() | 	return newMemCache() | ||||||
| } | } | ||||||
|  | @ -491,6 +491,7 @@ func (s *Server) handlePublishWithoutResponse(r *http.Request, v *visitor) (*mes | ||||||
| 		log.Debug("%s Message delayed, will process later", logMessagePrefix(v, m)) | 		log.Debug("%s Message delayed, will process later", logMessagePrefix(v, m)) | ||||||
| 	} | 	} | ||||||
| 	if cache { | 	if cache { | ||||||
|  | 		log.Debug("%s Adding message to cache", logMessagePrefix(v, m)) | ||||||
| 		if err := s.messageCache.AddMessage(m); err != nil { | 		if err := s.messageCache.AddMessage(m); err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | @ -53,6 +53,12 @@ | ||||||
| #       pragma journal_mode = WAL; | #       pragma journal_mode = WAL; | ||||||
| #       pragma synchronous = normal; | #       pragma synchronous = normal; | ||||||
| #       pragma temp_store = memory; | #       pragma temp_store = memory; | ||||||
|  | #       pragma busy_timeout = 15000; | ||||||
|  | #       vacuum; | ||||||
|  | # | ||||||
|  | # The "cache-batch-size" and "cache-batch-timeout" parameter allow enabling async batch writing | ||||||
|  | # of messages. If set, messages will be queued and written to the database in batches of the given | ||||||
|  | # size, or after the given timeout. This is only required for high volume servers. | ||||||
| # | # | ||||||
| # Debian/RPM package users: | # Debian/RPM package users: | ||||||
| #   Use /var/cache/ntfy/cache.db as cache file to avoid permission issues. The package | #   Use /var/cache/ntfy/cache.db as cache file to avoid permission issues. The package | ||||||
|  | @ -65,6 +71,8 @@ | ||||||
| # cache-file: <filename> | # cache-file: <filename> | ||||||
| # cache-duration: "12h" | # cache-duration: "12h" | ||||||
| # cache-startup-queries: | # cache-startup-queries: | ||||||
|  | # cache-batch-size: 0 | ||||||
|  | # cache-batch-timeout: "0ms" | ||||||
| 
 | 
 | ||||||
| # If set, access to the ntfy server and API can be controlled on a granular level using | # If set, access to the ntfy server and API can be controlled on a granular level using | ||||||
| # the 'ntfy user' and 'ntfy access' commands. See the --help pages for details, or check the docs. | # the 'ntfy user' and 'ntfy access' commands. See the --help pages for details, or check the docs. | ||||||
|  | @ -173,8 +181,9 @@ | ||||||
| # Rate limiting: Allowed GET/PUT/POST requests per second, per visitor: | # Rate limiting: Allowed GET/PUT/POST requests per second, per visitor: | ||||||
| # - visitor-request-limit-burst is the initial bucket of requests each visitor has | # - visitor-request-limit-burst is the initial bucket of requests each visitor has | ||||||
| # - visitor-request-limit-replenish is the rate at which the bucket is refilled | # - visitor-request-limit-replenish is the rate at which the bucket is refilled | ||||||
| # - visitor-request-limit-exempt-hosts is a comma-separated list of hostnames and IPs to be | # - visitor-request-limit-exempt-hosts is a comma-separated list of hostnames, IPs or CIDRs to be | ||||||
| #   exempt from request rate limiting; hostnames are resolved at the time the server is started | #   exempt from request rate limiting. Hostnames are resolved at the time the server is started. | ||||||
|  | #   Example: "1.2.3.4,ntfy.example.com,8.7.6.0/24" | ||||||
| # | # | ||||||
| # visitor-request-limit-burst: 60 | # visitor-request-limit-burst: 60 | ||||||
| # visitor-request-limit-replenish: "5s" | # visitor-request-limit-replenish: "5s" | ||||||
|  |  | ||||||
							
								
								
									
										86
									
								
								util/batching_queue.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								util/batching_queue.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,86 @@ | ||||||
|  | package util | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"sync" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // BatchingQueue is a queue that creates batches of the enqueued elements based on a | ||||||
|  | // max batch size and a batch timeout. | ||||||
|  | // | ||||||
|  | // Example: | ||||||
|  | // | ||||||
|  | //	q := NewBatchingQueue[int](2, 500 * time.Millisecond) | ||||||
|  | //	go func() { | ||||||
|  | //	  for batch := range q.Dequeue() { | ||||||
|  | //	    fmt.Println(batch) | ||||||
|  | //	  } | ||||||
|  | //	}() | ||||||
|  | //	q.Enqueue(1) | ||||||
|  | //	q.Enqueue(2) | ||||||
|  | //	q.Enqueue(3) | ||||||
|  | //	time.Sleep(time.Second) | ||||||
|  | // | ||||||
|  | // This example will emit batch [1, 2] immediately (because the batch size is 2), and | ||||||
|  | // a batch [3] after 500ms. | ||||||
|  | type BatchingQueue[T any] struct { | ||||||
|  | 	batchSize int | ||||||
|  | 	timeout   time.Duration | ||||||
|  | 	in        []T | ||||||
|  | 	out       chan []T | ||||||
|  | 	mu        sync.Mutex | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewBatchingQueue creates a new BatchingQueue | ||||||
|  | func NewBatchingQueue[T any](batchSize int, timeout time.Duration) *BatchingQueue[T] { | ||||||
|  | 	q := &BatchingQueue[T]{ | ||||||
|  | 		batchSize: batchSize, | ||||||
|  | 		timeout:   timeout, | ||||||
|  | 		in:        make([]T, 0), | ||||||
|  | 		out:       make(chan []T), | ||||||
|  | 	} | ||||||
|  | 	go q.timeoutTicker() | ||||||
|  | 	return q | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Enqueue enqueues an element to the queue. If the configured batch size is reached, | ||||||
|  | // the batch will be emitted immediately. | ||||||
|  | func (q *BatchingQueue[T]) Enqueue(element T) { | ||||||
|  | 	q.mu.Lock() | ||||||
|  | 	q.in = append(q.in, element) | ||||||
|  | 	var elements []T | ||||||
|  | 	if len(q.in) == q.batchSize { | ||||||
|  | 		elements = q.dequeueAll() | ||||||
|  | 	} | ||||||
|  | 	q.mu.Unlock() | ||||||
|  | 	if len(elements) > 0 { | ||||||
|  | 		q.out <- elements | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Dequeue returns a channel emitting batches of elements | ||||||
|  | func (q *BatchingQueue[T]) Dequeue() <-chan []T { | ||||||
|  | 	return q.out | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (q *BatchingQueue[T]) dequeueAll() []T { | ||||||
|  | 	elements := make([]T, len(q.in)) | ||||||
|  | 	copy(elements, q.in) | ||||||
|  | 	q.in = q.in[:0] | ||||||
|  | 	return elements | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (q *BatchingQueue[T]) timeoutTicker() { | ||||||
|  | 	if q.timeout == 0 { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	ticker := time.NewTicker(q.timeout) | ||||||
|  | 	for range ticker.C { | ||||||
|  | 		q.mu.Lock() | ||||||
|  | 		elements := q.dequeueAll() | ||||||
|  | 		q.mu.Unlock() | ||||||
|  | 		if len(elements) > 0 { | ||||||
|  | 			q.out <- elements | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										58
									
								
								util/batching_queue_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								util/batching_queue_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,58 @@ | ||||||
|  | package util_test | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/stretchr/testify/require" | ||||||
|  | 	"heckel.io/ntfy/util" | ||||||
|  | 	"math/rand" | ||||||
|  | 	"sync" | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestBatchingQueue_InfTimeout(t *testing.T) { | ||||||
|  | 	q := util.NewBatchingQueue[int](25, 1*time.Hour) | ||||||
|  | 	batches, total := make([][]int, 0), 0 | ||||||
|  | 	var mu sync.Mutex | ||||||
|  | 	go func() { | ||||||
|  | 		for batch := range q.Dequeue() { | ||||||
|  | 			mu.Lock() | ||||||
|  | 			batches = append(batches, batch) | ||||||
|  | 			total += len(batch) | ||||||
|  | 			mu.Unlock() | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  | 	for i := 0; i < 101; i++ { | ||||||
|  | 		go q.Enqueue(i) | ||||||
|  | 	} | ||||||
|  | 	time.Sleep(time.Second) | ||||||
|  | 	mu.Lock() | ||||||
|  | 	require.Equal(t, 100, total) // One is missing, stuck in the last batch! | ||||||
|  | 	require.Equal(t, 4, len(batches)) | ||||||
|  | 	mu.Unlock() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestBatchingQueue_WithTimeout(t *testing.T) { | ||||||
|  | 	q := util.NewBatchingQueue[int](25, 100*time.Millisecond) | ||||||
|  | 	batches, total := make([][]int, 0), 0 | ||||||
|  | 	var mu sync.Mutex | ||||||
|  | 	go func() { | ||||||
|  | 		for batch := range q.Dequeue() { | ||||||
|  | 			mu.Lock() | ||||||
|  | 			batches = append(batches, batch) | ||||||
|  | 			total += len(batch) | ||||||
|  | 			mu.Unlock() | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  | 	for i := 0; i < 101; i++ { | ||||||
|  | 		go func(i int) { | ||||||
|  | 			time.Sleep(time.Duration(rand.Intn(700)) * time.Millisecond) | ||||||
|  | 			q.Enqueue(i) | ||||||
|  | 		}(i) | ||||||
|  | 	} | ||||||
|  | 	time.Sleep(time.Second) | ||||||
|  | 	mu.Lock() | ||||||
|  | 	require.Equal(t, 101, total) | ||||||
|  | 	require.True(t, len(batches) > 4) // 101/25 | ||||||
|  | 	require.True(t, len(batches) < 21) | ||||||
|  | 	mu.Unlock() | ||||||
|  | } | ||||||
							
								
								
									
										739
									
								
								web/package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										739
									
								
								web/package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										45
									
								
								web/public/static/langs/sv.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								web/public/static/langs/sv.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,45 @@ | ||||||
|  | { | ||||||
|  |     "action_bar_settings": "Inställningar", | ||||||
|  |     "action_bar_send_test_notification": "Skicka test notis", | ||||||
|  |     "action_bar_toggle_action_menu": "Öppna/stäng åtgärdsmeny", | ||||||
|  |     "message_bar_type_message": "Skriv ett meddelande här", | ||||||
|  |     "message_bar_error_publishing": "Fel vid publicering av notis", | ||||||
|  |     "message_bar_show_dialog": "Visa publicerings dialog", | ||||||
|  |     "message_bar_publish": "Publicera meddelande", | ||||||
|  |     "nav_topics_title": "Prenumererade kategorier", | ||||||
|  |     "nav_button_all_notifications": "Alla notiser", | ||||||
|  |     "nav_button_documentation": "Dokumentation", | ||||||
|  |     "nav_button_publish_message": "Publicera notis", | ||||||
|  |     "nav_button_subscribe": "Prenumerera på kategori", | ||||||
|  |     "alert_grant_title": "Notiser är avstängda", | ||||||
|  |     "alert_grant_button": "Bevilja nu", | ||||||
|  |     "alert_not_supported_title": "Notiser stöds inte", | ||||||
|  |     "notifications_list": "Notis-lista", | ||||||
|  |     "notifications_list_item": "Notis", | ||||||
|  |     "notifications_delete": "Radera", | ||||||
|  |     "notifications_copied_to_clipboard": "Kopierat till urklipp", | ||||||
|  |     "notifications_tags": "Taggar", | ||||||
|  |     "notifications_new_indicator": "Ny notis", | ||||||
|  |     "notifications_attachment_copy_url_title": "Kopiera bifogad URL till urklipp", | ||||||
|  |     "notifications_attachment_copy_url_button": "Kopiera URL", | ||||||
|  |     "notifications_attachment_open_title": "Gå till {{url}}", | ||||||
|  |     "notifications_attachment_open_button": "Öppna bilagan", | ||||||
|  |     "notifications_attachment_link_expired": "Nedladdningslänk utgått", | ||||||
|  |     "notifications_priority_x": "Prioritet {{priority}}", | ||||||
|  |     "action_bar_show_menu": "Visa meny", | ||||||
|  |     "action_bar_logo_alt": "ntfy logga", | ||||||
|  |     "action_bar_unsubscribe": "Avprenumerera", | ||||||
|  |     "action_bar_toggle_mute": "Tysta/aktivera notiser", | ||||||
|  |     "action_bar_clear_notifications": "Rensa alla notiser", | ||||||
|  |     "nav_button_connecting": "ansluter", | ||||||
|  |     "notifications_attachment_image": "Bifogad bild", | ||||||
|  |     "nav_button_settings": "Inställningar", | ||||||
|  |     "nav_button_muted": "Notiser tystade", | ||||||
|  |     "notifications_attachment_link_expires": "länken utgår {{date}}", | ||||||
|  |     "notifications_attachment_file_image": "bild fil", | ||||||
|  |     "notifications_attachment_file_audio": "ljud fil", | ||||||
|  |     "alert_grant_description": "Ge din webbläsare behörighet att visa skrivbordsnotiser.", | ||||||
|  |     "alert_not_supported_description": "Notiser stöds inte i din webbläsare.", | ||||||
|  |     "notifications_mark_read": "Markera som läst", | ||||||
|  |     "notifications_attachment_file_video": "video fil" | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue