From c6f74f5296203a8c334fad730f72b0df1309eb76 Mon Sep 17 00:00:00 2001 From: Aevann1 Date: Fri, 2 Dec 2022 23:50:57 +0200 Subject: [PATCH] stop using pusher --- env | 4 +- files/assets/js/register_service_worker.js | 70 +++++++++++++++++++ .../{service-worker.js => service_worker.js} | 39 +++++++++-- files/assets/js/vendor/pusher.js | 1 - files/classes/__init__.py | 1 + files/classes/push_subscriptions.py | 19 +++++ files/helpers/alerts.py | 52 ++++++-------- files/helpers/const.py | 6 +- files/routes/__init__.py | 1 + files/routes/comments.py | 6 +- files/routes/jinja2.py | 2 +- files/routes/push_notifs.py | 23 ++++++ files/routes/users.py | 12 ++-- files/templates/home.html | 15 ++-- nginx-serve-static.conf | 2 +- nginx.conf | 2 +- requirements.txt | 2 +- startup_docker.sh | 2 +- ubuntu_setup.sh | 4 +- 19 files changed, 192 insertions(+), 71 deletions(-) create mode 100644 files/assets/js/register_service_worker.js rename files/assets/js/{service-worker.js => service_worker.js} (60%) delete mode 100644 files/assets/js/vendor/pusher.js create mode 100644 files/classes/push_subscriptions.py create mode 100644 files/routes/push_notifs.py diff --git a/env b/env index b00bed215..27a5fb312 100644 --- a/env +++ b/env @@ -12,8 +12,8 @@ export DISCORD_BOT_TOKEN="blahblahblah" export TURNSTILE_SITEKEY="blahblahblah" export TURNSTILE_SECRET="blahblahblah" export YOUTUBE_KEY="blahblahblah" -export PUSHER_ID="blahblahblah" -export PUSHER_KEY="blahblahblah" +export VAPID_PUBLIC_KEY="blahblahblah" +export VAPID_PRIVATE_KEY="blahblahblah" export IMGUR_KEY="blahblahblah" export SPAM_SIMILARITY_THRESHOLD="0.5" export SPAM_URL_SIMILARITY_THRESHOLD="0.1" diff --git a/files/assets/js/register_service_worker.js b/files/assets/js/register_service_worker.js new file mode 100644 index 000000000..ecdd757aa --- /dev/null +++ b/files/assets/js/register_service_worker.js @@ -0,0 +1,70 @@ +'use strict'; + +function urlB64ToUint8Array(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; +} + +function updateSubscriptionOnServer(subscription, apiEndpoint) { + var formData = new FormData(); + formData.append("subscription_json", JSON.stringify(subscription)); + + const xhr = createXhrWithFormKey( + apiEndpoint, + 'POST', + formData + ); + + xhr[0].send(xhr[1]); +} + +function subscribeUser(swRegistration, applicationServerPublicKey, apiEndpoint) { + const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey); + swRegistration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: applicationServerKey + }) + .then(function(subscription) { + return updateSubscriptionOnServer(subscription, apiEndpoint); + + }) + .then(function(response) { + if (!response.ok) { + throw new Error('Bad status code from server.'); + } + return response.json(); + }) + .then(function(responseData) { + if (responseData.status!=="success") { + throw new Error('Bad response from server.'); + } + }) + .catch(function() { + }); +} + +function registerServiceWorker(serviceWorkerUrl, applicationServerPublicKey, apiEndpoint){ + let swRegistration = null; + if ('serviceWorker' in navigator && 'PushManager' in window) { + navigator.serviceWorker.register(serviceWorkerUrl) + .then(function(swReg) { + subscribeUser(swReg, applicationServerPublicKey, apiEndpoint); + + swRegistration = swReg; + }) + .catch(function() { + }); + } else { + } + return swRegistration; +} diff --git a/files/assets/js/service-worker.js b/files/assets/js/service_worker.js similarity index 60% rename from files/assets/js/service-worker.js rename to files/assets/js/service_worker.js index 80103200d..4d6f9e29b 100644 --- a/files/assets/js/service-worker.js +++ b/files/assets/js/service_worker.js @@ -1,12 +1,9 @@ -importScripts("https://js.pusher.com/beams/service-worker.js"); - -// offline static page handler -// @crgd +'use strict'; const CACHE_NAME = "offlineCache-v1"; const OFFLINE_URL = "/assets/offline.html"; -self.addEventListener("install", (event) => { +self.addEventListener("install", () => { const cacheOfflinePage = async () => { const cache = await caches.open(CACHE_NAME); await cache.add(new Request(OFFLINE_URL, {cache: "reload"})); @@ -41,11 +38,39 @@ self.addEventListener("fetch", (event) => { const networkResponse = await fetch(event.request); return networkResponse; } catch (error) { - console.log("Fetch failed; returning offline page instead.", error); - const cachedResponse = await caches.match(OFFLINE_URL); return cachedResponse; } })()); } }); + +self.addEventListener('push', function(event) { + const pushData = event.data.text(); + let data, title, body, url, icon; + try { + data = JSON.parse(pushData); + title = data.title; + body = data.body; + url = data.url; + icon = data.icon; + } catch(e) { + title = "Untitled"; + body = pushData; + } + const options = { + body: body, + data: {url: url}, + icon: icon + }; + + event.waitUntil( + self.registration.showNotification(title, options) + ); +}); + +self.addEventListener('notificationclick', (e) => { + if (e.notification.data.url) + e.waitUntil(clients.openWindow(e.notification.data.url)); + e.notification.close(); +}); diff --git a/files/assets/js/vendor/pusher.js b/files/assets/js/vendor/pusher.js deleted file mode 100644 index 98e37d5dd..000000000 --- a/files/assets/js/vendor/pusher.js +++ /dev/null @@ -1 +0,0 @@ -var PusherPushNotifications=function(e){"use strict";var t,r,n=function e(t){if(Array.isArray(t)){for(var r=0,n=Array(t.length);r=0;--a){var s=this.tryEntries[a],o=s.completion;if("root"===s.tryLoc)return i("end");if(s.tryLoc<=this.prev){var c=n.call(s,"catchLoc"),u=n.call(s,"finallyLoc");if(c&&u){if(this.prev=0;--r){var i=this.tryEntries[r];if(i.tryLoc<=this.prev&&n.call(i,"finallyLoc")&&this.prev=0;--t){var r=this.tryEntries[t];if(r.finallyLoc===e)return this.complete(r.completion,r.afterLoc),$(r),d}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var r=this.tryEntries[t];if(r.tryLoc===e){var n=r.completion;if("throw"===n.type){var i=n.arg;$(r)}return i}}throw Error("illegal catch attempt")},delegateYield:function(e,r,n){return this.delegate={iterator:E(e),resultName:r,nextLoc:n},"next"===this.method&&(this.arg=t),d}},e}(e.exports);try{globalThis.regeneratorRuntime = undefined;regeneratorRuntime=t}catch(r){Function("r","regeneratorRuntime = r")(t)}})(r={exports:{}},r.exports),r.exports);function o(e,t,r,n,i,a,s){try{var o=e[a](s),c=o.value}catch(u){r(u);return}o.done?t(c):Promise.resolve(c).then(n,i)}var c=function e(t){return function(){var e=this,r=arguments;return new Promise(function(n,i){var a=t.apply(e,r);function s(e){o(a,n,i,s,c,"next",e)}function c(e){o(a,n,i,s,c,"throw",e)}s(void 0)})}},u=function e(t,r){if(!(t instanceof r))throw TypeError("Cannot call a class as a function")};function h(e,t){for(var r=0;r0&&void 0!==arguments[0]?arguments[0]:{},r=e.url,n=e.queryParams,i=e.headers;u(this,t),this.url=r,this.queryParams=n,this.headers=i}return f(t,[{key:"fetchToken",value:(e=c(s.mark(function e(t){var r,n,i,a;return s.wrap(function e(s){for(;;)switch(s.prev=s.next){case 0:return n=Object.entries(r=p({user_id:t},this.queryParams)).map(function(e){return e.map(encodeURIComponent).join("=")}).join("&"),i={method:"GET",path:"".concat(this.url,"?").concat(n),headers:this.headers},s.next=5,d(i);case 5:return a=s.sent,s.abrupt("return",a);case 7:case"end":return s.stop()}},e,this)})),function t(r){return e.apply(this,arguments)})}]),t}(),g=function(){var e,t;function r(e){u(this,r),this._instanceId=e,this._dbConn=null}return f(r,[{key:"connect",value:function e(){var t=this;return new Promise(function(e,r){var n=indexedDB.open(t._dbName);n.onsuccess=function(r){var n=r.target.result;t._dbConn=n,t._readState().then(function(e){return null===e?t.clear():Promise.resolve()}).then(e)},n.onupgradeneeded=function(e){e.target.result.createObjectStore("beams",{keyPath:"instance_id"})},n.onerror=function(e){r(Error("Database error: ".concat(e.target.error)))}})}},{key:"clear",value:function e(){return this._writeState({instance_id:this._instanceId,device_id:null,token:null,user_id:null})}},{key:"_readState",value:function e(){var t=this;if(!this.isConnected)throw Error("Cannot read value: DeviceStateStore not connected to IndexedDB");return new Promise(function(e,r){var n=t._dbConn.transaction("beams").objectStore("beams").get(t._instanceId);n.onsuccess=function(t){var r=t.target.result;r||e(null),e(r)},n.onerror=function(e){r(e.target.error)}})}},{key:"_readProperty",value:(e=c(s.mark(function e(t){var r;return s.wrap(function e(n){for(;;)switch(n.prev=n.next){case 0:return n.next=2,this._readState();case 2:if(null!==(r=n.sent)){n.next=5;break}return n.abrupt("return",null);case 5:return n.abrupt("return",r[t]||null);case 6:case"end":return n.stop()}},e,this)})),function t(r){return e.apply(this,arguments)})},{key:"_writeState",value:function e(t){var r=this;if(!this.isConnected)throw Error("Cannot write value: DeviceStateStore not connected to IndexedDB");return new Promise(function(e,n){var i=r._dbConn.transaction("beams","readwrite").objectStore("beams").put(t);i.onsuccess=function(t){e()},i.onerror=function(e){n(e.target.error)}})}},{key:"_writeProperty",value:(t=c(s.mark(function e(t,r){var n;return s.wrap(function e(i){for(;;)switch(i.prev=i.next){case 0:return i.next=2,this._readState();case 2:return(n=i.sent)[t]=r,i.next=6,this._writeState(n);case 6:case"end":return i.stop()}},e,this)})),function e(r,n){return t.apply(this,arguments)})},{key:"getToken",value:function e(){return this._readProperty("token")}},{key:"setToken",value:function e(t){return this._writeProperty("token",t)}},{key:"getDeviceId",value:function e(){return this._readProperty("device_id")}},{key:"setDeviceId",value:function e(t){return this._writeProperty("device_id",t)}},{key:"getUserId",value:function e(){return this._readProperty("user_id")}},{key:"setUserId",value:function e(t){return this._writeProperty("user_id",t)}},{key:"getLastSeenSdkVersion",value:function e(){return this._readProperty("last_seen_sdk_version")}},{key:"setLastSeenSdkVersion",value:function e(t){return this._writeProperty("last_seen_sdk_version",t)}},{key:"getLastSeenUserAgent",value:function e(){return this._readProperty("last_seen_user_agent")}},{key:"setLastSeenUserAgent",value:function e(t){return this._writeProperty("last_seen_user_agent",t)}},{key:"_dbName",get:function e(){return"beams-".concat(this._instanceId)}},{key:"isConnected",get:function e(){return null!==this._dbConn}}]),r}(),w="1.0.3",k=RegExp("^(_|\\-|=|@|,|\\.|;|[A-Z]|[a-z]|[0-9])*$"),x="/service-worker.js?v=3&pusherBeamsWebSDKVersion=".concat(w),b=Object.freeze({PERMISSION_PROMPT_REQUIRED:"PERMISSION_PROMPT_REQUIRED",PERMISSION_GRANTED_NOT_REGISTERED_WITH_BEAMS:"PERMISSION_GRANTED_NOT_REGISTERED_WITH_BEAMS",PERMISSION_GRANTED_REGISTERED_WITH_BEAMS:"PERMISSION_GRANTED_REGISTERED_WITH_BEAMS",PERMISSION_DENIED:"PERMISSION_DENIED"}),S=function(){var e,t,r,n,i,a,o,h,p,l,v,y,k,x,S,$,P,R,T,L,N,C;function O(e){if(u(this,O),!e)throw Error("Config object required");var t=e.instanceId,r=e.endpointOverride,n=e.serviceWorkerRegistration,i=void 0===n?null:n;if(void 0===t)throw Error("Instance ID is required");if("string"!=typeof t)throw Error("Instance ID must be a string");if(0===t.length)throw Error("Instance ID cannot be empty");if(!("indexedDB"in window))throw Error("Pusher Beams does not support this browser version (IndexedDB not supported)");if(!window.isSecureContext)throw Error("Pusher Beams relies on Service Workers, which only work in secure contexts. Check that your page is being served from localhost/over HTTPS");if(!("serviceWorker"in navigator))throw Error("Pusher Beams does not support this browser version (Service Workers not supported)");if(!("PushManager"in window))throw Error("Pusher Beams does not support this browser version (Web Push not supported)");if(i){var a=i.scope;if(!location.href.startsWith(a))throw Error("Could not initialize Pusher web push: current page not in serviceWorkerRegistration scope (".concat(a,")"))}this.instanceId=t,this._deviceId=null,this._token=null,this._userId=null,this._serviceWorkerRegistration=i,this._deviceStateStore=new g(t),this._endpoint=void 0===r?null:r,this._ready=this._init()}return f(O,[{key:"_init",value:(e=c(s.mark(function e(){return s.wrap(function e(t){for(;;)switch(t.prev=t.next){case 0:if(!(null!==this._deviceId)){t.next=2;break}return t.abrupt("return");case 2:return t.next=4,this._deviceStateStore.connect();case 4:if(!this._serviceWorkerRegistration){t.next=9;break}return t.next=7,window.navigator.serviceWorker.ready;case 7:t.next=12;break;case 9:return t.next=11,I();case 11:this._serviceWorkerRegistration=t.sent;case 12:return t.next=14,this._detectSubscriptionChange();case 14:return t.next=16,this._deviceStateStore.getDeviceId();case 16:return this._deviceId=t.sent,t.next=19,this._deviceStateStore.getToken();case 19:return this._token=t.sent,t.next=22,this._deviceStateStore.getUserId();case 22:this._userId=t.sent;case 23:case"end":return t.stop()}},e,this)})),function t(){return e.apply(this,arguments)})},{key:"_resolveSDKState",value:(t=c(s.mark(function e(){return s.wrap(function e(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,this._ready;case 2:return t.next=4,this._detectSubscriptionChange();case 4:case"end":return t.stop()}},e,this)})),function e(){return t.apply(this,arguments)})},{key:"_detectSubscriptionChange",value:(r=c(s.mark(function e(){var t,r,n;return s.wrap(function e(i){for(;;)switch(i.prev=i.next){case 0:return i.next=2,this._deviceStateStore.getToken();case 2:return t=i.sent,i.next=5,_(this._serviceWorkerRegistration);case 5:if(!(n=t!==(r=i.sent))){i.next=13;break}return i.next=10,this._deviceStateStore.clear();case 10:this._deviceId=null,this._token=null,this._userId=null;case 13:case"end":return i.stop()}},e,this)})),function e(){return r.apply(this,arguments)})},{key:"getDeviceId",value:(n=c(s.mark(function e(){var t=this;return s.wrap(function e(r){for(;;)switch(r.prev=r.next){case 0:return r.next=2,this._resolveSDKState();case 2:return r.abrupt("return",this._ready.then(function(){return t._deviceId}));case 3:case"end":return r.stop()}},e,this)})),function e(){return n.apply(this,arguments)})},{key:"getToken",value:(i=c(s.mark(function e(){var t=this;return s.wrap(function e(r){for(;;)switch(r.prev=r.next){case 0:return r.next=2,this._resolveSDKState();case 2:return r.abrupt("return",this._ready.then(function(){return t._token}));case 3:case"end":return r.stop()}},e,this)})),function e(){return i.apply(this,arguments)})},{key:"getUserId",value:(a=c(s.mark(function e(){var t=this;return s.wrap(function e(r){for(;;)switch(r.prev=r.next){case 0:return r.next=2,this._resolveSDKState();case 2:return r.abrupt("return",this._ready.then(function(){return t._userId}));case 3:case"end":return r.stop()}},e,this)})),function e(){return a.apply(this,arguments)})},{key:"_throwIfNotStarted",value:function e(t){if(!this._deviceId)throw Error("".concat(t,". SDK not registered with Beams. Did you call .start?"))}},{key:"start",value:(o=c(s.mark(function e(){var t,r,n,i;return s.wrap(function e(a){for(;;)switch(a.prev=a.next){case 0:return a.next=2,this._resolveSDKState();case 2:if(D()){a.next=4;break}return a.abrupt("return",this);case 4:if(!(null!==this._deviceId)){a.next=6;break}return a.abrupt("return",this);case 6:return a.next=8,this._getPublicKey();case 8:return r=(t=a.sent).vapidPublicKey,a.next=12,this._getPushToken(r);case 12:return n=a.sent,a.next=15,this._registerDevice(n);case 15:return i=a.sent,a.next=18,this._deviceStateStore.setToken(n);case 18:return a.next=20,this._deviceStateStore.setDeviceId(i);case 20:return a.next=22,this._deviceStateStore.setLastSeenSdkVersion(w);case 22:return a.next=24,this._deviceStateStore.setLastSeenUserAgent(window.navigator.userAgent);case 24:return this._token=n,this._deviceId=i,a.abrupt("return",this);case 27:case"end":return a.stop()}},e,this)})),function e(){return o.apply(this,arguments)})},{key:"getRegistrationState",value:(h=c(s.mark(function e(){return s.wrap(function e(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,this._resolveSDKState();case 2:if("denied"!==Notification.permission){t.next=4;break}return t.abrupt("return",b.PERMISSION_DENIED);case 4:if(!("granted"===Notification.permission&&null!==this._deviceId)){t.next=6;break}return t.abrupt("return",b.PERMISSION_GRANTED_REGISTERED_WITH_BEAMS);case 6:if(!("granted"===Notification.permission&&null===this._deviceId)){t.next=8;break}return t.abrupt("return",b.PERMISSION_GRANTED_NOT_REGISTERED_WITH_BEAMS);case 8:return t.abrupt("return",b.PERMISSION_PROMPT_REQUIRED);case 9:case"end":return t.stop()}},e,this)})),function e(){return h.apply(this,arguments)})},{key:"addDeviceInterest",value:(p=c(s.mark(function e(t){var r,n;return s.wrap(function e(i){for(;;)switch(i.prev=i.next){case 0:return i.next=2,this._resolveSDKState();case 2:return this._throwIfNotStarted("Could not add Device Interest"),m(t),n={method:"POST",path:r="".concat(this._baseURL,"/device_api/v1/instances/").concat(encodeURIComponent(this.instanceId),"/devices/web/").concat(this._deviceId,"/interests/").concat(encodeURIComponent(t))},i.next=8,d(n);case 8:case"end":return i.stop()}},e,this)})),function e(t){return p.apply(this,arguments)})},{key:"removeDeviceInterest",value:(l=c(s.mark(function e(t){var r,n;return s.wrap(function e(i){for(;;)switch(i.prev=i.next){case 0:return i.next=2,this._resolveSDKState();case 2:return this._throwIfNotStarted("Could not remove Device Interest"),m(t),n={method:"DELETE",path:r="".concat(this._baseURL,"/device_api/v1/instances/").concat(encodeURIComponent(this.instanceId),"/devices/web/").concat(this._deviceId,"/interests/").concat(encodeURIComponent(t))},i.next=8,d(n);case 8:case"end":return i.stop()}},e,this)})),function e(t){return l.apply(this,arguments)})},{key:"getDeviceInterests",value:(v=c(s.mark(function e(){var t,r;return s.wrap(function e(n){for(;;)switch(n.prev=n.next){case 0:return n.next=2,this._resolveSDKState();case 2:return this._throwIfNotStarted("Could not get Device Interests"),r={method:"GET",path:t="".concat(this._baseURL,"/device_api/v1/instances/").concat(encodeURIComponent(this.instanceId),"/devices/web/").concat(this._deviceId,"/interests")},n.next=7,d(r);case 7:if(n.t0=n.sent.interests,n.t0){n.next=10;break}n.t0=[];case 10:return n.abrupt("return",n.t0);case 11:case"end":return n.stop()}},e,this)})),function e(){return v.apply(this,arguments)})},{key:"setDeviceInterests",value:(y=c(s.mark(function e(t){var r,n,i,a,o,c,u,h,f;return s.wrap(function e(s){for(;;)switch(s.prev=s.next){case 0:return s.next=2,this._resolveSDKState();case 2:if(this._throwIfNotStarted("Could not set Device Interests"),null!=t){s.next=5;break}throw Error("interests argument is required");case 5:if(Array.isArray(t)){s.next=7;break}throw Error("interests argument must be an array");case 7:if(!(t.length>5e3)){s.next=9;break}throw Error("Number of interests (".concat(t.length,") exceeds maximum of ").concat(5e3));case 9:for(r=!0,n=!1,i=void 0,s.prev=12,a=t[Symbol.iterator]();!(r=(o=a.next()).done);r=!0)m(c=o.value);s.next=20;break;case 16:s.prev=16,s.t0=s.catch(12),n=!0,i=s.t0;case 20:s.prev=20,s.prev=21,r||null==a.return||a.return();case 23:if(s.prev=23,!n){s.next=26;break}throw i;case 26:return s.finish(23);case 27:return s.finish(20);case 28:return u=Array.from(new Set(t)),f={method:"PUT",path:h="".concat(this._baseURL,"/device_api/v1/instances/").concat(encodeURIComponent(this.instanceId),"/devices/web/").concat(this._deviceId,"/interests"),body:{interests:u}},s.next=33,d(f);case 33:case"end":return s.stop()}},e,this,[[12,16,20,28],[21,,23,27]])})),function e(t){return y.apply(this,arguments)})},{key:"clearDeviceInterests",value:(k=c(s.mark(function e(){return s.wrap(function e(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,this._resolveSDKState();case 2:return this._throwIfNotStarted("Could not clear Device Interests"),t.next=5,this.setDeviceInterests([]);case 5:case"end":return t.stop()}},e,this)})),function e(){return k.apply(this,arguments)})},{key:"setUserId",value:(x=c(s.mark(function e(t,r){var n,i,a,o,c;return s.wrap(function e(s){for(;;)switch(s.prev=s.next){case 0:return s.next=2,this._resolveSDKState();case 2:if(D()){s.next=4;break}return s.abrupt("return");case 4:if(null!==this._deviceId){s.next=7;break}return n=Error(".start must be called before .setUserId"),s.abrupt("return",Promise.reject(n));case 7:if(!("string"!=typeof t)){s.next=9;break}throw Error("User ID must be a string (was ".concat(t,")"));case 9:if(""!==t){s.next=11;break}throw Error("User ID cannot be the empty string");case 11:if(!(null!==this._userId&&this._userId!==t)){s.next=13;break}throw Error("Changing the `userId` is not allowed.");case 13:return i="".concat(this._baseURL,"/device_api/v1/instances/").concat(encodeURIComponent(this.instanceId),"/devices/web/").concat(this._deviceId,"/user"),s.next=16,r.fetchToken(t);case 16:return o=(a=s.sent).token,c={method:"PUT",path:i,headers:{Authorization:"Bearer ".concat(o)}},s.next=21,d(c);case 21:return this._userId=t,s.abrupt("return",this._deviceStateStore.setUserId(t));case 23:case"end":return s.stop()}},e,this)})),function e(t,r){return x.apply(this,arguments)})},{key:"stop",value:(S=c(s.mark(function e(){return s.wrap(function e(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,this._resolveSDKState();case 2:if(D()){t.next=4;break}return t.abrupt("return");case 4:if(null!==this._deviceId){t.next=6;break}return t.abrupt("return");case 6:return t.next=8,this._deleteDevice();case 8:return t.next=10,this._deviceStateStore.clear();case 10:this._clearPushToken().catch(function(){}),this._deviceId=null,this._token=null,this._userId=null;case 14:case"end":return t.stop()}},e,this)})),function e(){return S.apply(this,arguments)})},{key:"clearAllState",value:($=c(s.mark(function e(){return s.wrap(function e(t){for(;;)switch(t.prev=t.next){case 0:if(D()){t.next=2;break}return t.abrupt("return");case 2:return t.next=4,this.stop();case 4:return t.next=6,this.start();case 6:case"end":return t.stop()}},e,this)})),function e(){return $.apply(this,arguments)})},{key:"_getPublicKey",value:(P=c(s.mark(function e(){var t,r;return s.wrap(function e(n){for(;;)switch(n.prev=n.next){case 0:return r={method:"GET",path:t="".concat(this._baseURL,"/device_api/v1/instances/").concat(encodeURIComponent(this.instanceId),"/web-vapid-public-key")},n.abrupt("return",d(r));case 3:case"end":return n.stop()}},e,this)})),function e(){return P.apply(this,arguments)})},{key:"_getPushToken",value:(R=c(s.mark(function e(t){var r;return s.wrap(function e(n){for(;;)switch(n.prev=n.next){case 0:return n.prev=0,n.next=3,this._clearPushToken();case 3:return n.next=5,this._serviceWorkerRegistration.pushManager.subscribe({userVisibleOnly:!0,applicationServerKey:E(t)});case 5:return r=n.sent,n.abrupt("return",btoa(JSON.stringify(r)));case 9:return n.prev=9,n.t0=n.catch(0),n.abrupt("return",Promise.reject(n.t0));case 12:case"end":return n.stop()}},e,this,[[0,9]])})),function e(t){return R.apply(this,arguments)})},{key:"_clearPushToken",value:(T=c(s.mark(function e(){return s.wrap(function e(t){for(;;)switch(t.prev=t.next){case 0:return t.abrupt("return",navigator.serviceWorker.ready.then(function(e){return e.pushManager.getSubscription()}).then(function(e){e&&e.unsubscribe()}));case 1:case"end":return t.stop()}},e)})),function e(){return T.apply(this,arguments)})},{key:"_registerDevice",value:(L=c(s.mark(function e(t){var r,n,i,a;return s.wrap(function e(s){for(;;)switch(s.prev=s.next){case 0:return r="".concat(this._baseURL,"/device_api/v1/instances/").concat(encodeURIComponent(this.instanceId),"/devices/web"),n={token:t,metadata:{sdkVersion:w}},i={method:"POST",path:r,body:n},s.next=5,d(i);case 5:return a=s.sent,s.abrupt("return",a.id);case 7:case"end":return s.stop()}},e,this)})),function e(t){return L.apply(this,arguments)})},{key:"_deleteDevice",value:(N=c(s.mark(function e(){var t,r;return s.wrap(function e(n){for(;;)switch(n.prev=n.next){case 0:return r={method:"DELETE",path:t="".concat(this._baseURL,"/device_api/v1/instances/").concat(encodeURIComponent(this.instanceId),"/devices/web/").concat(encodeURIComponent(this._deviceId))},n.next=4,d(r);case 4:case"end":return n.stop()}},e,this)})),function e(){return N.apply(this,arguments)})},{key:"_updateDeviceMetadata",value:(C=c(s.mark(function e(){var t,r,n,i,a,o;return s.wrap(function e(s){for(;;)switch(s.prev=s.next){case 0:return t=window.navigator.userAgent,s.next=3,this._deviceStateStore.getLastSeenUserAgent();case 3:return r=s.sent,s.next=6,this._deviceStateStore.getLastSeenSdkVersion();case 6:if(n=s.sent,!(t===r&&w===n)){s.next=9;break}return s.abrupt("return");case 9:return i="".concat(this._baseURL,"/device_api/v1/instances/").concat(encodeURIComponent(this.instanceId),"/devices/web/").concat(this._deviceId,"/metadata"),a={sdkVersion:w},o={method:"PUT",path:i,body:a},s.next=14,d(o);case 14:return s.next=16,this._deviceStateStore.setLastSeenSdkVersion(w);case 16:return s.next=18,this._deviceStateStore.setLastSeenUserAgent(t);case 18:case"end":return s.stop()}},e,this)})),function e(){return C.apply(this,arguments)})},{key:"_baseURL",get:function e(){return null!==this._endpoint?this._endpoint:"https://".concat(this.instanceId,".pushnotifications.pusher.com")}}]),O}(),m=function e(t){if(null==t)throw Error("Interest name is required");if("string"!=typeof t)throw Error("Interest ".concat(t," is not a string"));if(!k.test(t))throw Error('interest "'.concat(t,'" contains a forbidden character. ')+"Allowed characters are: ASCII upper/lower-case letters, numbers or one of _-=@,.;");if(t.length>164)throw Error("Interest is longer than the maximum of ".concat(164," chars"))};function I(){return $.apply(this,arguments)}function $(){return($=c(s.mark(function e(){var t,r;return s.wrap(function e(n){for(;;)switch(n.prev=n.next){case 0:return n.next=2,fetch(x);case 2:if(!(200!==(r=(t=n.sent).status))){n.next=6;break}throw Error("Cannot start SDK, service worker missing: No file found at /service-worker.js?v=3");case 6:return window.navigator.serviceWorker.register(x,{updateViaCache:"none"}),n.abrupt("return",window.navigator.serviceWorker.ready);case 8:case"end":return n.stop()}},e)}))).apply(this,arguments)}function _(e){return e.pushManager.getSubscription().then(function(e){var t;return e?(t=e,btoa(JSON.stringify(t))):null})}function E(e){var t,r="=".repeat((4-e.length%4)%4),s=(e+r).replace(/-/g,"+").replace(/_/g,"/"),o=window.atob(s);return Uint8Array.from((n(t=o)||i(t)||a()).map(function(e){return e.charCodeAt(0)}))}function D(){var e=window.navigator,t=e.vendor,r=null!==window.chrome&&void 0!==window.chrome,n=e.userAgent.indexOf("OPR")>-1,i=e.userAgent.indexOf("Edg")>-1,a=e.userAgent.indexOf("Firefox")>-1,s=r&&"Google Inc."===t&&!i&&!n||n||a||i;return s||console.warn("Pusher Web Push Notifications supports Chrome, Firefox, Edge and Opera."),s}return e.Client=S,e.RegistrationState=b,e.TokenProvider=y,e}({});const pusherid=document.getElementById("pusherid").innerHTML,beamsClient=new PusherPushNotifications.Client({instanceId:pusherid}),strid=document.getElementById("strid").innerHTML;beamsClient.start().then(e=>e.getDeviceId()).then(()=>beamsClient.addDeviceInterest(strid)).then(()=>beamsClient.getDeviceInterests()).catch(console.error); diff --git a/files/classes/__init__.py b/files/classes/__init__.py index dd1f6f2e1..85fd12f5e 100644 --- a/files/classes/__init__.py +++ b/files/classes/__init__.py @@ -35,3 +35,4 @@ from .sub_logs import * from .media import * if FEATURES['STREAMERS']: from .streamers import * +from .push_subscriptions import * diff --git a/files/classes/push_subscriptions.py b/files/classes/push_subscriptions.py new file mode 100644 index 000000000..0a34763a9 --- /dev/null +++ b/files/classes/push_subscriptions.py @@ -0,0 +1,19 @@ +import time + +from sqlalchemy import Column, ForeignKey +from sqlalchemy.sql.sqltypes import * + +from files.classes import Base + +class PushSubscription(Base): + __tablename__ = "push_subscriptions" + user_id = Column(Integer, ForeignKey("users.id"), primary_key=True) + subscription_json = Column(String, primary_key=True) + created_utc = Column(Integer) + + def __init__(self, *args, **kwargs): + if "created_utc" not in kwargs: kwargs["created_utc"] = int(time.time()) + super().__init__(*args, **kwargs) + + def __repr__(self): + return f"<{self.__class__.__name__}(id={self.id})>" diff --git a/files/helpers/alerts.py b/files/helpers/alerts.py index 4afed5b1c..672f47971 100644 --- a/files/helpers/alerts.py +++ b/files/helpers/alerts.py @@ -1,9 +1,8 @@ from sys import stdout from flask import g -from pusher_push_notifications import PushNotifications -from files.classes import Comment, Notification +from files.classes import Comment, Notification, PushSubscription from .const import * from .regex import * @@ -103,35 +102,24 @@ def NOTIFY_USERS(text, v): return notify_users - bots -if PUSHER_ID != DEFAULT_CONFIG_VALUE: - beams_client = PushNotifications(instance_id=PUSHER_ID, secret_key=PUSHER_KEY) +if VAPID_PUBLIC_KEY != DEFAULT_CONFIG_VALUE: + from pywebpush import webpush + import json - def pusher_thread(interests, title, notifbody, url): - title = censor_slurs(title, None) - notifbody = censor_slurs(notifbody, None) - if len(notifbody) > PUSHER_LIMIT: - notifbody = notifbody[:PUSHER_LIMIT] + "..." + claims = {"sub": f"mailto:{EMAIL}"} - beams_client.publish_to_interests( - interests=[interests], - publish_body={ - 'web': { - 'notification': { - 'title': title, - 'body': notifbody, - 'deep_link': url, - 'icon': f'{SITE_FULL}/icon.webp?v=1', - } - }, - 'fcm': { - 'notification': { - 'title': title, - 'body': notifbody, - }, - 'data': { - 'url': url, - } - } - }, - ) - stdout.flush() + def push_notif(uid, title, body, url): + subscriptions = g.db.query(PushSubscription).filter_by(user_id=uid).all() + for subscription in subscriptions: + try: response = webpush( + subscription_info=json.loads(subscription.subscription_json), + data=json.dumps({ + "title": title, + "body": body, + 'url': url, + 'icon': f'{SITE_FULL}/icon.webp?v=1', + }), + vapid_private_key=VAPID_PRIVATE_KEY, + vapid_claims=claims + ) + except: continue diff --git a/files/helpers/const.py b/files/helpers/const.py index ea0fb8a39..3c830c65d 100644 --- a/files/helpers/const.py +++ b/files/helpers/const.py @@ -16,8 +16,8 @@ DISCORD_BOT_TOKEN = environ.get("DISCORD_BOT_TOKEN", DEFAULT_CONFIG_VALUE).strip TURNSTILE_SITEKEY = environ.get("TURNSTILE_SITEKEY", DEFAULT_CONFIG_VALUE).strip() TURNSTILE_SECRET = environ.get("TURNSTILE_SECRET", DEFAULT_CONFIG_VALUE).strip() YOUTUBE_KEY = environ.get("YOUTUBE_KEY", DEFAULT_CONFIG_VALUE).strip() -PUSHER_ID = environ.get("PUSHER_ID", DEFAULT_CONFIG_VALUE).strip() -PUSHER_KEY = environ.get("PUSHER_KEY", DEFAULT_CONFIG_VALUE).strip() +VAPID_PUBLIC_KEY = environ.get("VAPID_PUBLIC_KEY", DEFAULT_CONFIG_VALUE).strip() +VAPID_PRIVATE_KEY = environ.get("VAPID_PRIVATE_KEY", DEFAULT_CONFIG_VALUE).strip() IMGUR_KEY = environ.get("IMGUR_KEY", DEFAULT_CONFIG_VALUE).strip() SPAM_SIMILARITY_THRESHOLD = float(environ.get("SPAM_SIMILARITY_THRESHOLD", "0.5").strip()) SPAM_URL_SIMILARITY_THRESHOLD = float(environ.get("SPAM_URL_SIMILARITY_THRESHOLD", "0.1").strip()) @@ -55,7 +55,7 @@ DEFAULT_RATELIMIT = "3/second;30/minute;200/hour;1000/day" DEFAULT_RATELIMIT_SLOWER = "1/second;30/minute;200/hour;1000/day" DEFAULT_RATELIMIT_USER = DEFAULT_RATELIMIT_SLOWER -PUSHER_LIMIT = 1000 # API allows 10 KB but better safe than sorry +PUSH_NOTIF_LIMIT = 1000 # API allows 10 KB but better safe than sorry IS_LOCALHOST = SITE == "localhost" or SITE == "127.0.0.1" or SITE.startswith("192.168.") or SITE.endswith(".local") diff --git a/files/routes/__init__.py b/files/routes/__init__.py index 3be1167d0..a11c7c6c2 100644 --- a/files/routes/__init__.py +++ b/files/routes/__init__.py @@ -48,3 +48,4 @@ if FEATURES['ASSET_SUBMISSIONS']: if FEATURES['STREAMERS']: from .streamers import * from .special import * +from .push_notifs import * diff --git a/files/routes/comments.py b/files/routes/comments.py index 72faa75fe..17374a3b7 100644 --- a/files/routes/comments.py +++ b/files/routes/comments.py @@ -313,9 +313,7 @@ def comment(v): n = Notification(comment_id=c.id, user_id=x) g.db.add(n) - if parent.author.id != v.id and PUSHER_ID != DEFAULT_CONFIG_VALUE and not v.shadowbanned: - interests = f'{SITE}{parent.author.id}' - + if VAPID_PUBLIC_KEY != DEFAULT_CONFIG_VALUE and parent.author.id != v.id and not v.shadowbanned: title = f'New reply by @{c.author_name}' if len(c.body) > 500: notifbody = c.body[:500] + '...' @@ -323,7 +321,7 @@ def comment(v): url = f'{SITE_FULL}/comment/{c.id}?context=8&read=true#context' - gevent.spawn(pusher_thread, interests, title, notifbody, url) + push_notif(parent.author.id, title, notifbody, url) diff --git a/files/routes/jinja2.py b/files/routes/jinja2.py index 0b6d15491..4b8afed06 100644 --- a/files/routes/jinja2.py +++ b/files/routes/jinja2.py @@ -76,7 +76,7 @@ def calc_users(): @app.context_processor def inject_constants(): return {"environ":environ, "SITE":SITE, "SITE_NAME":SITE_NAME, "SITE_FULL":SITE_FULL, - "AUTOJANNY_ID":AUTOJANNY_ID, "MODMAIL_ID":MODMAIL_ID, "PUSHER_ID":PUSHER_ID, + "AUTOJANNY_ID":AUTOJANNY_ID, "MODMAIL_ID":MODMAIL_ID, "VAPID_PUBLIC_KEY":VAPID_PUBLIC_KEY, "CC":CC, "CC_TITLE":CC_TITLE, "listdir":listdir, "os_path":path, "AEVANN_ID":AEVANN_ID, "PIZZASHILL_ID":PIZZASHILL_ID, "DEFAULT_COLOR":DEFAULT_COLOR, "COLORS":COLORS, "time":time, "PERMS":PERMS, "FEATURES":FEATURES, diff --git a/files/routes/push_notifs.py b/files/routes/push_notifs.py new file mode 100644 index 000000000..13591b168 --- /dev/null +++ b/files/routes/push_notifs.py @@ -0,0 +1,23 @@ +from files.routes.wrappers import * +from files.__main__ import app +from flask import request, g +from files.classes.push_subscriptions import PushSubscription + +@app.post("/push_subscribe") +@auth_required +def push_subscribe(v): + subscription_json = request.values.get("subscription_json") + + subscription = g.db.query(PushSubscription).filter_by( + user_id=v.id, + subscription_json=subscription_json, + ).one_or_none() + + if not subscription: + subscription = PushSubscription( + user_id=v.id, + subscription_json=subscription_json, + ) + g.db.add(subscription) + + return '' diff --git a/files/routes/users.py b/files/routes/users.py index f20f83dd6..a98727d8b 100644 --- a/files/routes/users.py +++ b/files/routes/users.py @@ -471,9 +471,7 @@ def message2(v, username): g.db.add(notif) - if PUSHER_ID != DEFAULT_CONFIG_VALUE and not v.shadowbanned: - interests = f'{SITE}{user.id}' - + if VAPID_PUBLIC_KEY != DEFAULT_CONFIG_VALUE and not v.shadowbanned: title = f'New message from @{username}' if len(message) > 500: notifbody = message[:500] + '...' @@ -481,7 +479,7 @@ def message2(v, username): url = f'{SITE_FULL}/notifications/messages' - gevent.spawn(pusher_thread, interests, title, notifbody, url) + push_notif(user.id, title, notifbody, url) return {"message": "Message sent!"} @@ -545,9 +543,7 @@ def messagereply(v): notif = Notification(comment_id=c.id, user_id=user_id) g.db.add(notif) - if PUSHER_ID != DEFAULT_CONFIG_VALUE and not v.shadowbanned: - interests = f'{SITE}{user_id}' - + if VAPID_PUBLIC_KEY != DEFAULT_CONFIG_VALUE and not v.shadowbanned: title = f'New message from @{v.username}' if len(body) > 500: notifbody = body[:500] + '...' @@ -555,7 +551,7 @@ def messagereply(v): url = f'{SITE_FULL}/notifications/messages' - gevent.spawn(pusher_thread, interests, title, notifbody, url) + push_notif(user_id, title, notifbody, url) top_comment = c.top_comment(g.db) diff --git a/files/templates/home.html b/files/templates/home.html index 8d8abe0d1..bc02e075b 100644 --- a/files/templates/home.html +++ b/files/templates/home.html @@ -167,14 +167,15 @@ {% endif %} -{% if request.path == '/' and PUSHER_ID != DEFAULT_CONFIG_VALUE and v %} -
{{SITE}}{{v.id}}
-
{{PUSHER_ID}}
- +{% if request.path == '/' and v %} + {% endif %} diff --git a/nginx-serve-static.conf b/nginx-serve-static.conf index f211e54d7..c0da036a6 100644 --- a/nginx-serve-static.conf +++ b/nginx-serve-static.conf @@ -5,7 +5,7 @@ add_header Referrer-Policy "same-origin"; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"; add_header X-Frame-Options "deny"; add_header X-Content-Type-Options "nosniff"; -add_header Content-Security-Policy "script-src 'self' 'unsafe-inline' challenges.cloudflare.com; connect-src 'self' tls-use1.fpapi.io api.fpjs.io 00bb6d59-7b11-4339-b1ae-b1f1259d1316.pushnotifications.pusher.com; object-src 'none';"; +add_header Content-Security-Policy "script-src 'self' 'unsafe-inline' challenges.cloudflare.com; connect-src 'self' tls-use1.fpapi.io api.fpjs.io; object-src 'none';"; sendfile on; sendfile_max_chunk 1m; diff --git a/nginx.conf b/nginx.conf index d7540c261..52c6bbe79 100644 --- a/nginx.conf +++ b/nginx.conf @@ -8,7 +8,7 @@ server { add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"; add_header X-Frame-Options "deny"; add_header X-Content-Type-Options "nosniff"; - add_header Content-Security-Policy "script-src 'self' 'unsafe-inline' challenges.cloudflare.com rdrama.net; connect-src 'self' tls-use1.fpapi.io api.fpjs.io 00bb6d59-7b11-4339-b1ae-b1f1259d1316.pushnotifications.pusher.com; object-src 'none';"; + add_header Content-Security-Policy "script-src 'self' 'unsafe-inline' challenges.cloudflare.com rdrama.net; connect-src 'self' tls-use1.fpapi.io api.fpjs.io; object-src 'none';"; location / { proxy_pass http://localhost:5000/; diff --git a/requirements.txt b/requirements.txt index ad4a75c58..bf7cc8b0f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,6 +19,7 @@ matplotlib owoify-py Pillow pyotp +pywebpush qrcode redis requests @@ -27,7 +28,6 @@ tinycss2 tldextract user-agents psycopg2-binary -pusher_push_notifications youtube-dl yattag webptools diff --git a/startup_docker.sh b/startup_docker.sh index 77a1d87c5..36968e10d 100644 --- a/startup_docker.sh +++ b/startup_docker.sh @@ -1,4 +1,4 @@ -. ./env +. ./.env export DATABASE_URL="postgresql://postgres@postgres:5432" export REDIS_URL="redis://redis:6379" export PROXY_URL="http://opera-proxy:18080" diff --git a/ubuntu_setup.sh b/ubuntu_setup.sh index 1bca9f90d..73244a6ba 100644 --- a/ubuntu_setup.sh +++ b/ubuntu_setup.sh @@ -5,8 +5,8 @@ apt -y install git redis-server python3-pip ffmpeg tmux nginx snapd ufw gpg-agen git config --global credential.helper store cd /rDrama git config branch.frost.rebase true -cp ./env /env -. /env +cp ./env ./.env +. ./.env mkdir /scripts cp ./startup.sh /scripts/s