Line width

This commit is contained in:
binwiederhier 2023-05-23 19:29:47 -04:00
parent 2e27f58963
commit ca5d736a71
33 changed files with 521 additions and 2033 deletions

View file

@ -56,9 +56,7 @@ class AccountApi {
async logout() {
const url = accountTokenUrl(config.base_url);
console.log(
`[AccountApi] Logging out from ${url} using token ${session.token()}`
);
console.log(`[AccountApi] Logging out from ${url} using token ${session.token()}`);
await fetchOrThrow(url, {
method: "DELETE",
headers: withBearerAuth({}, session.token()),
@ -227,9 +225,7 @@ class AccountApi {
async upsertReservation(topic, everyone) {
const url = accountReservationUrl(config.base_url);
console.log(
`[AccountApi] Upserting user access to topic ${topic}, everyone=${everyone}`
);
console.log(`[AccountApi] Upserting user access to topic ${topic}, everyone=${everyone}`);
await fetchOrThrow(url, {
method: "POST",
headers: withBearerAuth({}, session.token()),
@ -264,16 +260,12 @@ class AccountApi {
}
async createBillingSubscription(tier, interval) {
console.log(
`[AccountApi] Creating billing subscription with ${tier} and interval ${interval}`
);
console.log(`[AccountApi] Creating billing subscription with ${tier} and interval ${interval}`);
return await this.upsertBillingSubscription("POST", tier, interval);
}
async updateBillingSubscription(tier, interval) {
console.log(
`[AccountApi] Updating billing subscription with ${tier} and interval ${interval}`
);
console.log(`[AccountApi] Updating billing subscription with ${tier} and interval ${interval}`);
return await this.upsertBillingSubscription("PUT", tier, interval);
}
@ -324,9 +316,7 @@ class AccountApi {
async addPhoneNumber(phoneNumber, code) {
const url = accountPhoneUrl(config.base_url);
console.log(
`[AccountApi] Adding phone number with verification code ${url}`
);
console.log(`[AccountApi] Adding phone number with verification code ${url}`);
await fetchOrThrow(url, {
method: "PUT",
headers: withBearerAuth({}, session.token()),
@ -371,10 +361,7 @@ class AccountApi {
}
}
if (account.subscriptions) {
await subscriptionManager.syncFromRemote(
account.subscriptions,
account.reservations
);
await subscriptionManager.syncFromRemote(account.subscriptions, account.reservations);
}
return account;
} catch (e) {

View file

@ -1,12 +1,4 @@
import {
fetchLinesIterator,
maybeWithAuth,
topicShortUrl,
topicUrl,
topicUrlAuth,
topicUrlJsonPoll,
topicUrlJsonPollWithSince,
} from "./utils";
import { fetchLinesIterator, maybeWithAuth, topicShortUrl, topicUrl, topicUrlAuth, topicUrlJsonPoll, topicUrlJsonPollWithSince } from "./utils";
import userManager from "./UserManager";
import { fetchOrThrow } from "./errors";
@ -14,9 +6,7 @@ class Api {
async poll(baseUrl, topic, since) {
const user = await userManager.get(baseUrl);
const shortUrl = topicShortUrl(baseUrl, topic);
const url = since
? topicUrlJsonPollWithSince(baseUrl, topic, since)
: topicUrlJsonPoll(baseUrl, topic);
const url = since ? topicUrlJsonPollWithSince(baseUrl, topic, since) : topicUrlJsonPoll(baseUrl, topic);
const messages = [];
const headers = maybeWithAuth({}, user);
console.log(`[Api] Polling ${url}`);
@ -73,17 +63,11 @@ class Api {
xhr.upload.addEventListener("progress", onProgress);
xhr.addEventListener("readystatechange", () => {
if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status <= 299) {
console.log(
`[Api] Publish successful (HTTP ${xhr.status})`,
xhr.response
);
console.log(`[Api] Publish successful (HTTP ${xhr.status})`, xhr.response);
resolve(xhr.response);
} else if (xhr.readyState === 4) {
// Firefox bug; see description above!
console.log(
`[Api] Publish failed (HTTP ${xhr.status})`,
xhr.responseText
);
console.log(`[Api] Publish failed (HTTP ${xhr.status})`, xhr.responseText);
let errorText;
try {
const error = JSON.parse(xhr.responseText);

View file

@ -1,10 +1,4 @@
import {
basicAuth,
bearerAuth,
encodeBase64Url,
topicShortUrl,
topicUrlWs,
} from "./utils";
import { basicAuth, bearerAuth, encodeBase64Url, topicShortUrl, topicUrlWs } from "./utils";
const retryBackoffSeconds = [5, 10, 20, 30, 60, 120];
@ -15,16 +9,7 @@ const retryBackoffSeconds = [5, 10, 20, 30, 60, 120];
* Incoming messages and state changes are forwarded via listeners.
*/
class Connection {
constructor(
connectionId,
subscriptionId,
baseUrl,
topic,
user,
since,
onNotification,
onStateChanged
) {
constructor(connectionId, subscriptionId, baseUrl, topic, user, since, onNotification, onStateChanged) {
this.connectionId = connectionId;
this.subscriptionId = subscriptionId;
this.baseUrl = baseUrl;
@ -44,78 +29,51 @@ class Connection {
// we don't want to re-trigger the main view re-render potentially hundreds of times.
const wsUrl = this.wsUrl();
console.log(
`[Connection, ${this.shortUrl}, ${this.connectionId}] Opening connection to ${wsUrl}`
);
console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Opening connection to ${wsUrl}`);
this.ws = new WebSocket(wsUrl);
this.ws.onopen = (event) => {
console.log(
`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection established`,
event
);
console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection established`, event);
this.retryCount = 0;
this.onStateChanged(this.subscriptionId, ConnectionState.Connected);
};
this.ws.onmessage = (event) => {
console.log(
`[Connection, ${this.shortUrl}, ${this.connectionId}] Message received from server: ${event.data}`
);
console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Message received from server: ${event.data}`);
try {
const data = JSON.parse(event.data);
if (data.event === "open") {
return;
}
const relevantAndValid =
data.event === "message" &&
"id" in data &&
"time" in data &&
"message" in data;
const relevantAndValid = data.event === "message" && "id" in data && "time" in data && "message" in data;
if (!relevantAndValid) {
console.log(
`[Connection, ${this.shortUrl}, ${this.connectionId}] Unexpected message. Ignoring.`
);
console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Unexpected message. Ignoring.`);
return;
}
this.since = data.id;
this.onNotification(this.subscriptionId, data);
} catch (e) {
console.log(
`[Connection, ${this.shortUrl}, ${this.connectionId}] Error handling message: ${e}`
);
console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Error handling message: ${e}`);
}
};
this.ws.onclose = (event) => {
if (event.wasClean) {
console.log(
`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection closed cleanly, code=${event.code} reason=${event.reason}`
);
console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
this.ws = null;
} else {
const retrySeconds =
retryBackoffSeconds[
Math.min(this.retryCount, retryBackoffSeconds.length - 1)
];
const retrySeconds = retryBackoffSeconds[Math.min(this.retryCount, retryBackoffSeconds.length - 1)];
this.retryCount++;
console.log(
`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection died, retrying in ${retrySeconds} seconds`
);
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);
}
};
this.ws.onerror = (event) => {
console.log(
`[Connection, ${this.shortUrl}, ${this.connectionId}] Error occurred: ${event}`,
event
);
console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Error occurred: ${event}`, event);
};
}
close() {
console.log(
`[Connection, ${this.shortUrl}, ${this.connectionId}] Closing connection`
);
console.log(`[Connection, ${this.shortUrl}, ${this.connectionId}] Closing connection`);
const socket = this.ws;
const retryTimeout = this.retryTimeout;
if (socket !== null) {

View file

@ -49,12 +49,8 @@ class ConnectionManager {
return { ...s, user, connectionId };
})
);
const targetIds = subscriptionsWithUsersAndConnectionId.map(
(s) => s.connectionId
);
const deletedIds = Array.from(this.connections.keys()).filter(
(id) => !targetIds.includes(id)
);
const targetIds = subscriptionsWithUsersAndConnectionId.map((s) => s.connectionId);
const deletedIds = Array.from(this.connections.keys()).filter((id) => !targetIds.includes(id));
// Create and add new connections
subscriptionsWithUsersAndConnectionId.forEach((subscription) => {
@ -73,15 +69,12 @@ class ConnectionManager {
topic,
user,
since,
(subscriptionId, notification) =>
this.notificationReceived(subscriptionId, notification),
(subscriptionId, notification) => this.notificationReceived(subscriptionId, notification),
(subscriptionId, state) => this.stateChanged(subscriptionId, state)
);
this.connections.set(connectionId, connection);
console.log(
`[ConnectionManager] Starting new connection ${connectionId} (subscription ${subscriptionId} with user ${
user ? user.username : "anonymous"
})`
`[ConnectionManager] Starting new connection ${connectionId} (subscription ${subscriptionId} with user ${user ? user.username : "anonymous"})`
);
connection.start();
}
@ -101,10 +94,7 @@ class ConnectionManager {
try {
this.stateListener(subscriptionId, state);
} catch (e) {
console.error(
`[ConnectionManager] Error updating state of ${subscriptionId} to ${state}`,
e
);
console.error(`[ConnectionManager] Error updating state of ${subscriptionId} to ${state}`, e);
}
}
}
@ -114,23 +104,14 @@ class ConnectionManager {
try {
this.messageListener(subscriptionId, notification);
} catch (e) {
console.error(
`[ConnectionManager] Error handling notification for ${subscriptionId}`,
e
);
console.error(`[ConnectionManager] Error handling notification for ${subscriptionId}`, e);
}
}
}
}
const makeConnectionId = async (subscription, user) => {
return user
? hashCode(
`${subscription.id}|${user.username}|${user.password ?? ""}|${
user.token ?? ""
}`
)
: hashCode(`${subscription.id}`);
return user ? hashCode(`${subscription.id}|${user.username}|${user.password ?? ""}|${user.token ?? ""}`) : hashCode(`${subscription.id}`);
};
const connectionManager = new ConnectionManager();

View file

@ -1,11 +1,4 @@
import {
formatMessage,
formatTitleWithDefault,
openUrl,
playSound,
topicDisplayName,
topicShortUrl,
} from "./utils";
import { formatMessage, formatTitleWithDefault, openUrl, playSound, topicDisplayName, topicShortUrl } from "./utils";
import prefs from "./Prefs";
import subscriptionManager from "./SubscriptionManager";
import logo from "../img/ntfy.png";
@ -30,9 +23,7 @@ class Notifier {
const title = formatTitleWithDefault(notification, displayName);
// Show notification
console.log(
`[Notifier, ${shortUrl}] Displaying notification ${notification.id}: ${message}`
);
console.log(`[Notifier, ${shortUrl}] Displaying notification ${notification.id}: ${message}`);
const n = new Notification(title, {
body: message,
icon: logo,
@ -96,11 +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 location.protocol === "https:" || location.hostname.match("^127.") || location.hostname === "localhost";
}
}

View file

@ -34,18 +34,12 @@ class Poller {
console.log(`[Poller] Polling ${subscription.id}`);
const since = subscription.last;
const notifications = await api.poll(
subscription.baseUrl,
subscription.topic,
since
);
const notifications = await api.poll(subscription.baseUrl, subscription.topic, since);
if (!notifications || notifications.length === 0) {
console.log(`[Poller] No new notifications found for ${subscription.id}`);
return;
}
console.log(
`[Poller] Adding ${notifications.length} notification(s) for ${subscription.id}`
);
console.log(`[Poller] Adding ${notifications.length} notification(s) for ${subscription.id}`);
await subscriptionManager.addNotifications(subscription.id, notifications);
}

View file

@ -20,15 +20,12 @@ class Pruner {
async prune() {
const deleteAfterSeconds = await prefs.deleteAfter();
const pruneThresholdTimestamp =
Math.round(Date.now() / 1000) - deleteAfterSeconds;
const pruneThresholdTimestamp = Math.round(Date.now() / 1000) - deleteAfterSeconds;
if (deleteAfterSeconds === 0) {
console.log(`[Pruner] Pruning is disabled. Skipping.`);
return;
}
console.log(
`[Pruner] Pruning notifications older than ${deleteAfterSeconds}s (timestamp ${pruneThresholdTimestamp})`
);
console.log(`[Pruner] Pruning notifications older than ${deleteAfterSeconds}s (timestamp ${pruneThresholdTimestamp})`);
try {
await subscriptionManager.pruneNotifications(pruneThresholdTimestamp);
} catch (e) {

View file

@ -7,9 +7,7 @@ class SubscriptionManager {
const subscriptions = await db.subscriptions.toArray();
await Promise.all(
subscriptions.map(async (s) => {
s.new = await db.notifications
.where({ subscriptionId: s.id, new: 1 })
.count();
s.new = await db.notifications.where({ subscriptionId: s.id, new: 1 }).count();
})
);
return subscriptions;
@ -38,20 +36,14 @@ class SubscriptionManager {
}
async syncFromRemote(remoteSubscriptions, remoteReservations) {
console.log(
`[SubscriptionManager] Syncing subscriptions from remote`,
remoteSubscriptions
);
console.log(`[SubscriptionManager] Syncing subscriptions from remote`, remoteSubscriptions);
// Add remote subscriptions
let remoteIds = []; // = topicUrl(baseUrl, topic)
for (let i = 0; i < remoteSubscriptions.length; i++) {
const remote = remoteSubscriptions[i];
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;
const reservation = remoteReservations?.find((r) => remote.base_url === config.base_url && remote.topic === r.topic) || null;
await this.update(local.id, {
displayName: remote.display_name, // May be undefined
reservation: reservation, // May be null!
@ -122,9 +114,7 @@ class SubscriptionManager {
/** Adds/replaces notifications, will not throw if they exist */
async addNotifications(subscriptionId, notifications) {
const notificationsWithSubscriptionId = notifications.map(
(notification) => ({ ...notification, subscriptionId })
);
const notificationsWithSubscriptionId = notifications.map((notification) => ({ ...notification, subscriptionId }));
const lastNotificationId = notifications.at(-1).id;
await db.notifications.bulkPut(notificationsWithSubscriptionId);
await db.subscriptions.update(subscriptionId, {
@ -158,9 +148,7 @@ class SubscriptionManager {
}
async markNotificationsRead(subscriptionId) {
await db.notifications
.where({ subscriptionId: subscriptionId, new: 1 })
.modify({ new: 0 });
await db.notifications.where({ subscriptionId: subscriptionId, new: 1 }).modify({ new: 0 });
}
async setMutedUntil(subscriptionId, mutedUntil) {

View file

@ -15,12 +15,7 @@ export const throwAppError = async (response) => {
}
const error = await maybeToJson(response);
if (error?.code) {
console.log(
`[Error] HTTP ${response.status}, ntfy error ${error.code}: ${
error.error || ""
}`,
response
);
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) {

View file

@ -10,37 +10,23 @@ import config from "./config";
import { Base64 } from "js-base64";
export const topicUrl = (baseUrl, topic) => `${baseUrl}/${topic}`;
export const topicUrlWs = (baseUrl, topic) =>
`${topicUrl(baseUrl, topic)}/ws`
.replaceAll("https://", "wss://")
.replaceAll("http://", "ws://");
export const topicUrlJson = (baseUrl, topic) =>
`${topicUrl(baseUrl, topic)}/json`;
export const topicUrlJsonPoll = (baseUrl, topic) =>
`${topicUrlJson(baseUrl, topic)}?poll=1`;
export const topicUrlJsonPollWithSince = (baseUrl, topic, since) =>
`${topicUrlJson(baseUrl, topic)}?poll=1&since=${since}`;
export const topicUrlAuth = (baseUrl, topic) =>
`${topicUrl(baseUrl, topic)}/auth`;
export const topicShortUrl = (baseUrl, topic) =>
shortUrl(topicUrl(baseUrl, topic));
export const topicUrlWs = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/ws`.replaceAll("https://", "wss://").replaceAll("http://", "ws://");
export const topicUrlJson = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/json`;
export const topicUrlJsonPoll = (baseUrl, topic) => `${topicUrlJson(baseUrl, topic)}?poll=1`;
export const topicUrlJsonPollWithSince = (baseUrl, topic, since) => `${topicUrlJson(baseUrl, topic)}?poll=1&since=${since}`;
export const topicUrlAuth = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/auth`;
export const topicShortUrl = (baseUrl, topic) => shortUrl(topicUrl(baseUrl, topic));
export const accountUrl = (baseUrl) => `${baseUrl}/v1/account`;
export const accountPasswordUrl = (baseUrl) => `${baseUrl}/v1/account/password`;
export const accountTokenUrl = (baseUrl) => `${baseUrl}/v1/account/token`;
export const accountSettingsUrl = (baseUrl) => `${baseUrl}/v1/account/settings`;
export const accountSubscriptionUrl = (baseUrl) =>
`${baseUrl}/v1/account/subscription`;
export const accountReservationUrl = (baseUrl) =>
`${baseUrl}/v1/account/reservation`;
export const accountReservationSingleUrl = (baseUrl, topic) =>
`${baseUrl}/v1/account/reservation/${topic}`;
export const accountBillingSubscriptionUrl = (baseUrl) =>
`${baseUrl}/v1/account/billing/subscription`;
export const accountBillingPortalUrl = (baseUrl) =>
`${baseUrl}/v1/account/billing/portal`;
export const accountSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account/subscription`;
export const accountReservationUrl = (baseUrl) => `${baseUrl}/v1/account/reservation`;
export const accountReservationSingleUrl = (baseUrl, topic) => `${baseUrl}/v1/account/reservation/${topic}`;
export const accountBillingSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account/billing/subscription`;
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 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}`];
@ -208,9 +194,7 @@ export const formatShortDateTime = (timestamp) => {
};
export const formatShortDate = (timestamp) => {
return new Intl.DateTimeFormat("default", { dateStyle: "short" }).format(
new Date(timestamp * 1000)
);
return new Intl.DateTimeFormat("default", { dateStyle: "short" }).format(new Date(timestamp * 1000));
};
export const formatBytes = (bytes, decimals = 2) => {
@ -312,8 +296,7 @@ export async function* fetchLinesIterator(fileURL, headers) {
}
export const randomAlphanumericString = (len) => {
const alphabet =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let id = "";
for (let i = 0; i < len; i++) {
id += alphabet[(Math.random() * alphabet.length) | 0];