diff --git a/env b/env index 32b95bb8f..4d8b5c95f 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 deleted file mode 100644 index c7ee06147..000000000 --- a/files/assets/js/service-worker.js +++ /dev/null @@ -1,1488 +0,0 @@ -// SDK version: v1.0.2 -// Git commit: bc6831d1ab41b3b1a3de2297f7024efbb2772d8e - -'use strict'; - -function _defineProperty(obj, key, value) { - if (key in obj) { - Object.defineProperty(obj, key, { - value: value, - enumerable: true, - configurable: true, - writable: true - }); - } else { - obj[key] = value; - } - - return obj; -} - -var defineProperty = _defineProperty; - -function _objectSpread(target) { - for (var i = 1; i < arguments.length; i++) { - var source = arguments[i] != null ? arguments[i] : {}; - var ownKeys = Object.keys(source); - - if (typeof Object.getOwnPropertySymbols === 'function') { - ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { - return Object.getOwnPropertyDescriptor(source, sym).enumerable; - })); - } - - ownKeys.forEach(function (key) { - defineProperty(target, key, source[key]); - }); - } - - return target; -} - -var objectSpread = _objectSpread; - -function createCommonjsModule(fn, module) { - return module = { exports: {} }, fn(module, module.exports), module.exports; -} - -var runtime_1 = createCommonjsModule(function (module) { -/** - * Copyright (c) 2014-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -var runtime = (function (exports) { - - var Op = Object.prototype; - var hasOwn = Op.hasOwnProperty; - var undefined$1; // More compressible than void 0. - var $Symbol = typeof Symbol === "function" ? Symbol : {}; - var iteratorSymbol = $Symbol.iterator || "@@iterator"; - var asyncIteratorSymbol = $Symbol.asyncIterator || "@@asyncIterator"; - var toStringTagSymbol = $Symbol.toStringTag || "@@toStringTag"; - - function wrap(innerFn, outerFn, self, tryLocsList) { - // If outerFn provided and outerFn.prototype is a Generator, then outerFn.prototype instanceof Generator. - var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator; - var generator = Object.create(protoGenerator.prototype); - var context = new Context(tryLocsList || []); - - // The ._invoke method unifies the implementations of the .next, - // .throw, and .return methods. - generator._invoke = makeInvokeMethod(innerFn, self, context); - - return generator; - } - exports.wrap = wrap; - - // Try/catch helper to minimize deoptimizations. Returns a completion - // record like context.tryEntries[i].completion. This interface could - // have been (and was previously) designed to take a closure to be - // invoked without arguments, but in all the cases we care about we - // already have an existing method we want to call, so there's no need - // to create a new function object. We can even get away with assuming - // the method takes exactly one argument, since that happens to be true - // in every case, so we don't have to touch the arguments object. The - // only additional allocation required is the completion record, which - // has a stable shape and so hopefully should be cheap to allocate. - function tryCatch(fn, obj, arg) { - try { - return { type: "normal", arg: fn.call(obj, arg) }; - } catch (err) { - return { type: "throw", arg: err }; - } - } - - var GenStateSuspendedStart = "suspendedStart"; - var GenStateSuspendedYield = "suspendedYield"; - var GenStateExecuting = "executing"; - var GenStateCompleted = "completed"; - - // Returning this object from the innerFn has the same effect as - // breaking out of the dispatch switch statement. - var ContinueSentinel = {}; - - // Dummy constructor functions that we use as the .constructor and - // .constructor.prototype properties for functions that return Generator - // objects. For full spec compliance, you may wish to configure your - // minifier not to mangle the names of these two functions. - function Generator() {} - function GeneratorFunction() {} - function GeneratorFunctionPrototype() {} - - // This is a polyfill for %IteratorPrototype% for environments that - // don't natively support it. - var IteratorPrototype = {}; - IteratorPrototype[iteratorSymbol] = function () { - return this; - }; - - var getProto = Object.getPrototypeOf; - var NativeIteratorPrototype = getProto && getProto(getProto(values([]))); - if (NativeIteratorPrototype && - NativeIteratorPrototype !== Op && - hasOwn.call(NativeIteratorPrototype, iteratorSymbol)) { - // This environment has a native %IteratorPrototype%; use it instead - // of the polyfill. - IteratorPrototype = NativeIteratorPrototype; - } - - var Gp = GeneratorFunctionPrototype.prototype = - Generator.prototype = Object.create(IteratorPrototype); - GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype; - GeneratorFunctionPrototype.constructor = GeneratorFunction; - GeneratorFunctionPrototype[toStringTagSymbol] = - GeneratorFunction.displayName = "GeneratorFunction"; - - // Helper for defining the .next, .throw, and .return methods of the - // Iterator interface in terms of a single ._invoke method. - function defineIteratorMethods(prototype) { - ["next", "throw", "return"].forEach(function(method) { - prototype[method] = function(arg) { - return this._invoke(method, arg); - }; - }); - } - - exports.isGeneratorFunction = function(genFun) { - var ctor = typeof genFun === "function" && genFun.constructor; - return ctor - ? ctor === GeneratorFunction || - // For the native GeneratorFunction constructor, the best we can - // do is to check its .name property. - (ctor.displayName || ctor.name) === "GeneratorFunction" - : false; - }; - - exports.mark = function(genFun) { - if (Object.setPrototypeOf) { - Object.setPrototypeOf(genFun, GeneratorFunctionPrototype); - } else { - genFun.__proto__ = GeneratorFunctionPrototype; - if (!(toStringTagSymbol in genFun)) { - genFun[toStringTagSymbol] = "GeneratorFunction"; - } - } - genFun.prototype = Object.create(Gp); - return genFun; - }; - - // Within the body of any async function, `await x` is transformed to - // `yield regeneratorRuntime.awrap(x)`, so that the runtime can test - // `hasOwn.call(value, "__await")` to determine if the yielded value is - // meant to be awaited. - exports.awrap = function(arg) { - return { __await: arg }; - }; - - function AsyncIterator(generator) { - function invoke(method, arg, resolve, reject) { - var record = tryCatch(generator[method], generator, arg); - if (record.type === "throw") { - reject(record.arg); - } else { - var result = record.arg; - var value = result.value; - if (value && - typeof value === "object" && - hasOwn.call(value, "__await")) { - return Promise.resolve(value.__await).then(function(value) { - invoke("next", value, resolve, reject); - }, function(err) { - invoke("throw", err, resolve, reject); - }); - } - - return Promise.resolve(value).then(function(unwrapped) { - // When a yielded Promise is resolved, its final value becomes - // the .value of the Promise<{value,done}> result for the - // current iteration. - result.value = unwrapped; - resolve(result); - }, function(error) { - // If a rejected Promise was yielded, throw the rejection back - // into the async generator function so it can be handled there. - return invoke("throw", error, resolve, reject); - }); - } - } - - var previousPromise; - - function enqueue(method, arg) { - function callInvokeWithMethodAndArg() { - return new Promise(function(resolve, reject) { - invoke(method, arg, resolve, reject); - }); - } - - return previousPromise = - // If enqueue has been called before, then we want to wait until - // all previous Promises have been resolved before calling invoke, - // so that results are always delivered in the correct order. If - // enqueue has not been called before, then it is important to - // call invoke immediately, without waiting on a callback to fire, - // so that the async generator function has the opportunity to do - // any necessary setup in a predictable way. This predictability - // is why the Promise constructor synchronously invokes its - // executor callback, and why async functions synchronously - // execute code before the first await. Since we implement simple - // async functions in terms of async generators, it is especially - // important to get this right, even though it requires care. - previousPromise ? previousPromise.then( - callInvokeWithMethodAndArg, - // Avoid propagating failures to Promises returned by later - // invocations of the iterator. - callInvokeWithMethodAndArg - ) : callInvokeWithMethodAndArg(); - } - - // Define the unified helper method that is used to implement .next, - // .throw, and .return (see defineIteratorMethods). - this._invoke = enqueue; - } - - defineIteratorMethods(AsyncIterator.prototype); - AsyncIterator.prototype[asyncIteratorSymbol] = function () { - return this; - }; - exports.AsyncIterator = AsyncIterator; - - // Note that simple async functions are implemented on top of - // AsyncIterator objects; they just return a Promise for the value of - // the final result produced by the iterator. - exports.async = function(innerFn, outerFn, self, tryLocsList) { - var iter = new AsyncIterator( - wrap(innerFn, outerFn, self, tryLocsList) - ); - - return exports.isGeneratorFunction(outerFn) - ? iter // If outerFn is a generator, return the full iterator. - : iter.next().then(function(result) { - return result.done ? result.value : iter.next(); - }); - }; - - function makeInvokeMethod(innerFn, self, context) { - var state = GenStateSuspendedStart; - - return function invoke(method, arg) { - if (state === GenStateExecuting) { - throw new Error("Generator is already running"); - } - - if (state === GenStateCompleted) { - if (method === "throw") { - throw arg; - } - - // Be forgiving, per 25.3.3.3.3 of the spec: - // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-generatorresume - return doneResult(); - } - - context.method = method; - context.arg = arg; - - while (true) { - var delegate = context.delegate; - if (delegate) { - var delegateResult = maybeInvokeDelegate(delegate, context); - if (delegateResult) { - if (delegateResult === ContinueSentinel) continue; - return delegateResult; - } - } - - if (context.method === "next") { - // Setting context._sent for legacy support of Babel's - // function.sent implementation. - context.sent = context._sent = context.arg; - - } else if (context.method === "throw") { - if (state === GenStateSuspendedStart) { - state = GenStateCompleted; - throw context.arg; - } - - context.dispatchException(context.arg); - - } else if (context.method === "return") { - context.abrupt("return", context.arg); - } - - state = GenStateExecuting; - - var record = tryCatch(innerFn, self, context); - if (record.type === "normal") { - // If an exception is thrown from innerFn, we leave state === - // GenStateExecuting and loop back for another invocation. - state = context.done - ? GenStateCompleted - : GenStateSuspendedYield; - - if (record.arg === ContinueSentinel) { - continue; - } - - return { - value: record.arg, - done: context.done - }; - - } else if (record.type === "throw") { - state = GenStateCompleted; - // Dispatch the exception by looping back around to the - // context.dispatchException(context.arg) call above. - context.method = "throw"; - context.arg = record.arg; - } - } - }; - } - - // Call delegate.iterator[context.method](context.arg) and handle the - // result, either by returning a { value, done } result from the - // delegate iterator, or by modifying context.method and context.arg, - // setting context.delegate to null, and returning the ContinueSentinel. - function maybeInvokeDelegate(delegate, context) { - var method = delegate.iterator[context.method]; - if (method === undefined$1) { - // A .throw or .return when the delegate iterator has no .throw - // method always terminates the yield* loop. - context.delegate = null; - - if (context.method === "throw") { - // Note: ["return"] must be used for ES3 parsing compatibility. - if (delegate.iterator["return"]) { - // If the delegate iterator has a return method, give it a - // chance to clean up. - context.method = "return"; - context.arg = undefined$1; - maybeInvokeDelegate(delegate, context); - - if (context.method === "throw") { - // If maybeInvokeDelegate(context) changed context.method from - // "return" to "throw", let that override the TypeError below. - return ContinueSentinel; - } - } - - context.method = "throw"; - context.arg = new TypeError( - "The iterator does not provide a 'throw' method"); - } - - return ContinueSentinel; - } - - var record = tryCatch(method, delegate.iterator, context.arg); - - if (record.type === "throw") { - context.method = "throw"; - context.arg = record.arg; - context.delegate = null; - return ContinueSentinel; - } - - var info = record.arg; - - if (! info) { - context.method = "throw"; - context.arg = new TypeError("iterator result is not an object"); - context.delegate = null; - return ContinueSentinel; - } - - if (info.done) { - // Assign the result of the finished delegate to the temporary - // variable specified by delegate.resultName (see delegateYield). - context[delegate.resultName] = info.value; - - // Resume execution at the desired location (see delegateYield). - context.next = delegate.nextLoc; - - // If context.method was "throw" but the delegate handled the - // exception, let the outer generator proceed normally. If - // context.method was "next", forget context.arg since it has been - // "consumed" by the delegate iterator. If context.method was - // "return", allow the original .return call to continue in the - // outer generator. - if (context.method !== "return") { - context.method = "next"; - context.arg = undefined$1; - } - - } else { - // Re-yield the result returned by the delegate method. - return info; - } - - // The delegate iterator is finished, so forget it and continue with - // the outer generator. - context.delegate = null; - return ContinueSentinel; - } - - // Define Generator.prototype.{next,throw,return} in terms of the - // unified ._invoke helper method. - defineIteratorMethods(Gp); - - Gp[toStringTagSymbol] = "Generator"; - - // A Generator should always return itself as the iterator object when the - // @@iterator function is called on it. Some browsers' implementations of the - // iterator prototype chain incorrectly implement this, causing the Generator - // object to not be returned from this call. This ensures that doesn't happen. - // See https://github.com/facebook/regenerator/issues/274 for more details. - Gp[iteratorSymbol] = function() { - return this; - }; - - Gp.toString = function() { - return "[object Generator]"; - }; - - function pushTryEntry(locs) { - var entry = { tryLoc: locs[0] }; - - if (1 in locs) { - entry.catchLoc = locs[1]; - } - - if (2 in locs) { - entry.finallyLoc = locs[2]; - entry.afterLoc = locs[3]; - } - - this.tryEntries.push(entry); - } - - function resetTryEntry(entry) { - var record = entry.completion || {}; - record.type = "normal"; - delete record.arg; - entry.completion = record; - } - - function Context(tryLocsList) { - // The root entry object (effectively a try statement without a catch - // or a finally block) gives us a place to store values thrown from - // locations where there is no enclosing try statement. - this.tryEntries = [{ tryLoc: "root" }]; - tryLocsList.forEach(pushTryEntry, this); - this.reset(true); - } - - exports.keys = function(object) { - var keys = []; - for (var key in object) { - keys.push(key); - } - keys.reverse(); - - // Rather than returning an object with a next method, we keep - // things simple and return the next function itself. - return function next() { - while (keys.length) { - var key = keys.pop(); - if (key in object) { - next.value = key; - next.done = false; - return next; - } - } - - // To avoid creating an additional object, we just hang the .value - // and .done properties off the next function object itself. This - // also ensures that the minifier will not anonymize the function. - next.done = true; - return next; - }; - }; - - function values(iterable) { - if (iterable) { - var iteratorMethod = iterable[iteratorSymbol]; - if (iteratorMethod) { - return iteratorMethod.call(iterable); - } - - if (typeof iterable.next === "function") { - return iterable; - } - - if (!isNaN(iterable.length)) { - var i = -1, next = function next() { - while (++i < iterable.length) { - if (hasOwn.call(iterable, i)) { - next.value = iterable[i]; - next.done = false; - return next; - } - } - - next.value = undefined$1; - next.done = true; - - return next; - }; - - return next.next = next; - } - } - - // Return an iterator with no values. - return { next: doneResult }; - } - exports.values = values; - - function doneResult() { - return { value: undefined$1, done: true }; - } - - Context.prototype = { - constructor: Context, - - reset: function(skipTempReset) { - this.prev = 0; - this.next = 0; - // Resetting context._sent for legacy support of Babel's - // function.sent implementation. - this.sent = this._sent = undefined$1; - this.done = false; - this.delegate = null; - - this.method = "next"; - this.arg = undefined$1; - - this.tryEntries.forEach(resetTryEntry); - - if (!skipTempReset) { - for (var name in this) { - // Not sure about the optimal order of these conditions: - if (name.charAt(0) === "t" && - hasOwn.call(this, name) && - !isNaN(+name.slice(1))) { - this[name] = undefined$1; - } - } - } - }, - - stop: function() { - this.done = true; - - var rootEntry = this.tryEntries[0]; - var rootRecord = rootEntry.completion; - if (rootRecord.type === "throw") { - throw rootRecord.arg; - } - - return this.rval; - }, - - dispatchException: function(exception) { - if (this.done) { - throw exception; - } - - var context = this; - function handle(loc, caught) { - record.type = "throw"; - record.arg = exception; - context.next = loc; - - if (caught) { - // If the dispatched exception was caught by a catch block, - // then let that catch block handle the exception normally. - context.method = "next"; - context.arg = undefined$1; - } - - return !! caught; - } - - for (var i = this.tryEntries.length - 1; i >= 0; --i) { - var entry = this.tryEntries[i]; - var record = entry.completion; - - if (entry.tryLoc === "root") { - // Exception thrown outside of any try block that could handle - // it, so set the completion value of the entire function to - // throw the exception. - return handle("end"); - } - - if (entry.tryLoc <= this.prev) { - var hasCatch = hasOwn.call(entry, "catchLoc"); - var hasFinally = hasOwn.call(entry, "finallyLoc"); - - if (hasCatch && hasFinally) { - if (this.prev < entry.catchLoc) { - return handle(entry.catchLoc, true); - } else if (this.prev < entry.finallyLoc) { - return handle(entry.finallyLoc); - } - - } else if (hasCatch) { - if (this.prev < entry.catchLoc) { - return handle(entry.catchLoc, true); - } - - } else if (hasFinally) { - if (this.prev < entry.finallyLoc) { - return handle(entry.finallyLoc); - } - - } else { - throw new Error("try statement without catch or finally"); - } - } - } - }, - - abrupt: function(type, arg) { - for (var i = this.tryEntries.length - 1; i >= 0; --i) { - var entry = this.tryEntries[i]; - if (entry.tryLoc <= this.prev && - hasOwn.call(entry, "finallyLoc") && - this.prev < entry.finallyLoc) { - var finallyEntry = entry; - break; - } - } - - if (finallyEntry && - (type === "break" || - type === "continue") && - finallyEntry.tryLoc <= arg && - arg <= finallyEntry.finallyLoc) { - // Ignore the finally entry if control is not jumping to a - // location outside the try/catch block. - finallyEntry = null; - } - - var record = finallyEntry ? finallyEntry.completion : {}; - record.type = type; - record.arg = arg; - - if (finallyEntry) { - this.method = "next"; - this.next = finallyEntry.finallyLoc; - return ContinueSentinel; - } - - return this.complete(record); - }, - - complete: function(record, afterLoc) { - if (record.type === "throw") { - throw record.arg; - } - - if (record.type === "break" || - record.type === "continue") { - this.next = record.arg; - } else if (record.type === "return") { - this.rval = this.arg = record.arg; - this.method = "return"; - this.next = "end"; - } else if (record.type === "normal" && afterLoc) { - this.next = afterLoc; - } - - return ContinueSentinel; - }, - - finish: function(finallyLoc) { - for (var i = this.tryEntries.length - 1; i >= 0; --i) { - var entry = this.tryEntries[i]; - if (entry.finallyLoc === finallyLoc) { - this.complete(entry.completion, entry.afterLoc); - resetTryEntry(entry); - return ContinueSentinel; - } - } - }, - - "catch": function(tryLoc) { - for (var i = this.tryEntries.length - 1; i >= 0; --i) { - var entry = this.tryEntries[i]; - if (entry.tryLoc === tryLoc) { - var record = entry.completion; - if (record.type === "throw") { - var thrown = record.arg; - resetTryEntry(entry); - } - return thrown; - } - } - - // The context.catch method must only be called with a location - // argument that corresponds to a known catch block. - throw new Error("illegal catch attempt"); - }, - - delegateYield: function(iterable, resultName, nextLoc) { - this.delegate = { - iterator: values(iterable), - resultName: resultName, - nextLoc: nextLoc - }; - - if (this.method === "next") { - // Deliberately forget the last sent value so that we don't - // accidentally pass it on to the delegate. - this.arg = undefined$1; - } - - return ContinueSentinel; - } - }; - - // Regardless of whether this script is executing as a CommonJS module - // or not, return the runtime object so that we can declare the variable - // regeneratorRuntime in the outer scope, which allows this module to be - // injected easily by `bin/regenerator --include-runtime script.js`. - return exports; - -}( - // If this script is executing as a CommonJS module, use module.exports - // as the regeneratorRuntime namespace. Otherwise create a new empty - // object. Either way, the resulting object will be used to initialize - // the regeneratorRuntime variable at the top of this file. - module.exports -)); - -try { - globalThis.regeneratorRuntime = undefined; - regeneratorRuntime = runtime; -} catch (accidentalStrictMode) { - // This module should not be running in strict mode, so the above - // assignment should always work unless something is misconfigured. Just - // in case runtime.js accidentally runs in strict mode, we can escape - // strict mode using a global Function call. This could conceivably fail - // if a Content Security Policy forbids using Function, but in that case - // the proper solution is to fix the accidental strict mode problem. If - // you've misconfigured your bundler to force strict mode and applied a - // CSP to forbid Function, and you're not willing to fix either of those - // problems, please detail your unique predicament in a GitHub issue. - Function("r", "regeneratorRuntime = r")(runtime); -} -}); - -var regenerator = runtime_1; - -function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { - try { - var info = gen[key](arg); - var value = info.value; - } catch (error) { - reject(error); - return; - } - - if (info.done) { - resolve(value); - } else { - Promise.resolve(value).then(_next, _throw); - } -} - -function _asyncToGenerator(fn) { - return function () { - var self = this, - args = arguments; - return new Promise(function (resolve, reject) { - var gen = fn.apply(self, args); - - function _next(value) { - asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); - } - - function _throw(err) { - asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); - } - - _next(undefined); - }); - }; -} - -var asyncToGenerator = _asyncToGenerator; - -function doRequest(_ref) { - var method = _ref.method, - path = _ref.path, - _ref$body = _ref.body, - body = _ref$body === void 0 ? null : _ref$body, - _ref$headers = _ref.headers, - headers = _ref$headers === void 0 ? {} : _ref$headers; - var options = { - method: method, - headers: headers - }; - - if (body !== null) { - options.body = JSON.stringify(body); - options.headers = objectSpread({ - 'Content-Type': 'application/json' - }, headers); - } - - return fetch(path, options).then( - /*#__PURE__*/ - function () { - var _ref2 = asyncToGenerator( - /*#__PURE__*/ - regenerator.mark(function _callee(response) { - return regenerator.wrap(function _callee$(_context) { - while (1) { - switch (_context.prev = _context.next) { - case 0: - if (response.ok) { - _context.next = 3; - break; - } - - _context.next = 3; - return handleError(response); - - case 3: - _context.prev = 3; - _context.next = 6; - return response.json(); - - case 6: - return _context.abrupt("return", _context.sent); - - case 9: - _context.prev = 9; - _context.t0 = _context["catch"](3); - return _context.abrupt("return", null); - - case 12: - case "end": - return _context.stop(); - } - } - }, _callee, null, [[3, 9]]); - })); - - return function (_x) { - return _ref2.apply(this, arguments); - }; - }()); -} - -function handleError(_x2) { - return _handleError.apply(this, arguments); -} - -function _handleError() { - _handleError = asyncToGenerator( - /*#__PURE__*/ - regenerator.mark(function _callee2(response) { - var errorMessage, _ref3, _ref3$error, error, _ref3$description, description; - - return regenerator.wrap(function _callee2$(_context2) { - while (1) { - switch (_context2.prev = _context2.next) { - case 0: - _context2.prev = 0; - _context2.next = 3; - return response.json(); - - case 3: - _ref3 = _context2.sent; - _ref3$error = _ref3.error; - error = _ref3$error === void 0 ? 'Unknown error' : _ref3$error; - _ref3$description = _ref3.description; - description = _ref3$description === void 0 ? 'No description' : _ref3$description; - errorMessage = "Unexpected status code ".concat(response.status, ": ").concat(error, ", ").concat(description); - _context2.next = 14; - break; - - case 11: - _context2.prev = 11; - _context2.t0 = _context2["catch"](0); - errorMessage = "Unexpected status code ".concat(response.status, ": Cannot parse error response"); - - case 14: - throw new Error(errorMessage); - - case 15: - case "end": - return _context2.stop(); - } - } - }, _callee2, null, [[0, 11]]); - })); - return _handleError.apply(this, arguments); -} - -function _classCallCheck(instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError("Cannot call a class as a function"); - } -} - -var classCallCheck = _classCallCheck; - -function _defineProperties(target, props) { - for (var i = 0; i < props.length; i++) { - var descriptor = props[i]; - descriptor.enumerable = descriptor.enumerable || false; - descriptor.configurable = true; - if ("value" in descriptor) descriptor.writable = true; - Object.defineProperty(target, descriptor.key, descriptor); - } -} - -function _createClass(Constructor, protoProps, staticProps) { - if (protoProps) _defineProperties(Constructor.prototype, protoProps); - if (staticProps) _defineProperties(Constructor, staticProps); - return Constructor; -} - -var createClass = _createClass; - -var DeviceStateStore = -/*#__PURE__*/ -function () { - function DeviceStateStore(instanceId) { - classCallCheck(this, DeviceStateStore); - - this._instanceId = instanceId; - this._dbConn = null; - } - - createClass(DeviceStateStore, [{ - key: "connect", - value: function connect() { - var _this = this; - - return new Promise(function (resolve, reject) { - var request = indexedDB.open(_this._dbName); - - request.onsuccess = function (event) { - var db = event.target.result; - _this._dbConn = db; - - _this._readState().then(function (state) { - return state === null ? _this.clear() : Promise.resolve(); - }).then(resolve); - }; - - request.onupgradeneeded = function (event) { - var db = event.target.result; - db.createObjectStore('beams', { - keyPath: 'instance_id' - }); - }; - - request.onerror = function (event) { - var error = new Error("Database error: ".concat(event.target.error)); - reject(error); - }; - }); - } - }, { - key: "clear", - value: function clear() { - return this._writeState({ - instance_id: this._instanceId, - device_id: null, - token: null, - user_id: null - }); - } - }, { - key: "_readState", - value: function _readState() { - var _this2 = this; - - if (!this.isConnected) { - throw new Error('Cannot read value: DeviceStateStore not connected to IndexedDB'); - } - - return new Promise(function (resolve, reject) { - var request = _this2._dbConn.transaction('beams').objectStore('beams').get(_this2._instanceId); - - request.onsuccess = function (event) { - var state = event.target.result; - - if (!state) { - resolve(null); - } - - resolve(state); - }; - - request.onerror = function (event) { - reject(event.target.error); - }; - }); - } - }, { - key: "_readProperty", - value: function () { - var _readProperty2 = asyncToGenerator( - /*#__PURE__*/ - regenerator.mark(function _callee(name) { - var state; - return regenerator.wrap(function _callee$(_context) { - while (1) { - switch (_context.prev = _context.next) { - case 0: - _context.next = 2; - return this._readState(); - - case 2: - state = _context.sent; - - if (!(state === null)) { - _context.next = 5; - break; - } - - return _context.abrupt("return", null); - - case 5: - return _context.abrupt("return", state[name] || null); - - case 6: - case "end": - return _context.stop(); - } - } - }, _callee, this); - })); - - function _readProperty(_x) { - return _readProperty2.apply(this, arguments); - } - - return _readProperty; - }() - }, { - key: "_writeState", - value: function _writeState(state) { - var _this3 = this; - - if (!this.isConnected) { - throw new Error('Cannot write value: DeviceStateStore not connected to IndexedDB'); - } - - return new Promise(function (resolve, reject) { - var request = _this3._dbConn.transaction('beams', 'readwrite').objectStore('beams').put(state); - - request.onsuccess = function (_) { - resolve(); - }; - - request.onerror = function (event) { - reject(event.target.error); - }; - }); - } - }, { - key: "_writeProperty", - value: function () { - var _writeProperty2 = asyncToGenerator( - /*#__PURE__*/ - regenerator.mark(function _callee2(name, value) { - var state; - return regenerator.wrap(function _callee2$(_context2) { - while (1) { - switch (_context2.prev = _context2.next) { - case 0: - _context2.next = 2; - return this._readState(); - - case 2: - state = _context2.sent; - state[name] = value; - _context2.next = 6; - return this._writeState(state); - - case 6: - case "end": - return _context2.stop(); - } - } - }, _callee2, this); - })); - - function _writeProperty(_x2, _x3) { - return _writeProperty2.apply(this, arguments); - } - - return _writeProperty; - }() - }, { - key: "getToken", - value: function getToken() { - return this._readProperty('token'); - } - }, { - key: "setToken", - value: function setToken(token) { - return this._writeProperty('token', token); - } - }, { - key: "getDeviceId", - value: function getDeviceId() { - return this._readProperty('device_id'); - } - }, { - key: "setDeviceId", - value: function setDeviceId(deviceId) { - return this._writeProperty('device_id', deviceId); - } - }, { - key: "getUserId", - value: function getUserId() { - return this._readProperty('user_id'); - } - }, { - key: "setUserId", - value: function setUserId(userId) { - return this._writeProperty('user_id', userId); - } - }, { - key: "getLastSeenSdkVersion", - value: function getLastSeenSdkVersion() { - return this._readProperty('last_seen_sdk_version'); - } - }, { - key: "setLastSeenSdkVersion", - value: function setLastSeenSdkVersion(sdkVersion) { - return this._writeProperty('last_seen_sdk_version', sdkVersion); - } - }, { - key: "getLastSeenUserAgent", - value: function getLastSeenUserAgent() { - return this._readProperty('last_seen_user_agent'); - } - }, { - key: "setLastSeenUserAgent", - value: function setLastSeenUserAgent(userAgent) { - return this._writeProperty('last_seen_user_agent', userAgent); - } - }, { - key: "_dbName", - get: function get() { - return "beams-".concat(this._instanceId); - } - }, { - key: "isConnected", - get: function get() { - return this._dbConn !== null; - } - }]); - - return DeviceStateStore; -}(); - -self.PusherPushNotifications = { - endpointOverride: null, - onNotificationReceived: null, - _endpoint: function _endpoint(instanceId) { - return self.PusherPushNotifications.endpointOverride ? self.PusherPushNotifications.endpointOverride : "https://".concat(instanceId, ".pushnotifications.pusher.com"); - }, - _getVisibleClient: function _getVisibleClient() { - return self.clients.matchAll({ - type: 'window', - includeUncontrolled: true - }).then(function (clients) { - return clients.find(function (c) { - return c.visibilityState === 'visible'; - }); - }); - }, - _hasVisibleClient: function _hasVisibleClient() { - return self.PusherPushNotifications._getVisibleClient().then(function (client) { - return client !== undefined; - }); - }, - _getFocusedClient: function _getFocusedClient() { - return self.clients.matchAll({ - type: 'window', - includeUncontrolled: true - }).then(function (clients) { - return clients.find(function (c) { - return c.focused === true; - }); - }); - }, - _hasFocusedClient: function _hasFocusedClient() { - return self.PusherPushNotifications._getFocusedClient().then(function (client) { - return client !== undefined; - }); - }, - reportEvent: function () { - var _reportEvent = asyncToGenerator( - /*#__PURE__*/ - regenerator.mark(function _callee(_ref) { - var eventType, pusherMetadata, instanceId, publishId, hasDisplayableContent, hasData, deviceStateStore, deviceId, userId, appInBackground, path, options; - return regenerator.wrap(function _callee$(_context) { - while (1) { - switch (_context.prev = _context.next) { - case 0: - eventType = _ref.eventType, pusherMetadata = _ref.pusherMetadata; - instanceId = pusherMetadata.instanceId, publishId = pusherMetadata.publishId, hasDisplayableContent = pusherMetadata.hasDisplayableContent, hasData = pusherMetadata.hasData; - - if (!(!instanceId || !publishId)) { - _context.next = 4; - break; - } - - return _context.abrupt("return"); - - case 4: - deviceStateStore = new DeviceStateStore(instanceId); - _context.next = 7; - return deviceStateStore.connect(); - - case 7: - _context.next = 9; - return deviceStateStore.getDeviceId(); - - case 9: - deviceId = _context.sent; - _context.next = 12; - return deviceStateStore.getUserId(); - - case 12: - _context.t0 = _context.sent; - - if (_context.t0) { - _context.next = 15; - break; - } - - _context.t0 = null; - - case 15: - userId = _context.t0; - _context.next = 18; - return self.PusherPushNotifications._hasVisibleClient(); - - case 18: - appInBackground = !_context.sent; - path = "".concat(self.PusherPushNotifications._endpoint(instanceId), "/reporting_api/v2/instances/").concat(instanceId, "/events"); - options = { - method: 'POST', - path: path, - body: { - publishId: publishId, - event: eventType, - deviceId: deviceId, - userId: userId, - timestampSecs: Math.floor(Date.now() / 1000), - appInBackground: appInBackground, - hasDisplayableContent: hasDisplayableContent, - hasData: hasData - } - }; - _context.prev = 21; - _context.next = 24; - return doRequest(options); - - case 24: - _context.next = 28; - break; - - case 26: - _context.prev = 26; - _context.t1 = _context["catch"](21); - - case 28: - case "end": - return _context.stop(); - } - } - }, _callee, null, [[21, 26]]); - })); - - function reportEvent(_x) { - return _reportEvent.apply(this, arguments); - } - - return reportEvent; - }() -}; -self.addEventListener('push', function (e) { - var payload; - - try { - payload = e.data.json(); - } catch (_) { - return; // Not a pusher notification - } - - if (!payload.data || !payload.data.pusher) { - return; // Not a pusher notification - } // Report analytics event, best effort - - - self.PusherPushNotifications.reportEvent({ - eventType: 'delivery', - pusherMetadata: payload.data.pusher - }); - - var customerPayload = objectSpread({}, payload); - - var customerData = {}; - Object.keys(customerPayload.data || {}).forEach(function (key) { - if (key !== 'pusher') { - customerData[key] = customerPayload.data[key]; - } - }); - customerPayload.data = customerData; - var pusherMetadata = payload.data.pusher; - - var handleNotification = - /*#__PURE__*/ - function () { - var _ref2 = asyncToGenerator( - /*#__PURE__*/ - regenerator.mark(function _callee2(payloadFromCallback) { - var hideNotificationIfSiteHasFocus, title, body, icon, options; - return regenerator.wrap(function _callee2$(_context2) { - while (1) { - switch (_context2.prev = _context2.next) { - case 0: - hideNotificationIfSiteHasFocus = payloadFromCallback.notification.hide_notification_if_site_has_focus === true; - _context2.t0 = hideNotificationIfSiteHasFocus; - - if (!_context2.t0) { - _context2.next = 6; - break; - } - - _context2.next = 5; - return self.PusherPushNotifications._hasFocusedClient(); - - case 5: - _context2.t0 = _context2.sent; - - case 6: - if (!_context2.t0) { - _context2.next = 8; - break; - } - - return _context2.abrupt("return"); - - case 8: - title = payloadFromCallback.notification.title || ''; - body = payloadFromCallback.notification.body || ''; - icon = payloadFromCallback.notification.icon; - options = { - body: body, - icon: icon, - data: { - pusher: { - customerPayload: payloadFromCallback, - pusherMetadata: pusherMetadata - } - } - }; - return _context2.abrupt("return", self.registration.showNotification(title, options)); - - case 13: - case "end": - return _context2.stop(); - } - } - }, _callee2); - })); - - return function handleNotification(_x2) { - return _ref2.apply(this, arguments); - }; - }(); - - if (self.PusherPushNotifications.onNotificationReceived) { - self.PusherPushNotifications.onNotificationReceived({ - payload: customerPayload, - pushEvent: e, - handleNotification: handleNotification - }); - } else { - e.waitUntil(handleNotification(customerPayload)); - } -}); -self.addEventListener('notificationclick', function (e) { - var pusher = e.notification.data.pusher; - var isPusherNotification = pusher !== undefined; - - if (isPusherNotification) { - // Report analytics event, best effort - self.PusherPushNotifications.reportEvent({ - eventType: 'open', - pusherMetadata: pusher.pusherMetadata - }); - - if (pusher.customerPayload.notification.deep_link) { - e.waitUntil(clients.openWindow(pusher.customerPayload.notification.deep_link)); - } - - e.notification.close(); - } -}); - -// offline static page handler -// @crgd - -const CACHE_NAME = "offlineCache-v1"; -const OFFLINE_URL = "/assets/offline.html"; - -self.addEventListener("install", (event) => { - const cacheOfflinePage = async () => { - const cache = await caches.open(CACHE_NAME); - await cache.add(new Request(OFFLINE_URL, {cache: "reload"})); - }; - - cacheOfflinePage().then(() => { - this.skipWaiting(); - }); -}); - -self.addEventListener("activate", (event) => { - const expectedCaches = [CACHE_NAME]; - - event.waitUntil( - caches.keys().then(keys => Promise.all( - keys.map(key => { - if (!expectedCaches.includes(key)) { - return caches.delete(key); - } - }) - )) - ); -}); - -self.addEventListener("fetch", (event) => { - if (event.request.mode === "navigate") { - event.respondWith((async () => { - try { - const preloadResponse = await event.preloadResponse; - if (preloadResponse) return preloadResponse; - - 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; - } - })()); - } -}); diff --git a/files/assets/js/service_worker.js b/files/assets/js/service_worker.js new file mode 100644 index 000000000..4d6f9e29b --- /dev/null +++ b/files/assets/js/service_worker.js @@ -0,0 +1,76 @@ +'use strict'; + +const CACHE_NAME = "offlineCache-v1"; +const OFFLINE_URL = "/assets/offline.html"; + +self.addEventListener("install", () => { + const cacheOfflinePage = async () => { + const cache = await caches.open(CACHE_NAME); + await cache.add(new Request(OFFLINE_URL, {cache: "reload"})); + }; + + cacheOfflinePage().then(() => { + this.skipWaiting(); + }); +}); + +self.addEventListener("activate", (event) => { + const expectedCaches = [CACHE_NAME]; + + event.waitUntil( + caches.keys().then(keys => Promise.all( + keys.map(key => { + if (!expectedCaches.includes(key)) { + return caches.delete(key); + } + }) + )) + ); +}); + +self.addEventListener("fetch", (event) => { + if (event.request.mode === "navigate") { + event.respondWith((async () => { + try { + const preloadResponse = await event.preloadResponse; + if (preloadResponse) return preloadResponse; + + const networkResponse = await fetch(event.request); + return networkResponse; + } catch (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 ae2a00038..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=5&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=5");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..a25279227 100644 --- a/files/helpers/alerts.py +++ b/files/helpers/alerts.py @@ -1,9 +1,10 @@ +import json from sys import stdout from flask import g -from pusher_push_notifications import PushNotifications +from pywebpush import webpush -from files.classes import Comment, Notification +from files.classes import Comment, Notification, PushSubscription from .const import * from .regex import * @@ -103,35 +104,26 @@ 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) - 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] + "..." +def push_notif(uid, title, body, url): + if VAPID_PUBLIC_KEY == DEFAULT_CONFIG_VALUE: + return - 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() + if len(body) > PUSH_NOTIF_LIMIT: + body = body[:PUSH_NOTIF_LIMIT] + "..." + + 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={"sub": f"mailto:{EMAIL}"} + ) + except: continue diff --git a/files/helpers/const.py b/files/helpers/const.py index 96ece78bc..eee7a4047 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 33ba2ac06..a0acb0957 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 14342315c..9006c6e8c 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