mirror of
https://github.com/besoeasy/file-drop.git
synced 2026-01-27 13:48:50 -06:00
feat: add pins history table and database management for pinned content
- Updated package.json to include better-sqlite3 and dotenv dependencies. - Added a new admin interface for displaying pinned content history with filtering and pagination. - Implemented database module for managing pinned content, including CRUD operations and statistics. - Enhanced frontend functionality to load and display pins with real-time updates and error handling.
This commit is contained in:
@@ -1,2 +1,4 @@
|
||||
node_modules
|
||||
temp_uploads/
|
||||
db/
|
||||
.env
|
||||
@@ -1,3 +1,5 @@
|
||||
require("dotenv").config();
|
||||
|
||||
// Main application entry point
|
||||
const express = require("express");
|
||||
const fs = require("fs");
|
||||
@@ -16,7 +18,8 @@ const {
|
||||
healthHandler,
|
||||
statusHandler,
|
||||
nostrHandler,
|
||||
uploadHandler
|
||||
uploadHandler,
|
||||
pinsHandler,
|
||||
} = require("./modules/routes");
|
||||
|
||||
const { runNostrJob, pinnerJob } = require("./modules/jobs");
|
||||
@@ -50,6 +53,7 @@ setupMiddleware(app);
|
||||
app.get("/health", healthHandler);
|
||||
app.get("/status", statusHandler);
|
||||
app.get("/nostr", (req, res) => nostrHandler(req, res, NPUB));
|
||||
app.get("/api/pins", pinsHandler);
|
||||
app.post("/upload", upload.single("file"), uploadHandler);
|
||||
|
||||
// Apply error handler
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ const HOST = "0.0.0.0";
|
||||
const UPLOAD_TEMP_DIR = "/tmp/filedrop";
|
||||
|
||||
// Nostr timing configuration
|
||||
const NOSTR_CHECK_INTERVAL_MS = 11 * 60 * 1000; // 11 minutes
|
||||
const NOSTR_CHECK_INTERVAL_MS = 7 * 60 * 1000; // 7 minutes
|
||||
const PINNER_INTERVAL_MS = 2 * 60 * 1000; // 2 minutes
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
// Database module for tracking pinned content
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// Initialize database
|
||||
const dbDir = path.join(__dirname, '..', 'db');
|
||||
const dbPath = process.env.DB_PATH || path.join(dbDir, 'pins.db');
|
||||
|
||||
// Ensure db directory exists
|
||||
if (!fs.existsSync(dbDir)) {
|
||||
fs.mkdirSync(dbDir, { recursive: true });
|
||||
}
|
||||
|
||||
const db = new Database(dbPath);
|
||||
|
||||
// Enable WAL mode for better concurrency
|
||||
db.pragma('journal_mode = WAL');
|
||||
|
||||
// Create pins table
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS pins (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
event_id TEXT NOT NULL,
|
||||
cid TEXT NOT NULL UNIQUE,
|
||||
size INTEGER DEFAULT 0,
|
||||
timestamp INTEGER NOT NULL,
|
||||
author TEXT,
|
||||
type TEXT NOT NULL CHECK(type IN ('self', 'friend')),
|
||||
status TEXT NOT NULL DEFAULT 'pinned' CHECK(status IN ('pending', 'pinned', 'cached', 'failed')),
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pins_cid ON pins(cid);
|
||||
CREATE INDEX IF NOT EXISTS idx_pins_event_id ON pins(event_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pins_type ON pins(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_pins_status ON pins(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_pins_timestamp ON pins(timestamp DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_pins_created_at ON pins(created_at DESC);
|
||||
`);
|
||||
|
||||
// Prepared statements
|
||||
const insertPinStmt = db.prepare(`
|
||||
INSERT OR REPLACE INTO pins (event_id, cid, size, timestamp, author, type, status, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, strftime('%s', 'now'))
|
||||
`);
|
||||
|
||||
const updatePinSizeStmt = db.prepare(`
|
||||
UPDATE pins SET size = ?, status = ?, updated_at = strftime('%s', 'now')
|
||||
WHERE cid = ?
|
||||
`);
|
||||
|
||||
const getPinByCidStmt = db.prepare(`
|
||||
SELECT * FROM pins WHERE cid = ?
|
||||
`);
|
||||
|
||||
const getPinsStmt = db.prepare(`
|
||||
SELECT * FROM pins
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`);
|
||||
|
||||
const getPinsByTypeStmt = db.prepare(`
|
||||
SELECT * FROM pins
|
||||
WHERE type = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`);
|
||||
|
||||
const getStatsStmt = db.prepare(`
|
||||
SELECT
|
||||
type,
|
||||
status,
|
||||
COUNT(*) as count,
|
||||
COALESCE(SUM(size), 0) as total_size
|
||||
FROM pins
|
||||
GROUP BY type, status
|
||||
`);
|
||||
|
||||
const getTotalCountStmt = db.prepare(`
|
||||
SELECT COUNT(*) as total FROM pins
|
||||
`);
|
||||
|
||||
const getRecentPinsStmt = db.prepare(`
|
||||
SELECT * FROM pins
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
|
||||
// Functions
|
||||
const recordPin = ({ eventId, cid, size = 0, timestamp, author, type, status = 'pinned' }) => {
|
||||
try {
|
||||
insertPinStmt.run(eventId, cid, size, timestamp, author, type, status);
|
||||
console.log(`[DB] Recorded ${type} pin: ${cid} from event ${eventId}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(`[DB] Failed to record pin:`, err.message);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const updatePinSize = (cid, size, status = 'pinned') => {
|
||||
try {
|
||||
updatePinSizeStmt.run(size, status, cid);
|
||||
console.log(`[DB] Updated pin size: ${cid} = ${size} bytes`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(`[DB] Failed to update pin size:`, err.message);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const getPinByCid = (cid) => {
|
||||
try {
|
||||
return getPinByCidStmt.get(cid);
|
||||
} catch (err) {
|
||||
console.error(`[DB] Failed to get pin by CID:`, err.message);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getPins = (limit = 50, offset = 0) => {
|
||||
try {
|
||||
return getPinsStmt.all(limit, offset);
|
||||
} catch (err) {
|
||||
console.error(`[DB] Failed to get pins:`, err.message);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const getPinsByType = (type, limit = 50, offset = 0) => {
|
||||
try {
|
||||
return getPinsByTypeStmt.all(type, limit, offset);
|
||||
} catch (err) {
|
||||
console.error(`[DB] Failed to get pins by type:`, err.message);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const getStats = () => {
|
||||
try {
|
||||
return getStatsStmt.all();
|
||||
} catch (err) {
|
||||
console.error(`[DB] Failed to get stats:`, err.message);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const getTotalCount = () => {
|
||||
try {
|
||||
const result = getTotalCountStmt.get();
|
||||
return result ? result.total : 0;
|
||||
} catch (err) {
|
||||
console.error(`[DB] Failed to get total count:`, err.message);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const getRecentPins = (limit = 10) => {
|
||||
try {
|
||||
return getRecentPinsStmt.all(limit);
|
||||
} catch (err) {
|
||||
console.error(`[DB] Failed to get recent pins:`, err.message);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// Cleanup on exit
|
||||
process.on('exit', () => {
|
||||
db.close();
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
db.close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
db,
|
||||
recordPin,
|
||||
updatePinSize,
|
||||
getPinByCid,
|
||||
getPins,
|
||||
getPinsByType,
|
||||
getStats,
|
||||
getTotalCount,
|
||||
getRecentPins,
|
||||
};
|
||||
+12
-5
@@ -5,18 +5,25 @@ const { IPFS_API } = require("./config");
|
||||
// Get total size of pinned content
|
||||
const getPinnedSize = async () => {
|
||||
try {
|
||||
const pinResponse = await axios.post(`${IPFS_API}/api/v0/pin/ls?type=recursive`, null, { timeout: 10000 });
|
||||
const pinResponse = await axios.post(`${IPFS_API}/api/v0/pin/ls?type=recursive`, {}, { timeout: 10000 });
|
||||
const pins = pinResponse.data.Keys || {};
|
||||
const cids = Object.keys(pins);
|
||||
|
||||
let totalSize = 0;
|
||||
for (const cid of cids) {
|
||||
try {
|
||||
const statResponse = await axios.post(`${IPFS_API}/api/v0/object/stat?arg=${encodeURIComponent(cid)}`, null, { timeout: 5000 });
|
||||
totalSize += statResponse.data.CumulativeSize || 0;
|
||||
const statResponse = await axios.post(`${IPFS_API}/api/v0/files/stat?arg=/ipfs/${encodeURIComponent(cid)}`, {}, { timeout: 5000 });
|
||||
const size = statResponse.data.CumulativeSize || statResponse.data.Size || 0;
|
||||
totalSize += size;
|
||||
} catch (err) {
|
||||
// Skip CIDs that fail to stat
|
||||
console.warn(`Failed to stat pinned CID ${cid}:`, err.message);
|
||||
// Try alternative method with block/stat
|
||||
try {
|
||||
const blockResponse = await axios.post(`${IPFS_API}/api/v0/block/stat?arg=${encodeURIComponent(cid)}`, {}, { timeout: 5000 });
|
||||
totalSize += blockResponse.data.Size || 0;
|
||||
} catch (blockErr) {
|
||||
// Skip CIDs that fail to stat
|
||||
console.warn(`Failed to stat pinned CID ${cid}:`, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { totalSize, count: cids.length };
|
||||
|
||||
+52
-5
@@ -16,7 +16,8 @@ const {
|
||||
setLastNostrRun,
|
||||
} = require("./queue");
|
||||
|
||||
const { isPinned, pinCid, addCid } = require("./nostr");
|
||||
const { isPinned, pinCid, addCid, getCidSize } = require("./nostr");
|
||||
const { recordPin, updatePinSize, getPinByCid } = require("./database");
|
||||
|
||||
let timerProbabilityMethod = 0.9;
|
||||
|
||||
@@ -160,11 +161,37 @@ const pinnerJob = async () => {
|
||||
}
|
||||
|
||||
if (cidToPin) {
|
||||
const cidObj = selfQueue[cidToPinIndex];
|
||||
console.log(`\n[Self] Pinning CID: ${cidToPin}`);
|
||||
|
||||
// Record to database first (as pending)
|
||||
recordPin({
|
||||
eventId: cidObj.eventId,
|
||||
cid: cidToPin,
|
||||
size: 0,
|
||||
timestamp: cidObj.timestamp,
|
||||
author: cidObj.author,
|
||||
type: 'self',
|
||||
status: 'pending'
|
||||
});
|
||||
|
||||
// Fire-and-forget: start pinning without waiting
|
||||
pinCid(cidToPin)
|
||||
.then(() => console.log(`✓ Successfully pinned: ${cidToPin}`))
|
||||
.catch(err => console.error(`❌ Failed to pin ${cidToPin}:`, err.message));
|
||||
.then(async () => {
|
||||
console.log(`✓ Successfully pinned: ${cidToPin}`);
|
||||
// Try to get size after pinning
|
||||
try {
|
||||
const size = await getCidSize(cidToPin);
|
||||
updatePinSize(cidToPin, size, 'pinned');
|
||||
} catch (err) {
|
||||
updatePinSize(cidToPin, 0, 'pinned');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(`❌ Failed to pin ${cidToPin}:`, err.message);
|
||||
updatePinSize(cidToPin, 0, 'failed');
|
||||
});
|
||||
|
||||
removeFromSelfQueue(cidToPinIndex);
|
||||
incrementPinnedSelf();
|
||||
console.log(`📊 Counter updated: totalPinnedSelf = ${incrementPinnedSelf.length}`);
|
||||
@@ -188,10 +215,30 @@ const pinnerJob = async () => {
|
||||
console.log(` Event: ${primalLink}`);
|
||||
console.log(` Author: ${cidObj.author} | Time: ${new Date(cidObj.timestamp * 1000).toISOString()}`);
|
||||
|
||||
// Record to database first (as pending)
|
||||
recordPin({
|
||||
eventId: cidObj.eventId,
|
||||
cid: cid,
|
||||
size: 0,
|
||||
timestamp: cidObj.timestamp,
|
||||
author: cidObj.author,
|
||||
type: 'friend',
|
||||
status: 'pending'
|
||||
});
|
||||
|
||||
// Fire-and-forget: start caching without waiting
|
||||
addCid(cid)
|
||||
.then(() => console.log(`✓ Successfully cached: ${cid}`))
|
||||
.catch(err => console.error(`❌ Failed to cache ${cid}:`, err.message));
|
||||
.then((result) => {
|
||||
console.log(`✓ Successfully cached: ${cid}`);
|
||||
// Update with actual size from result
|
||||
const size = result?.size || 0;
|
||||
updatePinSize(cid, size, 'cached');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(`❌ Failed to cache ${cid}:`, err.message);
|
||||
updatePinSize(cid, 0, 'failed');
|
||||
});
|
||||
|
||||
removeFromFriendsQueue(randomIndex);
|
||||
incrementCachedFriends();
|
||||
console.log(`📊 Counter updated: totalCachedFriends = ${incrementCachedFriends.length}`);
|
||||
|
||||
@@ -290,6 +290,24 @@ const addCid = async (cid, ipfsApi = IPFS_API) => {
|
||||
return { cid, size: res.data.length, alreadyPinned, newlyAdded: !alreadyPinned };
|
||||
};
|
||||
|
||||
// Get size of a CID
|
||||
const getCidSize = async (cid, ipfsApi = IPFS_API) => {
|
||||
try {
|
||||
// Try files/stat first
|
||||
const statResponse = await axios.post(`${ipfsApi}/api/v0/files/stat?arg=/ipfs/${encodeURIComponent(cid)}`, {}, { timeout: 5000 });
|
||||
return statResponse.data.CumulativeSize || statResponse.data.Size || 0;
|
||||
} catch (err) {
|
||||
// Try block/stat as fallback
|
||||
try {
|
||||
const blockResponse = await axios.post(`${ipfsApi}/api/v0/block/stat?arg=${encodeURIComponent(cid)}`, {}, { timeout: 5000 });
|
||||
return blockResponse.data.Size || 0;
|
||||
} catch (blockErr) {
|
||||
console.warn(`Failed to get size for CID ${cid}:`, blockErr.message);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const syncNostrPins = async ({
|
||||
npubOrPubkey,
|
||||
ipfsApi = IPFS_API,
|
||||
@@ -467,6 +485,7 @@ module.exports = {
|
||||
isPinned,
|
||||
pinCid,
|
||||
addCid,
|
||||
getCidSize,
|
||||
syncNostrPins,
|
||||
syncFollowPins,
|
||||
toNpub,
|
||||
|
||||
@@ -23,6 +23,14 @@ const {
|
||||
constants: { DEFAULT_RELAYS },
|
||||
} = require("./nostr");
|
||||
|
||||
const {
|
||||
getPins,
|
||||
getPinsByType,
|
||||
getStats,
|
||||
getTotalCount,
|
||||
getRecentPins,
|
||||
} = require("./database");
|
||||
|
||||
const unlinkAsync = promisify(fs.unlink);
|
||||
|
||||
// Health check endpoint
|
||||
@@ -265,9 +273,65 @@ const uploadHandler = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Pins history endpoint
|
||||
const pinsHandler = async (req, res) => {
|
||||
try {
|
||||
const limit = Math.min(parseInt(req.query.limit) || 50, 200);
|
||||
const offset = parseInt(req.query.offset) || 0;
|
||||
const type = req.query.type; // 'self', 'friend', or undefined for all
|
||||
|
||||
let pins;
|
||||
if (type && (type === 'self' || type === 'friend')) {
|
||||
pins = getPinsByType(type, limit, offset);
|
||||
} else {
|
||||
pins = getPins(limit, offset);
|
||||
}
|
||||
|
||||
const total = getTotalCount();
|
||||
const stats = getStats();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
pins: pins.map(pin => ({
|
||||
id: pin.id,
|
||||
eventId: pin.event_id,
|
||||
cid: pin.cid,
|
||||
size: pin.size,
|
||||
timestamp: pin.timestamp,
|
||||
author: pin.author,
|
||||
type: pin.type,
|
||||
status: pin.status,
|
||||
createdAt: pin.created_at,
|
||||
updatedAt: pin.updated_at,
|
||||
})),
|
||||
pagination: {
|
||||
limit,
|
||||
offset,
|
||||
total,
|
||||
hasMore: offset + limit < total,
|
||||
},
|
||||
stats: stats.reduce((acc, stat) => {
|
||||
const key = `${stat.type}_${stat.status}`;
|
||||
acc[key] = {
|
||||
count: stat.count,
|
||||
totalSize: stat.total_size,
|
||||
};
|
||||
return acc;
|
||||
}, {}),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Pins handler error:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
healthHandler,
|
||||
statusHandler,
|
||||
nostrHandler,
|
||||
uploadHandler,
|
||||
pinsHandler,
|
||||
};
|
||||
|
||||
Generated
+429
@@ -10,8 +10,10 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.4",
|
||||
"better-sqlite3": "^12.5.0",
|
||||
"compression": "^1.8.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^4.21.2",
|
||||
"form-data": "^4.0.2",
|
||||
"mime-types": "^3.0.2",
|
||||
@@ -170,6 +172,74 @@
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
"version": "12.5.0",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.5.0.tgz",
|
||||
"integrity": "sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"prebuild-install": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20.x || 22.x || 23.x || 24.x || 25.x"
|
||||
}
|
||||
},
|
||||
"node_modules/bindings": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bl/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.4",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||
@@ -223,6 +293,30 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
@@ -278,6 +372,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@@ -408,6 +508,30 @@
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-extend": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -436,6 +560,27 @@
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.2.3",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
|
||||
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -465,6 +610,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
@@ -525,6 +679,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/expand-template": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
||||
"license": "(MIT OR WTFPL)",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||
@@ -571,6 +734,12 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||
@@ -655,6 +824,12 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -701,6 +876,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/github-from-package": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@@ -780,12 +961,38 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@@ -883,6 +1090,18 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
@@ -904,6 +1123,12 @@
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
@@ -929,6 +1154,12 @@
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/napi-build-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
@@ -938,6 +1169,18 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
"version": "3.85.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz",
|
||||
"integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/nostr-tools": {
|
||||
"version": "2.19.4",
|
||||
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.19.4.tgz",
|
||||
@@ -1009,6 +1252,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
@@ -1024,6 +1276,32 @@
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
"expand-template": "^2.0.3",
|
||||
"github-from-package": "0.0.0",
|
||||
"minimist": "^1.2.3",
|
||||
"mkdirp-classic": "^0.5.3",
|
||||
"napi-build-utils": "^2.0.0",
|
||||
"node-abi": "^3.3.0",
|
||||
"pump": "^3.0.0",
|
||||
"rc": "^1.2.7",
|
||||
"simple-get": "^4.0.0",
|
||||
"tar-fs": "^2.0.0",
|
||||
"tunnel-agent": "^0.6.0"
|
||||
},
|
||||
"bin": {
|
||||
"prebuild-install": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
@@ -1049,6 +1327,16 @@
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
||||
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
@@ -1117,6 +1405,21 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/rc": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||
"dependencies": {
|
||||
"deep-extend": "^0.6.0",
|
||||
"ini": "~1.3.0",
|
||||
"minimist": "^1.2.0",
|
||||
"strip-json-comments": "~2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"rc": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
@@ -1164,6 +1467,18 @@
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
||||
@@ -1296,6 +1611,51 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-concat": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/simple-get": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
|
||||
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decompress-response": "^6.0.0",
|
||||
"once": "^1.3.1",
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
@@ -1328,6 +1688,57 @@
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
||||
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^2.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
"fs-constants": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
@@ -1337,6 +1748,18 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
@@ -1401,6 +1824,12 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
|
||||
+3
-1
@@ -19,8 +19,10 @@
|
||||
"homepage": "https://github.com/besoeasy/file-drop#readme",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.4",
|
||||
"better-sqlite3": "^12.5.0",
|
||||
"compression": "^1.8.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^4.21.2",
|
||||
"form-data": "^4.0.2",
|
||||
"mime-types": "^3.0.2",
|
||||
@@ -28,4 +30,4 @@
|
||||
"nostr-tools": "^2.9.0",
|
||||
"ws": "^8.18.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +219,93 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pins History Table -->
|
||||
<div class="mt-6">
|
||||
<div class="glassmorphism rounded-2xl shadow-xl p-6 animate-fade-in">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<i class="fas fa-history text-2xl text-black"></i>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-black">Pinned Content History</h2>
|
||||
<p class="text-gray-500 text-sm">Track all media pinned from Nostr events</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<select id="filterType" class="px-3 py-2 border border-gray-300 rounded-lg text-sm">
|
||||
<option value="">All Types</option>
|
||||
<option value="self">Self Only</option>
|
||||
<option value="friend">Friends Only</option>
|
||||
</select>
|
||||
<button id="refreshPins"
|
||||
class="px-4 py-2 rounded-lg bg-black text-white hover:bg-gray-800 transition-colors">
|
||||
<i class="fas fa-sync-alt mr-2"></i>Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Row -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<div class="bg-blue-50 rounded-lg p-4 border border-blue-200">
|
||||
<p class="text-blue-600 text-xs font-semibold uppercase mb-1">Total Pins</p>
|
||||
<p class="text-blue-900 text-2xl font-bold" id="statsTotal">0</p>
|
||||
</div>
|
||||
<div class="bg-green-50 rounded-lg p-4 border border-green-200">
|
||||
<p class="text-green-600 text-xs font-semibold uppercase mb-1">Self Pinned</p>
|
||||
<p class="text-green-900 text-2xl font-bold" id="statsSelfPinned">0</p>
|
||||
</div>
|
||||
<div class="bg-purple-50 rounded-lg p-4 border border-purple-200">
|
||||
<p class="text-purple-600 text-xs font-semibold uppercase mb-1">Friends Cached</p>
|
||||
<p class="text-purple-900 text-2xl font-bold" id="statsFriendsCached">0</p>
|
||||
</div>
|
||||
<div class="bg-orange-50 rounded-lg p-4 border border-orange-200">
|
||||
<p class="text-orange-600 text-xs font-semibold uppercase mb-1">Total Size</p>
|
||||
<p class="text-orange-900 text-xl font-bold" id="statsSize">0 B</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-100 border-b-2 border-gray-300">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-700 uppercase">Event ID</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-700 uppercase">CID</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-700 uppercase">Size</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-700 uppercase">Type</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-700 uppercase">Status</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-700 uppercase">Timestamp</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold text-gray-700 uppercase">Author</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="pinsTableBody" class="divide-y divide-gray-200">
|
||||
<tr>
|
||||
<td colspan="7" class="px-4 py-8 text-center text-gray-500">
|
||||
<i class="fas fa-spinner fa-spin mr-2"></i>Loading pins...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="flex items-center justify-between mt-4 pt-4 border-t border-gray-200">
|
||||
<div class="text-sm text-gray-600" id="paginationInfo">Showing 0 of 0</div>
|
||||
<div class="flex gap-2">
|
||||
<button id="prevPage"
|
||||
class="px-4 py-2 rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled>
|
||||
<i class="fas fa-chevron-left mr-2"></i>Previous
|
||||
</button>
|
||||
<button id="nextPage"
|
||||
class="px-4 py-2 rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled>
|
||||
Next<i class="fas fa-chevron-right ml-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -311,11 +398,149 @@
|
||||
return (bytes / Math.pow(1024, i)).toFixed(2) + " " + sizes[i];
|
||||
}
|
||||
|
||||
function formatTimestamp(timestamp) {
|
||||
const date = new Date(timestamp * 1000);
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
function truncate(str, len = 12) {
|
||||
if (!str) return '-';
|
||||
if (str.length <= len) return str;
|
||||
return str.substring(0, len) + '...';
|
||||
}
|
||||
|
||||
// Pins table management
|
||||
let currentPage = 0;
|
||||
const limit = 50;
|
||||
|
||||
async function loadPins() {
|
||||
const filterType = document.getElementById("filterType").value;
|
||||
const pinsTableBody = document.getElementById("pinsTableBody");
|
||||
|
||||
try {
|
||||
pinsTableBody.innerHTML = '<tr><td colspan="7" class="px-4 py-8 text-center text-gray-500"><i class="fas fa-spinner fa-spin mr-2"></i>Loading...</td></tr>';
|
||||
|
||||
let url = `/api/pins?limit=${limit}&offset=${currentPage * limit}`;
|
||||
if (filterType) {
|
||||
url += `&type=${filterType}`;
|
||||
}
|
||||
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'Failed to load pins');
|
||||
}
|
||||
|
||||
// Update stats
|
||||
const stats = data.stats || {};
|
||||
const selfPinned = (stats.self_pinned?.count || 0) + (stats.self_pending?.count || 0);
|
||||
const friendsCached = (stats.friend_cached?.count || 0) + (stats.friend_pending?.count || 0);
|
||||
const totalSize = Object.values(stats).reduce((sum, stat) => sum + (stat.totalSize || 0), 0);
|
||||
|
||||
document.getElementById("statsTotal").textContent = data.pagination.total;
|
||||
document.getElementById("statsSelfPinned").textContent = selfPinned;
|
||||
document.getElementById("statsFriendsCached").textContent = friendsCached;
|
||||
document.getElementById("statsSize").textContent = formatBytes(totalSize);
|
||||
|
||||
// Update table
|
||||
const pins = data.pins || [];
|
||||
if (pins.length === 0) {
|
||||
pinsTableBody.innerHTML = '<tr><td colspan="7" class="px-4 py-8 text-center text-gray-500">No pins found</td></tr>';
|
||||
} else {
|
||||
pinsTableBody.innerHTML = pins.map(pin => {
|
||||
const statusColor = {
|
||||
'pinned': 'bg-green-100 text-green-800',
|
||||
'cached': 'bg-blue-100 text-blue-800',
|
||||
'pending': 'bg-yellow-100 text-yellow-800',
|
||||
'failed': 'bg-red-100 text-red-800'
|
||||
}[pin.status] || 'bg-gray-100 text-gray-800';
|
||||
|
||||
const typeColor = pin.type === 'self' ? 'bg-purple-100 text-purple-800' : 'bg-indigo-100 text-indigo-800';
|
||||
|
||||
return `
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-3">
|
||||
<a href="https://primal.net/e/${pin.eventId}" target="_blank"
|
||||
class="text-blue-600 hover:underline font-mono text-xs"
|
||||
title="${pin.eventId}">
|
||||
${truncate(pin.eventId, 16)}
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<a href="https://ipfs.io/ipfs/${pin.cid}" target="_blank"
|
||||
class="text-blue-600 hover:underline font-mono text-xs"
|
||||
title="${pin.cid}">
|
||||
${truncate(pin.cid, 16)}
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-4 py-3 font-mono text-xs">${formatBytes(pin.size)}</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="px-2 py-1 rounded-full text-xs font-semibold ${typeColor}">
|
||||
${pin.type}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="px-2 py-1 rounded-full text-xs font-semibold ${statusColor}">
|
||||
${pin.status}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-xs text-gray-600">${formatTimestamp(pin.timestamp)}</td>
|
||||
<td class="px-4 py-3">
|
||||
<a href="https://nosta.me/${pin.author}" target="_blank"
|
||||
class="text-blue-600 hover:underline font-mono text-xs"
|
||||
title="${pin.author}">
|
||||
${truncate(pin.author, 12)}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Update pagination
|
||||
const start = currentPage * limit + 1;
|
||||
const end = Math.min((currentPage + 1) * limit, data.pagination.total);
|
||||
document.getElementById("paginationInfo").textContent =
|
||||
`Showing ${start}-${end} of ${data.pagination.total}`;
|
||||
|
||||
document.getElementById("prevPage").disabled = currentPage === 0;
|
||||
document.getElementById("nextPage").disabled = !data.pagination.hasMore;
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to load pins:', err);
|
||||
pinsTableBody.innerHTML = `<tr><td colspan="7" class="px-4 py-8 text-center text-red-500"><i class="fas fa-exclamation-circle mr-2"></i>Error: ${err.message}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("refresh").addEventListener("click", () => {
|
||||
load();
|
||||
});
|
||||
|
||||
document.getElementById("refreshPins").addEventListener("click", () => {
|
||||
currentPage = 0;
|
||||
loadPins();
|
||||
});
|
||||
|
||||
document.getElementById("filterType").addEventListener("change", () => {
|
||||
currentPage = 0;
|
||||
loadPins();
|
||||
});
|
||||
|
||||
document.getElementById("prevPage").addEventListener("click", () => {
|
||||
if (currentPage > 0) {
|
||||
currentPage--;
|
||||
loadPins();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("nextPage").addEventListener("click", () => {
|
||||
currentPage++;
|
||||
loadPins();
|
||||
});
|
||||
|
||||
load();
|
||||
loadPins();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user