Make manual eslint fixes
These are safe fixes, more complicated fixes can be done separately (just disabled those errors for now). - Reorder declarations to fix `no-use-before-define` - Rename parameters for `no-shadow` - Remove unused parameters, functions, imports - Switch from `++` and `—` to `+= 1` and `-= 1` for `no-unary` - Use object spreading instead of parameter reassignment in auth utils - Use `window.location` instead of `location` global - Use inline JSX strings instead of unescaped values -
This commit is contained in:
parent
8319f1cf26
commit
59011c8a32
20 changed files with 369 additions and 351 deletions
|
@ -261,12 +261,12 @@ class AccountApi {
|
|||
|
||||
async createBillingSubscription(tier, interval) {
|
||||
console.log(`[AccountApi] Creating billing subscription with ${tier} and interval ${interval}`);
|
||||
return await this.upsertBillingSubscription("POST", tier, interval);
|
||||
return this.upsertBillingSubscription("POST", tier, interval);
|
||||
}
|
||||
|
||||
async updateBillingSubscription(tier, interval) {
|
||||
console.log(`[AccountApi] Updating billing subscription with ${tier} and interval ${interval}`);
|
||||
return await this.upsertBillingSubscription("PUT", tier, interval);
|
||||
return this.upsertBillingSubscription("PUT", tier, interval);
|
||||
}
|
||||
|
||||
async upsertBillingSubscription(method, tier, interval) {
|
||||
|
@ -279,7 +279,7 @@ class AccountApi {
|
|||
interval,
|
||||
}),
|
||||
});
|
||||
return await response.json(); // May throw SyntaxError
|
||||
return response.json(); // May throw SyntaxError
|
||||
}
|
||||
|
||||
async deleteBillingSubscription() {
|
||||
|
@ -298,7 +298,7 @@ class AccountApi {
|
|||
method: "POST",
|
||||
headers: withBearerAuth({}, session.token()),
|
||||
});
|
||||
return await response.json(); // May throw SyntaxError
|
||||
return response.json(); // May throw SyntaxError
|
||||
}
|
||||
|
||||
async verifyPhoneNumber(phoneNumber, channel) {
|
||||
|
@ -327,7 +327,7 @@ class AccountApi {
|
|||
});
|
||||
}
|
||||
|
||||
async deletePhoneNumber(phoneNumber, code) {
|
||||
async deletePhoneNumber(phoneNumber) {
|
||||
const url = accountPhoneUrl(config.base_url);
|
||||
console.log(`[AccountApi] Deleting phone number ${url}`);
|
||||
await fetchOrThrow(url, {
|
||||
|
@ -369,6 +369,7 @@ class AccountApi {
|
|||
if (e instanceof UnauthorizedError) {
|
||||
session.resetAndRedirect(routes.login);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
/* eslint-disable max-classes-per-file */
|
||||
import { basicAuth, bearerAuth, encodeBase64Url, topicShortUrl, topicUrlWs } from "./utils";
|
||||
|
||||
const retryBackoffSeconds = [5, 10, 20, 30, 60, 120];
|
||||
|
||||
export class ConnectionState {
|
||||
static Connected = "connected";
|
||||
|
||||
static Connecting = "connecting";
|
||||
}
|
||||
|
||||
/**
|
||||
* A connection contains a single WebSocket connection for one topic. It handles its connection
|
||||
* status itself, including reconnect attempts and backoff.
|
||||
|
@ -63,7 +70,7 @@ class Connection {
|
|||
this.ws = null;
|
||||
} else {
|
||||
const retrySeconds = retryBackoffSeconds[Math.min(this.retryCount, retryBackoffSeconds.length - 1)];
|
||||
this.retryCount++;
|
||||
this.retryCount += 1;
|
||||
console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection died, retrying in ${retrySeconds} seconds`);
|
||||
this.retryTimeout = setTimeout(() => this.start(), retrySeconds * 1000);
|
||||
this.onStateChanged(this.subscriptionId, ConnectionState.Connecting);
|
||||
|
@ -108,10 +115,4 @@ class Connection {
|
|||
}
|
||||
}
|
||||
|
||||
export class ConnectionState {
|
||||
static Connected = "connected";
|
||||
|
||||
static Connecting = "connecting";
|
||||
}
|
||||
|
||||
export default Connection;
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import Connection from "./Connection";
|
||||
import { hashCode } from "./utils";
|
||||
|
||||
const makeConnectionId = async (subscription, user) =>
|
||||
user ? hashCode(`${subscription.id}|${user.username}|${user.password ?? ""}|${user.token ?? ""}`) : hashCode(`${subscription.id}`);
|
||||
|
||||
/**
|
||||
* The connection manager keeps track of active connections (WebSocket connections, see Connection).
|
||||
*
|
||||
|
@ -69,8 +72,8 @@ class ConnectionManager {
|
|||
topic,
|
||||
user,
|
||||
since,
|
||||
(subscriptionId, notification) => this.notificationReceived(subscriptionId, notification),
|
||||
(subscriptionId, state) => this.stateChanged(subscriptionId, state)
|
||||
(subId, notification) => this.notificationReceived(subId, notification),
|
||||
(subId, state) => this.stateChanged(subId, state)
|
||||
);
|
||||
this.connections.set(connectionId, connection);
|
||||
console.log(
|
||||
|
@ -112,8 +115,5 @@ class ConnectionManager {
|
|||
}
|
||||
}
|
||||
|
||||
const makeConnectionId = async (subscription, user) =>
|
||||
user ? hashCode(`${subscription.id}|${user.username}|${user.password ?? ""}|${user.token ?? ""}`) : hashCode(`${subscription.id}`);
|
||||
|
||||
const connectionManager = new ConnectionManager();
|
||||
export default connectionManager;
|
||||
|
|
|
@ -29,7 +29,7 @@ class Notifier {
|
|||
icon: logo,
|
||||
});
|
||||
if (notification.click) {
|
||||
n.onclick = (e) => openUrl(notification.click);
|
||||
n.onclick = () => openUrl(notification.click);
|
||||
} else {
|
||||
n.onclick = () => onClickFallback(subscription);
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ class Notifier {
|
|||
* is not supported, see https://developer.mozilla.org/en-US/docs/Web/API/notification
|
||||
*/
|
||||
contextSupported() {
|
||||
return location.protocol === "https:" || location.hostname.match("^127.") || location.hostname === "localhost";
|
||||
return window.location.protocol === "https:" || window.location.hostname.match("^127.") || window.location.hostname === "localhost";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,8 @@ class Poller {
|
|||
const subscriptions = await subscriptionManager.all();
|
||||
for (const s of subscriptions) {
|
||||
try {
|
||||
// TODO(eslint): Switch to Promise.all
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await this.poll(s);
|
||||
} catch (e) {
|
||||
console.log(`[Poller] Error polling ${s.id}`, e);
|
||||
|
|
|
@ -7,6 +7,7 @@ class SubscriptionManager {
|
|||
const subscriptions = await db.subscriptions.toArray();
|
||||
await Promise.all(
|
||||
subscriptions.map(async (s) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
s.new = await db.notifications.where({ subscriptionId: s.id, new: 1 }).count();
|
||||
})
|
||||
);
|
||||
|
@ -14,7 +15,7 @@ class SubscriptionManager {
|
|||
}
|
||||
|
||||
async get(subscriptionId) {
|
||||
return await db.subscriptions.get(subscriptionId);
|
||||
return db.subscriptions.get(subscriptionId);
|
||||
}
|
||||
|
||||
async add(baseUrl, topic, internal) {
|
||||
|
@ -40,10 +41,14 @@ class SubscriptionManager {
|
|||
|
||||
// Add remote subscriptions
|
||||
const remoteIds = []; // = topicUrl(baseUrl, topic)
|
||||
for (let i = 0; i < remoteSubscriptions.length; i++) {
|
||||
for (let i = 0; i < remoteSubscriptions.length; i += 1) {
|
||||
const remote = remoteSubscriptions[i];
|
||||
// TODO(eslint): Switch to Promise.all
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const local = await this.add(remote.base_url, remote.topic, false);
|
||||
const reservation = remoteReservations?.find((r) => remote.base_url === config.base_url && remote.topic === r.topic) || null;
|
||||
// TODO(eslint): Switch to Promise.all
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await this.update(local.id, {
|
||||
displayName: remote.display_name, // May be undefined
|
||||
reservation, // May be null!
|
||||
|
@ -53,10 +58,12 @@ class SubscriptionManager {
|
|||
|
||||
// Remove local subscriptions that do not exist remotely
|
||||
const localSubscriptions = await db.subscriptions.toArray();
|
||||
for (let i = 0; i < localSubscriptions.length; i++) {
|
||||
for (let i = 0; i < localSubscriptions.length; i += 1) {
|
||||
const local = localSubscriptions[i];
|
||||
const remoteExists = remoteIds.includes(local.id);
|
||||
if (!local.internal && !remoteExists) {
|
||||
// TODO(eslint): Switch to Promise.all
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await this.remove(local.id);
|
||||
}
|
||||
}
|
||||
|
@ -101,6 +108,7 @@ class SubscriptionManager {
|
|||
return false;
|
||||
}
|
||||
try {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
notification.new = 1; // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation
|
||||
await db.notifications.add({ ...notification, subscriptionId }); // FIXME consider put() for double tab
|
||||
await db.subscriptions.update(subscriptionId, {
|
||||
|
|
|
@ -1,37 +1,6 @@
|
|||
/* eslint-disable max-classes-per-file */
|
||||
// This is a subset of, and the counterpart to errors.go
|
||||
|
||||
export const fetchOrThrow = async (url, options) => {
|
||||
const response = await fetch(url, options);
|
||||
if (response.status !== 200) {
|
||||
await throwAppError(response);
|
||||
}
|
||||
return response; // Promise!
|
||||
};
|
||||
|
||||
export const throwAppError = async (response) => {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
console.log(`[Error] HTTP ${response.status}`, response);
|
||||
throw new UnauthorizedError();
|
||||
}
|
||||
const error = await maybeToJson(response);
|
||||
if (error?.code) {
|
||||
console.log(`[Error] HTTP ${response.status}, ntfy error ${error.code}: ${error.error || ""}`, response);
|
||||
if (error.code === UserExistsError.CODE) {
|
||||
throw new UserExistsError();
|
||||
} else if (error.code === TopicReservedError.CODE) {
|
||||
throw new TopicReservedError();
|
||||
} else if (error.code === AccountCreateLimitReachedError.CODE) {
|
||||
throw new AccountCreateLimitReachedError();
|
||||
} else if (error.code === IncorrectPasswordError.CODE) {
|
||||
throw new IncorrectPasswordError();
|
||||
} else if (error?.error) {
|
||||
throw new Error(`Error ${error.code}: ${error.error}`);
|
||||
}
|
||||
}
|
||||
console.log(`[Error] HTTP ${response.status}, not a ntfy error`, response);
|
||||
throw new Error(`Unexpected response ${response.status}`);
|
||||
};
|
||||
|
||||
const maybeToJson = async (response) => {
|
||||
try {
|
||||
return await response.json();
|
||||
|
@ -77,3 +46,35 @@ export class IncorrectPasswordError extends Error {
|
|||
super("Password incorrect");
|
||||
}
|
||||
}
|
||||
|
||||
export const throwAppError = async (response) => {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
console.log(`[Error] HTTP ${response.status}`, response);
|
||||
throw new UnauthorizedError();
|
||||
}
|
||||
const error = await maybeToJson(response);
|
||||
if (error?.code) {
|
||||
console.log(`[Error] HTTP ${response.status}, ntfy error ${error.code}: ${error.error || ""}`, response);
|
||||
if (error.code === UserExistsError.CODE) {
|
||||
throw new UserExistsError();
|
||||
} else if (error.code === TopicReservedError.CODE) {
|
||||
throw new TopicReservedError();
|
||||
} else if (error.code === AccountCreateLimitReachedError.CODE) {
|
||||
throw new AccountCreateLimitReachedError();
|
||||
} else if (error.code === IncorrectPasswordError.CODE) {
|
||||
throw new IncorrectPasswordError();
|
||||
} else if (error?.error) {
|
||||
throw new Error(`Error ${error.code}: ${error.error}`);
|
||||
}
|
||||
}
|
||||
console.log(`[Error] HTTP ${response.status}, not a ntfy error`, response);
|
||||
throw new Error(`Unexpected response ${response.status}`);
|
||||
};
|
||||
|
||||
export const fetchOrThrow = async (url, options) => {
|
||||
const response = await fetch(url, options);
|
||||
if (response.status !== 200) {
|
||||
await throwAppError(response);
|
||||
}
|
||||
return response; // Promise!
|
||||
};
|
||||
|
|
|
@ -9,6 +9,10 @@ import pop from "../sounds/pop.mp3";
|
|||
import popSwoosh from "../sounds/pop-swoosh.mp3";
|
||||
import config from "./config";
|
||||
|
||||
export const tiersUrl = (baseUrl) => `${baseUrl}/v1/tiers`;
|
||||
export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
|
||||
export const expandUrl = (url) => [`https://${url}`, `http://${url}`];
|
||||
export const expandSecureUrl = (url) => `https://${url}`;
|
||||
export const topicUrl = (baseUrl, topic) => `${baseUrl}/${topic}`;
|
||||
export const topicUrlWs = (baseUrl, topic) =>
|
||||
`${topicUrl(baseUrl, topic)}/ws`.replaceAll("https://", "wss://").replaceAll("http://", "ws://");
|
||||
|
@ -28,13 +32,11 @@ export const accountBillingSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account
|
|||
export const accountBillingPortalUrl = (baseUrl) => `${baseUrl}/v1/account/billing/portal`;
|
||||
export const accountPhoneUrl = (baseUrl) => `${baseUrl}/v1/account/phone`;
|
||||
export const accountPhoneVerifyUrl = (baseUrl) => `${baseUrl}/v1/account/phone/verify`;
|
||||
export const tiersUrl = (baseUrl) => `${baseUrl}/v1/tiers`;
|
||||
export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
|
||||
export const expandUrl = (url) => [`https://${url}`, `http://${url}`];
|
||||
export const expandSecureUrl = (url) => `https://${url}`;
|
||||
|
||||
export const validUrl = (url) => url.match(/^https?:\/\/.+/);
|
||||
|
||||
export const disallowedTopic = (topic) => config.disallowed_topics.includes(topic);
|
||||
|
||||
export const validTopic = (topic) => {
|
||||
if (disallowedTopic(topic)) {
|
||||
return false;
|
||||
|
@ -42,8 +44,6 @@ export const validTopic = (topic) => {
|
|||
return topic.match(/^([-_a-zA-Z0-9]{1,64})$/); // Regex must match Go & Android app!
|
||||
};
|
||||
|
||||
export const disallowedTopic = (topic) => config.disallowed_topics.includes(topic);
|
||||
|
||||
export const topicDisplayName = (subscription) => {
|
||||
if (subscription.displayName) {
|
||||
return subscription.displayName;
|
||||
|
@ -67,13 +67,6 @@ const toEmojis = (tags) => {
|
|||
return tags.filter((tag) => tag in emojis).map((tag) => emojis[tag]);
|
||||
};
|
||||
|
||||
export const formatTitleWithDefault = (m, fallback) => {
|
||||
if (m.title) {
|
||||
return formatTitle(m);
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
export const formatTitle = (m) => {
|
||||
const emojiList = toEmojis(m.tags);
|
||||
if (emojiList.length > 0) {
|
||||
|
@ -82,6 +75,13 @@ export const formatTitle = (m) => {
|
|||
return m.title;
|
||||
};
|
||||
|
||||
export const formatTitleWithDefault = (m, fallback) => {
|
||||
if (m.title) {
|
||||
return formatTitle(m);
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
export const formatMessage = (m) => {
|
||||
if (m.title) {
|
||||
return m.message;
|
||||
|
@ -98,6 +98,25 @@ export const unmatchedTags = (tags) => {
|
|||
return tags.filter((tag) => !(tag in emojis));
|
||||
};
|
||||
|
||||
export const encodeBase64 = (s) => Base64.encode(s);
|
||||
|
||||
export const encodeBase64Url = (s) => Base64.encodeURI(s);
|
||||
|
||||
export const bearerAuth = (token) => `Bearer ${token}`;
|
||||
|
||||
export const basicAuth = (username, password) => `Basic ${encodeBase64(`${username}:${password}`)}`;
|
||||
|
||||
export const withBearerAuth = (headers, token) => ({ ...headers, Authorization: bearerAuth(token) });
|
||||
|
||||
export const maybeWithBearerAuth = (headers, token) => {
|
||||
if (token) {
|
||||
return withBearerAuth(headers, token);
|
||||
}
|
||||
return headers;
|
||||
};
|
||||
|
||||
export const withBasicAuth = (headers, username, password) => ({ ...headers, Authorization: basicAuth(username, password) });
|
||||
|
||||
export const maybeWithAuth = (headers, user) => {
|
||||
if (user && user.password) {
|
||||
return withBasicAuth(headers, user.username, user.password);
|
||||
|
@ -108,31 +127,6 @@ export const maybeWithAuth = (headers, user) => {
|
|||
return headers;
|
||||
};
|
||||
|
||||
export const maybeWithBearerAuth = (headers, token) => {
|
||||
if (token) {
|
||||
return withBearerAuth(headers, token);
|
||||
}
|
||||
return headers;
|
||||
};
|
||||
|
||||
export const withBasicAuth = (headers, username, password) => {
|
||||
headers.Authorization = basicAuth(username, password);
|
||||
return headers;
|
||||
};
|
||||
|
||||
export const basicAuth = (username, password) => `Basic ${encodeBase64(`${username}:${password}`)}`;
|
||||
|
||||
export const withBearerAuth = (headers, token) => {
|
||||
headers.Authorization = bearerAuth(token);
|
||||
return headers;
|
||||
};
|
||||
|
||||
export const bearerAuth = (token) => `Bearer ${token}`;
|
||||
|
||||
export const encodeBase64 = (s) => Base64.encode(s);
|
||||
|
||||
export const encodeBase64Url = (s) => Base64.encodeURI(s);
|
||||
|
||||
export const maybeAppendActionErrors = (message, notification) => {
|
||||
const actionErrors = (notification.actions ?? [])
|
||||
.map((action) => action.error)
|
||||
|
@ -147,10 +141,12 @@ export const maybeAppendActionErrors = (message, notification) => {
|
|||
export const shuffle = (arr) => {
|
||||
let j;
|
||||
let x;
|
||||
for (let index = arr.length - 1; index > 0; index--) {
|
||||
for (let index = arr.length - 1; index > 0; index -= 1) {
|
||||
j = Math.floor(Math.random() * (index + 1));
|
||||
x = arr[index];
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
arr[index] = arr[j];
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
arr[j] = x;
|
||||
}
|
||||
return arr;
|
||||
|
@ -165,9 +161,11 @@ export const splitNoEmpty = (s, delimiter) =>
|
|||
/** Non-cryptographic hash function, see https://stackoverflow.com/a/8831937/1440785 */
|
||||
export const hashCode = async (s) => {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
for (let i = 0; i < s.length; i += 1) {
|
||||
const char = s.charCodeAt(i);
|
||||
// eslint-disable-next-line no-bitwise
|
||||
hash = (hash << 5) - hash + char;
|
||||
// eslint-disable-next-line no-bitwise
|
||||
hash &= hash; // Convert to 32bit integer
|
||||
}
|
||||
return hash;
|
||||
|
@ -248,6 +246,7 @@ export const playSound = async (id) => {
|
|||
};
|
||||
|
||||
// From: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
|
||||
// eslint-disable-next-line func-style
|
||||
export async function* fetchLinesIterator(fileURL, headers) {
|
||||
const utf8Decoder = new TextDecoder("utf-8");
|
||||
const response = await fetch(fileURL, {
|
||||
|
@ -267,9 +266,12 @@ export async function* fetchLinesIterator(fileURL, headers) {
|
|||
break;
|
||||
}
|
||||
const remainder = chunk.substr(startIndex);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
({ value: chunk, done: readerDone } = await reader.read());
|
||||
chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : "");
|
||||
startIndex = re.lastIndex = 0;
|
||||
startIndex = 0;
|
||||
re.lastIndex = 0;
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
yield chunk.substring(startIndex, result.index);
|
||||
|
@ -283,7 +285,8 @@ export async function* fetchLinesIterator(fileURL, headers) {
|
|||
export const randomAlphanumericString = (len) => {
|
||||
const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
let id = "";
|
||||
for (let i = 0; i < len; i++) {
|
||||
for (let i = 0; i < len; i += 1) {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
id += alphabet[(Math.random() * alphabet.length) | 0];
|
||||
}
|
||||
return id;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue