Remove old homepage
19
cmd/serve.go
|
@ -59,7 +59,7 @@ var flagsServe = append(
|
|||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "keepalive-interval", Aliases: []string{"keepalive_interval", "k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: server.DefaultKeepaliveInterval, Usage: "interval of keepalive messages"}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "manager-interval", Aliases: []string{"manager_interval", "m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: server.DefaultManagerInterval, Usage: "interval of for message pruning and stats printing"}),
|
||||
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "disallowed-topics", Aliases: []string{"disallowed_topics"}, EnvVars: []string{"NTFY_DISALLOWED_TOPICS"}, Usage: "topics that are not allowed to be used"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-root", Aliases: []string{"web_root"}, EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "app", Usage: "sets web root to landing page (home), web app (app) or disabled (disable)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-root", Aliases: []string{"web_root"}, EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "/", Usage: "sets root of the web app (e.g. /, or /app), or disables it (disable)"}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-signup", Aliases: []string{"enable_signup"}, EnvVars: []string{"NTFY_ENABLE_SIGNUP"}, Value: false, Usage: "allows users to sign up via the web app, or API"}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-login", Aliases: []string{"enable_login"}, EnvVars: []string{"NTFY_ENABLE_LOGIN"}, Value: false, Usage: "allows users to log in via the web app, or API"}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-reservations", Aliases: []string{"enable_reservations"}, EnvVars: []string{"NTFY_ENABLE_RESERVATIONS"}, Value: false, Usage: "allows users to reserve topics (if their tier allows it)"}),
|
||||
|
@ -195,8 +195,6 @@ func execServe(c *cli.Context) error {
|
|||
return errors.New("if set, base-url must start with http:// or https://")
|
||||
} else if baseURL != "" && strings.HasSuffix(baseURL, "/") {
|
||||
return errors.New("if set, base-url must not end with a slash (/)")
|
||||
} else if !util.Contains([]string{"app", "home", "disable"}, webRoot) {
|
||||
return errors.New("if set, web-root must be 'home' or 'app'")
|
||||
} else if upstreamBaseURL != "" && !strings.HasPrefix(upstreamBaseURL, "http://") && !strings.HasPrefix(upstreamBaseURL, "https://") {
|
||||
return errors.New("if set, upstream-base-url must start with http:// or https://")
|
||||
} else if upstreamBaseURL != "" && strings.HasSuffix(upstreamBaseURL, "/") {
|
||||
|
@ -213,8 +211,16 @@ func execServe(c *cli.Context) error {
|
|||
return errors.New("if stripe-secret-key is set, stripe-webhook-key and base-url must also be set")
|
||||
}
|
||||
|
||||
webRootIsApp := webRoot == "app"
|
||||
enableWeb := webRoot != "disable"
|
||||
// Backwards compatibility
|
||||
if webRoot == "app" {
|
||||
webRoot = "/"
|
||||
} else if webRoot == "home" {
|
||||
webRoot = "/app"
|
||||
} else if webRoot == "disable" {
|
||||
webRoot = ""
|
||||
} else if !strings.HasPrefix(webRoot, "/") {
|
||||
webRoot = "/" + webRoot
|
||||
}
|
||||
|
||||
// Default auth permissions
|
||||
authDefault, err := user.ParsePermission(authDefaultAccess)
|
||||
|
@ -293,7 +299,7 @@ func execServe(c *cli.Context) error {
|
|||
conf.KeepaliveInterval = keepaliveInterval
|
||||
conf.ManagerInterval = managerInterval
|
||||
conf.DisallowedTopics = disallowedTopics
|
||||
conf.WebRootIsApp = webRootIsApp
|
||||
conf.WebRoot = webRoot
|
||||
conf.UpstreamBaseURL = upstreamBaseURL
|
||||
conf.SMTPSenderAddr = smtpSenderAddr
|
||||
conf.SMTPSenderUser = smtpSenderUser
|
||||
|
@ -317,7 +323,6 @@ func execServe(c *cli.Context) error {
|
|||
conf.StripeSecretKey = stripeSecretKey
|
||||
conf.StripeWebhookKey = stripeWebhookKey
|
||||
conf.BillingContact = billingContact
|
||||
conf.EnableWeb = enableWeb
|
||||
conf.EnableSignup = enableSignup
|
||||
conf.EnableLogin = enableLogin
|
||||
conf.EnableReservations = enableReservations
|
||||
|
|
|
@ -1255,7 +1255,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
|||
| `visitor-request-limit-exempt-hosts` | `NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS` | *comma-separated host/IP list* | - | Rate limiting: List of hostnames and IPs to be exempt from request rate limiting |
|
||||
| `visitor-subscription-limit` | `NTFY_VISITOR_SUBSCRIPTION_LIMIT` | *number* | 30 | Rate limiting: Number of subscriptions per visitor (IP address) |
|
||||
| `visitor-subscriber-rate-limiting` | `NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING` | *bool* | `false` | Rate limiting: Enables subscriber-based rate limiting |
|
||||
| `web-root` | `NTFY_WEB_ROOT` | `app`, `home` or `disable` | `app` | Sets web root to landing page (home), web app (app) or disables the web app entirely (disable) |
|
||||
| `web-root` | `NTFY_WEB_ROOT` | *path*, e.g. `/` or `/app`, or `disable` | `/` | Sets root of the web app (e.g. /, or /app), or disables it entirely (disable) |
|
||||
| `enable-signup` | `NTFY_ENABLE_SIGNUP` | *boolean* (`true` or `false`) | `false` | Allows users to sign up via the web app, or API |
|
||||
| `enable-login` | `NTFY_ENABLE_LOGIN` | *boolean* (`true` or `false`) | `false` | Allows users to log in via the web app, or API |
|
||||
| `enable-reservations` | `NTFY_ENABLE_RESERVATIONS` | *boolean* (`true` or `false`) | `false` | Allows users to reserve topics (if their tier allows it) |
|
||||
|
|
|
@ -1178,6 +1178,12 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
|||
|
||||
## Not released yet
|
||||
|
||||
## ntfy server v2.5.0 (UNRELEASED)
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* Removed old ntfy website from ntfy entirely (no ticket)
|
||||
|
||||
### ntfy Android app v1.16.1 (UNRELEASED)
|
||||
|
||||
**Features:**
|
||||
|
|
|
@ -92,7 +92,7 @@ type Config struct {
|
|||
KeepaliveInterval time.Duration
|
||||
ManagerInterval time.Duration
|
||||
DisallowedTopics []string
|
||||
WebRootIsApp bool
|
||||
WebRoot string // empty to disable
|
||||
DelayedSenderInterval time.Duration
|
||||
FirebaseKeepaliveInterval time.Duration
|
||||
FirebasePollInterval time.Duration
|
||||
|
@ -133,7 +133,6 @@ type Config struct {
|
|||
StripeWebhookKey string
|
||||
StripePriceCacheDuration time.Duration
|
||||
BillingContact string
|
||||
EnableWeb bool
|
||||
EnableSignup bool // Enable creation of accounts via API and UI
|
||||
EnableLogin bool
|
||||
EnableReservations bool // Allow users with role "user" to own/reserve topics
|
||||
|
@ -171,7 +170,7 @@ func NewConfig() *Config {
|
|||
KeepaliveInterval: DefaultKeepaliveInterval,
|
||||
ManagerInterval: DefaultManagerInterval,
|
||||
DisallowedTopics: DefaultDisallowedTopics,
|
||||
WebRootIsApp: false,
|
||||
WebRoot: "/",
|
||||
DelayedSenderInterval: DefaultDelayedSenderInterval,
|
||||
FirebaseKeepaliveInterval: DefaultFirebaseKeepaliveInterval,
|
||||
FirebasePollInterval: DefaultFirebasePollInterval,
|
||||
|
@ -209,7 +208,6 @@ func NewConfig() *Config {
|
|||
StripeWebhookKey: "",
|
||||
StripePriceCacheDuration: DefaultStripePriceCacheDuration,
|
||||
BillingContact: "",
|
||||
EnableWeb: true,
|
||||
EnableSignup: false,
|
||||
EnableLogin: false,
|
||||
EnableReservations: false,
|
||||
|
|
|
@ -100,11 +100,10 @@ var (
|
|||
urlRegex = regexp.MustCompile(`^https?://`)
|
||||
|
||||
//go:embed site
|
||||
webFs embed.FS
|
||||
webFsCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: webFs}
|
||||
webSiteDir = "/site"
|
||||
webHomeIndex = "/home.html" // Landing page, only if "web-root: home"
|
||||
webAppIndex = "/app.html" // React app
|
||||
webFs embed.FS
|
||||
webFsCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: webFs}
|
||||
webSiteDir = "/site"
|
||||
webAppIndex = "/app.html" // React app
|
||||
|
||||
//go:embed docs
|
||||
docsStaticFs embed.FS
|
||||
|
@ -404,8 +403,8 @@ func (s *Server) handleError(w http.ResponseWriter, r *http.Request, v *visitor,
|
|||
}
|
||||
|
||||
func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||
if r.Method == http.MethodGet && r.URL.Path == "/" {
|
||||
return s.ensureWebEnabled(s.handleHome)(w, r, v)
|
||||
if r.Method == http.MethodGet && r.URL.Path == "/" && s.config.WebRoot == "/" {
|
||||
return s.ensureWebEnabled(s.handleRoot)(w, r, v)
|
||||
} else if r.Method == http.MethodHead && r.URL.Path == "/" {
|
||||
return s.ensureWebEnabled(s.handleEmpty)(w, r, v)
|
||||
} else if r.Method == http.MethodGet && r.URL.Path == apiHealthPath {
|
||||
|
@ -490,12 +489,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
|
|||
return errHTTPNotFound
|
||||
}
|
||||
|
||||
func (s *Server) handleHome(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||
if s.config.WebRootIsApp {
|
||||
r.URL.Path = webAppIndex
|
||||
} else {
|
||||
r.URL.Path = webHomeIndex
|
||||
}
|
||||
func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||
r.URL.Path = webAppIndex
|
||||
return s.handleStatic(w, r, v)
|
||||
}
|
||||
|
||||
|
@ -527,13 +522,9 @@ func (s *Server) handleHealth(w http.ResponseWriter, _ *http.Request, _ *visitor
|
|||
}
|
||||
|
||||
func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
|
||||
appRoot := "/"
|
||||
if !s.config.WebRootIsApp {
|
||||
appRoot = "/app"
|
||||
}
|
||||
response := &apiConfigResponse{
|
||||
BaseURL: "", // Will translate to window.location.origin
|
||||
AppRoot: appRoot,
|
||||
AppRoot: s.config.WebRoot,
|
||||
EnableLogin: s.config.EnableLogin,
|
||||
EnableSignup: s.config.EnableSignup,
|
||||
EnablePayments: s.config.StripeSecretKey != "",
|
||||
|
|
|
@ -167,11 +167,13 @@
|
|||
#
|
||||
# disallowed-topics:
|
||||
|
||||
# Defines if the root route (/) is pointing to the landing page (as on ntfy.sh) or the
|
||||
# web app. If you self-host, you don't want to change this.
|
||||
# Can be "app" (default), "home" or "disable" to disable the web app entirely.
|
||||
# Defines the root path of the web app, or disables the web app entirely.
|
||||
#
|
||||
# web-root: app
|
||||
# Can be any simple path, e.g. "/", "/app", or "/ntfy". For backwards-compatibility reasons,
|
||||
# the values "app" (maps to "/"), "home" (maps to "/app"), or "disable" (maps to "") to disable
|
||||
# the web app entirely.
|
||||
#
|
||||
# web-root: /
|
||||
|
||||
# Various feature flags used to control the web app, and API access, mainly around user and
|
||||
# account management.
|
||||
|
|
|
@ -51,7 +51,7 @@ func (s *Server) limitRequestsWithTopic(next handleFunc) handleFunc {
|
|||
|
||||
func (s *Server) ensureWebEnabled(next handleFunc) handleFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||
if !s.config.EnableWeb {
|
||||
if s.config.WebRoot == "" {
|
||||
return errHTTPNotFound
|
||||
}
|
||||
return next(w, r, v)
|
||||
|
|
|
@ -220,10 +220,6 @@ func TestServer_StaticSites(t *testing.T) {
|
|||
require.Equal(t, 200, rr.Code)
|
||||
require.Contains(t, rr.Body.String(), `<meta name="robots" content="noindex, nofollow"/>`)
|
||||
|
||||
rr = request(t, s, "GET", "/static/css/home.css", "", nil)
|
||||
require.Equal(t, 200, rr.Code)
|
||||
require.Contains(t, rr.Body.String(), `/* general styling */`)
|
||||
|
||||
rr = request(t, s, "GET", "/docs", "", nil)
|
||||
require.Equal(t, 301, rr.Code)
|
||||
|
||||
|
@ -232,7 +228,7 @@ func TestServer_StaticSites(t *testing.T) {
|
|||
|
||||
func TestServer_WebEnabled(t *testing.T) {
|
||||
conf := newTestConfig(t)
|
||||
conf.EnableWeb = false
|
||||
conf.WebRoot = "" // Disable web app
|
||||
s := newTestServer(t, conf)
|
||||
|
||||
rr := request(t, s, "GET", "/", "", nil)
|
||||
|
@ -245,7 +241,7 @@ func TestServer_WebEnabled(t *testing.T) {
|
|||
require.Equal(t, 404, rr.Code)
|
||||
|
||||
conf2 := newTestConfig(t)
|
||||
conf2.EnableWeb = true
|
||||
conf2.WebRoot = "/"
|
||||
s2 := newTestServer(t, conf2)
|
||||
|
||||
rr = request(t, s2, "GET", "/", "", nil)
|
||||
|
@ -253,9 +249,6 @@ func TestServer_WebEnabled(t *testing.T) {
|
|||
|
||||
rr = request(t, s2, "GET", "/config.js", "", nil)
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
||||
rr = request(t, s2, "GET", "/static/css/home.css", "", nil)
|
||||
require.Equal(t, 200, rr.Code)
|
||||
}
|
||||
|
||||
func TestServer_PublishLargeMessage(t *testing.T) {
|
||||
|
|
|
@ -1,182 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<title>ntfy.sh | Send push notifications to your phone via PUT/POST</title>
|
||||
<link rel="stylesheet" href="static/css/home.css" type="text/css">
|
||||
|
||||
<!-- Mobile view -->
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="HandheldFriendly" content="true">
|
||||
|
||||
<!-- Mobile browsers, background color -->
|
||||
<meta name="theme-color" content="#317f6f">
|
||||
<meta name="msapplication-navbutton-color" content="#317f6f">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="#317f6f">
|
||||
|
||||
<!-- Favicon, see favicon.io -->
|
||||
<link rel="icon" type="image/png" href="static/img/favicon.png">
|
||||
|
||||
<!-- Previews in Google, Slack, WhatsApp, etc. -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta property="og:site_name" content="ntfy.sh" />
|
||||
<meta property="og:title" content="ntfy.sh | Push notifications to your phone or desktop via PUT/POST" />
|
||||
<meta property="og:description" content="ntfy is a simple HTTP-based pub-sub notification service. It allows you to send desktop notifications via scripts from any computer, entirely without signup or cost. Made with ❤ by Philipp C. Heckel, Apache License 2.0, source at https://heckel.io/ntfy." />
|
||||
<meta property="og:image" content="/static/img/ntfy.png" />
|
||||
<meta property="og:url" content="https://ntfy.sh" />
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="stylesheet" href="static/css/fonts.css" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav id="header">
|
||||
<div id="headerBox">
|
||||
<img id="logo" src="static/img/ntfy.png" alt="logo"/>
|
||||
<div id="name">ntfy</div>
|
||||
<ol>
|
||||
<li><a href="app">Web app</a></li>
|
||||
<li><a href="docs/subscribe/phone/">Android/iOS</a></li>
|
||||
<li><a href="docs/">Docs</a></li>
|
||||
<li><a href="docs/publish/">API</a></li>
|
||||
<li><a href="https://github.com/binwiederhier/ntfy">GitHub</a></li>
|
||||
</ol>
|
||||
</div>
|
||||
</nav>
|
||||
<div id="main">
|
||||
<h1>Send push notifications to your phone or desktop via PUT/POST</h1>
|
||||
<p>
|
||||
<b>ntfy</b> (pronounce: <i>notify</i>) is a simple HTTP-based <a href="https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern">pub-sub</a> notification service.
|
||||
It allows you to send notifications to your phone or desktop via scripts from any computer,
|
||||
entirely <b>without signup, cost or setup</b>. It's also <a href="https://github.com/binwiederhier/ntfy">open source</a> if you want to run your own.
|
||||
</p>
|
||||
<div id="screenshots">
|
||||
<a href="static/img/screenshot-curl.png"><img src="static/img/screenshot-curl.png"/></a>
|
||||
<a href="static/img/screenshot-web-detail.png"><img src="static/img/screenshot-web-detail.png"/></a>
|
||||
<span class="nowrap">
|
||||
<a href="static/img/screenshot-phone-main.jpg"><img src="static/img/screenshot-phone-main.jpg"/></a>
|
||||
<a href="static/img/screenshot-phone-detail.jpg"><img src="static/img/screenshot-phone-detail.jpg"/></a>
|
||||
<a href="static/img/screenshot-phone-notification.jpg"><img src="static/img/screenshot-phone-notification.jpg"/></a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h2 id="publish" class="anchor">Publishing messages</h2>
|
||||
<p>
|
||||
<a href="docs/publish/">Publishing messages</a> can be done via PUT or POST. Topics are created on the fly by subscribing or publishing to them.
|
||||
Because there is no sign-up, <b>the topic is essentially a password</b>, so pick something that's not easily guessable.
|
||||
</p>
|
||||
<p class="smallMarginBottom">
|
||||
Here's an example showing how to publish a message using a POST request (via <tt>curl -d</tt>):
|
||||
</p>
|
||||
<code>
|
||||
curl -d "Backup successful 😀" <span class="ntfyUrl">ntfy.sh</span>/mytopic
|
||||
</code>
|
||||
<p class="smallMarginBottom">
|
||||
There are <a href="docs/publish/">more features</a> related to publishing messages: You can set a
|
||||
<a href="docs/publish/#message-priority">notification priority</a>, a <a href="docs/publish/#message-title">title</a>,
|
||||
and <a href="docs/publish/#tags-emojis">tag messages</a>.
|
||||
Here's an example using some of them together:
|
||||
</p>
|
||||
<code>
|
||||
curl \<br/>
|
||||
-H "Title: Unauthorized access detected" \<br/>
|
||||
-H "Priority: urgent" \<br/>
|
||||
-H "Tags: warning,skull" \<br/>
|
||||
-d "Remote access to $(hostname) detected. Act right away." \<br/>
|
||||
<span class="ntfyUrl">ntfy.sh</span>/mytopic
|
||||
</code>
|
||||
<p>
|
||||
Here's what that looks like in the <a href="docs/subscribe/phone/">Android app</a>:
|
||||
</p>
|
||||
<figure>
|
||||
<img src="static/img/screenshot-phone-popover.png" style="max-height: 200px"/>
|
||||
<figcaption>Urgent notification with pop-over</figcaption>
|
||||
</figure>
|
||||
|
||||
<h2 id="subscribe" class="anchor">Subscribe to a topic</h2>
|
||||
<p>
|
||||
You can create and subscribe to a topic either <a href="docs/subscribe/phone/">using your phone</a>,
|
||||
in <a href="docs/subscribe/web/">this web UI</a>, or in your own app by <a href="docs/subscribe/api/">subscribing via the API</a>.
|
||||
</p>
|
||||
|
||||
<h3 id="subscribe-phone" class="anchor">Subscribe from your phone</h3>
|
||||
<p>
|
||||
Simply get the app and start <a href="docs/publish/">publishing messages</a>. To learn more about the app,
|
||||
<a href="docs/subscribe/phone/">check out the documentation</a>.
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img src="static/img/badge-googleplay.png"></a>
|
||||
<a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img src="static/img/badge-fdroid.png"></a>
|
||||
<a href="https://apps.apple.com/us/app/ntfy/id1625396347"><img src="static/img/badge-appstore.png"></a>
|
||||
</p>
|
||||
<p>
|
||||
Here's a video showing the app in action:
|
||||
</p>
|
||||
<figure>
|
||||
<video controls muted autoplay loop src="static/img/android-video-overview.mp4" style="max-width: 650px"></video>
|
||||
<figcaption>Sending push notifications to your Android phone</figcaption>
|
||||
</figure>
|
||||
|
||||
<h3 id="subscribe-web" class="anchor">Subscribe via web app</h3>
|
||||
<p>
|
||||
Subscribe to topics in the <a href="app">web app</a> and receive messages as <b>desktop notification</b>.
|
||||
It is available at <b><a href="app"><span class="ntfyUrl">ntfy.sh</span>/app</a></b>.
|
||||
</p>
|
||||
<figure>
|
||||
<a href="app"><img src="static/img/screenshot-web-detail.png" width="100%"/></a>
|
||||
<figcaption>ntfy web app, available at <a href="app"><span class="ntfyUrl">ntfy.sh</span>/app</a></figcaption>
|
||||
</figure>
|
||||
|
||||
<h3 id="subscribe-api" class="anchor">Subscribe using the API</h3>
|
||||
<p>
|
||||
There's a super simple API that you can use to integrate your own app. You can consume
|
||||
a <a href="docs/subscribe/api/#subscribe-as-json-stream">JSON stream</a>,
|
||||
an <a href="docs/subscribe/api/#subscribe-as-sse-stream">SSE/EventSource stream</a>,
|
||||
a <a href="docs/subscribe/api/#subscribe-as-raw-stream">plain text stream</a>,
|
||||
or <a href="docs/subscribe/api/#websockets">via WebSockets</a>.
|
||||
</p>
|
||||
<p class="smallMarginBottom">
|
||||
Here's an example for JSON. The <b>connection stays open</b>, so you can retrieve messages as they come in:
|
||||
</p>
|
||||
<code>
|
||||
$ curl -s <span class="ntfyUrl">ntfy.sh</span>/mytopic/json<br/>
|
||||
{"id":"SLiKI64DOt","time":1635528757,"event":"open","topic":"mytopic"}<br/>
|
||||
{"id":"hwQ2YpKdmg","time":1635528741,"event":"message","topic":"mytopic","message":"Hi!"}<br/>
|
||||
{"id":"DGUDShMCsc","time":1635528787,"event":"keepalive","topic":"mytopic"}<br/>
|
||||
...
|
||||
</code>
|
||||
<p>
|
||||
Here's a short video demonstrating it in action:
|
||||
</p>
|
||||
<figure>
|
||||
<video controls muted autoplay loop src="static/img/android-video-subscribe-api.mp4" style="max-width: 650px"></video>
|
||||
<figcaption>Subscribing to the JSON stream with <tt>curl</tt></figcaption>
|
||||
</figure>
|
||||
|
||||
<h3 id="docs" class="anchor">Check out the docs!</h3>
|
||||
<p>
|
||||
ntfy has so many more features and you can learn about all of them <a href="docs/">in the documentation</a>
|
||||
(I tried my very best to make it the best docs ever 😉, not sure if I succeeded, hehe).
|
||||
</p>
|
||||
<figure>
|
||||
<a href="docs/"><img width="100%" src="static/img/screenshot-docs.png"/></a>
|
||||
<figcaption>Check out the documentation</figcaption>
|
||||
</figure>
|
||||
|
||||
<h3 id="free-software" class="anchor">100% open source & forever free</h3>
|
||||
<p>
|
||||
I love free software, and I'm doing this because it's fun. I have no bad intentions, and I will
|
||||
never monetize or sell your information. This service will always stay
|
||||
<a href="https://github.com/binwiederhier/ntfy">free and open</a>.
|
||||
You can read more in the <a href="docs/faq/">FAQs</a> and in the <a href="docs/privacy/">privacy policy</a>.
|
||||
</p>
|
||||
|
||||
<center id="ironicCenterTagDontFreakOut"><i>Made with ❤️ by <a href="https://heckel.io">Philipp C. Heckel</a></i></center>
|
||||
</div>
|
||||
<div id="lightbox" class="lightbox"></div>
|
||||
<script src="static/js/home.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -15,7 +15,7 @@
|
|||
<meta name="apple-mobile-web-app-status-bar-style" content="#317f6f">
|
||||
|
||||
<!-- Favicon, see favicon.io -->
|
||||
<link rel="icon" type="image/png" href="%PUBLIC_URL%/static/img/favicon.ico">
|
||||
<link rel="icon" type="image/png" href="%PUBLIC_URL%/static/images/favicon.ico">
|
||||
|
||||
<!-- Previews in Google, Slack, WhatsApp, etc. -->
|
||||
<meta property="og:type" content="website" />
|
||||
|
@ -23,7 +23,7 @@
|
|||
<meta property="og:site_name" content="ntfy web" />
|
||||
<meta property="og:title" content="ntfy web" />
|
||||
<meta property="og:description" content="ntfy lets you send push notifications via scripts from any computer or phone. Made with ❤ by Philipp C. Heckel, Apache License 2.0, source at https://heckel.io/ntfy." />
|
||||
<meta property="og:image" content="%PUBLIC_URL%/static/img/ntfy.png" />
|
||||
<meta property="og:image" content="%PUBLIC_URL%/static/images/ntfy.png" />
|
||||
<meta property="og:url" content="https://ntfy.sh" />
|
||||
|
||||
<!-- Never index -->
|
||||
|
|
|
@ -1,280 +0,0 @@
|
|||
/* general styling */
|
||||
|
||||
html, body {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 1.1em;
|
||||
color: #444;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
/* prevent scrollbar from repositioning website:
|
||||
* https://www.w3docs.com/snippets/css/how-to-prevent-scrollbar-from-repositioning-web-page.html */
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
a, a:visited {
|
||||
color: #338574;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: none;
|
||||
color: #317f6f;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 35px;
|
||||
margin-bottom: 30px;
|
||||
font-size: 2.5em;
|
||||
word-wrap: break-word; /* For very long topics */
|
||||
padding-right: 40px; /* For the X on the detail page */
|
||||
font-weight: 300;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 5px;
|
||||
font-size: 1.8em;
|
||||
font-weight: 300;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 25px;
|
||||
margin-bottom: 5px;
|
||||
font-size: 1.3em;
|
||||
font-weight: 300;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
line-height: 160%;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
p.smallMarginBottom {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
b {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
tt {
|
||||
background: #eee;
|
||||
padding: 2px 7px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
code {
|
||||
display: block;
|
||||
background: #eee;
|
||||
font-family: monospace;
|
||||
padding: 20px;
|
||||
border-radius: 3px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Main page */
|
||||
|
||||
#main {
|
||||
max-width: 900px;
|
||||
margin: 0 auto 50px auto;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
#error {
|
||||
color: darkred;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#ironicCenterTagDontFreakOut {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Anchors */
|
||||
|
||||
.anchor .anchorLink {
|
||||
color: #ccc;
|
||||
text-decoration: none;
|
||||
padding: 0 5px;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.anchor:hover .anchorLink {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.anchor .anchorLink:hover {
|
||||
color: #338574;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Figures */
|
||||
|
||||
figure {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
figure img, figure video {
|
||||
filter: drop-shadow(3px 3px 3px #ccc);
|
||||
border-radius: 7px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
figure video {
|
||||
width: 100%;
|
||||
max-height: 450px;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
/* Screenshots */
|
||||
|
||||
#screenshots {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#screenshots img {
|
||||
height: 190px;
|
||||
margin: 3px;
|
||||
border-radius: 5px;
|
||||
filter: drop-shadow(2px 2px 2px #ddd);
|
||||
}
|
||||
|
||||
#screenshots .nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Lightbox; thanks to https://yossiabramov.com/blog/vanilla-js-lightbox */
|
||||
|
||||
.lightbox {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
position: fixed;
|
||||
left:0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s ease-in;
|
||||
}
|
||||
|
||||
.lightbox.show {
|
||||
background-color: rgba(0,0,0, 0.75);
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.lightbox img {
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
filter: drop-shadow(5px 5px 10px #222);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.lightbox .close-lightbox {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
right: 30px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.lightbox .close-lightbox::after,
|
||||
.lightbox .close-lightbox::before {
|
||||
content: '';
|
||||
width: 3px;
|
||||
height: 20px;
|
||||
background-color: #ddd;
|
||||
position: absolute;
|
||||
border-radius: 5px;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.lightbox .close-lightbox::before {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.lightbox .close-lightbox:hover::after,
|
||||
.lightbox .close-lightbox:hover::before {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
|
||||
#header {
|
||||
background: #338574;
|
||||
height: 130px;
|
||||
}
|
||||
|
||||
#header #headerBox {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
#header #logo {
|
||||
margin-top: 23px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#header #name {
|
||||
float: left;
|
||||
color: white;
|
||||
font-size: 2.6em;
|
||||
font-weight: 300;
|
||||
margin: 35px 0 0 20px;
|
||||
}
|
||||
|
||||
#header ol {
|
||||
list-style-type: none;
|
||||
float: right;
|
||||
margin-top: 80px;
|
||||
}
|
||||
|
||||
#header ol li {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
#header ol li a, nav ol li a:visited {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#header ol li a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
li {
|
||||
padding: 4px 0;
|
||||
margin: 4px 0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
|
||||
/* Hide top menu SMALL SCREEN */
|
||||
@media only screen and (max-width: 780px) {
|
||||
#header ol {
|
||||
display: none;
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 297 KiB |
Before Width: | Height: | Size: 113 KiB |
Before Width: | Height: | Size: 227 KiB |
Before Width: | Height: | Size: 225 KiB |
Before Width: | Height: | Size: 128 KiB |
Before Width: | Height: | Size: 224 KiB |
Before Width: | Height: | Size: 270 KiB |
Before Width: | Height: | Size: 473 KiB |
|
@ -1,84 +0,0 @@
|
|||
|
||||
/* All the things */
|
||||
|
||||
let currentUrl = window.location.hostname;
|
||||
if (window.location.port) {
|
||||
currentUrl += ':' + window.location.port
|
||||
}
|
||||
|
||||
/* Screenshots */
|
||||
const lightbox = document.getElementById("lightbox");
|
||||
|
||||
const showScreenshotOverlay = (e, el, index) => {
|
||||
lightbox.classList.add('show');
|
||||
document.addEventListener('keydown', nextScreenshotKeyboardListener);
|
||||
return showScreenshot(e, index);
|
||||
};
|
||||
|
||||
const showScreenshot = (e, index) => {
|
||||
const actualIndex = resolveScreenshotIndex(index);
|
||||
lightbox.innerHTML = '<div class="close-lightbox"></div>' + screenshots[actualIndex].innerHTML;
|
||||
lightbox.querySelector('img').onclick = (e) => { return showScreenshot(e,actualIndex+1); };
|
||||
currentScreenshotIndex = actualIndex;
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
};
|
||||
|
||||
const nextScreenshot = (e) => {
|
||||
return showScreenshot(e, currentScreenshotIndex+1);
|
||||
};
|
||||
|
||||
const previousScreenshot = (e) => {
|
||||
return showScreenshot(e, currentScreenshotIndex-1);
|
||||
};
|
||||
|
||||
const resolveScreenshotIndex = (index) => {
|
||||
if (index < 0) {
|
||||
return screenshots.length - 1;
|
||||
} else if (index > screenshots.length - 1) {
|
||||
return 0;
|
||||
}
|
||||
return index;
|
||||
};
|
||||
|
||||
const hideScreenshotOverlay = (e) => {
|
||||
lightbox.classList.remove('show');
|
||||
document.removeEventListener('keydown', nextScreenshotKeyboardListener);
|
||||
};
|
||||
|
||||
const nextScreenshotKeyboardListener = (e) => {
|
||||
switch (e.keyCode) {
|
||||
case 37:
|
||||
previousScreenshot(e);
|
||||
break;
|
||||
case 39:
|
||||
nextScreenshot(e);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let currentScreenshotIndex = 0;
|
||||
const screenshots = [...document.querySelectorAll("#screenshots a")];
|
||||
screenshots.forEach((el, index) => {
|
||||
el.onclick = (e) => { return showScreenshotOverlay(e, el, index); };
|
||||
});
|
||||
|
||||
lightbox.onclick = hideScreenshotOverlay;
|
||||
|
||||
// Add anchor links
|
||||
document.querySelectorAll('.anchor').forEach((el) => {
|
||||
if (el.hasAttribute('id')) {
|
||||
const id = el.getAttribute('id');
|
||||
const anchor = document.createElement('a');
|
||||
anchor.innerHTML = `<a href="#${id}" class="anchorLink">#</a>`;
|
||||
el.appendChild(anchor);
|
||||
}
|
||||
});
|
||||
|
||||
// Change ntfy.sh url and protocol to match self-hosted one
|
||||
document.querySelectorAll('.ntfyUrl').forEach((el) => {
|
||||
el.innerHTML = currentUrl;
|
||||
});
|
||||
document.querySelectorAll('.ntfyProtocol').forEach((el) => {
|
||||
el.innerHTML = window.location.protocol + "//";
|
||||
});
|