Ship the Go/SQLite API and Web UI, Chrome/Brave capture addon, Docker Compose, Pangolin reverse-proxy support, and a user-crontab watchdog so the binary stays running without systemd.
177 lines
5.1 KiB
JavaScript
177 lines
5.1 KiB
JavaScript
// Shared API helpers.
|
|
// Content scripts must NEVER fetch the backend directly (Chrome Private Network
|
|
// Access would prompt "allow Reddit to access local network"). All page-context
|
|
// calls go through the background service worker instead.
|
|
|
|
async function rcsGetSettings() {
|
|
const stored = await chrome.storage.sync.get({
|
|
backendUrl: "",
|
|
apiKey: "",
|
|
});
|
|
return {
|
|
backendUrl: (stored.backendUrl || "").replace(/\/+$/, ""),
|
|
apiKey: stored.apiKey || "",
|
|
};
|
|
}
|
|
|
|
function rcsMustProxyViaBackground() {
|
|
// Popup/options are chrome-extension:// and may fetch directly.
|
|
// Content scripts run on https://reddit.com and must proxy.
|
|
try {
|
|
return typeof location === "undefined" || location.protocol !== "chrome-extension:";
|
|
} catch {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
async function rcsFetchViaBackground(path, options = {}) {
|
|
const response = await chrome.runtime.sendMessage({
|
|
type: "rcs.api",
|
|
path,
|
|
method: options.method || "GET",
|
|
headers: options.headers || {},
|
|
body: options.body || null,
|
|
});
|
|
if (!response || !response.ok) {
|
|
throw new Error((response && response.error) || "backend request failed");
|
|
}
|
|
return response.data;
|
|
}
|
|
|
|
async function rcsFetchDirect(path, options = {}) {
|
|
const settings = await rcsGetSettings();
|
|
if (!settings.backendUrl) {
|
|
throw new Error("Backend URL not configured");
|
|
}
|
|
const headers = Object.assign(
|
|
{ Accept: "application/json" },
|
|
options.headers || {}
|
|
);
|
|
if (settings.apiKey) {
|
|
headers["X-API-Key"] = settings.apiKey;
|
|
}
|
|
const response = await fetch(`${settings.backendUrl}${path}`, {
|
|
...options,
|
|
headers,
|
|
credentials: "include",
|
|
});
|
|
const text = await response.text();
|
|
let data = null;
|
|
try {
|
|
data = text ? JSON.parse(text) : null;
|
|
} catch {
|
|
data = { raw: text };
|
|
}
|
|
if (!response.ok) {
|
|
throw new Error(rcsProxyAuthErrorMessage(response.status, text, data));
|
|
}
|
|
if (data && data.raw != null && rcsLooksLikeHtml(data.raw)) {
|
|
throw new Error(rcsProxyAuthErrorMessage(response.status, text, data));
|
|
}
|
|
return data;
|
|
}
|
|
|
|
function rcsLooksLikeHtml(text) {
|
|
return typeof text === "string" && /^\s*</.test(text);
|
|
}
|
|
|
|
function rcsProxyAuthErrorMessage(status, text, data) {
|
|
if (status === 401 || status === 403 || rcsLooksLikeHtml(text) || (data && rcsLooksLikeHtml(data.raw))) {
|
|
return "Open the RCS Web UI in this browser, log in through the reverse proxy (Pangolin), then retry.";
|
|
}
|
|
return (data && data.error) || (status ? `HTTP ${status}` : null) || "request failed";
|
|
}
|
|
|
|
async function rcsFetch(path, options = {}) {
|
|
if (rcsMustProxyViaBackground()) {
|
|
return rcsFetchViaBackground(path, options);
|
|
}
|
|
return rcsFetchDirect(path, options);
|
|
}
|
|
|
|
async function rcsCommentExists(commentId) {
|
|
const data = await rcsFetch(`/api/comments/exists?id=${encodeURIComponent(commentId)}`);
|
|
return {
|
|
exists: Boolean(data && data.exists),
|
|
has_screenshot: Boolean(data && data.has_screenshot),
|
|
};
|
|
}
|
|
|
|
async function rcsPostExists(postId) {
|
|
const data = await rcsFetch(`/api/posts/exists?id=${encodeURIComponent(postId)}`);
|
|
return Boolean(data && data.exists);
|
|
}
|
|
|
|
async function rcsSaveComment(payload) {
|
|
return rcsFetch("/api/comments", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
async function rcsAttachScreenshot(commentId, screenshotBase64) {
|
|
return rcsFetch(`/api/comments/${encodeURIComponent(commentId)}/screenshot`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ screenshot_base64: screenshotBase64 }),
|
|
});
|
|
}
|
|
|
|
async function rcsSavePost(payload) {
|
|
return rcsFetch("/api/posts", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
}
|
|
|
|
async function rcsHealth() {
|
|
return rcsFetch("/api/health");
|
|
}
|
|
|
|
function rcsNormalizeUsername(name) {
|
|
return String(name || "")
|
|
.trim()
|
|
.replace(/^\/?u\//i, "")
|
|
.trim()
|
|
.toLowerCase();
|
|
}
|
|
|
|
async function rcsCacheBlocklist(usernames) {
|
|
const normalized = (usernames || [])
|
|
.map(rcsNormalizeUsername)
|
|
.filter(Boolean);
|
|
const unique = [...new Set(normalized)].sort();
|
|
await chrome.storage.sync.set({ blockedUsernames: unique });
|
|
return unique;
|
|
}
|
|
|
|
async function rcsGetCachedBlocklist() {
|
|
const stored = await chrome.storage.sync.get({ blockedUsernames: [] });
|
|
return Array.isArray(stored.blockedUsernames) ? stored.blockedUsernames : [];
|
|
}
|
|
|
|
async function rcsPullBlocklist() {
|
|
const data = await rcsFetch("/api/blocklist");
|
|
return rcsCacheBlocklist(data && data.usernames ? data.usernames : []);
|
|
}
|
|
|
|
async function rcsAddBlocked(username) {
|
|
const data = await rcsFetch("/api/blocklist", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ username }),
|
|
});
|
|
return rcsCacheBlocklist(data && data.usernames ? data.usernames : []);
|
|
}
|
|
|
|
async function rcsRemoveBlocked(username) {
|
|
const encoded = encodeURIComponent(rcsNormalizeUsername(username));
|
|
const data = await rcsFetch(`/api/blocklist/${encoded}`, {
|
|
method: "DELETE",
|
|
});
|
|
return rcsCacheBlocklist(data && data.usernames ? data.usernames : []);
|
|
}
|
|
|