ntfy/web/src/app/Api.js

222 lines
8.3 KiB
JavaScript
Raw Normal View History

import {
fetchLinesIterator,
2022-12-03 20:20:59 +00:00
maybeWithBasicAuth, maybeWithBearerAuth,
topicShortUrl,
topicUrl,
topicUrlAuth,
topicUrlJsonPoll,
2022-12-03 20:20:59 +00:00
topicUrlJsonPollWithSince,
userAccountUrl,
2022-12-08 01:44:20 +00:00
userTokenUrl,
2022-12-09 01:50:48 +00:00
userStatsUrl, userSubscriptionUrl, userSubscriptionDeleteUrl
} from "./utils";
import userManager from "./UserManager";
2022-02-23 04:22:30 +00:00
class Api {
async poll(baseUrl, topic, since) {
const user = await userManager.get(baseUrl);
const shortUrl = topicShortUrl(baseUrl, topic);
2022-02-28 00:29:17 +00:00
const url = (since)
? topicUrlJsonPollWithSince(baseUrl, topic, since)
: topicUrlJsonPoll(baseUrl, topic);
2022-02-23 04:22:30 +00:00
const messages = [];
const headers = maybeWithBasicAuth({}, user);
2022-02-23 04:22:30 +00:00
console.log(`[Api] Polling ${url}`);
for await (let line of fetchLinesIterator(url, headers)) {
console.log(`[Api, ${shortUrl}] Received message ${line}`);
2022-02-23 04:22:30 +00:00
messages.push(JSON.parse(line));
}
2022-02-24 19:53:45 +00:00
return messages;
2022-02-23 04:22:30 +00:00
}
async publish(baseUrl, topic, message, options) {
const user = await userManager.get(baseUrl);
console.log(`[Api] Publishing message to ${topicUrl(baseUrl, topic)}`);
2022-03-11 03:58:24 +00:00
const headers = {};
const body = {
topic: topic,
message: message,
...options
};
const response = await fetch(baseUrl, {
2022-02-23 04:22:30 +00:00
method: 'PUT',
body: JSON.stringify(body),
2022-03-11 03:58:24 +00:00
headers: maybeWithBasicAuth(headers, user)
2022-02-23 04:22:30 +00:00
});
if (response.status < 200 || response.status > 299) {
throw new Error(`Unexpected response: ${response.status}`);
}
return response;
2022-02-23 04:22:30 +00:00
}
2022-02-25 18:40:03 +00:00
2022-04-05 23:55:43 +00:00
/**
* Publishes to a topic using XMLHttpRequest (XHR), and returns a Promise with the active request.
* Unfortunately, fetch() does not support a progress hook, which is why XHR has to be used.
*
* Firefox XHR bug:
* Firefox has a bug(?), which returns 0 and "" for all fields of the XHR response in the case of an error,
* so we cannot determine the exact error. It also sometimes complains about CORS violations, even when the
* correct headers are clearly set. It's quite the odd behavior.
*
* There is an example, and the bug report here:
* - https://bugzilla.mozilla.org/show_bug.cgi?id=1733755
* - https://gist.github.com/binwiederhier/627f146d1959799be207ad8c17a8f345
*/
publishXHR(url, body, headers, onProgress) {
2022-04-01 12:41:45 +00:00
console.log(`[Api] Publishing message to ${url}`);
const xhr = new XMLHttpRequest();
2022-04-01 12:41:45 +00:00
const send = new Promise(function (resolve, reject) {
xhr.open("PUT", url);
2022-04-04 00:19:43 +00:00
if (body.type) {
xhr.overrideMimeType(body.type);
}
for (const [key, value] of Object.entries(headers)) {
xhr.setRequestHeader(key, value);
}
xhr.upload.addEventListener("progress", onProgress);
2022-04-01 12:41:45 +00:00
xhr.addEventListener('readystatechange', (ev) => {
if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status <= 299) {
console.log(`[Api] Publish successful (HTTP ${xhr.status})`, xhr.response);
2022-04-01 12:41:45 +00:00
resolve(xhr.response);
} else if (xhr.readyState === 4) {
2022-04-05 23:55:43 +00:00
// Firefox bug; see description above!
2022-04-03 23:51:32 +00:00
console.log(`[Api] Publish failed (HTTP ${xhr.status})`, xhr.responseText);
let errorText;
try {
const error = JSON.parse(xhr.responseText);
if (error.code && error.error) {
errorText = `Error ${error.code}: ${error.error}`;
}
} catch (e) {
// Nothing
}
2022-04-01 12:41:45 +00:00
xhr.abort();
2022-04-03 23:51:32 +00:00
reject(errorText ?? "An error occurred");
2022-04-01 12:41:45 +00:00
}
})
xhr.send(body);
});
send.abort = () => {
console.log(`[Api] Publish aborted by user`);
xhr.abort();
}
return send;
}
2022-12-02 20:37:48 +00:00
async topicAuth(baseUrl, topic, user) {
2022-02-25 18:40:03 +00:00
const url = topicUrlAuth(baseUrl, topic);
console.log(`[Api] Checking auth for ${url}`);
const response = await fetch(url, {
headers: maybeWithBasicAuth({}, user)
});
2022-02-25 18:40:03 +00:00
if (response.status >= 200 && response.status <= 299) {
return true;
} else if (!user && response.status === 404) {
return true; // Special case: Anonymous login to old servers return 404 since /<topic>/auth doesn't exist
} else if (response.status === 401 || response.status === 403) { // See server/server.go
return false;
}
throw new Error(`Unexpected server response ${response.status}`);
}
2022-04-03 16:39:52 +00:00
2022-12-08 01:44:20 +00:00
async login(baseUrl, user) {
const url = userTokenUrl(baseUrl);
2022-12-02 20:37:48 +00:00
console.log(`[Api] Checking auth for ${url}`);
const response = await fetch(url, {
headers: maybeWithBasicAuth({}, user)
});
if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
const json = await response.json();
if (!json.token) {
throw new Error(`Unexpected server response: Cannot find token`);
}
return json.token;
}
2022-12-08 01:44:20 +00:00
async logout(baseUrl, token) {
const url = userTokenUrl(baseUrl);
console.log(`[Api] Logging out from ${url} using token ${token}`);
const response = await fetch(url, {
method: "DELETE",
headers: maybeWithBearerAuth({}, token)
});
if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
}
2022-04-03 16:39:52 +00:00
async userStats(baseUrl) {
const url = userStatsUrl(baseUrl);
console.log(`[Api] Fetching user stats ${url}`);
const response = await fetch(url);
if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
2022-04-06 03:33:07 +00:00
const stats = await response.json();
2022-04-05 23:55:43 +00:00
console.log(`[Api] Stats`, stats);
return stats;
2022-04-03 16:39:52 +00:00
}
2022-12-03 20:20:59 +00:00
async userAccount(baseUrl, token) {
const url = userAccountUrl(baseUrl);
console.log(`[Api] Fetching user account ${url}`);
const response = await fetch(url, {
headers: maybeWithBearerAuth({}, token)
});
if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
const account = await response.json();
console.log(`[Api] Account`, account);
return account;
}
2022-12-08 02:26:18 +00:00
async updateUserAccount(baseUrl, token, payload) {
const url = userAccountUrl(baseUrl);
const body = JSON.stringify(payload);
console.log(`[Api] Updating user account ${url}: ${body}`);
const response = await fetch(url, {
method: "POST",
headers: maybeWithBearerAuth({}, token),
body: body
});
if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
}
2022-12-09 01:50:48 +00:00
async userSubscriptionAdd(baseUrl, token, payload) {
const url = userSubscriptionUrl(baseUrl);
const body = JSON.stringify(payload);
console.log(`[Api] Adding user subscription ${url}: ${body}`);
const response = await fetch(url, {
method: "POST",
headers: maybeWithBearerAuth({}, token),
body: body
});
if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
const subscription = await response.json();
console.log(`[Api] Subscription`, subscription);
return subscription;
}
async userSubscriptionDelete(baseUrl, token, remoteId) {
const url = userSubscriptionDeleteUrl(baseUrl, remoteId);
console.log(`[Api] Removing user subscription ${url}`);
const response = await fetch(url, {
method: "DELETE",
headers: maybeWithBearerAuth({}, token)
});
if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`);
}
}
2022-02-23 04:22:30 +00:00
}
const api = new Api();
export default api;