WIP CLI
This commit is contained in:
parent
1e8421e8ce
commit
a1f513f6a5
9 changed files with 138 additions and 65 deletions
|
@ -34,12 +34,12 @@ type Message struct {
|
||||||
Event string
|
Event string
|
||||||
Time int64
|
Time int64
|
||||||
Topic string
|
Topic string
|
||||||
|
BaseURL string
|
||||||
|
TopicURL string
|
||||||
Message string
|
Message string
|
||||||
Title string
|
Title string
|
||||||
Priority int
|
Priority int
|
||||||
Tags []string
|
Tags []string
|
||||||
BaseURL string
|
|
||||||
TopicURL string
|
|
||||||
Raw string
|
Raw string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,7 +73,23 @@ func (c *Client) Publish(topicURL, message string, options ...PublishOption) err
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Subscribe(topicURL string) {
|
func (c *Client) Poll(topicURL string, options ...SubscribeOption) ([]*Message, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
messages := make([]*Message, 0)
|
||||||
|
msgChan := make(chan *Message)
|
||||||
|
errChan := make(chan error)
|
||||||
|
go func() {
|
||||||
|
err := performSubscribeRequest(ctx, msgChan, topicURL, options...)
|
||||||
|
close(msgChan)
|
||||||
|
errChan <- err
|
||||||
|
}()
|
||||||
|
for m := range msgChan {
|
||||||
|
messages = append(messages, m)
|
||||||
|
}
|
||||||
|
return messages, <-errChan
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Subscribe(topicURL string, options ...SubscribeOption) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
if _, ok := c.subscriptions[topicURL]; ok {
|
if _, ok := c.subscriptions[topicURL]; ok {
|
||||||
|
@ -81,7 +97,7 @@ func (c *Client) Subscribe(topicURL string) {
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
c.subscriptions[topicURL] = &subscription{cancel}
|
c.subscriptions[topicURL] = &subscription{cancel}
|
||||||
go handleConnectionLoop(ctx, c.Messages, topicURL)
|
go handleSubscribeConnLoop(ctx, c.Messages, topicURL, options...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Unsubscribe(topicURL string) {
|
func (c *Client) Unsubscribe(topicURL string) {
|
||||||
|
@ -95,25 +111,30 @@ func (c *Client) Unsubscribe(topicURL string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleConnectionLoop(ctx context.Context, msgChan chan *Message, topicURL string) {
|
func handleSubscribeConnLoop(ctx context.Context, msgChan chan *Message, topicURL string, options ...SubscribeOption) {
|
||||||
for {
|
for {
|
||||||
if err := handleConnection(ctx, msgChan, topicURL); err != nil {
|
if err := performSubscribeRequest(ctx, msgChan, topicURL, options...); err != nil {
|
||||||
log.Printf("connection to %s failed: %s", topicURL, err.Error())
|
log.Printf("Connection to %s failed: %s", topicURL, err.Error())
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
log.Printf("connection to %s exited", topicURL)
|
log.Printf("Connection to %s exited", topicURL)
|
||||||
return
|
return
|
||||||
case <-time.After(5 * time.Second):
|
case <-time.After(5 * time.Second):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleConnection(ctx context.Context, msgChan chan *Message, topicURL string) error {
|
func performSubscribeRequest(ctx context.Context, msgChan chan *Message, topicURL string, options ...SubscribeOption) error {
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/json", topicURL), nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/json", topicURL), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
for _, option := range options {
|
||||||
|
if err := option(req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -4,42 +4,24 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PublishOption func(r *http.Request) error
|
type RequestOption func(r *http.Request) error
|
||||||
|
type PublishOption = RequestOption
|
||||||
|
type SubscribeOption = RequestOption
|
||||||
|
|
||||||
func WithTitle(title string) PublishOption {
|
func WithTitle(title string) PublishOption {
|
||||||
return func(r *http.Request) error {
|
return WithHeader("X-Title", title)
|
||||||
if title != "" {
|
|
||||||
r.Header.Set("X-Title", title)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithPriority(priority string) PublishOption {
|
func WithPriority(priority string) PublishOption {
|
||||||
return func(r *http.Request) error {
|
return WithHeader("X-Priority", priority)
|
||||||
if priority != "" {
|
|
||||||
r.Header.Set("X-Priority", priority)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithTags(tags string) PublishOption {
|
func WithTags(tags string) PublishOption {
|
||||||
return func(r *http.Request) error {
|
return WithHeader("X-Tags", tags)
|
||||||
if tags != "" {
|
|
||||||
r.Header.Set("X-Tags", tags)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithDelay(delay string) PublishOption {
|
func WithDelay(delay string) PublishOption {
|
||||||
return func(r *http.Request) error {
|
return WithHeader("X-Delay", delay)
|
||||||
if delay != "" {
|
|
||||||
r.Header.Set("X-Delay", delay)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithNoCache() PublishOption {
|
func WithNoCache() PublishOption {
|
||||||
|
@ -50,20 +32,32 @@ func WithNoFirebase() PublishOption {
|
||||||
return WithHeader("X-Firebase", "no")
|
return WithHeader("X-Firebase", "no")
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithHeader(header, value string) PublishOption {
|
func WithSince(since string) SubscribeOption {
|
||||||
|
return WithQueryParam("since", since)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPoll() SubscribeOption {
|
||||||
|
return WithQueryParam("poll", "1")
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithScheduled() SubscribeOption {
|
||||||
|
return WithQueryParam("scheduled", "1")
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithHeader(header, value string) RequestOption {
|
||||||
return func(r *http.Request) error {
|
return func(r *http.Request) error {
|
||||||
|
if value != "" {
|
||||||
r.Header.Set(header, value)
|
r.Header.Set(header, value)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubscribeOption func(r *http.Request) error
|
func WithQueryParam(param, value string) RequestOption {
|
||||||
|
|
||||||
func WithSince(since string) PublishOption {
|
|
||||||
return func(r *http.Request) error {
|
return func(r *http.Request) error {
|
||||||
if since != "" {
|
if value != "" {
|
||||||
q := r.URL.Query()
|
q := r.URL.Query()
|
||||||
q.Add("since", since)
|
q.Add(param, value)
|
||||||
r.URL.RawQuery = q.Encode()
|
r.URL.RawQuery = q.Encode()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -36,7 +36,7 @@ func New() *cli.App {
|
||||||
|
|
||||||
func execMainApp(c *cli.Context) error {
|
func execMainApp(c *cli.Context) error {
|
||||||
log.Printf("\x1b[1;33mDeprecation notice: Please run the server using 'ntfy serve'; see 'ntfy -h' for help.\x1b[0m")
|
log.Printf("\x1b[1;33mDeprecation notice: Please run the server using 'ntfy serve'; see 'ntfy -h' for help.\x1b[0m")
|
||||||
log.Printf("\x1b[1;33mThis way of running the server will be removed Feb 2022.\x1b[0m")
|
log.Printf("\x1b[1;33mThis way of running the server will be removed March 2022. See https://ntfy.sh/docs/deprecations/ for details.\x1b[0m")
|
||||||
return execServe(c)
|
return execServe(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
var cmdPublish = &cli.Command{
|
var cmdPublish = &cli.Command{
|
||||||
Name: "publish",
|
Name: "publish",
|
||||||
Aliases: []string{"pub", "send"},
|
Aliases: []string{"pub", "send", "push"},
|
||||||
Usage: "Send message via a ntfy server",
|
Usage: "Send message via a ntfy server",
|
||||||
UsageText: "ntfy send [OPTIONS..] TOPIC MESSAGE",
|
UsageText: "ntfy send [OPTIONS..] TOPIC MESSAGE",
|
||||||
Action: execPublish,
|
Action: execPublish,
|
||||||
|
|
|
@ -21,6 +21,8 @@ var cmdSubscribe = &cli.Command{
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
&cli.StringFlag{Name: "exec", Aliases: []string{"e"}, Usage: "execute command for each message event"},
|
&cli.StringFlag{Name: "exec", Aliases: []string{"e"}, Usage: "execute command for each message event"},
|
||||||
&cli.StringFlag{Name: "since", Aliases: []string{"s"}, Usage: "return events since (Unix timestamp, or all)"},
|
&cli.StringFlag{Name: "since", Aliases: []string{"s"}, Usage: "return events since (Unix timestamp, or all)"},
|
||||||
|
&cli.BoolFlag{Name: "poll", Aliases: []string{"p"}, Usage: "return events and exit, do not listen for new events"},
|
||||||
|
&cli.BoolFlag{Name: "scheduled", Aliases: []string{"sched", "S"}, Usage: "also return scheduled/delayed events"},
|
||||||
},
|
},
|
||||||
Description: `(THIS COMMAND IS INCUBATING. IT MAY CHANGE WITHOUT NOTICE.)
|
Description: `(THIS COMMAND IS INCUBATING. IT MAY CHANGE WITHOUT NOTICE.)
|
||||||
|
|
||||||
|
@ -45,7 +47,8 @@ are passed to the command as environment variables:
|
||||||
Examples:
|
Examples:
|
||||||
ntfy subscribe mytopic # Prints JSON for incoming messages to stdout
|
ntfy subscribe mytopic # Prints JSON for incoming messages to stdout
|
||||||
ntfy sub home.lan/backups alerts # Subscribe to two different topics
|
ntfy sub home.lan/backups alerts # Subscribe to two different topics
|
||||||
ntfy sub --exec='notify-send "$m"' mytopic # Execute command for incoming messages'
|
ntfy sub --exec='notify-send "$m"' mytopic # Execute command for incoming messages
|
||||||
|
ntfy sub --exec=/my/script topic1 topic2 # Subscribe to two topics and execute command for each message
|
||||||
`,
|
`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,12 +59,38 @@ func execSubscribe(c *cli.Context) error {
|
||||||
log.Printf("\x1b[1;33mThis command is incubating. The interface may change without notice.\x1b[0m")
|
log.Printf("\x1b[1;33mThis command is incubating. The interface may change without notice.\x1b[0m")
|
||||||
cl := client.DefaultClient
|
cl := client.DefaultClient
|
||||||
command := c.String("exec")
|
command := c.String("exec")
|
||||||
for _, topic := range c.Args().Slice() {
|
since := c.String("since")
|
||||||
cl.Subscribe(expandTopicURL(topic))
|
poll := c.Bool("poll")
|
||||||
|
scheduled := c.Bool("scheduled")
|
||||||
|
topics := c.Args().Slice()
|
||||||
|
var options []client.SubscribeOption
|
||||||
|
if since != "" {
|
||||||
|
options = append(options, client.WithSince(since))
|
||||||
|
}
|
||||||
|
if poll {
|
||||||
|
options = append(options, client.WithPoll())
|
||||||
|
}
|
||||||
|
if scheduled {
|
||||||
|
options = append(options, client.WithScheduled())
|
||||||
|
}
|
||||||
|
if poll {
|
||||||
|
for _, topic := range topics {
|
||||||
|
messages, err := cl.Poll(expandTopicURL(topic), options...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, m := range messages {
|
||||||
|
_ = dispatchMessage(c, command, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, topic := range topics {
|
||||||
|
cl.Subscribe(expandTopicURL(topic), options...)
|
||||||
}
|
}
|
||||||
for m := range cl.Messages {
|
for m := range cl.Messages {
|
||||||
_ = dispatchMessage(c, command, m)
|
_ = dispatchMessage(c, command, m)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,11 +106,9 @@ func execCommand(c *cli.Context, command string, m *client.Message) error {
|
||||||
if m.Event == client.OpenEvent {
|
if m.Event == client.OpenEvent {
|
||||||
log.Printf("[%s] Connection opened, subscribed to topic", collapseTopicURL(m.TopicURL))
|
log.Printf("[%s] Connection opened, subscribed to topic", collapseTopicURL(m.TopicURL))
|
||||||
} else if m.Event == client.MessageEvent {
|
} else if m.Event == client.MessageEvent {
|
||||||
go func() {
|
|
||||||
if err := runCommandInternal(c, command, m); err != nil {
|
if err := runCommandInternal(c, command, m); err != nil {
|
||||||
log.Printf("[%s] Command failed: %s", collapseTopicURL(m.TopicURL), err.Error())
|
log.Printf("[%s] Command failed: %s", collapseTopicURL(m.TopicURL), err.Error())
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
25
docs/deprecations.md
Normal file
25
docs/deprecations.md
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# Deprecation notices
|
||||||
|
This page is used to list deprecation notices for ntfy. Deprecated commands and options will be
|
||||||
|
**removed after ~3 months** from the time they were deprecated.
|
||||||
|
|
||||||
|
## Active deprecations
|
||||||
|
|
||||||
|
### Running server via `ntfy` (instead of `ntfy serve`)
|
||||||
|
> since 2021-12-17
|
||||||
|
|
||||||
|
As more commands are added to the `ntfy` CLI tool, using just `ntfy` to run the server is not practical
|
||||||
|
anymore. Please use `ntfy serve` instead. This also applies to Docker images, as they can also execute more than
|
||||||
|
just the server.
|
||||||
|
|
||||||
|
=== "Before"
|
||||||
|
```
|
||||||
|
$ ntfy
|
||||||
|
2021/12/17 08:16:01 Listening on :80/http
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "After"
|
||||||
|
```
|
||||||
|
$ ntfy serve
|
||||||
|
2021/12/17 08:16:01 Listening on :80/http
|
||||||
|
```
|
||||||
|
|
|
@ -12,7 +12,7 @@ We support amd64, armv7 and arm64.
|
||||||
|
|
||||||
1. Install ntfy using one of the methods described below
|
1. Install ntfy using one of the methods described below
|
||||||
2. Then (optionally) edit `/etc/ntfy/config.yml` (see [configuration](config.md))
|
2. Then (optionally) edit `/etc/ntfy/config.yml` (see [configuration](config.md))
|
||||||
3. Then just run it with `ntfy` (or `systemctl start ntfy` when using the deb/rpm).
|
3. Then just run it with `ntfy serve` (or `systemctl start ntfy` when using the deb/rpm).
|
||||||
|
|
||||||
## Binaries and packages
|
## Binaries and packages
|
||||||
Please check out the [releases page](https://github.com/binwiederhier/ntfy/releases) for binaries and
|
Please check out the [releases page](https://github.com/binwiederhier/ntfy/releases) for binaries and
|
||||||
|
@ -22,21 +22,21 @@ deb/rpm packages.
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.7.0/ntfy_1.7.0_linux_x86_64.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v1.7.0/ntfy_1.7.0_linux_x86_64.tar.gz
|
||||||
sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy
|
sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy
|
||||||
sudo ./ntfy
|
sudo ./ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "armv7/armhf"
|
=== "armv7/armhf"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.7.0/ntfy_1.7.0_linux_armv7.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v1.7.0/ntfy_1.7.0_linux_armv7.tar.gz
|
||||||
sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy
|
sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy
|
||||||
sudo ./ntfy
|
sudo ./ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "arm64"
|
=== "arm64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.7.0/ntfy_1.7.0_linux_arm64.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v1.7.0/ntfy_1.7.0_linux_arm64.tar.gz
|
||||||
sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy
|
sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy
|
||||||
sudo ./ntfy
|
sudo ./ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
## Debian/Ubuntu repository
|
## Debian/Ubuntu repository
|
||||||
|
@ -132,12 +132,12 @@ The [ntfy image](https://hub.docker.com/r/binwiederhier/ntfy) is available for a
|
||||||
straight forward to use.
|
straight forward to use.
|
||||||
|
|
||||||
The server exposes its web UI and the API on port 80, so you need to expose that in Docker. To use the persistent
|
The server exposes its web UI and the API on port 80, so you need to expose that in Docker. To use the persistent
|
||||||
[message cache](config.md#message-cache), you also need to map a volume to `/var/cache/ntfy`. To change other settings, you should map `/etc/ntfy`,
|
[message cache](config.md#message-cache), you also need to map a volume to `/var/cache/ntfy`. To change other settings,
|
||||||
so you can edit `/etc/ntfy/config.yml`.
|
you should map `/etc/ntfy`, so you can edit `/etc/ntfy/config.yml`.
|
||||||
|
|
||||||
Basic usage (no cache or additional config):
|
Basic usage (no cache or additional config):
|
||||||
```
|
```
|
||||||
docker run -p 80:80 -it binwiederhier/ntfy
|
docker run -p 80:80 -it binwiederhier/ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
With persistent cache (configured as command line arguments):
|
With persistent cache (configured as command line arguments):
|
||||||
|
@ -147,7 +147,8 @@ docker run \
|
||||||
-p 80:80 \
|
-p 80:80 \
|
||||||
-it \
|
-it \
|
||||||
binwiederhier/ntfy \
|
binwiederhier/ntfy \
|
||||||
--cache-file /var/cache/ntfy/cache.db
|
--cache-file /var/cache/ntfy/cache.db \
|
||||||
|
serve
|
||||||
```
|
```
|
||||||
|
|
||||||
With other config options (configured via `/etc/ntfy/config.yml`, see [configuration](config.md) for details):
|
With other config options (configured via `/etc/ntfy/config.yml`, see [configuration](config.md) for details):
|
||||||
|
@ -156,7 +157,8 @@ docker run \
|
||||||
-v /etc/ntfy:/etc/ntfy \
|
-v /etc/ntfy:/etc/ntfy \
|
||||||
-p 80:80 \
|
-p 80:80 \
|
||||||
-it \
|
-it \
|
||||||
binwiederhier/ntfy
|
binwiederhier/ntfy \
|
||||||
|
serve
|
||||||
```
|
```
|
||||||
|
|
||||||
## Go
|
## Go
|
||||||
|
|
3
docs/subscribe/cli.md
Normal file
3
docs/subscribe/cli.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Subscribe via CLI
|
||||||
|
|
||||||
|
XXXXXXXXXxxx
|
|
@ -1,11 +1,11 @@
|
||||||
site_dir: server/docs
|
site_dir: server/docs
|
||||||
site_name: ntfy
|
site_name: ntfy
|
||||||
site_url: https://ntfy.sh
|
site_url: https://ntfy.sh
|
||||||
site_description: simple HTTP-based pub-sub
|
site_description: Send push notifications to your phone via PUT/POST
|
||||||
copyright: Made with ❤️ by Philipp C. Heckel
|
copyright: Made with ❤️ by Philipp C. Heckel
|
||||||
repo_name: binwiederhier/ntfy
|
repo_name: binwiederhier/ntfy
|
||||||
repo_url: https://github.com/binwiederhier/ntfy
|
repo_url: https://github.com/binwiederhier/ntfy
|
||||||
edit_uri: edit/main/docs/
|
edit_uri: blob/main/docs/
|
||||||
|
|
||||||
theme:
|
theme:
|
||||||
name: material
|
name: material
|
||||||
|
@ -31,7 +31,6 @@ theme:
|
||||||
- search.highlight
|
- search.highlight
|
||||||
- search.share
|
- search.share
|
||||||
- navigation.sections
|
- navigation.sections
|
||||||
# - navigation.instant
|
|
||||||
- toc.integrate
|
- toc.integrate
|
||||||
- content.tabs.link
|
- content.tabs.link
|
||||||
extra:
|
extra:
|
||||||
|
@ -75,6 +74,7 @@ nav:
|
||||||
- "Subscribing":
|
- "Subscribing":
|
||||||
- "From your phone": subscribe/phone.md
|
- "From your phone": subscribe/phone.md
|
||||||
- "From the Web UI": subscribe/web.md
|
- "From the Web UI": subscribe/web.md
|
||||||
|
- "Using the CLI": subscribe/cli.md
|
||||||
- "Using the API": subscribe/api.md
|
- "Using the API": subscribe/api.md
|
||||||
- "Self-hosting":
|
- "Self-hosting":
|
||||||
- "Installation": install.md
|
- "Installation": install.md
|
||||||
|
@ -83,6 +83,7 @@ nav:
|
||||||
- "FAQs": faq.md
|
- "FAQs": faq.md
|
||||||
- "Examples": examples.md
|
- "Examples": examples.md
|
||||||
- "Emojis 🥳 🎉": emojis.md
|
- "Emojis 🥳 🎉": emojis.md
|
||||||
|
- "Deprecation notices": deprecations.md
|
||||||
- "Development": develop.md
|
- "Development": develop.md
|
||||||
- "Privacy policy": privacy.md
|
- "Privacy policy": privacy.md
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue