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/newsoundtrack"
|
||||
"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/profile"
|
||||
"github.com/animenotifier/notify.moe/pages/search"
|
||||
@ -126,6 +127,9 @@ func configure(app *aero.Application) *aero.Application {
|
||||
// Browser extension
|
||||
app.Ajax("/extension/embed", embed.Get)
|
||||
|
||||
// API
|
||||
app.Get("/api/test/notification", notifications.Test)
|
||||
|
||||
// Middleware
|
||||
app.Use(middleware.Log())
|
||||
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.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
|
||||
h3.widget-title
|
||||
Icon("user-plus")
|
||||
@ -49,17 +72,6 @@ component Settings(user *arn.User)
|
||||
Icon("circle-o")
|
||||
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
|
||||
h3.widget-title
|
||||
Icon("download")
|
||||
@ -78,6 +90,17 @@ component Settings(user *arn.User)
|
||||
Icon("upload")
|
||||
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)
|
||||
//- h3.widget-title
|
||||
//- 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
export function addAnimeToCollection(arn: AnimeNotifier, button: HTMLElement) {
|
||||
button.innerText = "Adding..."
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Application } from "./Application"
|
||||
import { Diff } from "./Diff"
|
||||
import * as actions from "./Actions"
|
||||
import { displayAiringDate, displayDate } from "./DateView"
|
||||
import { findAll, delay, canUseWebP } from "./Utils"
|
||||
import { Application } from "./Application"
|
||||
import { Diff } from "./Diff"
|
||||
import { MutationQueue } from "./MutationQueue"
|
||||
import { StatusMessage } from "./StatusMessage"
|
||||
import * as actions from "./Actions"
|
||||
import { PushManager } from "./PushManager"
|
||||
|
||||
export class AnimeNotifier {
|
||||
app: Application
|
||||
@ -14,6 +15,7 @@ export class AnimeNotifier {
|
||||
contentLoadedActions: Promise<any>
|
||||
statusMessage: StatusMessage
|
||||
visibilityObserver: IntersectionObserver
|
||||
pushManager: PushManager
|
||||
|
||||
imageFound: MutationQueue
|
||||
imageNotFound: MutationQueue
|
||||
@ -104,6 +106,9 @@ export class AnimeNotifier {
|
||||
|
||||
// Service worker
|
||||
this.registerServiceWorker()
|
||||
|
||||
// Push manager
|
||||
this.pushManager = new PushManager()
|
||||
}
|
||||
|
||||
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 ETAGS = new Map<string, string>()
|
||||
|
||||
self.addEventListener("install", (evt: any) => {
|
||||
self.addEventListener("install", (evt: InstallEvent) => {
|
||||
console.log("Service worker install")
|
||||
|
||||
evt.waitUntil(
|
||||
@ -37,7 +37,7 @@ self.addEventListener("message", (evt: any) => {
|
||||
evt.waitUntil(
|
||||
refresh.then((response: Response) => {
|
||||
// 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) {
|
||||
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 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
|
||||
if(isAuth) {
|
||||
@ -70,7 +71,7 @@ self.addEventListener("fetch", async (evt: any) => {
|
||||
}
|
||||
|
||||
// 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)))
|
||||
}
|
||||
|
||||
@ -109,6 +110,38 @@ self.addEventListener("fetch", async (evt: any) => {
|
||||
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() {
|
||||
return caches.open(CACHE).then(cache => {
|
||||
return cache.addAll([
|
||||
|
Loading…
Reference in New Issue
Block a user