Push notifications
This commit is contained in:
parent
2e504548c4
commit
92a540e024
4
main.go
4
main.go
@ -31,6 +31,7 @@ import (
|
|||||||
"github.com/animenotifier/notify.moe/pages/music"
|
"github.com/animenotifier/notify.moe/pages/music"
|
||||||
"github.com/animenotifier/notify.moe/pages/newsoundtrack"
|
"github.com/animenotifier/notify.moe/pages/newsoundtrack"
|
||||||
"github.com/animenotifier/notify.moe/pages/newthread"
|
"github.com/animenotifier/notify.moe/pages/newthread"
|
||||||
|
"github.com/animenotifier/notify.moe/pages/notifications"
|
||||||
"github.com/animenotifier/notify.moe/pages/posts"
|
"github.com/animenotifier/notify.moe/pages/posts"
|
||||||
"github.com/animenotifier/notify.moe/pages/profile"
|
"github.com/animenotifier/notify.moe/pages/profile"
|
||||||
"github.com/animenotifier/notify.moe/pages/search"
|
"github.com/animenotifier/notify.moe/pages/search"
|
||||||
@ -126,6 +127,9 @@ func configure(app *aero.Application) *aero.Application {
|
|||||||
// Browser extension
|
// Browser extension
|
||||||
app.Ajax("/extension/embed", embed.Get)
|
app.Ajax("/extension/embed", embed.Get)
|
||||||
|
|
||||||
|
// API
|
||||||
|
app.Get("/api/test/notification", notifications.Test)
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
app.Use(middleware.Log())
|
app.Use(middleware.Log())
|
||||||
app.Use(middleware.Session())
|
app.Use(middleware.Session())
|
||||||
|
28
pages/notifications/notifications.go
Normal file
28
pages/notifications/notifications.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package notifications
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/aerogo/aero"
|
||||||
|
"github.com/animenotifier/notify.moe/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test ...
|
||||||
|
func Test(ctx *aero.Context) string {
|
||||||
|
user := utils.GetUser(ctx)
|
||||||
|
|
||||||
|
if user == nil {
|
||||||
|
return ctx.Error(http.StatusBadRequest, "Not logged in", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sub := range user.PushSubscriptions().Items {
|
||||||
|
err := sub.SendNotification("Yay, it works!")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "ok"
|
||||||
|
}
|
@ -21,6 +21,29 @@ component Settings(user *arn.User)
|
|||||||
InputText("Accounts.Osu.Nick", user.Accounts.Osu.Nick, "Osu", "Your username on osu.ppy.sh")
|
InputText("Accounts.Osu.Nick", user.Accounts.Osu.Nick, "Osu", "Your username on osu.ppy.sh")
|
||||||
//- InputText("Accounts.AnimePlanet.Nick", user.Accounts.AnimePlanet.Nick, "AnimePlanet", "Your username on anime-planet.com")
|
//- InputText("Accounts.AnimePlanet.Nick", user.Accounts.AnimePlanet.Nick, "AnimePlanet", "Your username on anime-planet.com")
|
||||||
|
|
||||||
|
.widget.mountable
|
||||||
|
h3.widget-title
|
||||||
|
Icon("bell")
|
||||||
|
span Notifications
|
||||||
|
|
||||||
|
.widget-input
|
||||||
|
label Enable:
|
||||||
|
button.action(data-action="enableNotifications", data-trigger="click")
|
||||||
|
Icon("toggle-on")
|
||||||
|
span Enable notifications
|
||||||
|
|
||||||
|
.widget-input
|
||||||
|
label Disable:
|
||||||
|
button.action(data-action="disableNotifications", data-trigger="click")
|
||||||
|
Icon("toggle-off")
|
||||||
|
span Disable notifications
|
||||||
|
|
||||||
|
.widget-input
|
||||||
|
label Test:
|
||||||
|
button.action(data-action="testNotification", data-trigger="click")
|
||||||
|
Icon("paper-plane")
|
||||||
|
span Send test notification
|
||||||
|
|
||||||
.widget.mountable
|
.widget.mountable
|
||||||
h3.widget-title
|
h3.widget-title
|
||||||
Icon("user-plus")
|
Icon("user-plus")
|
||||||
@ -49,17 +72,6 @@ component Settings(user *arn.User)
|
|||||||
Icon("circle-o")
|
Icon("circle-o")
|
||||||
span Not connected
|
span Not connected
|
||||||
|
|
||||||
.widget.mountable
|
|
||||||
h3.widget-title
|
|
||||||
Icon("puzzle-piece")
|
|
||||||
span Extensions
|
|
||||||
|
|
||||||
.widget-input
|
|
||||||
label Chrome Extension:
|
|
||||||
button.action(data-action="installExtension", data-trigger="click")
|
|
||||||
Icon("chrome")
|
|
||||||
span Get the Chrome Extension
|
|
||||||
|
|
||||||
.widget.mountable
|
.widget.mountable
|
||||||
h3.widget-title
|
h3.widget-title
|
||||||
Icon("download")
|
Icon("download")
|
||||||
@ -78,6 +90,17 @@ component Settings(user *arn.User)
|
|||||||
Icon("upload")
|
Icon("upload")
|
||||||
span Export anime list as JSON
|
span Export anime list as JSON
|
||||||
|
|
||||||
|
.widget.mountable
|
||||||
|
h3.widget-title
|
||||||
|
Icon("puzzle-piece")
|
||||||
|
span Extensions
|
||||||
|
|
||||||
|
.widget-input
|
||||||
|
label Chrome Extension:
|
||||||
|
button.action(data-action="installExtension", data-trigger="click")
|
||||||
|
Icon("chrome")
|
||||||
|
span Get the Chrome Extension
|
||||||
|
|
||||||
//- .widget.mountable(data-api="/api/settings/" + user.ID)
|
//- .widget.mountable(data-api="/api/settings/" + user.ID)
|
||||||
//- h3.widget-title
|
//- h3.widget-title
|
||||||
//- Icon("cogs")
|
//- Icon("cogs")
|
||||||
|
39
patches/add-push-subs/add-push-subs.go
Normal file
39
patches/add-push-subs/add-push-subs.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/animenotifier/arn"
|
||||||
|
"github.com/fatih/color"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
color.Yellow("Adding push subscriptions to users who don't have one")
|
||||||
|
|
||||||
|
// Get a stream of all users
|
||||||
|
allUsers, err := arn.StreamUsers()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over the stream
|
||||||
|
for user := range allUsers {
|
||||||
|
exists, err := arn.DB.Exists("PushSubscriptions", user.ID)
|
||||||
|
|
||||||
|
if err == nil && !exists {
|
||||||
|
fmt.Println(user.Nick)
|
||||||
|
|
||||||
|
err := arn.DB.Set("PushSubscriptions", user.ID, &arn.PushSubscriptions{
|
||||||
|
UserID: user.ID,
|
||||||
|
Items: make([]*arn.PushSubscription, 0),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
color.Red(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
color.Green("Finished.")
|
||||||
|
}
|
@ -214,6 +214,23 @@ export function search(arn: AnimeNotifier, search: HTMLInputElement, e: Keyboard
|
|||||||
arn.diff("/search/" + term)
|
arn.diff("/search/" + term)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enable notifications
|
||||||
|
export function enableNotifications(arn: AnimeNotifier, button: HTMLElement) {
|
||||||
|
arn.pushManager.subscribe(arn.user.dataset.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable notifications
|
||||||
|
export function disableNotifications(arn: AnimeNotifier, button: HTMLElement) {
|
||||||
|
arn.pushManager.unsubscribe(arn.user.dataset.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test notification
|
||||||
|
export function testNotification(arn: AnimeNotifier) {
|
||||||
|
fetch("/api/test/notification", {
|
||||||
|
credentials: "same-origin"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Add anime to collection
|
// Add anime to collection
|
||||||
export function addAnimeToCollection(arn: AnimeNotifier, button: HTMLElement) {
|
export function addAnimeToCollection(arn: AnimeNotifier, button: HTMLElement) {
|
||||||
button.innerText = "Adding..."
|
button.innerText = "Adding..."
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { Application } from "./Application"
|
import * as actions from "./Actions"
|
||||||
import { Diff } from "./Diff"
|
|
||||||
import { displayAiringDate, displayDate } from "./DateView"
|
import { displayAiringDate, displayDate } from "./DateView"
|
||||||
import { findAll, delay, canUseWebP } from "./Utils"
|
import { findAll, delay, canUseWebP } from "./Utils"
|
||||||
|
import { Application } from "./Application"
|
||||||
|
import { Diff } from "./Diff"
|
||||||
import { MutationQueue } from "./MutationQueue"
|
import { MutationQueue } from "./MutationQueue"
|
||||||
import { StatusMessage } from "./StatusMessage"
|
import { StatusMessage } from "./StatusMessage"
|
||||||
import * as actions from "./Actions"
|
import { PushManager } from "./PushManager"
|
||||||
|
|
||||||
export class AnimeNotifier {
|
export class AnimeNotifier {
|
||||||
app: Application
|
app: Application
|
||||||
@ -14,6 +15,7 @@ export class AnimeNotifier {
|
|||||||
contentLoadedActions: Promise<any>
|
contentLoadedActions: Promise<any>
|
||||||
statusMessage: StatusMessage
|
statusMessage: StatusMessage
|
||||||
visibilityObserver: IntersectionObserver
|
visibilityObserver: IntersectionObserver
|
||||||
|
pushManager: PushManager
|
||||||
|
|
||||||
imageFound: MutationQueue
|
imageFound: MutationQueue
|
||||||
imageNotFound: MutationQueue
|
imageNotFound: MutationQueue
|
||||||
@ -104,6 +106,9 @@ export class AnimeNotifier {
|
|||||||
|
|
||||||
// Service worker
|
// Service worker
|
||||||
this.registerServiceWorker()
|
this.registerServiceWorker()
|
||||||
|
|
||||||
|
// Push manager
|
||||||
|
this.pushManager = new PushManager()
|
||||||
}
|
}
|
||||||
|
|
||||||
onContentLoaded() {
|
onContentLoaded() {
|
||||||
|
105
scripts/PushManager.ts
Normal file
105
scripts/PushManager.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
export class PushManager {
|
||||||
|
pushSupported: boolean
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.pushSupported = ("serviceWorker" in navigator) && ("PushManager" in window)
|
||||||
|
}
|
||||||
|
|
||||||
|
async subscribe(userId: string) {
|
||||||
|
if(!this.pushSupported) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let registration = await navigator.serviceWorker.ready
|
||||||
|
let subscription = await registration.pushManager.getSubscription()
|
||||||
|
|
||||||
|
if(!subscription) {
|
||||||
|
subscription = await registration.pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: urlBase64ToUint8Array("BAwPKVCWQ2_nc7SIGltYfWZhMpW54BSkbwelpa8eYMbqSitmCAGm3xRBdRiq1Wt-hUsE7x59GCcaJxqQtF2hZPw")
|
||||||
|
})
|
||||||
|
|
||||||
|
this.subscribeOnServer(subscription, userId)
|
||||||
|
} else {
|
||||||
|
console.log("Using existing subscription", subscription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async unsubscribe(userId: string) {
|
||||||
|
if(!this.pushSupported) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let registration = await navigator.serviceWorker.ready
|
||||||
|
let subscription = await registration.pushManager.getSubscription()
|
||||||
|
|
||||||
|
if(!subscription) {
|
||||||
|
console.error("Subscription does not exist")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await subscription.unsubscribe()
|
||||||
|
|
||||||
|
this.unsubscribeOnServer(subscription, userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeOnServer(subscription: PushSubscription, userId: string) {
|
||||||
|
console.log("Send subscription to server...")
|
||||||
|
|
||||||
|
let rawKey = subscription.getKey("p256dh")
|
||||||
|
let key = rawKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : ""
|
||||||
|
|
||||||
|
let rawSecret = subscription.getKey("auth")
|
||||||
|
let secret = rawSecret ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawSecret))) : ""
|
||||||
|
|
||||||
|
let endpoint = subscription.endpoint
|
||||||
|
|
||||||
|
let pushSubscription = {
|
||||||
|
endpoint,
|
||||||
|
p256dh: key,
|
||||||
|
auth: secret,
|
||||||
|
platform: navigator.platform,
|
||||||
|
screen: {
|
||||||
|
width: window.screen.width,
|
||||||
|
height: window.screen.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch("/api/pushsubscriptions/" + userId + "/add", {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "same-origin",
|
||||||
|
body: JSON.stringify(pushSubscription)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribeOnServer(subscription: PushSubscription, userId: string) {
|
||||||
|
console.log("Send unsubscription to server...")
|
||||||
|
console.log(subscription)
|
||||||
|
|
||||||
|
let pushSubscription = {
|
||||||
|
endpoint: subscription.endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch("/api/pushsubscriptions/" + userId + "/remove", {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "same-origin",
|
||||||
|
body: JSON.stringify(pushSubscription)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function urlBase64ToUint8Array(base64String) {
|
||||||
|
const padding = "=".repeat((4 - base64String.length % 4) % 4)
|
||||||
|
const base64 = (base64String + padding)
|
||||||
|
.replace(/\-/g, "+")
|
||||||
|
.replace(/_/g, "/")
|
||||||
|
|
||||||
|
const rawData = window.atob(base64)
|
||||||
|
const outputArray = new Uint8Array(rawData.length)
|
||||||
|
|
||||||
|
for(let i = 0; i < rawData.length; ++i) {
|
||||||
|
outputArray[i] = rawData.charCodeAt(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputArray
|
||||||
|
}
|
4
sw/index.d.ts
vendored
Normal file
4
sw/index.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
type NotificationEvent = any;
|
||||||
|
type InstallEvent = any;
|
||||||
|
type FetchEvent = any;
|
||||||
|
type PushEvent = any;
|
@ -4,7 +4,7 @@ const CACHE = "v-1"
|
|||||||
const RELOADS = new Map<string, Promise<Response>>()
|
const RELOADS = new Map<string, Promise<Response>>()
|
||||||
const ETAGS = new Map<string, string>()
|
const ETAGS = new Map<string, string>()
|
||||||
|
|
||||||
self.addEventListener("install", (evt: any) => {
|
self.addEventListener("install", (evt: InstallEvent) => {
|
||||||
console.log("Service worker install")
|
console.log("Service worker install")
|
||||||
|
|
||||||
evt.waitUntil(
|
evt.waitUntil(
|
||||||
@ -37,7 +37,7 @@ self.addEventListener("message", (evt: any) => {
|
|||||||
evt.waitUntil(
|
evt.waitUntil(
|
||||||
refresh.then((response: Response) => {
|
refresh.then((response: Response) => {
|
||||||
// If the fresh copy was used to serve the request instead of the cache,
|
// If the fresh copy was used to serve the request instead of the cache,
|
||||||
// we don't need to tell the client to do a refresh.
|
// we don"t need to tell the client to do a refresh.
|
||||||
if(response.bodyUsed) {
|
if(response.bodyUsed) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -60,9 +60,10 @@ self.addEventListener("message", (evt: any) => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
self.addEventListener("fetch", async (evt: any) => {
|
self.addEventListener("fetch", async (evt: FetchEvent) => {
|
||||||
let request = evt.request
|
let request = evt.request
|
||||||
let isAuth = request.url.includes("/auth/") || request.url.includes("/logout")
|
let isAuth = request.url.includes("/auth/") || request.url.includes("/logout")
|
||||||
|
let ignoreCache = request.url.includes("/api/") || request.url.includes("chrome-extension")
|
||||||
|
|
||||||
// Delete existing cache on authentication
|
// Delete existing cache on authentication
|
||||||
if(isAuth) {
|
if(isAuth) {
|
||||||
@ -70,7 +71,7 @@ self.addEventListener("fetch", async (evt: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Do not use cache in some cases
|
// Do not use cache in some cases
|
||||||
if(request.method !== "GET" || isAuth || request.url.includes("chrome-extension")) {
|
if(request.method !== "GET" || isAuth || ignoreCache) {
|
||||||
return evt.waitUntil(evt.respondWith(fetch(request)))
|
return evt.waitUntil(evt.respondWith(fetch(request)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,6 +110,38 @@ self.addEventListener("fetch", async (evt: any) => {
|
|||||||
return evt.waitUntil(evt.respondWith(networkOrCache))
|
return evt.waitUntil(evt.respondWith(networkOrCache))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
self.addEventListener("push", (evt: PushEvent) => {
|
||||||
|
var payload = evt.data ? evt.data.text() : "no payload"
|
||||||
|
|
||||||
|
evt.waitUntil(
|
||||||
|
(self as any).registration.showNotification("beta.notify.moe Service Worker", {
|
||||||
|
body: payload
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
self.addEventListener("pushsubscriptionchange", (evt: any) => {
|
||||||
|
console.log("pushsubscriptionchange", evt)
|
||||||
|
})
|
||||||
|
|
||||||
|
self.addEventListener("notificationclick", (evt: NotificationEvent) => {
|
||||||
|
console.log(evt)
|
||||||
|
|
||||||
|
evt.notification.close()
|
||||||
|
|
||||||
|
evt.waitUntil(
|
||||||
|
(self as any).clients.matchAll().then(function(clientList) {
|
||||||
|
// If there is at least one client, focus it.
|
||||||
|
if(clientList.length > 0) {
|
||||||
|
return clientList[0].focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise open a new window
|
||||||
|
return (self as any).clients.openWindow("https://notify.moe")
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
function installCache() {
|
function installCache() {
|
||||||
return caches.open(CACHE).then(cache => {
|
return caches.open(CACHE).then(cache => {
|
||||||
return cache.addAll([
|
return cache.addAll([
|
||||||
|
Loading…
Reference in New Issue
Block a user